mockaton 10.7.0 → 11.0.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/Makefile ADDED
@@ -0,0 +1,33 @@
1
+ start:
2
+ @node src/cli.js
3
+
4
+ watch:
5
+ @node --watch src/cli.js
6
+
7
+
8
+ test:
9
+ node --test 'src/**/*.test.js'
10
+
11
+ test-docker:
12
+ docker run --rm -it \
13
+ -v "$$PWD":/app \
14
+ -w /app \
15
+ node:24 \
16
+ make test
17
+
18
+ coverage:
19
+ node --test --experimental-test-coverage \
20
+ --test-reporter=spec --test-reporter-destination=stdout \
21
+ --test-reporter=lcov --test-reporter-destination=lcov.info \
22
+ 'src/**/*.test.js'
23
+
24
+ pixaton:
25
+ node --test --experimental-test-isolation=none \
26
+ --import=./pixaton-tests/_setup.js \
27
+ 'pixaton-tests/**/*.test.js'
28
+
29
+ outdated:
30
+ @npm outdated --parseable |\
31
+ awk -F: '{ printf "npm i %-30s ;# %s\n", $$4, $$2 }'
32
+
33
+ .PHONY: *
package/README.md CHANGED
@@ -10,33 +10,6 @@
10
10
  An HTTP mock server for simulating APIs with minimal setup
11
11
  — ideal for testing difficult to reproduce states.
12
12
 
13
- <br/>
14
-
15
- ## Motivation
16
-
17
- **No API state should be too hard to test.**
18
- With Mockaton, developers can achieve correctness and speed.
19
-
20
- ### Correctness
21
- - Enables testing of complex scenarios that would otherwise be skipped. e.g.,
22
- - Triggering errors on third-party APIs.
23
- - Triggering errors on your project’s backend (if you are a frontend developer).
24
- - Allows for deterministic, comprehensive, and consistent state.
25
- - Spot inadvertent regressions during development.
26
- - Use it to set up screenshot tests, e.g., with [pixaton](https://github.com/ericfortis/pixaton).
27
-
28
- ### Speed
29
- - Works around unstable dev backends while developing UIs.
30
- - Spinning up development infrastructure.
31
- - Syncing database states.
32
- - Prevents progress from being blocked by waiting for APIs.
33
- - Time travel. If you commit the mocks to your repo,
34
- you don’t have to downgrade backends for:
35
- - checking out long-lived branches
36
- - bisecting bugs
37
-
38
- <br/>
39
-
40
13
  ## Overview
41
14
  With Mockaton, you don’t need to write code for wiring up your
42
15
  mocks. Instead, a given directory is scanned for filenames
@@ -118,6 +91,31 @@ checking ✅ **Save Mocks**, you can collect the responses that hit your backend
118
91
  They will be saved in your `config.mocksDir` following the filename convention.
119
92
  </details>
120
93
 
94
+ <br/>
95
+
96
+ ## Motivation
97
+
98
+ **No API state should be too hard to test.**
99
+ With Mockaton, developers can achieve correctness and speed.
100
+
101
+ ### Correctness
102
+ - Enables testing of complex scenarios that would otherwise be skipped. e.g.,
103
+ - Triggering errors on third-party APIs.
104
+ - Triggering errors on your project’s backend (if you are a frontend developer).
105
+ - Allows for deterministic, comprehensive, and consistent state.
106
+ - Spot inadvertent regressions during development.
107
+ - Use it to set up screenshot tests, e.g., with [pixaton](https://github.com/ericfortis/pixaton).
108
+
109
+ ### Speed
110
+ - Works around unstable dev backends while developing UIs.
111
+ - Spinning up development infrastructure.
112
+ - Syncing database states.
113
+ - Prevents progress from being blocked by waiting for APIs.
114
+ - Time travel. If you commit the mocks to your repo,
115
+ you don’t have to downgrade backends for:
116
+ - checking out long-lived branches
117
+ - bisecting bugs
118
+
121
119
 
122
120
  <br/>
123
121
 
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.7.0",
5
+ "version": "11.0.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -18,12 +18,7 @@
18
18
  "mockaton": "src/cli.js"
19
19
  },
20
20
  "scripts": {
21
- "test": "node --test 'src/**/*.test.js'",
22
- "coverage": "node --test --test-reporter=lcov --test-reporter-destination=lcov.info --experimental-test-coverage 'src/**/*.test.js'",
23
- "start": "node src/cli.js",
24
- "watch": "node --watch src/cli.js",
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 }'"
21
+ "start": "make start"
27
22
  },
28
23
  "devDependencies": {
29
24
  "pixaton": "1.1.3",
package/src/Api.js CHANGED
@@ -12,17 +12,21 @@ import * as staticCollection from './staticCollection.js'
12
12
  import * as mockBrokersCollection from './mockBrokersCollection.js'
13
13
  import { config, ConfigValidator } from './config.js'
14
14
  import { DashboardHtml, CSP } from './DashboardHtml.js'
15
- import { DF, API, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
16
15
  import { sendOK, sendJSON, sendUnprocessableContent, sendFile, sendHTML } from './utils/http-response.js'
16
+ import { API, LONG_POLL_SERVER_TIMEOUT, HEADER_FOR_SYNC_VERSION } from './ApiConstants.js'
17
17
 
18
18
 
19
19
  export const apiGetRequests = new Map([
20
20
  [API.dashboard, serveDashboard],
21
21
  ...[
22
+ 'Logo.svg',
22
23
  'Dashboard.css',
24
+ 'ApiCommander.js',
25
+ 'ApiConstants.js',
23
26
  'Dashboard.js',
24
- 'ApiCommander.js', 'ApiConstants.js', 'DashboardDom.js', 'DashboardStore.js', 'Filename.js',
25
- 'Logo.svg'
27
+ 'DashboardDom.js',
28
+ 'DashboardStore.js',
29
+ 'Filename.js'
26
30
  ].map(f => [API.dashboard + '/' + f, serveStatic(f)]),
27
31
 
28
32
  [API.state, getState],
@@ -57,6 +61,7 @@ function serveStatic(f) {
57
61
  return (_, response) => sendFile(response, join(import.meta.dirname, f))
58
62
  }
59
63
 
64
+
60
65
  function getState(_, response) {
61
66
  sendJSON(response, {
62
67
  cookies: cookie.list(),
@@ -74,12 +79,14 @@ function getState(_, response) {
74
79
  })
75
80
  }
76
81
 
82
+
77
83
  function longPollClientSyncVersion(req, response) {
78
- if (uiSyncVersion.version !== Number(req.headers[DF.syncVersion])) {
84
+ if (uiSyncVersion.version !== Number(req.headers[HEADER_FOR_SYNC_VERSION])) {
79
85
  // e.g., tab was hidden while new mocks were added or removed
80
86
  sendJSON(response, uiSyncVersion.version)
81
87
  return
82
88
  }
89
+
83
90
  function onAddOrRemoveMock() {
84
91
  uiSyncVersion.unsubscribe(onAddOrRemoveMock)
85
92
  sendJSON(response, uiSyncVersion.version)
@@ -102,16 +109,21 @@ function reinitialize(_, response) {
102
109
  sendOK(response)
103
110
  }
104
111
 
112
+
105
113
  async function selectCookie(req, response) {
106
- const error = cookie.setCurrent(await parseJSON(req))
114
+ const label = await parseJSON(req)
115
+
116
+ const error = cookie.setCurrent(label)
107
117
  if (error)
108
118
  sendUnprocessableContent(response, error?.message || error)
109
119
  else
110
120
  sendJSON(response, cookie.list())
111
121
  }
112
122
 
123
+
113
124
  async function selectMock(req, response) {
114
125
  const file = await parseJSON(req)
126
+
115
127
  const broker = mockBrokersCollection.brokerByFilename(file)
116
128
  if (!broker || !broker.hasMock(file))
117
129
  sendUnprocessableContent(response, `Missing Mock: ${file}`)
@@ -121,28 +133,26 @@ async function selectMock(req, response) {
121
133
  }
122
134
  }
123
135
 
136
+
124
137
  async function toggle500(req, response) {
125
- const body = await parseJSON(req)
126
- const broker = mockBrokersCollection.brokerByRoute(
127
- body[DF.routeMethod],
128
- body[DF.routeUrlMask])
138
+ const [method, urlMask] = await parseJSON(req)
139
+
140
+ const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
129
141
  if (!broker)
130
- sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
142
+ sendUnprocessableContent(response, `Route does not exist: ${method} ${urlMask}`)
131
143
  else {
132
144
  broker.toggle500()
133
145
  sendJSON(response, broker)
134
146
  }
135
147
  }
136
148
 
149
+
137
150
  async function setRouteIsDelayed(req, response) {
138
- const body = await parseJSON(req)
139
- const delayed = body[DF.delayed]
140
- const broker = mockBrokersCollection.brokerByRoute(
141
- body[DF.routeMethod],
142
- body[DF.routeUrlMask])
151
+ const [method, urlMask, delayed] = await parseJSON(req)
143
152
 
153
+ const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
144
154
  if (!broker)
145
- sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
155
+ sendUnprocessableContent(response, `Route does not exist: ${method} ${urlMask}`)
146
156
  else if (typeof delayed !== 'boolean')
147
157
  sendUnprocessableContent(response, `Expected boolean for "delayed"`)
148
158
  else {
@@ -151,15 +161,13 @@ async function setRouteIsDelayed(req, response) {
151
161
  }
152
162
  }
153
163
 
164
+
154
165
  async function setRouteIsProxied(req, response) {
155
- const body = await parseJSON(req)
156
- const proxied = body[DF.proxied]
157
- const broker = mockBrokersCollection.brokerByRoute(
158
- body[DF.routeMethod],
159
- body[DF.routeUrlMask])
166
+ const [method, urlMask, proxied] = await parseJSON(req)
160
167
 
168
+ const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
161
169
  if (!broker)
162
- sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
170
+ sendUnprocessableContent(response, `Route does not exist: ${method} ${urlMask}`)
163
171
  else if (typeof proxied !== 'boolean')
164
172
  sendUnprocessableContent(response, `Expected boolean for "proxied"`)
165
173
  else if (proxied && !config.proxyFallback)
@@ -170,59 +178,70 @@ async function setRouteIsProxied(req, response) {
170
178
  }
171
179
  }
172
180
 
181
+
173
182
  async function updateProxyFallback(req, response) {
174
183
  const fallback = await parseJSON(req)
175
- if (!ConfigValidator.proxyFallback(fallback)) {
184
+
185
+ if (!ConfigValidator.proxyFallback(fallback))
176
186
  sendUnprocessableContent(response, `Invalid Proxy Fallback URL`)
177
- return
187
+ else {
188
+ config.proxyFallback = fallback
189
+ sendOK(response)
178
190
  }
179
- config.proxyFallback = fallback
180
- sendOK(response)
181
191
  }
182
192
 
193
+
183
194
  async function setCollectProxied(req, response) {
184
195
  const collectProxied = await parseJSON(req)
185
- if (!ConfigValidator.collectProxied(collectProxied)) {
196
+
197
+ if (!ConfigValidator.collectProxied(collectProxied))
186
198
  sendUnprocessableContent(response, `Expected a boolean for "collectProxied"`)
187
- return
199
+ else {
200
+ config.collectProxied = collectProxied
201
+ sendOK(response)
188
202
  }
189
- config.collectProxied = collectProxied
190
- sendOK(response)
191
203
  }
192
204
 
205
+
193
206
  async function bulkUpdateBrokersByCommentTag(req, response) {
194
- mockBrokersCollection.setMocksMatchingComment(await parseJSON(req))
207
+ const comment = await parseJSON(req)
208
+
209
+ mockBrokersCollection.setMocksMatchingComment(comment)
195
210
  sendOK(response)
196
211
  }
197
212
 
213
+
198
214
  async function setCorsAllowed(req, response) {
199
215
  const corsAllowed = await parseJSON(req)
200
- if (!ConfigValidator.corsAllowed(corsAllowed)) {
216
+
217
+ if (!ConfigValidator.corsAllowed(corsAllowed))
201
218
  sendUnprocessableContent(response, `Expected boolean for "corsAllowed"`)
202
- return
219
+ else {
220
+ config.corsAllowed = corsAllowed
221
+ sendOK(response)
203
222
  }
204
- config.corsAllowed = corsAllowed
205
- sendOK(response)
206
223
  }
207
224
 
225
+
208
226
  async function setGlobalDelay(req, response) {
209
227
  const delay = await parseJSON(req)
210
- if (!ConfigValidator.delay(delay)) {
228
+
229
+ if (!ConfigValidator.delay(delay))
211
230
  sendUnprocessableContent(response, `Expected non-negative integer for "delay"`)
212
- return
231
+ else {
232
+ config.delay = delay
233
+ sendOK(response)
213
234
  }
214
- config.delay = delay
215
- sendOK(response)
216
235
  }
217
236
 
218
237
 
238
+
219
239
  async function setStaticRouteStatusCode(req, response) {
220
- const body = await parseJSON(req)
221
- const status = Number(body[DF.statusCode])
222
- const broker = staticCollection.brokerByRoute(body[DF.routeUrlMask])
240
+ const [urlMask, status] = await parseJSON(req)
223
241
 
242
+ const broker = staticCollection.brokerByRoute(urlMask)
224
243
  if (!broker)
225
- sendUnprocessableContent(response, `Static route does not exist: ${body[DF.routeUrlMask]}`)
244
+ sendUnprocessableContent(response, `Static route does not exist: ${urlMask}`)
226
245
  else if (!(status === 200 || status === 404))
227
246
  sendUnprocessableContent(response, `Expected 200 or 404 status code`)
228
247
  else {
@@ -231,13 +250,13 @@ async function setStaticRouteStatusCode(req, response) {
231
250
  }
232
251
  }
233
252
 
253
+
234
254
  async function setStaticRouteIsDelayed(req, response) {
235
- const body = await parseJSON(req)
236
- const delayed = body[DF.delayed]
237
- const broker = staticCollection.brokerByRoute(body[DF.routeUrlMask])
255
+ const [urlMask, delayed] = await parseJSON(req)
238
256
 
257
+ const broker = staticCollection.brokerByRoute(urlMask)
239
258
  if (!broker)
240
- sendUnprocessableContent(response, `Static route does not exist: ${body[DF.routeUrlMask]}`)
259
+ sendUnprocessableContent(response, `Static route does not exist: ${urlMask}`)
241
260
  else if (typeof delayed !== 'boolean')
242
261
  sendUnprocessableContent(response, `Expected boolean for "delayed"`)
243
262
  else {
@@ -1,4 +1,4 @@
1
- import { API, DF, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
1
+ import { API, LONG_POLL_SERVER_TIMEOUT, HEADER_FOR_SYNC_VERSION } from './ApiConstants.js'
2
2
 
3
3
 
4
4
  /** Client for controlling Mockaton via its HTTP API */
@@ -27,7 +27,7 @@ export class Commander {
27
27
  AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
28
28
  ]),
29
29
  headers: {
30
- [DF.syncVersion]: currSyncVer
30
+ [HEADER_FOR_SYNC_VERSION]: currSyncVer
31
31
  }
32
32
  })
33
33
 
@@ -40,45 +40,28 @@ export class Commander {
40
40
  return this.#patch(API.select, file)
41
41
  }
42
42
 
43
- toggle500(routeMethod, routeUrlMask) {
44
- return this.#patch(API.toggle500, {
45
- [DF.routeMethod]: routeMethod,
46
- [DF.routeUrlMask]: routeUrlMask
47
- })
43
+ toggle500(method, urlMask) {
44
+ return this.#patch(API.toggle500, [method, urlMask])
48
45
  }
49
46
 
50
47
  bulkSelectByComment(comment) {
51
48
  return this.#patch(API.bulkSelect, comment)
52
49
  }
53
50
 
54
- setRouteIsDelayed(routeMethod, routeUrlMask, delayed) {
55
- return this.#patch(API.delay, {
56
- [DF.routeMethod]: routeMethod,
57
- [DF.routeUrlMask]: routeUrlMask,
58
- [DF.delayed]: delayed
59
- })
51
+ setRouteIsDelayed(method, urlMask, delayed) {
52
+ return this.#patch(API.delay, [method, urlMask, delayed])
60
53
  }
61
54
 
62
- setStaticRouteIsDelayed(routeUrlMask, delayed) {
63
- return this.#patch(API.delayStatic, {
64
- [DF.routeUrlMask]: routeUrlMask,
65
- [DF.delayed]: delayed
66
- })
55
+ setStaticRouteIsDelayed(urlMask, delayed) {
56
+ return this.#patch(API.delayStatic, [urlMask, delayed])
67
57
  }
68
58
 
69
- setStaticRouteStatus(routeUrlMask, status) {
70
- return this.#patch(API.staticStatus, {
71
- [DF.routeUrlMask]: routeUrlMask,
72
- [DF.statusCode]: status
73
- })
59
+ setStaticRouteStatus(urlMask, status) {
60
+ return this.#patch(API.staticStatus, [urlMask, status])
74
61
  }
75
62
 
76
- setRouteIsProxied(routeMethod, routeUrlMask, proxied) {
77
- return this.#patch(API.proxied, {
78
- [DF.routeMethod]: routeMethod,
79
- [DF.routeUrlMask]: routeUrlMask,
80
- [DF.proxied]: proxied
81
- })
63
+ setRouteIsProxied(method, urlMask, proxied) {
64
+ return this.#patch(API.proxied, [method, urlMask, proxied])
82
65
  }
83
66
 
84
67
  selectCookie(cookieKey) {
@@ -19,16 +19,9 @@ export const API = {
19
19
  toggle500: MOUNT + '/toggle500'
20
20
  }
21
21
 
22
- export const DF = { // Dashboard Fields (XHR)
23
- routeMethod: 'route_method',
24
- routeUrlMask: 'route_url_mask',
25
- delayed: 'delayed',
26
- proxied: 'proxied',
27
- statusCode: 'status_code',
28
- syncVersion: 'last_received_sync_version'
29
- }
30
-
31
22
  export const HEADER_FOR_502 = 'Mockaton502'
23
+ export const HEADER_FOR_SYNC_VERSION = 'sync_version'
24
+
32
25
  export const DEFAULT_MOCK_COMMENT = '(default)'
33
26
  export const EXT_FOR_UNKNOWN_MIME = 'unknown'
34
27
  export const LONG_POLL_SERVER_TIMEOUT = 8_000
package/src/Dashboard.css CHANGED
@@ -369,7 +369,7 @@ table {
369
369
  > tr:first-child > th {
370
370
  border-top: 0;
371
371
  }
372
-
372
+
373
373
  tr.animIn {
374
374
  opacity: 0;
375
375
  transform: scaleY(0);
package/src/Dashboard.js CHANGED
@@ -1,7 +1,7 @@
1
- import { createElement as r, createSvgElement as s, className, restoreFocus, Defer, Fragment, useRef } from './DashboardDom.js'
2
- import { store, BrokerRowModel } from './DashboardStore.js'
1
+ import { createElement as r, createSvgElement as s, className, restoreFocus, Defer, Fragment } from './DashboardDom.js'
3
2
  import { HEADER_FOR_502 } from './ApiConstants.js'
4
3
  import { parseFilename } from './Filename.js'
4
+ import { store } from './DashboardStore.js'
5
5
 
6
6
 
7
7
  const CSS = {
@@ -56,6 +56,8 @@ const FocusGroup = {
56
56
  PreviewLink: 3
57
57
  }
58
58
 
59
+ const t = translation => translation[0]
60
+
59
61
  store.onError = onError
60
62
  store.render = render
61
63
  store.renderRow = renderRow
@@ -71,9 +73,8 @@ function render() {
71
73
  }
72
74
  render.count = 0
73
75
 
74
- const t = translation => translation[0]
75
76
 
76
- const leftSideRef = useRef()
77
+ const leftSideRef = {}
77
78
 
78
79
  function App() {
79
80
  return [
@@ -81,14 +82,14 @@ function App() {
81
82
  r('main', null,
82
83
  r('div', {
83
84
  ref: leftSideRef,
84
- style: { width: store.leftSideWidth + 'px' },
85
+ style: { width: leftSideRef.width },
85
86
  className: CSS.leftSide
86
87
  },
87
88
  r('table', null,
88
89
  MockList(),
89
90
  StaticFilesList())),
90
91
  r('div', { className: CSS.rightSide },
91
- Resizer(),
92
+ Resizer(leftSideRef),
92
93
  PayloadViewer()))
93
94
  ]
94
95
  }
@@ -159,8 +160,7 @@ function BulkSelector() {
159
160
  },
160
161
  r('option', { value: firstOption }, firstOption),
161
162
  r('hr'),
162
- comments.map(value => r('option', { value }, value))
163
- )))
163
+ comments.map(value => r('option', { value }, value)))))
164
164
  }
165
165
 
166
166
  function CookieSelector() {
@@ -181,9 +181,9 @@ function CookieSelector() {
181
181
 
182
182
 
183
183
  function ProxyFallbackField() {
184
- const checkboxRef = useRef()
184
+ const checkboxRef = {}
185
185
  function onChange() {
186
- checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
186
+ checkboxRef.elem.disabled = !this.validity.valid || !this.value.trim()
187
187
  if (!this.validity.valid)
188
188
  this.reportValidity()
189
189
  else
@@ -239,7 +239,7 @@ function SettingsMenuTrigger() {
239
239
  }
240
240
 
241
241
  function SettingsMenu(id) {
242
- const firstInputRef = useRef()
242
+ const firstInputRef = {}
243
243
  return (
244
244
  r('menu', {
245
245
  id,
@@ -247,7 +247,7 @@ function SettingsMenu(id) {
247
247
  className: CSS.SettingsMenu,
248
248
  onToggle(event) {
249
249
  if (event.newState === 'open')
250
- firstInputRef.current.focus()
250
+ firstInputRef.elem.focus()
251
251
  }
252
252
  },
253
253
 
@@ -307,8 +307,7 @@ function Row(row, i) {
307
307
  method,
308
308
  urlMask,
309
309
  !row.proxied && row.status === 500, // checked
310
- row.opts.length === 1 && row.status === 500 // disabled
311
- )),
310
+ row.opts.length === 1 && row.status === 500)), // disabled
312
311
 
313
312
  !store.groupByMethod && r('td', className(CSS.Method),
314
313
  method),
@@ -329,10 +328,10 @@ function renderRow(method, urlMask) {
329
328
  })
330
329
 
331
330
  function trFor(key) {
332
- return leftSideRef.current.querySelector(`tr[key="${key}"]`)
331
+ return leftSideRef.elem.querySelector(`tr[key="${key}"]`)
333
332
  }
334
333
  function unChooseOld() {
335
- return leftSideRef.current.querySelector(`td > .${CSS.chosen}`)
334
+ return leftSideRef.elem.querySelector(`td > a.${CSS.chosen}`)
336
335
  ?.classList.remove(CSS.chosen)
337
336
  }
338
337
  }
@@ -522,14 +521,14 @@ function ClickDragToggler({ checked, commit, focusGroup }) {
522
521
  TimerIcon()))
523
522
  }
524
523
 
525
- function Resizer() {
524
+ function Resizer(ref) {
526
525
  let raf = 0
527
526
  let initialX = 0
528
- let panelWidth = 0
527
+ let initialWidth = 0
529
528
 
530
529
  function onPointerDown(event) {
531
530
  initialX = event.clientX
532
- panelWidth = leftSideRef.current.clientWidth
531
+ initialWidth = ref.elem.clientWidth
533
532
  addEventListener('pointerup', onUp, { once: true })
534
533
  addEventListener('pointermove', onMove)
535
534
  Object.assign(document.body.style, {
@@ -542,8 +541,8 @@ function Resizer() {
542
541
  function onMove(event) {
543
542
  const MIN_LEFT_WIDTH = 380
544
543
  raf = raf || requestAnimationFrame(() => {
545
- store.leftSideWidth = Math.max(panelWidth - (initialX - event.clientX), MIN_LEFT_WIDTH)
546
- leftSideRef.current.style.width = store.leftSideWidth + 'px'
544
+ ref.width = Math.max(initialWidth - (initialX - event.clientX), MIN_LEFT_WIDTH) + 'px'
545
+ ref.elem.style.width = ref.width
547
546
  raf = 0
548
547
  })
549
548
  }
@@ -569,8 +568,8 @@ function Resizer() {
569
568
 
570
569
  /** # Payload Preview */
571
570
 
572
- const payloadViewerTitleRef = useRef()
573
- const payloadViewerCodeRef = useRef()
571
+ const payloadViewerTitleRef = {}
572
+ const payloadViewerCodeRef = {}
574
573
 
575
574
  function PayloadViewer() {
576
575
  return (
@@ -582,7 +581,7 @@ function PayloadViewer() {
582
581
  !store.hasChosenLink && t`Click a link to preview it`))))
583
582
  }
584
583
 
585
- function PayloadViewerTitle({ file, statusText }) {
584
+ function PayloadViewerTitle(file, statusText) {
586
585
  const { method, status, ext } = parseFilename(file)
587
586
  const fileNameWithComments = file.split('.').slice(0, -3).join('.')
588
587
  return (
@@ -592,13 +591,15 @@ function PayloadViewerTitle({ file, statusText }) {
592
591
  '.' + ext))
593
592
  }
594
593
 
595
- function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad }) {
594
+ function PayloadViewerTitleWhenProxied(response) {
595
+ const mime = response.headers.get('content-type') || ''
596
+ const badGateway = response.headers.get(HEADER_FOR_502)
596
597
  return (
597
598
  r('span', null,
598
- gatewayIsBad
599
+ badGateway
599
600
  ? r('span', null, t`⛔ Fallback Backend Error` + ' ')
600
601
  : r('span', null, t`Got` + ' '),
601
- r('abbr', { title: statusText }, status),
602
+ r('abbr', { title: response.statusText }, response.status),
602
603
  ' ' + mime))
603
604
  }
604
605
 
@@ -616,8 +617,8 @@ async function previewMock() {
616
617
  previewMock.controller = new AbortController
617
618
 
618
619
  const spinnerTimer = setTimeout(() => {
619
- payloadViewerTitleRef.current.replaceChildren(t`Fetching…`)
620
- payloadViewerCodeRef.current.replaceChildren(PayloadViewerProgressBar())
620
+ payloadViewerTitleRef.elem.replaceChildren(t`Fetching…`)
621
+ payloadViewerCodeRef.elem.replaceChildren(PayloadViewerProgressBar())
621
622
  }, SPINNER_DELAY)
622
623
 
623
624
  try {
@@ -631,37 +632,29 @@ async function previewMock() {
631
632
  await updatePayloadViewer(proxied, file, response)
632
633
  }
633
634
  catch {
634
- payloadViewerCodeRef.current.replaceChildren()
635
+ payloadViewerCodeRef.elem.replaceChildren()
635
636
  }
636
637
  }
637
638
 
638
639
  async function updatePayloadViewer(proxied, file, response) {
639
640
  const mime = response.headers.get('content-type') || ''
640
641
 
641
- payloadViewerTitleRef.current.replaceChildren(
642
- proxied
643
- ? PayloadViewerTitleWhenProxied({
644
- mime,
645
- status: response.status,
646
- statusText: response.statusText,
647
- gatewayIsBad: response.headers.get(HEADER_FOR_502)
648
- })
649
- : PayloadViewerTitle({
650
- file,
651
- statusText: response.statusText
652
- }))
653
-
654
- if (mime.startsWith('image/')) // Naively assumes GET.200
655
- payloadViewerCodeRef.current.replaceChildren(
656
- r('img', { src: URL.createObjectURL(await response.blob()) }))
642
+ payloadViewerTitleRef.elem.replaceChildren(proxied
643
+ ? PayloadViewerTitleWhenProxied(response)
644
+ : PayloadViewerTitle(file, response.statusText))
645
+
646
+ if (mime.startsWith('image/')) // Naively assumes GET 200
647
+ payloadViewerCodeRef.elem.replaceChildren(r('img', {
648
+ src: URL.createObjectURL(await response.blob())
649
+ }))
657
650
  else {
658
651
  const body = await response.text() || t`/* Empty Response Body */`
659
652
  if (mime === 'application/json')
660
- payloadViewerCodeRef.current.replaceChildren(r('span', className(CSS.json), SyntaxJSON(body)))
653
+ payloadViewerCodeRef.elem.replaceChildren(r('span', className(CSS.json), SyntaxJSON(body)))
661
654
  else if (isXML(mime))
662
- payloadViewerCodeRef.current.replaceChildren(SyntaxXML(body))
655
+ payloadViewerCodeRef.elem.replaceChildren(SyntaxXML(body))
663
656
  else
664
- payloadViewerCodeRef.current.textContent = body
657
+ payloadViewerCodeRef.elem.textContent = body
665
658
  }
666
659
  }
667
660
 
@@ -671,38 +664,47 @@ function isXML(mime) {
671
664
  }
672
665
 
673
666
 
674
- async function onError(_error) {
675
- let error = _error
667
+ async function onError(error) {
668
+ if (error?.name === 'AbortError')
669
+ return
676
670
 
677
- if (_error instanceof Response) {
678
- if (_error.status === 422)
679
- error = await _error.text()
680
- else if (_error.statusText)
681
- error = _error.statusText
671
+ let msg = ''
672
+ let isOffline = false
673
+
674
+ if (error instanceof Response) {
675
+ if (error.status === 422)
676
+ msg = await error.text()
677
+ else if (error.statusText)
678
+ msg = error.statusText
682
679
  }
683
- else {
684
- if (error?.name === 'AbortError')
685
- return
686
- if (error?.message === 'Failed to fetch')
687
- error = t`Looks like the Mockaton server is not running` // TODO clear Error if comes back in ui-sync
688
- else
689
- error = error || t`Unexpected Error`
680
+ else if (error?.message === 'Failed to fetch') {
681
+ msg = t`Looks like the Mockaton server is not running`
682
+ isOffline = true
690
683
  }
691
- showErrorToast(error)
692
- console.error(_error)
684
+ else
685
+ msg = error?.message || t`Unexpected Error`
686
+
687
+ ErrorToast(msg, isOffline)
688
+ console.error(error)
693
689
  }
694
690
 
695
- function showErrorToast(msg) {
696
- document.getElementsByClassName(CSS.ErrorToast)[0]?.remove()
691
+ function ErrorToast(msg, isOffline) {
692
+ ErrorToast.isOffline = isOffline
693
+ ErrorToast.ref.elem?.remove()
697
694
  document.body.appendChild(
698
695
  r('div', {
696
+ role: 'alert',
697
+ ref: ErrorToast.ref,
699
698
  className: CSS.ErrorToast,
700
- onClick() {
701
- const toast = this
702
- document.startViewTransition(() => toast.remove())
703
- }
699
+ onClick: ErrorToast.close
704
700
  }, msg))
705
701
  }
702
+ ErrorToast.isOffline = false
703
+ ErrorToast.ref = {}
704
+ ErrorToast.close = () => {
705
+ document.startViewTransition(() =>
706
+ ErrorToast.ref.elem?.remove())
707
+ }
706
708
 
707
709
 
708
710
  /** # Icons */
@@ -732,7 +734,7 @@ function SettingsIcon() {
732
734
  * The version increments when a mock file is added, removed, or renamed.
733
735
  */
734
736
  function initRealTimeUpdates() {
735
- let oldSyncVersion = -1
737
+ let oldVersion = -1
736
738
  let controller = new AbortController()
737
739
 
738
740
  longPoll()
@@ -747,12 +749,15 @@ function initRealTimeUpdates() {
747
749
 
748
750
  async function longPoll() {
749
751
  try {
750
- const response = await store.getSyncVersion(oldSyncVersion, controller.signal)
752
+ const response = await store.getSyncVersion(oldVersion, controller.signal)
751
753
  if (response.ok) {
752
- const syncVersion = await response.json()
753
- const skipUpdate = oldSyncVersion === -1
754
- if (oldSyncVersion !== syncVersion) { // because it could be < or >
755
- oldSyncVersion = syncVersion
754
+ if (ErrorToast.isOffline)
755
+ ErrorToast.close()
756
+
757
+ const version = await response.json()
758
+ const skipUpdate = oldVersion === -1
759
+ if (oldVersion !== version) { // because it could be < or >
760
+ oldVersion = version
756
761
  if (!skipUpdate)
757
762
  store.fetchState()
758
763
  }
@@ -804,7 +809,7 @@ function initKeyboardNavigation() {
804
809
  }
805
810
 
806
811
  function allInFocusGroup(focusGroup) {
807
- return Array.from(leftSideRef.current.querySelectorAll(
812
+ return Array.from(leftSideRef.elem.querySelectorAll(
808
813
  `tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
809
814
  }
810
815
 
@@ -3,15 +3,15 @@ export function className(...args) {
3
3
  }
4
4
 
5
5
  export function createElement(tag, props, ...children) {
6
- const node = document.createElement(tag)
6
+ const elem = document.createElement(tag)
7
7
  for (const [k, v] of Object.entries(props || {}))
8
- if (k === 'ref') v.current = node
9
- else if (k === 'style') Object.assign(node.style, v)
10
- else if (k.startsWith('on')) node.addEventListener(k.slice(2).toLowerCase(), ...[v].flat())
11
- else if (k in node) node[k] = v
12
- else node.setAttribute(k, v)
13
- node.append(...children.flat().filter(Boolean))
14
- return node
8
+ if (k === 'ref') v.elem = elem
9
+ else if (k === 'style') Object.assign(elem.style, v)
10
+ else if (k.startsWith('on')) elem.addEventListener(k.slice(2).toLowerCase(), ...[v].flat())
11
+ else if (k in elem) elem[k] = v
12
+ else elem.setAttribute(k, v)
13
+ elem.append(...children.flat().filter(Boolean))
14
+ return elem
15
15
  }
16
16
 
17
17
  export function createSvgElement(tagName, props, ...children) {
@@ -22,10 +22,6 @@ export function createSvgElement(tagName, props, ...children) {
22
22
  return elem
23
23
  }
24
24
 
25
- export function useRef() {
26
- return { current: null }
27
- }
28
-
29
25
  export function Fragment(...args) {
30
26
  const frag = new DocumentFragment()
31
27
  for (const arg of args)
@@ -11,8 +11,8 @@ export const store = {
11
11
  render() {},
12
12
  renderRow(method, urlMask) {},
13
13
 
14
- brokersByMethod: /** @type {State.brokersByMethod} */ {},
15
- staticBrokers: /** @type {State.staticBrokers} */ {},
14
+ brokersByMethod: /** @type ClientBrokersByMethod */ {},
15
+ staticBrokers: /** @type ClientStaticBrokers */ {},
16
16
 
17
17
  cookies: [],
18
18
  comments: [],
@@ -37,8 +37,6 @@ export const store = {
37
37
  },
38
38
 
39
39
 
40
- leftSideWidth: undefined,
41
-
42
40
  groupByMethod: initPreference('groupByMethod'),
43
41
  toggleGroupByMethod() {
44
42
  store.groupByMethod = !store.groupByMethod
@@ -47,17 +45,13 @@ export const store = {
47
45
  },
48
46
 
49
47
 
50
- chosenLink: {
51
- method: '',
52
- urlMask: ''
53
- },
54
- get hasChosenLink() {
55
- return store.chosenLink.method
56
- && store.chosenLink.urlMask
57
- },
48
+ chosenLink: { method: '', urlMask: '' },
58
49
  setChosenLink(method, urlMask) {
59
50
  store.chosenLink = { method, urlMask }
60
51
  },
52
+ get hasChosenLink() {
53
+ return store.chosenLink.method && store.chosenLink.urlMask
54
+ },
61
55
 
62
56
 
63
57
  async reset() {
@@ -277,7 +271,7 @@ function togglePreference(param, nextVal) {
277
271
  * the repeated folder paths are kept but styled differently.
278
272
  * @param {string[]} paths - sorted
279
273
  */
280
- export function dittoSplitPaths(paths) {
274
+ function dittoSplitPaths(paths) {
281
275
  const result = [['', paths[0]]]
282
276
  const pathsInParts = paths.map(p => p.split('/').filter(Boolean))
283
277
 
@@ -327,14 +321,14 @@ dittoSplitPaths.test = function () {
327
321
  deferred(dittoSplitPaths.test)
328
322
 
329
323
 
330
- export class BrokerRowModel {
324
+ class BrokerRowModel {
331
325
  opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
332
326
  isNew = false
333
327
  key = ''
334
328
  method = ''
335
329
  urlMask = ''
336
330
  urlMaskDittoed = ['', '']
337
- #broker = /** @type {ClientMockBroker} */ {}
331
+ #broker = /** @type ClientMockBroker */ {}
338
332
  #canProxy = false
339
333
 
340
334
  /**
@@ -456,12 +450,12 @@ const TestBrokerRowModel = {
456
450
  deferred(() => Object.values(TestBrokerRowModel).forEach(t => t()))
457
451
 
458
452
 
459
- export class StaticBrokerRowModel {
453
+ class StaticBrokerRowModel {
460
454
  isNew = false
461
455
  key = ''
462
456
  method = 'GET'
463
457
  urlMaskDittoed = ['', '']
464
- #broker = /** @type {ClientStaticBroker} */ {}
458
+ #broker = /** @type ClientStaticBroker */ {}
465
459
 
466
460
  /** @param {ClientStaticBroker} broker */
467
461
  constructor(broker) {
@@ -23,7 +23,6 @@ export async function dispatchMock(req, response) {
23
23
  await proxy(req, response, broker?.delayed ? calcDelay() : 0)
24
24
  return
25
25
  }
26
-
27
26
  if (!broker) {
28
27
  sendMockNotFound(response)
29
28
  return
package/src/Watcher.js CHANGED
@@ -10,7 +10,7 @@ import * as mockBrokerCollection from './mockBrokersCollection.js'
10
10
 
11
11
  /**
12
12
  * ARR = Add, Remove, or Rename Mock Event
13
- *
13
+ *
14
14
  * The emitter is debounced so it handles e.g. bulk deletes,
15
15
  * and also renames, which are two events (delete + add).
16
16
  */
@@ -44,22 +44,19 @@ export function watchMocksDir() {
44
44
  if (!file)
45
45
  return
46
46
 
47
- const path = join(dir, file)
48
-
49
- if (isDirectory(path)) {
47
+ if (isDirectory(join(dir, file))) {
50
48
  mockBrokerCollection.init()
51
49
  uiSyncVersion.increment()
52
- return
53
- }
54
-
55
- if (isFile(path)) {
56
- if (mockBrokerCollection.registerMock(file, Boolean('isFromWatcher')))
57
- uiSyncVersion.increment()
58
50
  }
59
- else {
51
+ else if (!isFile(join(dir, file))) { // file deleted
60
52
  mockBrokerCollection.unregisterMock(file)
61
53
  uiSyncVersion.increment()
62
54
  }
55
+ else if (mockBrokerCollection.registerMock(file, Boolean('isFromWatcher')))
56
+ uiSyncVersion.increment()
57
+ else {
58
+ // ignore file edits
59
+ }
63
60
  })
64
61
  }
65
62
 
@@ -67,27 +64,25 @@ export function watchStaticDir() {
67
64
  const dir = config.staticDir
68
65
  if (!dir)
69
66
  return
67
+
70
68
  watch(dir, { recursive: true, persistent: false }, (_, file) => {
71
69
  if (!file)
72
70
  return
73
71
 
74
- const path = join(dir, file)
75
-
76
- if (isDirectory(path)) {
72
+ if (isDirectory(join(dir, file))) {
77
73
  staticCollection.init()
78
74
  uiSyncVersion.increment()
79
- return
80
- }
81
-
82
- if (isFile(path)) {
83
- if (staticCollection.registerMock(file))
84
- uiSyncVersion.increment()
85
75
  }
86
- else {
76
+ else if (!isFile(join(dir, file))) { // file deleted
87
77
  staticCollection.unregisterMock(file)
88
78
  uiSyncVersion.increment()
89
79
  }
80
+ else if (staticCollection.registerMock(file))
81
+ uiSyncVersion.increment()
82
+ else {
83
+ // ignore file edits
84
+ }
90
85
  })
91
86
  }
92
87
 
93
- // TODO config changes
88
+ // TODO ThinkAbout watching for config changes
package/src/config.js CHANGED
@@ -60,7 +60,7 @@ for (const [k, [defaultVal, validator]] of Object.entries(schema)) {
60
60
  validators[k] = validator
61
61
  }
62
62
 
63
- /** @type {Config} */
63
+ /** @type Config */
64
64
  export const config = Object.seal(defaults)
65
65
 
66
66
  /** @type {Record<keyof Config, (val: unknown) => val is Config[keyof Config]>} */
@@ -105,16 +105,16 @@ export function brokerByRoute(method, url) {
105
105
 
106
106
  export function extractAllComments() {
107
107
  const comments = new Set()
108
- forEachBroker(broker => {
109
- for (const c of broker.extractComments())
108
+ forEachBroker(b => {
109
+ for (const c of b.extractComments())
110
110
  comments.add(c)
111
111
  })
112
112
  return Array.from(comments)
113
113
  }
114
114
 
115
115
  export function setMocksMatchingComment(comment) {
116
- forEachBroker(broker =>
117
- broker.setByMatchingComment(comment))
116
+ forEachBroker(b =>
117
+ b.setByMatchingComment(comment))
118
118
  }
119
119
 
120
120
  function forEachBroker(fn) {
package/src/utils/mime.js CHANGED
@@ -105,9 +105,9 @@ export function mimeFor(filename) {
105
105
  }
106
106
  function extname(filename) {
107
107
  const i = filename.lastIndexOf('.')
108
- return i >= 0
109
- ? filename.substring(i + 1)
110
- : ''
108
+ return i === -1
109
+ ? ''
110
+ : filename.slice(i + 1)
111
111
  }
112
112
 
113
113