mockaton 8.6.0 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -29
- package/index.d.ts +6 -6
- package/package.json +5 -2
- package/src/Api.js +1 -1
- package/src/Commander.js +1 -0
- package/src/Dashboard.js +49 -44
- package/src/MockBroker.js +2 -5
- package/src/Mockaton.js +5 -5
- package/src/config.js +1 -0
- package/src/mockBrokersCollection.js +9 -3
package/README.md
CHANGED
|
@@ -5,50 +5,55 @@
|
|
|
5
5
|
|
|
6
6
|
## Mock your APIs, Enhance your Development Workflow
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
development and testing
|
|
8
|
+
Welcome to developer experience tooling! Mockaton is here to help
|
|
9
|
+
make your frontend development and testing easier—and a lot more fun.
|
|
10
10
|
|
|
11
|
-
With Mockaton you don’t need to write code for wiring your mocks. Instead,
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
With Mockaton you don’t need to write code for wiring your mocks. Instead, just
|
|
12
|
+
place your mocks in a directory and let Mockaton do the rest. It will automatically
|
|
13
|
+
scan the directory for filenames that follow a convention similar to the URL paths.
|
|
14
|
+
|
|
15
|
+
For example, for this route `/api/user/1234`, the mock filename would be:
|
|
14
16
|
```
|
|
15
17
|
my-mocks-dir/api/user/[user-id].GET.200.json
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
And hey, no need to mock everything. Mockaton can fallback to your real
|
|
21
|
+
backend on routes you don’t have mocks for. Just type your backend
|
|
22
|
+
address in the **Fallback Backend** field. Check **Save Mocks** so you
|
|
23
|
+
can collect those responses that hit your fallback server. The mocks will
|
|
24
|
+
be saved to your `config.mocksDir` following the filename convention.
|
|
20
25
|
|
|
21
|
-
`config.proxyFallback = 'http://mybackend'`
|
|
22
26
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
for the routes that reached your `proxyFallback` with:
|
|
27
|
+
## Multiple Mock Variants
|
|
28
|
+
Here’s how you can create multiple mocks for a particular route:
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
### Adding comments in filenames
|
|
31
|
+
Want to mock a locked-out user or an invalid login attempt? You
|
|
32
|
+
can just add a comment to the filename in parentheses. For example:
|
|
28
33
|
|
|
34
|
+
`api/login(locked out user).POST.423.json`
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- __Comment__ on the filename, which is anything within parentheses.
|
|
34
|
-
- e.g. `api/login(locked out user).POST.423.json`
|
|
36
|
+
### Different response status code
|
|
37
|
+
For instance, you can have mocks with a `4xx` or `5xx` status code for triggering
|
|
38
|
+
error responses. Or with a `204` (No Content) for testing empty collections.
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
## Dashboard
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
In the dashboard you can select a mock variant for a particular route, among
|
|
43
|
+
other features such as delaying responses, or triggering an autogenerated
|
|
44
|
+
`500` (Internal Server Error). Nonetheless, there’s a programmatic API,
|
|
41
45
|
which is handy for setting up tests (see **Commander API** below).
|
|
42
46
|
|
|
43
47
|
<picture>
|
|
44
|
-
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.
|
|
45
|
-
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.
|
|
46
|
-
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.
|
|
48
|
+
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp860x800.light.gold.png">
|
|
49
|
+
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp860x800.dark.gold.png">
|
|
50
|
+
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp860x800.light.gold.png">
|
|
47
51
|
</picture>
|
|
48
52
|
|
|
49
53
|
|
|
54
|
+
|
|
50
55
|
## Basic Usage
|
|
51
|
-
`tsx` is only needed if you want to write mocks in TypeScript
|
|
56
|
+
`tsx` is only needed if you want to write mocks in TypeScript.
|
|
52
57
|
```sh
|
|
53
58
|
npm install mockaton tsx --save-dev
|
|
54
59
|
```
|
|
@@ -72,7 +77,12 @@ node --import=tsx my-mockaton.js
|
|
|
72
77
|
|
|
73
78
|
## Running the demo app (Vite)
|
|
74
79
|
|
|
75
|
-
This is a minimal React + Vite + Mockaton app.
|
|
80
|
+
This is a minimal React + Vite + Mockaton app. It’s mainly a list of
|
|
81
|
+
colors, which contains all of their possible states. For example,
|
|
82
|
+
permutations for out-of-stock, new-arrival, and discontinued.
|
|
83
|
+
|
|
84
|
+
Also, if you select the **Admin User** from the Mockaton dashboard,
|
|
85
|
+
the color cards will have a Delete button as well.
|
|
76
86
|
|
|
77
87
|
```sh
|
|
78
88
|
git clone https://github.com/ericfortis/mockaton.git
|
|
@@ -82,9 +92,11 @@ npm run mockaton
|
|
|
82
92
|
npm run start
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
By the way, that directory has
|
|
95
|
+
By the way, that directory has scripts for opening Mockaton and Vite in one command.
|
|
96
|
+
|
|
97
|
+
The app looks like this:
|
|
86
98
|
|
|
87
|
-
<img src="./demo-app-vite/
|
|
99
|
+
<img src="./demo-app-vite/pixaton-tests/pic-for-readme.vp500x800.light.gold.png" alt="Mockaton Demo App Screenshot" width="500" />
|
|
88
100
|
|
|
89
101
|
|
|
90
102
|
## Use Cases
|
|
@@ -107,7 +119,7 @@ backends to old API contracts or databases.
|
|
|
107
119
|
Perhaps you need to demo your app, but the ideal flow is too complex to
|
|
108
120
|
simulate from the actual backend. In this case, compile your frontend app and
|
|
109
121
|
put its built assets in `config.staticDir`. Then, on the dashboard
|
|
110
|
-
|
|
122
|
+
**Bulk Select** mocks to simulate the complete states you want to demo.
|
|
111
123
|
For bulk-selecting, you just need to add a comment to the mock
|
|
112
124
|
filename, such as `(demo-part1)`, `(demo-part2)`.
|
|
113
125
|
|
|
@@ -453,7 +465,7 @@ Nonetheless, you can trigger any command besides opening a browser.
|
|
|
453
465
|
---
|
|
454
466
|
|
|
455
467
|
## Commander API
|
|
456
|
-
`Commander` is a
|
|
468
|
+
`Commander` is a client for Mockaton’s HTTP API.
|
|
457
469
|
All of its methods return their `fetch` response promise.
|
|
458
470
|
```js
|
|
459
471
|
import { Commander } from 'mockaton'
|
package/index.d.ts
CHANGED
|
@@ -27,12 +27,12 @@ interface Config {
|
|
|
27
27
|
plugins?: [filenameTester: RegExp, plugin: Plugin][]
|
|
28
28
|
|
|
29
29
|
corsAllowed?: boolean,
|
|
30
|
-
corsOrigins
|
|
31
|
-
corsMethods
|
|
32
|
-
corsHeaders
|
|
33
|
-
corsExposedHeaders
|
|
34
|
-
corsCredentials
|
|
35
|
-
corsMaxAge
|
|
30
|
+
corsOrigins?: string[]
|
|
31
|
+
corsMethods?: string[]
|
|
32
|
+
corsHeaders?: string[]
|
|
33
|
+
corsExposedHeaders?: string[]
|
|
34
|
+
corsCredentials?: boolean
|
|
35
|
+
corsMaxAge?: number
|
|
36
36
|
|
|
37
37
|
onReady?: (address: string) => void
|
|
38
38
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "mockaton",
|
|
3
3
|
"description": "A deterministic server-side for developing and testing frontend clients",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.7.0",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"optionalDependencies": {
|
|
18
18
|
"pixaton": ">=1.0.2",
|
|
19
|
-
"puppeteer": ">=
|
|
19
|
+
"puppeteer": ">=24.1.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"pixaton": "1.0.2"
|
|
20
23
|
}
|
|
21
24
|
}
|
package/src/Api.js
CHANGED
|
@@ -90,7 +90,7 @@ async function selectMock(req, response) {
|
|
|
90
90
|
try {
|
|
91
91
|
const file = await parseJSON(req)
|
|
92
92
|
const broker = mockBrokersCollection.getBrokerByFilename(file)
|
|
93
|
-
if (!broker || !broker.
|
|
93
|
+
if (!broker || !broker.hasMock(file))
|
|
94
94
|
throw `Missing Mock: ${file}`
|
|
95
95
|
broker.updateFile(file)
|
|
96
96
|
sendOK(response)
|
package/src/Commander.js
CHANGED
package/src/Dashboard.js
CHANGED
|
@@ -47,7 +47,7 @@ const refPayloadViewerFileTitle = useRef()
|
|
|
47
47
|
const mockaton = new Commander(window.location.origin)
|
|
48
48
|
|
|
49
49
|
function init() {
|
|
50
|
-
Promise.all([
|
|
50
|
+
return Promise.all([
|
|
51
51
|
mockaton.listMocks(),
|
|
52
52
|
mockaton.listCookies(),
|
|
53
53
|
mockaton.listComments(),
|
|
@@ -98,13 +98,12 @@ function CookieSelector({ list }) {
|
|
|
98
98
|
r('label', { className: CSS.Field },
|
|
99
99
|
r('span', null, Strings.cookie),
|
|
100
100
|
r('select', {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
r('option', { value, selected }, value)))))
|
|
101
|
+
autocomplete: 'off',
|
|
102
|
+
disabled,
|
|
103
|
+
title: disabled ? Strings.cookie_disabled_title : '',
|
|
104
|
+
onChange
|
|
105
|
+
}, list.map(([value, selected]) =>
|
|
106
|
+
r('option', { value, selected }, value)))))
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
|
|
@@ -122,14 +121,13 @@ function BulkSelector({ comments }) {
|
|
|
122
121
|
r('label', { className: CSS.Field },
|
|
123
122
|
r('span', null, Strings.bulk_select_by_comment),
|
|
124
123
|
r('select', {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
r('option', { value }, value)))))
|
|
124
|
+
'data-qaid': 'BulkSelector',
|
|
125
|
+
autocomplete: 'off',
|
|
126
|
+
disabled,
|
|
127
|
+
title: disabled ? Strings.bulk_select_by_comment_disabled_title : '',
|
|
128
|
+
onChange
|
|
129
|
+
}, list.map(value =>
|
|
130
|
+
r('option', { value }, value)))))
|
|
133
131
|
}
|
|
134
132
|
|
|
135
133
|
|
|
@@ -145,7 +143,7 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
|
145
143
|
.catch(onError)
|
|
146
144
|
}
|
|
147
145
|
return (
|
|
148
|
-
r('div', { className: CSS.Field
|
|
146
|
+
r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
|
|
149
147
|
r('label', null,
|
|
150
148
|
r('span', null, Strings.fallback_server),
|
|
151
149
|
r('input', {
|
|
@@ -203,13 +201,9 @@ function StaticFilesList({ staticFiles }) {
|
|
|
203
201
|
className: CSS.StaticFilesList
|
|
204
202
|
},
|
|
205
203
|
r('summary', null, Strings.static),
|
|
206
|
-
r('ul', null,
|
|
207
|
-
|
|
208
|
-
r('
|
|
209
|
-
r('a', {
|
|
210
|
-
href: f,
|
|
211
|
-
target: '_blank'
|
|
212
|
-
}, f))))))
|
|
204
|
+
r('ul', null, staticFiles.map(f =>
|
|
205
|
+
r('li', null,
|
|
206
|
+
r('a', { href: f, target: '_blank' }, f))))))
|
|
213
207
|
}
|
|
214
208
|
|
|
215
209
|
|
|
@@ -221,7 +215,7 @@ function SectionByMethod({ method, brokers }) {
|
|
|
221
215
|
.filter(([, broker]) => broker.mocks.length > 1) // >1 because of autogen500
|
|
222
216
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
223
217
|
.map(([urlMask, broker]) =>
|
|
224
|
-
r('tr',
|
|
218
|
+
r('tr', { 'data-method': method, 'data-urlMask': urlMask },
|
|
225
219
|
r('td', null, r(PreviewLink, { method, urlMask })),
|
|
226
220
|
r('td', null, r(MockSelector, { broker })),
|
|
227
221
|
r('td', null, r(DelayRouteToggler, { broker })),
|
|
@@ -237,9 +231,7 @@ function PreviewLink({ method, urlMask }) {
|
|
|
237
231
|
empty(refPayloadViewer.current)
|
|
238
232
|
refPayloadViewer.current.append(ProgressBar())
|
|
239
233
|
}, 180)
|
|
240
|
-
const res = await fetch(this.href, {
|
|
241
|
-
method: this.getAttribute('data-method')
|
|
242
|
-
})
|
|
234
|
+
const res = await fetch(this.href, { method })
|
|
243
235
|
document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
|
|
244
236
|
this.classList.add(CSS.chosen)
|
|
245
237
|
clearTimeout(spinner)
|
|
@@ -252,7 +244,7 @@ function PreviewLink({ method, urlMask }) {
|
|
|
252
244
|
|
|
253
245
|
empty(refPayloadViewerFileTitle.current)
|
|
254
246
|
refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
|
|
255
|
-
file:
|
|
247
|
+
file: mockSelectorFor(method, urlMask).value
|
|
256
248
|
}))
|
|
257
249
|
}
|
|
258
250
|
catch (error) {
|
|
@@ -263,7 +255,6 @@ function PreviewLink({ method, urlMask }) {
|
|
|
263
255
|
r('a', {
|
|
264
256
|
className: CSS.PreviewLink,
|
|
265
257
|
href: urlMask,
|
|
266
|
-
'data-method': method,
|
|
267
258
|
onClick
|
|
268
259
|
}, urlMask))
|
|
269
260
|
}
|
|
@@ -301,14 +292,14 @@ function updatePayloadViewer(body, mime) {
|
|
|
301
292
|
|
|
302
293
|
function MockSelector({ broker }) {
|
|
303
294
|
function onChange() {
|
|
304
|
-
const { status } = parseFilename(this.value)
|
|
295
|
+
const { status, urlMask, method } = parseFilename(this.value)
|
|
305
296
|
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
306
297
|
? 'normal'
|
|
307
298
|
: 'bold'
|
|
308
299
|
mockaton.select(this.value)
|
|
309
300
|
.then(() => {
|
|
310
|
-
|
|
311
|
-
|
|
301
|
+
linkFor(method, urlMask)?.click()
|
|
302
|
+
checkbox500For(method, urlMask).checked = status === 500
|
|
312
303
|
this.className = className(this.value === this.options[0].value, status)
|
|
313
304
|
})
|
|
314
305
|
.catch(onError)
|
|
@@ -329,17 +320,16 @@ function MockSelector({ broker }) {
|
|
|
329
320
|
|
|
330
321
|
return (
|
|
331
322
|
r('select', {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}, file))))
|
|
323
|
+
'data-qaid': urlMask,
|
|
324
|
+
autocomplete: 'off',
|
|
325
|
+
className: className(selected === files[0], status),
|
|
326
|
+
disabled: files.length <= 1,
|
|
327
|
+
onChange
|
|
328
|
+
}, files.map(file =>
|
|
329
|
+
r('option', {
|
|
330
|
+
value: file,
|
|
331
|
+
selected: file === selected
|
|
332
|
+
}, file))))
|
|
343
333
|
}
|
|
344
334
|
|
|
345
335
|
|
|
@@ -373,10 +363,12 @@ function TimerIcon() {
|
|
|
373
363
|
|
|
374
364
|
function InternalServerErrorToggler({ broker }) {
|
|
375
365
|
function onChange(event) {
|
|
366
|
+
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
376
367
|
mockaton.select(event.currentTarget.checked
|
|
377
368
|
? broker.mocks.find(f => parseFilename(f).status === 500)
|
|
378
369
|
: broker.mocks[0])
|
|
379
370
|
.then(init)
|
|
371
|
+
.then(() => linkFor(method, urlMask)?.click())
|
|
380
372
|
.catch(onError)
|
|
381
373
|
}
|
|
382
374
|
return (
|
|
@@ -395,6 +387,19 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
395
387
|
)
|
|
396
388
|
}
|
|
397
389
|
|
|
390
|
+
function trFor(method, urlMask) {
|
|
391
|
+
return document.querySelector(`tr[data-method="${method}"][data-urlMask="${urlMask}"]`)
|
|
392
|
+
}
|
|
393
|
+
function linkFor(method, urlMask) {
|
|
394
|
+
return trFor(method, urlMask)?.querySelector(`a.${CSS.PreviewLink}`)
|
|
395
|
+
}
|
|
396
|
+
function checkbox500For(method, urlMask) {
|
|
397
|
+
return trFor(method, urlMask)?.querySelector(`.${CSS.InternalServerErrorToggler} > input`)
|
|
398
|
+
}
|
|
399
|
+
function mockSelectorFor(method, urlMask) {
|
|
400
|
+
return trFor(method, urlMask)?.querySelector(`select.${CSS.MockSelector}`)
|
|
401
|
+
}
|
|
402
|
+
|
|
398
403
|
|
|
399
404
|
function onError(error) {
|
|
400
405
|
if (error?.message === 'Failed to fetch')
|
package/src/MockBroker.js
CHANGED
|
@@ -19,10 +19,7 @@ export class MockBroker {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
register(file) {
|
|
22
|
-
if (
|
|
23
|
-
return
|
|
24
|
-
const { status } = parseFilename(file)
|
|
25
|
-
if (status === 500) {
|
|
22
|
+
if (parseFilename(file).status === 500) {
|
|
26
23
|
this.#deleteTemp500()
|
|
27
24
|
if (this.temp500IsSelected)
|
|
28
25
|
this.updateFile(file)
|
|
@@ -84,7 +81,7 @@ export class MockBroker {
|
|
|
84
81
|
this.updateFile(this.mocks[0])
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
|
|
84
|
+
hasMock(file) { return this.mocks.includes(file) }
|
|
88
85
|
updateFile(filename) { this.currentMock.file = filename }
|
|
89
86
|
updateDelay(delayed) { this.currentMock.delay = Number(delayed) * config.delay }
|
|
90
87
|
|
package/src/Mockaton.js
CHANGED
|
@@ -17,13 +17,13 @@ export function Mockaton(options) {
|
|
|
17
17
|
mockBrokerCollection.init()
|
|
18
18
|
|
|
19
19
|
watch(config.mocksDir, { recursive: true, persistent: false },
|
|
20
|
-
function handleAddedOrDeletedMocks(_,
|
|
21
|
-
if (!
|
|
20
|
+
function handleAddedOrDeletedMocks(_, file) {
|
|
21
|
+
if (!file)
|
|
22
22
|
return
|
|
23
|
-
if (existsSync(join(config.mocksDir,
|
|
24
|
-
mockBrokerCollection.registerMock(
|
|
23
|
+
if (existsSync(join(config.mocksDir, file)))
|
|
24
|
+
mockBrokerCollection.registerMock(file, 'ensureItHas500')
|
|
25
25
|
else
|
|
26
|
-
mockBrokerCollection.unregisterMock(
|
|
26
|
+
mockBrokerCollection.unregisterMock(file)
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
return createServer(onRequest).listen(config.port, config.host, function (error) {
|
package/src/config.js
CHANGED
|
@@ -6,11 +6,15 @@ import { parseFilename, filenameIsValid } from './Filename.js'
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
+
* @type {{
|
|
10
|
+
* [method: string]:
|
|
11
|
+
* { [route: string]: MockBroker }
|
|
12
|
+
* }}
|
|
9
13
|
* @example
|
|
10
14
|
* {
|
|
11
15
|
* GET: {
|
|
12
|
-
* /api/route-a:
|
|
13
|
-
* /api/route-b:
|
|
16
|
+
* '/api/route-a': mockBrokerA,
|
|
17
|
+
* '/api/route-b': mockBrokerB
|
|
14
18
|
* },
|
|
15
19
|
* POST: {…}
|
|
16
20
|
* }
|
|
@@ -32,7 +36,9 @@ export function init() {
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export function registerMock(file, shouldEnsure500) {
|
|
35
|
-
if (
|
|
39
|
+
if (getBrokerByFilename(file)?.hasMock(file)
|
|
40
|
+
|| config.ignore.test(file)
|
|
41
|
+
|| !filenameIsValid(file))
|
|
36
42
|
return
|
|
37
43
|
|
|
38
44
|
const { method, urlMask } = parseFilename(file)
|