mockaton 10.5.4 → 10.6.1

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.1",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -27,6 +27,6 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "pixaton": "1.1.3",
30
- "puppeteer": "24.23.0"
30
+ "puppeteer": "24.24.1"
31
31
  }
32
32
  }
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 {
@@ -344,7 +343,7 @@ main {
344
343
  }
345
344
 
346
345
  .leftSide {
347
- width: 50%;
346
+ /* the width is set in js (it’s resizable) */
348
347
  padding: 16px;
349
348
  border-right: 1px solid var(--colorSecondaryActionBorder);
350
349
  user-select: none;
@@ -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,20 @@ const state = /** @type {State} */ {
70
76
  updateState()
71
77
  },
72
78
 
73
- leftSideWidth: undefined
79
+ leftSideWidth: window.innerWidth / 2,
80
+
81
+ chosenLink: { method: '', urlMask: '' },
82
+ clearChosenLink() { state.setChosenLink('', '') },
83
+ setChosenLink(method, urlMask) {
84
+ state.chosenLink = { method, urlMask }
85
+ }
74
86
  }
75
87
 
76
88
 
77
89
  const mockaton = new Commander(location.origin)
78
90
  updateState()
79
- deferred(initLongPoll)
91
+ initLongPoll()
92
+ initKeyboardNavigation()
80
93
 
81
94
  async function updateState() {
82
95
  try {
@@ -85,6 +98,11 @@ async function updateState() {
85
98
  throw response.status
86
99
  Object.assign(state, await response.json())
87
100
  document.body.replaceChildren(...App())
101
+
102
+ findChosenLink()?.focus()
103
+ const { method, urlMask } = state.chosenLink
104
+ if (method && urlMask)
105
+ await previewMock(method, urlMask)
88
106
  }
89
107
  catch (error) {
90
108
  onError(error)
@@ -132,12 +150,10 @@ function Header() {
132
150
  CookieSelector(),
133
151
  ProxyFallbackField(),
134
152
  ResetButton(),
135
- SettingsMenu())))
153
+ SettingsMenuTrigger())))
136
154
  }
137
155
 
138
- function SettingsMenu() {
139
- const { groupByMethod, toggleGroupByMethod } = state
140
-
156
+ function SettingsMenuTrigger() {
141
157
  const id = '_settings_menu_'
142
158
  return (
143
159
  r('button', {
@@ -146,29 +162,42 @@ function SettingsMenu() {
146
162
  className: CSS.MenuTrigger
147
163
  },
148
164
  SettingsIcon(),
165
+ Defer(() => SettingsMenu(id))))
166
+ }
149
167
 
150
- r('menu', {
151
- id,
152
- popover: '',
153
- className: CSS.SettingsMenu
154
- },
168
+ function SettingsMenu(id) {
169
+ const { groupByMethod, toggleGroupByMethod } = state
170
+
171
+ const firstInputRef = useRef()
172
+ function onToggle(event) {
173
+ if (event.newState === 'open')
174
+ firstInputRef.current.focus()
175
+ }
176
+ return (
177
+ r('menu', {
178
+ id,
179
+ popover: '',
180
+ className: CSS.SettingsMenu,
181
+ onToggle
182
+ },
155
183
 
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`)),
164
-
165
- r('a', {
166
- href: 'https://github.com/ericfortis/mockaton',
167
- target: '_blank',
168
- rel: 'noopener noreferrer'
169
- }, t`Documentation`))))
184
+ r('label', className(CSS.GroupByMethod),
185
+ r('input', {
186
+ ref: firstInputRef,
187
+ type: 'checkbox',
188
+ checked: groupByMethod,
189
+ onChange: toggleGroupByMethod
190
+ }),
191
+ r('span', null, t`Group by Method`)),
192
+
193
+ r('a', {
194
+ href: 'https://github.com/ericfortis/mockaton',
195
+ target: '_blank',
196
+ rel: 'noopener noreferrer'
197
+ }, t`Documentation`)))
170
198
  }
171
199
 
200
+
172
201
  function CookieSelector() {
173
202
  const { cookies } = state
174
203
  function onChange() {
@@ -196,6 +225,7 @@ function BulkSelector() {
196
225
  // But this way is easier to implement, with a few hacks.
197
226
  const firstOption = t`Pick Comment…`
198
227
  function onChange() {
228
+ state.clearChosenLink()
199
229
  const value = this.value
200
230
  this.value = firstOption // Hack
201
231
  mockaton.bulkSelectByComment(value)
@@ -233,8 +263,10 @@ function GlobalDelayField() {
233
263
  .catch(onError)
234
264
  }
235
265
  function onWheel(event) {
236
- const val = this.valueAsNumber - event.deltaY * 10
237
- this.valueAsNumber = Math.max(val, 0)
266
+ if (event.deltaY > 0)
267
+ this.stepUp()
268
+ else
269
+ this.stepDown()
238
270
  clearTimeout(onWheel.timer)
239
271
  onWheel.timer = setTimeout(onChange.bind(this), 300)
240
272
  }
@@ -254,9 +286,9 @@ function GlobalDelayField() {
254
286
 
255
287
  function ProxyFallbackField() {
256
288
  const { proxyFallback } = state
289
+ const checkboxRef = useRef()
257
290
  function onChange() {
258
- const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
259
- saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
291
+ checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
260
292
 
261
293
  if (!this.validity.valid)
262
294
  this.reportValidity()
@@ -278,10 +310,10 @@ function ProxyFallbackField() {
278
310
  value: proxyFallback,
279
311
  onChange
280
312
  })),
281
- SaveProxiedCheckbox()))
313
+ SaveProxiedCheckbox(checkboxRef)))
282
314
  }
283
315
 
284
- function SaveProxiedCheckbox() {
316
+ function SaveProxiedCheckbox(ref) {
285
317
  const { collectProxied, canProxy } = state
286
318
  function onChange() {
287
319
  mockaton.setCollectProxied(this.checked)
@@ -291,6 +323,7 @@ function SaveProxiedCheckbox() {
291
323
  return (
292
324
  r('label', className(CSS.SaveProxiedCheckbox),
293
325
  r('input', {
326
+ ref,
294
327
  type: 'checkbox',
295
328
  disabled: !canProxy,
296
329
  checked: collectProxied,
@@ -301,6 +334,7 @@ function SaveProxiedCheckbox() {
301
334
 
302
335
  function ResetButton() {
303
336
  function onClick() {
337
+ state.clearChosenLink()
304
338
  mockaton.reset()
305
339
  .then(parseError)
306
340
  .then(updateState)
@@ -327,7 +361,7 @@ function MockList() {
327
361
  t`No mocks found`))
328
362
 
329
363
  if (groupByMethod)
330
- return Object.keys(brokersByMethod).map((method) => Fragment(
364
+ return Object.keys(brokersByMethod).map(method => Fragment(
331
365
  r('tr', null,
332
366
  r('th', { colspan: 2 + Number(canProxy) }),
333
367
  r('th', null, method)),
@@ -337,7 +371,7 @@ function MockList() {
337
371
  }
338
372
 
339
373
 
340
- function Row({ method, urlMask, urlMaskDittoed, broker }) {
374
+ function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
341
375
  const { canProxy, groupByMethod } = state
342
376
  return (
343
377
  r('tr', { 'data-method': method, 'data-urlMask': urlMask },
@@ -345,7 +379,7 @@ function Row({ method, urlMask, urlMaskDittoed, broker }) {
345
379
  r('td', null, DelayRouteToggler(broker)),
346
380
  r('td', null, InternalServerErrorToggler(broker)),
347
381
  !groupByMethod && r('td', className(CSS.Method), method),
348
- r('td', null, PreviewLink(method, urlMask, urlMaskDittoed)),
382
+ r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
349
383
  r('td', null, MockSelector(broker))))
350
384
  }
351
385
 
@@ -358,9 +392,7 @@ function rowsFor(targetMethod) {
358
392
  for (const [urlMask, broker] of Object.entries(brokers))
359
393
  rows.push({ method, urlMask, broker })
360
394
 
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))
395
+ const sorted = rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
364
396
 
365
397
  const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
366
398
  return sorted.map((r, i) => ({
@@ -369,39 +401,50 @@ function rowsFor(targetMethod) {
369
401
  }))
370
402
  }
371
403
 
372
- function PreviewLink(method, urlMask, urlMaskDittoed) {
404
+
405
+ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
373
406
  async function onClick(event) {
374
407
  event.preventDefault()
375
408
  try {
376
- document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
409
+ findChosenLink()?.classList.remove(CSS.chosen)
377
410
  this.classList.add(CSS.chosen)
378
- await previewMock(method, urlMask, this.href)
411
+ state.setChosenLink(method, urlMask)
412
+ await previewMock(method, urlMask)
379
413
  }
380
414
  catch (error) {
381
415
  onError(error)
382
416
  }
383
417
  }
418
+ const { chosenLink } = state
419
+ const isChosen = chosenLink.method === method && chosenLink.urlMask === urlMask
384
420
  const [ditto, tail] = urlMaskDittoed
385
421
  return (
386
422
  r('a', {
387
- className: CSS.PreviewLink,
423
+ ...className(CSS.PreviewLink, isChosen && CSS.chosen),
388
424
  href: urlMask,
425
+ autofocus,
426
+ 'data-focus-group': FocusGroup.PreviewLink,
389
427
  onClick
390
428
  }, ditto
391
429
  ? [r('span', className(CSS.dittoDir), ditto), tail]
392
430
  : tail))
393
431
  }
394
432
 
433
+ function findChosenLink() {
434
+ return document.querySelector(
435
+ `body > main > .${CSS.leftSide} .${CSS.PreviewLink}.${CSS.chosen}`)
436
+ }
437
+
395
438
  const STR_PROXIED = t`Proxied`
396
439
 
397
440
  /** @param {ClientMockBroker} broker */
398
441
  function MockSelector(broker) {
399
442
  function onChange() {
400
- const { urlMask, method } = parseFilename(this.value)
443
+ const { method, urlMask } = parseFilename(this.value)
444
+ state.setChosenLink(method, urlMask)
401
445
  mockaton.select(this.value)
402
446
  .then(parseError)
403
447
  .then(updateState)
404
- .then(() => linkFor(method, urlMask)?.click())
405
448
  .catch(onError)
406
449
  }
407
450
 
@@ -455,7 +498,8 @@ function DelayRouteToggler(broker) {
455
498
  }
456
499
  return ClickDragToggler({
457
500
  checked: broker.currentMock.delayed,
458
- commit
501
+ commit,
502
+ focusGroup: FocusGroup.DelayToggler
459
503
  })
460
504
  }
461
505
 
@@ -464,10 +508,10 @@ function DelayRouteToggler(broker) {
464
508
  function InternalServerErrorToggler(broker) {
465
509
  function onChange() {
466
510
  const { method, urlMask } = parseFilename(broker.mocks[0])
511
+ state.setChosenLink(method, urlMask)
467
512
  mockaton.toggle500(method, urlMask)
468
513
  .then(parseError)
469
514
  .then(updateState)
470
- .then(() => linkFor(method, urlMask)?.click())
471
515
  .catch(onError)
472
516
  }
473
517
  return (
@@ -477,6 +521,7 @@ function InternalServerErrorToggler(broker) {
477
521
  },
478
522
  r('input', {
479
523
  type: 'checkbox',
524
+ 'data-focus-group': FocusGroup.StatusToggler,
480
525
  name: broker.currentMock.file,
481
526
  checked: parseFilename(broker.currentMock.file).status === 500,
482
527
  onChange
@@ -488,10 +533,10 @@ function InternalServerErrorToggler(broker) {
488
533
  function ProxyToggler(broker) {
489
534
  function onChange() {
490
535
  const { urlMask, method } = parseFilename(broker.mocks[0])
536
+ state.setChosenLink(method, urlMask)
491
537
  mockaton.setRouteIsProxied(method, urlMask, this.checked)
492
538
  .then(parseError)
493
539
  .then(updateState)
494
- .then(() => linkFor(method, urlMask)?.click())
495
540
  .catch(onError)
496
541
  }
497
542
  return (
@@ -502,7 +547,8 @@ function ProxyToggler(broker) {
502
547
  r('input', {
503
548
  type: 'checkbox',
504
549
  checked: !broker.currentMock.file,
505
- onChange
550
+ onChange,
551
+ 'data-focus-group': FocusGroup.ProxyToggler
506
552
  }),
507
553
  CloudIcon()))
508
554
  }
@@ -532,7 +578,8 @@ function StaticFilesList() {
532
578
  r('td', null, r('a', {
533
579
  href: broker.route,
534
580
  target: '_blank',
535
- className: CSS.StaticFileLink
581
+ className: CSS.PreviewLink,
582
+ 'data-focus-group': FocusGroup.PreviewLink
536
583
  }, dp[i]))
537
584
  ))))
538
585
  }
@@ -546,7 +593,8 @@ function DelayStaticRouteToggler(broker) {
546
593
  }
547
594
  return ClickDragToggler({
548
595
  checked: broker.delayed,
549
- commit
596
+ commit,
597
+ focusGroup: FocusGroup.DelayToggler
550
598
  })
551
599
  }
552
600
 
@@ -565,13 +613,14 @@ function NotFoundToggler(broker) {
565
613
  r('input', {
566
614
  type: 'checkbox',
567
615
  checked: broker.status === 404,
568
- onChange
616
+ onChange,
617
+ 'data-focus-group': FocusGroup.StatusToggler
569
618
  }),
570
619
  r('span', null, t`404`)))
571
620
  }
572
621
 
573
622
 
574
- function ClickDragToggler({ checked, commit }) {
623
+ function ClickDragToggler({ checked, commit, focusGroup }) {
575
624
  function onPointerEnter(event) {
576
625
  if (event.buttons === 1)
577
626
  onPointerDown.call(this)
@@ -594,6 +643,7 @@ function ClickDragToggler({ checked, commit }) {
594
643
  },
595
644
  r('input', {
596
645
  type: 'checkbox',
646
+ 'data-focus-group': focusGroup,
597
647
  checked,
598
648
  onPointerEnter,
599
649
  onPointerDown,
@@ -604,7 +654,6 @@ function ClickDragToggler({ checked, commit }) {
604
654
  }
605
655
 
606
656
 
607
-
608
657
  function Resizer() {
609
658
  return (
610
659
  r('div', {
@@ -682,7 +731,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
682
731
  ' ' + mime))
683
732
  }
684
733
 
685
- async function previewMock(method, urlMask, href) {
734
+ async function previewMock(method, urlMask) {
686
735
  previewMock.controller?.abort()
687
736
  previewMock.controller = new AbortController
688
737
 
@@ -692,7 +741,7 @@ async function previewMock(method, urlMask, href) {
692
741
  }, SPINNER_DELAY)
693
742
 
694
743
  try {
695
- const response = await fetch(href, {
744
+ const response = await fetch(urlMask, {
696
745
  method,
697
746
  signal: previewMock.controller.signal
698
747
  })
@@ -736,7 +785,7 @@ async function updatePayloadViewer(method, urlMask, response) {
736
785
  else if (isXML(mime))
737
786
  payloadViewerRef.current.replaceChildren(syntaxXML(body))
738
787
  else
739
- payloadViewerRef.current.innerText = body
788
+ payloadViewerRef.current.textContent = body
740
789
  }
741
790
  }
742
791
 
@@ -746,20 +795,61 @@ function isXML(mime) {
746
795
  }
747
796
 
748
797
 
749
- function trFor(method, urlMask) {
750
- return document.querySelector(`tr[data-method="${method}"][data-urlMask="${urlMask}"]`)
751
- }
752
- function linkFor(method, urlMask) {
753
- return trFor(method, urlMask)?.querySelector(`a.${CSS.PreviewLink}`)
754
- }
755
798
  function mockSelectorFor(method, urlMask) {
756
799
  return trFor(method, urlMask)?.querySelector(`select.${CSS.MockSelector}`)
757
800
  }
801
+ function trFor(method, urlMask) {
802
+ return document.querySelector(`tr[data-method="${method}"][data-urlMask="${urlMask}"]`)
803
+ }
758
804
 
759
805
  function focus(selector) {
760
806
  document.querySelector(selector)?.focus()
761
807
  }
762
808
 
809
+ function initKeyboardNavigation() {
810
+ addEventListener('keydown', onKeyDown)
811
+
812
+ function onKeyDown(event) {
813
+ const pivot = document.activeElement
814
+ switch (event.key) {
815
+ case 'ArrowDown':
816
+ case 'ArrowUp': {
817
+ let fg = pivot.getAttribute('data-focus-group')
818
+ if (fg !== null) {
819
+ const offset = event.key === 'ArrowDown' ? +1 : -1
820
+ circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
821
+ }
822
+ break
823
+ }
824
+ case 'ArrowRight':
825
+ case 'ArrowLeft': {
826
+ if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
827
+ const offset = event.key === 'ArrowRight' ? +1 : -1
828
+ rowFocusable(pivot, offset)?.focus()
829
+ }
830
+ break
831
+ }
832
+ }
833
+ }
834
+
835
+ function rowFocusable(el, step) {
836
+ const row = el.closest('tr')
837
+ if (row) {
838
+ const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
839
+ return circularAdjacent(step, focusables, el)
840
+ }
841
+ }
842
+
843
+ function allInFocusGroup(focusGroup) {
844
+ return Array.from(document.querySelectorAll(
845
+ `body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
846
+ }
847
+
848
+ function circularAdjacent(step = 1, arr, pivot) {
849
+ return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
850
+ }
851
+ }
852
+
763
853
  /** # Misc */
764
854
 
765
855
  async function parseError(response) {
@@ -817,7 +907,7 @@ function SettingsIcon() {
817
907
  */
818
908
 
819
909
  function initLongPoll() {
820
- poll.oldSyncVersion = 0
910
+ poll.oldSyncVersion = -1
821
911
  poll.controller = new AbortController()
822
912
  poll()
823
913
  document.addEventListener('visibilitychange', () => {
@@ -835,9 +925,11 @@ async function poll() {
835
925
  const response = await mockaton.getSyncVersion(poll.oldSyncVersion, poll.controller.signal)
836
926
  if (response.ok) {
837
927
  const syncVersion = await response.json()
928
+ const skipUpdate = poll.oldSyncVersion === -1
838
929
  if (poll.oldSyncVersion !== syncVersion) { // because it could be < or >
839
930
  poll.oldSyncVersion = syncVersion
840
- await updateState()
931
+ if (!skipUpdate)
932
+ await updateState()
841
933
  }
842
934
  poll()
843
935
  }
@@ -894,6 +986,12 @@ function Fragment(...args) {
894
986
  return frag
895
987
  }
896
988
 
989
+ function Defer(cb) {
990
+ const placeholder = document.createComment('')
991
+ deferred(() => placeholder.replaceWith(cb()))
992
+ return placeholder
993
+ }
994
+
897
995
  function deferred(cb) {
898
996
  return window.requestIdleCallback
899
997
  ? requestIdleCallback(cb)