mockaton 10.6.6 → 10.6.8

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/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.6",
5
+ "version": "10.6.8",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -107,7 +107,7 @@ async function selectCookie(req, response) {
107
107
  if (error)
108
108
  sendUnprocessableContent(response, error?.message || error)
109
109
  else
110
- sendOK(response)
110
+ sendJSON(response, cookie.list())
111
111
  }
112
112
 
113
113
  async function selectMock(req, response) {
@@ -4,25 +4,16 @@ 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
- #then = a => a
8
- #catch = e => { throw e }
9
7
 
10
8
  constructor(addr) {
11
9
  this.#addr = addr
12
10
  }
13
11
 
14
- setupPatchCallbacks(_then = undefined, _catch = undefined) {
15
- if (_then) this.#then = _then
16
- if (_catch) this.#catch = _catch
17
- }
18
-
19
12
  #patch = (api, body) => {
20
13
  return fetch(this.#addr + api, {
21
14
  method: 'PATCH',
22
15
  body: JSON.stringify(body)
23
16
  })
24
- .then(this.#then)
25
- .catch(this.#catch)
26
17
  }
27
18
 
28
19
  /** @returns {JsonPromise<State>} */
package/src/Dashboard.css CHANGED
@@ -78,7 +78,7 @@ body {
78
78
 
79
79
  select, a, input, button {
80
80
  cursor: pointer;
81
-
81
+
82
82
  &:focus-visible {
83
83
  outline: 2px solid var(--colorAccent);
84
84
  }
@@ -325,7 +325,7 @@ main {
325
325
  }
326
326
 
327
327
  .leftSide {
328
- /* the width is set in js (it’s resizable) */
328
+ width: 50%; /* resizable in js */
329
329
  padding: 16px;
330
330
  border-right: 1px solid var(--colorSecondaryActionBorder);
331
331
  user-select: none;
@@ -628,6 +628,8 @@ table {
628
628
  background: var(--colorAccent);
629
629
  animation-name: _kfProgress;
630
630
  animation-timing-function: linear;
631
+ animation-iteration-count: infinite;
632
+ animation-direction: alternate;
631
633
  /* duration in JavaScript */
632
634
  }
633
635
  }
package/src/Dashboard.js CHANGED
@@ -1,7 +1,7 @@
1
- import { createElement as r, createSvgElement as s, className, restoreFocus, deferred, Defer, Fragment, useRef } from './DashboardDom.js'
1
+ import { createElement as r, createSvgElement as s, className, restoreFocus, Defer, Fragment, useRef } from './DashboardDom.js'
2
2
  import { AUTO_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
3
- import { parseFilename, extractComments } from './Filename.js'
4
- import { store, dittoSplitPaths } from './DashboardStore.js'
3
+ import { store, dittoSplitPaths, BrokerRowModel } from './DashboardStore.js'
4
+ import { parseFilename } from './Filename.js'
5
5
 
6
6
 
7
7
  const CSS = {
@@ -55,18 +55,17 @@ const FocusGroup = {
55
55
  PreviewLink: 3
56
56
  }
57
57
 
58
+ store.onError = onError
58
59
  store.render = render
59
60
  store.renderRow = renderRow
60
- store.setupPatchCallbacks(parseError, onError)
61
- store.fetchState().catch(onError)
62
-
61
+ store.fetchState()
63
62
  initRealTimeUpdates()
64
63
  initKeyboardNavigation()
65
64
 
66
65
  function render() {
67
66
  restoreFocus(() => document.body.replaceChildren(...App()))
68
67
  if (store.hasChosenLink)
69
- previewMock(store.chosenLink.method, store.chosenLink.urlMask)
68
+ previewMock()
70
69
  }
71
70
 
72
71
  const t = translation => translation[0]
@@ -136,13 +135,12 @@ function GlobalDelayField() {
136
135
  }
137
136
 
138
137
  function BulkSelector() {
138
+ // TODO For a11y, this should be a `menu` instead of this `select`
139
139
  const { comments } = store
140
- // UX wise this should be a menu instead of this `select`.
141
- // But this way is easier to implement, with a few hacks.
142
140
  const firstOption = t`Pick Comment…`
143
141
  function onChange() {
144
142
  const value = this.value
145
- this.value = firstOption // Hack
143
+ this.value = firstOption // Hack
146
144
  store.bulkSelectByComment(value)
147
145
  }
148
146
  const disabled = !comments.length
@@ -281,58 +279,45 @@ function MockList() {
281
279
  r('tr', null,
282
280
  r('th', { colspan: 2 + Number(store.canProxy) }),
283
281
  r('th', null, method)),
284
- rowsFor(method).map(Row)))
285
-
286
- return rowsFor('*').map(Row)
287
- }
282
+ store.brokersAsRowsByMethod(method).map(Row)))
288
283
 
289
- function rowsFor(targetMethod) {
290
- const sorted = store.brokersByMethodAsArray(targetMethod)
291
- const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
292
- return sorted.map((r, i) => ({
293
- ...r,
294
- urlMaskDittoed: urlMasksDittoed[i]
295
- }))
284
+ return store.brokersAsRowsByMethod('*').map(Row)
296
285
  }
297
286
 
298
- function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
299
- const key = Row.key(method, urlMask)
300
- Row.ditto.set(key, urlMaskDittoed)
301
- const { proxied, delayed, file } = broker.currentMock
287
+ /**
288
+ * @param {BrokerRowModel} row
289
+ * @param {number} i
290
+ */
291
+ function Row(row, i) {
292
+ const { method, urlMask } = row
302
293
  return (
303
- r('tr', { key },
294
+ r('tr', { key: Row.key(method, urlMask) },
304
295
  store.canProxy && r('td', null,
305
- ProxyToggler(method, urlMask, proxied)),
296
+ ProxyToggler(method, urlMask, row.proxied)),
306
297
 
307
298
  r('td', null,
308
- DelayRouteToggler(method, urlMask, delayed)),
299
+ DelayRouteToggler(method, urlMask, row.delayed)),
309
300
 
310
301
  r('td', null,
311
- InternalServerErrorToggler(method, urlMask, parseFilename(file).status === 500)),
302
+ InternalServerErrorToggler(method, urlMask, !row.proxied && row.selectedFileIs500)),
312
303
 
313
304
  !store.groupByMethod && r('td', className(CSS.Method),
314
305
  method),
315
306
 
316
307
  r('td', null,
317
- PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
308
+ PreviewLink(method, urlMask, row.urlMaskDittoed, i === 0)),
318
309
 
319
310
  r('td', null,
320
- MockSelector(broker))))
311
+ MockSelector(row))))
321
312
  }
322
313
  Row.key = (method, urlMask) => method + '::' + urlMask
323
- Row.ditto = new Map()
324
314
 
325
315
  function renderRow(method, urlMask) {
326
316
  restoreFocus(() => {
327
317
  unChooseOld()
328
- const key = Row.key(method, urlMask)
329
- trFor(key).replaceWith(Row({
330
- method,
331
- urlMask,
332
- urlMaskDittoed: Row.ditto.get(key),
333
- broker: store.brokerFor(method, urlMask)
334
- }))
335
- previewMock(method, urlMask)
318
+ trFor(Row.key(method, urlMask))
319
+ .replaceWith(Row(store.brokerAsRow(method, urlMask)))
320
+ previewMock()
336
321
  })
337
322
 
338
323
  function trFor(key) {
@@ -365,91 +350,21 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
365
350
  }
366
351
 
367
352
 
368
- /** @param {ClientMockBroker} broker */
369
- function MockSelector(broker) {
370
- let selected = broker.currentMock.file
371
- const selectedStatus = parseFilename(selected).status
372
-
373
- const files = baseOptionsFor(broker.mocks, selectedStatus === 500)
374
- if (store.canProxy && broker.currentMock.proxied) {
375
- selected = t`Proxied`
376
- files.push([selected, selected])
377
- }
378
-
353
+ /** @param {BrokerRowModel} row */
354
+ function MockSelector(row) {
379
355
  return (
380
356
  r('select', {
381
357
  onChange() { store.selectFile(this.value) },
382
358
  autocomplete: 'off',
383
359
  'aria-label': t`Mock Selector`,
384
- disabled: files.length <= 1,
360
+ disabled: row.opts.length < 2,
385
361
  ...className(
386
362
  CSS.MockSelector,
387
- selected !== files[0][0] && CSS.nonDefault,
388
- selectedStatus >= 400 && selectedStatus < 500 && CSS.status4xx)
389
- }, files.map(([file, name]) => (
390
- r('option', {
391
- value: file,
392
- selected: file === selected
393
- }, name)))))
394
- }
395
- function baseOptionsFor(mocks, selectedIs500) {
396
- return mocks
397
- .filter(f => selectedIs500 || !f.includes(AUTO_500_COMMENT))
398
- .map(f => [f, nameFor(f)])
399
-
400
- function nameFor(file) {
401
- const { status, ext } = parseFilename(file)
402
- const comments = extractComments(file)
403
- const isAutogen500 = comments.includes(AUTO_500_COMMENT)
404
- return [
405
- isAutogen500 ? '' : status,
406
- ext === 'empty' || ext === 'unknown' ? '' : ext,
407
- isAutogen500 ? t`Auto500` : comments.join(' ')
408
- ].filter(Boolean).join(' ')
409
- }
363
+ row.selectedIdx > 0 && CSS.nonDefault,
364
+ row.selectedFileIs4xx && CSS.status4xx)
365
+ }, row.opts.map(([value, label, selected]) =>
366
+ r('option', { value, selected }, label))))
410
367
  }
411
- baseOptionsFor.test = function () {
412
- (function ignoresAutoGen500WhenUnselected() {
413
- const files = baseOptionsFor([
414
- `api/user${AUTO_500_COMMENT}.GET.500.empty`
415
- ], false)
416
- console.assert(files.length === 0)
417
- }());
418
-
419
- (function keepsNonAutoGen500WhenUnselected() {
420
- const files = baseOptionsFor([
421
- `api/user.GET.500.txt`
422
- ], false)
423
- console.assert(files.length === 1)
424
- console.assert(files[0][1] === t`500 txt`)
425
- }());
426
-
427
- (function renamesAutoGenFileToAuto500() {
428
- const files = baseOptionsFor([
429
- `api/user${AUTO_500_COMMENT}.GET.500.empty`
430
- ], true)
431
- console.assert(files.length === 1)
432
- console.assert(files[0][1] === t`Auto500`)
433
- }());
434
-
435
- (function filenameHasExtensionExceptWhenEmptyOrUnknown() {
436
- const files = baseOptionsFor([
437
- `api/user0.GET.200.empty`,
438
- `api/user1.GET.200.unknown`,
439
- `api/user2.GET.200.json`,
440
- `api/user3(another json).GET.200.json`,
441
- ], true)
442
- console.assert(deepEqual(files.map(([, n]) => n), [
443
- // Think about, in cases like this, the only option the user
444
- // has for discerning empty and unknown is on the Previewer Title
445
- '200',
446
- '200',
447
- '200 json',
448
- '200 json (another json)',
449
- ]))
450
- }())
451
- }
452
- deferred(baseOptionsFor.test)
453
368
 
454
369
 
455
370
  function DelayRouteToggler(method, urlMask, checked) {
@@ -498,6 +413,7 @@ function StaticFilesList() {
498
413
  const { staticBrokers, canProxy, groupByMethod } = store
499
414
  if (!Object.keys(staticBrokers).length)
500
415
  return null
416
+
501
417
  const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
502
418
  ? [r('span', className(CSS.dittoDir), ditto), tail]
503
419
  : tail)
@@ -677,7 +593,9 @@ function PayloadViewerProgressBar() {
677
593
  r('div', { style: { animationDuration: store.delay - SPINNER_DELAY + 'ms' } })))
678
594
  }
679
595
 
680
- async function previewMock(method, urlMask) {
596
+ async function previewMock() {
597
+ const { method, urlMask } = store.chosenLink
598
+
681
599
  previewMock.controller?.abort()
682
600
  previewMock.controller = new AbortController
683
601
 
@@ -696,8 +614,7 @@ async function previewMock(method, urlMask) {
696
614
  if (proxied || file)
697
615
  await updatePayloadViewer(proxied, file, response)
698
616
  }
699
- catch (err) {
700
- onError(err)
617
+ catch {
701
618
  payloadViewerCodeRef.current.replaceChildren()
702
619
  }
703
620
  }
@@ -717,7 +634,7 @@ async function updatePayloadViewer(proxied, file, response) {
717
634
  file,
718
635
  statusText: response.statusText
719
636
  }))
720
-
637
+
721
638
  if (mime.startsWith('image/')) // Naively assumes GET.200
722
639
  payloadViewerCodeRef.current.replaceChildren(
723
640
  r('img', { src: URL.createObjectURL(await response.blob()) }))
@@ -725,7 +642,7 @@ async function updatePayloadViewer(proxied, file, response) {
725
642
  const body = await response.text() || t`/* Empty Response Body */`
726
643
  if (mime === 'application/json')
727
644
  payloadViewerCodeRef.current.replaceChildren(r('span', className(CSS.json), SyntaxJSON(body)))
728
- else if (isXML(mime))
645
+ else if (isXML(mime))
729
646
  payloadViewerCodeRef.current.replaceChildren(SyntaxXML(body))
730
647
  else
731
648
  payloadViewerCodeRef.current.textContent = body
@@ -738,24 +655,25 @@ function isXML(mime) {
738
655
  }
739
656
 
740
657
 
741
- /** # Error */
742
-
743
- async function parseError(response) {
744
- if (response.ok)
745
- return response
746
- if (response.status === 422)
747
- throw await response.text()
748
- throw response.statusText
749
- }
658
+ async function onError(_error) {
659
+ let error = _error
750
660
 
751
- function onError(error) {
752
- if (error?.name === 'AbortError')
753
- return
754
- if (error?.message === 'Failed to fetch')
755
- showErrorToast(t`Looks like the Mockaton server is not running`)
756
- else
757
- showErrorToast(error || t`Unexpected Error`)
758
- console.error(error)
661
+ if (_error instanceof Response) {
662
+ if (_error.status === 422)
663
+ error = await _error.text()
664
+ else if (_error.statusText)
665
+ error = _error.statusText
666
+ }
667
+ else {
668
+ if (error?.name === 'AbortError')
669
+ return
670
+ if (error?.message === 'Failed to fetch')
671
+ error = t`Looks like the Mockaton server is not running` // TODO clear Error if comes back in ui-sync
672
+ else
673
+ error = error || t`Unexpected Error`
674
+ }
675
+ showErrorToast(error)
676
+ console.error(_error)
759
677
  }
760
678
 
761
679
  function showErrorToast(msg) {
@@ -801,17 +719,17 @@ function initRealTimeUpdates() {
801
719
  let oldSyncVersion = -1
802
720
  let controller = new AbortController()
803
721
 
804
- poll()
722
+ longPoll()
805
723
  document.addEventListener('visibilitychange', () => {
806
724
  if (document.hidden) {
807
725
  controller.abort('_hidden_tab_')
808
726
  controller = new AbortController()
809
727
  }
810
728
  else
811
- poll()
729
+ longPoll()
812
730
  })
813
731
 
814
- async function poll() {
732
+ async function longPoll() {
815
733
  try {
816
734
  const response = await store.getSyncVersion(oldSyncVersion, controller.signal)
817
735
  if (response.ok) {
@@ -820,16 +738,16 @@ function initRealTimeUpdates() {
820
738
  if (oldSyncVersion !== syncVersion) { // because it could be < or >
821
739
  oldSyncVersion = syncVersion
822
740
  if (!skipUpdate)
823
- store.fetchState().catch(onError)
741
+ store.fetchState()
824
742
  }
825
- poll()
743
+ longPoll()
826
744
  }
827
745
  else
828
746
  throw response.status
829
747
  }
830
748
  catch (error) {
831
749
  if (error !== '_hidden_tab_')
832
- setTimeout(poll, 3000)
750
+ setTimeout(longPoll, 3000)
833
751
  }
834
752
  }
835
753
  }
@@ -969,8 +887,3 @@ function SyntaxXML(xml) {
969
887
  SyntaxXML.regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
970
888
  // Capture groups order: [tagPunc, tagName, attrName, attrVal]
971
889
 
972
-
973
- function deepEqual(a, b) {
974
- return JSON.stringify(a) === JSON.stringify(b)
975
- }
976
-
@@ -1,7 +1,5 @@
1
1
  export function className(...args) {
2
- return {
3
- className: args.filter(Boolean).join(' ')
4
- }
2
+ return { className: args.filter(Boolean).join(' ') }
5
3
  }
6
4
 
7
5
  export function createElement(tag, props, ...children) {
@@ -1,23 +1,19 @@
1
1
  import { deferred } from './DashboardDom.js'
2
2
  import { Commander } from './ApiCommander.js'
3
- import { parseFilename } from './Filename.js'
3
+ import { parseFilename, extractComments } from './Filename.js'
4
+ import { AUTO_500_COMMENT } from './ApiConstants.js'
4
5
 
5
6
 
7
+ const t = translation => translation[0]
6
8
  const mockaton = new Commander(location.origin)
7
9
 
8
10
  export const store = {
9
- setupPatchCallbacks(_then, _catch) {
10
- mockaton.setupPatchCallbacks(_then, _catch)
11
- },
12
-
11
+ onError(err) {},
13
12
  render() {},
14
13
  renderRow(method, urlMask) {},
15
14
 
16
- /** @type {State.brokersByMethod} */
17
- brokersByMethod: {},
18
-
19
- /** @type {State.staticBrokers} */
20
- staticBrokers: {},
15
+ brokersByMethod: /** @type {State.brokersByMethod} */ {},
16
+ staticBrokers: /** @type {State.staticBrokers} */ {},
21
17
 
22
18
  cookies: [],
23
19
  comments: [],
@@ -31,20 +27,18 @@ export const store = {
31
27
 
32
28
  getSyncVersion: mockaton.getSyncVersion,
33
29
 
34
- fetchState() {
35
- return mockaton.getState().then(response => {
36
- if (!response.ok)
37
- throw response.status
38
-
39
- response.json().then(state => {
40
- Object.assign(store, state)
41
- store.render()
42
- })
43
- })
30
+ async fetchState() {
31
+ try {
32
+ const response = await mockaton.getState()
33
+ if (!response.ok) throw response
34
+ Object.assign(store, await response.json())
35
+ store.render()
36
+ }
37
+ catch (error) { store.onError(error) }
44
38
  },
45
39
 
46
40
 
47
- leftSideWidth: window.innerWidth / 2,
41
+ leftSideWidth: undefined,
48
42
 
49
43
  groupByMethod: initPreference('groupByMethod'),
50
44
  toggleGroupByMethod() {
@@ -54,7 +48,6 @@ export const store = {
54
48
  },
55
49
 
56
50
 
57
-
58
51
  chosenLink: {
59
52
  method: '',
60
53
  urlMask: ''
@@ -66,38 +59,63 @@ export const store = {
66
59
  setChosenLink(method, urlMask) {
67
60
  store.chosenLink = { method, urlMask }
68
61
  },
69
-
70
- reset() {
71
- store.setChosenLink('', '')
72
- mockaton.reset()
73
- .then(store.fetchState)
62
+
63
+
64
+ async reset() {
65
+ try {
66
+ const response = await mockaton.reset()
67
+ if (!response.ok) throw response
68
+ store.setChosenLink('', '')
69
+ await store.fetchState()
70
+ }
71
+ catch (error) { store.onError(error) }
74
72
  },
75
73
 
76
- bulkSelectByComment(value) {
77
- mockaton.bulkSelectByComment(value)
78
- .then(store.fetchState)
74
+ async bulkSelectByComment(value) {
75
+ try {
76
+ const response = await mockaton.bulkSelectByComment(value)
77
+ if (!response.ok) throw response
78
+ await store.fetchState()
79
+ }
80
+ catch (error) { store.onError(error) }
79
81
  },
80
82
 
81
-
82
- setGlobalDelay(value) {
83
- store.delay = value
84
- mockaton.setGlobalDelay(value)
83
+
84
+ async setGlobalDelay(value) {
85
+ try {
86
+ const response = await mockaton.setGlobalDelay(value)
87
+ if (!response.ok) throw response
88
+ store.delay = value
89
+ }
90
+ catch (error) { store.onError(error) }
85
91
  },
86
92
 
87
- selectCookie(name) {
88
- store.cookies = store.cookies.map(([n]) => [n, n === name])
89
- mockaton.selectCookie(name)
93
+ async selectCookie(name) {
94
+ try {
95
+ const response = await mockaton.selectCookie(name)
96
+ if (!response.ok) throw response
97
+ store.cookies = await response.json()
98
+ }
99
+ catch (error) { store.onError(error) }
90
100
  },
91
101
 
92
- setProxyFallback(value) {
93
- store.proxyFallback = value
94
- mockaton.setProxyFallback(value)
95
- .then(store.render)
102
+ async setProxyFallback(value) {
103
+ try {
104
+ const response = await mockaton.setProxyFallback(value)
105
+ if (!response.ok) throw response
106
+ store.proxyFallback = value
107
+ store.render()
108
+ }
109
+ catch (error) { store.onError(error) }
96
110
  },
97
111
 
98
- setCollectProxied(checked) {
99
- store.collectProxied = checked
100
- mockaton.setCollectProxied(checked)
112
+ async setCollectProxied(checked) {
113
+ try {
114
+ const response = await mockaton.setCollectProxied(checked)
115
+ if (!response.ok) throw response
116
+ store.collectProxied = checked
117
+ }
118
+ catch (error) { store.onError(error) }
101
119
  },
102
120
 
103
121
 
@@ -105,62 +123,101 @@ export const store = {
105
123
  return store.brokersByMethod[method]?.[urlMask]
106
124
  },
107
125
 
108
- brokersByMethodAsArray(targetMethod = '*') {
109
- const rows = []
126
+ _dittoCache: new Map(),
127
+
128
+ brokersAsRowsByMethod(method) {
129
+ const rows = store._brokersAsArray(method)
130
+ .map(b => new BrokerRowModel(b, store.canProxy))
131
+ .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
132
+ const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
133
+ rows.forEach((r, i) => {
134
+ store._dittoCache.set(r.method + '::' + r.urlMask, urlMasksDittoed[i])
135
+ r.setUrlMaskDittoed(urlMasksDittoed[i])
136
+ })
137
+ return rows
138
+ },
139
+
140
+ brokerAsRow(method, urlMask) {
141
+ const r = new BrokerRowModel(store.brokerFor(method, urlMask), store.canProxy)
142
+ r.setUrlMaskDittoed(store._dittoCache.get(r.method + '::' + r.urlMask))
143
+ return r
144
+ },
145
+
146
+ _brokersAsArray(byMethod = '*') {
147
+ const arr = []
110
148
  for (const [method, brokers] of Object.entries(store.brokersByMethod))
111
- if (targetMethod === '*' || targetMethod === method)
112
- for (const [urlMask, broker] of Object.entries(brokers))
113
- rows.push({ method, urlMask, broker })
114
- return rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
149
+ if (byMethod === '*' || byMethod === method)
150
+ for (const broker of Object.values(brokers))
151
+ arr.push(broker)
152
+ return arr
115
153
  },
154
+
116
155
 
117
156
  previewLink(method, urlMask) {
118
157
  store.setChosenLink(method, urlMask)
119
158
  store.renderRow(method, urlMask)
120
159
  },
121
160
 
122
- selectFile(file) {
123
- mockaton.select(file).then(async response => {
161
+ async selectFile(file) {
162
+ try {
163
+ const response = await mockaton.select(file)
164
+ if (!response.ok) throw response
124
165
  const { method, urlMask } = parseFilename(file)
125
166
  store.brokerFor(method, urlMask).currentMock = await response.json()
126
167
  store.setChosenLink(method, urlMask)
127
168
  store.renderRow(method, urlMask)
128
- })
169
+ }
170
+ catch (error) { store.onError(error) }
129
171
  },
130
172
 
131
- toggle500(method, urlMask) {
132
- mockaton.toggle500(method, urlMask).then(async response => {
173
+ async toggle500(method, urlMask) {
174
+ try {
175
+ const response = await mockaton.toggle500(method, urlMask)
176
+ if (!response.ok) throw response
133
177
  store.brokerFor(method, urlMask).currentMock = await response.json()
134
178
  store.setChosenLink(method, urlMask)
135
179
  store.renderRow(method, urlMask)
136
- })
180
+ }
181
+ catch (error) { store.onError(error) }
137
182
  },
138
183
 
139
- setProxied(method, urlMask, checked) {
140
- mockaton.setRouteIsProxied(method, urlMask, checked).then(() => {
184
+ async setProxied(method, urlMask, checked) {
185
+ try {
186
+ const response = await mockaton.setRouteIsProxied(method, urlMask, checked)
187
+ if (!response.ok) throw response
141
188
  store.brokerFor(method, urlMask).currentMock.proxied = checked
142
189
  store.setChosenLink(method, urlMask)
143
190
  store.renderRow(method, urlMask)
144
- })
191
+ }
192
+ catch (error) { store.onError(error) }
145
193
  },
146
194
 
147
- setDelayed(method, urlMask, checked) {
148
- mockaton.setRouteIsDelayed(method, urlMask, checked).then(() => {
195
+ async setDelayed(method, urlMask, checked) {
196
+ try {
197
+ const response = await mockaton.setRouteIsDelayed(method, urlMask, checked)
198
+ if (!response.ok) throw response
149
199
  store.brokerFor(method, urlMask).currentMock.delayed = checked
150
- })
200
+ }
201
+ catch (error) { store.onError(error) }
151
202
  },
152
203
 
153
204
 
154
- staticBrokerFor(route) { return store.staticBrokers[route] },
155
-
156
- setDelayedStatic(route, checked) {
157
- store.staticBrokerFor(route).delayed = checked
158
- mockaton.setStaticRouteIsDelayed(route, checked)
205
+ async setDelayedStatic(route, checked) {
206
+ try {
207
+ const response = await mockaton.setStaticRouteIsDelayed(route, checked)
208
+ if (!response.ok) throw response
209
+ store.staticBrokers[route].delayed = checked
210
+ }
211
+ catch (error) { store.onError(error) }
159
212
  },
160
213
 
161
- setStaticRouteStatus(route, status) {
162
- store.staticBrokerFor(route).status = status
163
- mockaton.setStaticRouteStatus(route, status)
214
+ async setStaticRouteStatus(route, status) {
215
+ try {
216
+ const response = await mockaton.setStaticRouteStatus(route, status)
217
+ if (!response.ok) throw response
218
+ store.staticBrokers[route].status = status
219
+ }
220
+ catch (error) { store.onError(error) }
164
221
  }
165
222
  }
166
223
 
@@ -251,6 +308,156 @@ dittoSplitPaths.test = function () {
251
308
  }
252
309
  deferred(dittoSplitPaths.test)
253
310
 
311
+
312
+
313
+ export class BrokerRowModel {
314
+ opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
315
+ method = ''
316
+ urlMask = ''
317
+ urlMaskDittoed = ['', '']
318
+ #broker = /** @type {ClientMockBroker} */ {}
319
+ #canProxy = false
320
+
321
+ /**
322
+ * @param {ClientMockBroker} broker
323
+ * @param {boolean} canProxy
324
+ */
325
+ constructor(broker, canProxy) {
326
+ this.#broker = broker
327
+ this.#canProxy = canProxy
328
+ const { method, urlMask } = parseFilename(broker.currentMock.file)
329
+ this.method = method
330
+ this.urlMask = urlMask
331
+ this.opts = this.#makeOptions()
332
+ }
333
+
334
+ setUrlMaskDittoed(urlMaskDittoed) {
335
+ this.urlMaskDittoed = urlMaskDittoed
336
+ }
337
+
338
+ get delayed() {
339
+ return this.#broker.currentMock.delayed
340
+ }
341
+ get proxied() {
342
+ return this.#canProxy && this.#broker.currentMock.proxied
343
+ }
344
+ get selectedIdx() {
345
+ return this.opts.findIndex(([, , selected]) => selected)
346
+ }
347
+ get selectedFile() {
348
+ return this.#broker.currentMock.file
349
+ }
350
+ get selectedFileIs4xx() {
351
+ const { status } = parseFilename(this.selectedFile)
352
+ return status >= 400 && status < 500
353
+ }
354
+ get selectedFileIs500() {
355
+ const { status } = parseFilename(this.selectedFile)
356
+ return status === 500
357
+ }
358
+
359
+ #makeOptions() {
360
+ const proxied = this.proxied
361
+ const selectedIs500 = this.selectedFileIs500
362
+
363
+ const opts = this.#broker.mocks
364
+ .filter(f => selectedIs500 || !f.includes(AUTO_500_COMMENT))
365
+ .map(f => [
366
+ f,
367
+ this.#optionLabelFor(f),
368
+ !proxied && f === this.selectedFile
369
+ ])
370
+
371
+ if (proxied)
372
+ opts.push([
373
+ '__PROXIED__',
374
+ t`Proxied`,
375
+ true
376
+ ])
377
+
378
+ return opts
379
+ }
380
+
381
+ #optionLabelFor(file) {
382
+ const { status, ext } = parseFilename(file)
383
+ const comments = extractComments(file)
384
+ const isAutogen500 = comments.includes(AUTO_500_COMMENT)
385
+ return [
386
+ isAutogen500 ? '' : status,
387
+ ext === 'empty' || ext === 'unknown' ? '' : ext,
388
+ isAutogen500 ? t`Auto500` : comments.join(' ')
389
+ ].filter(Boolean).join(' ')
390
+ }
391
+ }
392
+
393
+ const TestBrokerRowModelOptions = {
394
+ 'ignores autogen500 when unselected'() {
395
+ const broker = {
396
+ currentMock: { file: 'api/other' },
397
+ mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
398
+ }
399
+ const row = new BrokerRowModel(broker, false)
400
+ console.assert(row.opts.length === 0)
401
+ },
402
+
403
+ 'keeps non-autogen500 when unselected'() {
404
+ const broker = {
405
+ currentMock: { file: 'api/other' },
406
+ mocks: [`api/user.GET.500.txt`]
407
+ }
408
+ const row = new BrokerRowModel(broker, false)
409
+ console.assert(row.opts.length === 1)
410
+ console.assert(row.opts[0][1] === t`500 txt`)
411
+ },
412
+
413
+ 'renames autogen file to Auto500'() {
414
+ const broker = {
415
+ currentMock: { file: `api/user${AUTO_500_COMMENT}.GET.500.empty` },
416
+ mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
417
+ }
418
+ const row = new BrokerRowModel(broker, false)
419
+ console.assert(row.opts.length === 1)
420
+ console.assert(row.opts[0][1] === t`Auto500`)
421
+ },
422
+
423
+ 'filename has extension except when empty or unknown'() {
424
+ const broker = {
425
+ currentMock: { file: `api/other` },
426
+ mocks: [
427
+ `api/user0.GET.200.empty`,
428
+ `api/user1.GET.200.unknown`,
429
+ `api/user2.GET.200.json`,
430
+ `api/user3(another json).GET.200.json`,
431
+ ]
432
+ }
433
+ const row = new BrokerRowModel(broker, false)
434
+ // Think about, in cases like this, the only option the user has
435
+ // for discerning empty and unknown is on the Previewer Title
436
+ console.assert(deepEqual(row.opts.map(([, n]) => n), [
437
+ '200',
438
+ '200',
439
+ '200 json',
440
+ '200 json (another json)',
441
+ ]))
442
+ },
443
+
444
+ 'appends "Proxied" label iff current is proxied'() {
445
+ const broker = {
446
+ currentMock: {
447
+ file: 'api/foo',
448
+ proxied: true
449
+ },
450
+ mocks: [`api/foo.GET.200.json`]
451
+ }
452
+ const row = new BrokerRowModel(broker, true)
453
+ console.assert(deepEqual(row.opts.map(([, n, selected]) => [n, selected]), [
454
+ ['200 json', false],
455
+ [t`Proxied`, true]
456
+ ]))
457
+ }
458
+ }
459
+ deferred(() => Object.values(TestBrokerRowModelOptions).forEach(t => t()))
460
+
254
461
  function deepEqual(a, b) {
255
462
  return JSON.stringify(a) === JSON.stringify(b)
256
463
  }