mockaton 8.1.1 → 8.1.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 +2 -14
- package/package.json +8 -3
- package/src/Dashboard.js +128 -114
- package/ui-tests/_setup.js +32 -0
- package/ui-tests/bulk-select.test.js +10 -0
- package/ui-tests/bulk-select.vp1024x800.dark.gold.png +0 -0
- package/ui-tests/bulk-select.vp1024x800.light.gold.png +0 -0
- package/ui-tests/initial-dashboard-state.test.js +4 -0
- package/ui-tests/initial-dashboard-state.vp1024x800.dark.gold.png +0 -0
- package/ui-tests/initial-dashboard-state.vp1024x800.light.gold.png +0 -0
- package/ui-tests/payload-viewer-formats-json.test.js +9 -0
- package/ui-tests/payload-viewer-formats-json.vp1024x800.dark.gold.png +0 -0
- package/ui-tests/payload-viewer-formats-json.vp1024x800.light.gold.png +0 -0
- package/ui-tests/payload-viewer-renders-images.test.js +9 -0
- package/ui-tests/payload-viewer-renders-images.vp1024x800.dark.gold.png +0 -0
- package/ui-tests/payload-viewer-renders-images.vp1024x800.light.gold.png +0 -0
- package/ui-tests/select-mock-variant.test.js +10 -0
- package/ui-tests/select-mock-variant.vp1024x800.dark.gold.png +0 -0
- package/ui-tests/select-mock-variant.vp1024x800.light.gold.png +0 -0
package/README.md
CHANGED
|
@@ -226,7 +226,7 @@ Also, they take precedence over the `GET` mocks in `Config.mockDir`.
|
|
|
226
226
|
For example, if you have two files for `GET /foo/bar.jpg`
|
|
227
227
|
```
|
|
228
228
|
my-static-dir/foo/bar.jpg
|
|
229
|
-
|
|
229
|
+
my-mocks-dir/foo/bar.jpg.GET.200.jpg // Unreacheable
|
|
230
230
|
```
|
|
231
231
|
|
|
232
232
|
Use Case 1: If you have a bunch of static assets you don’t want to add `.GET.200.ext`
|
|
@@ -347,17 +347,7 @@ If you don’t want to open a browser, pass a noop, such as
|
|
|
347
347
|
Config.onReady = () => {}
|
|
348
348
|
```
|
|
349
349
|
|
|
350
|
-
|
|
351
|
-
```js
|
|
352
|
-
import { exec } from 'node:child_process'
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
Config.onReady = function openInBrowser(address) {
|
|
356
|
-
exec(`xdg-open ${address}`)
|
|
357
|
-
}
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
Or, for more cross-platform utility, you could `npm install open` and pass it.
|
|
350
|
+
For a more cross-platform utility, you could `npm install open` and pass it.
|
|
361
351
|
```js
|
|
362
352
|
import open from 'open'
|
|
363
353
|
|
|
@@ -420,5 +410,3 @@ await mockaton.reset()
|
|
|
420
410
|
|
|
421
411
|
## TODO
|
|
422
412
|
- Refactor Tests
|
|
423
|
-
- Dashboard. Indicate if some file on `staticDir` is overriding a mock.
|
|
424
|
-
- jsonc, json5?
|
package/package.json
CHANGED
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
"name": "mockaton",
|
|
3
3
|
"description": "A deterministic server-side for developing and testing frontend clients",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.1.
|
|
5
|
+
"version": "8.1.3",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": "https://github.com/ericfortis/mockaton",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node --test",
|
|
11
|
+
"test": "node --test src/**.test.js",
|
|
12
12
|
"demo": "node _usage_example.js",
|
|
13
|
-
"demo:ts": "node --import=tsx _usage_example.js"
|
|
13
|
+
"demo:ts": "node --import=tsx _usage_example.js",
|
|
14
|
+
"demo:test-ui": "node --test --import=./ui-tests/_setup.js --experimental-test-isolation=none \"./ui-tests/**/*.test.js\""
|
|
15
|
+
},
|
|
16
|
+
"optionalDependencies": {
|
|
17
|
+
"puppeteer": "23.7.1",
|
|
18
|
+
"pixaton": "0.1.0"
|
|
14
19
|
}
|
|
15
20
|
}
|
package/src/Dashboard.js
CHANGED
|
@@ -53,13 +53,13 @@ function init() {
|
|
|
53
53
|
}
|
|
54
54
|
init()
|
|
55
55
|
|
|
56
|
-
function App(
|
|
56
|
+
function App(apiResponses) {
|
|
57
57
|
empty(document.body)
|
|
58
|
-
createRoot(document.body)
|
|
59
|
-
DevPanel(
|
|
58
|
+
createRoot(document.body)
|
|
59
|
+
.render(DevPanel(apiResponses))
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function DevPanel(brokersByMethod, cookies, comments, corsAllowed, staticFiles) {
|
|
62
|
+
function DevPanel([brokersByMethod, cookies, comments, corsAllowed, staticFiles]) {
|
|
63
63
|
return (
|
|
64
64
|
r('div', null,
|
|
65
65
|
r('menu', null,
|
|
@@ -78,27 +78,33 @@ function DevPanel(brokersByMethod, cookies, comments, corsAllowed, staticFiles)
|
|
|
78
78
|
r(StaticFilesList, { staticFiles })))
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
|
|
81
82
|
function CookieSelector({ list }) {
|
|
83
|
+
function onChange() {
|
|
84
|
+
mockaton.selectCookie(this.value)
|
|
85
|
+
.catch(console.error)
|
|
86
|
+
}
|
|
82
87
|
const disabled = list.length <= 1
|
|
83
88
|
return (
|
|
84
89
|
r('label', null,
|
|
85
90
|
r('span', null, Strings.cookie),
|
|
86
91
|
r('select', {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}, list.map(([key, selected]) =>
|
|
95
|
-
r('option', {
|
|
96
|
-
value: key,
|
|
97
|
-
selected
|
|
98
|
-
}, key)))))
|
|
92
|
+
autocomplete: 'off',
|
|
93
|
+
disabled,
|
|
94
|
+
title: disabled ? Strings.cookie_disabled_title : '',
|
|
95
|
+
onChange
|
|
96
|
+
},
|
|
97
|
+
list.map(([value, selected]) =>
|
|
98
|
+
r('option', { value, selected }, value)))))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
|
|
101
102
|
function BulkSelector({ comments }) {
|
|
103
|
+
function onChange() {
|
|
104
|
+
mockaton.bulkSelectByComment(this.value)
|
|
105
|
+
.then(init)
|
|
106
|
+
.catch(console.error)
|
|
107
|
+
}
|
|
102
108
|
const disabled = !comments.length
|
|
103
109
|
const list = disabled
|
|
104
110
|
? []
|
|
@@ -107,34 +113,33 @@ function BulkSelector({ comments }) {
|
|
|
107
113
|
r('label', null,
|
|
108
114
|
r('span', null, Strings.bulk_select_by_comment),
|
|
109
115
|
r('select', {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}, list.map(item =>
|
|
119
|
-
r('option', {
|
|
120
|
-
value: item
|
|
121
|
-
}, item)))))
|
|
116
|
+
'data-qaid': 'BulkSelector',
|
|
117
|
+
autocomplete: 'off',
|
|
118
|
+
disabled,
|
|
119
|
+
title: disabled ? Strings.bulk_select_by_comment_disabled_title : '',
|
|
120
|
+
onChange
|
|
121
|
+
},
|
|
122
|
+
list.map(value =>
|
|
123
|
+
r('option', { value }, value)))))
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
|
|
124
127
|
function CorsCheckbox({ corsAllowed }) {
|
|
128
|
+
function onChange(event) {
|
|
129
|
+
mockaton.setCorsAllowed(event.currentTarget.checked)
|
|
130
|
+
.catch(console.error)
|
|
131
|
+
}
|
|
125
132
|
return (
|
|
126
133
|
r('label', { className: CSS.CorsCheckbox },
|
|
127
134
|
r('input', {
|
|
128
135
|
type: 'checkbox',
|
|
129
136
|
checked: corsAllowed,
|
|
130
|
-
onChange
|
|
131
|
-
mockaton.setCorsAllowed(event.currentTarget.checked)
|
|
132
|
-
.catch(console.error)
|
|
133
|
-
}
|
|
137
|
+
onChange
|
|
134
138
|
}),
|
|
135
139
|
Strings.allow_cors))
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
|
|
138
143
|
function ResetButton() {
|
|
139
144
|
return (
|
|
140
145
|
r('button', {
|
|
@@ -147,6 +152,7 @@ function ResetButton() {
|
|
|
147
152
|
)
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
|
|
150
156
|
function StaticFilesList({ staticFiles }) {
|
|
151
157
|
if (!staticFiles.length)
|
|
152
158
|
return null
|
|
@@ -177,48 +183,50 @@ function SectionByMethod({ method, brokers }) {
|
|
|
177
183
|
r('td', null, r(PreviewLink, { method, urlMask })),
|
|
178
184
|
r('td', null, r(MockSelector, { broker })),
|
|
179
185
|
r('td', null, r(DelayRouteToggler, { broker })),
|
|
180
|
-
r('td', null, r(InternalServerErrorToggler, { broker }))
|
|
181
|
-
))))
|
|
186
|
+
r('td', null, r(InternalServerErrorToggler, { broker }))))))
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
|
|
184
190
|
function PreviewLink({ method, urlMask }) {
|
|
191
|
+
async function onClick(event) {
|
|
192
|
+
event.preventDefault()
|
|
193
|
+
try {
|
|
194
|
+
const spinner = setTimeout(() => {
|
|
195
|
+
empty(refPayloadViewer.current)
|
|
196
|
+
refPayloadViewer.current.append(ProgressBar())
|
|
197
|
+
}, 180)
|
|
198
|
+
const res = await fetch(this.href, {
|
|
199
|
+
method: this.getAttribute('data-method')
|
|
200
|
+
})
|
|
201
|
+
document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
|
|
202
|
+
this.classList.add(CSS.chosen)
|
|
203
|
+
clearTimeout(spinner)
|
|
204
|
+
|
|
205
|
+
const mime = res.headers.get('content-type') || ''
|
|
206
|
+
if (mime.startsWith('image/')) // naively assumes GET.200
|
|
207
|
+
renderPayloadImage(this.href)
|
|
208
|
+
else
|
|
209
|
+
updatePayloadViewer(await res.text() || Strings.empty_response_body, mime)
|
|
210
|
+
|
|
211
|
+
empty(refPayloadViewerFileTitle.current)
|
|
212
|
+
refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
|
|
213
|
+
file: this.closest('tr').querySelector('select').value
|
|
214
|
+
}))
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
console.error(error)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
185
220
|
return (
|
|
186
221
|
r('a', {
|
|
187
222
|
className: CSS.PreviewLink,
|
|
188
223
|
href: urlMask,
|
|
189
224
|
'data-method': method,
|
|
190
|
-
|
|
191
|
-
event.preventDefault()
|
|
192
|
-
try {
|
|
193
|
-
const spinner = setTimeout(() => {
|
|
194
|
-
empty(refPayloadViewer.current)
|
|
195
|
-
refPayloadViewer.current.append(ProgressBar())
|
|
196
|
-
}, 180)
|
|
197
|
-
const res = await fetch(this.href, {
|
|
198
|
-
method: this.getAttribute('data-method')
|
|
199
|
-
})
|
|
200
|
-
document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
|
|
201
|
-
this.classList.add(CSS.chosen)
|
|
202
|
-
clearTimeout(spinner)
|
|
203
|
-
|
|
204
|
-
const mime = res.headers.get('content-type') || ''
|
|
205
|
-
if (mime.startsWith('image/')) // naively assumes GET.200
|
|
206
|
-
renderPayloadImage(this.href)
|
|
207
|
-
else
|
|
208
|
-
updatePayloadViewer(await res.text() || Strings.empty_response_body, mime)
|
|
209
|
-
|
|
210
|
-
empty(refPayloadViewerFileTitle.current)
|
|
211
|
-
refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
|
|
212
|
-
file: this.closest('tr').querySelector('select').value
|
|
213
|
-
}))
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
console.error(error)
|
|
217
|
-
}
|
|
218
|
-
}
|
|
225
|
+
onClick
|
|
219
226
|
}, urlMask))
|
|
220
227
|
}
|
|
221
228
|
|
|
229
|
+
|
|
222
230
|
function PayloadViewerTitle({ file }) {
|
|
223
231
|
const { urlMask, method, status, ext } = parseFilename(file)
|
|
224
232
|
return (
|
|
@@ -228,12 +236,14 @@ function PayloadViewerTitle({ file }) {
|
|
|
228
236
|
'.' + ext))
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
|
|
231
240
|
function ProgressBar() {
|
|
232
241
|
return (
|
|
233
242
|
r('div', { className: CSS.ProgressBar },
|
|
234
243
|
r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
|
|
235
244
|
}
|
|
236
245
|
|
|
246
|
+
|
|
237
247
|
function renderPayloadImage(href) {
|
|
238
248
|
empty(refPayloadViewer.current)
|
|
239
249
|
refPayloadViewer.current.append(r('img', { src: href }))
|
|
@@ -248,46 +258,56 @@ function updatePayloadViewer(body, mime) {
|
|
|
248
258
|
|
|
249
259
|
|
|
250
260
|
function MockSelector({ broker }) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
function onChange() {
|
|
262
|
+
const { status } = parseFilename(this.value)
|
|
263
|
+
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
264
|
+
? 'normal'
|
|
265
|
+
: 'bold'
|
|
266
|
+
mockaton.select(this.value)
|
|
267
|
+
.then(() => {
|
|
268
|
+
this.closest('tr').querySelector('a').click()
|
|
269
|
+
this.closest('tr').querySelector(`.${CSS.InternalServerErrorToggler}>[type=checkbox]`).checked = status === 500
|
|
270
|
+
this.className = className(this.value === this.options[0].value, status)
|
|
271
|
+
})
|
|
272
|
+
.catch(console.error)
|
|
273
|
+
}
|
|
255
274
|
|
|
256
|
-
const items = broker.mocks
|
|
257
|
-
const selected = broker.currentMock.file
|
|
258
275
|
|
|
259
|
-
|
|
260
|
-
|
|
276
|
+
function className(defaultIsSelected, status) {
|
|
277
|
+
return cssClass(
|
|
278
|
+
CSS.MockSelector,
|
|
279
|
+
!defaultIsSelected && CSS.bold,
|
|
280
|
+
status >= 400 && status < 500 && CSS.status4xx)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const selected = broker.currentMock.file
|
|
284
|
+
const { status, urlMask } = parseFilename(selected)
|
|
285
|
+
const files = broker.mocks.filter(item =>
|
|
261
286
|
status === 500 ||
|
|
262
287
|
!item.includes(DEFAULT_500_COMMENT))
|
|
263
288
|
|
|
264
289
|
return (
|
|
265
290
|
r('select', {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.closest('tr').querySelector(`.${CSS.InternalServerErrorToggler}>[type=checkbox]`).checked = status === 500
|
|
278
|
-
this.className = className(this.value === this.options[0].value, status)
|
|
279
|
-
})
|
|
280
|
-
.catch(console.error)
|
|
281
|
-
}
|
|
282
|
-
}, files.map(file => r('option', {
|
|
283
|
-
value: file,
|
|
284
|
-
selected: file === selected
|
|
285
|
-
}, file))))
|
|
291
|
+
'data-qaid': urlMask,
|
|
292
|
+
autocomplete: 'off',
|
|
293
|
+
className: className(selected === files[0], status),
|
|
294
|
+
disabled: files.length <= 1,
|
|
295
|
+
onChange
|
|
296
|
+
},
|
|
297
|
+
files.map(file =>
|
|
298
|
+
r('option', {
|
|
299
|
+
value: file,
|
|
300
|
+
selected: file === selected
|
|
301
|
+
}, file))))
|
|
286
302
|
}
|
|
287
303
|
|
|
304
|
+
|
|
288
305
|
function DelayRouteToggler({ broker }) {
|
|
289
|
-
|
|
290
|
-
|
|
306
|
+
function onChange(event) {
|
|
307
|
+
const { method, urlMask } = parseFilename(this.name)
|
|
308
|
+
mockaton.setRouteIsDelayed(method, urlMask, event.currentTarget.checked)
|
|
309
|
+
.catch(console.error)
|
|
310
|
+
}
|
|
291
311
|
return (
|
|
292
312
|
r('label', {
|
|
293
313
|
className: CSS.DelayToggler,
|
|
@@ -295,28 +315,29 @@ function DelayRouteToggler({ broker }) {
|
|
|
295
315
|
},
|
|
296
316
|
r('input', {
|
|
297
317
|
type: 'checkbox',
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
onChange(event) {
|
|
302
|
-
const { method, urlMask } = parseFilename(this.name)
|
|
303
|
-
mockaton.setRouteIsDelayed(method, urlMask, event.currentTarget.checked)
|
|
304
|
-
.catch(console.error)
|
|
305
|
-
}
|
|
318
|
+
name: broker.currentMock.file,
|
|
319
|
+
checked: Boolean(broker.currentMock.delay),
|
|
320
|
+
onChange
|
|
306
321
|
}),
|
|
307
322
|
TimerIcon()))
|
|
308
323
|
}
|
|
309
324
|
|
|
325
|
+
|
|
310
326
|
function TimerIcon() {
|
|
311
327
|
return (
|
|
312
328
|
r('svg', { viewBox: '0 0 24 24' },
|
|
313
329
|
r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
|
|
314
330
|
}
|
|
315
331
|
|
|
332
|
+
|
|
316
333
|
function InternalServerErrorToggler({ broker }) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
334
|
+
function onChange(event) {
|
|
335
|
+
mockaton.select(event.currentTarget.checked
|
|
336
|
+
? broker.mocks.find(f => parseFilename(f).status === 500)
|
|
337
|
+
: broker.mocks[0])
|
|
338
|
+
.then(init)
|
|
339
|
+
.catch(console.error)
|
|
340
|
+
}
|
|
320
341
|
return (
|
|
321
342
|
r('label', {
|
|
322
343
|
className: CSS.InternalServerErrorToggler,
|
|
@@ -324,16 +345,9 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
324
345
|
},
|
|
325
346
|
r('input', {
|
|
326
347
|
type: 'checkbox',
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
onChange(event) {
|
|
331
|
-
mockaton.select(event.currentTarget.checked
|
|
332
|
-
? items.find(f => parseFilename(f).status === 500)
|
|
333
|
-
: items[0])
|
|
334
|
-
.then(init)
|
|
335
|
-
.catch(console.error)
|
|
336
|
-
}
|
|
348
|
+
name: broker.currentMock.file,
|
|
349
|
+
checked: parseFilename(broker.currentMock.file).status === 500,
|
|
350
|
+
onChange
|
|
337
351
|
}),
|
|
338
352
|
r('span', null, '500')
|
|
339
353
|
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { after } from 'node:test'
|
|
2
|
+
import { launch } from 'puppeteer'
|
|
3
|
+
import {
|
|
4
|
+
removeDiffsAndCandidates,
|
|
5
|
+
testPixels as _testPixels,
|
|
6
|
+
diffServer
|
|
7
|
+
} from 'pixaton'
|
|
8
|
+
import { Commander } from '../index.js'
|
|
9
|
+
|
|
10
|
+
// Before running these tests you need to spin up the demo:
|
|
11
|
+
// npm run demo
|
|
12
|
+
|
|
13
|
+
const MOCKATON_ADDR = 'http://localhost:2345'
|
|
14
|
+
const mockaton = new Commander(MOCKATON_ADDR)
|
|
15
|
+
|
|
16
|
+
const testsDir = import.meta.dirname
|
|
17
|
+
|
|
18
|
+
removeDiffsAndCandidates(testsDir)
|
|
19
|
+
const browser = await launch({ headless: true })
|
|
20
|
+
const page = await browser.newPage()
|
|
21
|
+
|
|
22
|
+
after(() => {
|
|
23
|
+
browser?.close()
|
|
24
|
+
diffServer(testsDir)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export function testPixels(testFileName, options = {}) {
|
|
28
|
+
options.beforeSuite = async () => await mockaton.reset()
|
|
29
|
+
options.viewports ??= [{ width: 1024, height: 800 }]
|
|
30
|
+
options.colorSchemes ??= ['light', 'dark']
|
|
31
|
+
_testPixels(page, testFileName, MOCKATON_ADDR + '/mockaton', 'body', options)
|
|
32
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|