mockaton 10.6.1 → 10.6.3

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.1",
5
+ "version": "10.6.3",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "start": "node src/cli.js",
24
24
  "watch": "node --watch src/cli.js",
25
25
  "pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none 'pixaton-tests/**/*.test.js'",
26
- "outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
26
+ "outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %s ;# %s\\n\", $4, $2 }'"
27
27
  },
28
28
  "devDependencies": {
29
29
  "pixaton": "1.1.3",
package/src/Dashboard.js CHANGED
@@ -69,6 +69,12 @@ const state = /** @type {State} */ {
69
69
  return Boolean(state.proxyFallback)
70
70
  },
71
71
 
72
+ fileFor(method, urlMask) {
73
+ return state.brokersByMethod[method]?.[urlMask]?.currentMock.file
74
+ },
75
+
76
+ leftSideWidth: window.innerWidth / 2,
77
+
72
78
  groupByMethod: initPreference('groupByMethod'),
73
79
  toggleGroupByMethod() {
74
80
  state.groupByMethod = !state.groupByMethod
@@ -76,10 +82,7 @@ const state = /** @type {State} */ {
76
82
  updateState()
77
83
  },
78
84
 
79
- leftSideWidth: window.innerWidth / 2,
80
-
81
85
  chosenLink: { method: '', urlMask: '' },
82
- clearChosenLink() { state.setChosenLink('', '') },
83
86
  setChosenLink(method, urlMask) {
84
87
  state.chosenLink = { method, urlMask }
85
88
  }
@@ -88,7 +91,7 @@ const state = /** @type {State} */ {
88
91
 
89
92
  const mockaton = new Commander(location.origin)
90
93
  updateState()
91
- initLongPoll()
94
+ initRealTimeUpdates()
92
95
  initKeyboardNavigation()
93
96
 
94
97
  async function updateState() {
@@ -96,13 +99,18 @@ async function updateState() {
96
99
  const response = await mockaton.getState()
97
100
  if (!response.ok)
98
101
  throw response.status
102
+
99
103
  Object.assign(state, await response.json())
104
+
105
+ const focusedElem = selectorFor(document.activeElement)
100
106
  document.body.replaceChildren(...App())
107
+ if (focusedElem)
108
+ document.querySelector(focusedElem)?.focus()
101
109
 
102
- findChosenLink()?.focus()
103
110
  const { method, urlMask } = state.chosenLink
104
111
  if (method && urlMask)
105
112
  await previewMock(method, urlMask)
113
+
106
114
  }
107
115
  catch (error) {
108
116
  onError(error)
@@ -167,7 +175,7 @@ function SettingsMenuTrigger() {
167
175
 
168
176
  function SettingsMenu(id) {
169
177
  const { groupByMethod, toggleGroupByMethod } = state
170
-
178
+
171
179
  const firstInputRef = useRef()
172
180
  function onToggle(event) {
173
181
  if (event.newState === 'open')
@@ -225,13 +233,11 @@ function BulkSelector() {
225
233
  // But this way is easier to implement, with a few hacks.
226
234
  const firstOption = t`Pick Comment…`
227
235
  function onChange() {
228
- state.clearChosenLink()
229
236
  const value = this.value
230
237
  this.value = firstOption // Hack
231
238
  mockaton.bulkSelectByComment(value)
232
239
  .then(parseError)
233
240
  .then(updateState)
234
- .then(() => focus(`.${CSS.BulkSelector}`))
235
241
  .catch(onError)
236
242
  }
237
243
  const disabled = !comments.length
@@ -296,7 +302,6 @@ function ProxyFallbackField() {
296
302
  mockaton.setProxyFallback(this.value.trim())
297
303
  .then(parseError)
298
304
  .then(updateState)
299
- .then(() => focus(`.${CSS.FallbackBackend} input`))
300
305
  .catch(onError)
301
306
  }
302
307
  return (
@@ -334,11 +339,10 @@ function SaveProxiedCheckbox(ref) {
334
339
 
335
340
  function ResetButton() {
336
341
  function onClick() {
337
- state.clearChosenLink()
342
+ state.setChosenLink('', '')
338
343
  mockaton.reset()
339
344
  .then(parseError)
340
345
  .then(updateState)
341
- .then(() => focus(`.${CSS.ResetButton}`))
342
346
  .catch(onError)
343
347
  }
344
348
  return (
@@ -374,7 +378,7 @@ function MockList() {
374
378
  function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
375
379
  const { canProxy, groupByMethod } = state
376
380
  return (
377
- r('tr', { 'data-method': method, 'data-urlMask': urlMask },
381
+ r('tr', { key: method + '::' + urlMask },
378
382
  canProxy && r('td', null, ProxyToggler(broker)),
379
383
  r('td', null, DelayRouteToggler(broker)),
380
384
  r('td', null, InternalServerErrorToggler(broker)),
@@ -627,6 +631,7 @@ function ClickDragToggler({ checked, commit, focusGroup }) {
627
631
  }
628
632
  function onPointerDown() {
629
633
  this.checked = !this.checked
634
+ this.focus()
630
635
  commit(this.checked)
631
636
  }
632
637
  function onClick(event) {
@@ -655,68 +660,69 @@ function ClickDragToggler({ checked, commit, focusGroup }) {
655
660
 
656
661
 
657
662
  function Resizer() {
663
+ let raf = 0
664
+ let initialX = 0
665
+ let panelWidth = 0
666
+
667
+ function onPointerDown(event) {
668
+ initialX = event.clientX
669
+ panelWidth = leftSideRef.current.clientWidth
670
+ addEventListener('pointerup', onUp, { once: true })
671
+ addEventListener('pointermove', onMove)
672
+ Object.assign(document.body.style, {
673
+ cursor: 'col-resize',
674
+ userSelect: 'none',
675
+ pointerEvents: 'none'
676
+ })
677
+ }
678
+
679
+ function onMove(event) {
680
+ const MIN_LEFT_WIDTH = 380
681
+ raf = raf || requestAnimationFrame(() => {
682
+ state.leftSideWidth = Math.max(panelWidth - (initialX - event.clientX), MIN_LEFT_WIDTH)
683
+ leftSideRef.current.style.width = state.leftSideWidth + 'px'
684
+ raf = 0
685
+ })
686
+ }
687
+
688
+ function onUp() {
689
+ removeEventListener('pointermove', onMove)
690
+ cancelAnimationFrame(raf)
691
+ raf = 0
692
+ Object.assign(document.body.style, {
693
+ cursor: 'auto',
694
+ userSelect: 'auto',
695
+ pointerEvents: 'auto'
696
+ })
697
+ }
698
+
658
699
  return (
659
700
  r('div', {
660
701
  className: CSS.Resizer,
661
- onPointerDown: Resizer.onPointerDown
702
+ onPointerDown
662
703
  }))
663
704
  }
664
- Resizer.raf = 0
665
- Resizer.initialX = 0
666
- Resizer.panelWidth = 0
667
- Resizer.onPointerDown = function (event) {
668
- Resizer.initialX = event.clientX
669
- Resizer.panelWidth = leftSideRef.current.clientWidth
670
- addEventListener('pointerup', Resizer.onUp, { once: true })
671
- addEventListener('pointermove', Resizer.onMove)
672
- document.body.style.userSelect = 'none'
673
- document.body.style.cursor = 'col-resize'
674
- }
675
- Resizer.onMove = function (event) {
676
- const MIN_LEFT_WIDTH = 380
677
- Resizer.raf = Resizer.raf || requestAnimationFrame(() => {
678
- state.leftSideWidth = Math.max(Resizer.panelWidth - (Resizer.initialX - event.clientX), MIN_LEFT_WIDTH)
679
- leftSideRef.current.style.width = state.leftSideWidth + 'px'
680
- Resizer.raf = 0
681
- })
682
- }
683
- Resizer.onUp = function () {
684
- removeEventListener('pointermove', Resizer.onMove)
685
- cancelAnimationFrame(Resizer.raf)
686
- Resizer.raf = 0
687
- document.body.style.userSelect = 'auto'
688
- document.body.style.cursor = 'auto'
689
- }
690
705
 
691
706
 
692
707
  /** # Payload Preview */
693
708
 
694
709
  const payloadViewerTitleRef = useRef()
695
- const payloadViewerRef = useRef()
696
- const SPINNER_DELAY = 80
710
+ const payloadViewerCodeRef = useRef()
697
711
 
698
712
  function PayloadViewer() {
699
713
  return (
700
714
  r('div', className(CSS.PayloadViewer),
701
715
  r('h2', { ref: payloadViewerTitleRef }, t`Preview`),
702
716
  r('pre', null,
703
- r('code', { ref: payloadViewerRef }, t`Click a link to preview it`))))
704
- }
705
-
706
- function PayloadViewerProgressBar() {
707
- return (
708
- r('div', className(CSS.ProgressBar),
709
- r('div', { style: { animationDuration: state.delay - SPINNER_DELAY + 'ms' } })))
717
+ r('code', { ref: payloadViewerCodeRef }, t`Click a link to preview it`))))
710
718
  }
711
719
 
712
720
  function PayloadViewerTitle({ file, statusText }) {
713
- const tokens = file.split('.')
714
- const ext = tokens.pop()
715
- const status = tokens.pop()
716
- const urlAndMethod = tokens.join('.') + '.'
721
+ const { method, status, ext } = parseFilename(file)
722
+ const fileNameWithComments = file.split('.').slice(0, -3).join('.')
717
723
  return (
718
724
  r('span', null,
719
- urlAndMethod,
725
+ fileNameWithComments + '.' + method + '.',
720
726
  r('abbr', { title: statusText }, status),
721
727
  '.' + ext))
722
728
  }
@@ -731,13 +737,20 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
731
737
  ' ' + mime))
732
738
  }
733
739
 
740
+ const SPINNER_DELAY = 80
741
+ function PayloadViewerProgressBar() {
742
+ return (
743
+ r('div', className(CSS.ProgressBar),
744
+ r('div', { style: { animationDuration: state.delay - SPINNER_DELAY + 'ms' } })))
745
+ }
746
+
734
747
  async function previewMock(method, urlMask) {
735
748
  previewMock.controller?.abort()
736
749
  previewMock.controller = new AbortController
737
750
 
738
751
  const spinnerTimer = setTimeout(() => {
739
752
  payloadViewerTitleRef.current.replaceChildren(t`Fetching…`)
740
- payloadViewerRef.current.replaceChildren(PayloadViewerProgressBar())
753
+ payloadViewerCodeRef.current.replaceChildren(PayloadViewerProgressBar())
741
754
  }, SPINNER_DELAY)
742
755
 
743
756
  try {
@@ -746,46 +759,48 @@ async function previewMock(method, urlMask) {
746
759
  signal: previewMock.controller.signal
747
760
  })
748
761
  clearTimeout(spinnerTimer)
749
- await updatePayloadViewer(method, urlMask, response)
762
+ const file = state.fileFor(method, urlMask)
763
+ if (file === '')
764
+ await updatePayloadViewer(STR_PROXIED, response)
765
+ else if (file)
766
+ await updatePayloadViewer(file, response)
767
+ else {/* e.g. selected was deleted */}
750
768
  }
751
769
  catch (err) {
752
770
  onError(err)
753
- payloadViewerRef.current.replaceChildren()
771
+ payloadViewerCodeRef.current.replaceChildren()
754
772
  }
755
773
  }
756
774
 
757
-
758
- async function updatePayloadViewer(method, urlMask, response) {
775
+ async function updatePayloadViewer(file, response) {
759
776
  const mime = response.headers.get('content-type') || ''
760
777
 
761
- const file = mockSelectorFor(method, urlMask).value
762
- if (file === STR_PROXIED)
763
- payloadViewerTitleRef.current.replaceChildren(PayloadViewerTitleWhenProxied({
764
- status: response.status,
765
- statusText: response.statusText,
766
- mime,
767
- gatewayIsBad: response.headers.get(HEADER_FOR_502)
768
- }))
769
- else
770
- payloadViewerTitleRef.current.replaceChildren(PayloadViewerTitle({
771
- statusText: response.statusText,
772
- file
773
- }))
778
+ payloadViewerTitleRef.current.replaceChildren(
779
+ file === STR_PROXIED
780
+ ? PayloadViewerTitleWhenProxied({
781
+ mime,
782
+ status: response.status,
783
+ statusText: response.statusText,
784
+ gatewayIsBad: response.headers.get(HEADER_FOR_502)
785
+ })
786
+ : PayloadViewerTitle({
787
+ file,
788
+ statusText: response.statusText
789
+ }))
774
790
 
775
- if (mime.startsWith('image/')) { // Naively assumes GET.200
776
- payloadViewerRef.current.replaceChildren(
791
+ if (mime.startsWith('image/')) // Naively assumes GET.200
792
+ payloadViewerCodeRef.current.replaceChildren(
777
793
  r('img', {
778
794
  src: URL.createObjectURL(await response.blob())
779
795
  }))
780
- }
781
796
  else {
782
797
  const body = await response.text() || t`/* Empty Response Body */`
783
798
  if (mime === 'application/json')
784
- payloadViewerRef.current.replaceChildren(r('span', className(CSS.json), syntaxJSON(body)))
799
+ payloadViewerCodeRef.current.replaceChildren(r('span', className(CSS.json), syntaxJSON(body)))
785
800
  else if (isXML(mime))
786
- payloadViewerRef.current.replaceChildren(syntaxXML(body))
801
+ payloadViewerCodeRef.current.replaceChildren(syntaxXML(body))
787
802
  else
788
- payloadViewerRef.current.textContent = body
803
+ payloadViewerCodeRef.current.textContent = body
789
804
  }
790
805
  }
791
806
 
@@ -795,17 +810,6 @@ function isXML(mime) {
795
810
  }
796
811
 
797
812
 
798
- function mockSelectorFor(method, urlMask) {
799
- return trFor(method, urlMask)?.querySelector(`select.${CSS.MockSelector}`)
800
- }
801
- function trFor(method, urlMask) {
802
- return document.querySelector(`tr[data-method="${method}"][data-urlMask="${urlMask}"]`)
803
- }
804
-
805
- function focus(selector) {
806
- document.querySelector(selector)?.focus()
807
- }
808
-
809
813
  function initKeyboardNavigation() {
810
814
  addEventListener('keydown', onKeyDown)
811
815
 
@@ -825,7 +829,7 @@ function initKeyboardNavigation() {
825
829
  case 'ArrowLeft': {
826
830
  if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
827
831
  const offset = event.key === 'ArrowRight' ? +1 : -1
828
- rowFocusable(pivot, offset)?.focus()
832
+ rowFocusable(pivot, offset).focus()
829
833
  }
830
834
  break
831
835
  }
@@ -850,7 +854,7 @@ function initKeyboardNavigation() {
850
854
  }
851
855
  }
852
856
 
853
- /** # Misc */
857
+ /** # Error */
854
858
 
855
859
  async function parseError(response) {
856
860
  if (response.ok)
@@ -882,6 +886,9 @@ function showErrorToast(msg) {
882
886
  }, msg))
883
887
  }
884
888
 
889
+
890
+ /** # Icons */
891
+
885
892
  function TimerIcon() {
886
893
  return (
887
894
  s('svg', { viewBox: '0 0 24 24' },
@@ -902,43 +909,43 @@ function SettingsIcon() {
902
909
  }
903
910
 
904
911
  /**
905
- * # Poll UI Sync Version
906
- * The version increments when a mock file is added or removed
912
+ * # Long polls UI sync version
913
+ * The version increments when a mock file is added, removed, or renamed.
907
914
  */
915
+ function initRealTimeUpdates() {
916
+ let oldSyncVersion = -1
917
+ let controller = new AbortController()
908
918
 
909
- function initLongPoll() {
910
- poll.oldSyncVersion = -1
911
- poll.controller = new AbortController()
912
919
  poll()
913
920
  document.addEventListener('visibilitychange', () => {
914
921
  if (document.hidden) {
915
- poll.controller.abort('_hidden_tab_')
916
- poll.controller = new AbortController()
922
+ controller.abort('_hidden_tab_')
923
+ controller = new AbortController()
917
924
  }
918
925
  else
919
926
  poll()
920
927
  })
921
- }
922
928
 
923
- async function poll() {
924
- try {
925
- const response = await mockaton.getSyncVersion(poll.oldSyncVersion, poll.controller.signal)
926
- if (response.ok) {
927
- const syncVersion = await response.json()
928
- const skipUpdate = poll.oldSyncVersion === -1
929
- if (poll.oldSyncVersion !== syncVersion) { // because it could be < or >
930
- poll.oldSyncVersion = syncVersion
931
- if (!skipUpdate)
932
- await updateState()
929
+ async function poll() {
930
+ try {
931
+ const response = await mockaton.getSyncVersion(oldSyncVersion, controller.signal)
932
+ if (response.ok) {
933
+ const syncVersion = await response.json()
934
+ const skipUpdate = oldSyncVersion === -1
935
+ if (oldSyncVersion !== syncVersion) { // because it could be < or >
936
+ oldSyncVersion = syncVersion
937
+ if (!skipUpdate)
938
+ await updateState()
939
+ }
940
+ poll()
933
941
  }
934
- poll()
942
+ else
943
+ throw response.status
944
+ }
945
+ catch (error) {
946
+ if (error !== '_hidden_tab_')
947
+ setTimeout(poll, 3000)
935
948
  }
936
- else
937
- throw response.status
938
- }
939
- catch (error) {
940
- if (error !== '_hidden_tab_')
941
- setTimeout(poll, 3000)
942
949
  }
943
950
  }
944
951
 
@@ -951,7 +958,6 @@ function className(...args) {
951
958
  }
952
959
  }
953
960
 
954
-
955
961
  function createElement(tag, props, ...children) {
956
962
  const node = document.createElement(tag)
957
963
  for (const [k, v] of Object.entries(props || {}))
@@ -998,6 +1004,30 @@ function deferred(cb) {
998
1004
  : setTimeout(cb, 100) // Safari
999
1005
  }
1000
1006
 
1007
+ function selectorFor(elem) {
1008
+ if (!(elem instanceof Element))
1009
+ return
1010
+
1011
+ const path = []
1012
+ while (elem) {
1013
+ let qualifier = ''
1014
+ if (elem.hasAttribute('key'))
1015
+ qualifier = `[key="${elem.getAttribute('key')}"]`
1016
+ else {
1017
+ let i = 0
1018
+ let sib = elem
1019
+ while ((sib = sib.previousElementSibling))
1020
+ if (sib.tagName === elem.tagName)
1021
+ i++
1022
+ if (i)
1023
+ qualifier = `:nth-of-type(${i + 1})`
1024
+ }
1025
+ path.push(elem.tagName + qualifier)
1026
+ elem = elem.parentElement
1027
+ }
1028
+ return path.reverse().join('>')
1029
+ }
1030
+
1001
1031
 
1002
1032
  // When false, the URL will be updated with param=false
1003
1033
  function initPreference(param) {
@@ -9,23 +9,23 @@ export const CSP = [
9
9
  export const DashboardHtml = `<!DOCTYPE html>
10
10
  <html lang="en-US">
11
11
  <head>
12
- <meta charset="UTF-8">
13
- <base href="${API.dashboard}/">
14
-
15
- <link rel="stylesheet" href="Dashboard.css">
16
- <script type="module" src="Dashboard.js"></script>
17
-
12
+ <meta charset="UTF-8">
13
+ <base href="${API.dashboard}/">
14
+
15
+ <link rel="stylesheet" href="Dashboard.css">
16
+ <script type="module" src="Dashboard.js"></script>
17
+
18
18
  <link rel="preload" href="${API.state}" as="fetch" crossorigin>
19
19
 
20
20
  <link rel="modulepreload" href="ApiConstants.js">
21
21
  <link rel="modulepreload" href="ApiCommander.js">
22
22
  <link rel="modulepreload" href="Filename.js">
23
23
  <link rel="preload" href="Logo.svg" as="image">
24
-
25
- <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
26
- <meta name="viewport" content="width=device-width, initial-scale=1">
27
- <meta name="description" content="HTTP Mock Server">
28
- <title>Mockaton</title>
24
+
25
+ <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
26
+ <meta name="viewport" content="width=device-width, initial-scale=1">
27
+ <meta name="description" content="HTTP Mock Server">
28
+ <title>Mockaton</title>
29
29
  </head>
30
30
  <body>
31
31
  </body>