mockaton 10.6.4 → 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.4",
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,58 +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: '' },
86
- setChosenLink(method, urlMask) {
87
- state.chosenLink = { method, urlMask }
83
+ chosenLink: {
84
+ method: '',
85
+ urlMask: ''
88
86
  },
89
87
  get hasChosenLink() {
90
- return state.chosenLink.method && state.chosenLink.urlMask
88
+ return store.chosenLink.method
89
+ && store.chosenLink.urlMask
90
+ },
91
+ setChosenLink(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)
91
176
  }
92
177
  }
93
178
 
94
179
 
95
- const mockaton = new Commander(location.origin)
96
- updateState()
180
+ fetchState()
97
181
  initRealTimeUpdates()
98
182
  initKeyboardNavigation()
99
183
 
100
- async function updateState() {
184
+ async function fetchState() {
101
185
  try {
102
186
  const response = await mockaton.getState()
103
187
  if (!response.ok)
104
188
  throw response.status
105
-
106
- Object.assign(state, await response.json())
107
-
108
- const focusedElem = selectorFor(document.activeElement)
109
- document.body.replaceChildren(...App())
110
- if (focusedElem)
111
- document.querySelector(focusedElem)?.focus()
112
-
113
- if (state.hasChosenLink)
114
- await previewMock(state.chosenLink.method, state.chosenLink.urlMask)
189
+ Object.assign(store, await response.json())
190
+ render()
115
191
  }
116
192
  catch (error) {
117
193
  onError(error)
118
194
  }
119
195
  }
120
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
+
121
210
  const r = createElement
122
211
  const s = createSvgElement
123
212
  const t = translation => translation[0]
@@ -125,7 +214,7 @@ const t = translation => translation[0]
125
214
  const leftSideRef = useRef()
126
215
 
127
216
  function App() {
128
- const { leftSideWidth } = state
217
+ const { leftSideWidth } = store
129
218
  return [
130
219
  Header(),
131
220
  r('main', null,
@@ -162,84 +251,41 @@ function Header() {
162
251
  SettingsMenuTrigger())))
163
252
  }
164
253
 
165
- function SettingsMenuTrigger() {
166
- const id = '_settings_menu_'
167
- return (
168
- r('button', {
169
- title: t`Settings`,
170
- popovertarget: id,
171
- className: CSS.MenuTrigger
172
- },
173
- SettingsIcon(),
174
- Defer(() => SettingsMenu(id))))
175
- }
176
-
177
- function SettingsMenu(id) {
178
- const { groupByMethod, toggleGroupByMethod } = state
179
-
180
- const firstInputRef = useRef()
181
- function onToggle(event) {
182
- if (event.newState === 'open')
183
- firstInputRef.current.focus()
184
- }
185
- return (
186
- r('menu', {
187
- id,
188
- popover: '',
189
- className: CSS.SettingsMenu,
190
- onToggle
191
- },
192
-
193
- r('label', className(CSS.GroupByMethod),
194
- r('input', {
195
- ref: firstInputRef,
196
- type: 'checkbox',
197
- checked: groupByMethod,
198
- onChange: toggleGroupByMethod
199
- }),
200
- r('span', null, t`Group by Method`)),
201
-
202
- r('a', {
203
- href: 'https://github.com/ericfortis/mockaton',
204
- target: '_blank',
205
- rel: 'noopener noreferrer'
206
- }, t`Documentation`)))
207
- }
208
-
209
-
210
- function CookieSelector() {
211
- const { cookies } = state
254
+ function GlobalDelayField() {
212
255
  function onChange() {
213
- mockaton.selectCookie(this.value)
214
- .then(parseError)
215
- .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)
216
265
  }
217
- const disabled = cookies.length <= 1
218
- const list = cookies.length ? cookies : [[t`None`, true]]
219
266
  return (
220
- r('label', className(CSS.Field, CSS.CookieSelector),
221
- r('span', null, t`Cookie`),
222
- r('select', {
223
- autocomplete: 'off',
224
- disabled,
225
- title: disabled ? t`No cookies specified in config.cookies` : '',
226
- onChange
227
- }, list.map(([value, selected]) =>
228
- 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
+ })))
229
278
  }
230
279
 
231
280
  function BulkSelector() {
232
- const { comments } = state
281
+ const { comments } = store
233
282
  // UX wise this should be a menu instead of this `select`.
234
283
  // But this way is easier to implement, with a few hacks.
235
284
  const firstOption = t`Pick Comment…`
236
285
  function onChange() {
237
286
  const value = this.value
238
287
  this.value = firstOption // Hack
239
- mockaton.bulkSelectByComment(value)
240
- .then(parseError)
241
- .then(updateState)
242
- .catch(onError)
288
+ store.bulkSelectByComment(value)
243
289
  }
244
290
  const disabled = !comments.length
245
291
  return (
@@ -254,45 +300,29 @@ function BulkSelector() {
254
300
  },
255
301
  r('option', { value: firstOption }, firstOption),
256
302
  r('hr'),
257
- comments.map(value =>
258
- r('option', { value }, value)),
303
+ comments.map(value => r('option', { value }, value)),
259
304
  r('hr'),
260
305
  r('option', { value: AUTOGENERATED_500_COMMENT }, t`Auto500`)
261
306
  )))
262
307
  }
263
308
 
264
- function GlobalDelayField() {
265
- const { delay } = state
266
- function onChange() {
267
- state.delay = this.valueAsNumber
268
- mockaton.setGlobalDelay(state.delay)
269
- .then(parseError)
270
- .catch(onError)
271
- }
272
- function onWheel(event) {
273
- if (event.deltaY > 0)
274
- this.stepUp()
275
- else
276
- this.stepDown()
277
- clearTimeout(onWheel.timer)
278
- onWheel.timer = setTimeout(onChange.bind(this), 300)
279
- }
309
+ function CookieSelector() {
310
+ const { cookies } = store
311
+ const disabled = cookies.length <= 1
312
+ const list = cookies.length ? cookies : [[t`None`, true]]
280
313
  return (
281
- r('label', className(CSS.Field, CSS.GlobalDelayField),
282
- r('span', null, t`Delay (ms)`),
283
- r('input', {
284
- type: 'number',
285
- min: 0,
286
- step: 100,
287
- autocomplete: 'none',
288
- value: delay,
289
- onChange,
290
- onWheel: [onWheel, { passive: true }]
291
- })))
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)))))
292
323
  }
293
324
 
294
325
  function ProxyFallbackField() {
295
- const { proxyFallback } = state
296
326
  const checkboxRef = useRef()
297
327
  function onChange() {
298
328
  checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
@@ -300,10 +330,7 @@ function ProxyFallbackField() {
300
330
  if (!this.validity.valid)
301
331
  this.reportValidity()
302
332
  else
303
- mockaton.setProxyFallback(this.value.trim())
304
- .then(parseError)
305
- .then(updateState)
306
- .catch(onError)
333
+ store.setProxyFallback(this.value.trim())
307
334
  }
308
335
  return (
309
336
  r('div', className(CSS.Field, CSS.FallbackBackend),
@@ -313,92 +340,103 @@ function ProxyFallbackField() {
313
340
  type: 'url',
314
341
  autocomplete: 'none',
315
342
  placeholder: t`Type backend address`,
316
- value: proxyFallback,
343
+ value: store.proxyFallback,
317
344
  onChange
318
345
  })),
319
346
  SaveProxiedCheckbox(checkboxRef)))
320
347
  }
321
348
 
322
349
  function SaveProxiedCheckbox(ref) {
323
- const { collectProxied, canProxy } = state
324
- function onChange() {
325
- mockaton.setCollectProxied(this.checked)
326
- .then(parseError)
327
- .catch(onError)
328
- }
329
350
  return (
330
351
  r('label', className(CSS.SaveProxiedCheckbox),
331
352
  r('input', {
332
353
  ref,
333
354
  type: 'checkbox',
334
- disabled: !canProxy,
335
- checked: collectProxied,
336
- onChange
355
+ disabled: !store.canProxy,
356
+ checked: store.collectProxied,
357
+ onChange() { store.setCollectProxied(this.checked) }
337
358
  }),
338
359
  r('span', null, t`Save Mocks`)))
339
360
  }
340
361
 
341
362
  function ResetButton() {
342
- function onClick() {
343
- state.setChosenLink('', '')
344
- mockaton.reset()
345
- .then(parseError)
346
- .then(updateState)
347
- .catch(onError)
348
- }
349
363
  return (
350
364
  r('button', {
351
365
  className: CSS.ResetButton,
352
- onClick
366
+ onClick: store.reset
353
367
  }, t`Reset`))
354
368
  }
355
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
+
356
412
 
357
413
 
358
414
  /** # MockList */
359
415
 
360
416
  function MockList() {
361
- const { brokersByMethod, groupByMethod, canProxy } = state
362
-
363
- if (!Object.keys(brokersByMethod).length)
417
+ if (!Object.keys(store.brokersByMethod).length)
364
418
  return (
365
419
  r('div', className(CSS.empty),
366
420
  t`No mocks found`))
367
421
 
368
- if (groupByMethod)
369
- return Object.keys(brokersByMethod).map(method => Fragment(
422
+ if (store.groupByMethod)
423
+ return Object.keys(store.brokersByMethod).map(method => Fragment(
370
424
  r('tr', null,
371
- r('th', { colspan: 2 + Number(canProxy) }),
425
+ r('th', { colspan: 2 + Number(store.canProxy) }),
372
426
  r('th', null, method)),
373
427
  rowsFor(method).map(Row)))
374
428
 
375
429
  return rowsFor('*').map(Row)
376
430
  }
377
431
 
378
-
379
- function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
380
- const { canProxy, groupByMethod } = state
381
- return (
382
- r('tr', { key: method + '::' + urlMask },
383
- canProxy && r('td', null, ProxyToggler(broker)),
384
- r('td', null, DelayRouteToggler(broker)),
385
- r('td', null, InternalServerErrorToggler(broker)),
386
- !groupByMethod && r('td', className(CSS.Method), method),
387
- r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
388
- r('td', null, MockSelector(broker))))
389
- }
390
-
391
432
  function rowsFor(targetMethod) {
392
- const { brokersByMethod } = state
393
-
394
433
  const rows = []
395
- for (const [method, brokers] of Object.entries(brokersByMethod))
434
+ for (const [method, brokers] of Object.entries(store.brokersByMethod))
396
435
  if (targetMethod === '*' || targetMethod === method)
397
436
  for (const [urlMask, broker] of Object.entries(brokers))
398
437
  rows.push({ method, urlMask, broker })
399
438
 
400
439
  const sorted = rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
401
-
402
440
  const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
403
441
  return sorted.map((r, i) => ({
404
442
  ...r,
@@ -406,22 +444,50 @@ function rowsFor(targetMethod) {
406
444
  }))
407
445
  }
408
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
+
409
484
 
410
485
  function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
411
- async function onClick(event) {
486
+ function onClick(event) {
412
487
  event.preventDefault()
413
- try {
414
- findChosenLink()?.classList.remove(CSS.chosen)
415
- this.classList.add(CSS.chosen)
416
- state.setChosenLink(method, urlMask)
417
- await previewMock(method, urlMask)
418
- }
419
- catch (error) {
420
- onError(error)
421
- }
488
+ store.previewLink(method, urlMask)
422
489
  }
423
- const { chosenLink } = state
424
- const isChosen = chosenLink.method === method && chosenLink.urlMask === urlMask
490
+ const isChosen = store.chosenLink.method === method && store.chosenLink.urlMask === urlMask
425
491
  const [ditto, tail] = urlMaskDittoed
426
492
  return (
427
493
  r('a', {
@@ -435,30 +501,18 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
435
501
  : tail))
436
502
  }
437
503
 
438
- function findChosenLink() {
439
- return document.querySelector(
440
- `body > main > .${CSS.leftSide} .${CSS.PreviewLink}.${CSS.chosen}`)
441
- }
442
-
443
- const STR_PROXIED = t`Proxied`
444
504
 
445
505
  /** @param {ClientMockBroker} broker */
446
506
  function MockSelector(broker) {
447
- function onChange() {
448
- const { method, urlMask } = parseFilename(this.value)
449
- state.setChosenLink(method, urlMask)
450
- mockaton.select(this.value)
451
- .then(parseError)
452
- .then(updateState)
453
- .catch(onError)
454
- }
455
-
507
+ const STR_PROXIED = t`Proxied`
508
+
456
509
  let selected = broker.currentMock.file
457
510
  const { status } = parseFilename(selected)
458
511
  const files = broker.mocks.filter(item =>
459
512
  status === 500 ||
460
513
  !item.includes(AUTOGENERATED_500_COMMENT))
461
- if (!selected) {
514
+
515
+ if (store.canProxy && broker.currentMock.proxied) {
462
516
  selected = STR_PROXIED
463
517
  files.push(selected)
464
518
  }
@@ -478,7 +532,7 @@ function MockSelector(broker) {
478
532
 
479
533
  return (
480
534
  r('select', {
481
- onChange,
535
+ onChange() { store.selectFile(this.value) },
482
536
  autocomplete: 'off',
483
537
  'aria-label': t`Mock Selector`,
484
538
  disabled: files.length <= 1,
@@ -497,9 +551,7 @@ function MockSelector(broker) {
497
551
  function DelayRouteToggler(broker) {
498
552
  function commit(checked) {
499
553
  const { method, urlMask } = parseFilename(broker.mocks[0])
500
- mockaton.setRouteIsDelayed(method, urlMask, checked)
501
- .then(parseError)
502
- .catch(onError)
554
+ store.setDelayed(method, urlMask, checked)
503
555
  }
504
556
  return ClickDragToggler({
505
557
  checked: broker.currentMock.delayed,
@@ -513,11 +565,7 @@ function DelayRouteToggler(broker) {
513
565
  function InternalServerErrorToggler(broker) {
514
566
  function onChange() {
515
567
  const { method, urlMask } = parseFilename(broker.mocks[0])
516
- state.setChosenLink(method, urlMask)
517
- mockaton.toggle500(method, urlMask)
518
- .then(parseError)
519
- .then(updateState)
520
- .catch(onError)
568
+ store.toggle500(method, urlMask)
521
569
  }
522
570
  return (
523
571
  r('label', {
@@ -527,7 +575,6 @@ function InternalServerErrorToggler(broker) {
527
575
  r('input', {
528
576
  type: 'checkbox',
529
577
  'data-focus-group': FocusGroup.StatusToggler,
530
- name: broker.currentMock.file,
531
578
  checked: parseFilename(broker.currentMock.file).status === 500,
532
579
  onChange
533
580
  }),
@@ -538,11 +585,7 @@ function InternalServerErrorToggler(broker) {
538
585
  function ProxyToggler(broker) {
539
586
  function onChange() {
540
587
  const { urlMask, method } = parseFilename(broker.mocks[0])
541
- state.setChosenLink(method, urlMask)
542
- mockaton.setRouteIsProxied(method, urlMask, this.checked)
543
- .then(parseError)
544
- .then(updateState)
545
- .catch(onError)
588
+ store.toggleProxied(method, urlMask, this.checked)
546
589
  }
547
590
  return (
548
591
  r('label', {
@@ -551,7 +594,7 @@ function ProxyToggler(broker) {
551
594
  },
552
595
  r('input', {
553
596
  type: 'checkbox',
554
- checked: !broker.currentMock.file,
597
+ checked: broker.currentMock.proxied,
555
598
  onChange,
556
599
  'data-focus-group': FocusGroup.ProxyToggler
557
600
  }),
@@ -563,7 +606,7 @@ function ProxyToggler(broker) {
563
606
  /** # StaticFilesList */
564
607
 
565
608
  function StaticFilesList() {
566
- const { staticBrokers, canProxy, groupByMethod } = state
609
+ const { staticBrokers, canProxy, groupByMethod } = store
567
610
  if (!Object.keys(staticBrokers).length)
568
611
  return null
569
612
  const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
@@ -591,25 +634,17 @@ function StaticFilesList() {
591
634
 
592
635
  /** @param {ClientStaticBroker} broker */
593
636
  function DelayStaticRouteToggler(broker) {
594
- function commit(checked) {
595
- mockaton.setStaticRouteIsDelayed(broker.route, checked)
596
- .then(parseError)
597
- .catch(onError)
598
- }
599
637
  return ClickDragToggler({
600
638
  checked: broker.delayed,
601
- commit,
602
- focusGroup: FocusGroup.DelayToggler
639
+ focusGroup: FocusGroup.DelayToggler,
640
+ commit(checked) {
641
+ store.setDelayedStatic(broker.route, checked)
642
+ }
603
643
  })
604
644
  }
605
645
 
606
646
  /** @param {ClientStaticBroker} broker */
607
647
  function NotFoundToggler(broker) {
608
- function onChange() {
609
- mockaton.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
610
- .then(parseError)
611
- .catch(onError)
612
- }
613
648
  return (
614
649
  r('label', {
615
650
  className: CSS.NotFoundToggler,
@@ -618,8 +653,10 @@ function NotFoundToggler(broker) {
618
653
  r('input', {
619
654
  type: 'checkbox',
620
655
  checked: broker.status === 404,
621
- onChange,
622
- 'data-focus-group': FocusGroup.StatusToggler
656
+ 'data-focus-group': FocusGroup.StatusToggler,
657
+ onChange() {
658
+ store.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
659
+ }
623
660
  }),
624
661
  r('span', null, t`404`)))
625
662
  }
@@ -659,7 +696,6 @@ function ClickDragToggler({ checked, commit, focusGroup }) {
659
696
  TimerIcon()))
660
697
  }
661
698
 
662
-
663
699
  function Resizer() {
664
700
  let raf = 0
665
701
  let initialX = 0
@@ -680,8 +716,8 @@ function Resizer() {
680
716
  function onMove(event) {
681
717
  const MIN_LEFT_WIDTH = 380
682
718
  raf = raf || requestAnimationFrame(() => {
683
- state.leftSideWidth = Math.max(panelWidth - (initialX - event.clientX), MIN_LEFT_WIDTH)
684
- 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'
685
721
  raf = 0
686
722
  })
687
723
  }
@@ -711,14 +747,13 @@ const payloadViewerTitleRef = useRef()
711
747
  const payloadViewerCodeRef = useRef()
712
748
 
713
749
  function PayloadViewer() {
714
- const { hasChosenLink } = state
715
750
  return (
716
751
  r('div', className(CSS.PayloadViewer),
717
752
  r('h2', { ref: payloadViewerTitleRef },
718
- !hasChosenLink && t`Preview`),
753
+ !store.hasChosenLink && t`Preview`),
719
754
  r('pre', null,
720
755
  r('code', { ref: payloadViewerCodeRef },
721
- !hasChosenLink && t`Click a link to preview it`))))
756
+ !store.hasChosenLink && t`Click a link to preview it`))))
722
757
  }
723
758
 
724
759
  function PayloadViewerTitle({ file, statusText }) {
@@ -745,7 +780,7 @@ const SPINNER_DELAY = 80
745
780
  function PayloadViewerProgressBar() {
746
781
  return (
747
782
  r('div', className(CSS.ProgressBar),
748
- r('div', { style: { animationDuration: state.delay - SPINNER_DELAY + 'ms' } })))
783
+ r('div', { style: { animationDuration: store.delay - SPINNER_DELAY + 'ms' } })))
749
784
  }
750
785
 
751
786
  async function previewMock(method, urlMask) {
@@ -763,11 +798,11 @@ async function previewMock(method, urlMask) {
763
798
  signal: previewMock.controller.signal
764
799
  })
765
800
  clearTimeout(spinnerTimer)
766
- const file = state.fileFor(method, urlMask)
767
- if (file === '')
768
- await updatePayloadViewer(STR_PROXIED, response)
801
+ const { proxied, file } = store.brokerFor(method, urlMask).currentMock
802
+ if (proxied)
803
+ await updatePayloadViewer(true, '', response)
769
804
  else if (file)
770
- await updatePayloadViewer(file, response)
805
+ await updatePayloadViewer(false, file, response)
771
806
  else {/* e.g. selected was deleted */}
772
807
  }
773
808
  catch (err) {
@@ -776,11 +811,11 @@ async function previewMock(method, urlMask) {
776
811
  }
777
812
  }
778
813
 
779
- async function updatePayloadViewer(file, response) {
814
+ async function updatePayloadViewer(proxied, file, response) {
780
815
  const mime = response.headers.get('content-type') || ''
781
816
 
782
817
  payloadViewerTitleRef.current.replaceChildren(
783
- file === STR_PROXIED
818
+ proxied
784
819
  ? PayloadViewerTitleWhenProxied({
785
820
  mime,
786
821
  status: response.status,
@@ -794,9 +829,7 @@ async function updatePayloadViewer(file, response) {
794
829
 
795
830
  if (mime.startsWith('image/')) // Naively assumes GET.200
796
831
  payloadViewerCodeRef.current.replaceChildren(
797
- r('img', {
798
- src: URL.createObjectURL(await response.blob())
799
- }))
832
+ r('img', { src: URL.createObjectURL(await response.blob()) }))
800
833
  else {
801
834
  const body = await response.text() || t`/* Empty Response Body */`
802
835
  if (mime === 'application/json')
@@ -814,55 +847,11 @@ function isXML(mime) {
814
847
  }
815
848
 
816
849
 
817
- function initKeyboardNavigation() {
818
- addEventListener('keydown', onKeyDown)
819
-
820
- function onKeyDown(event) {
821
- const pivot = document.activeElement
822
- switch (event.key) {
823
- case 'ArrowDown':
824
- case 'ArrowUp': {
825
- let fg = pivot.getAttribute('data-focus-group')
826
- if (fg !== null) {
827
- const offset = event.key === 'ArrowDown' ? +1 : -1
828
- circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
829
- }
830
- break
831
- }
832
- case 'ArrowRight':
833
- case 'ArrowLeft': {
834
- if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
835
- const offset = event.key === 'ArrowRight' ? +1 : -1
836
- rowFocusable(pivot, offset).focus()
837
- }
838
- break
839
- }
840
- }
841
- }
842
-
843
- function rowFocusable(el, step) {
844
- const row = el.closest('tr')
845
- if (row) {
846
- const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
847
- return circularAdjacent(step, focusables, el)
848
- }
849
- }
850
-
851
- function allInFocusGroup(focusGroup) {
852
- return Array.from(document.querySelectorAll(
853
- `body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
854
- }
855
-
856
- function circularAdjacent(step = 1, arr, pivot) {
857
- return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
858
- }
859
- }
860
-
861
850
  /** # Error */
862
851
 
863
852
  async function parseError(response) {
864
853
  if (response.ok)
865
- return
854
+ return response
866
855
  if (response.status === 422)
867
856
  throw await response.text()
868
857
  throw response.statusText
@@ -939,7 +928,7 @@ function initRealTimeUpdates() {
939
928
  if (oldSyncVersion !== syncVersion) { // because it could be < or >
940
929
  oldSyncVersion = syncVersion
941
930
  if (!skipUpdate)
942
- await updateState()
931
+ await fetchState()
943
932
  }
944
933
  poll()
945
934
  }
@@ -953,6 +942,50 @@ function initRealTimeUpdates() {
953
942
  }
954
943
  }
955
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
+
956
989
 
957
990
  /** # Utils */
958
991
 
@@ -1119,7 +1152,6 @@ dittoSplitPaths.test = function () {
1119
1152
  }
1120
1153
 
1121
1154
 
1122
-
1123
1155
  function syntaxJSON(json) {
1124
1156
  const MAX_NODES = 50_000
1125
1157
  let nNodes = 0
@@ -1167,7 +1199,6 @@ syntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s
1167
1199
  // Capture group order: [string, optional colon, punc]
1168
1200
 
1169
1201
 
1170
-
1171
1202
  function syntaxXML(xml) {
1172
1203
  const MAX_NODES = 50_000
1173
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)