mockaton 10.6.3 → 10.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -67,6 +67,7 @@ export type ClientMockBroker = {
67
67
  currentMock: {
68
68
  file: string
69
69
  delayed: boolean
70
+ proxied: boolean
70
71
  }
71
72
  }
72
73
  export type ClientBrokersByMethod = {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "10.6.3",
5
+ "version": "10.6.5",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -117,7 +117,7 @@ async function selectMock(req, response) {
117
117
  sendUnprocessableContent(response, `Missing Mock: ${file}`)
118
118
  else {
119
119
  broker.selectFile(file)
120
- sendOK(response)
120
+ sendJSON(response, broker.currentMock)
121
121
  }
122
122
  }
123
123
 
@@ -130,7 +130,7 @@ async function toggle500(req, response) {
130
130
  sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
131
131
  else {
132
132
  broker.toggle500()
133
- sendOK(response)
133
+ sendJSON(response, broker.currentMock)
134
134
  }
135
135
  }
136
136
 
@@ -176,8 +176,6 @@ async function updateProxyFallback(req, response) {
176
176
  sendUnprocessableContent(response, `Invalid Proxy Fallback URL`)
177
177
  return
178
178
  }
179
- if (!fallback)
180
- mockBrokersCollection.ensureAllRoutesHaveSelectedMock()
181
179
  config.proxyFallback = fallback
182
180
  sendOK(response)
183
181
  }
@@ -4,8 +4,13 @@ import { API, DF, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
4
4
  /** Client for controlling Mockaton via its HTTP API */
5
5
  export class Commander {
6
6
  #addr = ''
7
- constructor(addr) {
7
+ #then = a => a
8
+ #catch = e => { throw e }
9
+
10
+ constructor(addr, _then = undefined, _catch = undefined) {
8
11
  this.#addr = addr
12
+ if (_then) this.#then = _then
13
+ if (_catch) this.#catch = _catch
9
14
  }
10
15
 
11
16
  #patch(api, body) {
@@ -13,6 +18,8 @@ export class Commander {
13
18
  method: 'PATCH',
14
19
  body: JSON.stringify(body)
15
20
  })
21
+ .then(this.#then)
22
+ .catch(this.#catch)
16
23
  }
17
24
 
18
25
  /** @returns {JsonPromise<State>} */
@@ -42,7 +49,7 @@ export class Commander {
42
49
  toggle500(routeMethod, routeUrlMask) {
43
50
  return this.#patch(API.toggle500, {
44
51
  [DF.routeMethod]: routeMethod,
45
- [DF.routeUrlMask]: routeUrlMask,
52
+ [DF.routeUrlMask]: routeUrlMask
46
53
  })
47
54
  }
48
55
 
package/src/Dashboard.css CHANGED
@@ -579,8 +579,7 @@ table {
579
579
  border-radius: var(--radius);
580
580
 
581
581
  &:hover {
582
- border-color: var(--colorLightRed);
583
- background: var(--colorLightRed);
582
+ border-color: var(--colorRed);
584
583
  color: var(--colorRed);
585
584
  }
586
585
  }
package/src/Dashboard.js CHANGED
@@ -56,7 +56,9 @@ const FocusGroup = {
56
56
  PreviewLink: 3
57
57
  }
58
58
 
59
- const state = /** @type {State} */ {
59
+ const mockaton = new Commander(location.origin, parseError, onError)
60
+
61
+ const store = /** @type {State} */ {
60
62
  brokersByMethod: {},
61
63
  staticBrokers: {},
62
64
  cookies: [],
@@ -66,57 +68,145 @@ const state = /** @type {State} */ {
66
68
  collectProxied: false,
67
69
  proxyFallback: '',
68
70
  get canProxy() {
69
- return Boolean(state.proxyFallback)
70
- },
71
-
72
- fileFor(method, urlMask) {
73
- return state.brokersByMethod[method]?.[urlMask]?.currentMock.file
71
+ return Boolean(store.proxyFallback)
74
72
  },
75
73
 
76
74
  leftSideWidth: window.innerWidth / 2,
77
75
 
78
76
  groupByMethod: initPreference('groupByMethod'),
79
77
  toggleGroupByMethod() {
80
- state.groupByMethod = !state.groupByMethod
81
- togglePreference('groupByMethod', state.groupByMethod)
82
- updateState()
78
+ store.groupByMethod = !store.groupByMethod
79
+ togglePreference('groupByMethod', store.groupByMethod)
80
+ render()
83
81
  },
84
82
 
85
- chosenLink: { method: '', urlMask: '' },
83
+ chosenLink: {
84
+ method: '',
85
+ urlMask: ''
86
+ },
87
+ get hasChosenLink() {
88
+ return store.chosenLink.method
89
+ && store.chosenLink.urlMask
90
+ },
86
91
  setChosenLink(method, urlMask) {
87
- state.chosenLink = { method, urlMask }
92
+ store.chosenLink = { method, urlMask }
93
+ },
94
+
95
+ reset() {
96
+ store.setChosenLink('', '')
97
+ mockaton.reset()
98
+ .then(fetchState)
99
+ },
100
+
101
+ bulkSelectByComment(value) {
102
+ mockaton.bulkSelectByComment(value)
103
+ .then(fetchState)
104
+ },
105
+
106
+
107
+ setGlobalDelay(value) {
108
+ store.delay = value
109
+ mockaton.setGlobalDelay(value)
110
+ },
111
+
112
+ selectCookie(name) {
113
+ store.cookies = store.cookies.map(([n]) => [n, n === name])
114
+ mockaton.selectCookie(name)
115
+ },
116
+
117
+ setProxyFallback(value) {
118
+ store.proxyFallback = value
119
+ mockaton.setProxyFallback(value)
120
+ .then(render)
121
+ },
122
+
123
+ setCollectProxied(checked) {
124
+ store.collectProxied = checked
125
+ mockaton.setCollectProxied(checked)
126
+ },
127
+
128
+ brokerFor(method, urlMask) { return store.brokersByMethod[method]?.[urlMask] },
129
+ staticBrokerFor(route) { return store.staticBrokers[route] },
130
+
131
+ previewLink(method, urlMask) {
132
+ store.setChosenLink(method, urlMask)
133
+ renderRow(method, urlMask)
134
+ },
135
+
136
+ selectFile(file) {
137
+ mockaton.select(file).then(async response => {
138
+ const { method, urlMask } = parseFilename(file)
139
+ store.brokerFor(method, urlMask).currentMock = await response.json()
140
+ store.setChosenLink(method, urlMask)
141
+ renderRow(method, urlMask)
142
+ })
143
+ },
144
+
145
+ toggle500(method, urlMask) {
146
+ mockaton.toggle500(method, urlMask).then(async response => {
147
+ store.brokerFor(method, urlMask).currentMock = await response.json()
148
+ store.setChosenLink(method, urlMask)
149
+ renderRow(method, urlMask)
150
+ })
151
+ },
152
+
153
+ toggleProxied(method, urlMask, checked) {
154
+ mockaton.setRouteIsProxied(method, urlMask, checked).then(() => {
155
+ store.brokerFor(method, urlMask).currentMock.proxied = checked
156
+ store.setChosenLink(method, urlMask)
157
+ renderRow(method, urlMask)
158
+ })
159
+ },
160
+
161
+ setDelayed(method, urlMask, checked) {
162
+ mockaton.setRouteIsDelayed(method, urlMask, checked).then(() => {
163
+ store.brokerFor(method, urlMask).currentMock.delayed = checked
164
+ })
165
+ },
166
+
167
+
168
+ setDelayedStatic(route, checked) {
169
+ store.staticBrokerFor(route).delayed = checked
170
+ mockaton.setStaticRouteIsDelayed(route, checked)
171
+ },
172
+
173
+ setStaticRouteStatus(route, status) {
174
+ store.staticBrokerFor(route).status = status
175
+ mockaton.setStaticRouteStatus(route, status)
88
176
  }
89
177
  }
90
178
 
91
179
 
92
- const mockaton = new Commander(location.origin)
93
- updateState()
180
+ fetchState()
94
181
  initRealTimeUpdates()
95
182
  initKeyboardNavigation()
96
183
 
97
- async function updateState() {
184
+ async function fetchState() {
98
185
  try {
99
186
  const response = await mockaton.getState()
100
187
  if (!response.ok)
101
188
  throw response.status
102
-
103
- Object.assign(state, await response.json())
104
-
105
- const focusedElem = selectorFor(document.activeElement)
106
- document.body.replaceChildren(...App())
107
- if (focusedElem)
108
- document.querySelector(focusedElem)?.focus()
109
-
110
- const { method, urlMask } = state.chosenLink
111
- if (method && urlMask)
112
- await previewMock(method, urlMask)
113
-
189
+ Object.assign(store, await response.json())
190
+ render()
114
191
  }
115
192
  catch (error) {
116
193
  onError(error)
117
194
  }
118
195
  }
119
196
 
197
+ function render() {
198
+ restoreFocus(() => document.body.replaceChildren(...App()))
199
+ if (store.hasChosenLink)
200
+ previewMock(store.chosenLink.method, store.chosenLink.urlMask)
201
+ }
202
+
203
+ function restoreFocus(cb) {
204
+ const focusQuery = selectorFor(document.activeElement)
205
+ cb()
206
+ if (focusQuery)
207
+ document.querySelector(focusQuery)?.focus()
208
+ }
209
+
120
210
  const r = createElement
121
211
  const s = createSvgElement
122
212
  const t = translation => translation[0]
@@ -124,7 +214,7 @@ const t = translation => translation[0]
124
214
  const leftSideRef = useRef()
125
215
 
126
216
  function App() {
127
- const { leftSideWidth } = state
217
+ const { leftSideWidth } = store
128
218
  return [
129
219
  Header(),
130
220
  r('main', null,
@@ -161,84 +251,41 @@ function Header() {
161
251
  SettingsMenuTrigger())))
162
252
  }
163
253
 
164
- function SettingsMenuTrigger() {
165
- const id = '_settings_menu_'
166
- return (
167
- r('button', {
168
- title: t`Settings`,
169
- popovertarget: id,
170
- className: CSS.MenuTrigger
171
- },
172
- SettingsIcon(),
173
- Defer(() => SettingsMenu(id))))
174
- }
175
-
176
- function SettingsMenu(id) {
177
- const { groupByMethod, toggleGroupByMethod } = state
178
-
179
- const firstInputRef = useRef()
180
- function onToggle(event) {
181
- if (event.newState === 'open')
182
- firstInputRef.current.focus()
183
- }
184
- return (
185
- r('menu', {
186
- id,
187
- popover: '',
188
- className: CSS.SettingsMenu,
189
- onToggle
190
- },
191
-
192
- r('label', className(CSS.GroupByMethod),
193
- r('input', {
194
- ref: firstInputRef,
195
- type: 'checkbox',
196
- checked: groupByMethod,
197
- onChange: toggleGroupByMethod
198
- }),
199
- r('span', null, t`Group by Method`)),
200
-
201
- r('a', {
202
- href: 'https://github.com/ericfortis/mockaton',
203
- target: '_blank',
204
- rel: 'noopener noreferrer'
205
- }, t`Documentation`)))
206
- }
207
-
208
-
209
- function CookieSelector() {
210
- const { cookies } = state
254
+ function GlobalDelayField() {
211
255
  function onChange() {
212
- mockaton.selectCookie(this.value)
213
- .then(parseError)
214
- .catch(onError)
256
+ store.setGlobalDelay(this.valueAsNumber)
257
+ }
258
+ function onWheel(event) {
259
+ if (event.deltaY > 0)
260
+ this.stepUp()
261
+ else
262
+ this.stepDown()
263
+ clearTimeout(onWheel.timer)
264
+ onWheel.timer = setTimeout(onChange.bind(this), 300)
215
265
  }
216
- const disabled = cookies.length <= 1
217
- const list = cookies.length ? cookies : [[t`None`, true]]
218
266
  return (
219
- r('label', className(CSS.Field, CSS.CookieSelector),
220
- r('span', null, t`Cookie`),
221
- r('select', {
222
- autocomplete: 'off',
223
- disabled,
224
- title: disabled ? t`No cookies specified in config.cookies` : '',
225
- onChange
226
- }, list.map(([value, selected]) =>
227
- r('option', { value, selected }, value)))))
267
+ r('label', className(CSS.Field, CSS.GlobalDelayField),
268
+ r('span', null, t`Delay (ms)`),
269
+ r('input', {
270
+ type: 'number',
271
+ min: 0,
272
+ step: 100,
273
+ autocomplete: 'none',
274
+ value: store.delay,
275
+ onChange,
276
+ onWheel: [onWheel, { passive: true }]
277
+ })))
228
278
  }
229
279
 
230
280
  function BulkSelector() {
231
- const { comments } = state
281
+ const { comments } = store
232
282
  // UX wise this should be a menu instead of this `select`.
233
283
  // But this way is easier to implement, with a few hacks.
234
284
  const firstOption = t`Pick Comment…`
235
285
  function onChange() {
236
286
  const value = this.value
237
287
  this.value = firstOption // Hack
238
- mockaton.bulkSelectByComment(value)
239
- .then(parseError)
240
- .then(updateState)
241
- .catch(onError)
288
+ store.bulkSelectByComment(value)
242
289
  }
243
290
  const disabled = !comments.length
244
291
  return (
@@ -253,45 +300,29 @@ function BulkSelector() {
253
300
  },
254
301
  r('option', { value: firstOption }, firstOption),
255
302
  r('hr'),
256
- comments.map(value =>
257
- r('option', { value }, value)),
303
+ comments.map(value => r('option', { value }, value)),
258
304
  r('hr'),
259
305
  r('option', { value: AUTOGENERATED_500_COMMENT }, t`Auto500`)
260
306
  )))
261
307
  }
262
308
 
263
- function GlobalDelayField() {
264
- const { delay } = state
265
- function onChange() {
266
- state.delay = this.valueAsNumber
267
- mockaton.setGlobalDelay(state.delay)
268
- .then(parseError)
269
- .catch(onError)
270
- }
271
- function onWheel(event) {
272
- if (event.deltaY > 0)
273
- this.stepUp()
274
- else
275
- this.stepDown()
276
- clearTimeout(onWheel.timer)
277
- onWheel.timer = setTimeout(onChange.bind(this), 300)
278
- }
309
+ function CookieSelector() {
310
+ const { cookies } = store
311
+ const disabled = cookies.length <= 1
312
+ const list = cookies.length ? cookies : [[t`None`, true]]
279
313
  return (
280
- r('label', className(CSS.Field, CSS.GlobalDelayField),
281
- r('span', null, t`Delay (ms)`),
282
- r('input', {
283
- type: 'number',
284
- min: 0,
285
- step: 100,
286
- autocomplete: 'none',
287
- value: delay,
288
- onChange,
289
- onWheel: [onWheel, { passive: true }]
290
- })))
314
+ r('label', className(CSS.Field, CSS.CookieSelector),
315
+ r('span', null, t`Cookie`),
316
+ r('select', {
317
+ autocomplete: 'off',
318
+ disabled,
319
+ title: disabled ? t`No cookies specified in config.cookies` : '',
320
+ onChange() { store.selectCookie(this.value) }
321
+ }, list.map(([value, selected]) =>
322
+ r('option', { value, selected }, value)))))
291
323
  }
292
324
 
293
325
  function ProxyFallbackField() {
294
- const { proxyFallback } = state
295
326
  const checkboxRef = useRef()
296
327
  function onChange() {
297
328
  checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
@@ -299,10 +330,7 @@ function ProxyFallbackField() {
299
330
  if (!this.validity.valid)
300
331
  this.reportValidity()
301
332
  else
302
- mockaton.setProxyFallback(this.value.trim())
303
- .then(parseError)
304
- .then(updateState)
305
- .catch(onError)
333
+ store.setProxyFallback(this.value.trim())
306
334
  }
307
335
  return (
308
336
  r('div', className(CSS.Field, CSS.FallbackBackend),
@@ -312,92 +340,103 @@ function ProxyFallbackField() {
312
340
  type: 'url',
313
341
  autocomplete: 'none',
314
342
  placeholder: t`Type backend address`,
315
- value: proxyFallback,
343
+ value: store.proxyFallback,
316
344
  onChange
317
345
  })),
318
346
  SaveProxiedCheckbox(checkboxRef)))
319
347
  }
320
348
 
321
349
  function SaveProxiedCheckbox(ref) {
322
- const { collectProxied, canProxy } = state
323
- function onChange() {
324
- mockaton.setCollectProxied(this.checked)
325
- .then(parseError)
326
- .catch(onError)
327
- }
328
350
  return (
329
351
  r('label', className(CSS.SaveProxiedCheckbox),
330
352
  r('input', {
331
353
  ref,
332
354
  type: 'checkbox',
333
- disabled: !canProxy,
334
- checked: collectProxied,
335
- onChange
355
+ disabled: !store.canProxy,
356
+ checked: store.collectProxied,
357
+ onChange() { store.setCollectProxied(this.checked) }
336
358
  }),
337
359
  r('span', null, t`Save Mocks`)))
338
360
  }
339
361
 
340
362
  function ResetButton() {
341
- function onClick() {
342
- state.setChosenLink('', '')
343
- mockaton.reset()
344
- .then(parseError)
345
- .then(updateState)
346
- .catch(onError)
347
- }
348
363
  return (
349
364
  r('button', {
350
365
  className: CSS.ResetButton,
351
- onClick
366
+ onClick: store.reset
352
367
  }, t`Reset`))
353
368
  }
354
369
 
370
+ function SettingsMenuTrigger() {
371
+ const id = '_settings_menu_'
372
+ return (
373
+ r('button', {
374
+ title: t`Settings`,
375
+ popovertarget: id,
376
+ className: CSS.MenuTrigger
377
+ },
378
+ SettingsIcon(),
379
+ Defer(() => SettingsMenu(id))))
380
+ }
381
+
382
+ function SettingsMenu(id) {
383
+ const firstInputRef = useRef()
384
+ function onToggle(event) {
385
+ if (event.newState === 'open')
386
+ firstInputRef.current.focus()
387
+ }
388
+ return (
389
+ r('menu', {
390
+ id,
391
+ popover: '',
392
+ className: CSS.SettingsMenu,
393
+ onToggle
394
+ },
395
+
396
+ r('label', className(CSS.GroupByMethod),
397
+ r('input', {
398
+ ref: firstInputRef,
399
+ type: 'checkbox',
400
+ checked: store.groupByMethod,
401
+ onChange: store.toggleGroupByMethod
402
+ }),
403
+ r('span', null, t`Group by Method`)),
404
+
405
+ r('a', {
406
+ href: 'https://github.com/ericfortis/mockaton',
407
+ target: '_blank',
408
+ rel: 'noopener noreferrer'
409
+ }, t`Documentation`)))
410
+ }
411
+
355
412
 
356
413
 
357
414
  /** # MockList */
358
415
 
359
416
  function MockList() {
360
- const { brokersByMethod, groupByMethod, canProxy } = state
361
-
362
- if (!Object.keys(brokersByMethod).length)
417
+ if (!Object.keys(store.brokersByMethod).length)
363
418
  return (
364
419
  r('div', className(CSS.empty),
365
420
  t`No mocks found`))
366
421
 
367
- if (groupByMethod)
368
- return Object.keys(brokersByMethod).map(method => Fragment(
422
+ if (store.groupByMethod)
423
+ return Object.keys(store.brokersByMethod).map(method => Fragment(
369
424
  r('tr', null,
370
- r('th', { colspan: 2 + Number(canProxy) }),
425
+ r('th', { colspan: 2 + Number(store.canProxy) }),
371
426
  r('th', null, method)),
372
427
  rowsFor(method).map(Row)))
373
428
 
374
429
  return rowsFor('*').map(Row)
375
430
  }
376
431
 
377
-
378
- function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
379
- const { canProxy, groupByMethod } = state
380
- return (
381
- r('tr', { key: method + '::' + urlMask },
382
- canProxy && r('td', null, ProxyToggler(broker)),
383
- r('td', null, DelayRouteToggler(broker)),
384
- r('td', null, InternalServerErrorToggler(broker)),
385
- !groupByMethod && r('td', className(CSS.Method), method),
386
- r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
387
- r('td', null, MockSelector(broker))))
388
- }
389
-
390
432
  function rowsFor(targetMethod) {
391
- const { brokersByMethod } = state
392
-
393
433
  const rows = []
394
- for (const [method, brokers] of Object.entries(brokersByMethod))
434
+ for (const [method, brokers] of Object.entries(store.brokersByMethod))
395
435
  if (targetMethod === '*' || targetMethod === method)
396
436
  for (const [urlMask, broker] of Object.entries(brokers))
397
437
  rows.push({ method, urlMask, broker })
398
438
 
399
439
  const sorted = rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
400
-
401
440
  const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
402
441
  return sorted.map((r, i) => ({
403
442
  ...r,
@@ -405,22 +444,50 @@ function rowsFor(targetMethod) {
405
444
  }))
406
445
  }
407
446
 
447
+ function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
448
+ const key = Row.key(method, urlMask)
449
+ Row.ditto.set(key, urlMaskDittoed)
450
+ return (
451
+ r('tr', { key },
452
+ store.canProxy && r('td', null, ProxyToggler(broker)),
453
+ r('td', null, DelayRouteToggler(broker)),
454
+ r('td', null, InternalServerErrorToggler(broker)),
455
+ !store.groupByMethod && r('td', className(CSS.Method), method),
456
+ r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
457
+ r('td', null, MockSelector(broker))))
458
+ }
459
+ Row.key = (method, urlMask) => method + '::' + urlMask
460
+ Row.ditto = new Map()
461
+
462
+ function renderRow(method, urlMask) {
463
+ restoreFocus(() => {
464
+ unChooseOld()
465
+ const key = Row.key(method, urlMask)
466
+ trFor(key).replaceWith(Row({
467
+ method,
468
+ urlMask,
469
+ urlMaskDittoed: Row.ditto.get(key),
470
+ broker: store.brokerFor(method, urlMask)
471
+ }))
472
+ previewMock(method, urlMask)
473
+ })
474
+
475
+ function trFor(key) {
476
+ return document.querySelector(`body > main > .${CSS.leftSide} tr[key="${key}"]`)
477
+ }
478
+ function unChooseOld() {
479
+ return document.querySelector(`body > main > .${CSS.leftSide} tr .${CSS.chosen}`)
480
+ ?.classList.remove(CSS.chosen)
481
+ }
482
+ }
483
+
408
484
 
409
485
  function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
410
- async function onClick(event) {
486
+ function onClick(event) {
411
487
  event.preventDefault()
412
- try {
413
- findChosenLink()?.classList.remove(CSS.chosen)
414
- this.classList.add(CSS.chosen)
415
- state.setChosenLink(method, urlMask)
416
- await previewMock(method, urlMask)
417
- }
418
- catch (error) {
419
- onError(error)
420
- }
488
+ store.previewLink(method, urlMask)
421
489
  }
422
- const { chosenLink } = state
423
- const isChosen = chosenLink.method === method && chosenLink.urlMask === urlMask
490
+ const isChosen = store.chosenLink.method === method && store.chosenLink.urlMask === urlMask
424
491
  const [ditto, tail] = urlMaskDittoed
425
492
  return (
426
493
  r('a', {
@@ -434,30 +501,18 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
434
501
  : tail))
435
502
  }
436
503
 
437
- function findChosenLink() {
438
- return document.querySelector(
439
- `body > main > .${CSS.leftSide} .${CSS.PreviewLink}.${CSS.chosen}`)
440
- }
441
-
442
- const STR_PROXIED = t`Proxied`
443
504
 
444
505
  /** @param {ClientMockBroker} broker */
445
506
  function MockSelector(broker) {
446
- function onChange() {
447
- const { method, urlMask } = parseFilename(this.value)
448
- state.setChosenLink(method, urlMask)
449
- mockaton.select(this.value)
450
- .then(parseError)
451
- .then(updateState)
452
- .catch(onError)
453
- }
454
-
507
+ const STR_PROXIED = t`Proxied`
508
+
455
509
  let selected = broker.currentMock.file
456
510
  const { status } = parseFilename(selected)
457
511
  const files = broker.mocks.filter(item =>
458
512
  status === 500 ||
459
513
  !item.includes(AUTOGENERATED_500_COMMENT))
460
- if (!selected) {
514
+
515
+ if (store.canProxy && broker.currentMock.proxied) {
461
516
  selected = STR_PROXIED
462
517
  files.push(selected)
463
518
  }
@@ -477,7 +532,7 @@ function MockSelector(broker) {
477
532
 
478
533
  return (
479
534
  r('select', {
480
- onChange,
535
+ onChange() { store.selectFile(this.value) },
481
536
  autocomplete: 'off',
482
537
  'aria-label': t`Mock Selector`,
483
538
  disabled: files.length <= 1,
@@ -496,9 +551,7 @@ function MockSelector(broker) {
496
551
  function DelayRouteToggler(broker) {
497
552
  function commit(checked) {
498
553
  const { method, urlMask } = parseFilename(broker.mocks[0])
499
- mockaton.setRouteIsDelayed(method, urlMask, checked)
500
- .then(parseError)
501
- .catch(onError)
554
+ store.setDelayed(method, urlMask, checked)
502
555
  }
503
556
  return ClickDragToggler({
504
557
  checked: broker.currentMock.delayed,
@@ -512,11 +565,7 @@ function DelayRouteToggler(broker) {
512
565
  function InternalServerErrorToggler(broker) {
513
566
  function onChange() {
514
567
  const { method, urlMask } = parseFilename(broker.mocks[0])
515
- state.setChosenLink(method, urlMask)
516
- mockaton.toggle500(method, urlMask)
517
- .then(parseError)
518
- .then(updateState)
519
- .catch(onError)
568
+ store.toggle500(method, urlMask)
520
569
  }
521
570
  return (
522
571
  r('label', {
@@ -526,7 +575,6 @@ function InternalServerErrorToggler(broker) {
526
575
  r('input', {
527
576
  type: 'checkbox',
528
577
  'data-focus-group': FocusGroup.StatusToggler,
529
- name: broker.currentMock.file,
530
578
  checked: parseFilename(broker.currentMock.file).status === 500,
531
579
  onChange
532
580
  }),
@@ -537,11 +585,7 @@ function InternalServerErrorToggler(broker) {
537
585
  function ProxyToggler(broker) {
538
586
  function onChange() {
539
587
  const { urlMask, method } = parseFilename(broker.mocks[0])
540
- state.setChosenLink(method, urlMask)
541
- mockaton.setRouteIsProxied(method, urlMask, this.checked)
542
- .then(parseError)
543
- .then(updateState)
544
- .catch(onError)
588
+ store.toggleProxied(method, urlMask, this.checked)
545
589
  }
546
590
  return (
547
591
  r('label', {
@@ -550,7 +594,7 @@ function ProxyToggler(broker) {
550
594
  },
551
595
  r('input', {
552
596
  type: 'checkbox',
553
- checked: !broker.currentMock.file,
597
+ checked: broker.currentMock.proxied,
554
598
  onChange,
555
599
  'data-focus-group': FocusGroup.ProxyToggler
556
600
  }),
@@ -562,7 +606,7 @@ function ProxyToggler(broker) {
562
606
  /** # StaticFilesList */
563
607
 
564
608
  function StaticFilesList() {
565
- const { staticBrokers, canProxy, groupByMethod } = state
609
+ const { staticBrokers, canProxy, groupByMethod } = store
566
610
  if (!Object.keys(staticBrokers).length)
567
611
  return null
568
612
  const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
@@ -590,25 +634,17 @@ function StaticFilesList() {
590
634
 
591
635
  /** @param {ClientStaticBroker} broker */
592
636
  function DelayStaticRouteToggler(broker) {
593
- function commit(checked) {
594
- mockaton.setStaticRouteIsDelayed(broker.route, checked)
595
- .then(parseError)
596
- .catch(onError)
597
- }
598
637
  return ClickDragToggler({
599
638
  checked: broker.delayed,
600
- commit,
601
- focusGroup: FocusGroup.DelayToggler
639
+ focusGroup: FocusGroup.DelayToggler,
640
+ commit(checked) {
641
+ store.setDelayedStatic(broker.route, checked)
642
+ }
602
643
  })
603
644
  }
604
645
 
605
646
  /** @param {ClientStaticBroker} broker */
606
647
  function NotFoundToggler(broker) {
607
- function onChange() {
608
- mockaton.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
609
- .then(parseError)
610
- .catch(onError)
611
- }
612
648
  return (
613
649
  r('label', {
614
650
  className: CSS.NotFoundToggler,
@@ -617,8 +653,10 @@ function NotFoundToggler(broker) {
617
653
  r('input', {
618
654
  type: 'checkbox',
619
655
  checked: broker.status === 404,
620
- onChange,
621
- 'data-focus-group': FocusGroup.StatusToggler
656
+ 'data-focus-group': FocusGroup.StatusToggler,
657
+ onChange() {
658
+ store.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
659
+ }
622
660
  }),
623
661
  r('span', null, t`404`)))
624
662
  }
@@ -658,7 +696,6 @@ function ClickDragToggler({ checked, commit, focusGroup }) {
658
696
  TimerIcon()))
659
697
  }
660
698
 
661
-
662
699
  function Resizer() {
663
700
  let raf = 0
664
701
  let initialX = 0
@@ -679,8 +716,8 @@ function Resizer() {
679
716
  function onMove(event) {
680
717
  const MIN_LEFT_WIDTH = 380
681
718
  raf = raf || requestAnimationFrame(() => {
682
- state.leftSideWidth = Math.max(panelWidth - (initialX - event.clientX), MIN_LEFT_WIDTH)
683
- leftSideRef.current.style.width = state.leftSideWidth + 'px'
719
+ store.leftSideWidth = Math.max(panelWidth - (initialX - event.clientX), MIN_LEFT_WIDTH)
720
+ leftSideRef.current.style.width = store.leftSideWidth + 'px'
684
721
  raf = 0
685
722
  })
686
723
  }
@@ -712,9 +749,11 @@ const payloadViewerCodeRef = useRef()
712
749
  function PayloadViewer() {
713
750
  return (
714
751
  r('div', className(CSS.PayloadViewer),
715
- r('h2', { ref: payloadViewerTitleRef }, t`Preview`),
752
+ r('h2', { ref: payloadViewerTitleRef },
753
+ !store.hasChosenLink && t`Preview`),
716
754
  r('pre', null,
717
- r('code', { ref: payloadViewerCodeRef }, t`Click a link to preview it`))))
755
+ r('code', { ref: payloadViewerCodeRef },
756
+ !store.hasChosenLink && t`Click a link to preview it`))))
718
757
  }
719
758
 
720
759
  function PayloadViewerTitle({ file, statusText }) {
@@ -741,7 +780,7 @@ const SPINNER_DELAY = 80
741
780
  function PayloadViewerProgressBar() {
742
781
  return (
743
782
  r('div', className(CSS.ProgressBar),
744
- r('div', { style: { animationDuration: state.delay - SPINNER_DELAY + 'ms' } })))
783
+ r('div', { style: { animationDuration: store.delay - SPINNER_DELAY + 'ms' } })))
745
784
  }
746
785
 
747
786
  async function previewMock(method, urlMask) {
@@ -759,11 +798,11 @@ async function previewMock(method, urlMask) {
759
798
  signal: previewMock.controller.signal
760
799
  })
761
800
  clearTimeout(spinnerTimer)
762
- const file = state.fileFor(method, urlMask)
763
- if (file === '')
764
- await updatePayloadViewer(STR_PROXIED, response)
801
+ const { proxied, file } = store.brokerFor(method, urlMask).currentMock
802
+ if (proxied)
803
+ await updatePayloadViewer(true, '', response)
765
804
  else if (file)
766
- await updatePayloadViewer(file, response)
805
+ await updatePayloadViewer(false, file, response)
767
806
  else {/* e.g. selected was deleted */}
768
807
  }
769
808
  catch (err) {
@@ -772,11 +811,11 @@ async function previewMock(method, urlMask) {
772
811
  }
773
812
  }
774
813
 
775
- async function updatePayloadViewer(file, response) {
814
+ async function updatePayloadViewer(proxied, file, response) {
776
815
  const mime = response.headers.get('content-type') || ''
777
816
 
778
817
  payloadViewerTitleRef.current.replaceChildren(
779
- file === STR_PROXIED
818
+ proxied
780
819
  ? PayloadViewerTitleWhenProxied({
781
820
  mime,
782
821
  status: response.status,
@@ -790,9 +829,7 @@ async function updatePayloadViewer(file, response) {
790
829
 
791
830
  if (mime.startsWith('image/')) // Naively assumes GET.200
792
831
  payloadViewerCodeRef.current.replaceChildren(
793
- r('img', {
794
- src: URL.createObjectURL(await response.blob())
795
- }))
832
+ r('img', { src: URL.createObjectURL(await response.blob()) }))
796
833
  else {
797
834
  const body = await response.text() || t`/* Empty Response Body */`
798
835
  if (mime === 'application/json')
@@ -810,55 +847,11 @@ function isXML(mime) {
810
847
  }
811
848
 
812
849
 
813
- function initKeyboardNavigation() {
814
- addEventListener('keydown', onKeyDown)
815
-
816
- function onKeyDown(event) {
817
- const pivot = document.activeElement
818
- switch (event.key) {
819
- case 'ArrowDown':
820
- case 'ArrowUp': {
821
- let fg = pivot.getAttribute('data-focus-group')
822
- if (fg !== null) {
823
- const offset = event.key === 'ArrowDown' ? +1 : -1
824
- circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
825
- }
826
- break
827
- }
828
- case 'ArrowRight':
829
- case 'ArrowLeft': {
830
- if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
831
- const offset = event.key === 'ArrowRight' ? +1 : -1
832
- rowFocusable(pivot, offset).focus()
833
- }
834
- break
835
- }
836
- }
837
- }
838
-
839
- function rowFocusable(el, step) {
840
- const row = el.closest('tr')
841
- if (row) {
842
- const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
843
- return circularAdjacent(step, focusables, el)
844
- }
845
- }
846
-
847
- function allInFocusGroup(focusGroup) {
848
- return Array.from(document.querySelectorAll(
849
- `body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
850
- }
851
-
852
- function circularAdjacent(step = 1, arr, pivot) {
853
- return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
854
- }
855
- }
856
-
857
850
  /** # Error */
858
851
 
859
852
  async function parseError(response) {
860
853
  if (response.ok)
861
- return
854
+ return response
862
855
  if (response.status === 422)
863
856
  throw await response.text()
864
857
  throw response.statusText
@@ -935,7 +928,7 @@ function initRealTimeUpdates() {
935
928
  if (oldSyncVersion !== syncVersion) { // because it could be < or >
936
929
  oldSyncVersion = syncVersion
937
930
  if (!skipUpdate)
938
- await updateState()
931
+ await fetchState()
939
932
  }
940
933
  poll()
941
934
  }
@@ -949,6 +942,50 @@ function initRealTimeUpdates() {
949
942
  }
950
943
  }
951
944
 
945
+ function initKeyboardNavigation() {
946
+ addEventListener('keydown', onKeyDown)
947
+
948
+ function onKeyDown(event) {
949
+ const pivot = document.activeElement
950
+ switch (event.key) {
951
+ case 'ArrowDown':
952
+ case 'ArrowUp': {
953
+ let fg = pivot.getAttribute('data-focus-group')
954
+ if (fg !== null) {
955
+ const offset = event.key === 'ArrowDown' ? +1 : -1
956
+ circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
957
+ }
958
+ break
959
+ }
960
+ case 'ArrowRight':
961
+ case 'ArrowLeft': {
962
+ if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
963
+ const offset = event.key === 'ArrowRight' ? +1 : -1
964
+ rowFocusable(pivot, offset).focus()
965
+ }
966
+ break
967
+ }
968
+ }
969
+ }
970
+
971
+ function rowFocusable(el, step) {
972
+ const row = el.closest('tr')
973
+ if (row) {
974
+ const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
975
+ return circularAdjacent(step, focusables, el)
976
+ }
977
+ }
978
+
979
+ function allInFocusGroup(focusGroup) {
980
+ return Array.from(document.querySelectorAll(
981
+ `body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
982
+ }
983
+
984
+ function circularAdjacent(step = 1, arr, pivot) {
985
+ return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
986
+ }
987
+ }
988
+
952
989
 
953
990
  /** # Utils */
954
991
 
@@ -1115,7 +1152,6 @@ dittoSplitPaths.test = function () {
1115
1152
  }
1116
1153
 
1117
1154
 
1118
-
1119
1155
  function syntaxJSON(json) {
1120
1156
  const MAX_NODES = 50_000
1121
1157
  let nNodes = 0
@@ -1163,7 +1199,6 @@ syntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s
1163
1199
  // Capture group order: [string, optional colon, punc]
1164
1200
 
1165
1201
 
1166
-
1167
1202
  function syntaxXML(xml) {
1168
1203
  const MAX_NODES = 50_000
1169
1204
  let nNodes = 0
package/src/MockBroker.js CHANGED
@@ -10,15 +10,17 @@ export class MockBroker {
10
10
  this.mocks = []
11
11
  this.currentMock = {
12
12
  file: '',
13
- delayed: false
13
+ delayed: false,
14
+ proxied: false
14
15
  }
15
16
  this.register(file)
16
17
  }
17
18
 
18
19
  get file() { return this.currentMock.file }
19
- get status() { return parseFilename(this.file).status }
20
20
  get delayed() { return this.currentMock.delayed }
21
- get proxied() { return !this.currentMock.file }
21
+ get proxied() { return this.currentMock.proxied }
22
+
23
+ get status() { return parseFilename(this.file).status }
22
24
  get temp500IsSelected() { return this.#isTemp500(this.file) }
23
25
 
24
26
  hasMock(file) { return this.mocks.includes(file) }
@@ -80,25 +82,18 @@ export class MockBroker {
80
82
  }
81
83
 
82
84
  selectFile(filename) {
85
+ this.currentMock.proxied = false
83
86
  this.currentMock.file = filename
84
87
  }
85
-
88
+
86
89
  toggle500() {
87
90
  this.#is500(this.currentMock.file)
88
91
  ? this.selectDefaultFile()
89
92
  : this.selectFile(this.mocks.find(this.#is500))
90
93
  }
91
94
 
92
- setDelayed(delayed) {
93
- this.currentMock.delayed = delayed
94
- }
95
-
96
- setProxied(proxied) {
97
- if (proxied)
98
- this.selectFile('')
99
- else
100
- this.selectDefaultFile()
101
- }
95
+ setDelayed(delayed) { this.currentMock.delayed = delayed }
96
+ setProxied(proxied) { this.currentMock.proxied = proxied }
102
97
 
103
98
  setByMatchingComment(comment) {
104
99
  for (const file of this.mocks)
@@ -13,15 +13,19 @@ import { sendInternalServerError, sendMockNotFound } from './utils/http-response
13
13
 
14
14
  export async function dispatchMock(req, response) {
15
15
  try {
16
- let broker = mockBrokerCollection.brokerByRoute(req.method, req.url)
17
16
  const isHead = req.method === 'HEAD'
17
+
18
+ let broker = mockBrokerCollection.brokerByRoute(req.method, req.url)
18
19
  if (!broker && isHead)
19
20
  broker = mockBrokerCollection.brokerByRoute('GET', req.url)
20
- if (!broker || broker.proxied) {
21
- if (config.proxyFallback)
22
- await proxy(req, response, broker?.delayed ? calcDelay() : 0)
23
- else
24
- sendMockNotFound(response)
21
+
22
+ if (config.proxyFallback && (!broker || broker.proxied)) {
23
+ await proxy(req, response, broker?.delayed ? calcDelay() : 0)
24
+ return
25
+ }
26
+
27
+ if (!broker) {
28
+ sendMockNotFound(response)
25
29
  return
26
30
  }
27
31
 
@@ -125,13 +125,6 @@ export function setMocksMatchingComment(comment) {
125
125
  broker.setByMatchingComment(comment))
126
126
  }
127
127
 
128
- export function ensureAllRoutesHaveSelectedMock() {
129
- forEachBroker(broker => {
130
- if (broker.proxied)
131
- broker.selectDefaultFile()
132
- })
133
- }
134
-
135
128
  function forEachBroker(fn) {
136
129
  for (const brokers of Object.values(collection))
137
130
  Object.values(brokers).forEach(fn)