mockaton 8.6.1 → 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 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. If you don’t have a mock for
21
- a certain API, Mockaton can fallback to your real backend. Just
22
- type your backend address in the **Fallback Backend** field.
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/README-screenshot.png" alt="Mockaton Demo App Screenshot" width="580" />
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
- "Bulk Select" mocks to simulate the complete states you want to demo.
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
 
@@ -470,7 +465,7 @@ Nonetheless, you can trigger any command besides opening a browser.
470
465
  ---
471
466
 
472
467
  ## Commander API
473
- `Commander` is a wrapper for the Mockaton HTTP API.
468
+ `Commander` is a client for Mockaton’s HTTP API.
474
469
  All of its methods return their `fetch` response promise.
475
470
  ```js
476
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: string[]
31
- corsMethods: string[]
32
- corsHeaders: string[]
33
- corsExposedHeaders: string[]
34
- corsCredentials: boolean
35
- corsMaxAge: number
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.6.1",
5
+ "version": "8.7.0",
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.mockExists(file))
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
@@ -1,6 +1,7 @@
1
1
  import { API, DF } from './ApiConstants.js'
2
2
 
3
3
 
4
+ // Client for controlling Mockaton via its HTTP API
4
5
  export class Commander {
5
6
  #addr = ''
6
7
  constructor(addr) {
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
- autocomplete: 'off',
102
- disabled,
103
- title: disabled ? Strings.cookie_disabled_title : '',
104
- onChange
105
- },
106
- list.map(([value, selected]) =>
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
- 'data-qaid': 'BulkSelector',
126
- autocomplete: 'off',
127
- disabled,
128
- title: disabled ? Strings.bulk_select_by_comment_disabled_title : '',
129
- onChange
130
- },
131
- list.map(value =>
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 + ' ' + CSS.FallbackBackend },
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
- staticFiles.map(f =>
208
- r('li', null,
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', null,
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: this.closest('tr').querySelector('select').value
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
- this.closest('tr').querySelector('a').click()
311
- this.closest('tr').querySelector(`.${CSS.InternalServerErrorToggler}>[type=checkbox]`).checked = status === 500
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
- 'data-qaid': urlMask,
333
- autocomplete: 'off',
334
- className: className(selected === files[0], status),
335
- disabled: files.length <= 1,
336
- onChange
337
- },
338
- files.map(file =>
339
- r('option', {
340
- value: file,
341
- selected: file === selected
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 (this.mockExists(file))
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
- mockExists(file) { return this.mocks.includes(file) }
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(_, filename) {
21
- if (!filename)
20
+ function handleAddedOrDeletedMocks(_, file) {
21
+ if (!file)
22
22
  return
23
- if (existsSync(join(config.mocksDir, filename)))
24
- mockBrokerCollection.registerMock(filename, 'ensureItHas500')
23
+ if (existsSync(join(config.mocksDir, file)))
24
+ mockBrokerCollection.registerMock(file, 'ensureItHas500')
25
25
  else
26
- mockBrokerCollection.unregisterMock(filename)
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
@@ -5,6 +5,7 @@ import { StandardMethods } from './utils/http-request.js'
5
5
  import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
6
6
 
7
7
 
8
+ /** @type {Config} */
8
9
  export const config = Object.seal({
9
10
  mocksDir: '',
10
11
  staticDir: '',
@@ -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: <MockBroker>
13
- * /api/route-b: <MockBroker>
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 (config.ignore.test(file) || !filenameIsValid(file))
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)