mockaton 8.7.0 → 8.7.2

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,27 +5,27 @@
5
5
 
6
6
  ## Mock your APIs, Enhance your Development Workflow
7
7
 
8
- Welcome to developer experience tooling! Mockaton is here to help
9
- make your frontend development and testing easier—and a lot more fun.
8
+ Mockaton is an HTTP mock server with the goal of making
9
+ 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, 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.
11
+ With Mockaton you don’t need to write code for wiring your mocks.
12
+ Instead, just place your mocks in a directory, and it will scan it
13
+ for filenames that follow a convention similar to the URL paths.
14
14
 
15
15
  For example, for this route `/api/user/1234`, the mock filename would be:
16
16
  ```
17
17
  my-mocks-dir/api/user/[user-id].GET.200.json
18
18
  ```
19
19
 
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
+ ## Scrapping Mocks from you Backend
21
+
22
+ Mockaton can fallback to your real backend on routes you don’t have mocks for. That’s
23
+ done by typing your backend address in the **Fallback Backend** field. And if you
24
+ check **Save Mocks**, you can collect those responses that hit your backend.
25
+ Those mocks will be saved to your `config.mocksDir` following the filename convention.
25
26
 
26
27
 
27
28
  ## Multiple Mock Variants
28
- Here’s how you can create multiple mocks for a particular route:
29
29
 
30
30
  ### Adding comments in filenames
31
31
  Want to mock a locked-out user or an invalid login attempt? You
@@ -214,6 +214,9 @@ Response Status Code, and File Extension.
214
214
  api/user.GET.200.json
215
215
  ```
216
216
 
217
+ You can also use `.empty` if you don’t want the response to have a
218
+ `Content-Type` header.
219
+
217
220
 
218
221
  ### Dynamic Parameters
219
222
  Anything within square brackets is always matched. For example, for this route
@@ -269,7 +272,7 @@ api/foo/bar.GET.200.json
269
272
  ---
270
273
  ## Config
271
274
  ### `mocksDir: string`
272
- This is the only required field
275
+ This is the only required field. The directory must exist.
273
276
 
274
277
 
275
278
  ### `host?: string`
@@ -285,10 +288,10 @@ Defaults to `/(\.DS_Store|~)$/`
285
288
 
286
289
 
287
290
  ### `delay?: number`
288
- Defaults to `config.delay=1200` milliseconds.
291
+ Defaults to `1200` milliseconds.
289
292
 
290
293
  Although routes can individually be delayed with the 🕓
291
- checkbox, delay the amount is globally configurable.
294
+ checkbox, the delay amount is globally configurable.
292
295
 
293
296
 
294
297
  ### `proxyFallback?: string`
@@ -298,7 +301,7 @@ For example, `config.proxyFallback = 'http://example.com'`
298
301
  ### `collectProxied?: boolean`
299
302
  Defaults to `false`. With this flag you can save mocks that hit
300
303
  your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
301
- URL the filename will have `[id]` in their place. For example,
304
+ URL, the filename will have `[id]` in their place. For example,
302
305
 
303
306
  ```
304
307
  /api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
@@ -377,7 +380,7 @@ config.extraMimes = {
377
380
  jpe: 'application/jpeg'
378
381
  }
379
382
  ```
380
- These media types take precedence over the built-in
383
+ Those extra media types take precedence over the built-in
381
384
  [utils/mime.js](src/utils/mime.js), so you can override them.
382
385
 
383
386
 
@@ -392,8 +395,9 @@ type Plugin = (
392
395
  body: string | Uint8Array
393
396
  }>
394
397
  ```
395
- Plugins are for processing mocks before sending them. If no regex matches the filename,
396
- it fallbacks to reading the file from disk and computing the MIME from the extension.
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.
397
401
 
398
402
  Note: don’t call `response.end()` on any plugin.
399
403
 
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.7.0",
5
+ "version": "8.7.2",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Dashboard.css CHANGED
@@ -84,7 +84,7 @@ select {
84
84
  }
85
85
  }
86
86
 
87
- menu {
87
+ .Header {
88
88
  position: fixed;
89
89
  z-index: 100;
90
90
  top: 0;
@@ -153,6 +153,11 @@ menu {
153
153
  }
154
154
  }
155
155
 
156
+ .BulkSelector {
157
+ appearance: none;
158
+ text-align: center;
159
+ }
160
+
156
161
  .ResetButton {
157
162
  padding: 4px 12px;
158
163
  border: 1px solid var(--colorRed);
@@ -171,7 +176,7 @@ menu {
171
176
  }
172
177
 
173
178
 
174
- main {
179
+ .MockList {
175
180
  display: flex;
176
181
  align-items: flex-start;
177
182
  margin-top: 64px;
@@ -189,6 +194,10 @@ main {
189
194
  border-top: 1px solid transparent;
190
195
  }
191
196
  }
197
+
198
+ &.empty {
199
+ margin-top: 80px;
200
+ }
192
201
  }
193
202
 
194
203
 
package/src/Dashboard.js CHANGED
@@ -16,17 +16,20 @@ const Strings = {
16
16
  internal_server_error: 'Internal Server Error',
17
17
  mock: 'Mock',
18
18
  no_mocks_found: 'No mocks found',
19
+ pick: 'Pick…',
19
20
  reset: 'Reset',
20
21
  save_proxied: 'Save Mocks',
21
- select_one: 'Select One',
22
22
  static: 'Static'
23
23
  }
24
24
 
25
25
  const CSS = {
26
+ BulkSelector: 'BulkSelector',
26
27
  DelayToggler: 'DelayToggler',
27
28
  FallbackBackend: 'FallbackBackend',
28
29
  Field: 'Field',
30
+ Header: 'Header',
29
31
  InternalServerErrorToggler: 'InternalServerErrorToggler',
32
+ MockList: 'MockList',
30
33
  MockSelector: 'MockSelector',
31
34
  PayloadViewer: 'PayloadViewer',
32
35
  PreviewLink: 'PreviewLink',
@@ -36,6 +39,7 @@ const CSS = {
36
39
  StaticFilesList: 'StaticFilesList',
37
40
 
38
41
  bold: 'bold',
42
+ empty: 'empty',
39
43
  chosen: 'chosen',
40
44
  status4xx: 'status4xx'
41
45
  }
@@ -55,45 +59,50 @@ function init() {
55
59
  mockaton.getProxyFallback(),
56
60
  mockaton.listStaticFiles()
57
61
  ].map(api => api.then(response => response.ok && response.json())))
58
- .then(App)
62
+ .then(data => {
63
+ empty(document.body)
64
+ document.body.append(...App(data))
65
+ })
59
66
  .catch(onError)
60
67
  }
61
68
  init()
62
69
 
63
- function App(apiResponses) {
64
- empty(document.body)
65
- document.body.appendChild(DevPanel(apiResponses))
70
+ function App([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
71
+ return [
72
+ r(Header, { cookies, comments, fallbackAddress, collectProxied }),
73
+ r(MockList, { brokersByMethod }),
74
+ r(StaticFilesList, { staticFiles })
75
+ ]
66
76
  }
67
77
 
68
- function DevPanel([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
69
- const isEmpty = Object.keys(brokersByMethod).length === 0
78
+
79
+ // Header ===============
80
+
81
+ function Header({ cookies, comments, fallbackAddress, collectProxied }) {
82
+ return (
83
+ r('menu', { className: CSS.Header },
84
+ r(Logo),
85
+ r(CookieSelector, { cookies }),
86
+ r(BulkSelector, { comments }),
87
+ r(ProxyFallbackField, { fallbackAddress, collectProxied }),
88
+ r(ResetButton)))
89
+ }
90
+
91
+ function Logo() {
70
92
  return (
71
- r('div', null,
72
- r('menu', null,
73
- r('img', { src: '/mockaton-logo.svg', width: 160, alt: Strings.title }),
74
- r(CookieSelector, { list: cookies }),
75
- r(BulkSelector, { comments }),
76
- r(ProxyFallbackField, { fallbackAddress, collectProxied }),
77
- r(ResetButton)),
78
- isEmpty
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 }) {
93
+ r('img', {
94
+ alt: Strings.title,
95
+ src: '/mockaton-logo.svg',
96
+ width: 160
97
+ }))
98
+ }
99
+
100
+ function CookieSelector({ cookies }) {
92
101
  function onChange() {
93
102
  mockaton.selectCookie(this.value)
94
103
  .catch(onError)
95
104
  }
96
- const disabled = list.length <= 1
105
+ const disabled = cookies.length <= 1
97
106
  return (
98
107
  r('label', { className: CSS.Field },
99
108
  r('span', null, Strings.cookie),
@@ -102,25 +111,29 @@ function CookieSelector({ list }) {
102
111
  disabled,
103
112
  title: disabled ? Strings.cookie_disabled_title : '',
104
113
  onChange
105
- }, list.map(([value, selected]) =>
114
+ }, cookies.map(([value, selected]) =>
106
115
  r('option', { value, selected }, value)))))
107
116
  }
108
117
 
109
-
110
118
  function BulkSelector({ comments }) {
119
+ // UX wise this should be a menu instead of this `select`.
120
+ // But this way is easier to implement, with a few hacks.
111
121
  function onChange() {
112
- mockaton.bulkSelectByComment(this.value)
122
+ const value = this.value
123
+ this.value = Strings.pick // Hack
124
+ mockaton.bulkSelectByComment(value)
113
125
  .then(init)
114
126
  .catch(onError)
115
127
  }
116
128
  const disabled = !comments.length
117
129
  const list = disabled
118
130
  ? []
119
- : [Strings.select_one].concat(comments)
131
+ : [Strings.pick].concat(comments)
120
132
  return (
121
133
  r('label', { className: CSS.Field },
122
134
  r('span', null, Strings.bulk_select_by_comment),
123
135
  r('select', {
136
+ className: CSS.BulkSelector,
124
137
  'data-qaid': 'BulkSelector',
125
138
  autocomplete: 'off',
126
139
  disabled,
@@ -130,7 +143,6 @@ function BulkSelector({ comments }) {
130
143
  r('option', { value }, value)))))
131
144
  }
132
145
 
133
-
134
146
  function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
135
147
  const refSaveProxiedCheckbox = useRef()
136
148
  function onChange(event) {
@@ -139,8 +151,7 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
139
151
  if (!input.validity.valid)
140
152
  input.reportValidity()
141
153
  else
142
- mockaton.setProxyFallback(input.value.trim())
143
- .catch(onError)
154
+ mockaton.setProxyFallback(input.value.trim()).catch(onError)
144
155
  }
145
156
  return (
146
157
  r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
@@ -160,7 +171,6 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
160
171
  })))
161
172
  }
162
173
 
163
-
164
174
  function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
165
175
  function onChange(event) {
166
176
  mockaton.setCollectProxied(event.currentTarget.checked)
@@ -178,7 +188,6 @@ function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
178
188
  r('span', null, Strings.save_proxied)))
179
189
  }
180
190
 
181
-
182
191
  function ResetButton() {
183
192
  return (
184
193
  r('button', {
@@ -192,18 +201,20 @@ function ResetButton() {
192
201
  }
193
202
 
194
203
 
195
- function StaticFilesList({ staticFiles }) {
196
- if (!staticFiles.length)
197
- return null
204
+
205
+ // MockList ===============
206
+
207
+ function MockList({ brokersByMethod }) {
208
+ const hasMocks = Object.keys(brokersByMethod).length
209
+ if (!hasMocks)
210
+ return (
211
+ r('main', { className: cssClass(CSS.MockList, CSS.empty) },
212
+ Strings.no_mocks_found))
198
213
  return (
199
- r('details', {
200
- open: true,
201
- className: CSS.StaticFilesList
202
- },
203
- r('summary', null, Strings.static),
204
- r('ul', null, staticFiles.map(f =>
205
- r('li', null,
206
- r('a', { href: f, target: '_blank' }, f))))))
214
+ r('main', { className: CSS.MockList },
215
+ r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
216
+ r(SectionByMethod, { method, brokers }))),
217
+ r(PayloadViewer)))
207
218
  }
208
219
 
209
220
 
@@ -227,25 +238,16 @@ function PreviewLink({ method, urlMask }) {
227
238
  async function onClick(event) {
228
239
  event.preventDefault()
229
240
  try {
230
- const spinner = setTimeout(() => {
241
+ const preloader = setTimeout(() => {
231
242
  empty(refPayloadViewer.current)
232
- refPayloadViewer.current.append(ProgressBar())
243
+ refPayloadViewer.current.append(PayloadViewerProgressBar())
233
244
  }, 180)
234
- const res = await fetch(this.href, { method })
245
+
246
+ const response = await fetch(this.href, { method })
247
+ clearTimeout(preloader)
248
+ await updatePayloadViewer(method, urlMask, this.href, response)
235
249
  document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
236
250
  this.classList.add(CSS.chosen)
237
- clearTimeout(spinner)
238
-
239
- const mime = res.headers.get('content-type') || ''
240
- if (mime.startsWith('image/')) // naively assumes GET.200
241
- renderPayloadImage(this.href)
242
- else
243
- updatePayloadViewer(await res.text() || Strings.empty_response_body, mime)
244
-
245
- empty(refPayloadViewerFileTitle.current)
246
- refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
247
- file: mockSelectorFor(method, urlMask).value
248
- }))
249
251
  }
250
252
  catch (error) {
251
253
  onError(error)
@@ -260,37 +262,14 @@ function PreviewLink({ method, urlMask }) {
260
262
  }
261
263
 
262
264
 
263
- function PayloadViewerTitle({ file }) {
264
- const { urlMask, method, status, ext } = parseFilename(file)
265
- return (
266
- r('span', null,
267
- urlMask + '.' + method + '.',
268
- r('abbr', { title: HttpStatus[status] }, status),
269
- '.' + ext))
270
- }
271
-
272
-
273
- function ProgressBar() {
274
- return (
275
- r('div', { className: CSS.ProgressBar },
276
- r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
277
- }
278
-
279
-
280
- function renderPayloadImage(href) {
281
- empty(refPayloadViewer.current)
282
- refPayloadViewer.current.append(r('img', { src: href }))
283
- }
284
-
285
- function updatePayloadViewer(body, mime) {
286
- if (mime === 'application/json' && window?.Prism.languages)
287
- refPayloadViewer.current.innerHTML = window.Prism.highlight(body, window.Prism.languages.json, 'json')
288
- else
289
- refPayloadViewer.current.innerText = body
290
- }
291
-
292
-
293
265
  function MockSelector({ broker }) {
266
+ function className(defaultIsSelected, status) {
267
+ return cssClass(
268
+ CSS.MockSelector,
269
+ !defaultIsSelected && CSS.bold,
270
+ status >= 400 && status < 500 && CSS.status4xx)
271
+ }
272
+
294
273
  function onChange() {
295
274
  const { status, urlMask, method } = parseFilename(this.value)
296
275
  this.style.fontWeight = this.value === this.options[0].value // default is selected
@@ -305,13 +284,6 @@ function MockSelector({ broker }) {
305
284
  .catch(onError)
306
285
  }
307
286
 
308
- function className(defaultIsSelected, status) {
309
- return cssClass(
310
- CSS.MockSelector,
311
- !defaultIsSelected && CSS.bold,
312
- status >= 400 && status < 500 && CSS.status4xx)
313
- }
314
-
315
287
  const selected = broker.currentMock.file
316
288
  const { status, urlMask } = parseFilename(selected)
317
289
  const files = broker.mocks.filter(item =>
@@ -351,13 +323,12 @@ function DelayRouteToggler({ broker }) {
351
323
  onChange
352
324
  }),
353
325
  TimerIcon()))
354
- }
355
326
 
356
-
357
- function TimerIcon() {
358
- return (
359
- r('svg', { viewBox: '0 0 24 24' },
360
- r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
327
+ function TimerIcon() {
328
+ return (
329
+ r('svg', { viewBox: '0 0 24 24' },
330
+ r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
331
+ }
361
332
  }
362
333
 
363
334
 
@@ -387,6 +358,54 @@ function InternalServerErrorToggler({ broker }) {
387
358
  )
388
359
  }
389
360
 
361
+ function PayloadViewerProgressBar() {
362
+ return (
363
+ r('div', { className: CSS.ProgressBar },
364
+ r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
365
+ }
366
+
367
+ function PayloadViewer() {
368
+ return (
369
+ r('div', { className: CSS.PayloadViewer },
370
+ r('h2', { ref: refPayloadViewerFileTitle }, Strings.mock),
371
+ r('pre', null,
372
+ r('code', { ref: refPayloadViewer }, Strings.click_link_to_preview))))
373
+ }
374
+
375
+ function PayloadViewerTitle({ file }) {
376
+ const { urlMask, method, status, ext } = parseFilename(file)
377
+ return (
378
+ r('span', null,
379
+ urlMask + '.' + method + '.',
380
+ r('abbr', { title: HttpStatus[status] }, status),
381
+ '.' + ext))
382
+ }
383
+
384
+ async function updatePayloadViewer(method, urlMask, imgSrc, response) {
385
+ empty(refPayloadViewerFileTitle.current)
386
+ refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
387
+ file: mockSelectorFor(method, urlMask).value
388
+ }))
389
+
390
+ const mime = response.headers.get('content-type') || ''
391
+ if (mime.startsWith('image/')) // naively assumes GET.200
392
+ renderPayloadImage(imgSrc) // TESTME in pixaton
393
+ else
394
+ renderPayloadBody(await response.text() || Strings.empty_response_body, mime)
395
+
396
+ function renderPayloadImage(src) {
397
+ empty(refPayloadViewer.current)
398
+ refPayloadViewer.current.append(r('img', { src }))
399
+ }
400
+ function renderPayloadBody(body, mime) {
401
+ if (mime === 'application/json' && window.Prism?.highlight && window.Prism?.languages)
402
+ refPayloadViewer.current.innerHTML = window.Prism.highlight(body, window.Prism.languages.json, 'json')
403
+ else
404
+ refPayloadViewer.current.innerText = body
405
+ }
406
+ }
407
+
408
+
390
409
  function trFor(method, urlMask) {
391
410
  return document.querySelector(`tr[data-method="${method}"][data-urlMask="${urlMask}"]`)
392
411
  }
@@ -401,6 +420,25 @@ function mockSelectorFor(method, urlMask) {
401
420
  }
402
421
 
403
422
 
423
+
424
+ // StaticFilesList ===============
425
+
426
+ function StaticFilesList({ staticFiles }) {
427
+ if (!staticFiles.length)
428
+ return null
429
+ return (
430
+ r('details', {
431
+ open: true,
432
+ className: CSS.StaticFilesList
433
+ },
434
+ r('summary', null, Strings.static),
435
+ r('ul', null, staticFiles.map(f =>
436
+ r('li', null,
437
+ r('a', { href: f, target: '_blank' }, f))))))
438
+ }
439
+
440
+
441
+
404
442
  function onError(error) {
405
443
  if (error?.message === 'Failed to fetch')
406
444
  alert('Looks like the Mockaton server is not running')
@@ -408,7 +446,9 @@ function onError(error) {
408
446
  }
409
447
 
410
448
 
411
- /* === Utils === */
449
+
450
+ // Utils ============
451
+
412
452
  function cssClass(...args) {
413
453
  return args.filter(a => a).join(' ')
414
454
  }