mockaton 8.20.1 → 8.20.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/README.md CHANGED
@@ -6,20 +6,39 @@
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
8
  An HTTP mock server for simulating APIs with minimal setup
9
- — ideal for testing difficult to reproduce backend states.
9
+ — ideal for testing difficult to reproduce states.
10
10
 
11
+ <br/>
12
+
13
+ ## Motivation
14
+
15
+ **No API state should be too hard to test.**
16
+ With Mockaton, developers can achieve correctness without sacrificing speed.
17
+
18
+ ### Correctness
19
+ - Enables testing of complex scenarios that would otherwise be
20
+ skipped. For example, triggering an error on a third-party API. Or if
21
+ you are a frontend developer, triggering it on your project’s backend.
22
+ - Allows for deterministic, comprehensive, and consistent state.
23
+
24
+ ### Speed
25
+ - Prevents progress from being blocked by waiting for APIs.
26
+ - Avoids spinning up and updating hefty backends when developing UIs.
27
+
28
+ <br/>
11
29
 
12
30
  ## Overview
13
31
  With Mockaton, you don’t need to write code for wiring up your
14
32
  mocks. Instead, a given directory is scanned for filenames
15
- following a convention similar to the URLs.
33
+ following a convention similar to the URLs.
16
34
 
17
- For example, for [/api/user/123](#), the mock filename could be:
35
+ For example, for [/api/user/123](#), the filename could be:
18
36
 
19
37
  <pre>
20
38
  <code>my-mocks-dir/<b>api/user</b>/[user-id].GET.200.json</code>
21
39
  </pre>
22
40
 
41
+ <br/>
23
42
 
24
43
  ## Dashboard
25
44
 
@@ -62,30 +81,34 @@ api/videos.GET.<b>500</b>.txt # Internal Server Error
62
81
 
63
82
  <br/>
64
83
 
65
- ## Scraping mocks from your Backend
84
+ ## Scraping Mocks from your Backend
66
85
 
67
86
  ### 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
87
+ This companion [browser-devtools extension](https://github.com/ericfortis/download-http-requests-browser-ext)
88
+ lets you download all the HTTP responses at once, and they
70
89
  get saved following Mockaton’s filename convention.
71
90
 
72
91
  ### Option 2: Fallback to Your Backend
73
- This option could be a bit elaborate if your backend uses third-party auth,
92
+ <details>
93
+ <summary>Details</summary>
94
+
95
+ This option could be a bit elaborate if your backend uses third-party auth,
74
96
  because you’d have to manually inject cookies or `sessionStorage` tokens.
75
97
 
76
98
  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.
99
+ handles the session cookie, or if you can develop without auth.
78
100
 
79
101
  Either way you can forward requests to your backend for routes you don’t have
80
102
  mocks for, or routes that have the ☁️ **Cloud Checkbox** checked. In addition, by
81
103
  checking ✅ **Save Mocks**, you can collect the responses that hit your backend.
82
104
  They will be saved in your `config.mocksDir` following the filename convention.
105
+ </details>
83
106
 
84
107
 
85
108
  <br/>
86
109
 
87
110
 
88
- ## Basic Usage (See below for Node < 23.6)
111
+ ## Basic Usage
89
112
  ```sh
90
113
  npm install mockaton --save-dev
91
114
  ```
@@ -95,23 +118,25 @@ Create a `my-mockaton.js` file
95
118
  import { resolve } from 'node:path'
96
119
  import { Mockaton } from 'mockaton'
97
120
 
98
- // See the Config section for more options
99
121
  Mockaton({
100
122
  mocksDir: resolve('my-mocks-dir'), // must exist
101
123
  port: 2345
102
- })
124
+ }) // The Config section below documents more options
103
125
  ```
104
126
 
105
127
  ```sh
106
128
  node my-mockaton.js
107
129
  ```
108
130
 
109
- ### TypeScript with Node < 23.6
131
+ <details>
132
+ <summary>About TypeScript in Node < 23.6</summary>
110
133
  If you want to write mocks in TypeScript in a version older than Node 23.6:
134
+
111
135
  ```shell
112
136
  npm install tsx
113
137
  node --import=tsx my-mockaton.js
114
138
  ```
139
+ </details>
115
140
 
116
141
 
117
142
  <br/>
@@ -128,11 +153,13 @@ npm run start # in another terminal
128
153
  ```
129
154
 
130
155
  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:
156
+ permutations for out-of-stock, new-arrival, and discontinued.
132
157
 
133
158
  <img src="./demo-app-vite/pixaton-tests/pic-for-readme.vp740x880.light.gold.png" alt="Mockaton Demo App Screenshot" width="740" />
134
159
 
135
160
  <br/>
161
+ <br/>
162
+
136
163
 
137
164
  ## Use Cases
138
165
  ### Testing Backend or Frontend
@@ -163,21 +190,12 @@ putting its built assets in `config.staticDir`. And simulate the flow by Bulk Se
163
190
  The [aot-fetch-demo repo](https://github.com/ericfortis/aot-fetch-demo) has a working example.
164
191
 
165
192
 
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
193
  <br/>
175
194
 
176
195
 
177
196
  ## You can write JSON mocks in JavaScript or TypeScript
178
197
  For example, `api/foo.GET.200.js`
179
198
 
180
-
181
199
  **Option A:** An Object, Array, or String is sent as JSON.
182
200
 
183
201
  ```js
@@ -189,7 +207,7 @@ export default { foo: 'bar' }
189
207
  Return a `string | Buffer | Uint8Array`, but don’t call `response.end()`
190
208
 
191
209
  ```js
192
- export default (request, response) =>
210
+ export default (request, response) =>
193
211
  JSON.stringify({ foo: 'bar' })
194
212
  ```
195
213
 
@@ -206,6 +224,7 @@ you want to concatenate newly added colors.
206
224
  ```js
207
225
  import { parseJSON } from 'mockaton'
208
226
 
227
+
209
228
  export default async function insertColor(request, response) {
210
229
  const color = await parseJSON(request)
211
230
  globalThis.newColorsDatabase ??= []
@@ -223,6 +242,7 @@ export default async function insertColor(request, response) {
223
242
  ```js
224
243
  import colorsFixture from './colors.json' with { type: 'json' }
225
244
 
245
+
226
246
  export default function listColors() {
227
247
  return JSON.stringify([
228
248
  ...colorsFixture,
@@ -305,7 +325,7 @@ A filename can have many comments.
305
325
  <br/>
306
326
 
307
327
  ### Default mock for a route
308
- You can add the comment: `(default)`.
328
+ You can add the comment: `(default)`.
309
329
  Otherwise, the first file in **alphabetical order** wins.
310
330
 
311
331
  <pre>
@@ -322,7 +342,8 @@ api/video<b>?limit=[limit]</b>.GET.200.json
322
342
  </pre>
323
343
 
324
344
  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.
345
+ permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query
346
+ string it’s ignored anyway.
326
347
 
327
348
  <br/>
328
349
 
@@ -353,9 +374,9 @@ it’s convenient for serving 200 GET requests without having to add the filenam
353
374
  extension convention. For example, for using Mockaton as a standalone demo server,
354
375
  as explained above in the _Use Cases_ section.
355
376
 
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`:
377
+ Files under `config.staticDir` take precedence over corresponding
378
+ `GET` mocks in `config.mocksDir` (regardless of status code).
379
+ For example, if you have two files for `GET /foo/bar.jpg`:
359
380
  <pre>
360
381
  my-static-dir<b>/foo/bar.jpg</b> <span style="color:green"> // Wins</span>
361
382
  my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg <span style="color:red"> // Unreachable</span>
@@ -365,7 +386,9 @@ my-static-dir<b>/foo/bar.jpg</b> <span style="color:green"> // Wins</span>
365
386
  <br/>
366
387
 
367
388
  ### `ignore?: RegExp`
368
- Defaults to `/(\.DS_Store|~)$/`
389
+ Defaults to `/(\.DS_Store|~)$/`. The regex rule is
390
+ tested against the basename (filename without directory path).
391
+
369
392
 
370
393
  <br/>
371
394
 
@@ -378,13 +401,13 @@ Defaults to `0`, which means auto-assigned
378
401
 
379
402
  <br/>
380
403
 
381
- ### `delay?: number`
404
+ ### `delay?: number`
382
405
  Defaults to `1200` milliseconds. Although routes can individually be delayed
383
406
  with the 🕓 Checkbox, the amount is globally configurable with this option.
384
407
 
385
408
  ### `delayJitter?: number`
386
409
  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.
410
+ For example, `0.5` will add at most `600ms` to the default delay.
388
411
 
389
412
  <br/>
390
413
 
@@ -396,7 +419,7 @@ or that you manually picked with the ☁️ **Cloud Checkbox**.
396
419
 
397
420
  ### `collectProxied?: boolean`
398
421
  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,
422
+ your proxy fallback to `config.mocksDir`. If the URL has v4 UUIDs,
400
423
  the filename will have `[id]` in their place. For example:
401
424
 
402
425
  <pre>
@@ -423,7 +446,7 @@ the predefined list. For that, you can add it to <code>config.extraMimes</code>
423
446
 
424
447
 
425
448
  ### `formatCollectedJSON?: boolean`
426
- Defaults to `true`. Saves the mock with two spaces indentation &mdash;
449
+ Defaults to `true`. Saves the mock with two spaces indentation &mdash;
427
450
  the formatting output of `JSON.stringify(data, null, ' ')`
428
451
 
429
452
 
@@ -435,6 +458,7 @@ the formatting output of `JSON.stringify(data, null, ' ')`
435
458
  ```js
436
459
  import { jwtCookie } from 'mockaton'
437
460
 
461
+
438
462
  config.cookies = {
439
463
  'My Admin User': 'my-cookie=1;Path=/;SameSite=strict',
440
464
  'My Normal User': 'my-cookie=0;Path=/;SameSite=strict',
@@ -449,7 +473,7 @@ The selected cookie, which is the first one by default, is sent in every respons
449
473
  `Set-Cookie` header (as long as its value is not an empty string). The object key is just
450
474
  a label for UI display purposes, and also for selecting a cookie via the Commander API.
451
475
 
452
- If you need to send more than one cookie, you can inject them globally
476
+ If you need to send more than one cookie, you can inject them globally
453
477
  in `config.extraHeaders`, or individually in a function `.js` or `.ts` mock.
454
478
 
455
479
  By the way, the `jwtCookie` helper has a hardcoded header and signature.
@@ -509,12 +533,13 @@ import { parse } from 'yaml'
509
533
  import { readFileSync } from 'node:js'
510
534
  import { jsToJsonPlugin } from 'mockaton'
511
535
 
536
+
512
537
  config.plugins = [
513
-
538
+
514
539
  // Although `jsToJsonPlugin` is set by default, you need to include it if you need it.
515
540
  // IOW, your plugins array overwrites the default list. This way you can remove it.
516
- [/\.(js|ts)$/, jsToJsonPlugin],
517
-
541
+ [/\.(js|ts)$/, jsToJsonPlugin],
542
+
518
543
  [/\.yml$/, yamlToJsonPlugin],
519
544
  [/foo\.GET\.200\.txt$/, capitalizePlugin], // e.g. GET /api/foo would be capitalized
520
545
  ]
@@ -575,6 +600,7 @@ All of its methods return their `fetch` response promise.
575
600
  ```js
576
601
  import { Commander } from 'mockaton'
577
602
 
603
+
578
604
  const myMockatonAddr = 'http://localhost:2345'
579
605
  const mockaton = new Commander(myMockatonAddr)
580
606
  ```
@@ -649,11 +675,17 @@ example, if you are polling, and you want to test the state change.
649
675
  - Chrome DevTools allows for [overriding responses](https://developer.chrome.com/docs/devtools/overrides)
650
676
  - Reverse Proxies such as [Burp](https://portswigger.net/burp) are also handy for overriding responses
651
677
 
652
- ### Client side
678
+ ### Client Side
653
679
  In contrast to Mockaton, which is an HTTP Server, these programs
654
- mock the client (e.g., `fetch`) in Node.js and browsers.
680
+ hijack the client (e.g., `fetch`) in Node.js and browsers.
655
681
 
656
- - [MSW (Mock Server Worker)](https://mswjs.io)
682
+ - [Mock Server Worker (MSW)](https://mswjs.io)
657
683
  - [Nock](https://github.com/nock/nock)
658
684
  - [Fetch Mock](https://github.com/wheresrhys/fetch-mock)
659
685
  - [Mentoss](https://github.com/humanwhocodes/mentoss) Has a server side too
686
+
687
+ <br/>
688
+
689
+ ---
690
+
691
+ ![](./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.3",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -18,15 +18,15 @@
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",
30
- "puppeteer": "24.17.0"
30
+ "puppeteer": "24.18.0"
31
31
  }
32
32
  }
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.css CHANGED
@@ -165,6 +165,7 @@ header {
165
165
  border: 0;
166
166
  border-right: 3px solid transparent;
167
167
  margin-top: 4px;
168
+ outline: 1px solid var(--colorSecondaryActionBorder);
168
169
  color: var(--colorText);
169
170
  font-size: 11px;
170
171
  background-color: var(--colorComboBoxHeaderBackground);
@@ -252,7 +253,7 @@ header {
252
253
  main {
253
254
  display: grid;
254
255
  min-height: 0;
255
- grid-template-columns: min(820px) 1fr;
256
+ grid-template-columns: minmax(min-content, max-content) 1fr;
256
257
 
257
258
  @media (max-width: 1160px) {
258
259
  grid-template-columns: min(620px) 1fr;
@@ -322,13 +323,11 @@ table {
322
323
  .MockSelector {
323
324
  width: 100%;
324
325
  height: 26px;
325
- padding-right: 5px;
326
- padding-left: 16px;
327
- text-align: right;
328
- direction: rtl;
326
+ padding-right: 8px;
327
+ padding-left: 8px;
329
328
  text-overflow: ellipsis;
330
329
  font-size: 12px;
331
- background-position: 2px center;
330
+ background-position: calc(100% - 4px) center;
332
331
 
333
332
  &.nonDefault {
334
333
  font-weight: bold;
@@ -519,6 +518,7 @@ table {
519
518
 
520
519
  .ProgressBar {
521
520
  position: relative;
521
+ top: -4px;
522
522
  width: 100%;
523
523
  height: 2px;
524
524
  background: var(--colorComboBoxHeaderBackground);
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
  })