mockaton 10.6.6 → 10.6.7

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.7",
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
@@ -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;
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,11 +55,10 @@ 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
 
@@ -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,57 +279,44 @@ 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
- }))
318
+ trFor(Row.key(method, urlMask))
319
+ .replaceWith(Row(store.brokerAsRow(method, urlMask)))
335
320
  previewMock(method, urlMask)
336
321
  })
337
322
 
@@ -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)
@@ -670,6 +586,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
670
586
  ' ' + mime))
671
587
  }
672
588
 
589
+ // TODO indeterminate when there's store.delayJitter
673
590
  const SPINNER_DELAY = 80
674
591
  function PayloadViewerProgressBar() {
675
592
  return (
@@ -696,8 +613,7 @@ async function previewMock(method, urlMask) {
696
613
  if (proxied || file)
697
614
  await updatePayloadViewer(proxied, file, response)
698
615
  }
699
- catch (err) {
700
- onError(err)
616
+ catch {
701
617
  payloadViewerCodeRef.current.replaceChildren()
702
618
  }
703
619
  }
@@ -717,7 +633,7 @@ async function updatePayloadViewer(proxied, file, response) {
717
633
  file,
718
634
  statusText: response.statusText
719
635
  }))
720
-
636
+
721
637
  if (mime.startsWith('image/')) // Naively assumes GET.200
722
638
  payloadViewerCodeRef.current.replaceChildren(
723
639
  r('img', { src: URL.createObjectURL(await response.blob()) }))
@@ -725,7 +641,7 @@ async function updatePayloadViewer(proxied, file, response) {
725
641
  const body = await response.text() || t`/* Empty Response Body */`
726
642
  if (mime === 'application/json')
727
643
  payloadViewerCodeRef.current.replaceChildren(r('span', className(CSS.json), SyntaxJSON(body)))
728
- else if (isXML(mime))
644
+ else if (isXML(mime))
729
645
  payloadViewerCodeRef.current.replaceChildren(SyntaxXML(body))
730
646
  else
731
647
  payloadViewerCodeRef.current.textContent = body
@@ -738,24 +654,25 @@ function isXML(mime) {
738
654
  }
739
655
 
740
656
 
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
- }
657
+ async function onError(_error) {
658
+ let error = _error
750
659
 
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)
660
+ if (_error instanceof Response) {
661
+ if (_error.status === 422)
662
+ error = await _error.text()
663
+ else if (_error.statusText)
664
+ error = _error.statusText
665
+ }
666
+ else {
667
+ if (error?.name === 'AbortError')
668
+ return
669
+ if (error?.message === 'Failed to fetch')
670
+ error = t`Looks like the Mockaton server is not running` // TODO clear Error if comes back in ui-sync
671
+ else
672
+ error = error || t`Unexpected Error`
673
+ }
674
+ showErrorToast(error)
675
+ console.error(_error)
759
676
  }
760
677
 
761
678
  function showErrorToast(msg) {
@@ -801,17 +718,17 @@ function initRealTimeUpdates() {
801
718
  let oldSyncVersion = -1
802
719
  let controller = new AbortController()
803
720
 
804
- poll()
721
+ longPoll()
805
722
  document.addEventListener('visibilitychange', () => {
806
723
  if (document.hidden) {
807
724
  controller.abort('_hidden_tab_')
808
725
  controller = new AbortController()
809
726
  }
810
727
  else
811
- poll()
728
+ longPoll()
812
729
  })
813
730
 
814
- async function poll() {
731
+ async function longPoll() {
815
732
  try {
816
733
  const response = await store.getSyncVersion(oldSyncVersion, controller.signal)
817
734
  if (response.ok) {
@@ -820,16 +737,16 @@ function initRealTimeUpdates() {
820
737
  if (oldSyncVersion !== syncVersion) { // because it could be < or >
821
738
  oldSyncVersion = syncVersion
822
739
  if (!skipUpdate)
823
- store.fetchState().catch(onError)
740
+ store.fetchState()
824
741
  }
825
- poll()
742
+ longPoll()
826
743
  }
827
744
  else
828
745
  throw response.status
829
746
  }
830
747
  catch (error) {
831
748
  if (error !== '_hidden_tab_')
832
- setTimeout(poll, 3000)
749
+ setTimeout(longPoll, 3000)
833
750
  }
834
751
  }
835
752
  }
@@ -969,8 +886,3 @@ function SyntaxXML(xml) {
969
886
  SyntaxXML.regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
970
887
  // Capture groups order: [tagPunc, tagName, attrName, attrVal]
971
888
 
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,15 +1,14 @@
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
 
@@ -31,20 +30,18 @@ export const store = {
31
30
 
32
31
  getSyncVersion: mockaton.getSyncVersion,
33
32
 
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
- })
33
+ async fetchState() {
34
+ try {
35
+ const response = await mockaton.getState()
36
+ if (!response.ok) throw response
37
+ Object.assign(store, await response.json())
38
+ store.render()
39
+ }
40
+ catch (error) { store.onError(error) }
44
41
  },
45
42
 
46
43
 
47
- leftSideWidth: window.innerWidth / 2,
44
+ leftSideWidth: undefined,
48
45
 
49
46
  groupByMethod: initPreference('groupByMethod'),
50
47
  toggleGroupByMethod() {
@@ -54,7 +51,6 @@ export const store = {
54
51
  },
55
52
 
56
53
 
57
-
58
54
  chosenLink: {
59
55
  method: '',
60
56
  urlMask: ''
@@ -67,37 +63,60 @@ export const store = {
67
63
  store.chosenLink = { method, urlMask }
68
64
  },
69
65
 
70
- reset() {
71
- store.setChosenLink('', '')
72
- mockaton.reset()
73
- .then(store.fetchState)
66
+ async reset() {
67
+ try {
68
+ const response = await mockaton.reset()
69
+ if (!response.ok) throw response
70
+ store.setChosenLink('', '')
71
+ await store.fetchState()
72
+ }
73
+ catch (error) { store.onError(error) }
74
74
  },
75
75
 
76
- bulkSelectByComment(value) {
77
- mockaton.bulkSelectByComment(value)
78
- .then(store.fetchState)
76
+ async bulkSelectByComment(value) {
77
+ try {
78
+ const response = await mockaton.bulkSelectByComment(value)
79
+ if (!response.ok) throw response
80
+ await store.fetchState()
81
+ }
82
+ catch (error) { store.onError(error) }
79
83
  },
80
84
 
81
-
82
- setGlobalDelay(value) {
83
- store.delay = value
84
- mockaton.setGlobalDelay(value)
85
+ async setGlobalDelay(value) {
86
+ try {
87
+ const response = await mockaton.setGlobalDelay(value)
88
+ if (!response.ok) throw response
89
+ store.delay = value
90
+ }
91
+ catch (error) { store.onError(error) }
85
92
  },
86
93
 
87
- selectCookie(name) {
88
- store.cookies = store.cookies.map(([n]) => [n, n === name])
89
- mockaton.selectCookie(name)
94
+ async selectCookie(name) {
95
+ try {
96
+ const response = await mockaton.selectCookie(name)
97
+ if (!response.ok) throw response
98
+ store.cookies = await response.json()
99
+ }
100
+ catch (error) { store.onError(error) }
90
101
  },
91
102
 
92
- setProxyFallback(value) {
93
- store.proxyFallback = value
94
- mockaton.setProxyFallback(value)
95
- .then(store.render)
103
+ async setProxyFallback(value) {
104
+ try {
105
+ const response = await mockaton.setProxyFallback(value)
106
+ if (!response.ok) throw response
107
+ store.proxyFallback = value
108
+ store.render()
109
+ }
110
+ catch (error) { store.onError(error) }
96
111
  },
97
112
 
98
- setCollectProxied(checked) {
99
- store.collectProxied = checked
100
- mockaton.setCollectProxied(checked)
113
+ async setCollectProxied(checked) {
114
+ try {
115
+ const response = await mockaton.setCollectProxied(checked)
116
+ if (!response.ok) throw response
117
+ store.collectProxied = checked
118
+ }
119
+ catch (error) { store.onError(error) }
101
120
  },
102
121
 
103
122
 
@@ -105,62 +124,103 @@ export const store = {
105
124
  return store.brokersByMethod[method]?.[urlMask]
106
125
  },
107
126
 
108
- brokersByMethodAsArray(targetMethod = '*') {
127
+ _dittoCache: new Map(),
128
+
129
+ dittoedUrlFor(method, urlMask) {
130
+ return store._dittoCache.get(method + '::' + urlMask)
131
+ },
132
+
133
+ brokersAsRowsByMethod(method) {
134
+ const rows = store._brokersAsArray(method)
135
+ const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
136
+ for (let i = 0; i < rows.length; i++) {
137
+ const r = rows[i]
138
+ r.setUrlMaskDittoed(urlMasksDittoed[i])
139
+ store._dittoCache.set(r.method + '::' + r.urlMask, r.urlMaskDittoed)
140
+ }
141
+ return rows
142
+ },
143
+
144
+ _brokersAsArray(byMethod = '*') {
109
145
  const rows = []
110
146
  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 })
147
+ if (byMethod === '*' || byMethod === method)
148
+ for (const broker of Object.values(brokers))
149
+ rows.push(new BrokerRowModel(broker, store.canProxy))
114
150
  return rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
115
151
  },
116
152
 
153
+ brokerAsRow(method, urlMask) {
154
+ const row = new BrokerRowModel(store.brokerFor(method, urlMask), store.canProxy)
155
+ row.setUrlMaskDittoed(store.dittoedUrlFor(method, urlMask))
156
+ return row
157
+ },
158
+
117
159
  previewLink(method, urlMask) {
118
160
  store.setChosenLink(method, urlMask)
119
161
  store.renderRow(method, urlMask)
120
162
  },
121
163
 
122
- selectFile(file) {
123
- mockaton.select(file).then(async response => {
164
+ async selectFile(file) {
165
+ try {
166
+ const response = await mockaton.select(file)
167
+ if (!response.ok) throw response
124
168
  const { method, urlMask } = parseFilename(file)
125
169
  store.brokerFor(method, urlMask).currentMock = await response.json()
126
170
  store.setChosenLink(method, urlMask)
127
171
  store.renderRow(method, urlMask)
128
- })
172
+ }
173
+ catch (error) { store.onError(error) }
129
174
  },
130
175
 
131
- toggle500(method, urlMask) {
132
- mockaton.toggle500(method, urlMask).then(async response => {
176
+ async toggle500(method, urlMask) {
177
+ try {
178
+ const response = await mockaton.toggle500(method, urlMask)
179
+ if (!response.ok) throw response
133
180
  store.brokerFor(method, urlMask).currentMock = await response.json()
134
181
  store.setChosenLink(method, urlMask)
135
182
  store.renderRow(method, urlMask)
136
- })
183
+ }
184
+ catch (error) { store.onError(error) }
137
185
  },
138
186
 
139
- setProxied(method, urlMask, checked) {
140
- mockaton.setRouteIsProxied(method, urlMask, checked).then(() => {
187
+ async setProxied(method, urlMask, checked) {
188
+ try {
189
+ const response = await mockaton.setRouteIsProxied(method, urlMask, checked)
190
+ if (!response.ok) throw response
141
191
  store.brokerFor(method, urlMask).currentMock.proxied = checked
142
192
  store.setChosenLink(method, urlMask)
143
193
  store.renderRow(method, urlMask)
144
- })
194
+ }
195
+ catch (error) { store.onError(error) }
145
196
  },
146
197
 
147
- setDelayed(method, urlMask, checked) {
148
- mockaton.setRouteIsDelayed(method, urlMask, checked).then(() => {
198
+ async setDelayed(method, urlMask, checked) {
199
+ try {
200
+ const response = await mockaton.setRouteIsDelayed(method, urlMask, checked)
201
+ if (!response.ok) throw response
149
202
  store.brokerFor(method, urlMask).currentMock.delayed = checked
150
- })
203
+ }
204
+ catch (error) { store.onError(error) }
151
205
  },
152
206
 
153
207
 
154
- staticBrokerFor(route) { return store.staticBrokers[route] },
155
-
156
- setDelayedStatic(route, checked) {
157
- store.staticBrokerFor(route).delayed = checked
158
- mockaton.setStaticRouteIsDelayed(route, checked)
208
+ async setDelayedStatic(route, checked) {
209
+ try {
210
+ const response = await mockaton.setStaticRouteIsDelayed(route, checked)
211
+ if (!response.ok) throw response
212
+ store.staticBrokers[route].delayed = checked
213
+ }
214
+ catch (error) { store.onError(error) }
159
215
  },
160
216
 
161
- setStaticRouteStatus(route, status) {
162
- store.staticBrokerFor(route).status = status
163
- mockaton.setStaticRouteStatus(route, status)
217
+ async setStaticRouteStatus(route, status) {
218
+ try {
219
+ const response = await mockaton.setStaticRouteStatus(route, status)
220
+ if (!response.ok) throw response
221
+ store.staticBrokers[route].status = status
222
+ }
223
+ catch (error) { store.onError(error) }
164
224
  }
165
225
  }
166
226
 
@@ -251,6 +311,156 @@ dittoSplitPaths.test = function () {
251
311
  }
252
312
  deferred(dittoSplitPaths.test)
253
313
 
314
+
315
+
316
+ export class BrokerRowModel {
317
+ opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
318
+ method = ''
319
+ urlMask = ''
320
+ urlMaskDittoed = ['', '']
321
+ #broker = /** @type {ClientMockBroker} */ {}
322
+ #canProxy = false
323
+
324
+ /**
325
+ * @param {ClientMockBroker} broker
326
+ * @param {boolean} canProxy
327
+ */
328
+ constructor(broker, canProxy) {
329
+ this.#broker = broker
330
+ this.#canProxy = canProxy
331
+ const { method, urlMask } = parseFilename(broker.currentMock.file)
332
+ this.method = method
333
+ this.urlMask = urlMask
334
+ this.opts = this.#makeOptions()
335
+ }
336
+
337
+ setUrlMaskDittoed(urlMaskDittoed) {
338
+ this.urlMaskDittoed = urlMaskDittoed
339
+ }
340
+
341
+ get delayed() {
342
+ return this.#broker.currentMock.delayed
343
+ }
344
+ get proxied() {
345
+ return this.#canProxy && this.#broker.currentMock.proxied
346
+ }
347
+ get selectedIdx() {
348
+ return this.opts.findIndex(([, , selected]) => selected)
349
+ }
350
+ get selectedFile() {
351
+ return this.#broker.currentMock.file
352
+ }
353
+ get selectedFileIs4xx() {
354
+ const { status } = parseFilename(this.selectedFile)
355
+ return status >= 400 && status < 500
356
+ }
357
+ get selectedFileIs500() {
358
+ const { status } = parseFilename(this.selectedFile)
359
+ return status === 500
360
+ }
361
+
362
+ #makeOptions() {
363
+ const proxied = this.proxied
364
+ const selectedIs500 = this.selectedFileIs500
365
+
366
+ const opts = this.#broker.mocks
367
+ .filter(f => selectedIs500 || !f.includes(AUTO_500_COMMENT))
368
+ .map(f => [
369
+ f,
370
+ this.#optionLabelFor(f),
371
+ !proxied && f === this.selectedFile
372
+ ])
373
+
374
+ if (proxied)
375
+ opts.push([
376
+ '__PROXIED__',
377
+ t`Proxied`,
378
+ true
379
+ ])
380
+
381
+ return opts
382
+ }
383
+
384
+ #optionLabelFor(file) {
385
+ const { status, ext } = parseFilename(file)
386
+ const comments = extractComments(file)
387
+ const isAutogen500 = comments.includes(AUTO_500_COMMENT)
388
+ return [
389
+ isAutogen500 ? '' : status,
390
+ ext === 'empty' || ext === 'unknown' ? '' : ext,
391
+ isAutogen500 ? t`Auto500` : comments.join(' ')
392
+ ].filter(Boolean).join(' ')
393
+ }
394
+ }
395
+
396
+ const TestBrokerRowModelOptions = {
397
+ 'ignores autogen500 when unselected'() {
398
+ const broker = {
399
+ currentMock: { file: 'api/other' },
400
+ mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
401
+ }
402
+ const row = new BrokerRowModel(broker, false)
403
+ console.assert(row.opts.length === 0)
404
+ },
405
+
406
+ 'keeps non-autogen500 when unselected'() {
407
+ const broker = {
408
+ currentMock: { file: 'api/other' },
409
+ mocks: [`api/user.GET.500.txt`]
410
+ }
411
+ const row = new BrokerRowModel(broker, false)
412
+ console.assert(row.opts.length === 1)
413
+ console.assert(row.opts[0][1] === t`500 txt`)
414
+ },
415
+
416
+ 'renames autogen file to Auto500'() {
417
+ const broker = {
418
+ currentMock: { file: `api/user${AUTO_500_COMMENT}.GET.500.empty` },
419
+ mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
420
+ }
421
+ const row = new BrokerRowModel(broker, false)
422
+ console.assert(row.opts.length === 1)
423
+ console.assert(row.opts[0][1] === t`Auto500`)
424
+ },
425
+
426
+ 'filename has extension except when empty or unknown'() {
427
+ const broker = {
428
+ currentMock: { file: `api/other` },
429
+ mocks: [
430
+ `api/user0.GET.200.empty`,
431
+ `api/user1.GET.200.unknown`,
432
+ `api/user2.GET.200.json`,
433
+ `api/user3(another json).GET.200.json`,
434
+ ]
435
+ }
436
+ const row = new BrokerRowModel(broker, false)
437
+ // Think about, in cases like this, the only option the user has
438
+ // for discerning empty and unknown is on the Previewer Title
439
+ console.assert(deepEqual(row.opts.map(([, n]) => n), [
440
+ '200',
441
+ '200',
442
+ '200 json',
443
+ '200 json (another json)',
444
+ ]))
445
+ },
446
+
447
+ 'appends "Proxied" label iff current is proxied'() {
448
+ const broker = {
449
+ currentMock: {
450
+ file: 'api/foo',
451
+ proxied: true
452
+ },
453
+ mocks: [`api/foo.GET.200.json`]
454
+ }
455
+ const row = new BrokerRowModel(broker, true)
456
+ console.assert(deepEqual(row.opts.map(([, n, selected]) => [n, selected]), [
457
+ ['200 json', false],
458
+ [t`Proxied`, true]
459
+ ]))
460
+ }
461
+ }
462
+ deferred(() => Object.values(TestBrokerRowModelOptions).forEach(t => t()))
463
+
254
464
  function deepEqual(a, b) {
255
465
  return JSON.stringify(a) === JSON.stringify(b)
256
466
  }