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 CHANGED
@@ -5,50 +5,55 @@
5
5
 
6
6
  ## Mock your APIs, Enhance your Development Workflow
7
7
 
8
- _Mockaton_ is an HTTP mock server built for improving the frontend
9
- development and testing experience.
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, it
12
- scans a given directory for filenames following a convention similar to the
13
- URL paths. For example, the following file will be served on `/api/user/1234`
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
- By the way, you don’t need to mock all your APIs. You can request from
19
- your backend the routes you don’t have mocks for. That’s done with:
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
- ## Scraping Mocks
24
- You can save mocks following the filename convention
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
- `config.collectProxied = true`
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
- ## Multiple Mock Variants
31
- Each route can have many mocks, which could either be:
32
- - Different response __status code__. For example, for triggering errors.
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
- In the dashboard you can select a mock variant for a particular
40
- route, among other options. In addition, there’s a programmatic API,
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.vp1024x800.light.gold.png">
45
- <source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp1024x800.dark.gold.png">
46
- <img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp1024x800.light.gold.png">
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 a script for opening Mockaton and Vite in one command.
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/README-screenshot.png" alt="Mockaton Demo App Screenshot" style="max-width: 600px" />
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
- "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.
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 wrapper for the Mockaton HTTP API.
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: 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.0",
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": ">=23.10.1"
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)