mockaton 8.9.0 → 8.10.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
@@ -30,7 +30,7 @@ which is handy for setting up tests (see **Commander API** below).
30
30
 
31
31
 
32
32
 
33
- ## No Need to Mock Everything
33
+ ## Fallback to your Backend
34
34
  Mockaton can fallback to your real backend on routes you don’t have mocks
35
35
  for. For that, type your backend address in the **Fallback Backend** field.
36
36
 
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.9.0",
5
+ "version": "8.10.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -34,6 +34,7 @@ export const apiGetRequests = new Map([
34
34
  [API.fallback, getProxyFallback],
35
35
  [API.arEvents, longPollAR_Events],
36
36
  [API.comments, listComments],
37
+ [API.globalDelay, getGlobalDelay],
37
38
  [API.collectProxied, getCollectProxied]
38
39
  ])
39
40
 
@@ -46,6 +47,7 @@ export const apiPatchRequests = new Map([
46
47
  [API.cookies, selectCookie],
47
48
  [API.fallback, updateProxyFallback],
48
49
  [API.bulkSelect, bulkUpdateBrokersByCommentTag],
50
+ [API.globalDelay, setGlobalDelay],
49
51
  [API.collectProxied, setCollectProxied]
50
52
  ])
51
53
 
@@ -65,6 +67,7 @@ function serveDashboardAsset(req, response) {
65
67
 
66
68
  function listCookies(_, response) { sendJSON(response, cookie.list()) }
67
69
  function listComments(_, response) { sendJSON(response, mockBrokersCollection.extractAllComments()) }
70
+ function getGlobalDelay(_, response) { sendJSON(response, config.delay) }
68
71
  function listMockBrokers(_, response) { sendJSON(response, mockBrokersCollection.getAll()) }
69
72
  function getProxyFallback(_, response) { sendJSON(response, config.proxyFallback) }
70
73
  function getIsCorsAllowed(_, response) { sendJSON(response, config.corsAllowed) }
@@ -78,6 +81,13 @@ function listStaticFiles(req, response) {
78
81
  }
79
82
 
80
83
  function longPollAR_Events(req, response) {
84
+ // e.g. tab was hidden while new mocks were added or removed
85
+ const clientIsOutOfSync = parseInt(req.headers[DF.lastReceived_nAR], 10) !== countAR_Events()
86
+ if (clientIsOutOfSync) {
87
+ sendJSON(response, countAR_Events())
88
+ return
89
+ }
90
+
81
91
  function onAddOrRemoveMock() {
82
92
  unsubscribeAR_EventListener(onAddOrRemoveMock)
83
93
  sendJSON(response, countAR_Events())
@@ -130,7 +140,7 @@ async function setRouteIsDelayed(req, response) {
130
140
  else if (typeof delayed !== 'boolean')
131
141
  sendUnprocessableContent(response, `Expected a boolean for "delayed"`) // TESTME
132
142
  else {
133
- broker.updateDelay(body[DF.delayed])
143
+ broker.updateDelayed(body[DF.delayed])
134
144
  sendOK(response)
135
145
  }
136
146
  }
@@ -181,3 +191,7 @@ async function setCorsAllowed(req, response) {
181
191
  sendOK(response)
182
192
  }
183
193
 
194
+ async function setGlobalDelay(req, response) { // TESTME
195
+ config.delay = parseInt(await parseJSON(req), 10)
196
+ sendOK(response)
197
+ }
@@ -5,6 +5,7 @@ export const API = {
5
5
  comments: MOUNT + '/comments',
6
6
  select: MOUNT + '/select',
7
7
  delay: MOUNT + '/delay',
8
+ globalDelay: MOUNT + '/global-delay',
8
9
  mocks: MOUNT + '/mocks',
9
10
  reset: MOUNT + '/reset',
10
11
  cookies: MOUNT + '/cookies',
@@ -20,7 +21,8 @@ export const DF = { // Dashboard Fields (XHR)
20
21
  routeMethod: 'route_method',
21
22
  routeUrlMask: 'route_url_mask',
22
23
  delayed: 'delayed',
23
- proxied: 'proxied'
24
+ proxied: 'proxied',
25
+ lastReceived_nAR: 'last_received_n_ar'
24
26
  }
25
27
 
26
28
  export const DEFAULT_500_COMMENT = '(Mockaton 500)'
package/src/Commander.js CHANGED
@@ -77,6 +77,13 @@ export class Commander {
77
77
  return this.#patch(API.cors, value)
78
78
  }
79
79
 
80
+ getGlobalDelay() {
81
+ return this.#get(API.globalDelay)
82
+ }
83
+ setGlobalDelay(delay) {
84
+ return this.#patch(API.globalDelay, delay)
85
+ }
86
+
80
87
  listStaticFiles() {
81
88
  return this.#get(API.static)
82
89
  }
@@ -85,9 +92,12 @@ export class Commander {
85
92
  return this.#patch(API.reset)
86
93
  }
87
94
 
88
- getAR_EventsCount() {
95
+ getAR_EventsCount(nAR_EventReceived) {
89
96
  return fetch(API.arEvents, {
90
- signal: AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
97
+ signal: AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000),
98
+ headers: {
99
+ [DF.lastReceived_nAR]: nAR_EventReceived
100
+ }
91
101
  })
92
102
  }
93
103
  }
package/src/Dashboard.css CHANGED
@@ -112,7 +112,7 @@ select {
112
112
  border-bottom: 1px solid rgba(127, 127, 127, 0.1);
113
113
  background: var(--colorHeaderBackground);
114
114
  box-shadow: var(--boxShadow1);
115
- gap: 8px;
115
+ gap: 10px;
116
116
 
117
117
  img {
118
118
  width: 130px;
@@ -129,16 +129,26 @@ select {
129
129
  color: var(--colorLabel);
130
130
  font-size: 11px;
131
131
  gap: 4px;
132
+
133
+ svg {
134
+ width: 14px;
135
+ height: 14px;
136
+ fill: var(--colorLabel);
137
+ opacity: 0.6;
138
+ }
132
139
  }
133
140
 
134
141
  input[type=url],
142
+ input[type=number],
135
143
  select {
136
144
  width: 100%;
137
145
  height: 28px;
138
146
  padding: 4px 8px;
139
147
  border-right: 3px solid transparent;
140
148
  margin-top: 4px;
149
+ color: var(--colorText);
141
150
  font-size: 11px;
151
+ box-shadow: var(--boxShadow1);
142
152
  background-color: var(--colorComboBoxHeaderBackground);
143
153
  border-radius: var(--radius);
144
154
  }
@@ -147,21 +157,26 @@ select {
147
157
  background: var(--colorHover);
148
158
  }
149
159
 
150
- &.FallbackBackend {
151
- position: relative;
152
- width: 210px;
160
+ &.GlobalDelayField {
161
+ width: 82px;
162
+
163
+ input[type=number] {
164
+ padding-right: 0;
165
+ }
153
166
 
154
167
  svg {
155
- width: 14px;
156
- height: 14px;
157
- fill: var(--colorLabel);
158
- opacity: 0.6;
168
+ border: 1px solid var(--colorLabel);
169
+ border-radius: 50%;
170
+ transform: scale(.85);
159
171
  }
172
+ }
173
+
174
+ &.FallbackBackend {
175
+ position: relative;
176
+ width: 210px;
160
177
 
161
178
  input[type=url] {
162
179
  padding: 0 6px;
163
- box-shadow: var(--boxShadow1);
164
- color: var(--colorText);
165
180
  }
166
181
 
167
182
  .SaveProxiedCheckbox {
@@ -279,8 +294,8 @@ select {
279
294
  .MockSelector {
280
295
  width: 260px;
281
296
  height: 30px;
282
- border: 0;
283
297
  padding-right: 5px;
298
+ border: 0;
284
299
  text-align: right;
285
300
  direction: rtl;
286
301
  text-overflow: ellipsis;
@@ -333,7 +348,7 @@ select {
333
348
  }
334
349
 
335
350
  &:disabled ~ svg {
336
- opacity: .5;
351
+ fill-opacity: 0.4;
337
352
  cursor: not-allowed;
338
353
  box-shadow: none;
339
354
  }
@@ -350,10 +365,9 @@ select {
350
365
  }
351
366
 
352
367
  .ProxyToggler {
353
- margin-left: 4px;
354
368
  > svg {
355
- width: 22px;
356
- padding: 1px;
369
+ width: 20px;
370
+ padding: 3px;
357
371
  border-color: transparent;
358
372
  border-radius: var(--radiusSmall);
359
373
  }
package/src/Dashboard.js CHANGED
@@ -17,6 +17,7 @@ const Strings = {
17
17
  cookie: 'Cookie',
18
18
  cookie_disabled_title: 'No cookies specified in config.cookies',
19
19
  delay: 'Delay',
20
+ delay_ms: 'Delay (ms)',
20
21
  empty_response_body: '/* Empty Response Body */',
21
22
  fallback_server: 'Fallback Backend',
22
23
  fallback_server_placeholder: 'Type Server Address',
@@ -46,6 +47,7 @@ const CSS = {
46
47
  ProgressBar: 'ProgressBar',
47
48
  ProxyToggler: 'ProxyToggler',
48
49
  ResetButton: 'ResetButton',
50
+ GlobalDelayField: 'GlobalDelayField',
49
51
  SaveProxiedCheckbox: 'SaveProxiedCheckbox',
50
52
  StaticFilesList: 'StaticFilesList',
51
53
 
@@ -58,6 +60,10 @@ const CSS = {
58
60
  const r = createElement
59
61
  const mockaton = new Commander(window.location.origin)
60
62
 
63
+ const PROGRESS_BAR_DELAY = 180
64
+ let globalDelay = 1200
65
+
66
+
61
67
  init()
62
68
  pollAR_Events() // Add or Remove Mocks from File System
63
69
  document.addEventListener('visibilitychange', () => {
@@ -70,6 +76,7 @@ function init() {
70
76
  mockaton.listMocks(),
71
77
  mockaton.listCookies(),
72
78
  mockaton.listComments(),
79
+ mockaton.getGlobalDelay(),
73
80
  mockaton.getCollectProxied(),
74
81
  mockaton.getProxyFallback(),
75
82
  mockaton.listStaticFiles()
@@ -78,22 +85,24 @@ function init() {
78
85
  .catch(onError)
79
86
  }
80
87
 
81
- function App([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
88
+ function App([brokersByMethod, cookies, comments, delay, collectProxied, fallbackAddress, staticFiles]) {
89
+ globalDelay = delay
82
90
  return (
83
91
  r('div', null,
84
- r(Header, { cookies, comments, fallbackAddress, collectProxied }),
92
+ r(Header, { cookies, comments, delay, fallbackAddress, collectProxied }),
85
93
  r(MockList, { brokersByMethod, canProxy: Boolean(fallbackAddress) }),
86
94
  r(StaticFilesList, { staticFiles })))
87
95
  }
88
96
 
89
97
  // Header ===============
90
98
 
91
- function Header({ cookies, comments, fallbackAddress, collectProxied }) {
99
+ function Header({ cookies, comments, delay, fallbackAddress, collectProxied }) {
92
100
  return (
93
101
  r('menu', { className: CSS.Header },
94
102
  r(Logo),
95
103
  r(CookieSelector, { cookies }),
96
104
  r(BulkSelector, { comments }),
105
+ r(GlobalDelayField, { delay }),
97
106
  r(ProxyFallbackField, { fallbackAddress, collectProxied }),
98
107
  r(ResetButton)))
99
108
  }
@@ -153,6 +162,24 @@ function BulkSelector({ comments }) {
153
162
  r('option', { value }, value)))))
154
163
  }
155
164
 
165
+ function GlobalDelayField({ delay }) {
166
+ function onChange() {
167
+ globalDelay = this.valueAsNumber
168
+ mockaton.setGlobalDelay(globalDelay).catch(onError)
169
+ }
170
+ return (
171
+ r('label', { className: cssClass(CSS.Field, CSS.GlobalDelayField) },
172
+ r('span', null, r(TimerIcon), Strings.delay_ms),
173
+ r('input', {
174
+ type: 'number',
175
+ min: 0,
176
+ step: 100,
177
+ autocomplete: 'none',
178
+ value: delay,
179
+ onChange
180
+ })))
181
+ }
182
+
156
183
  function ProxyFallbackField({ fallbackAddress, collectProxied }) {
157
184
  function onChange() {
158
185
  const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
@@ -238,8 +265,8 @@ function SectionByMethod({ method, brokers, canProxy }) {
238
265
  r('tr', { 'data-method': method, 'data-urlMask': urlMask },
239
266
  r('td', null, r(PreviewLink, { method, urlMask })),
240
267
  r('td', null, r(MockSelector, { broker })),
241
- r('td', null, r(DelayRouteToggler, { broker })),
242
268
  r('td', null, r(InternalServerErrorToggler, { broker })),
269
+ r('td', null, r(DelayRouteToggler, { broker })),
243
270
  r('td', null, r(ProxyToggler, { broker, disabled: !canProxy }))))))
244
271
  }
245
272
 
@@ -313,7 +340,7 @@ function DelayRouteToggler({ broker }) {
313
340
  },
314
341
  r('input', {
315
342
  type: 'checkbox',
316
- checked: Boolean(broker.currentMock.delay),
343
+ checked: broker.currentMock.delayed,
317
344
  onChange
318
345
  }),
319
346
  TimerIcon()))
@@ -386,7 +413,7 @@ function PayloadViewer() {
386
413
  function PayloadViewerProgressBar() {
387
414
  return (
388
415
  r('div', { className: CSS.ProgressBar },
389
- r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
416
+ r('div', { style: { animationDuration: globalDelay - PROGRESS_BAR_DELAY + 'ms' } })))
390
417
  }
391
418
 
392
419
  function PayloadViewerTitle({ file, status, statusText }) {
@@ -406,7 +433,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText }) {
406
433
  }
407
434
 
408
435
  async function previewMock(method, urlMask, href) {
409
- const timer = setTimeout(renderProgressBar, 180)
436
+ const timer = setTimeout(renderProgressBar, PROGRESS_BAR_DELAY)
410
437
  const response = await fetch(href, { method })
411
438
  clearTimeout(timer)
412
439
  await updatePayloadViewer(method, urlMask, response)
@@ -512,7 +539,7 @@ async function pollAR_Events() {
512
539
  return
513
540
  try {
514
541
  pollAR_Events.isPolling = true
515
- const response = await mockaton.getAR_EventsCount()
542
+ const response = await mockaton.getAR_EventsCount(pollAR_Events.oldAR_EventsCount)
516
543
  if (response.ok) {
517
544
  const nAR_Events = await response.json()
518
545
  if (pollAR_Events.oldAR_EventsCount !== nAR_Events) { // because it could be < or >
package/src/MockBroker.js CHANGED
@@ -1,4 +1,3 @@
1
- import { config } from './config.js'
2
1
  import { includesComment, extractComments, parseFilename } from './Filename.js'
3
2
  import { DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
4
3
 
@@ -13,7 +12,7 @@ export class MockBroker {
13
12
  this.mocks = []
14
13
  this.currentMock = {
15
14
  file: '',
16
- delay: 0
15
+ delayed: false
17
16
  }
18
17
  this.register(file)
19
18
  }
@@ -59,7 +58,7 @@ export class MockBroker {
59
58
  }
60
59
 
61
60
  get file() { return this.currentMock.file }
62
- get delay() { return this.currentMock.delay }
61
+ get delayed() { return this.currentMock.delayed }
63
62
  get proxied() { return !this.currentMock.file }
64
63
  get status() { return parseFilename(this.file).status }
65
64
  get temp500IsSelected() { return this.#isTemp500(this.file) }
@@ -83,7 +82,7 @@ export class MockBroker {
83
82
 
84
83
  hasMock(file) { return this.mocks.includes(file) }
85
84
  updateFile(filename) { this.currentMock.file = filename }
86
- updateDelay(delayed) { this.currentMock.delay = Number(delayed) * config.delay }
85
+ updateDelayed(delayed) { this.currentMock.delayed = delayed }
87
86
 
88
87
  updateProxied(proxied) {
89
88
  if (proxied)
@@ -14,7 +14,7 @@ export async function dispatchMock(req, response) {
14
14
  const broker = mockBrokerCollection.getBrokerForUrl(req.method, req.url)
15
15
  if (!broker || broker.proxied) {
16
16
  if (config.proxyFallback)
17
- await proxy(req, response)
17
+ await proxy(req, response, Number(Boolean(broker?.delayed)) * config.delay)
18
18
  else
19
19
  sendNotFound(response)
20
20
  return
@@ -34,7 +34,7 @@ export async function dispatchMock(req, response) {
34
34
  : await applyPlugins(join(config.mocksDir, broker.file), req, response)
35
35
 
36
36
  response.setHeader('Content-Type', mime)
37
- setTimeout(() => response.end(body), broker.delay)
37
+ setTimeout(() => response.end(body), Number(broker.delayed) * config.delay)
38
38
  }
39
39
  catch (error) {
40
40
  if (error instanceof BodyReaderError)
@@ -352,7 +352,7 @@ async function testRegistering() {
352
352
  ])
353
353
  deepEqual(currentMock, {
354
354
  file: fixtureForRegisteringPutA[1],
355
- delay: 0
355
+ delayed: false
356
356
  })
357
357
  })
358
358
  await it('unregisters selected', async () => {
@@ -367,7 +367,7 @@ async function testRegistering() {
367
367
  ])
368
368
  deepEqual(currentMock, {
369
369
  file: fixtureForRegisteringPutB[1],
370
- delay: 0
370
+ delayed: false
371
371
  })
372
372
  })
373
373
  await it('unregistering the last mock removes broker', async () => {
package/src/ProxyRelay.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { join } from 'node:path'
2
- import { existsSync } from 'node:fs'
3
2
  import { randomUUID } from 'node:crypto'
4
- import { write } from './utils/fs.js'
3
+
5
4
  import { config } from './config.js'
6
5
  import { extFor } from './utils/mime.js'
7
6
  import { readBody } from './utils/http-request.js'
7
+ import { write, isFile } from './utils/fs.js'
8
8
  import { makeMockFilename } from './Filename.js'
9
9
 
10
10
 
11
- export async function proxy(req, response) {
11
+ export async function proxy(req, response, delay) {
12
12
  const proxyResponse = await fetch(config.proxyFallback + req.url, {
13
13
  method: req.method,
14
14
  headers: req.headers,
@@ -21,12 +21,12 @@ export async function proxy(req, response) {
21
21
  headers['set-cookie'] = proxyResponse.headers.getSetCookie() // parses multiple into an array
22
22
  response.writeHead(proxyResponse.status, headers)
23
23
  const body = await proxyResponse.text()
24
- response.end(body)
24
+ setTimeout(() => response.end(body), delay) // TESTME
25
25
 
26
26
  if (config.collectProxied) {
27
27
  const ext = extFor(proxyResponse.headers.get('content-type'))
28
28
  let filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
29
- if (existsSync(join(config.mocksDir, filename))) // TESTME
29
+ if (isFile(join(config.mocksDir, filename))) // TESTME
30
30
  filename = makeMockFilename(req.url + `(${randomUUID()})`, req.method, proxyResponse.status, ext)
31
31
  write(join(config.mocksDir, filename), body)
32
32
  }