mockaton 8.6.1 → 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
@@ -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
 
@@ -219,6 +214,9 @@ Response Status Code, and File Extension.
219
214
  api/user.GET.200.json
220
215
  ```
221
216
 
217
+ You can also use `.empty` if you don’t want the response to have a
218
+ `Content-Type` header.
219
+
222
220
 
223
221
  ### Dynamic Parameters
224
222
  Anything within square brackets is always matched. For example, for this route
@@ -274,7 +272,7 @@ api/foo/bar.GET.200.json
274
272
  ---
275
273
  ## Config
276
274
  ### `mocksDir: string`
277
- This is the only required field
275
+ This is the only required field. The directory must exist.
278
276
 
279
277
 
280
278
  ### `host?: string`
@@ -290,10 +288,10 @@ Defaults to `/(\.DS_Store|~)$/`
290
288
 
291
289
 
292
290
  ### `delay?: number`
293
- Defaults to `config.delay=1200` milliseconds.
291
+ Defaults to `1200` milliseconds.
294
292
 
295
293
  Although routes can individually be delayed with the 🕓
296
- checkbox, delay the amount is globally configurable.
294
+ checkbox, the delay amount is globally configurable.
297
295
 
298
296
 
299
297
  ### `proxyFallback?: string`
@@ -303,7 +301,7 @@ For example, `config.proxyFallback = 'http://example.com'`
303
301
  ### `collectProxied?: boolean`
304
302
  Defaults to `false`. With this flag you can save mocks that hit
305
303
  your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
306
- URL the filename will have `[id]` in their place. For example,
304
+ URL, the filename will have `[id]` in their place. For example,
307
305
 
308
306
  ```
309
307
  /api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
@@ -382,7 +380,7 @@ config.extraMimes = {
382
380
  jpe: 'application/jpeg'
383
381
  }
384
382
  ```
385
- These media types take precedence over the built-in
383
+ Those extra media types take precedence over the built-in
386
384
  [utils/mime.js](src/utils/mime.js), so you can override them.
387
385
 
388
386
 
@@ -397,8 +395,9 @@ type Plugin = (
397
395
  body: string | Uint8Array
398
396
  }>
399
397
  ```
400
- Plugins are for processing mocks before sending them. If no regex matches the filename,
401
- 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.
402
401
 
403
402
  Note: don’t call `response.end()` on any plugin.
404
403
 
@@ -470,7 +469,7 @@ Nonetheless, you can trigger any command besides opening a browser.
470
469
  ---
471
470
 
472
471
  ## Commander API
473
- `Commander` is a wrapper for the Mockaton HTTP API.
472
+ `Commander` is a client for Mockaton’s HTTP API.
474
473
  All of its methods return their `fetch` response promise.
475
474
  ```js
476
475
  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.1",
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.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
  }
@@ -47,7 +50,7 @@ const refPayloadViewerFileTitle = useRef()
47
50
  const mockaton = new Commander(window.location.origin)
48
51
 
49
52
  function init() {
50
- Promise.all([
53
+ return Promise.all([
51
54
  mockaton.listMocks(),
52
55
  mockaton.listCookies(),
53
56
  mockaton.listComments(),
@@ -55,59 +58,62 @@ 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),
100
108
  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)))))
109
+ autocomplete: 'off',
110
+ disabled,
111
+ title: disabled ? Strings.cookie_disabled_title : '',
112
+ onChange
113
+ }, cookies.map(([value, selected]) =>
114
+ r('option', { value, selected }, value)))))
108
115
  }
109
116
 
110
-
111
117
  function BulkSelector({ comments }) {
112
118
  function onChange() {
113
119
  mockaton.bulkSelectByComment(this.value)
@@ -122,17 +128,15 @@ function BulkSelector({ comments }) {
122
128
  r('label', { className: CSS.Field },
123
129
  r('span', null, Strings.bulk_select_by_comment),
124
130
  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)))))
131
+ 'data-qaid': 'BulkSelector',
132
+ autocomplete: 'off',
133
+ disabled,
134
+ title: disabled ? Strings.bulk_select_by_comment_disabled_title : '',
135
+ onChange
136
+ }, list.map(value =>
137
+ r('option', { value }, value)))))
133
138
  }
134
139
 
135
-
136
140
  function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
137
141
  const refSaveProxiedCheckbox = useRef()
138
142
  function onChange(event) {
@@ -141,11 +145,10 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
141
145
  if (!input.validity.valid)
142
146
  input.reportValidity()
143
147
  else
144
- mockaton.setProxyFallback(input.value.trim())
145
- .catch(onError)
148
+ mockaton.setProxyFallback(input.value.trim()).catch(onError)
146
149
  }
147
150
  return (
148
- r('div', { className: CSS.Field + ' ' + CSS.FallbackBackend },
151
+ r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
149
152
  r('label', null,
150
153
  r('span', null, Strings.fallback_server),
151
154
  r('input', {
@@ -162,7 +165,6 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
162
165
  })))
163
166
  }
164
167
 
165
-
166
168
  function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
167
169
  function onChange(event) {
168
170
  mockaton.setCollectProxied(event.currentTarget.checked)
@@ -180,7 +182,6 @@ function SaveProxiedCheckbox({ ref, disabled, collectProxied }) {
180
182
  r('span', null, Strings.save_proxied)))
181
183
  }
182
184
 
183
-
184
185
  function ResetButton() {
185
186
  return (
186
187
  r('button', {
@@ -194,22 +195,20 @@ function ResetButton() {
194
195
  }
195
196
 
196
197
 
197
- function StaticFilesList({ staticFiles }) {
198
- if (!staticFiles.length)
199
- 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))
200
207
  return (
201
- r('details', {
202
- open: true,
203
- className: CSS.StaticFilesList
204
- },
205
- 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))))))
208
+ r('main', { className: CSS.MockList },
209
+ r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
210
+ r(SectionByMethod, { method, brokers }))),
211
+ r(PayloadViewer)))
213
212
  }
214
213
 
215
214
 
@@ -221,7 +220,7 @@ function SectionByMethod({ method, brokers }) {
221
220
  .filter(([, broker]) => broker.mocks.length > 1) // >1 because of autogen500
222
221
  .sort((a, b) => a[0].localeCompare(b[0]))
223
222
  .map(([urlMask, broker]) =>
224
- r('tr', null,
223
+ r('tr', { 'data-method': method, 'data-urlMask': urlMask },
225
224
  r('td', null, r(PreviewLink, { method, urlMask })),
226
225
  r('td', null, r(MockSelector, { broker })),
227
226
  r('td', null, r(DelayRouteToggler, { broker })),
@@ -233,27 +232,16 @@ function PreviewLink({ method, urlMask }) {
233
232
  async function onClick(event) {
234
233
  event.preventDefault()
235
234
  try {
236
- const spinner = setTimeout(() => {
235
+ const preloader = setTimeout(() => {
237
236
  empty(refPayloadViewer.current)
238
- refPayloadViewer.current.append(ProgressBar())
237
+ refPayloadViewer.current.append(PayloadViewerProgressBar())
239
238
  }, 180)
240
- const res = await fetch(this.href, {
241
- method: this.getAttribute('data-method')
242
- })
239
+
240
+ const response = await fetch(this.href, { method })
241
+ clearTimeout(preloader)
242
+ await updatePayloadViewer(method, urlMask, this.href, response)
243
243
  document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
244
244
  this.classList.add(CSS.chosen)
245
- clearTimeout(spinner)
246
-
247
- const mime = res.headers.get('content-type') || ''
248
- if (mime.startsWith('image/')) // naively assumes GET.200
249
- renderPayloadImage(this.href)
250
- else
251
- updatePayloadViewer(await res.text() || Strings.empty_response_body, mime)
252
-
253
- empty(refPayloadViewerFileTitle.current)
254
- refPayloadViewerFileTitle.current.append(PayloadViewerTitle({
255
- file: this.closest('tr').querySelector('select').value
256
- }))
257
245
  }
258
246
  catch (error) {
259
247
  onError(error)
@@ -263,64 +251,33 @@ function PreviewLink({ method, urlMask }) {
263
251
  r('a', {
264
252
  className: CSS.PreviewLink,
265
253
  href: urlMask,
266
- 'data-method': method,
267
254
  onClick
268
255
  }, urlMask))
269
256
  }
270
257
 
271
258
 
272
- function PayloadViewerTitle({ file }) {
273
- const { urlMask, method, status, ext } = parseFilename(file)
274
- return (
275
- r('span', null,
276
- urlMask + '.' + method + '.',
277
- r('abbr', { title: HttpStatus[status] }, status),
278
- '.' + ext))
279
- }
280
-
281
-
282
- function ProgressBar() {
283
- return (
284
- r('div', { className: CSS.ProgressBar },
285
- r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
286
- }
287
-
288
-
289
- function renderPayloadImage(href) {
290
- empty(refPayloadViewer.current)
291
- refPayloadViewer.current.append(r('img', { src: href }))
292
- }
293
-
294
- function updatePayloadViewer(body, mime) {
295
- if (mime === 'application/json' && window?.Prism.languages)
296
- refPayloadViewer.current.innerHTML = window.Prism.highlight(body, window.Prism.languages.json, 'json')
297
- else
298
- refPayloadViewer.current.innerText = body
299
- }
300
-
301
-
302
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
+
303
267
  function onChange() {
304
- const { status } = parseFilename(this.value)
268
+ const { status, urlMask, method } = parseFilename(this.value)
305
269
  this.style.fontWeight = this.value === this.options[0].value // default is selected
306
270
  ? 'normal'
307
271
  : 'bold'
308
272
  mockaton.select(this.value)
309
273
  .then(() => {
310
- this.closest('tr').querySelector('a').click()
311
- this.closest('tr').querySelector(`.${CSS.InternalServerErrorToggler}>[type=checkbox]`).checked = status === 500
274
+ linkFor(method, urlMask)?.click()
275
+ checkbox500For(method, urlMask).checked = status === 500
312
276
  this.className = className(this.value === this.options[0].value, status)
313
277
  })
314
278
  .catch(onError)
315
279
  }
316
280
 
317
- function className(defaultIsSelected, status) {
318
- return cssClass(
319
- CSS.MockSelector,
320
- !defaultIsSelected && CSS.bold,
321
- status >= 400 && status < 500 && CSS.status4xx)
322
- }
323
-
324
281
  const selected = broker.currentMock.file
325
282
  const { status, urlMask } = parseFilename(selected)
326
283
  const files = broker.mocks.filter(item =>
@@ -329,17 +286,16 @@ function MockSelector({ broker }) {
329
286
 
330
287
  return (
331
288
  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))))
289
+ 'data-qaid': urlMask,
290
+ autocomplete: 'off',
291
+ className: className(selected === files[0], status),
292
+ disabled: files.length <= 1,
293
+ onChange
294
+ }, files.map(file =>
295
+ r('option', {
296
+ value: file,
297
+ selected: file === selected
298
+ }, file))))
343
299
  }
344
300
 
345
301
 
@@ -361,22 +317,23 @@ function DelayRouteToggler({ broker }) {
361
317
  onChange
362
318
  }),
363
319
  TimerIcon()))
364
- }
365
-
366
320
 
367
- function TimerIcon() {
368
- return (
369
- r('svg', { viewBox: '0 0 24 24' },
370
- 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
+ }
371
326
  }
372
327
 
373
328
 
374
329
  function InternalServerErrorToggler({ broker }) {
375
330
  function onChange(event) {
331
+ const { urlMask, method } = parseFilename(broker.mocks[0])
376
332
  mockaton.select(event.currentTarget.checked
377
333
  ? broker.mocks.find(f => parseFilename(f).status === 500)
378
334
  : broker.mocks[0])
379
335
  .then(init)
336
+ .then(() => linkFor(method, urlMask)?.click())
380
337
  .catch(onError)
381
338
  }
382
339
  return (
@@ -395,6 +352,86 @@ function InternalServerErrorToggler({ broker }) {
395
352
  )
396
353
  }
397
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
+
403
+ function trFor(method, urlMask) {
404
+ return document.querySelector(`tr[data-method="${method}"][data-urlMask="${urlMask}"]`)
405
+ }
406
+ function linkFor(method, urlMask) {
407
+ return trFor(method, urlMask)?.querySelector(`a.${CSS.PreviewLink}`)
408
+ }
409
+ function checkbox500For(method, urlMask) {
410
+ return trFor(method, urlMask)?.querySelector(`.${CSS.InternalServerErrorToggler} > input`)
411
+ }
412
+ function mockSelectorFor(method, urlMask) {
413
+ return trFor(method, urlMask)?.querySelector(`select.${CSS.MockSelector}`)
414
+ }
415
+
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
+
398
435
 
399
436
  function onError(error) {
400
437
  if (error?.message === 'Failed to fetch')
@@ -403,7 +440,9 @@ function onError(error) {
403
440
  }
404
441
 
405
442
 
406
- /* === Utils === */
443
+
444
+ // Utils ============
445
+
407
446
  function cssClass(...args) {
408
447
  return args.filter(a => a).join(' ')
409
448
  }
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)