mockaton 8.6.1 → 8.7.1
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 +19 -20
- package/index.d.ts +6 -6
- package/package.json +4 -1
- package/src/Api.js +1 -1
- package/src/Commander.js +1 -0
- package/src/Dashboard.css +6 -2
- package/src/Dashboard.js +182 -143
- 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
|
@@ -17,20 +17,15 @@ For example, for this route `/api/user/1234`, the mock filename would be:
|
|
|
17
17
|
my-mocks-dir/api/user/[user-id].GET.200.json
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
And hey, no need to mock everything.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
And there’s one more cool thing—you can collect those responses
|
|
25
|
-
by clicking the **Save Mocks** checkbox. Those mocks will
|
|
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
|
|
26
24
|
be saved to your `config.mocksDir` following the filename convention.
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
## Multiple Mock Variants
|
|
30
|
-
|
|
31
|
-
You can have many mocks for any route. For example, you might
|
|
32
|
-
want different mocks with different response status codes
|
|
33
|
-
(like triggering errors). Here are a couple of ways to do it:
|
|
28
|
+
Here’s how you can create multiple mocks for a particular route:
|
|
34
29
|
|
|
35
30
|
### Adding comments in filenames
|
|
36
31
|
Want to mock a locked-out user or an invalid login attempt? You
|
|
@@ -101,7 +96,7 @@ By the way, that directory has scripts for opening Mockaton and Vite in one comm
|
|
|
101
96
|
|
|
102
97
|
The app looks like this:
|
|
103
98
|
|
|
104
|
-
<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" />
|
|
105
100
|
|
|
106
101
|
|
|
107
102
|
## Use Cases
|
|
@@ -124,7 +119,7 @@ backends to old API contracts or databases.
|
|
|
124
119
|
Perhaps you need to demo your app, but the ideal flow is too complex to
|
|
125
120
|
simulate from the actual backend. In this case, compile your frontend app and
|
|
126
121
|
put its built assets in `config.staticDir`. Then, on the dashboard
|
|
127
|
-
|
|
122
|
+
**Bulk Select** mocks to simulate the complete states you want to demo.
|
|
128
123
|
For bulk-selecting, you just need to add a comment to the mock
|
|
129
124
|
filename, such as `(demo-part1)`, `(demo-part2)`.
|
|
130
125
|
|
|
@@ -219,6 +214,9 @@ Response Status Code, and File Extension.
|
|
|
219
214
|
api/user.GET.200.json
|
|
220
215
|
```
|
|
221
216
|
|
|
217
|
+
You can also use `.empty` if you don’t want the response to have a
|
|
218
|
+
`Content-Type` header.
|
|
219
|
+
|
|
222
220
|
|
|
223
221
|
### Dynamic Parameters
|
|
224
222
|
Anything within square brackets is always matched. For example, for this route
|
|
@@ -274,7 +272,7 @@ api/foo/bar.GET.200.json
|
|
|
274
272
|
---
|
|
275
273
|
## Config
|
|
276
274
|
### `mocksDir: string`
|
|
277
|
-
This is the only required field
|
|
275
|
+
This is the only required field. The directory must exist.
|
|
278
276
|
|
|
279
277
|
|
|
280
278
|
### `host?: string`
|
|
@@ -290,10 +288,10 @@ Defaults to `/(\.DS_Store|~)$/`
|
|
|
290
288
|
|
|
291
289
|
|
|
292
290
|
### `delay?: number`
|
|
293
|
-
Defaults to `
|
|
291
|
+
Defaults to `1200` milliseconds.
|
|
294
292
|
|
|
295
293
|
Although routes can individually be delayed with the 🕓
|
|
296
|
-
checkbox, delay
|
|
294
|
+
checkbox, the delay amount is globally configurable.
|
|
297
295
|
|
|
298
296
|
|
|
299
297
|
### `proxyFallback?: string`
|
|
@@ -303,7 +301,7 @@ For example, `config.proxyFallback = 'http://example.com'`
|
|
|
303
301
|
### `collectProxied?: boolean`
|
|
304
302
|
Defaults to `false`. With this flag you can save mocks that hit
|
|
305
303
|
your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
|
|
306
|
-
URL the filename will have `[id]` in their place. For example,
|
|
304
|
+
URL, the filename will have `[id]` in their place. For example,
|
|
307
305
|
|
|
308
306
|
```
|
|
309
307
|
/api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
|
|
@@ -382,7 +380,7 @@ config.extraMimes = {
|
|
|
382
380
|
jpe: 'application/jpeg'
|
|
383
381
|
}
|
|
384
382
|
```
|
|
385
|
-
|
|
383
|
+
Those extra media types take precedence over the built-in
|
|
386
384
|
[utils/mime.js](src/utils/mime.js), so you can override them.
|
|
387
385
|
|
|
388
386
|
|
|
@@ -397,8 +395,9 @@ type Plugin = (
|
|
|
397
395
|
body: string | Uint8Array
|
|
398
396
|
}>
|
|
399
397
|
```
|
|
400
|
-
Plugins are for processing mocks before sending them. If no
|
|
401
|
-
|
|
398
|
+
Plugins are for processing mocks before sending them. If no
|
|
399
|
+
regex matches the filename, the fallback plugin will read
|
|
400
|
+
the file from disk and compute the MIME from the extension.
|
|
402
401
|
|
|
403
402
|
Note: don’t call `response.end()` on any plugin.
|
|
404
403
|
|
|
@@ -470,7 +469,7 @@ Nonetheless, you can trigger any command besides opening a browser.
|
|
|
470
469
|
---
|
|
471
470
|
|
|
472
471
|
## Commander API
|
|
473
|
-
`Commander` is a
|
|
472
|
+
`Commander` is a client for Mockaton’s HTTP API.
|
|
474
473
|
All of its methods return their `fetch` response promise.
|
|
475
474
|
```js
|
|
476
475
|
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.1",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
@@ -17,5 +17,8 @@
|
|
|
17
17
|
"optionalDependencies": {
|
|
18
18
|
"pixaton": ">=1.0.2",
|
|
19
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.css
CHANGED
|
@@ -84,7 +84,7 @@ select {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
.Header {
|
|
88
88
|
position: fixed;
|
|
89
89
|
z-index: 100;
|
|
90
90
|
top: 0;
|
|
@@ -171,7 +171,7 @@ menu {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
.MockList {
|
|
175
175
|
display: flex;
|
|
176
176
|
align-items: flex-start;
|
|
177
177
|
margin-top: 64px;
|
|
@@ -189,6 +189,10 @@ main {
|
|
|
189
189
|
border-top: 1px solid transparent;
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
+
|
|
193
|
+
&.empty {
|
|
194
|
+
margin-top: 80px;
|
|
195
|
+
}
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
|
package/src/Dashboard.js
CHANGED
|
@@ -26,7 +26,9 @@ const CSS = {
|
|
|
26
26
|
DelayToggler: 'DelayToggler',
|
|
27
27
|
FallbackBackend: 'FallbackBackend',
|
|
28
28
|
Field: 'Field',
|
|
29
|
+
Header: 'Header',
|
|
29
30
|
InternalServerErrorToggler: 'InternalServerErrorToggler',
|
|
31
|
+
MockList: 'MockList',
|
|
30
32
|
MockSelector: 'MockSelector',
|
|
31
33
|
PayloadViewer: 'PayloadViewer',
|
|
32
34
|
PreviewLink: 'PreviewLink',
|
|
@@ -36,6 +38,7 @@ const CSS = {
|
|
|
36
38
|
StaticFilesList: 'StaticFilesList',
|
|
37
39
|
|
|
38
40
|
bold: 'bold',
|
|
41
|
+
empty: 'empty',
|
|
39
42
|
chosen: 'chosen',
|
|
40
43
|
status4xx: 'status4xx'
|
|
41
44
|
}
|
|
@@ -47,7 +50,7 @@ const refPayloadViewerFileTitle = useRef()
|
|
|
47
50
|
const mockaton = new Commander(window.location.origin)
|
|
48
51
|
|
|
49
52
|
function init() {
|
|
50
|
-
Promise.all([
|
|
53
|
+
return Promise.all([
|
|
51
54
|
mockaton.listMocks(),
|
|
52
55
|
mockaton.listCookies(),
|
|
53
56
|
mockaton.listComments(),
|
|
@@ -55,59 +58,62 @@ function init() {
|
|
|
55
58
|
mockaton.getProxyFallback(),
|
|
56
59
|
mockaton.listStaticFiles()
|
|
57
60
|
].map(api => api.then(response => response.ok && response.json())))
|
|
58
|
-
.then(
|
|
61
|
+
.then(data => {
|
|
62
|
+
empty(document.body)
|
|
63
|
+
document.body.append(...App(data))
|
|
64
|
+
})
|
|
59
65
|
.catch(onError)
|
|
60
66
|
}
|
|
61
67
|
init()
|
|
62
68
|
|
|
63
|
-
function App(
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
function App([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
|
|
70
|
+
return [
|
|
71
|
+
r(Header, { cookies, comments, fallbackAddress, collectProxied }),
|
|
72
|
+
r(MockList, { brokersByMethod }),
|
|
73
|
+
r(StaticFilesList, { staticFiles })
|
|
74
|
+
]
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
|
|
78
|
+
// Header ===============
|
|
79
|
+
|
|
80
|
+
function Header({ cookies, comments, fallbackAddress, collectProxied }) {
|
|
81
|
+
return (
|
|
82
|
+
r('menu', { className: CSS.Header },
|
|
83
|
+
r(Logo),
|
|
84
|
+
r(CookieSelector, { cookies }),
|
|
85
|
+
r(BulkSelector, { comments }),
|
|
86
|
+
r(ProxyFallbackField, { fallbackAddress, collectProxied }),
|
|
87
|
+
r(ResetButton)))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Logo() {
|
|
70
91
|
return (
|
|
71
|
-
r('
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
? r('main', null, Strings.no_mocks_found)
|
|
80
|
-
: r('main', null,
|
|
81
|
-
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
82
|
-
r(SectionByMethod, { method, brokers }))),
|
|
83
|
-
r('div', { className: CSS.PayloadViewer },
|
|
84
|
-
r('h2', { ref: refPayloadViewerFileTitle }, Strings.mock),
|
|
85
|
-
r('pre', null,
|
|
86
|
-
r('code', { ref: refPayloadViewer }, Strings.click_link_to_preview)))),
|
|
87
|
-
r(StaticFilesList, { staticFiles })))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
function CookieSelector({ list }) {
|
|
92
|
+
r('img', {
|
|
93
|
+
alt: Strings.title,
|
|
94
|
+
src: '/mockaton-logo.svg',
|
|
95
|
+
width: 160
|
|
96
|
+
}))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function CookieSelector({ cookies }) {
|
|
92
100
|
function onChange() {
|
|
93
101
|
mockaton.selectCookie(this.value)
|
|
94
102
|
.catch(onError)
|
|
95
103
|
}
|
|
96
|
-
const disabled =
|
|
104
|
+
const disabled = cookies.length <= 1
|
|
97
105
|
return (
|
|
98
106
|
r('label', { className: CSS.Field },
|
|
99
107
|
r('span', null, Strings.cookie),
|
|
100
108
|
r('select', {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
r('option', { value, selected }, value)))))
|
|
109
|
+
autocomplete: 'off',
|
|
110
|
+
disabled,
|
|
111
|
+
title: disabled ? Strings.cookie_disabled_title : '',
|
|
112
|
+
onChange
|
|
113
|
+
}, cookies.map(([value, selected]) =>
|
|
114
|
+
r('option', { value, selected }, value)))))
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
|
|
111
117
|
function BulkSelector({ comments }) {
|
|
112
118
|
function onChange() {
|
|
113
119
|
mockaton.bulkSelectByComment(this.value)
|
|
@@ -122,17 +128,15 @@ function BulkSelector({ comments }) {
|
|
|
122
128
|
r('label', { className: CSS.Field },
|
|
123
129
|
r('span', null, Strings.bulk_select_by_comment),
|
|
124
130
|
r('select', {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
r('option', { value }, value)))))
|
|
131
|
+
'data-qaid': 'BulkSelector',
|
|
132
|
+
autocomplete: 'off',
|
|
133
|
+
disabled,
|
|
134
|
+
title: disabled ? Strings.bulk_select_by_comment_disabled_title : '',
|
|
135
|
+
onChange
|
|
136
|
+
}, list.map(value =>
|
|
137
|
+
r('option', { value }, value)))))
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
|
|
136
140
|
function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
137
141
|
const refSaveProxiedCheckbox = useRef()
|
|
138
142
|
function onChange(event) {
|
|
@@ -141,11 +145,10 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
|
141
145
|
if (!input.validity.valid)
|
|
142
146
|
input.reportValidity()
|
|
143
147
|
else
|
|
144
|
-
mockaton.setProxyFallback(input.value.trim())
|
|
145
|
-
.catch(onError)
|
|
148
|
+
mockaton.setProxyFallback(input.value.trim()).catch(onError)
|
|
146
149
|
}
|
|
147
150
|
return (
|
|
148
|
-
r('div', { className: CSS.Field
|
|
151
|
+
r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
|
|
149
152
|
r('label', null,
|
|
150
153
|
r('span', null, Strings.fallback_server),
|
|
151
154
|
r('input', {
|
|
@@ -162,7 +165,6 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
|
162
165
|
})))
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
|
|
166
168
|
function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
|
|
167
169
|
function onChange(event) {
|
|
168
170
|
mockaton.setCollectProxied(event.currentTarget.checked)
|
|
@@ -180,7 +182,6 @@ function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
|
|
|
180
182
|
r('span', null, Strings.save_proxied)))
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
|
|
184
185
|
function ResetButton() {
|
|
185
186
|
return (
|
|
186
187
|
r('button', {
|
|
@@ -194,22 +195,20 @@ function ResetButton() {
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
|
|
199
|
+
// MockList ===============
|
|
200
|
+
|
|
201
|
+
function MockList({ brokersByMethod }) {
|
|
202
|
+
const hasMocks = Object.keys(brokersByMethod).length
|
|
203
|
+
if (!hasMocks)
|
|
204
|
+
return (
|
|
205
|
+
r('main', { className: cssClass(CSS.MockList, CSS.empty) },
|
|
206
|
+
Strings.no_mocks_found))
|
|
200
207
|
return (
|
|
201
|
-
r('
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
r('summary', null, Strings.static),
|
|
206
|
-
r('ul', null,
|
|
207
|
-
staticFiles.map(f =>
|
|
208
|
-
r('li', null,
|
|
209
|
-
r('a', {
|
|
210
|
-
href: f,
|
|
211
|
-
target: '_blank'
|
|
212
|
-
}, f))))))
|
|
208
|
+
r('main', { className: CSS.MockList },
|
|
209
|
+
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
210
|
+
r(SectionByMethod, { method, brokers }))),
|
|
211
|
+
r(PayloadViewer)))
|
|
213
212
|
}
|
|
214
213
|
|
|
215
214
|
|
|
@@ -221,7 +220,7 @@ function SectionByMethod({ method, brokers }) {
|
|
|
221
220
|
.filter(([, broker]) => broker.mocks.length > 1) // >1 because of autogen500
|
|
222
221
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
223
222
|
.map(([urlMask, broker]) =>
|
|
224
|
-
r('tr',
|
|
223
|
+
r('tr', { 'data-method': method, 'data-urlMask': urlMask },
|
|
225
224
|
r('td', null, r(PreviewLink, { method, urlMask })),
|
|
226
225
|
r('td', null, r(MockSelector, { broker })),
|
|
227
226
|
r('td', null, r(DelayRouteToggler, { broker })),
|
|
@@ -233,27 +232,16 @@ function PreviewLink({ method, urlMask }) {
|
|
|
233
232
|
async function onClick(event) {
|
|
234
233
|
event.preventDefault()
|
|
235
234
|
try {
|
|
236
|
-
const
|
|
235
|
+
const preloader = setTimeout(() => {
|
|
237
236
|
empty(refPayloadViewer.current)
|
|
238
|
-
refPayloadViewer.current.append(
|
|
237
|
+
refPayloadViewer.current.append(PayloadViewerProgressBar())
|
|
239
238
|
}, 180)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
|
|
240
|
+
const response = await fetch(this.href, { method })
|
|
241
|
+
clearTimeout(preloader)
|
|
242
|
+
await updatePayloadViewer(method, urlMask, this.href, response)
|
|
243
243
|
document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
|
|
244
244
|
this.classList.add(CSS.chosen)
|
|
245
|
-
clearTimeout(spinner)
|
|
246
|
-
|
|
247
|
-
const mime = res.headers.get('content-type') || ''
|
|
248
|
-
if (mime.startsWith('image/')) // naively assumes GET.200
|
|
249
|
-
renderPayloadImage(this.href)
|
|
250
|
-
else
|
|
251
|
-
updatePayloadViewer(await res.text() || Strings.empty_response_body, mime)
|
|
252
|
-
|
|
253
|
-
empty(refPayloadViewerFileTitle.current)
|
|
254
|
-
refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
|
|
255
|
-
file: this.closest('tr').querySelector('select').value
|
|
256
|
-
}))
|
|
257
245
|
}
|
|
258
246
|
catch (error) {
|
|
259
247
|
onError(error)
|
|
@@ -263,64 +251,33 @@ function PreviewLink({ method, urlMask }) {
|
|
|
263
251
|
r('a', {
|
|
264
252
|
className: CSS.PreviewLink,
|
|
265
253
|
href: urlMask,
|
|
266
|
-
'data-method': method,
|
|
267
254
|
onClick
|
|
268
255
|
}, urlMask))
|
|
269
256
|
}
|
|
270
257
|
|
|
271
258
|
|
|
272
|
-
function PayloadViewerTitle({ file }) {
|
|
273
|
-
const { urlMask, method, status, ext } = parseFilename(file)
|
|
274
|
-
return (
|
|
275
|
-
r('span', null,
|
|
276
|
-
urlMask + '.' + method + '.',
|
|
277
|
-
r('abbr', { title: HttpStatus[status] }, status),
|
|
278
|
-
'.' + ext))
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
function ProgressBar() {
|
|
283
|
-
return (
|
|
284
|
-
r('div', { className: CSS.ProgressBar },
|
|
285
|
-
r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
function renderPayloadImage(href) {
|
|
290
|
-
empty(refPayloadViewer.current)
|
|
291
|
-
refPayloadViewer.current.append(r('img', { src: href }))
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function updatePayloadViewer(body, mime) {
|
|
295
|
-
if (mime === 'application/json' && window?.Prism.languages)
|
|
296
|
-
refPayloadViewer.current.innerHTML = window.Prism.highlight(body, window.Prism.languages.json, 'json')
|
|
297
|
-
else
|
|
298
|
-
refPayloadViewer.current.innerText = body
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
259
|
function MockSelector({ broker }) {
|
|
260
|
+
function className(defaultIsSelected, status) {
|
|
261
|
+
return cssClass(
|
|
262
|
+
CSS.MockSelector,
|
|
263
|
+
!defaultIsSelected && CSS.bold,
|
|
264
|
+
status >= 400 && status < 500 && CSS.status4xx)
|
|
265
|
+
}
|
|
266
|
+
|
|
303
267
|
function onChange() {
|
|
304
|
-
const { status } = parseFilename(this.value)
|
|
268
|
+
const { status, urlMask, method } = parseFilename(this.value)
|
|
305
269
|
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
306
270
|
? 'normal'
|
|
307
271
|
: 'bold'
|
|
308
272
|
mockaton.select(this.value)
|
|
309
273
|
.then(() => {
|
|
310
|
-
|
|
311
|
-
|
|
274
|
+
linkFor(method, urlMask)?.click()
|
|
275
|
+
checkbox500For(method, urlMask).checked = status === 500
|
|
312
276
|
this.className = className(this.value === this.options[0].value, status)
|
|
313
277
|
})
|
|
314
278
|
.catch(onError)
|
|
315
279
|
}
|
|
316
280
|
|
|
317
|
-
function className(defaultIsSelected, status) {
|
|
318
|
-
return cssClass(
|
|
319
|
-
CSS.MockSelector,
|
|
320
|
-
!defaultIsSelected && CSS.bold,
|
|
321
|
-
status >= 400 && status < 500 && CSS.status4xx)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
281
|
const selected = broker.currentMock.file
|
|
325
282
|
const { status, urlMask } = parseFilename(selected)
|
|
326
283
|
const files = broker.mocks.filter(item =>
|
|
@@ -329,17 +286,16 @@ function MockSelector({ broker }) {
|
|
|
329
286
|
|
|
330
287
|
return (
|
|
331
288
|
r('select', {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}, file))))
|
|
289
|
+
'data-qaid': urlMask,
|
|
290
|
+
autocomplete: 'off',
|
|
291
|
+
className: className(selected === files[0], status),
|
|
292
|
+
disabled: files.length <= 1,
|
|
293
|
+
onChange
|
|
294
|
+
}, files.map(file =>
|
|
295
|
+
r('option', {
|
|
296
|
+
value: file,
|
|
297
|
+
selected: file === selected
|
|
298
|
+
}, file))))
|
|
343
299
|
}
|
|
344
300
|
|
|
345
301
|
|
|
@@ -361,22 +317,23 @@ function DelayRouteToggler({ broker }) {
|
|
|
361
317
|
onChange
|
|
362
318
|
}),
|
|
363
319
|
TimerIcon()))
|
|
364
|
-
}
|
|
365
|
-
|
|
366
320
|
|
|
367
|
-
function TimerIcon() {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
321
|
+
function TimerIcon() {
|
|
322
|
+
return (
|
|
323
|
+
r('svg', { viewBox: '0 0 24 24' },
|
|
324
|
+
r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
|
|
325
|
+
}
|
|
371
326
|
}
|
|
372
327
|
|
|
373
328
|
|
|
374
329
|
function InternalServerErrorToggler({ broker }) {
|
|
375
330
|
function onChange(event) {
|
|
331
|
+
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
376
332
|
mockaton.select(event.currentTarget.checked
|
|
377
333
|
? broker.mocks.find(f => parseFilename(f).status === 500)
|
|
378
334
|
: broker.mocks[0])
|
|
379
335
|
.then(init)
|
|
336
|
+
.then(() => linkFor(method, urlMask)?.click())
|
|
380
337
|
.catch(onError)
|
|
381
338
|
}
|
|
382
339
|
return (
|
|
@@ -395,6 +352,86 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
395
352
|
)
|
|
396
353
|
}
|
|
397
354
|
|
|
355
|
+
function PayloadViewerProgressBar() {
|
|
356
|
+
return (
|
|
357
|
+
r('div', { className: CSS.ProgressBar },
|
|
358
|
+
r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function PayloadViewer() {
|
|
362
|
+
return (
|
|
363
|
+
r('div', { className: CSS.PayloadViewer },
|
|
364
|
+
r('h2', { ref: refPayloadViewerFileTitle }, Strings.mock),
|
|
365
|
+
r('pre', null,
|
|
366
|
+
r('code', { ref: refPayloadViewer }, Strings.click_link_to_preview))))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function PayloadViewerTitle({ file }) {
|
|
370
|
+
const { urlMask, method, status, ext } = parseFilename(file)
|
|
371
|
+
return (
|
|
372
|
+
r('span', null,
|
|
373
|
+
urlMask + '.' + method + '.',
|
|
374
|
+
r('abbr', { title: HttpStatus[status] }, status),
|
|
375
|
+
'.' + ext))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function updatePayloadViewer(method, urlMask, imgSrc, response) {
|
|
379
|
+
empty(refPayloadViewerFileTitle.current)
|
|
380
|
+
refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
|
|
381
|
+
file: mockSelectorFor(method, urlMask).value
|
|
382
|
+
}))
|
|
383
|
+
|
|
384
|
+
const mime = response.headers.get('content-type') || ''
|
|
385
|
+
if (mime.startsWith('image/')) // naively assumes GET.200
|
|
386
|
+
renderPayloadImage(imgSrc) // TESTME in pixaton
|
|
387
|
+
else
|
|
388
|
+
renderPayloadBody(await response.text() || Strings.empty_response_body, mime)
|
|
389
|
+
|
|
390
|
+
function renderPayloadImage(src) {
|
|
391
|
+
empty(refPayloadViewer.current)
|
|
392
|
+
refPayloadViewer.current.append(r('img', { src }))
|
|
393
|
+
}
|
|
394
|
+
function renderPayloadBody(body, mime) {
|
|
395
|
+
if (mime === 'application/json' && window.Prism?.highlight && window.Prism?.languages)
|
|
396
|
+
refPayloadViewer.current.innerHTML = window.Prism.highlight(body, window.Prism.languages.json, 'json')
|
|
397
|
+
else
|
|
398
|
+
refPayloadViewer.current.innerText = body
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
function trFor(method, urlMask) {
|
|
404
|
+
return document.querySelector(`tr[data-method="${method}"][data-urlMask="${urlMask}"]`)
|
|
405
|
+
}
|
|
406
|
+
function linkFor(method, urlMask) {
|
|
407
|
+
return trFor(method, urlMask)?.querySelector(`a.${CSS.PreviewLink}`)
|
|
408
|
+
}
|
|
409
|
+
function checkbox500For(method, urlMask) {
|
|
410
|
+
return trFor(method, urlMask)?.querySelector(`.${CSS.InternalServerErrorToggler} > input`)
|
|
411
|
+
}
|
|
412
|
+
function mockSelectorFor(method, urlMask) {
|
|
413
|
+
return trFor(method, urlMask)?.querySelector(`select.${CSS.MockSelector}`)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
// StaticFilesList ===============
|
|
419
|
+
|
|
420
|
+
function StaticFilesList({ staticFiles }) {
|
|
421
|
+
if (!staticFiles.length)
|
|
422
|
+
return null
|
|
423
|
+
return (
|
|
424
|
+
r('details', {
|
|
425
|
+
open: true,
|
|
426
|
+
className: CSS.StaticFilesList
|
|
427
|
+
},
|
|
428
|
+
r('summary', null, Strings.static),
|
|
429
|
+
r('ul', null, staticFiles.map(f =>
|
|
430
|
+
r('li', null,
|
|
431
|
+
r('a', { href: f, target: '_blank' }, f))))))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
398
435
|
|
|
399
436
|
function onError(error) {
|
|
400
437
|
if (error?.message === 'Failed to fetch')
|
|
@@ -403,7 +440,9 @@ function onError(error) {
|
|
|
403
440
|
}
|
|
404
441
|
|
|
405
442
|
|
|
406
|
-
|
|
443
|
+
|
|
444
|
+
// Utils ============
|
|
445
|
+
|
|
407
446
|
function cssClass(...args) {
|
|
408
447
|
return args.filter(a => a).join(' ')
|
|
409
448
|
}
|
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)
|