mockaton 10.4.2 → 10.5.0

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.4.2",
5
+ "version": "10.5.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Dashboard.js CHANGED
@@ -3,40 +3,6 @@ import { parseFilename, extractComments } from './Filename.js'
3
3
  import { Commander } from './ApiCommander.js'
4
4
 
5
5
 
6
- const Strings = {
7
- auto500: 'Auto500',
8
- bulk_select: 'Bulk Select',
9
- bulk_select_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
10
- click_link_to_preview: 'Click a link to preview it',
11
- cookie: 'Cookie',
12
- cookie_disabled_title: 'No cookies specified in config.cookies',
13
- delay: 'Delay',
14
- delay_ms: 'Delay (ms)',
15
- empty_response_body: '/* Empty Response Body */',
16
- error_server_down: 'Looks like the Mockaton server is not running',
17
- error_unexpected_error: 'Unexpected Error',
18
- fallback_server: 'Fallback',
19
- fallback_server_error: '⛔ Fallback Backend Error',
20
- fallback_server_placeholder: 'Type backend address',
21
- fetching: 'Fetching…',
22
- got: 'Got',
23
- group_by_method: 'Group by Method',
24
- internal_server_error: 'Internal Server Error',
25
- mock_selector: 'Mock Selector',
26
- no_mocks_found: 'No mocks found',
27
- none: 'None',
28
- not_found: 'Not Found',
29
- pick_comment: 'Pick Comment…',
30
- preview: 'Preview',
31
- proxied: 'Proxied',
32
- proxy_toggler: 'Proxy Toggler',
33
- reset: 'Reset',
34
- save_proxied: 'Save Mocks',
35
- settings: 'Settings',
36
- static_get: 'Static GET',
37
- title: 'Mockaton'
38
- }
39
-
40
6
  const CSS = {
41
7
  BulkSelector: null,
42
8
  CookieSelector: null,
@@ -72,26 +38,19 @@ const CSS = {
72
38
  status4xx: null,
73
39
 
74
40
  json: null,
75
- syntaxKey: null,
76
- syntaxStr: null,
77
- syntaxVal: null,
78
-
79
41
  syntaxAttr: null,
80
42
  syntaxAttrVal: null,
43
+ syntaxKey: null,
44
+ syntaxPunc: null,
45
+ syntaxStr: null,
81
46
  syntaxTag: null,
82
- syntaxPunc: null
47
+ syntaxVal: null
83
48
  }
84
49
  for (const k of Object.keys(CSS))
85
50
  CSS[k] = k
86
51
 
87
52
 
88
- /** @type {State & {
89
- * canProxy: boolean
90
- * groupByMethod: boolean
91
- * toggleGroupByMethod: () => void
92
- * leftSideWidth?: number
93
- * }} */
94
- const state = {
53
+ const state = /** @type {State} */ {
95
54
  brokersByMethod: {},
96
55
  staticBrokers: {},
97
56
  cookies: [],
@@ -101,18 +60,20 @@ const state = {
101
60
  collectProxied: false,
102
61
  proxyFallback: '',
103
62
  get canProxy() {
104
- return Boolean(this.proxyFallback)
63
+ return Boolean(state.proxyFallback)
105
64
  },
106
65
 
107
- groupByMethod: localStorage.getItem('groupByMethod') !== 'false',
66
+ groupByMethod: initPreference('groupByMethod'),
108
67
  toggleGroupByMethod() {
109
- this.groupByMethod = !this.groupByMethod
110
- localStorage.setItem('groupByMethod', String(this.groupByMethod))
68
+ state.groupByMethod = !state.groupByMethod
69
+ togglePreference('groupByMethod', state.groupByMethod)
70
+ updateState()
111
71
  },
112
72
 
113
73
  leftSideWidth: undefined
114
74
  }
115
75
 
76
+
116
77
  const mockaton = new Commander(location.origin)
117
78
  updateState()
118
79
  initLongPoll()
@@ -133,6 +94,8 @@ async function updateState() {
133
94
  const r = createElement
134
95
  const s = createSvgElement
135
96
 
97
+ const t = translation => translation[0]
98
+
136
99
  const leftSideRef = useRef()
137
100
 
138
101
  function App() {
@@ -159,7 +122,7 @@ function Header() {
159
122
  return (
160
123
  r('header', null,
161
124
  r('img', {
162
- alt: Strings.title,
125
+ alt: t`Mockaton`,
163
126
  src: 'mockaton/Logo.svg',
164
127
  width: 160
165
128
  }),
@@ -173,47 +136,38 @@ function Header() {
173
136
  }
174
137
 
175
138
  function SettingsMenu() {
176
- const id = '_SettingsMenu_'
139
+ const { groupByMethod, toggleGroupByMethod } = state
140
+
141
+ const id = '_settings_menu_'
142
+ return (
143
+ r('button', {
144
+ title: t`Settings`,
145
+ popovertarget: id,
146
+ className: CSS.MenuTrigger
147
+ },
148
+ SettingsIcon(),
177
149
 
178
- function MenuContent() {
179
- return (
180
150
  r('menu', {
181
151
  id,
152
+ deferred: true,
182
153
  popover: '',
183
- className: CSS.SettingsMenu,
184
- onToggle(event) {
185
- if (event.newState === 'closed')
186
- this.parentNode.removeChild(this)
187
- }
154
+ className: CSS.SettingsMenu
188
155
  },
156
+
189
157
  r('label', className(CSS.GroupByMethod),
190
158
  r('input', {
191
159
  type: 'checkbox',
192
- checked: state.groupByMethod,
193
- onChange() {
194
- state.toggleGroupByMethod()
195
- updateState()
196
- }
160
+ checked: groupByMethod,
161
+ autofocus: true,
162
+ onChange: toggleGroupByMethod
197
163
  }),
198
- r('span', null, Strings.group_by_method)),
164
+ r('span', null, t`Group by Method`)),
199
165
 
200
166
  r('a', {
201
167
  href: 'https://github.com/ericfortis/mockaton',
202
168
  target: '_blank',
203
169
  rel: 'noopener noreferrer'
204
- }, 'Documentation')))
205
- }
206
-
207
- return (
208
- r('button', {
209
- title: Strings.settings,
210
- onClick() {
211
- if (!this.querySelector('menu'))
212
- this.appendChild(MenuContent())
213
- },
214
- className: CSS.MenuTrigger,
215
- popovertarget: id
216
- }, SettingsIcon()))
170
+ }, t`Documentation`))))
217
171
  }
218
172
 
219
173
  function CookieSelector() {
@@ -224,14 +178,14 @@ function CookieSelector() {
224
178
  .catch(onError)
225
179
  }
226
180
  const disabled = cookies.length <= 1
227
- const list = cookies.length ? cookies : [[Strings.none, true]]
181
+ const list = cookies.length ? cookies : [[t`None`, true]]
228
182
  return (
229
183
  r('label', className(CSS.Field, CSS.CookieSelector),
230
- r('span', null, Strings.cookie),
184
+ r('span', null, t`Cookie`),
231
185
  r('select', {
232
186
  autocomplete: 'off',
233
187
  disabled,
234
- title: disabled ? Strings.cookie_disabled_title : '',
188
+ title: disabled ? t`No cookies specified in config.cookies` : '',
235
189
  onChange
236
190
  }, list.map(([value, selected]) =>
237
191
  r('option', { value, selected }, value)))))
@@ -241,7 +195,7 @@ function BulkSelector() {
241
195
  const { comments } = state
242
196
  // UX wise this should be a menu instead of this `select`.
243
197
  // But this way is easier to implement, with a few hacks.
244
- const firstOption = Strings.pick_comment
198
+ const firstOption = t`Pick Comment…`
245
199
  function onChange() {
246
200
  const value = this.value
247
201
  this.value = firstOption // Hack
@@ -254,12 +208,12 @@ function BulkSelector() {
254
208
  const disabled = !comments.length
255
209
  return (
256
210
  r('label', className(CSS.Field),
257
- r('span', null, Strings.bulk_select),
211
+ r('span', null, t`Bulk Select`),
258
212
  r('select', {
259
213
  className: CSS.BulkSelector,
260
214
  autocomplete: 'off',
261
215
  disabled,
262
- title: disabled ? Strings.bulk_select_disabled_title : '',
216
+ title: disabled ? t`No mock files have comments which are anything within parentheses on the filename.` : '',
263
217
  onChange
264
218
  },
265
219
  r('option', { value: firstOption }, firstOption),
@@ -267,7 +221,7 @@ function BulkSelector() {
267
221
  comments.map(value =>
268
222
  r('option', { value }, value)),
269
223
  r('hr'),
270
- r('option', { value: AUTOGENERATED_500_COMMENT }, Strings.auto500)
224
+ r('option', { value: AUTOGENERATED_500_COMMENT }, t`Auto500`)
271
225
  )))
272
226
  }
273
227
 
@@ -287,7 +241,7 @@ function GlobalDelayField() {
287
241
  }
288
242
  return (
289
243
  r('label', className(CSS.Field, CSS.GlobalDelayField),
290
- r('span', null, Strings.delay_ms),
244
+ r('span', null, t`Delay (ms)`),
291
245
  r('input', {
292
246
  type: 'number',
293
247
  min: 0,
@@ -317,11 +271,11 @@ function ProxyFallbackField() {
317
271
  return (
318
272
  r('div', className(CSS.Field, CSS.FallbackBackend),
319
273
  r('label', null,
320
- r('span', null, Strings.fallback_server),
274
+ r('span', null, t`Fallback`),
321
275
  r('input', {
322
276
  type: 'url',
323
277
  autocomplete: 'none',
324
- placeholder: Strings.fallback_server_placeholder,
278
+ placeholder: t`Type backend address`,
325
279
  value: proxyFallback,
326
280
  onChange
327
281
  })),
@@ -343,7 +297,7 @@ function SaveProxiedCheckbox() {
343
297
  checked: collectProxied,
344
298
  onChange
345
299
  }),
346
- r('span', null, Strings.save_proxied)))
300
+ r('span', null, t`Save Mocks`)))
347
301
  }
348
302
 
349
303
  function ResetButton() {
@@ -358,7 +312,7 @@ function ResetButton() {
358
312
  r('button', {
359
313
  className: CSS.ResetButton,
360
314
  onClick
361
- }, Strings.reset))
315
+ }, t`Reset`))
362
316
  }
363
317
 
364
318
 
@@ -371,7 +325,7 @@ function MockList() {
371
325
  if (!Object.keys(brokersByMethod).length)
372
326
  return (
373
327
  r('div', className(CSS.empty),
374
- Strings.no_mocks_found))
328
+ t`No mocks found`))
375
329
 
376
330
  if (groupByMethod)
377
331
  return Object.keys(brokersByMethod).map((method) => Fragment(
@@ -439,6 +393,8 @@ function PreviewLink(method, urlMask, urlMaskDittoed) {
439
393
  : tail))
440
394
  }
441
395
 
396
+ const STR_PROXIED = t`Proxied`
397
+
442
398
  /** @param {ClientMockBroker} broker */
443
399
  function MockSelector(broker) {
444
400
  function onChange() {
@@ -456,20 +412,20 @@ function MockSelector(broker) {
456
412
  status === 500 ||
457
413
  !item.includes(AUTOGENERATED_500_COMMENT))
458
414
  if (!selected) {
459
- selected = Strings.proxied
415
+ selected = STR_PROXIED
460
416
  files.push(selected)
461
417
  }
462
418
 
463
419
  function nameFor(file) {
464
- if (file === Strings.proxied)
465
- return Strings.proxied
420
+ if (file === STR_PROXIED)
421
+ return STR_PROXIED
466
422
  const { status, ext } = parseFilename(file)
467
423
  const comments = extractComments(file)
468
424
  const isAutogen500 = comments.includes(AUTOGENERATED_500_COMMENT)
469
425
  return [
470
426
  isAutogen500 ? '' : status,
471
427
  ext === 'empty' || ext === 'unknown' ? '' : ext,
472
- isAutogen500 ? Strings.auto500 : comments.join(' ')
428
+ isAutogen500 ? t`Auto500` : comments.join(' ')
473
429
  ].filter(Boolean).join(' ')
474
430
  }
475
431
 
@@ -477,7 +433,7 @@ function MockSelector(broker) {
477
433
  r('select', {
478
434
  onChange,
479
435
  autocomplete: 'off',
480
- 'aria-label': Strings.mock_selector,
436
+ 'aria-label': t`Mock Selector`,
481
437
  disabled: files.length <= 1,
482
438
  ...className(
483
439
  CSS.MockSelector,
@@ -518,7 +474,7 @@ function InternalServerErrorToggler(broker) {
518
474
  return (
519
475
  r('label', {
520
476
  className: CSS.InternalServerErrorToggler,
521
- title: Strings.internal_server_error
477
+ title: t`Internal Server Error`
522
478
  },
523
479
  r('input', {
524
480
  type: 'checkbox',
@@ -526,7 +482,7 @@ function InternalServerErrorToggler(broker) {
526
482
  checked: parseFilename(broker.currentMock.file).status === 500,
527
483
  onChange
528
484
  }),
529
- r('span', null, '500')))
485
+ r('span', null, t`500`)))
530
486
  }
531
487
 
532
488
  /** @param {ClientMockBroker} broker */
@@ -542,7 +498,7 @@ function ProxyToggler(broker) {
542
498
  return (
543
499
  r('label', {
544
500
  className: CSS.ProxyToggler,
545
- title: Strings.proxy_toggler
501
+ title: t`Proxy Toggler`
546
502
  },
547
503
  r('input', {
548
504
  type: 'checkbox',
@@ -567,10 +523,10 @@ function StaticFilesList() {
567
523
  Fragment(
568
524
  r('tr', null,
569
525
  r('th', { colspan: (2 + Number(!groupByMethod)) + Number(canProxy) }),
570
- r('th', null, Strings.static_get)),
526
+ r('th', null, t`Static GET`)),
571
527
  Object.values(staticBrokers).map((broker, i) =>
572
528
  r('tr', null,
573
- canProxy && r('td', null, ProxyStaticToggler()),
529
+ canProxy && r('td'),
574
530
  r('td', null, DelayStaticRouteToggler(broker)),
575
531
  r('td', null, NotFoundToggler(broker)),
576
532
  !groupByMethod && r('td', className(CSS.Method), 'GET'),
@@ -605,31 +561,14 @@ function NotFoundToggler(broker) {
605
561
  return (
606
562
  r('label', {
607
563
  className: CSS.NotFoundToggler,
608
- title: Strings.not_found
564
+ title: t`Not Found`
609
565
  },
610
566
  r('input', {
611
567
  type: 'checkbox',
612
568
  checked: broker.status === 404,
613
569
  onChange
614
570
  }),
615
- r('span', null, '404')))
616
- }
617
-
618
- function ProxyStaticToggler() { // TODO
619
- function onChange() {
620
- }
621
- return (
622
- r('label', {
623
- style: { visibility: 'hidden' },
624
- className: CSS.ProxyToggler,
625
- title: Strings.proxy_toggler
626
- },
627
- r('input', {
628
- type: 'checkbox',
629
- disabled: true,
630
- onChange
631
- }),
632
- CloudIcon()))
571
+ r('span', null, t`404`)))
633
572
  }
634
573
 
635
574
 
@@ -652,7 +591,7 @@ function ClickDragToggler({ checked, commit }) {
652
591
  return (
653
592
  r('label', {
654
593
  className: CSS.DelayToggler,
655
- title: Strings.delay
594
+ title: t`Delay`
656
595
  },
657
596
  r('input', {
658
597
  type: 'checkbox',
@@ -710,9 +649,9 @@ const payloadViewerRef = useRef()
710
649
  function PayloadViewer() {
711
650
  return (
712
651
  r('div', className(CSS.PayloadViewer),
713
- r('h2', { ref: payloadViewerTitleRef }, Strings.preview),
652
+ r('h2', { ref: payloadViewerTitleRef }, t`Preview`),
714
653
  r('pre', null,
715
- r('code', { ref: payloadViewerRef }, Strings.click_link_to_preview))))
654
+ r('code', { ref: payloadViewerRef }, t`Click a link to preview it`))))
716
655
  }
717
656
 
718
657
  function PayloadViewerProgressBar() {
@@ -737,8 +676,8 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
737
676
  return (
738
677
  r('span', null,
739
678
  gatewayIsBad
740
- ? r('span', className(CSS.red), Strings.fallback_server_error + ' ')
741
- : r('span', null, Strings.got + ' '),
679
+ ? r('span', className(CSS.red), t`⛔ Fallback Backend Error` + ' ')
680
+ : r('span', null, t`Got` + ' '),
742
681
  r('abbr', { title: statusText }, status),
743
682
  ' ' + mime))
744
683
  }
@@ -748,7 +687,7 @@ async function previewMock(method, urlMask, href) {
748
687
  previewMock.controller = new AbortController
749
688
 
750
689
  const spinnerTimer = setTimeout(() => {
751
- payloadViewerTitleRef.current.replaceChildren(Strings.fetching)
690
+ payloadViewerTitleRef.current.replaceChildren(t`Fetching…`)
752
691
  payloadViewerRef.current.replaceChildren(PayloadViewerProgressBar())
753
692
  }, 80)
754
693
 
@@ -766,11 +705,12 @@ async function previewMock(method, urlMask, href) {
766
705
  }
767
706
  }
768
707
 
708
+
769
709
  async function updatePayloadViewer(method, urlMask, response) {
770
710
  const mime = response.headers.get('content-type') || ''
771
711
 
772
712
  const file = mockSelectorFor(method, urlMask).value
773
- if (file === Strings.proxied)
713
+ if (file === STR_PROXIED)
774
714
  payloadViewerTitleRef.current.replaceChildren(PayloadViewerTitleWhenProxied({
775
715
  status: response.status,
776
716
  statusText: response.statusText,
@@ -790,7 +730,7 @@ async function updatePayloadViewer(method, urlMask, response) {
790
730
  }))
791
731
  }
792
732
  else {
793
- const body = await response.text() || Strings.empty_response_body
733
+ const body = await response.text() || t`/* Empty Response Body */`
794
734
  if (mime === 'application/json')
795
735
  payloadViewerRef.current.replaceChildren(r('span', className(CSS.json), syntaxJSON(body)))
796
736
  else if (isXML(mime))
@@ -834,9 +774,9 @@ function onError(error) {
834
774
  if (error?.name === 'AbortError')
835
775
  return
836
776
  if (error?.message === 'Failed to fetch')
837
- showErrorToast(Strings.error_server_down)
777
+ showErrorToast(t`Looks like the Mockaton server is not running`)
838
778
  else
839
- showErrorToast(error || Strings.error_unexpected_error)
779
+ showErrorToast(error || t`Unexpected Error`)
840
780
  console.error(error)
841
781
  }
842
782
 
@@ -921,6 +861,14 @@ function className(...args) {
921
861
 
922
862
 
923
863
  function createElement(tag, props, ...children) {
864
+ if (props?.deferred) {
865
+ delete props.deferred
866
+ const placeholder = document.createComment('')
867
+ deferred(() =>
868
+ placeholder.replaceWith(createElement(tag, props, ...children)))
869
+ return placeholder
870
+ }
871
+
924
872
  const node = document.createElement(tag)
925
873
  for (const [k, v] of Object.entries(props || {}))
926
874
  if (k === 'ref') v.current = node
@@ -954,6 +902,43 @@ function Fragment(...args) {
954
902
  return frag
955
903
  }
956
904
 
905
+ function deferred(cb) {
906
+ return window.requestIdleCallback
907
+ ? requestIdleCallback(cb)
908
+ : setTimeout(cb, 100) // Safari
909
+ }
910
+
911
+
912
+ // When false, the URL will be updated with param=false
913
+ function initPreference(param) {
914
+ const qs = new URLSearchParams(location.search)
915
+ if (!qs.has(param)) {
916
+ const group = localStorage.getItem(param) !== 'false'
917
+ if (!group) {
918
+ const url = new URL(location.href)
919
+ url.searchParams.set(param, false)
920
+ history.replaceState(null, '', url)
921
+ }
922
+ return group
923
+ }
924
+ return qs.get(param) !== 'false'
925
+ }
926
+
927
+ // When false, the URL and localStorage will have param=false
928
+ function togglePreference(param, nextVal) {
929
+ if (nextVal)
930
+ localStorage.removeItem(param)
931
+ else
932
+ localStorage.setItem(param, nextVal)
933
+
934
+ const url = new URL(location.href)
935
+ if (nextVal)
936
+ url.searchParams.delete(param)
937
+ else
938
+ url.searchParams.set(param, false)
939
+ history.replaceState(null, '', url)
940
+ }
941
+
957
942
 
958
943
  /**
959
944
  * Think of this as a way of printing a directory tree in which
package/src/Mockaton.js CHANGED
@@ -52,8 +52,9 @@ async function onRequest(req, response) {
52
52
  sendBadRequest(response)
53
53
  return
54
54
  }
55
-
55
+
56
56
  try {
57
+ const route = new URL(url, 'http://_').pathname
57
58
  const { method } = req
58
59
 
59
60
  if (config.corsAllowed)
@@ -61,11 +62,11 @@ async function onRequest(req, response) {
61
62
 
62
63
  if (isPreflight(req))
63
64
  sendNoContent(response)
64
- else if (method === 'PATCH' && apiPatchRequests.has(url))
65
- await apiPatchRequests.get(url)(req, response)
66
- else if (method === 'GET' && apiGetRequests.has(url))
67
- apiGetRequests.get(url)(req, response)
68
- else if (method === 'GET' && staticCollection.brokerByRoute(url))
65
+ else if (method === 'PATCH' && apiPatchRequests.has(route))
66
+ await apiPatchRequests.get(route)(req, response)
67
+ else if (method === 'GET' && apiGetRequests.has(route))
68
+ apiGetRequests.get(route)(req, response)
69
+ else if (method === 'GET' && staticCollection.brokerByRoute(route))
69
70
  await dispatchStatic(req, response)
70
71
  else
71
72
  await dispatchMock(req, response)