mockaton 8.2.21 → 8.2.23
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 +36 -31
- package/package.json +1 -1
- package/src/Api.js +16 -22
- package/src/Commander.js +10 -11
- package/src/Dashboard.js +2 -2
- package/src/MockDispatcherPlugins.js +0 -4
- package/src/utils/fs.js +6 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
_Mockaton_ is a mock server for improving the frontend development and testing experience.
|
|
8
8
|
|
|
9
|
-
With Mockaton
|
|
9
|
+
With Mockaton you don’t need to write code for wiring your mocks. Instead, it
|
|
10
10
|
scans a given directory for filenames following a convention similar to the
|
|
11
11
|
URL paths. For example, the following file will be served on `/api/user/1234`
|
|
12
12
|
```
|
|
@@ -17,8 +17,8 @@ By the way, [this browser
|
|
|
17
17
|
extension](https://github.com/ericfortis/devtools-ext-tar-http-requests)
|
|
18
18
|
can create a TAR of your requests following that convention.
|
|
19
19
|
|
|
20
|
-
Nonetheless, you don’t need to mock all your APIs.
|
|
21
|
-
|
|
20
|
+
Nonetheless, you don’t need to mock all your APIs. Mockaton
|
|
21
|
+
can request from your backend the routes you don’t have mocks for.
|
|
22
22
|
That’s done with `config.proxyFallback = 'http://mybackend'`
|
|
23
23
|
|
|
24
24
|
## Multiple Mock Variants
|
|
@@ -37,7 +37,7 @@ which is handy for setting up tests (see **Commander API** below).
|
|
|
37
37
|
<picture>
|
|
38
38
|
<source media="(prefers-color-scheme: light)" srcset="./README-dashboard-light.png">
|
|
39
39
|
<source media="(prefers-color-scheme: dark)" srcset="./README-dashboard-dark.png">
|
|
40
|
-
<img alt="Mockaton Dashboard Demo" src="./README-dashboard-light.png"
|
|
40
|
+
<img alt="Mockaton Dashboard Demo" src="./README-dashboard-light.png">
|
|
41
41
|
</picture>
|
|
42
42
|
|
|
43
43
|
|
|
@@ -93,7 +93,7 @@ The _Reset_ button is for registering newly added, removed, or renamed mocks.
|
|
|
93
93
|
- Polled resources (for triggering their different states)
|
|
94
94
|
- alerts
|
|
95
95
|
- notifications
|
|
96
|
-
- slow to build
|
|
96
|
+
- slow to build resources
|
|
97
97
|
|
|
98
98
|
### Time Travel
|
|
99
99
|
If you commit the mocks to your repo, it’s straightforward to bisect bugs and
|
|
@@ -103,8 +103,8 @@ backends to old API contracts or databases.
|
|
|
103
103
|
### Deterministic Standalone Demo Server
|
|
104
104
|
Perhaps you need to demo your app, but the ideal flow is too complex to
|
|
105
105
|
simulate from the actual backend. In this case, compile your frontend app and
|
|
106
|
-
put its built assets in `config.staticDir`. Then,
|
|
107
|
-
|
|
106
|
+
put its built assets in `config.staticDir`. Then, on the dashboard
|
|
107
|
+
"Bulk Select" mocks to simulate the complete states you want to demo.
|
|
108
108
|
For bulk-selecting, you just need to add a comment to the mock
|
|
109
109
|
filename, such as `(demo-part1)`, `(demo-part2)`.
|
|
110
110
|
|
|
@@ -113,7 +113,7 @@ filename, such as `(demo-part1)`, `(demo-part2)`.
|
|
|
113
113
|
- Avoids spinning up and maintaining hefty backends when developing UIs.
|
|
114
114
|
- For a deterministic, comprehensive, and consistent backend state. For example, having
|
|
115
115
|
a collection with all the possible state variants helps for spotting inadvertent bugs.
|
|
116
|
-
- Sometimes
|
|
116
|
+
- Sometimes frontend progress is blocked waiting for some backend API. Similarly,
|
|
117
117
|
it’s often delayed due to missing data or inconvenient contracts. Therefore,
|
|
118
118
|
many meetings can be saved by prototyping frontend features with mocks, and
|
|
119
119
|
then showing those contracts to the backend team.
|
|
@@ -123,15 +123,6 @@ filename, such as `(demo-part1)`, `(demo-part2)`.
|
|
|
123
123
|
- Reverse Proxies such as [Burp](https://portswigger.net/burp) are also handy for overriding responses.
|
|
124
124
|
- [Mock Server Worker](https://mswjs.io)
|
|
125
125
|
|
|
126
|
-
---
|
|
127
|
-
## Default Mock for a Route
|
|
128
|
-
You can add the comment: `(default)` to a filename.
|
|
129
|
-
Otherwise, the first file in **alphabetical order** wins.
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
api/user(default).GET.200.json
|
|
133
|
-
```
|
|
134
|
-
|
|
135
126
|
---
|
|
136
127
|
|
|
137
128
|
## You can write JSON mocks in JavaScript or TypeScript
|
|
@@ -227,6 +218,15 @@ api/foo<b>(my comment)</b>.GET.200.json
|
|
|
227
218
|
api/foo.GET.200.json
|
|
228
219
|
</pre>
|
|
229
220
|
|
|
221
|
+
### Default Mock for a Route
|
|
222
|
+
You can add the comment: `(default)`.
|
|
223
|
+
Otherwise, the first file in **alphabetical order** wins.
|
|
224
|
+
|
|
225
|
+
<pre>
|
|
226
|
+
api/user<b>(default)</b>.GET.200.json
|
|
227
|
+
</pre>
|
|
228
|
+
|
|
229
|
+
|
|
230
230
|
### Query String Params
|
|
231
231
|
The query string is ignored when routing to it. In other words, it’s only used for
|
|
232
232
|
documenting the URL contract.
|
|
@@ -238,9 +238,8 @@ Speaking of which, on Windows filenames containing "?" are [not
|
|
|
238
238
|
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.
|
|
239
239
|
|
|
240
240
|
|
|
241
|
-
### Index-like
|
|
242
|
-
|
|
243
|
-
`api/foo`, you have two options:
|
|
241
|
+
### Index-like routes
|
|
242
|
+
If you have `api/foo` and `api/foo/bar`, you have two options:
|
|
244
243
|
|
|
245
244
|
**Option A:**
|
|
246
245
|
```
|
|
@@ -272,8 +271,9 @@ Defaults to `0`, which means auto-assigned
|
|
|
272
271
|
Defaults to `/(\.DS_Store|~)$/`
|
|
273
272
|
|
|
274
273
|
|
|
275
|
-
### `delay?: number`
|
|
276
|
-
|
|
274
|
+
### `delay?: number`
|
|
275
|
+
Routes can individually be delayed with the 🕓 checkbox. On the other hand,
|
|
276
|
+
the amount is globally configurable. It defaults to `config.delay=1200` milliseconds.
|
|
277
277
|
|
|
278
278
|
|
|
279
279
|
### `proxyFallback?: string`
|
|
@@ -309,15 +309,13 @@ config.cookies = {
|
|
|
309
309
|
})
|
|
310
310
|
}
|
|
311
311
|
```
|
|
312
|
-
The selected cookie is
|
|
312
|
+
The selected cookie, which is the first one by default, is sent in every
|
|
313
|
+
response in a `Set-Cookie` header. If you need to send more
|
|
314
|
+
cookies, inject them globally in `config.extraHeaders`.
|
|
313
315
|
|
|
314
316
|
By the way, the `jwtCookie` helper has a hardcoded header and signature.
|
|
315
317
|
In other words, it’s useful only if you care about the payload.
|
|
316
318
|
|
|
317
|
-
If you need to send more than one cookie,
|
|
318
|
-
inject them globally in `config.extraHeaders`.
|
|
319
|
-
|
|
320
|
-
|
|
321
319
|
|
|
322
320
|
### `extraHeaders?: string[]`
|
|
323
321
|
Note it’s a unidimensional array. The header name goes at even indices.
|
|
@@ -337,6 +335,8 @@ config.extraMimes = {
|
|
|
337
335
|
jpe: 'application/jpeg'
|
|
338
336
|
}
|
|
339
337
|
```
|
|
338
|
+
These media types take precedence over the built-in
|
|
339
|
+
[utils/mime.js](src/utils/mime.js), so you can override them.
|
|
340
340
|
|
|
341
341
|
|
|
342
342
|
### `plugins?: [filenameTester: RegExp, plugin: Plugin][]`
|
|
@@ -350,9 +350,10 @@ type Plugin = (
|
|
|
350
350
|
body: string | Uint8Array
|
|
351
351
|
}>
|
|
352
352
|
```
|
|
353
|
-
Plugins are for processing mocks before sending them.
|
|
353
|
+
Plugins are for processing mocks before sending them. If no regex matches the filename,
|
|
354
|
+
it fallbacks to reading the file from disk and computing the MIME from the extension.
|
|
354
355
|
|
|
355
|
-
Note: don’t call `response.end()`
|
|
356
|
+
Note: don’t call `response.end()` on any plugin.
|
|
356
357
|
|
|
357
358
|
<details>
|
|
358
359
|
<summary><b> See Plugin Examples </b></summary>
|
|
@@ -366,7 +367,11 @@ import { readFileSync } from 'node:js'
|
|
|
366
367
|
import { jsToJsonPlugin } from 'mockaton'
|
|
367
368
|
|
|
368
369
|
config.plugins = [
|
|
369
|
-
|
|
370
|
+
|
|
371
|
+
// Although `jsToJsonPlugin` is set by default, you need to add it to your list if you need it.
|
|
372
|
+
// In other words, your plugins array overwrites the default list. This way you can remove it.
|
|
373
|
+
[/\.(js|ts)$/, jsToJsonPlugin],
|
|
374
|
+
|
|
370
375
|
[/\.yml$/, yamlToJsonPlugin],
|
|
371
376
|
[/foo\.GET\.200\.txt$/, capitalizePlugin], // e.g. GET /api/foo would be capitalized
|
|
372
377
|
]
|
|
@@ -401,7 +406,7 @@ config.corsExposedHeaders = [] // headers you need to access in client-side JS
|
|
|
401
406
|
|
|
402
407
|
|
|
403
408
|
### `onReady?: (dashboardUrl: string) => void`
|
|
404
|
-
This defaults to trying to open the dashboard in your default browser
|
|
409
|
+
This defaults to trying to open the dashboard in your default browser on macOS and
|
|
405
410
|
Windows. For a more cross-platform utility, you could `npm install open` and pass it.
|
|
406
411
|
```js
|
|
407
412
|
import open from 'open'
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -39,6 +39,8 @@ export const apiPatchRequests = new Map([
|
|
|
39
39
|
[API.cors, setCorsAllowed]
|
|
40
40
|
])
|
|
41
41
|
|
|
42
|
+
/* GET */
|
|
43
|
+
|
|
42
44
|
function serveDashboard(_, response) { sendFile(response, join(import.meta.dirname, 'Dashboard.html')) }
|
|
43
45
|
function serveDashboardAsset(req, response) { sendFile(response, join(import.meta.dirname, req.url)) }
|
|
44
46
|
|
|
@@ -48,13 +50,26 @@ function listMockBrokers(_, response) { sendJSON(response, mockBrokersCollection
|
|
|
48
50
|
function getProxyFallback(_, response) { sendJSON(response, Config.proxyFallback) }
|
|
49
51
|
function getIsCorsAllowed(_, response) { sendJSON(response, Config.corsAllowed) }
|
|
50
52
|
|
|
53
|
+
async function listStaticFiles(req, response) { // TESTME
|
|
54
|
+
try {
|
|
55
|
+
const files = Config.staticDir
|
|
56
|
+
? listFilesRecursively(Config.staticDir).filter(f => !Config.ignore.test(f))
|
|
57
|
+
: []
|
|
58
|
+
sendJSON(response, files)
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
sendBadRequest(response, error)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/* PATCH */
|
|
51
67
|
|
|
52
68
|
function reinitialize(_, response) {
|
|
53
69
|
mockBrokersCollection.init()
|
|
54
70
|
sendOK(response)
|
|
55
71
|
}
|
|
56
72
|
|
|
57
|
-
|
|
58
73
|
async function selectCookie(req, response) {
|
|
59
74
|
try {
|
|
60
75
|
cookie.setCurrent(await parseJSON(req))
|
|
@@ -65,14 +80,12 @@ async function selectCookie(req, response) {
|
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
|
|
69
83
|
async function selectMock(req, response) {
|
|
70
84
|
try {
|
|
71
85
|
const file = await parseJSON(req)
|
|
72
86
|
const broker = mockBrokersCollection.getBrokerByFilename(file)
|
|
73
87
|
if (!broker || !broker.mockExists(file))
|
|
74
88
|
throw `Missing Mock: ${file}`
|
|
75
|
-
|
|
76
89
|
broker.updateFile(file)
|
|
77
90
|
sendOK(response)
|
|
78
91
|
}
|
|
@@ -81,17 +94,14 @@ async function selectMock(req, response) {
|
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
|
|
85
97
|
async function setRouteIsDelayed(req, response) {
|
|
86
98
|
try {
|
|
87
99
|
const body = await parseJSON(req)
|
|
88
100
|
const broker = mockBrokersCollection.getBrokerForUrl(
|
|
89
101
|
body[DF.routeMethod],
|
|
90
102
|
body[DF.routeUrlMask])
|
|
91
|
-
|
|
92
103
|
if (!broker)
|
|
93
104
|
throw `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`
|
|
94
|
-
|
|
95
105
|
broker.updateDelay(body[DF.delayed])
|
|
96
106
|
sendOK(response)
|
|
97
107
|
}
|
|
@@ -100,7 +110,6 @@ async function setRouteIsDelayed(req, response) {
|
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
|
|
104
113
|
async function updateProxyFallback(req, response) {
|
|
105
114
|
try {
|
|
106
115
|
const fallback = await parseJSON(req)
|
|
@@ -116,7 +125,6 @@ async function updateProxyFallback(req, response) {
|
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
|
|
120
128
|
async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
121
129
|
try {
|
|
122
130
|
mockBrokersCollection.setMocksMatchingComment(await parseJSON(req))
|
|
@@ -127,7 +135,6 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
|
|
131
138
|
async function setCorsAllowed(req, response) {
|
|
132
139
|
try {
|
|
133
140
|
Config.corsAllowed = await parseJSON(req)
|
|
@@ -138,16 +145,3 @@ async function setCorsAllowed(req, response) {
|
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
// TESTME
|
|
143
|
-
async function listStaticFiles(req, response) {
|
|
144
|
-
try {
|
|
145
|
-
const files = Config.staticDir
|
|
146
|
-
? listFilesRecursively(Config.staticDir).filter(f => !Config.ignore.test(f))
|
|
147
|
-
: []
|
|
148
|
-
sendJSON(response, files)
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
sendBadRequest(response, error)
|
|
152
|
-
}
|
|
153
|
-
}
|
package/src/Commander.js
CHANGED
|
@@ -7,6 +7,16 @@ export class Commander {
|
|
|
7
7
|
this.#addr = addr
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
#get(api) {
|
|
11
|
+
return fetch(this.#addr + api)
|
|
12
|
+
}
|
|
13
|
+
#patch(api, body) {
|
|
14
|
+
return fetch(this.#addr + api, {
|
|
15
|
+
method: 'PATCH',
|
|
16
|
+
body: JSON.stringify(body)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
listMocks() {
|
|
11
21
|
return this.#get(API.mocks)
|
|
12
22
|
}
|
|
@@ -58,15 +68,4 @@ export class Commander {
|
|
|
58
68
|
listStaticFiles() {
|
|
59
69
|
return this.#get(API.static)
|
|
60
70
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
#get(api) {
|
|
64
|
-
return fetch(this.#addr + api)
|
|
65
|
-
}
|
|
66
|
-
#patch(api, body) {
|
|
67
|
-
return fetch(this.#addr + api, {
|
|
68
|
-
method: 'PATCH',
|
|
69
|
-
body: JSON.stringify(body)
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
71
|
}
|
package/src/Dashboard.js
CHANGED
|
@@ -423,7 +423,7 @@ function createElement(elem, props = null, ...children) {
|
|
|
423
423
|
node[key] = value
|
|
424
424
|
else
|
|
425
425
|
node.setAttribute(key, value)
|
|
426
|
-
node.append(...children.flat())
|
|
426
|
+
node.append(...children.flat().filter(a => a))
|
|
427
427
|
return node
|
|
428
428
|
}
|
|
429
429
|
|
|
@@ -431,7 +431,7 @@ function createSvgElement(tagName, props, ...children) {
|
|
|
431
431
|
const elem = document.createElementNS('http://www.w3.org/2000/svg', tagName)
|
|
432
432
|
for (const [key, value] of Object.entries(props))
|
|
433
433
|
elem.setAttribute(key, value)
|
|
434
|
-
elem.append(...children.flat())
|
|
434
|
+
elem.append(...children.flat().filter(a => a))
|
|
435
435
|
return elem
|
|
436
436
|
}
|
|
437
437
|
|
|
@@ -7,10 +7,6 @@ export async function applyPlugins(filePath, req, response) {
|
|
|
7
7
|
for (const [regex, plugin] of Config.plugins) // TESTME capitalizePlugin
|
|
8
8
|
if (regex.test(filePath))
|
|
9
9
|
return await plugin(filePath, req, response)
|
|
10
|
-
return defaultPlugin(filePath)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function defaultPlugin(filePath) {
|
|
14
10
|
return {
|
|
15
11
|
mime: mimeFor(filePath),
|
|
16
12
|
body: read(filePath)
|
package/src/utils/fs.js
CHANGED
|
@@ -8,6 +8,9 @@ export const isDirectory = path => lstatSync(path, { throwIfNoEntry: false })?.i
|
|
|
8
8
|
export const read = path => readFileSync(path)
|
|
9
9
|
|
|
10
10
|
/** @returns {Array<string>} paths relative to `dir` */
|
|
11
|
-
export const listFilesRecursively = dir =>
|
|
12
|
-
.
|
|
13
|
-
.
|
|
11
|
+
export const listFilesRecursively = dir => {
|
|
12
|
+
const files = readdirSync(dir, { recursive: true }).filter(f => isFile(join(dir, f)))
|
|
13
|
+
return process.platform === 'win32'
|
|
14
|
+
? files.map(f => f.replaceAll(path.sep, path.posix.sep)) // TESTME
|
|
15
|
+
: files
|
|
16
|
+
}
|