mockaton 10.5.4 → 10.6.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.5.4",
5
+ "version": "10.6.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Dashboard.css CHANGED
@@ -95,7 +95,7 @@ body {
95
95
  }
96
96
 
97
97
  select, a, input, button {
98
- &:focus {
98
+ &:focus-visible {
99
99
  outline: 2px solid var(--colorAccent);
100
100
  }
101
101
  }
@@ -294,7 +294,6 @@ header {
294
294
  fill: var(--colorAccent);
295
295
  }
296
296
  }
297
-
298
297
  }
299
298
 
300
299
  .SettingsMenu {
@@ -379,9 +378,9 @@ table {
379
378
  border-collapse: collapse;
380
379
 
381
380
  th {
382
- padding-bottom: 2px;
381
+ padding-bottom: 4px;
383
382
  padding-left: 4px;
384
- border-top: 20px solid transparent;
383
+ border-top: 24px solid transparent;
385
384
  text-align: left;
386
385
  }
387
386
 
@@ -422,6 +421,7 @@ table {
422
421
  }
423
422
  }
424
423
 
424
+
425
425
  .MockSelector {
426
426
  height: 24px;
427
427
  padding-right: 20px;
@@ -458,7 +458,7 @@ table {
458
458
  height: 22px;
459
459
  opacity: 0;
460
460
 
461
- &:focus {
461
+ &:focus-visible {
462
462
  outline: 0;
463
463
  & + svg {
464
464
  outline: 2px solid var(--colorAccent)
@@ -556,7 +556,7 @@ table {
556
556
  > input {
557
557
  appearance: none;
558
558
 
559
- &:focus {
559
+ &:focus-visible {
560
560
  outline: 0;
561
561
  & + span {
562
562
  outline: 2px solid var(--colorAccent)
@@ -587,15 +587,6 @@ table {
587
587
  }
588
588
 
589
589
 
590
- .StaticFileLink {
591
- display: inline-block;
592
- padding: 6px 0;
593
- margin-left: 4px;
594
- border-radius: var(--radius);
595
- color: var(--colorAccent);
596
- }
597
-
598
-
599
590
  .PayloadViewer {
600
591
  display: flex;
601
592
  height: 100%;
package/src/Dashboard.js CHANGED
@@ -26,7 +26,6 @@ const CSS = {
26
26
  Resizer: null,
27
27
  SaveProxiedCheckbox: null,
28
28
  SettingsMenu: null,
29
- StaticFileLink: null,
30
29
 
31
30
  chosen: null,
32
31
  dittoDir: null,
@@ -50,6 +49,13 @@ for (const k of Object.keys(CSS))
50
49
  CSS[k] = k
51
50
 
52
51
 
52
+ const FocusGroup = {
53
+ ProxyToggler: 0,
54
+ DelayToggler: 1,
55
+ StatusToggler: 2,
56
+ PreviewLink: 3
57
+ }
58
+
53
59
  const state = /** @type {State} */ {
54
60
  brokersByMethod: {},
55
61
  staticBrokers: {},
@@ -70,13 +76,22 @@ const state = /** @type {State} */ {
70
76
  updateState()
71
77
  },
72
78
 
73
- leftSideWidth: undefined
79
+ leftSideWidth: undefined,
80
+
81
+ chosenLink: { method: '', urlMask: '' },
82
+ clearChosenLink() {
83
+ state.chosenLink = { method: '', urlMask: '' }
84
+ },
85
+ setChosenLink(method, urlMask) {
86
+ state.chosenLink = { method, urlMask }
87
+ }
74
88
  }
75
89
 
76
90
 
77
91
  const mockaton = new Commander(location.origin)
78
92
  updateState()
79
- deferred(initLongPoll)
93
+ initLongPoll()
94
+ initKeyboardNavigation()
80
95
 
81
96
  async function updateState() {
82
97
  try {
@@ -85,6 +100,10 @@ async function updateState() {
85
100
  throw response.status
86
101
  Object.assign(state, await response.json())
87
102
  document.body.replaceChildren(...App())
103
+ findChosenLink()?.focus()
104
+ const { method, urlMask } = state.chosenLink
105
+ if (method && urlMask)
106
+ await previewMock(method, urlMask)
88
107
  }
89
108
  catch (error) {
90
109
  onError(error)
@@ -132,12 +151,10 @@ function Header() {
132
151
  CookieSelector(),
133
152
  ProxyFallbackField(),
134
153
  ResetButton(),
135
- SettingsMenu())))
154
+ SettingsMenuTrigger())))
136
155
  }
137
156
 
138
- function SettingsMenu() {
139
- const { groupByMethod, toggleGroupByMethod } = state
140
-
157
+ function SettingsMenuTrigger() {
141
158
  const id = '_settings_menu_'
142
159
  return (
143
160
  r('button', {
@@ -146,29 +163,35 @@ function SettingsMenu() {
146
163
  className: CSS.MenuTrigger
147
164
  },
148
165
  SettingsIcon(),
166
+ Defer(() => SettingsMenu(id))))
167
+ }
149
168
 
150
- r('menu', {
151
- id,
152
- popover: '',
153
- className: CSS.SettingsMenu
154
- },
155
-
156
- r('label', className(CSS.GroupByMethod),
157
- r('input', {
158
- type: 'checkbox',
159
- checked: groupByMethod,
160
- autofocus: true,
161
- onChange: toggleGroupByMethod
162
- }),
163
- r('span', null, t`Group by Method`)),
169
+ function SettingsMenu(id) {
170
+ const { groupByMethod, toggleGroupByMethod } = state
171
+ return (
172
+ r('menu', {
173
+ id,
174
+ popover: '',
175
+ className: CSS.SettingsMenu
176
+ },
164
177
 
165
- r('a', {
166
- href: 'https://github.com/ericfortis/mockaton',
167
- target: '_blank',
168
- rel: 'noopener noreferrer'
169
- }, t`Documentation`))))
178
+ r('label', className(CSS.GroupByMethod),
179
+ r('input', {
180
+ type: 'checkbox',
181
+ checked: groupByMethod,
182
+ // autofocus: true, // TODO
183
+ onChange: toggleGroupByMethod
184
+ }),
185
+ r('span', null, t`Group by Method`)),
186
+
187
+ r('a', {
188
+ href: 'https://github.com/ericfortis/mockaton',
189
+ target: '_blank',
190
+ rel: 'noopener noreferrer'
191
+ }, t`Documentation`)))
170
192
  }
171
193
 
194
+
172
195
  function CookieSelector() {
173
196
  const { cookies } = state
174
197
  function onChange() {
@@ -196,6 +219,7 @@ function BulkSelector() {
196
219
  // But this way is easier to implement, with a few hacks.
197
220
  const firstOption = t`Pick Comment…`
198
221
  function onChange() {
222
+ state.clearChosenLink()
199
223
  const value = this.value
200
224
  this.value = firstOption // Hack
201
225
  mockaton.bulkSelectByComment(value)
@@ -233,8 +257,10 @@ function GlobalDelayField() {
233
257
  .catch(onError)
234
258
  }
235
259
  function onWheel(event) {
236
- const val = this.valueAsNumber - event.deltaY * 10
237
- this.valueAsNumber = Math.max(val, 0)
260
+ if (event.deltaY > 0)
261
+ this.stepUp()
262
+ else
263
+ this.stepDown()
238
264
  clearTimeout(onWheel.timer)
239
265
  onWheel.timer = setTimeout(onChange.bind(this), 300)
240
266
  }
@@ -254,9 +280,9 @@ function GlobalDelayField() {
254
280
 
255
281
  function ProxyFallbackField() {
256
282
  const { proxyFallback } = state
283
+ const checkboxRef = useRef()
257
284
  function onChange() {
258
- const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
259
- saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
285
+ checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
260
286
 
261
287
  if (!this.validity.valid)
262
288
  this.reportValidity()
@@ -278,10 +304,10 @@ function ProxyFallbackField() {
278
304
  value: proxyFallback,
279
305
  onChange
280
306
  })),
281
- SaveProxiedCheckbox()))
307
+ SaveProxiedCheckbox(checkboxRef)))
282
308
  }
283
309
 
284
- function SaveProxiedCheckbox() {
310
+ function SaveProxiedCheckbox(ref) {
285
311
  const { collectProxied, canProxy } = state
286
312
  function onChange() {
287
313
  mockaton.setCollectProxied(this.checked)
@@ -291,6 +317,7 @@ function SaveProxiedCheckbox() {
291
317
  return (
292
318
  r('label', className(CSS.SaveProxiedCheckbox),
293
319
  r('input', {
320
+ ref,
294
321
  type: 'checkbox',
295
322
  disabled: !canProxy,
296
323
  checked: collectProxied,
@@ -301,6 +328,7 @@ function SaveProxiedCheckbox() {
301
328
 
302
329
  function ResetButton() {
303
330
  function onClick() {
331
+ state.clearChosenLink()
304
332
  mockaton.reset()
305
333
  .then(parseError)
306
334
  .then(updateState)
@@ -327,7 +355,7 @@ function MockList() {
327
355
  t`No mocks found`))
328
356
 
329
357
  if (groupByMethod)
330
- return Object.keys(brokersByMethod).map((method) => Fragment(
358
+ return Object.keys(brokersByMethod).map(method => Fragment(
331
359
  r('tr', null,
332
360
  r('th', { colspan: 2 + Number(canProxy) }),
333
361
  r('th', null, method)),
@@ -337,7 +365,7 @@ function MockList() {
337
365
  }
338
366
 
339
367
 
340
- function Row({ method, urlMask, urlMaskDittoed, broker }) {
368
+ function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
341
369
  const { canProxy, groupByMethod } = state
342
370
  return (
343
371
  r('tr', { 'data-method': method, 'data-urlMask': urlMask },
@@ -345,7 +373,7 @@ function Row({ method, urlMask, urlMaskDittoed, broker }) {
345
373
  r('td', null, DelayRouteToggler(broker)),
346
374
  r('td', null, InternalServerErrorToggler(broker)),
347
375
  !groupByMethod && r('td', className(CSS.Method), method),
348
- r('td', null, PreviewLink(method, urlMask, urlMaskDittoed)),
376
+ r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
349
377
  r('td', null, MockSelector(broker))))
350
378
  }
351
379
 
@@ -358,9 +386,7 @@ function rowsFor(targetMethod) {
358
386
  for (const [urlMask, broker] of Object.entries(brokers))
359
387
  rows.push({ method, urlMask, broker })
360
388
 
361
- const sorted = rows
362
- .filter((r) => r.broker.mocks.length > 1) // >1 because of autogen500
363
- .sort((rA, rB) => rA.urlMask.localeCompare(rB.urlMask))
389
+ const sorted = rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
364
390
 
365
391
  const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
366
392
  return sorted.map((r, i) => ({
@@ -369,39 +395,50 @@ function rowsFor(targetMethod) {
369
395
  }))
370
396
  }
371
397
 
372
- function PreviewLink(method, urlMask, urlMaskDittoed) {
398
+
399
+ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
373
400
  async function onClick(event) {
374
401
  event.preventDefault()
375
402
  try {
376
- document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
403
+ findChosenLink()?.classList.remove(CSS.chosen)
377
404
  this.classList.add(CSS.chosen)
378
- await previewMock(method, urlMask, this.href)
405
+ state.setChosenLink(method, urlMask)
406
+ await previewMock(method, urlMask)
379
407
  }
380
408
  catch (error) {
381
409
  onError(error)
382
410
  }
383
411
  }
412
+ const { chosenLink } = state
413
+ const isChosen = chosenLink.method === method && chosenLink.urlMask === urlMask
384
414
  const [ditto, tail] = urlMaskDittoed
385
415
  return (
386
416
  r('a', {
387
- className: CSS.PreviewLink,
417
+ ...className(CSS.PreviewLink, isChosen && CSS.chosen),
388
418
  href: urlMask,
419
+ autofocus,
420
+ 'data-focus-group': FocusGroup.PreviewLink,
389
421
  onClick
390
422
  }, ditto
391
423
  ? [r('span', className(CSS.dittoDir), ditto), tail]
392
424
  : tail))
393
425
  }
394
426
 
427
+ function findChosenLink() {
428
+ return document.querySelector(
429
+ `body > main > .${CSS.leftSide} .${CSS.PreviewLink}.${CSS.chosen}`)
430
+ }
431
+
395
432
  const STR_PROXIED = t`Proxied`
396
433
 
397
434
  /** @param {ClientMockBroker} broker */
398
435
  function MockSelector(broker) {
399
436
  function onChange() {
400
- const { urlMask, method } = parseFilename(this.value)
437
+ const { method, urlMask } = parseFilename(this.value)
438
+ state.setChosenLink(method, urlMask)
401
439
  mockaton.select(this.value)
402
440
  .then(parseError)
403
441
  .then(updateState)
404
- .then(() => linkFor(method, urlMask)?.click())
405
442
  .catch(onError)
406
443
  }
407
444
 
@@ -455,7 +492,8 @@ function DelayRouteToggler(broker) {
455
492
  }
456
493
  return ClickDragToggler({
457
494
  checked: broker.currentMock.delayed,
458
- commit
495
+ commit,
496
+ focusGroup: FocusGroup.DelayToggler
459
497
  })
460
498
  }
461
499
 
@@ -464,10 +502,10 @@ function DelayRouteToggler(broker) {
464
502
  function InternalServerErrorToggler(broker) {
465
503
  function onChange() {
466
504
  const { method, urlMask } = parseFilename(broker.mocks[0])
505
+ state.setChosenLink(method, urlMask)
467
506
  mockaton.toggle500(method, urlMask)
468
507
  .then(parseError)
469
508
  .then(updateState)
470
- .then(() => linkFor(method, urlMask)?.click())
471
509
  .catch(onError)
472
510
  }
473
511
  return (
@@ -477,6 +515,7 @@ function InternalServerErrorToggler(broker) {
477
515
  },
478
516
  r('input', {
479
517
  type: 'checkbox',
518
+ 'data-focus-group': FocusGroup.StatusToggler,
480
519
  name: broker.currentMock.file,
481
520
  checked: parseFilename(broker.currentMock.file).status === 500,
482
521
  onChange
@@ -488,10 +527,10 @@ function InternalServerErrorToggler(broker) {
488
527
  function ProxyToggler(broker) {
489
528
  function onChange() {
490
529
  const { urlMask, method } = parseFilename(broker.mocks[0])
530
+ state.setChosenLink(method, urlMask)
491
531
  mockaton.setRouteIsProxied(method, urlMask, this.checked)
492
532
  .then(parseError)
493
533
  .then(updateState)
494
- .then(() => linkFor(method, urlMask)?.click())
495
534
  .catch(onError)
496
535
  }
497
536
  return (
@@ -502,7 +541,8 @@ function ProxyToggler(broker) {
502
541
  r('input', {
503
542
  type: 'checkbox',
504
543
  checked: !broker.currentMock.file,
505
- onChange
544
+ onChange,
545
+ 'data-focus-group': FocusGroup.ProxyToggler
506
546
  }),
507
547
  CloudIcon()))
508
548
  }
@@ -532,7 +572,8 @@ function StaticFilesList() {
532
572
  r('td', null, r('a', {
533
573
  href: broker.route,
534
574
  target: '_blank',
535
- className: CSS.StaticFileLink
575
+ className: CSS.PreviewLink,
576
+ 'data-focus-group': FocusGroup.PreviewLink
536
577
  }, dp[i]))
537
578
  ))))
538
579
  }
@@ -546,7 +587,8 @@ function DelayStaticRouteToggler(broker) {
546
587
  }
547
588
  return ClickDragToggler({
548
589
  checked: broker.delayed,
549
- commit
590
+ commit,
591
+ focusGroup: FocusGroup.DelayToggler
550
592
  })
551
593
  }
552
594
 
@@ -565,13 +607,14 @@ function NotFoundToggler(broker) {
565
607
  r('input', {
566
608
  type: 'checkbox',
567
609
  checked: broker.status === 404,
568
- onChange
610
+ onChange,
611
+ 'data-focus-group': FocusGroup.StatusToggler
569
612
  }),
570
613
  r('span', null, t`404`)))
571
614
  }
572
615
 
573
616
 
574
- function ClickDragToggler({ checked, commit }) {
617
+ function ClickDragToggler({ checked, commit, focusGroup }) {
575
618
  function onPointerEnter(event) {
576
619
  if (event.buttons === 1)
577
620
  onPointerDown.call(this)
@@ -594,6 +637,7 @@ function ClickDragToggler({ checked, commit }) {
594
637
  },
595
638
  r('input', {
596
639
  type: 'checkbox',
640
+ 'data-focus-group': focusGroup,
597
641
  checked,
598
642
  onPointerEnter,
599
643
  onPointerDown,
@@ -604,7 +648,6 @@ function ClickDragToggler({ checked, commit }) {
604
648
  }
605
649
 
606
650
 
607
-
608
651
  function Resizer() {
609
652
  return (
610
653
  r('div', {
@@ -682,7 +725,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
682
725
  ' ' + mime))
683
726
  }
684
727
 
685
- async function previewMock(method, urlMask, href) {
728
+ async function previewMock(method, urlMask) {
686
729
  previewMock.controller?.abort()
687
730
  previewMock.controller = new AbortController
688
731
 
@@ -692,7 +735,7 @@ async function previewMock(method, urlMask, href) {
692
735
  }, SPINNER_DELAY)
693
736
 
694
737
  try {
695
- const response = await fetch(href, {
738
+ const response = await fetch(urlMask, {
696
739
  method,
697
740
  signal: previewMock.controller.signal
698
741
  })
@@ -736,7 +779,7 @@ async function updatePayloadViewer(method, urlMask, response) {
736
779
  else if (isXML(mime))
737
780
  payloadViewerRef.current.replaceChildren(syntaxXML(body))
738
781
  else
739
- payloadViewerRef.current.innerText = body
782
+ payloadViewerRef.current.textContent = body
740
783
  }
741
784
  }
742
785
 
@@ -760,6 +803,50 @@ function focus(selector) {
760
803
  document.querySelector(selector)?.focus()
761
804
  }
762
805
 
806
+ function initKeyboardNavigation() {
807
+ addEventListener('keydown', onKeyDown)
808
+
809
+ function onKeyDown(event) {
810
+ const pivot = document.activeElement
811
+ switch (event.key) {
812
+ case 'ArrowDown':
813
+ case 'ArrowUp': {
814
+ let fg = pivot.getAttribute('data-focus-group')
815
+ if (fg !== null) {
816
+ const offset = event.key === 'ArrowDown' ? +1 : -1
817
+ circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
818
+ }
819
+ break
820
+ }
821
+ case 'ArrowRight':
822
+ case 'ArrowLeft': {
823
+ if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
824
+ const offset = event.key === 'ArrowRight' ? +1 : -1
825
+ rowFocusable(pivot, offset)?.focus()
826
+ }
827
+ break
828
+ }
829
+ }
830
+ }
831
+
832
+ function rowFocusable(el, step) {
833
+ const row = el.closest('tr')
834
+ if (row) {
835
+ const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
836
+ return circularAdjacent(step, focusables, el)
837
+ }
838
+ }
839
+
840
+ function allInFocusGroup(focusGroup) {
841
+ return Array.from(document.querySelectorAll(
842
+ `body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
843
+ }
844
+
845
+ function circularAdjacent(step = 1, arr, pivot) {
846
+ return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
847
+ }
848
+ }
849
+
763
850
  /** # Misc */
764
851
 
765
852
  async function parseError(response) {
@@ -817,7 +904,7 @@ function SettingsIcon() {
817
904
  */
818
905
 
819
906
  function initLongPoll() {
820
- poll.oldSyncVersion = 0
907
+ poll.oldSyncVersion = -1
821
908
  poll.controller = new AbortController()
822
909
  poll()
823
910
  document.addEventListener('visibilitychange', () => {
@@ -835,9 +922,11 @@ async function poll() {
835
922
  const response = await mockaton.getSyncVersion(poll.oldSyncVersion, poll.controller.signal)
836
923
  if (response.ok) {
837
924
  const syncVersion = await response.json()
925
+ const skipUpdate = poll.oldSyncVersion === -1
838
926
  if (poll.oldSyncVersion !== syncVersion) { // because it could be < or >
839
927
  poll.oldSyncVersion = syncVersion
840
- await updateState()
928
+ if (!skipUpdate)
929
+ await updateState()
841
930
  }
842
931
  poll()
843
932
  }
@@ -894,6 +983,12 @@ function Fragment(...args) {
894
983
  return frag
895
984
  }
896
985
 
986
+ function Defer(cb) {
987
+ const placeholder = document.createComment('')
988
+ deferred(() => placeholder.replaceWith(cb()))
989
+ return placeholder
990
+ }
991
+
897
992
  function deferred(cb) {
898
993
  return window.requestIdleCallback
899
994
  ? requestIdleCallback(cb)