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 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
- my-mocks-dir/foo/bar.jpg.GET.200.jpg // Unreacheable
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
- On Linux, you could pass:
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.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([brokersByMethod, cookies, comments, corsAllowed, staticFiles]) {
56
+ function App(apiResponses) {
57
57
  empty(document.body)
58
- createRoot(document.body).render(
59
- DevPanel(brokersByMethod, cookies, comments, corsAllowed, staticFiles))
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
- autocomplete: 'off',
88
- disabled,
89
- title: disabled ? Strings.cookie_disabled_title : '',
90
- onChange() {
91
- mockaton.selectCookie(this.value)
92
- .catch(console.error)
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
- autocomplete: 'off',
111
- disabled,
112
- title: disabled ? Strings.bulk_select_by_comment_disabled_title : '',
113
- onChange() {
114
- mockaton.bulkSelectByComment(this.value)
115
- .then(init)
116
- .catch(console.error)
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(event) {
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
- async onClick(event) {
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
- const className = (defaultIsSelected, status) => cssClass(
252
- CSS.MockSelector,
253
- !defaultIsSelected && CSS.bold,
254
- status >= 400 && status < 500 && CSS.status4xx)
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
- const { status } = parseFilename(selected)
260
- const files = items.filter(item =>
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
- className: className(selected === files[0], status),
267
- autocomplete: 'off',
268
- disabled: files.length <= 1,
269
- onChange() {
270
- const { status } = parseFilename(this.value)
271
- this.style.fontWeight = this.value === this.options[0].value // default is selected
272
- ? 'normal'
273
- : 'bold'
274
- mockaton.select(this.value)
275
- .then(() => {
276
- this.closest('tr').querySelector('a').click()
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
- const name = broker.currentMock.file
290
- const checked = Boolean(broker.currentMock.delay)
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
- autocomplete: 'off',
299
- name,
300
- checked,
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
- const items = broker.mocks
318
- const name = broker.currentMock.file
319
- const checked = parseFilename(broker.currentMock.file).status === 500
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
- autocomplete: 'off',
328
- name,
329
- checked,
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
+ }
@@ -0,0 +1,10 @@
1
+ import { testPixels } from './_setup.js'
2
+
3
+
4
+ testPixels(import.meta.filename, {
5
+ async setup(page) {
6
+ const qaId = 'BulkSelector'
7
+ const target = '(Mockaton 500)'
8
+ await page.select(`select[data-qaid="${qaId}"]`, target)
9
+ }
10
+ })
@@ -0,0 +1,4 @@
1
+ import { testPixels } from './_setup.js'
2
+
3
+
4
+ testPixels(import.meta.filename)
@@ -0,0 +1,9 @@
1
+ import { testPixels } from './_setup.js'
2
+
3
+
4
+ testPixels(import.meta.filename, {
5
+ async setup(page) {
6
+ const linkText = '/api/user'
7
+ await page.locator(`a ::-p-text(${linkText})`).click()
8
+ }
9
+ })
@@ -0,0 +1,9 @@
1
+ import { testPixels } from './_setup.js'
2
+
3
+
4
+ testPixels(import.meta.filename, {
5
+ async setup(page) {
6
+ const linkText = '/api/user/avatar'
7
+ await page.locator(`a ::-p-text(${linkText})`).click()
8
+ }
9
+ })
@@ -0,0 +1,10 @@
1
+ import { testPixels } from './_setup.js'
2
+
3
+
4
+ testPixels(import.meta.filename, {
5
+ async setup(page) {
6
+ const qaId = '/api/user/friends'
7
+ const target = 'api/user/friends.GET.204.json'
8
+ await page.select(`select[data-qaid="${qaId}"]`, target)
9
+ }
10
+ })