mockaton 8.20.1 → 8.20.2

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/README.md CHANGED
@@ -5,21 +5,39 @@
5
5
  [![Test](https://github.com/ericfortis/mockaton/actions/workflows/test.yml/badge.svg)](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
6
6
  [![CodeQL](https://github.com/ericfortis/mockaton/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/ericfortis/mockaton/actions/workflows/github-code-scanning/codeql)
7
7
 
8
- An HTTP mock server for simulating APIs with minimal setup
9
- — ideal for testing difficult to reproduce backend states.
8
+ An HTTP mock server for simulating APIs with minimal setup — ideal for testing
9
+ difficult to reproduce states. For example, triggering an error on a third-party API.
10
+ Similarly, if you are a frontend developer, triggering it on your project’s backend.
10
11
 
12
+ <br/>
13
+
14
+ ## Motivation
15
+
16
+ **No API state should be too hard to test.**
17
+ With Mockaton, developers can achieve correctness without sacrificing speed.
18
+
19
+ ### Correctness
20
+ - Enables testing of complex or rare scenarios that would otherwise be skipped.
21
+ - Allows for deterministic, comprehensive, and consistent state.
22
+
23
+ ### Speed
24
+ - Prevents progress from being blocked by waiting for APIs.
25
+ - Avoids spinning up and updating hefty backends when developing UIs.
26
+
27
+ <br/>
11
28
 
12
29
  ## Overview
13
30
  With Mockaton, you don’t need to write code for wiring up your
14
31
  mocks. Instead, a given directory is scanned for filenames
15
- following a convention similar to the URLs.
32
+ following a convention similar to the URLs.
16
33
 
17
- For example, for [/api/user/123](#), the mock filename could be:
34
+ For example, for [/api/user/123](#), the filename could be:
18
35
 
19
36
  <pre>
20
37
  <code>my-mocks-dir/<b>api/user</b>/[user-id].GET.200.json</code>
21
38
  </pre>
22
39
 
40
+ <br/>
23
41
 
24
42
  ## Dashboard
25
43
 
@@ -62,30 +80,34 @@ api/videos.GET.<b>500</b>.txt # Internal Server Error
62
80
 
63
81
  <br/>
64
82
 
65
- ## Scraping mocks from your Backend
83
+ ## Scraping Mocks from your Backend
66
84
 
67
85
  ### Option 1: Browser Extension
68
- This [browser extension](https://github.com/ericfortis/download-http-requests-browser-ext)
69
- lets you download all the HTTP responses, and they
86
+ This companion [browser-devtools extension](https://github.com/ericfortis/download-http-requests-browser-ext)
87
+ lets you download all the HTTP responses at once, and they
70
88
  get saved following Mockaton’s filename convention.
71
89
 
72
90
  ### Option 2: Fallback to Your Backend
73
- This option could be a bit elaborate if your backend uses third-party auth,
91
+ <details>
92
+ <summary>Details</summary>
93
+
94
+ This option could be a bit elaborate if your backend uses third-party auth,
74
95
  because you’d have to manually inject cookies or `sessionStorage` tokens.
75
96
 
76
97
  On the other hand, proxying to your backend is straightforward if your backend
77
- handles the session cookie, or if you can develop without auth.
98
+ handles the session cookie, or if you can develop without auth.
78
99
 
79
100
  Either way you can forward requests to your backend for routes you don’t have
80
101
  mocks for, or routes that have the ☁️ **Cloud Checkbox** checked. In addition, by
81
102
  checking ✅ **Save Mocks**, you can collect the responses that hit your backend.
82
103
  They will be saved in your `config.mocksDir` following the filename convention.
104
+ </details>
83
105
 
84
106
 
85
107
  <br/>
86
108
 
87
109
 
88
- ## Basic Usage (See below for Node < 23.6)
110
+ ## Basic Usage
89
111
  ```sh
90
112
  npm install mockaton --save-dev
91
113
  ```
@@ -95,23 +117,25 @@ Create a `my-mockaton.js` file
95
117
  import { resolve } from 'node:path'
96
118
  import { Mockaton } from 'mockaton'
97
119
 
98
- // See the Config section for more options
99
120
  Mockaton({
100
121
  mocksDir: resolve('my-mocks-dir'), // must exist
101
122
  port: 2345
102
- })
123
+ }) // The Config section below documents more options
103
124
  ```
104
125
 
105
126
  ```sh
106
127
  node my-mockaton.js
107
128
  ```
108
129
 
109
- ### TypeScript with Node < 23.6
130
+ <details>
131
+ <summary>About TypeScript in Node < 23.6</summary>
110
132
  If you want to write mocks in TypeScript in a version older than Node 23.6:
133
+
111
134
  ```shell
112
135
  npm install tsx
113
136
  node --import=tsx my-mockaton.js
114
137
  ```
138
+ </details>
115
139
 
116
140
 
117
141
  <br/>
@@ -128,11 +152,13 @@ npm run start # in another terminal
128
152
  ```
129
153
 
130
154
  The demo app has a list of colors containing all of their possible states. For example,
131
- permutations for out-of-stock, new-arrival, and discontinued. It looks like this:
155
+ permutations for out-of-stock, new-arrival, and discontinued.
132
156
 
133
157
  <img src="./demo-app-vite/pixaton-tests/pic-for-readme.vp740x880.light.gold.png" alt="Mockaton Demo App Screenshot" width="740" />
134
158
 
135
159
  <br/>
160
+ <br/>
161
+
136
162
 
137
163
  ## Use Cases
138
164
  ### Testing Backend or Frontend
@@ -163,21 +189,12 @@ putting its built assets in `config.staticDir`. And simulate the flow by Bulk Se
163
189
  The [aot-fetch-demo repo](https://github.com/ericfortis/aot-fetch-demo) has a working example.
164
190
 
165
191
 
166
- <br/>
167
-
168
- ## Motivation
169
- - Avoids spinning up and updating hefty backends when developing UIs.
170
- - Allows for a deterministic, comprehensive, and consistent backend state. For example, having
171
- a collection with all the possible state variants helps for spotting inadvertent bugs.
172
- - Sometimes frontend progress is blocked by waiting for backend APIs.
173
-
174
192
  <br/>
175
193
 
176
194
 
177
195
  ## You can write JSON mocks in JavaScript or TypeScript
178
196
  For example, `api/foo.GET.200.js`
179
197
 
180
-
181
198
  **Option A:** An Object, Array, or String is sent as JSON.
182
199
 
183
200
  ```js
@@ -189,7 +206,7 @@ export default { foo: 'bar' }
189
206
  Return a `string | Buffer | Uint8Array`, but don’t call `response.end()`
190
207
 
191
208
  ```js
192
- export default (request, response) =>
209
+ export default (request, response) =>
193
210
  JSON.stringify({ foo: 'bar' })
194
211
  ```
195
212
 
@@ -206,6 +223,7 @@ you want to concatenate newly added colors.
206
223
  ```js
207
224
  import { parseJSON } from 'mockaton'
208
225
 
226
+
209
227
  export default async function insertColor(request, response) {
210
228
  const color = await parseJSON(request)
211
229
  globalThis.newColorsDatabase ??= []
@@ -223,6 +241,7 @@ export default async function insertColor(request, response) {
223
241
  ```js
224
242
  import colorsFixture from './colors.json' with { type: 'json' }
225
243
 
244
+
226
245
  export default function listColors() {
227
246
  return JSON.stringify([
228
247
  ...colorsFixture,
@@ -305,7 +324,7 @@ A filename can have many comments.
305
324
  <br/>
306
325
 
307
326
  ### Default mock for a route
308
- You can add the comment: `(default)`.
327
+ You can add the comment: `(default)`.
309
328
  Otherwise, the first file in **alphabetical order** wins.
310
329
 
311
330
  <pre>
@@ -322,7 +341,8 @@ api/video<b>?limit=[limit]</b>.GET.200.json
322
341
  </pre>
323
342
 
324
343
  On Windows, filenames containing "?" are [not
325
- permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query string it’s ignored anyway.
344
+ permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query
345
+ string it’s ignored anyway.
326
346
 
327
347
  <br/>
328
348
 
@@ -353,9 +373,9 @@ it’s convenient for serving 200 GET requests without having to add the filenam
353
373
  extension convention. For example, for using Mockaton as a standalone demo server,
354
374
  as explained above in the _Use Cases_ section.
355
375
 
356
- Files under `config.staticDir` don’t use the filename convention, and they take
357
- precedence over corresponding `GET` mocks in `config.mocksDir` (regardless
358
- of status code). For example, if you have two files for `GET /foo/bar.jpg`:
376
+ Files under `config.staticDir` take precedence over corresponding
377
+ `GET` mocks in `config.mocksDir` (regardless of status code).
378
+ For example, if you have two files for `GET /foo/bar.jpg`:
359
379
  <pre>
360
380
  my-static-dir<b>/foo/bar.jpg</b> <span style="color:green"> // Wins</span>
361
381
  my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg <span style="color:red"> // Unreachable</span>
@@ -365,7 +385,9 @@ my-static-dir<b>/foo/bar.jpg</b> <span style="color:green"> // Wins</span>
365
385
  <br/>
366
386
 
367
387
  ### `ignore?: RegExp`
368
- Defaults to `/(\.DS_Store|~)$/`
388
+ Defaults to `/(\.DS_Store|~)$/`. The regex rule is
389
+ tested against the basename (filename without directory path).
390
+
369
391
 
370
392
  <br/>
371
393
 
@@ -378,13 +400,13 @@ Defaults to `0`, which means auto-assigned
378
400
 
379
401
  <br/>
380
402
 
381
- ### `delay?: number`
403
+ ### `delay?: number`
382
404
  Defaults to `1200` milliseconds. Although routes can individually be delayed
383
405
  with the 🕓 Checkbox, the amount is globally configurable with this option.
384
406
 
385
407
  ### `delayJitter?: number`
386
408
  Defaults to `0`. Range: `[0.0, 3.0]`. Maximum percentage of the delay to add.
387
- For example, `0.5` will add at most `600ms` to the default delay.
409
+ For example, `0.5` will add at most `600ms` to the default delay.
388
410
 
389
411
  <br/>
390
412
 
@@ -396,7 +418,7 @@ or that you manually picked with the ☁️ **Cloud Checkbox**.
396
418
 
397
419
  ### `collectProxied?: boolean`
398
420
  Defaults to `false`. With this flag you can save mocks that hit
399
- your proxy fallback to `config.mocksDir`. If the URL has v4 UUIDs,
421
+ your proxy fallback to `config.mocksDir`. If the URL has v4 UUIDs,
400
422
  the filename will have `[id]` in their place. For example:
401
423
 
402
424
  <pre>
@@ -423,7 +445,7 @@ the predefined list. For that, you can add it to <code>config.extraMimes</code>
423
445
 
424
446
 
425
447
  ### `formatCollectedJSON?: boolean`
426
- Defaults to `true`. Saves the mock with two spaces indentation &mdash;
448
+ Defaults to `true`. Saves the mock with two spaces indentation &mdash;
427
449
  the formatting output of `JSON.stringify(data, null, ' ')`
428
450
 
429
451
 
@@ -435,6 +457,7 @@ the formatting output of `JSON.stringify(data, null, ' ')`
435
457
  ```js
436
458
  import { jwtCookie } from 'mockaton'
437
459
 
460
+
438
461
  config.cookies = {
439
462
  'My Admin User': 'my-cookie=1;Path=/;SameSite=strict',
440
463
  'My Normal User': 'my-cookie=0;Path=/;SameSite=strict',
@@ -449,7 +472,7 @@ The selected cookie, which is the first one by default, is sent in every respons
449
472
  `Set-Cookie` header (as long as its value is not an empty string). The object key is just
450
473
  a label for UI display purposes, and also for selecting a cookie via the Commander API.
451
474
 
452
- If you need to send more than one cookie, you can inject them globally
475
+ If you need to send more than one cookie, you can inject them globally
453
476
  in `config.extraHeaders`, or individually in a function `.js` or `.ts` mock.
454
477
 
455
478
  By the way, the `jwtCookie` helper has a hardcoded header and signature.
@@ -509,12 +532,13 @@ import { parse } from 'yaml'
509
532
  import { readFileSync } from 'node:js'
510
533
  import { jsToJsonPlugin } from 'mockaton'
511
534
 
535
+
512
536
  config.plugins = [
513
-
537
+
514
538
  // Although `jsToJsonPlugin` is set by default, you need to include it if you need it.
515
539
  // IOW, your plugins array overwrites the default list. This way you can remove it.
516
- [/\.(js|ts)$/, jsToJsonPlugin],
517
-
540
+ [/\.(js|ts)$/, jsToJsonPlugin],
541
+
518
542
  [/\.yml$/, yamlToJsonPlugin],
519
543
  [/foo\.GET\.200\.txt$/, capitalizePlugin], // e.g. GET /api/foo would be capitalized
520
544
  ]
@@ -575,6 +599,7 @@ All of its methods return their `fetch` response promise.
575
599
  ```js
576
600
  import { Commander } from 'mockaton'
577
601
 
602
+
578
603
  const myMockatonAddr = 'http://localhost:2345'
579
604
  const mockaton = new Commander(myMockatonAddr)
580
605
  ```
@@ -649,11 +674,17 @@ example, if you are polling, and you want to test the state change.
649
674
  - Chrome DevTools allows for [overriding responses](https://developer.chrome.com/docs/devtools/overrides)
650
675
  - Reverse Proxies such as [Burp](https://portswigger.net/burp) are also handy for overriding responses
651
676
 
652
- ### Client side
677
+ ### Client Side
653
678
  In contrast to Mockaton, which is an HTTP Server, these programs
654
- mock the client (e.g., `fetch`) in Node.js and browsers.
679
+ hijack the client (e.g., `fetch`) in Node.js and browsers.
655
680
 
656
- - [MSW (Mock Server Worker)](https://mswjs.io)
681
+ - [Mock Server Worker (MSW)](https://mswjs.io)
657
682
  - [Nock](https://github.com/nock/nock)
658
683
  - [Fetch Mock](https://github.com/wheresrhys/fetch-mock)
659
684
  - [Mentoss](https://github.com/humanwhocodes/mentoss) Has a server side too
685
+
686
+ <br/>
687
+
688
+ ---
689
+
690
+ ![](./fixtures-mocks/api/user/avatar.GET.200.png)
package/index.d.ts CHANGED
@@ -55,33 +55,31 @@ export function jwtCookie(cookieName: string, payload: any, path?: string): stri
55
55
 
56
56
  export function parseJSON(request: IncomingMessage): Promise<any>
57
57
 
58
+ export type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
58
59
 
59
- export class Commander {
60
- constructor(addr: string)
61
60
 
62
- listMocks(): Promise<Response>
61
+ // API
63
62
 
64
- select(file: string): Promise<Response>
65
-
66
- bulkSelectByComment(comment: string): Promise<Response>
67
-
68
-
69
- setRouteIsDelayed(routeMethod: string, routeUrlMask: string, delayed: boolean): Promise<Response>
70
-
71
-
72
- listCookies(): Promise<Response>
73
-
74
- selectCookie(cookieKey: string): Promise<Response>
75
-
76
-
77
- listComments(): Promise<Response>
78
-
79
- setProxyFallback(proxyAddr: string): Promise<Response>
80
-
81
- reset(): Promise<Response>
63
+ export type ClientMockBroker = {
64
+ mocks: string[]
65
+ currentMock: {
66
+ file: string
67
+ delayed: boolean
68
+ }
69
+ }
70
+ export type ClientBrokersByMethod = {
71
+ [method: string]: {
72
+ [urlMask: string]: ClientMockBroker
73
+ }
74
+ }
82
75
 
76
+ export type ClientStaticBroker = {
77
+ route: string
78
+ delayed: boolean
79
+ status: number
80
+ }
81
+ export type ClientStaticBrokers = {
82
+ [route: string]: ClientStaticBroker
83
+ }
83
84
 
84
- getCorsAllowed(): Promise<Response>
85
85
 
86
- setCorsAllowed(value: boolean): Promise<Response>
87
- }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "8.20.1",
5
+ "version": "8.20.2",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -18,12 +18,12 @@
18
18
  "scripts": {
19
19
  "test": "node --test \"src/**/*.test.js\"",
20
20
  "coverage": "node --test --test-reporter=lcov --test-reporter-destination=.coverage/lcov.info --experimental-test-coverage \"src/**/*.test.js\"",
21
- "start": "node dev-mockaton.js",
21
+ "start": "node --watch dev-mockaton.js",
22
22
  "pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none \"pixaton-tests/**/*.test.js\"",
23
23
  "outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
24
24
  },
25
25
  "optionalDependencies": {
26
- "open": "^10.0.0"
26
+ "open": "^10"
27
27
  },
28
28
  "devDependencies": {
29
29
  "pixaton": "1.1.2",
package/src/Api.js CHANGED
@@ -101,7 +101,7 @@ function longPollClientSyncVersion(req, response) {
101
101
 
102
102
  function reinitialize(_, response) {
103
103
  mockBrokersCollection.init()
104
- staticCollection.init() // TESTME
104
+ staticCollection.init()
105
105
  sendOK(response)
106
106
  }
107
107
 
@@ -131,17 +131,17 @@ async function setRouteIsDelayed(req, response) {
131
131
  body[DF.routeMethod],
132
132
  body[DF.routeUrlMask])
133
133
 
134
- if (!broker) // TESTME
134
+ if (!broker)
135
135
  sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
136
136
  else if (typeof delayed !== 'boolean')
137
- sendUnprocessableContent(response, `Expected a boolean for "delayed"`) // TESTME
137
+ sendUnprocessableContent(response, `Expected boolean for "delayed"`)
138
138
  else {
139
139
  broker.setDelayed(delayed)
140
140
  sendOK(response)
141
141
  }
142
142
  }
143
143
 
144
- async function setRouteIsProxied(req, response) { // TESTME
144
+ async function setRouteIsProxied(req, response) {
145
145
  const body = await parseJSON(req)
146
146
  const proxied = body[DF.proxied]
147
147
  const broker = mockBrokersCollection.brokerByRoute(
@@ -151,7 +151,7 @@ async function setRouteIsProxied(req, response) { // TESTME
151
151
  if (!broker)
152
152
  sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
153
153
  else if (typeof proxied !== 'boolean')
154
- sendUnprocessableContent(response, `Expected a boolean for "proxied"`)
154
+ sendUnprocessableContent(response, `Expected boolean for "proxied"`)
155
155
  else if (proxied && !config.proxyFallback)
156
156
  sendUnprocessableContent(response, `There’s no proxy fallback`)
157
157
  else {
@@ -163,10 +163,10 @@ async function setRouteIsProxied(req, response) { // TESTME
163
163
  async function updateProxyFallback(req, response) {
164
164
  const fallback = await parseJSON(req)
165
165
  if (!ConfigValidator.proxyFallback(fallback)) {
166
- sendUnprocessableContent(response)
166
+ sendUnprocessableContent(response, `Invalid Proxy Fallback URL`)
167
167
  return
168
168
  }
169
- if (!fallback) // TESTME
169
+ if (!fallback)
170
170
  mockBrokersCollection.ensureAllRoutesHaveSelectedMock()
171
171
  config.proxyFallback = fallback
172
172
  sendOK(response)
@@ -174,8 +174,8 @@ async function updateProxyFallback(req, response) {
174
174
 
175
175
  async function setCollectProxied(req, response) {
176
176
  const collectProxied = await parseJSON(req)
177
- if (!ConfigValidator.collectProxied(collectProxied)) { // TESTME
178
- sendUnprocessableContent(response)
177
+ if (!ConfigValidator.collectProxied(collectProxied)) {
178
+ sendUnprocessableContent(response, `Expected a boolean for "collectProxied"`)
179
179
  return
180
180
  }
181
181
  config.collectProxied = collectProxied
@@ -188,12 +188,22 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
188
188
  }
189
189
 
190
190
  async function setCorsAllowed(req, response) {
191
- config.corsAllowed = await parseJSON(req)
191
+ const corsAllowed = await parseJSON(req)
192
+ if (!ConfigValidator.corsAllowed(corsAllowed)) {
193
+ sendUnprocessableContent(response, `Expected boolean for "corsAllowed"`)
194
+ return
195
+ }
196
+ config.corsAllowed = corsAllowed
192
197
  sendOK(response)
193
198
  }
194
199
 
195
- async function setGlobalDelay(req, response) { // TESTME
196
- config.delay = parseInt(await parseJSON(req), 10)
200
+ async function setGlobalDelay(req, response) {
201
+ const delay = await parseJSON(req)
202
+ if (!ConfigValidator.delay(delay)) {
203
+ sendUnprocessableContent(response, `Expected non-negative integer for "delay"`)
204
+ return
205
+ }
206
+ config.delay = delay
197
207
  sendOK(response)
198
208
  }
199
209
 
@@ -203,10 +213,10 @@ async function setStaticRouteStatusCode(req, response) {
203
213
  const status = Number(body[DF.statusCode])
204
214
  const broker = staticCollection.brokerByRoute(body[DF.routeUrlMask])
205
215
 
206
- if (!broker) // TESTME
207
- sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]}`)
216
+ if (!broker)
217
+ sendUnprocessableContent(response, `Static route does not exist: ${body[DF.routeUrlMask]}`)
208
218
  else if (!(status === 200 || status === 404))
209
- sendUnprocessableContent(response, `Expected a 200 or 404 status code. Received ${status}`) // TESTME
219
+ sendUnprocessableContent(response, `Expected 200 or 404 status code`)
210
220
  else {
211
221
  broker.setStatus(status)
212
222
  sendOK(response)
@@ -219,10 +229,10 @@ async function setStaticRouteIsDelayed(req, response) {
219
229
  const delayed = body[DF.delayed]
220
230
  const broker = staticCollection.brokerByRoute(body[DF.routeUrlMask])
221
231
 
222
- if (!broker) // TESTME
223
- sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]}`)
232
+ if (!broker)
233
+ sendUnprocessableContent(response, `Static route does not exist: ${body[DF.routeUrlMask]}`)
224
234
  else if (typeof delayed !== 'boolean')
225
- sendUnprocessableContent(response, `Expected a boolean for "delayed". Received ${delayed}`) // TESTME
235
+ sendUnprocessableContent(response, `Expected boolean for "delayed"`)
226
236
  else {
227
237
  broker.setDelayed(delayed)
228
238
  sendOK(response)
package/src/Commander.js CHANGED
@@ -18,13 +18,65 @@ export class Commander {
18
18
  })
19
19
  }
20
20
 
21
+ /** @type {JsonPromise<ClientBrokersByMethod>} */
21
22
  listMocks() {
22
23
  return this.#get(API.mocks)
23
24
  }
24
25
 
26
+ /** @type {JsonPromise<ClientStaticBrokers>} */
27
+ listStaticFiles() {
28
+ return this.#get(API.static)
29
+ }
30
+
31
+ /** @type {JsonPromise<[label:string, selected:boolean][]>} */
32
+ listCookies() {
33
+ return this.#get(API.cookies)
34
+ }
35
+
36
+ /** @type {JsonPromise<string[]>} */
37
+ listComments() {
38
+ return this.#get(API.comments)
39
+ }
40
+
41
+ /** @type {JsonPromise<string>} */
42
+ getProxyFallback() {
43
+ return this.#get(API.fallback)
44
+ }
45
+
46
+ /** @type {JsonPromise<boolean>} */
47
+ getCollectProxied() {
48
+ return this.#get(API.collectProxied)
49
+ }
50
+
51
+ /** @type {JsonPromise<boolean>} */
52
+ getCorsAllowed() {
53
+ return this.#get(API.cors)
54
+ }
55
+
56
+ /** @type {JsonPromise<number>} */
57
+ getGlobalDelay() {
58
+ return this.#get(API.globalDelay)
59
+ }
60
+
61
+ /** @type {JsonPromise<number>} */
62
+ getSyncVersion(currentSyncVersion, abortSignal) {
63
+ return fetch(API.syncVersion, {
64
+ signal: AbortSignal.any([abortSignal, AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)]),
65
+ headers: {
66
+ [DF.syncVersion]: currentSyncVersion
67
+ }
68
+ })
69
+ }
70
+
71
+
72
+ reset() {
73
+ return this.#patch(API.reset)
74
+ }
75
+
25
76
  select(file) {
26
77
  return this.#patch(API.select, file)
27
78
  }
79
+
28
80
  bulkSelectByComment(comment) {
29
81
  return this.#patch(API.bulkSelect, comment)
30
82
  }
@@ -59,59 +111,23 @@ export class Commander {
59
111
  })
60
112
  }
61
113
 
62
- listCookies() {
63
- return this.#get(API.cookies)
64
- }
65
114
  selectCookie(cookieKey) {
66
115
  return this.#patch(API.cookies, cookieKey)
67
116
  }
68
117
 
69
- listComments() {
70
- return this.#get(API.comments)
71
- }
72
-
73
- getProxyFallback() {
74
- return this.#get(API.fallback)
75
- }
76
118
  setProxyFallback(proxyAddr) {
77
119
  return this.#patch(API.fallback, proxyAddr)
78
120
  }
79
121
 
80
- getCollectProxied() {
81
- return this.#get(API.collectProxied)
82
- }
83
122
  setCollectProxied(shouldCollect) {
84
123
  return this.#patch(API.collectProxied, shouldCollect)
85
124
  }
86
125
 
87
- getCorsAllowed() {
88
- return this.#get(API.cors)
89
- }
90
126
  setCorsAllowed(value) {
91
127
  return this.#patch(API.cors, value)
92
128
  }
93
129
 
94
- getGlobalDelay() {
95
- return this.#get(API.globalDelay)
96
- }
97
130
  setGlobalDelay(delay) {
98
131
  return this.#patch(API.globalDelay, delay)
99
132
  }
100
-
101
- listStaticFiles() {
102
- return this.#get(API.static)
103
- }
104
-
105
- reset() {
106
- return this.#patch(API.reset)
107
- }
108
-
109
- getSyncVersion(currentSyncVersion, abortSignal) {
110
- return fetch(API.syncVersion, {
111
- signal: AbortSignal.any([abortSignal, AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)]),
112
- headers: {
113
- [DF.syncVersion]: currentSyncVersion
114
- }
115
- })
116
- }
117
133
  }
package/src/Dashboard.js CHANGED
@@ -38,83 +38,117 @@ const Strings = {
38
38
  }
39
39
 
40
40
  const CSS = {
41
- BulkSelector: 'BulkSelector',
42
- DelayToggler: 'DelayToggler',
43
- ErrorToast: 'ErrorToast',
44
- FallbackBackend: 'FallbackBackend',
45
- Field: 'Field',
46
- GlobalDelayField: 'GlobalDelayField',
47
- Help: 'Help',
48
- InternalServerErrorToggler: 'InternalServerErrorToggler',
49
- MainLeftSide: 'leftSide',
50
- MainRightSide: 'rightSide',
51
- MockList: 'MockList',
52
- MockSelector: 'MockSelector',
53
- NotFoundToggler: 'NotFoundToggler',
54
- PayloadViewer: 'PayloadViewer',
55
- PreviewLink: 'PreviewLink',
56
- ProgressBar: 'ProgressBar',
57
- ProxyToggler: 'ProxyToggler',
58
- ResetButton: 'ResetButton',
59
- SaveProxiedCheckbox: 'SaveProxiedCheckbox',
60
- StaticFilesList: 'StaticFilesList',
61
-
62
- chosen: 'chosen',
63
- dittoDir: 'dittoDir',
64
- empty: 'empty',
65
- nonDefault: 'nonDefault',
66
- red: 'red',
67
- status4xx: 'status4xx'
41
+ BulkSelector: null,
42
+ DelayToggler: null,
43
+ ErrorToast: null,
44
+ FallbackBackend: null,
45
+ Field: null,
46
+ GlobalDelayField: null,
47
+ Help: null,
48
+ InternalServerErrorToggler: null,
49
+ MockList: null,
50
+ MockSelector: null,
51
+ NotFoundToggler: null,
52
+ PayloadViewer: null,
53
+ PreviewLink: null,
54
+ ProgressBar: null,
55
+ ProxyToggler: null,
56
+ ResetButton: null,
57
+ SaveProxiedCheckbox: null,
58
+ StaticFilesList: null,
59
+
60
+ chosen: null,
61
+ dittoDir: null,
62
+ empty: null,
63
+ leftSide: null,
64
+ nonDefault: null,
65
+ red: null,
66
+ rightSide: null,
67
+ status4xx: null
68
+ }
69
+ for (const k of Object.keys(CSS))
70
+ CSS[k] = k
71
+
72
+
73
+ const state = {
74
+ /** @type {ClientBrokersByMethod} */
75
+ brokersByMethod: {},
76
+
77
+ /** @type {ClientStaticBrokers} */
78
+ staticBrokers: {},
79
+
80
+ /** @type {[label:string, selected:boolean][]} */
81
+ cookies: [],
82
+
83
+ /** @type {string[]} */
84
+ comments: [],
85
+
86
+ delay: 0,
87
+
88
+ collectProxied: false,
89
+
90
+ fallbackAddress: '',
91
+
92
+ get canProxy() {
93
+ return Boolean(this.fallbackAddress)
94
+ }
68
95
  }
69
96
 
70
- const r = createElement
71
- const s = createSvgElement
72
-
73
97
  const mockaton = new Commander(window.location.origin)
74
- let globalDelay = 1200
75
-
76
- init()
98
+ updateState()
77
99
  initLongPoll()
78
-
79
- function init() {
80
- return Promise.all([
100
+ function updateState() {
101
+ Promise.all([
81
102
  mockaton.listMocks(),
103
+ mockaton.listStaticFiles(),
82
104
  mockaton.listCookies(),
83
105
  mockaton.listComments(),
84
106
  mockaton.getGlobalDelay(),
85
107
  mockaton.getCollectProxied(),
86
- mockaton.getProxyFallback(),
87
- mockaton.listStaticFiles()
108
+ mockaton.getProxyFallback()
88
109
  ].map(api => api.then(response => response.ok && response.json())))
89
- .then(data => document.body.replaceChildren(...App(data)))
110
+ .then(data => {
111
+ state.brokersByMethod = data[0]
112
+ state.staticBrokers = data[1]
113
+ state.cookies = data[2]
114
+ state.comments = data[3]
115
+ state.delay = data[4]
116
+ state.collectProxied = data[5]
117
+ state.fallbackAddress = data[6]
118
+ document.body.replaceChildren(...App())
119
+ })
90
120
  .catch(onError)
91
121
  }
92
122
 
93
- function App([brokersByMethod, cookies, comments, delay, collectProxied, fallbackAddress, staticBrokers]) {
94
- globalDelay = delay
95
- const canProxy = Boolean(fallbackAddress)
123
+ const r = createElement
124
+ const s = createSvgElement
125
+
126
+ function App() {
96
127
  return [
97
- r(Header, { cookies, comments, delay, fallbackAddress, collectProxied }),
128
+ r(Header),
98
129
  r('main', null,
99
- r('div', { className: CSS.MainLeftSide },
100
- r(MockList, { brokersByMethod, canProxy }),
101
- r(StaticFilesList, { brokers: staticBrokers, canProxy })),
102
- r('div', { className: CSS.MainRightSide },
103
- r(PayloadViewer)))]
130
+ r('div', className(CSS.leftSide),
131
+ r(MockList),
132
+ r(StaticFilesList)),
133
+ r('div', className(CSS.rightSide),
134
+ r(PayloadViewer)))
135
+ ]
104
136
  }
105
137
 
106
138
 
107
- /** # Header */
108
-
109
- function Header({ cookies, comments, delay, fallbackAddress, collectProxied }) {
139
+ function Header() {
110
140
  return (
111
141
  r('header', null,
112
- r(Logo),
142
+ r('img', {
143
+ alt: Strings.title,
144
+ src: 'mockaton/logo.svg',
145
+ width: 160
146
+ }),
113
147
  r('div', null,
114
- r(CookieSelector, { cookies }),
115
- r(BulkSelector, { comments }),
116
- r(GlobalDelayField, { delay }),
117
- r(ProxyFallbackField, { fallbackAddress, collectProxied }),
148
+ r(CookieSelector),
149
+ r(BulkSelector),
150
+ r(GlobalDelayField),
151
+ r(ProxyFallbackField),
118
152
  r(ResetButton)),
119
153
  r('a', {
120
154
  className: CSS.Help,
@@ -124,22 +158,17 @@ function Header({ cookies, comments, delay, fallbackAddress, collectProxied }) {
124
158
  }, r(HelpIcon))))
125
159
  }
126
160
 
127
- function Logo() {
128
- return (
129
- r('img', {
130
- alt: Strings.title,
131
- src: 'mockaton/logo.svg',
132
- width: 160
133
- }))
134
- }
135
161
 
136
- function CookieSelector({ cookies }) {
162
+ function CookieSelector() {
163
+ const { cookies } = state
137
164
  function onChange() {
138
- mockaton.selectCookie(this.value).catch(onError)
165
+ mockaton.selectCookie(this.value)
166
+ .then(parseError)
167
+ .catch(onError)
139
168
  }
140
169
  const disabled = cookies.length <= 1
141
170
  return (
142
- r('label', { className: CSS.Field },
171
+ r('label', className(CSS.Field),
143
172
  r('span', null, Strings.cookie),
144
173
  r('select', {
145
174
  autocomplete: 'off',
@@ -150,7 +179,9 @@ function CookieSelector({ cookies }) {
150
179
  r('option', { value, selected }, value)))))
151
180
  }
152
181
 
153
- function BulkSelector({ comments }) {
182
+
183
+ function BulkSelector() {
184
+ const { comments } = state
154
185
  // UX wise this should be a menu instead of this `select`.
155
186
  // But this way is easier to implement, with a few hacks.
156
187
  const firstOption = Strings.pick_comment
@@ -158,7 +189,8 @@ function BulkSelector({ comments }) {
158
189
  const value = this.value
159
190
  this.value = firstOption // Hack
160
191
  mockaton.bulkSelectByComment(value)
161
- .then(init)
192
+ .then(parseError)
193
+ .then(updateState)
162
194
  .catch(onError)
163
195
  }
164
196
  const disabled = !comments.length
@@ -166,7 +198,7 @@ function BulkSelector({ comments }) {
166
198
  ? []
167
199
  : [firstOption].concat(comments)
168
200
  return (
169
- r('label', { className: CSS.Field },
201
+ r('label', className(CSS.Field),
170
202
  r('span', null, Strings.bulk_select),
171
203
  r('select', {
172
204
  className: CSS.BulkSelector,
@@ -179,13 +211,17 @@ function BulkSelector({ comments }) {
179
211
  r('option', { value }, value)))))
180
212
  }
181
213
 
182
- function GlobalDelayField({ delay }) {
214
+
215
+ function GlobalDelayField() {
216
+ const { delay } = state
183
217
  function onChange() {
184
- globalDelay = this.valueAsNumber
185
- mockaton.setGlobalDelay(globalDelay).catch(onError)
218
+ state.delay = this.valueAsNumber
219
+ mockaton.setGlobalDelay(state.delay)
220
+ .then(parseError)
221
+ .catch(onError)
186
222
  }
187
223
  return (
188
- r('label', { className: cssClass(CSS.Field, CSS.GlobalDelayField) },
224
+ r('label', className(CSS.Field, CSS.GlobalDelayField),
189
225
  r('span', null, r(TimerIcon), Strings.delay_ms),
190
226
  r('input', {
191
227
  type: 'number',
@@ -197,7 +233,9 @@ function GlobalDelayField({ delay }) {
197
233
  })))
198
234
  }
199
235
 
200
- function ProxyFallbackField({ fallbackAddress, collectProxied }) {
236
+
237
+ function ProxyFallbackField() {
238
+ const { fallbackAddress, collectProxied } = state
201
239
  function onChange() {
202
240
  const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
203
241
  saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
@@ -206,11 +244,12 @@ function ProxyFallbackField({ fallbackAddress, collectProxied }) {
206
244
  this.reportValidity()
207
245
  else
208
246
  mockaton.setProxyFallback(this.value.trim())
209
- .then(init)
247
+ .then(parseError)
248
+ .then(updateState)
210
249
  .catch(onError)
211
250
  }
212
251
  return (
213
- r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
252
+ r('div', className(CSS.Field, CSS.FallbackBackend),
214
253
  r('label', null,
215
254
  r('span', null, r(CloudIcon), Strings.fallback_server),
216
255
  r('input', {
@@ -226,12 +265,15 @@ function ProxyFallbackField({ fallbackAddress, collectProxied }) {
226
265
  })))
227
266
  }
228
267
 
229
- function SaveProxiedCheckbox({ disabled, collectProxied }) {
268
+ function SaveProxiedCheckbox({ disabled }) {
269
+ const { collectProxied } = state
230
270
  function onChange() {
231
- mockaton.setCollectProxied(this.checked).catch(onError)
271
+ mockaton.setCollectProxied(this.checked)
272
+ .then(parseError)
273
+ .catch(onError)
232
274
  }
233
275
  return (
234
- r('label', { className: CSS.SaveProxiedCheckbox },
276
+ r('label', className(CSS.SaveProxiedCheckbox),
235
277
  r('input', {
236
278
  type: 'checkbox',
237
279
  disabled,
@@ -241,34 +283,39 @@ function SaveProxiedCheckbox({ disabled, collectProxied }) {
241
283
  r('span', null, Strings.save_proxied)))
242
284
  }
243
285
 
286
+
244
287
  function ResetButton() {
288
+ function onClick() {
289
+ mockaton.reset()
290
+ .then(parseError)
291
+ .then(updateState)
292
+ .catch(onError)
293
+ }
245
294
  return (
246
295
  r('button', {
247
296
  className: CSS.ResetButton,
248
- onClick() {
249
- mockaton.reset()
250
- .then(init)
251
- .catch(onError)
252
- }
297
+ onClick
253
298
  }, Strings.reset))
254
299
  }
255
300
 
256
301
 
302
+
257
303
  /** # MockList */
258
304
 
259
- function MockList({ brokersByMethod, canProxy }) {
260
- const hasMocks = Object.keys(brokersByMethod).length
261
- if (!hasMocks)
305
+ function MockList() {
306
+ const { brokersByMethod } = state
307
+ if (!Object.keys(brokersByMethod).length)
262
308
  return (
263
- r('div', { className: CSS.empty },
309
+ r('div', className(CSS.empty),
264
310
  Strings.no_mocks_found))
265
311
  return (
266
312
  r('div', null,
267
313
  r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
268
- r(SectionByMethod, { method, brokers, canProxy })))))
314
+ r(SectionByMethod, { method, brokers })))))
269
315
  }
270
316
 
271
- function SectionByMethod({ method, brokers, canProxy }) {
317
+ function SectionByMethod({ method, brokers }) {
318
+ const canProxy = state.canProxy
272
319
  const brokersSorted = Object.entries(brokers)
273
320
  .filter(([, broker]) => broker.mocks.length > 1) // >1 because of autogen500
274
321
  .sort((a, b) => a[0].localeCompare(b[0]))
@@ -309,16 +356,17 @@ function PreviewLink({ method, urlMask, urlMaskDittoed }) {
309
356
  href: urlMask,
310
357
  onClick
311
358
  }, ditto
312
- ? [r('span', { className: CSS.dittoDir }, ditto), tail]
359
+ ? [r('span', className(CSS.dittoDir), ditto), tail]
313
360
  : tail))
314
361
  }
315
362
 
316
- /** @param {{ broker: MockBroker }} props */
363
+ /** @param {{ broker: ClientMockBroker }} props */
317
364
  function MockSelector({ broker }) {
318
365
  function onChange() {
319
366
  const { urlMask, method } = parseFilename(this.value)
320
367
  mockaton.select(this.value)
321
- .then(init)
368
+ .then(parseError)
369
+ .then(updateState)
322
370
  .then(() => linkFor(method, urlMask)?.click())
323
371
  .catch(onError)
324
372
  }
@@ -339,7 +387,7 @@ function MockSelector({ broker }) {
339
387
  autocomplete: 'off',
340
388
  'data-qaid': urlMask,
341
389
  disabled: files.length <= 1,
342
- className: cssClass(
390
+ ...className(
343
391
  CSS.MockSelector,
344
392
  selected !== files[0] && CSS.nonDefault,
345
393
  status >= 400 && status < 500 && CSS.status4xx)
@@ -350,11 +398,13 @@ function MockSelector({ broker }) {
350
398
  }, file))))
351
399
  }
352
400
 
353
- /** @param {{ broker: MockBroker }} props */
401
+ /** @param {{ broker: ClientMockBroker }} props */
354
402
  function DelayRouteToggler({ broker }) {
355
403
  function onChange() {
356
404
  const { method, urlMask } = parseFilename(broker.mocks[0])
357
- mockaton.setRouteIsDelayed(method, urlMask, this.checked).catch(onError)
405
+ mockaton.setRouteIsDelayed(method, urlMask, this.checked)
406
+ .then(parseError)
407
+ .catch(onError)
358
408
  }
359
409
  return (
360
410
  r('label', {
@@ -369,7 +419,7 @@ function DelayRouteToggler({ broker }) {
369
419
  TimerIcon()))
370
420
  }
371
421
 
372
- /** @param {{ broker: MockBroker }} props */
422
+ /** @param {{ broker: ClientMockBroker }} props */
373
423
  function InternalServerErrorToggler({ broker }) {
374
424
  function onChange() {
375
425
  const { urlMask, method } = parseFilename(broker.mocks[0])
@@ -377,7 +427,8 @@ function InternalServerErrorToggler({ broker }) {
377
427
  this.checked
378
428
  ? broker.mocks.find(f => parseFilename(f).status === 500)
379
429
  : broker.mocks[0])
380
- .then(init)
430
+ .then(parseError)
431
+ .then(updateState)
381
432
  .then(() => linkFor(method, urlMask)?.click())
382
433
  .catch(onError)
383
434
  }
@@ -395,12 +446,13 @@ function InternalServerErrorToggler({ broker }) {
395
446
  r('span', null, '500')))
396
447
  }
397
448
 
398
- /** @param {{ broker: MockBroker }} props */
449
+ /** @param {{ broker: ClientMockBroker }} props */
399
450
  function ProxyToggler({ broker }) {
400
451
  function onChange() {
401
452
  const { urlMask, method } = parseFilename(broker.mocks[0])
402
453
  mockaton.setRouteIsProxied(method, urlMask, this.checked)
403
- .then(init)
454
+ .then(parseError)
455
+ .then(updateState)
404
456
  .then(() => linkFor(method, urlMask)?.click())
405
457
  .catch(onError)
406
458
  }
@@ -418,24 +470,25 @@ function ProxyToggler({ broker }) {
418
470
  }
419
471
 
420
472
 
421
- /**
422
- * # StaticFilesList
423
- * @param {{ brokers: StaticBroker[] }} props
424
- */
425
- function StaticFilesList({ brokers, canProxy }) {
426
- if (!Object.keys(brokers).length)
473
+
474
+ /** # StaticFilesList */
475
+
476
+ function StaticFilesList() {
477
+ const { staticBrokers } = state
478
+ const canProxy = state.canProxy
479
+ if (!Object.keys(staticBrokers).length)
427
480
  return null
428
- const dp = dittoSplitPaths(Object.keys(brokers)).map(([ditto, tail]) => ditto
429
- ? [r('span', { className: CSS.dittoDir }, ditto), tail]
481
+ const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
482
+ ? [r('span', className(CSS.dittoDir), ditto), tail]
430
483
  : tail)
431
484
  return (
432
- r('table', { className: CSS.StaticFilesList },
485
+ r('table', className(CSS.StaticFilesList),
433
486
  r('thead', null,
434
487
  r('tr', null,
435
488
  r('th', { colspan: 2 + Number(canProxy) }),
436
489
  r('th', null, Strings.static_get))),
437
490
  r('tbody', null,
438
- Object.values(brokers).map((broker, i) =>
491
+ Object.values(staticBrokers).map((broker, i) =>
439
492
  r('tr', null,
440
493
  canProxy && r('td', null, r(ProxyStaticToggler, {})),
441
494
  r('td', null, r(DelayStaticRouteToggler, { broker })),
@@ -444,11 +497,11 @@ function StaticFilesList({ brokers, canProxy }) {
444
497
  )))))
445
498
  }
446
499
 
447
-
448
- /** @param {{ broker: StaticBroker }} props */
500
+ /** @param {{ broker: ClientStaticBroker }} props */
449
501
  function DelayStaticRouteToggler({ broker }) {
450
502
  function onChange() {
451
503
  mockaton.setStaticRouteIsDelayed(broker.route, this.checked)
504
+ .then(parseError)
452
505
  .catch(onError)
453
506
  }
454
507
  return (
@@ -464,10 +517,11 @@ function DelayStaticRouteToggler({ broker }) {
464
517
  TimerIcon()))
465
518
  }
466
519
 
467
- /** @param {{ broker: StaticBroker }} props */
520
+ /** @param {{ broker: ClientStaticBroker }} props */
468
521
  function NotFoundToggler({ broker }) {
469
522
  function onChange() {
470
523
  mockaton.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
524
+ .then(parseError)
471
525
  .catch(onError)
472
526
  }
473
527
  return (
@@ -500,6 +554,8 @@ function ProxyStaticToggler({}) { // TODO
500
554
  r(CloudIcon)))
501
555
  }
502
556
 
557
+
558
+
503
559
  /** # Payload Preview */
504
560
 
505
561
  const payloadViewerTitleRef = useRef()
@@ -507,17 +563,16 @@ const payloadViewerRef = useRef()
507
563
 
508
564
  function PayloadViewer() {
509
565
  return (
510
- r('div', { className: CSS.PayloadViewer },
566
+ r('div', className(CSS.PayloadViewer),
511
567
  r('h2', { ref: payloadViewerTitleRef }, Strings.preview),
512
568
  r('pre', null,
513
569
  r('code', { ref: payloadViewerRef }, Strings.click_link_to_preview))))
514
570
  }
515
571
 
516
-
517
572
  function PayloadViewerProgressBar() {
518
573
  return (
519
- r('div', { className: CSS.ProgressBar },
520
- r('div', { style: { animationDuration: globalDelay + 'ms' } })))
574
+ r('div', className(CSS.ProgressBar),
575
+ r('div', { style: { animationDuration: state.delay + 'ms' } })))
521
576
  }
522
577
 
523
578
  function PayloadViewerTitle({ file, status, statusText }) {
@@ -532,7 +587,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
532
587
  return (
533
588
  r('span', null,
534
589
  gatewayIsBad
535
- ? r('span', { className: CSS.red }, Strings.fallback_server_error + ' ')
590
+ ? r('span', className(CSS.red), Strings.fallback_server_error + ' ')
536
591
  : r('span', null, Strings.got + ' '),
537
592
  r('abbr', { title: statusText }, status),
538
593
  ' ' + mime))
@@ -610,9 +665,19 @@ function mockSelectorFor(method, urlMask) {
610
665
 
611
666
  /** # Misc */
612
667
 
668
+ async function parseError(response) {
669
+ if (response.ok)
670
+ return
671
+ if (response.status === 422)
672
+ throw await response.text()
673
+ throw response.statusText
674
+ }
675
+
613
676
  function onError(error) {
614
677
  if (error?.message === 'Failed to fetch')
615
678
  showErrorToast('Looks like the Mockaton server is not running')
679
+ else
680
+ showErrorToast(error || 'Unexpected Error')
616
681
  console.error(error)
617
682
  }
618
683
 
@@ -673,7 +738,7 @@ async function poll() {
673
738
  const syncVersion = await response.json()
674
739
  if (poll.oldSyncVersion !== syncVersion) { // because it could be < or >
675
740
  poll.oldSyncVersion = syncVersion
676
- await init()
741
+ await updateState()
677
742
  }
678
743
  poll()
679
744
  }
@@ -689,30 +754,27 @@ async function poll() {
689
754
 
690
755
  /** # Utils */
691
756
 
692
- function cssClass(...args) {
693
- return args.filter(Boolean).join(' ')
757
+ function className(...args) {
758
+ return { className: args.filter(Boolean).join(' ') }
694
759
  }
695
760
 
696
761
 
697
- /** ## React-compatible simplified implementations */
698
-
699
- function createElement(elem, props = null, ...children) {
762
+ function createElement(elem, props, ...children) {
700
763
  if (typeof elem === 'function')
701
764
  return elem(props)
702
765
 
703
766
  const node = document.createElement(elem)
704
- if (props)
705
- for (const [key, value] of Object.entries(props))
706
- if (key === 'ref')
707
- value.current = node
708
- else if (key.startsWith('on'))
709
- node.addEventListener(key.replace(/^on/, '').toLowerCase(), value)
710
- else if (key === 'style')
711
- Object.assign(node.style, value)
712
- else if (key in node)
713
- node[key] = value
714
- else
715
- node.setAttribute(key, value)
767
+ for (const [key, value] of Object.entries(props || {}))
768
+ if (key === 'ref')
769
+ value.current = node
770
+ else if (key.startsWith('on'))
771
+ node.addEventListener(key.replace(/^on/, '').toLowerCase(), value)
772
+ else if (key === 'style')
773
+ Object.assign(node.style, value)
774
+ else if (key in node)
775
+ node[key] = value
776
+ else
777
+ node.setAttribute(key, value)
716
778
  node.append(...children.flat().filter(Boolean))
717
779
  return node
718
780
  }
@@ -730,7 +792,6 @@ function useRef() {
730
792
  }
731
793
 
732
794
 
733
-
734
795
  /**
735
796
  * Think of this as a way of printing a directory tree in which
736
797
  * the repeated folder paths are kept but styled differently.
package/src/Mockaton.js CHANGED
@@ -63,7 +63,7 @@ async function onRequest(req, response) {
63
63
  }
64
64
  catch (error) {
65
65
  if (error instanceof BodyReaderError)
66
- sendUnprocessableContent(response, error.name)
66
+ sendUnprocessableContent(response, `${error.name}: ${error.message}`)
67
67
  else
68
68
  sendInternalServerError(response, error)
69
69
  }
@@ -9,8 +9,9 @@ import { parseFilename, filenameIsValid } from './Filename.js'
9
9
 
10
10
  /**
11
11
  * @type {{
12
- * [method: string]:
13
- * { [route: string]: MockBroker }
12
+ * [method: string]: {
13
+ * [urlMask: string]: MockBroker
14
+ * }
14
15
  * }}
15
16
  * @example
16
17
  * {
@@ -3,7 +3,7 @@ import { listFilesRecursively } from './utils/fs.js'
3
3
  import { config, isFileAllowed } from './config.js'
4
4
 
5
5
 
6
- class StaticBroker {
6
+ export class StaticBroker {
7
7
  constructor(route) {
8
8
  this.route = route
9
9
  this.delayed = false
@@ -14,7 +14,7 @@ class StaticBroker {
14
14
  setStatus(value) { this.status = value }
15
15
  }
16
16
 
17
- /** @type {{ [route: string]: StaticBroker }} */
17
+ /** @type {{ [route:string]: StaticBroker }} */
18
18
  let collection = {}
19
19
 
20
20
  export const all = () => collection
@@ -4,7 +4,13 @@ import { METHODS } from 'node:http'
4
4
  export const SUPPORTED_METHODS = METHODS
5
5
  export const methodIsSupported = method => SUPPORTED_METHODS.includes(method)
6
6
 
7
- export class BodyReaderError extends Error {name = 'BodyReaderError'}
7
+ export class BodyReaderError extends Error {
8
+ name = 'BodyReaderError'
9
+ constructor(msg) {
10
+ super()
11
+ this.message = msg
12
+ }
13
+ }
8
14
 
9
15
  export const parseJSON = req => readBody(req, JSON.parse)
10
16
 
@@ -31,13 +37,13 @@ export function readBody(req, parser = a => a) {
31
37
  req.removeListener('end', onEnd)
32
38
  req.removeListener('error', onEnd)
33
39
  if (lengthSoFar !== expectedLength)
34
- reject(new BodyReaderError())
40
+ reject(new BodyReaderError('Length mismatch'))
35
41
  else
36
42
  try {
37
43
  resolve(parser(Buffer.concat(body).toString()))
38
44
  }
39
45
  catch (_) {
40
- reject(new BodyReaderError())
46
+ reject(new BodyReaderError('Could not parse'))
41
47
  }
42
48
  }
43
49
  })