mockaton 8.7.0 → 8.7.1

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
@@ -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.1",
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;
@@ -171,7 +171,7 @@ menu {
171
171
  }
172
172
 
173
173
 
174
- main {
174
+ .MockList {
175
175
  display: flex;
176
176
  align-items: flex-start;
177
177
  margin-top: 64px;
@@ -189,6 +189,10 @@ main {
189
189
  border-top: 1px solid transparent;
190
190
  }
191
191
  }
192
+
193
+ &.empty {
194
+ margin-top: 80px;
195
+ }
192
196
  }
193
197
 
194
198
 
package/src/Dashboard.js CHANGED
@@ -26,7 +26,9 @@ const CSS = {
26
26
  DelayToggler: 'DelayToggler',
27
27
  FallbackBackend: 'FallbackBackend',
28
28
  Field: 'Field',
29
+ Header: 'Header',
29
30
  InternalServerErrorToggler: 'InternalServerErrorToggler',
31
+ MockList: 'MockList',
30
32
  MockSelector: 'MockSelector',
31
33
  PayloadViewer: 'PayloadViewer',
32
34
  PreviewLink: 'PreviewLink',
@@ -36,6 +38,7 @@ const CSS = {
36
38
  StaticFilesList: 'StaticFilesList',
37
39
 
38
40
  bold: 'bold',
41
+ empty: 'empty',
39
42
  chosen: 'chosen',
40
43
  status4xx: 'status4xx'
41
44
  }
@@ -55,45 +58,50 @@ function init() {
55
58
  mockaton.getProxyFallback(),
56
59
  mockaton.listStaticFiles()
57
60
  ].map(api => api.then(response => response.ok && response.json())))
58
- .then(App)
61
+ .then(data => {
62
+ empty(document.body)
63
+ document.body.append(...App(data))
64
+ })
59
65
  .catch(onError)
60
66
  }
61
67
  init()
62
68
 
63
- function App(apiResponses) {
64
- empty(document.body)
65
- document.body.appendChild(DevPanel(apiResponses))
69
+ function App([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
70
+ return [
71
+ r(Header, { cookies, comments, fallbackAddress, collectProxied }),
72
+ r(MockList, { brokersByMethod }),
73
+ r(StaticFilesList, { staticFiles })
74
+ ]
66
75
  }
67
76
 
68
- function DevPanel([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
69
- const isEmpty = Object.keys(brokersByMethod).length === 0
77
+
78
+ // Header ===============
79
+
80
+ function Header({ cookies, comments, fallbackAddress, collectProxied }) {
81
+ return (
82
+ r('menu', { className: CSS.Header },
83
+ r(Logo),
84
+ r(CookieSelector, { cookies }),
85
+ r(BulkSelector, { comments }),
86
+ r(ProxyFallbackField, { fallbackAddress, collectProxied }),
87
+ r(ResetButton)))
88
+ }
89
+
90
+ function Logo() {
70
91
  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 }) {
92
+ r('img', {
93
+ alt: Strings.title,
94
+ src: '/mockaton-logo.svg',
95
+ width: 160
96
+ }))
97
+ }
98
+
99
+ function CookieSelector({ cookies }) {
92
100
  function onChange() {
93
101
  mockaton.selectCookie(this.value)
94
102
  .catch(onError)
95
103
  }
96
- const disabled = list.length <= 1
104
+ const disabled = cookies.length <= 1
97
105
  return (
98
106
  r('label', { className: CSS.Field },
99
107
  r('span', null, Strings.cookie),
@@ -102,11 +110,10 @@ function CookieSelector({ list }) {
102
110
  disabled,
103
111
  title: disabled ? Strings.cookie_disabled_title : '',
104
112
  onChange
105
- }, list.map(([value, selected]) =>
113
+ }, cookies.map(([value, selected]) =>
106
114
  r('option', { value, selected }, value)))))
107
115
  }
108
116
 
109
-
110
117
  function BulkSelector({ comments }) {
111
118
  function onChange() {
112
119
  mockaton.bulkSelectByComment(this.value)
@@ -130,7 +137,6 @@ function BulkSelector({ comments }) {
130
137
  r('option', { value }, value)))))
131
138
  }
132
139
 
133
-
134
140
  function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
135
141
  const refSaveProxiedCheckbox = useRef()
136
142
  function onChange(event) {
@@ -139,8 +145,7 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
139
145
  if (!input.validity.valid)
140
146
  input.reportValidity()
141
147
  else
142
- mockaton.setProxyFallback(input.value.trim())
143
- .catch(onError)
148
+ mockaton.setProxyFallback(input.value.trim()).catch(onError)
144
149
  }
145
150
  return (
146
151
  r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
@@ -160,7 +165,6 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
160
165
  })))
161
166
  }
162
167
 
163
-
164
168
  function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
165
169
  function onChange(event) {
166
170
  mockaton.setCollectProxied(event.currentTarget.checked)
@@ -178,7 +182,6 @@ function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
178
182
  r('span', null, Strings.save_proxied)))
179
183
  }
180
184
 
181
-
182
185
  function ResetButton() {
183
186
  return (
184
187
  r('button', {
@@ -192,18 +195,20 @@ function ResetButton() {
192
195
  }
193
196
 
194
197
 
195
- function StaticFilesList({ staticFiles }) {
196
- if (!staticFiles.length)
197
- return null
198
+
199
+ // MockList ===============
200
+
201
+ function MockList({ brokersByMethod }) {
202
+ const hasMocks = Object.keys(brokersByMethod).length
203
+ if (!hasMocks)
204
+ return (
205
+ r('main', { className: cssClass(CSS.MockList, CSS.empty) },
206
+ Strings.no_mocks_found))
198
207
  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))))))
208
+ r('main', { className: CSS.MockList },
209
+ r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
210
+ r(SectionByMethod, { method, brokers }))),
211
+ r(PayloadViewer)))
207
212
  }
208
213
 
209
214
 
@@ -227,25 +232,16 @@ function PreviewLink({ method, urlMask }) {
227
232
  async function onClick(event) {
228
233
  event.preventDefault()
229
234
  try {
230
- const spinner = setTimeout(() => {
235
+ const preloader = setTimeout(() => {
231
236
  empty(refPayloadViewer.current)
232
- refPayloadViewer.current.append(ProgressBar())
237
+ refPayloadViewer.current.append(PayloadViewerProgressBar())
233
238
  }, 180)
234
- const res = await fetch(this.href, { method })
239
+
240
+ const response = await fetch(this.href, { method })
241
+ clearTimeout(preloader)
242
+ await updatePayloadViewer(method, urlMask, this.href, response)
235
243
  document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
236
244
  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
245
  }
250
246
  catch (error) {
251
247
  onError(error)
@@ -260,37 +256,14 @@ function PreviewLink({ method, urlMask }) {
260
256
  }
261
257
 
262
258
 
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
259
  function MockSelector({ broker }) {
260
+ function className(defaultIsSelected, status) {
261
+ return cssClass(
262
+ CSS.MockSelector,
263
+ !defaultIsSelected && CSS.bold,
264
+ status >= 400 && status < 500 && CSS.status4xx)
265
+ }
266
+
294
267
  function onChange() {
295
268
  const { status, urlMask, method } = parseFilename(this.value)
296
269
  this.style.fontWeight = this.value === this.options[0].value // default is selected
@@ -305,13 +278,6 @@ function MockSelector({ broker }) {
305
278
  .catch(onError)
306
279
  }
307
280
 
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
281
  const selected = broker.currentMock.file
316
282
  const { status, urlMask } = parseFilename(selected)
317
283
  const files = broker.mocks.filter(item =>
@@ -351,13 +317,12 @@ function DelayRouteToggler({ broker }) {
351
317
  onChange
352
318
  }),
353
319
  TimerIcon()))
354
- }
355
320
 
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' })))
321
+ function TimerIcon() {
322
+ return (
323
+ r('svg', { viewBox: '0 0 24 24' },
324
+ r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
325
+ }
361
326
  }
362
327
 
363
328
 
@@ -387,6 +352,54 @@ function InternalServerErrorToggler({ broker }) {
387
352
  )
388
353
  }
389
354
 
355
+ function PayloadViewerProgressBar() {
356
+ return (
357
+ r('div', { className: CSS.ProgressBar },
358
+ r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
359
+ }
360
+
361
+ function PayloadViewer() {
362
+ return (
363
+ r('div', { className: CSS.PayloadViewer },
364
+ r('h2', { ref: refPayloadViewerFileTitle }, Strings.mock),
365
+ r('pre', null,
366
+ r('code', { ref: refPayloadViewer }, Strings.click_link_to_preview))))
367
+ }
368
+
369
+ function PayloadViewerTitle({ file }) {
370
+ const { urlMask, method, status, ext } = parseFilename(file)
371
+ return (
372
+ r('span', null,
373
+ urlMask + '.' + method + '.',
374
+ r('abbr', { title: HttpStatus[status] }, status),
375
+ '.' + ext))
376
+ }
377
+
378
+ async function updatePayloadViewer(method, urlMask, imgSrc, response) {
379
+ empty(refPayloadViewerFileTitle.current)
380
+ refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
381
+ file: mockSelectorFor(method, urlMask).value
382
+ }))
383
+
384
+ const mime = response.headers.get('content-type') || ''
385
+ if (mime.startsWith('image/')) // naively assumes GET.200
386
+ renderPayloadImage(imgSrc) // TESTME in pixaton
387
+ else
388
+ renderPayloadBody(await response.text() || Strings.empty_response_body, mime)
389
+
390
+ function renderPayloadImage(src) {
391
+ empty(refPayloadViewer.current)
392
+ refPayloadViewer.current.append(r('img', { src }))
393
+ }
394
+ function renderPayloadBody(body, mime) {
395
+ if (mime === 'application/json' && window.Prism?.highlight && window.Prism?.languages)
396
+ refPayloadViewer.current.innerHTML = window.Prism.highlight(body, window.Prism.languages.json, 'json')
397
+ else
398
+ refPayloadViewer.current.innerText = body
399
+ }
400
+ }
401
+
402
+
390
403
  function trFor(method, urlMask) {
391
404
  return document.querySelector(`tr[data-method="${method}"][data-urlMask="${urlMask}"]`)
392
405
  }
@@ -401,6 +414,25 @@ function mockSelectorFor(method, urlMask) {
401
414
  }
402
415
 
403
416
 
417
+
418
+ // StaticFilesList ===============
419
+
420
+ function StaticFilesList({ staticFiles }) {
421
+ if (!staticFiles.length)
422
+ return null
423
+ return (
424
+ r('details', {
425
+ open: true,
426
+ className: CSS.StaticFilesList
427
+ },
428
+ r('summary', null, Strings.static),
429
+ r('ul', null, staticFiles.map(f =>
430
+ r('li', null,
431
+ r('a', { href: f, target: '_blank' }, f))))))
432
+ }
433
+
434
+
435
+
404
436
  function onError(error) {
405
437
  if (error?.message === 'Failed to fetch')
406
438
  alert('Looks like the Mockaton server is not running')
@@ -408,7 +440,9 @@ function onError(error) {
408
440
  }
409
441
 
410
442
 
411
- /* === Utils === */
443
+
444
+ // Utils ============
445
+
412
446
  function cssClass(...args) {
413
447
  return args.filter(a => a).join(' ')
414
448
  }