mockaton 10.6.8 → 10.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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Server, IncomingMessage, OutgoingMessage } from 'node:http';
1
+ import { Server, IncomingMessage, OutgoingMessage } from 'node:http'
2
2
 
3
3
  type Plugin = (
4
4
  filePath: string,
@@ -16,7 +16,7 @@ interface Config {
16
16
 
17
17
  host?: string,
18
18
  port?: number
19
-
19
+
20
20
  logLevel?: 'normal' | 'verbose' | 'quiet'
21
21
 
22
22
  delay?: number
@@ -37,8 +37,8 @@ interface Config {
37
37
  corsExposedHeaders?: string[]
38
38
  corsCredentials?: boolean
39
39
  corsMaxAge?: number
40
-
41
-
40
+
41
+
42
42
  plugins?: [filenameTester: RegExp, plugin: Plugin][]
43
43
 
44
44
  onReady?: (address: string) => void
@@ -64,11 +64,11 @@ export type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
64
64
 
65
65
  export type ClientMockBroker = {
66
66
  mocks: string[]
67
- currentMock: {
68
- file: string
69
- delayed: boolean
70
- proxied: boolean
71
- }
67
+ file: string
68
+ status: number
69
+ auto500: boolean
70
+ delayed: boolean
71
+ proxied: boolean
72
72
  }
73
73
  export type ClientBrokersByMethod = {
74
74
  [method: string]: {
@@ -89,14 +89,14 @@ export type ClientStaticBrokers = {
89
89
  export interface State {
90
90
  brokersByMethod: ClientBrokersByMethod
91
91
  staticBrokers: ClientStaticBrokers
92
-
93
- cookies: [label:string, selected:boolean][]
92
+
93
+ cookies: [label: string, selected: boolean][]
94
94
  comments: string[]
95
-
95
+
96
96
  delay: number
97
-
97
+
98
98
  collectProxied: boolean
99
99
  proxyFallback: string
100
-
100
+
101
101
  corsAllowed?: boolean
102
102
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "10.6.8",
5
+ "version": "10.7.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "start": "node src/cli.js",
24
24
  "watch": "node --watch src/cli.js",
25
25
  "pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none 'pixaton-tests/**/*.test.js'",
26
- "outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %s ;# %s\\n\", $4, $2 }'"
26
+ "outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
27
27
  },
28
28
  "devDependencies": {
29
29
  "pixaton": "1.1.3",
package/src/Api.js CHANGED
@@ -21,7 +21,7 @@ export const apiGetRequests = new Map([
21
21
  ...[
22
22
  'Dashboard.css',
23
23
  'Dashboard.js',
24
- 'ApiConstants.js', 'ApiCommander.js', 'Filename.js', 'DashboardStore.js', 'DashboardDom.js',
24
+ 'ApiCommander.js', 'ApiConstants.js', 'DashboardDom.js', 'DashboardStore.js', 'Filename.js',
25
25
  'Logo.svg'
26
26
  ].map(f => [API.dashboard + '/' + f, serveStatic(f)]),
27
27
 
@@ -117,7 +117,7 @@ async function selectMock(req, response) {
117
117
  sendUnprocessableContent(response, `Missing Mock: ${file}`)
118
118
  else {
119
119
  broker.selectFile(file)
120
- sendJSON(response, broker.currentMock)
120
+ sendJSON(response, broker)
121
121
  }
122
122
  }
123
123
 
@@ -130,7 +130,7 @@ async function toggle500(req, response) {
130
130
  sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
131
131
  else {
132
132
  broker.toggle500()
133
- sendJSON(response, broker.currentMock)
133
+ sendJSON(response, broker)
134
134
  }
135
135
  }
136
136
 
@@ -147,7 +147,7 @@ async function setRouteIsDelayed(req, response) {
147
147
  sendUnprocessableContent(response, `Expected boolean for "delayed"`)
148
148
  else {
149
149
  broker.setDelayed(delayed)
150
- sendOK(response)
150
+ sendJSON(response, broker)
151
151
  }
152
152
  }
153
153
 
@@ -166,7 +166,7 @@ async function setRouteIsProxied(req, response) {
166
166
  sendUnprocessableContent(response, `There’s no proxy fallback`)
167
167
  else {
168
168
  broker.setProxied(proxied)
169
- sendOK(response)
169
+ sendJSON(response, broker)
170
170
  }
171
171
  }
172
172
 
@@ -231,7 +231,6 @@ async function setStaticRouteStatusCode(req, response) {
231
231
  }
232
232
  }
233
233
 
234
-
235
234
  async function setStaticRouteIsDelayed(req, response) {
236
235
  const body = await parseJSON(req)
237
236
  const delayed = body[DF.delayed]
@@ -9,27 +9,27 @@ export class Commander {
9
9
  this.#addr = addr
10
10
  }
11
11
 
12
- #patch = (api, body) => {
13
- return fetch(this.#addr + api, {
12
+ #patch = (api, body) =>
13
+ fetch(this.#addr + api, {
14
14
  method: 'PATCH',
15
15
  body: JSON.stringify(body)
16
16
  })
17
- }
18
17
 
19
18
  /** @returns {JsonPromise<State>} */
20
- getState = () => {
21
- return fetch(this.#addr + API.state)
22
- }
19
+ getState = () =>
20
+ fetch(this.#addr + API.state)
23
21
 
24
22
  /** @returns {JsonPromise<number>} */
25
- getSyncVersion = (currentSyncVersion, abortSignal) => {
26
- return fetch(this.#addr + API.syncVersion, {
27
- signal: AbortSignal.any([abortSignal, AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)]),
23
+ getSyncVersion = (currSyncVer, abortSignal) =>
24
+ fetch(this.#addr + API.syncVersion, {
25
+ signal: AbortSignal.any([
26
+ abortSignal,
27
+ AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
28
+ ]),
28
29
  headers: {
29
- [DF.syncVersion]: currentSyncVersion
30
+ [DF.syncVersion]: currSyncVer
30
31
  }
31
32
  })
32
- }
33
33
 
34
34
 
35
35
  reset() {
@@ -28,12 +28,7 @@ export const DF = { // Dashboard Fields (XHR)
28
28
  syncVersion: 'last_received_sync_version'
29
29
  }
30
30
 
31
- // TODO @ThinkAbout these affecting partial matches when bulk-selecting
32
- // e.g. 'ton' would match
33
- export const AUTO_500_COMMENT = '(Mockaton 500)'
31
+ export const HEADER_FOR_502 = 'Mockaton502'
34
32
  export const DEFAULT_MOCK_COMMENT = '(default)'
35
-
36
33
  export const EXT_FOR_UNKNOWN_MIME = 'unknown'
37
34
  export const LONG_POLL_SERVER_TIMEOUT = 8_000
38
-
39
- export const HEADER_FOR_502 = 'Mockaton502'
package/src/Dashboard.css CHANGED
@@ -369,6 +369,19 @@ table {
369
369
  > tr:first-child > th {
370
370
  border-top: 0;
371
371
  }
372
+
373
+ tr.animIn {
374
+ opacity: 0;
375
+ transform: scaleY(0);
376
+ animation: _kfAnimIn 180ms ease-in-out forwards;
377
+ }
378
+ }
379
+
380
+ @keyframes _kfAnimIn {
381
+ to {
382
+ opacity: 1;
383
+ transform: scaleY(1);
384
+ }
372
385
  }
373
386
 
374
387
  .Method {
@@ -544,11 +557,22 @@ table {
544
557
  }
545
558
  }
546
559
 
560
+ &:disabled + span {
561
+ cursor: not-allowed;
562
+ opacity: 0.7;
563
+ }
547
564
  &:checked + span {
548
565
  border-color: var(--colorRed);
549
566
  color: white;
550
567
  background: var(--colorRed);
551
568
  }
569
+ &:not(:checked):enabled:hover + span {
570
+ border-color: var(--colorRed);
571
+ color: var(--colorRed);
572
+ }
573
+ &:enabled:active + span {
574
+ cursor: grabbing;
575
+ }
552
576
  }
553
577
 
554
578
  > span {
@@ -558,14 +582,6 @@ table {
558
582
  font-weight: bold;
559
583
  color: var(--colorSecondaryAction);
560
584
  border-radius: var(--radius);
561
-
562
- &:hover {
563
- border-color: var(--colorRed);
564
- color: var(--colorRed);
565
- }
566
- &:active {
567
- cursor: grabbing;
568
- }
569
585
  }
570
586
  }
571
587
 
package/src/Dashboard.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createElement as r, createSvgElement as s, className, restoreFocus, Defer, Fragment, useRef } from './DashboardDom.js'
2
- import { AUTO_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
3
- import { store, dittoSplitPaths, BrokerRowModel } from './DashboardStore.js'
2
+ import { store, BrokerRowModel } from './DashboardStore.js'
3
+ import { HEADER_FOR_502 } from './ApiConstants.js'
4
4
  import { parseFilename } from './Filename.js'
5
5
 
6
6
 
@@ -28,6 +28,7 @@ const CSS = {
28
28
  SaveProxiedCheckbox: null,
29
29
  SettingsMenu: null,
30
30
 
31
+ animIn: null,
31
32
  chosen: null,
32
33
  dittoDir: null,
33
34
  leftSide: null,
@@ -63,10 +64,12 @@ initRealTimeUpdates()
63
64
  initKeyboardNavigation()
64
65
 
65
66
  function render() {
67
+ render.count++
66
68
  restoreFocus(() => document.body.replaceChildren(...App()))
67
69
  if (store.hasChosenLink)
68
70
  previewMock()
69
71
  }
72
+ render.count = 0
70
73
 
71
74
  const t = translation => translation[0]
72
75
 
@@ -156,9 +159,7 @@ function BulkSelector() {
156
159
  },
157
160
  r('option', { value: firstOption }, firstOption),
158
161
  r('hr'),
159
- comments.map(value => r('option', { value }, value)),
160
- r('hr'),
161
- r('option', { value: AUTO_500_COMMENT }, t`Auto500`)
162
+ comments.map(value => r('option', { value }, value))
162
163
  )))
163
164
  }
164
165
 
@@ -278,7 +279,7 @@ function MockList() {
278
279
  return Object.keys(store.brokersByMethod).map(method => Fragment(
279
280
  r('tr', null,
280
281
  r('th', { colspan: 2 + Number(store.canProxy) }),
281
- r('th', null, method)),
282
+ r('th', { colspan: 2 }, method)),
282
283
  store.brokersAsRowsByMethod(method).map(Row)))
283
284
 
284
285
  return store.brokersAsRowsByMethod('*').map(Row)
@@ -289,9 +290,12 @@ function MockList() {
289
290
  * @param {number} i
290
291
  */
291
292
  function Row(row, i) {
292
- const { method, urlMask } = row
293
+ const { key, method, urlMask } = row
293
294
  return (
294
- r('tr', { key: Row.key(method, urlMask) },
295
+ r('tr', {
296
+ key,
297
+ ...className(render.count > 1 && row.isNew && CSS.animIn)
298
+ },
295
299
  store.canProxy && r('td', null,
296
300
  ProxyToggler(method, urlMask, row.proxied)),
297
301
 
@@ -299,7 +303,12 @@ function Row(row, i) {
299
303
  DelayRouteToggler(method, urlMask, row.delayed)),
300
304
 
301
305
  r('td', null,
302
- InternalServerErrorToggler(method, urlMask, !row.proxied && row.selectedFileIs500)),
306
+ InternalServerErrorToggler(
307
+ method,
308
+ urlMask,
309
+ !row.proxied && row.status === 500, // checked
310
+ row.opts.length === 1 && row.status === 500 // disabled
311
+ )),
303
312
 
304
313
  !store.groupByMethod && r('td', className(CSS.Method),
305
314
  method),
@@ -310,13 +319,12 @@ function Row(row, i) {
310
319
  r('td', null,
311
320
  MockSelector(row))))
312
321
  }
313
- Row.key = (method, urlMask) => method + '::' + urlMask
314
322
 
315
323
  function renderRow(method, urlMask) {
316
324
  restoreFocus(() => {
317
325
  unChooseOld()
318
- trFor(Row.key(method, urlMask))
319
- .replaceWith(Row(store.brokerAsRow(method, urlMask)))
326
+ const row = store.brokerAsRow(method, urlMask)
327
+ trFor(row.key).replaceWith(Row(row))
320
328
  previewMock()
321
329
  })
322
330
 
@@ -375,7 +383,7 @@ function DelayRouteToggler(method, urlMask, checked) {
375
383
  })
376
384
  }
377
385
 
378
- function InternalServerErrorToggler(method, urlMask, checked) {
386
+ function InternalServerErrorToggler(method, urlMask, checked, disabled) {
379
387
  return (
380
388
  r('label', {
381
389
  className: CSS.InternalServerErrorToggler,
@@ -383,6 +391,7 @@ function InternalServerErrorToggler(method, urlMask, checked) {
383
391
  },
384
392
  r('input', {
385
393
  type: 'checkbox',
394
+ disabled,
386
395
  checked,
387
396
  onChange() { store.toggle500(method, urlMask) },
388
397
  'data-focus-group': FocusGroup.StatusToggler
@@ -410,38 +419,45 @@ function ProxyToggler(method, urlMask, checked) {
410
419
  /** # StaticFilesList */
411
420
 
412
421
  function StaticFilesList() {
413
- const { staticBrokers, canProxy, groupByMethod } = store
414
- if (!Object.keys(staticBrokers).length)
415
- return null
416
-
417
- const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
418
- ? [r('span', className(CSS.dittoDir), ditto), tail]
419
- : tail)
420
- return (
421
- Fragment(
422
+ const { canProxy, groupByMethod } = store
423
+ const rows = store.staticBrokersAsRows()
424
+ return !rows.length
425
+ ? null
426
+ : Fragment(
422
427
  r('tr', null,
423
428
  r('th', { colspan: (2 + Number(!groupByMethod)) + Number(canProxy) }),
424
- r('th', null, t`Static GET`)),
425
- Object.values(staticBrokers).map(({ route, status, delayed }, i) =>
426
- r('tr', null,
427
- canProxy && r('td'),
428
- r('td', null,
429
- DelayStaticRouteToggler(route, delayed)),
429
+ r('th', { colspan: 2 }, t`Static GET`)),
430
+ rows.map(StaticRow))
431
+ }
430
432
 
431
- r('td', null,
432
- NotFoundToggler(route, status === 404)),
433
+ /** @param {StaticBrokerRowModel} row */
434
+ function StaticRow(row) {
435
+ const { canProxy, groupByMethod } = store
436
+ const [ditto, tail] = row.urlMaskDittoed
437
+ return (
438
+ r('tr', {
439
+ key: row.key,
440
+ ...className(render.count > 1 && row.isNew && CSS.animIn)
441
+ },
442
+ canProxy && r('td'),
443
+ r('td', null,
444
+ DelayStaticRouteToggler(row.urlMask, row.delayed)),
433
445
 
434
- !groupByMethod && r('td', className(CSS.Method),
435
- 'GET'),
446
+ r('td', null,
447
+ NotFoundToggler(row.urlMask, row.status === 404)),
448
+
449
+ !groupByMethod && r('td', className(CSS.Method),
450
+ 'GET'),
436
451
 
437
- r('td', null,
438
- r('a', {
439
- href: route,
440
- target: '_blank',
441
- className: CSS.PreviewLink,
442
- 'data-focus-group': FocusGroup.PreviewLink
443
- }, dp[i]))
444
- ))))
452
+ r('td', null,
453
+ r('a', {
454
+ href: row.urlMask,
455
+ target: '_blank',
456
+ className: CSS.PreviewLink,
457
+ 'data-focus-group': FocusGroup.PreviewLink
458
+ }, ditto
459
+ ? [r('span', className(CSS.dittoDir), ditto), tail]
460
+ : tail))))
445
461
  }
446
462
 
447
463
  function DelayStaticRouteToggler(route, checked) {
@@ -610,7 +626,7 @@ async function previewMock() {
610
626
  signal: previewMock.controller.signal
611
627
  })
612
628
  clearTimeout(spinnerTimer)
613
- const { proxied, file } = store.brokerFor(method, urlMask).currentMock
629
+ const { proxied, file } = store.brokerFor(method, urlMask)
614
630
  if (proxied || file)
615
631
  await updatePayloadViewer(proxied, file, response)
616
632
  }
@@ -1,11 +1,10 @@
1
1
  import { deferred } from './DashboardDom.js'
2
2
  import { Commander } from './ApiCommander.js'
3
3
  import { parseFilename, extractComments } from './Filename.js'
4
- import { AUTO_500_COMMENT } from './ApiConstants.js'
5
4
 
6
5
 
7
6
  const t = translation => translation[0]
8
- const mockaton = new Commander(location.origin)
7
+ const api = new Commander(location.origin)
9
8
 
10
9
  export const store = {
11
10
  onError(err) {},
@@ -25,11 +24,11 @@ export const store = {
25
24
  return Boolean(store.proxyFallback)
26
25
  },
27
26
 
28
- getSyncVersion: mockaton.getSyncVersion,
27
+ getSyncVersion: api.getSyncVersion,
29
28
 
30
29
  async fetchState() {
31
30
  try {
32
- const response = await mockaton.getState()
31
+ const response = await api.getState()
33
32
  if (!response.ok) throw response
34
33
  Object.assign(store, await response.json())
35
34
  store.render()
@@ -59,11 +58,11 @@ export const store = {
59
58
  setChosenLink(method, urlMask) {
60
59
  store.chosenLink = { method, urlMask }
61
60
  },
62
-
61
+
63
62
 
64
63
  async reset() {
65
64
  try {
66
- const response = await mockaton.reset()
65
+ const response = await api.reset()
67
66
  if (!response.ok) throw response
68
67
  store.setChosenLink('', '')
69
68
  await store.fetchState()
@@ -73,17 +72,17 @@ export const store = {
73
72
 
74
73
  async bulkSelectByComment(value) {
75
74
  try {
76
- const response = await mockaton.bulkSelectByComment(value)
75
+ const response = await api.bulkSelectByComment(value)
77
76
  if (!response.ok) throw response
78
77
  await store.fetchState()
79
78
  }
80
79
  catch (error) { store.onError(error) }
81
80
  },
82
81
 
83
-
82
+
84
83
  async setGlobalDelay(value) {
85
84
  try {
86
- const response = await mockaton.setGlobalDelay(value)
85
+ const response = await api.setGlobalDelay(value)
87
86
  if (!response.ok) throw response
88
87
  store.delay = value
89
88
  }
@@ -92,7 +91,7 @@ export const store = {
92
91
 
93
92
  async selectCookie(name) {
94
93
  try {
95
- const response = await mockaton.selectCookie(name)
94
+ const response = await api.selectCookie(name)
96
95
  if (!response.ok) throw response
97
96
  store.cookies = await response.json()
98
97
  }
@@ -101,7 +100,7 @@ export const store = {
101
100
 
102
101
  async setProxyFallback(value) {
103
102
  try {
104
- const response = await mockaton.setProxyFallback(value)
103
+ const response = await api.setProxyFallback(value)
105
104
  if (!response.ok) throw response
106
105
  store.proxyFallback = value
107
106
  store.render()
@@ -111,7 +110,7 @@ export const store = {
111
110
 
112
111
  async setCollectProxied(checked) {
113
112
  try {
114
- const response = await mockaton.setCollectProxied(checked)
113
+ const response = await api.setCollectProxied(checked)
115
114
  if (!response.ok) throw response
116
115
  store.collectProxied = checked
117
116
  }
@@ -123,35 +122,54 @@ export const store = {
123
122
  return store.brokersByMethod[method]?.[urlMask]
124
123
  },
125
124
 
125
+ setBroker(broker) {
126
+ const { method, urlMask } = parseFilename(broker.file)
127
+ store.brokersByMethod[method] ??= {}
128
+ store.brokersByMethod[method][urlMask] = broker
129
+ },
130
+
126
131
  _dittoCache: new Map(),
127
132
 
133
+ _brokersAsArray(byMethod = '*') {
134
+ const arr = []
135
+ for (const [method, brokers] of Object.entries(store.brokersByMethod))
136
+ if (byMethod === '*' || byMethod === method)
137
+ arr.push(...Object.values(brokers))
138
+ return arr
139
+ },
140
+
128
141
  brokersAsRowsByMethod(method) {
129
142
  const rows = store._brokersAsArray(method)
130
143
  .map(b => new BrokerRowModel(b, store.canProxy))
131
144
  .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
132
145
  const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
133
146
  rows.forEach((r, i) => {
134
- store._dittoCache.set(r.method + '::' + r.urlMask, urlMasksDittoed[i])
135
147
  r.setUrlMaskDittoed(urlMasksDittoed[i])
148
+ r.setIsNew(!store._dittoCache.has(r.key))
149
+ store._dittoCache.set(r.key, urlMasksDittoed[i])
136
150
  })
137
151
  return rows
138
152
  },
139
153
 
140
154
  brokerAsRow(method, urlMask) {
141
- const r = new BrokerRowModel(store.brokerFor(method, urlMask), store.canProxy)
142
- r.setUrlMaskDittoed(store._dittoCache.get(r.method + '::' + r.urlMask))
155
+ const b = store.brokerFor(method, urlMask)
156
+ const r = new BrokerRowModel(b, store.canProxy)
157
+ r.setUrlMaskDittoed(store._dittoCache.get(r.key))
143
158
  return r
144
159
  },
145
-
146
- _brokersAsArray(byMethod = '*') {
147
- const arr = []
148
- for (const [method, brokers] of Object.entries(store.brokersByMethod))
149
- if (byMethod === '*' || byMethod === method)
150
- for (const broker of Object.values(brokers))
151
- arr.push(broker)
152
- return arr
153
- },
154
160
 
161
+ staticBrokersAsRows() {
162
+ const rows = Object.values(store.staticBrokers)
163
+ .map(b => new StaticBrokerRowModel(b))
164
+ .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
165
+ const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
166
+ rows.forEach((r, i) => {
167
+ r.setUrlMaskDittoed(urlMasksDittoed[i])
168
+ r.setIsNew(!store._dittoCache.has(r.key))
169
+ store._dittoCache.set(r.key, urlMasksDittoed[i])
170
+ })
171
+ return rows
172
+ },
155
173
 
156
174
  previewLink(method, urlMask) {
157
175
  store.setChosenLink(method, urlMask)
@@ -160,10 +178,10 @@ export const store = {
160
178
 
161
179
  async selectFile(file) {
162
180
  try {
163
- const response = await mockaton.select(file)
181
+ const response = await api.select(file)
164
182
  if (!response.ok) throw response
165
183
  const { method, urlMask } = parseFilename(file)
166
- store.brokerFor(method, urlMask).currentMock = await response.json()
184
+ store.setBroker(await response.json())
167
185
  store.setChosenLink(method, urlMask)
168
186
  store.renderRow(method, urlMask)
169
187
  }
@@ -172,9 +190,9 @@ export const store = {
172
190
 
173
191
  async toggle500(method, urlMask) {
174
192
  try {
175
- const response = await mockaton.toggle500(method, urlMask)
193
+ const response = await api.toggle500(method, urlMask)
176
194
  if (!response.ok) throw response
177
- store.brokerFor(method, urlMask).currentMock = await response.json()
195
+ store.setBroker(await response.json())
178
196
  store.setChosenLink(method, urlMask)
179
197
  store.renderRow(method, urlMask)
180
198
  }
@@ -183,9 +201,9 @@ export const store = {
183
201
 
184
202
  async setProxied(method, urlMask, checked) {
185
203
  try {
186
- const response = await mockaton.setRouteIsProxied(method, urlMask, checked)
204
+ const response = await api.setRouteIsProxied(method, urlMask, checked)
187
205
  if (!response.ok) throw response
188
- store.brokerFor(method, urlMask).currentMock.proxied = checked
206
+ store.setBroker(await response.json())
189
207
  store.setChosenLink(method, urlMask)
190
208
  store.renderRow(method, urlMask)
191
209
  }
@@ -194,9 +212,9 @@ export const store = {
194
212
 
195
213
  async setDelayed(method, urlMask, checked) {
196
214
  try {
197
- const response = await mockaton.setRouteIsDelayed(method, urlMask, checked)
215
+ const response = await api.setRouteIsDelayed(method, urlMask, checked)
198
216
  if (!response.ok) throw response
199
- store.brokerFor(method, urlMask).currentMock.delayed = checked
217
+ store.setBroker(await response.json())
200
218
  }
201
219
  catch (error) { store.onError(error) }
202
220
  },
@@ -204,7 +222,7 @@ export const store = {
204
222
 
205
223
  async setDelayedStatic(route, checked) {
206
224
  try {
207
- const response = await mockaton.setStaticRouteIsDelayed(route, checked)
225
+ const response = await api.setStaticRouteIsDelayed(route, checked)
208
226
  if (!response.ok) throw response
209
227
  store.staticBrokers[route].delayed = checked
210
228
  }
@@ -213,7 +231,7 @@ export const store = {
213
231
 
214
232
  async setStaticRouteStatus(route, status) {
215
233
  try {
216
- const response = await mockaton.setStaticRouteStatus(route, status)
234
+ const response = await api.setStaticRouteStatus(route, status)
217
235
  if (!response.ok) throw response
218
236
  store.staticBrokers[route].status = status
219
237
  }
@@ -309,9 +327,10 @@ dittoSplitPaths.test = function () {
309
327
  deferred(dittoSplitPaths.test)
310
328
 
311
329
 
312
-
313
330
  export class BrokerRowModel {
314
331
  opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
332
+ isNew = false
333
+ key = ''
315
334
  method = ''
316
335
  urlMask = ''
317
336
  urlMaskDittoed = ['', '']
@@ -325,7 +344,8 @@ export class BrokerRowModel {
325
344
  constructor(broker, canProxy) {
326
345
  this.#broker = broker
327
346
  this.#canProxy = canProxy
328
- const { method, urlMask } = parseFilename(broker.currentMock.file)
347
+ const { method, urlMask } = parseFilename(broker.file)
348
+ this.key = 'brm' + '::' + method + '::' + urlMask
329
349
  this.method = method
330
350
  this.urlMask = urlMask
331
351
  this.opts = this.#makeOptions()
@@ -334,41 +354,36 @@ export class BrokerRowModel {
334
354
  setUrlMaskDittoed(urlMaskDittoed) {
335
355
  this.urlMaskDittoed = urlMaskDittoed
336
356
  }
337
-
338
- get delayed() {
339
- return this.#broker.currentMock.delayed
340
- }
341
- get proxied() {
342
- return this.#canProxy && this.#broker.currentMock.proxied
357
+ setIsNew(isNew) {
358
+ this.isNew = isNew
343
359
  }
360
+
361
+ get status() { return this.#broker.status }
362
+ get auto500() { return this.#broker.auto500 }
363
+ get delayed() { return this.#broker.delayed }
364
+ get proxied() { return this.#canProxy && this.#broker.proxied }
365
+ get selectedFile() { return this.#broker.file }
344
366
  get selectedIdx() {
345
367
  return this.opts.findIndex(([, , selected]) => selected)
346
368
  }
347
- get selectedFile() {
348
- return this.#broker.currentMock.file
349
- }
350
369
  get selectedFileIs4xx() {
351
- const { status } = parseFilename(this.selectedFile)
352
- return status >= 400 && status < 500
353
- }
354
- get selectedFileIs500() {
355
- const { status } = parseFilename(this.selectedFile)
356
- return status === 500
370
+ return this.status >= 400 && this.status < 500
357
371
  }
358
372
 
359
373
  #makeOptions() {
360
- const proxied = this.proxied
361
- const selectedIs500 = this.selectedFileIs500
362
-
363
- const opts = this.#broker.mocks
364
- .filter(f => selectedIs500 || !f.includes(AUTO_500_COMMENT))
365
- .map(f => [
366
- f,
367
- this.#optionLabelFor(f),
368
- !proxied && f === this.selectedFile
369
- ])
374
+ const opts = this.#broker.mocks.map(f => [
375
+ f,
376
+ this.#optionLabelFor(f),
377
+ !this.auto500 && !this.proxied && f === this.selectedFile
378
+ ])
370
379
 
371
- if (proxied)
380
+ if (this.auto500)
381
+ opts.push([
382
+ '__AUTO_500__',
383
+ t`Auto500`,
384
+ true
385
+ ])
386
+ else if (this.proxied)
372
387
  opts.push([
373
388
  '__PROXIED__',
374
389
  t`Proxied`,
@@ -380,49 +395,32 @@ export class BrokerRowModel {
380
395
 
381
396
  #optionLabelFor(file) {
382
397
  const { status, ext } = parseFilename(file)
383
- const comments = extractComments(file)
384
- const isAutogen500 = comments.includes(AUTO_500_COMMENT)
385
398
  return [
386
- isAutogen500 ? '' : status,
399
+ status,
387
400
  ext === 'empty' || ext === 'unknown' ? '' : ext,
388
- isAutogen500 ? t`Auto500` : comments.join(' ')
401
+ extractComments(file).join(' ')
389
402
  ].filter(Boolean).join(' ')
390
403
  }
391
404
  }
392
405
 
393
- const TestBrokerRowModelOptions = {
394
- 'ignores autogen500 when unselected'() {
395
- const broker = {
396
- currentMock: { file: 'api/other' },
397
- mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
398
- }
399
- const row = new BrokerRowModel(broker, false)
400
- console.assert(row.opts.length === 0)
401
- },
402
-
403
- 'keeps non-autogen500 when unselected'() {
404
- const broker = {
405
- currentMock: { file: 'api/other' },
406
- mocks: [`api/user.GET.500.txt`]
407
- }
408
- const row = new BrokerRowModel(broker, false)
409
- console.assert(row.opts.length === 1)
410
- console.assert(row.opts[0][1] === t`500 txt`)
411
- },
412
-
413
- 'renames autogen file to Auto500'() {
406
+ const TestBrokerRowModel = {
407
+ 'has Auto500 when is autogenerated 500'() {
414
408
  const broker = {
415
- currentMock: { file: `api/user${AUTO_500_COMMENT}.GET.500.empty` },
416
- mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
409
+ auto500: true,
410
+ file: 'api/user.GET.200.json',
411
+ mocks: ['api/user.GET.200.json']
417
412
  }
418
413
  const row = new BrokerRowModel(broker, false)
419
- console.assert(row.opts.length === 1)
420
- console.assert(row.opts[0][1] === t`Auto500`)
414
+ const opts = row.opts.map(([, n, selected]) => [n, selected])
415
+ console.assert(deepEqual(opts, [
416
+ ['200 json', false],
417
+ [t`Auto500`, true],
418
+ ]))
421
419
  },
422
420
 
423
421
  'filename has extension except when empty or unknown'() {
424
422
  const broker = {
425
- currentMock: { file: `api/other` },
423
+ file: `api/user0.GET.200.empty`,
426
424
  mocks: [
427
425
  `api/user0.GET.200.empty`,
428
426
  `api/user1.GET.200.unknown`,
@@ -431,22 +429,21 @@ const TestBrokerRowModelOptions = {
431
429
  ]
432
430
  }
433
431
  const row = new BrokerRowModel(broker, false)
434
- // Think about, in cases like this, the only option the user has
435
- // for discerning empty and unknown is on the Previewer Title
436
- console.assert(deepEqual(row.opts.map(([, n]) => n), [
437
- '200',
438
- '200',
439
- '200 json',
440
- '200 json (another json)',
432
+ const opts = row.opts.map(([, n, selected]) => [n, selected])
433
+ console.assert(deepEqual(opts, [
434
+ ['200', true],
435
+ ['200', false],
436
+ ['200 json', false],
437
+ ['200 json (another json)', false]
441
438
  ]))
439
+ // TODO Think about, in cases like this, the only option the user has
440
+ // for discerning empty and unknown is on the Previewer Title
442
441
  },
443
442
 
444
443
  'appends "Proxied" label iff current is proxied'() {
445
444
  const broker = {
446
- currentMock: {
447
- file: 'api/foo',
448
- proxied: true
449
- },
445
+ file: 'api/foo',
446
+ proxied: true,
450
447
  mocks: [`api/foo.GET.200.json`]
451
448
  }
452
449
  const row = new BrokerRowModel(broker, true)
@@ -456,7 +453,31 @@ const TestBrokerRowModelOptions = {
456
453
  ]))
457
454
  }
458
455
  }
459
- deferred(() => Object.values(TestBrokerRowModelOptions).forEach(t => t()))
456
+ deferred(() => Object.values(TestBrokerRowModel).forEach(t => t()))
457
+
458
+
459
+ export class StaticBrokerRowModel {
460
+ isNew = false
461
+ key = ''
462
+ method = 'GET'
463
+ urlMaskDittoed = ['', '']
464
+ #broker = /** @type {ClientStaticBroker} */ {}
465
+
466
+ /** @param {ClientStaticBroker} broker */
467
+ constructor(broker) {
468
+ this.#broker = broker
469
+ this.key = 'sbrm' + '::' + this.method + '::' + broker.route
470
+ }
471
+ setUrlMaskDittoed(urlMaskDittoed) {
472
+ this.urlMaskDittoed = urlMaskDittoed
473
+ }
474
+ setIsNew(isNew) {
475
+ this.isNew = isNew
476
+ }
477
+ get urlMask() { return this.#broker.route }
478
+ get delayed() { return this.#broker.delayed }
479
+ get status() { return this.#broker.status }
480
+ }
460
481
 
461
482
  function deepEqual(a, b) {
462
483
  return JSON.stringify(a) === JSON.stringify(b)
package/src/Filename.js CHANGED
@@ -22,7 +22,6 @@ export const includesComment = (filename, search) =>
22
22
  extractComments(filename).some(comment => comment.includes(search))
23
23
 
24
24
 
25
- // TODO ThinkAbout 206 (reject, handle, or send in full?)
26
25
  export function validateFilename(file) {
27
26
  const tokens = file.replace(reComments, '').split('.')
28
27
  if (tokens.length < 4)
@@ -34,7 +33,8 @@ export function validateFilename(file) {
34
33
 
35
34
  if (!httpMethods.includes(method))
36
35
  return `Unrecognized HTTP Method: "${method}"`
37
- }
36
+ }
37
+ // TODO ThinkAbout 206 (reject, handle, or send in full?)
38
38
 
39
39
 
40
40
  export function parseFilename(file) {
@@ -54,12 +54,12 @@ function removeTrailingSlash(url = '') {
54
54
  .replace('/#', '#')
55
55
  }
56
56
 
57
- // TODO ThinkAbout allowing custom status codes
58
57
  function responseStatusIsValid(status) {
59
58
  return Number.isInteger(status)
60
59
  && status >= 100
61
60
  && status <= 599
62
61
  }
62
+ // TODO ThinkAbout allowing custom status codes
63
63
 
64
64
 
65
65
  export function makeMockFilename(url, method, status, ext) {
package/src/MockBroker.js CHANGED
@@ -1,99 +1,82 @@
1
1
  import { includesComment, extractComments, parseFilename } from './Filename.js'
2
- import { AUTO_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
2
+ import { DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
3
3
 
4
4
 
5
- /** MockBroker is a state for a particular route. It knows the available mock files
6
- * that can be served for the route, the currently selected file, and if it’s delayed. */
5
+ /**
6
+ * MockBroker is a state for a particular route. It knows the available mock
7
+ * files that can be served for the route, and the currently selected file, etc.
8
+ */
7
9
  export class MockBroker {
8
10
  constructor(file) {
11
+ this.file = '' // selected mock filename
12
+ this.mocks = [] // filenames
13
+ this.status = -1
14
+ this.delayed = false
15
+ this.proxied = false
16
+ this.auto500 = false
9
17
  this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
10
- this.mocks = []
11
- this.currentMock = {
12
- file: '',
13
- delayed: false,
14
- proxied: false
15
- }
16
18
  this.register(file)
17
19
  }
18
20
 
19
- get file() { return this.currentMock.file }
20
- get delayed() { return this.currentMock.delayed }
21
- get proxied() { return this.currentMock.proxied }
22
-
23
- get status() { return parseFilename(this.file).status }
24
- get temp500IsSelected() { return this.#isTemp500(this.file) }
25
-
26
- hasMock(file) { return this.mocks.includes(file) }
27
-
28
- register(file) {
29
- if (this.#is500(file)) {
30
- if (this.temp500IsSelected)
31
- this.selectFile(file)
32
- this.#deleteTemp500()
33
- }
34
- this.mocks.push(file)
35
- this.#sortMocks()
36
- }
37
-
38
- #is500(file) {
39
- return parseFilename(file).status === 500
40
- }
41
-
42
- #deleteTemp500() {
43
- this.mocks = this.mocks.filter(file => !this.#isTemp500(file))
44
- }
45
-
46
- #isTemp500(file) {
47
- return includesComment(file, AUTO_500_COMMENT)
48
- }
21
+ #is500 = file => parseFilename(file).status === 500
49
22
 
50
23
  #sortMocks() {
51
24
  this.mocks.sort()
52
25
  const defaults = this.mocks.filter(file => includesComment(file, DEFAULT_MOCK_COMMENT))
53
- const temp500 = this.mocks.filter(this.#isTemp500)
54
- this.mocks = [
55
- ...defaults,
56
- ...this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file)),
57
- ...temp500
58
- ]
26
+ this.mocks = Array.from(new Set(defaults).union(new Set(this.mocks)))
59
27
  }
60
28
 
61
- ensureItHas500() {
62
- if (!this.mocks.some(this.#is500))
63
- this.#registerTemp500()
64
- }
65
- #registerTemp500() {
66
- const { urlMask, method } = parseFilename(this.mocks[0])
67
- const file = urlMask.replace(/^\//, '') // Removes leading slash
68
- this.mocks.push(`${file}${AUTO_500_COMMENT}.${method}.500.empty`)
29
+ register(file) {
30
+ if (this.auto500 && this.#is500(file)) // TESTME
31
+ this.selectFile(file)
32
+ this.mocks.push(file)
33
+ this.#sortMocks()
69
34
  }
70
35
 
71
36
  unregister(file) {
72
37
  this.mocks = this.mocks.filter(f => f !== file)
73
38
  const isEmpty = !this.mocks.length
74
- || this.mocks.length === 1 && this.#isTemp500(this.mocks[0])
75
39
  if (!isEmpty && this.file === file)
76
40
  this.selectDefaultFile()
77
41
  return isEmpty
78
42
  }
79
43
 
44
+ hasMock = file => this.mocks.includes(file)
45
+
46
+ selectFile(filename) {
47
+ this.file = filename
48
+ this.proxied = false
49
+ this.auto500 = false
50
+ this.status = parseFilename(filename).status
51
+ }
52
+
80
53
  selectDefaultFile() {
81
54
  this.selectFile(this.mocks[0])
82
55
  }
83
56
 
84
- selectFile(filename) {
85
- this.currentMock.proxied = false
86
- this.currentMock.file = filename
57
+ toggle500() {
58
+ this.proxied = false // TESTME
59
+ if (this.auto500 || this.status === 500)
60
+ this.selectDefaultFile()
61
+ else {
62
+ const f = this.mocks.find(this.#is500) // TESTME
63
+ if (f)
64
+ this.selectFile(f)
65
+ else {
66
+ this.auto500 = true
67
+ this.status = 500 // TESTME
68
+ }
69
+ }
87
70
  }
88
71
 
89
- toggle500() {
90
- this.#is500(this.currentMock.file)
91
- ? this.selectDefaultFile()
92
- : this.selectFile(this.mocks.find(this.#is500))
72
+ setDelayed(delayed) {
73
+ this.delayed = delayed
93
74
  }
94
75
 
95
- setDelayed(delayed) { this.currentMock.delayed = delayed }
96
- setProxied(proxied) { this.currentMock.proxied = proxied }
76
+ setProxied(proxied) {
77
+ this.auto500 = false // TESTME
78
+ this.proxied = proxied
79
+ }
97
80
 
98
81
  setByMatchingComment(comment) {
99
82
  for (const file of this.mocks)
@@ -35,8 +35,8 @@ export async function dispatchMock(req, response) {
35
35
  for (let i = 0; i < config.extraHeaders.length; i += 2)
36
36
  response.setHeader(config.extraHeaders[i], config.extraHeaders[i + 1])
37
37
 
38
- response.statusCode = broker.status // TESTME plugins can change it
39
- const { mime, body } = broker.temp500IsSelected
38
+ response.statusCode = broker.auto500 ? 500 : broker.status // TESTME plugins can change it
39
+ const { mime, body } = broker.auto500
40
40
  ? { mime: '', body: '' }
41
41
  : await applyPlugins(join(config.mocksDir, broker.file), req, response)
42
42
 
package/src/Mockaton.js CHANGED
@@ -10,7 +10,7 @@ import * as mockBrokerCollection from './mockBrokersCollection.js'
10
10
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
11
11
  import { watchMocksDir, watchStaticDir } from './Watcher.js'
12
12
  import { apiPatchRequests, apiGetRequests } from './Api.js'
13
- import { BodyReaderError, isControlCharFree } from './utils/http-request.js'
13
+ import { BodyReaderError, hasControlChars } from './utils/http-request.js'
14
14
  import { sendNoContent, sendInternalServerError, sendUnprocessableContent, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
15
15
 
16
16
 
@@ -26,8 +26,7 @@ export function Mockaton(options) {
26
26
  const server = createServer(onRequest)
27
27
  server.on('error', reject)
28
28
  server.listen(config.port, config.host, () => {
29
- const { address, port } = server.address()
30
- const url = `http://${address}:${port}`
29
+ const url = `http://${server.address().address}:${server.address().port}`
31
30
  const dashboardUrl = url + API.dashboard
32
31
  logger.info('Listening', url)
33
32
  logger.info('Dashboard', dashboardUrl)
@@ -47,27 +46,30 @@ async function onRequest(req, response) {
47
46
  sendTooLongURI(response)
48
47
  return
49
48
  }
50
-
51
- if (!isControlCharFree(url)) {
52
- sendBadRequest(response)
49
+ if (hasControlChars(url)) {
50
+ sendBadRequest(response)
53
51
  return
54
52
  }
55
-
56
- try {
57
- const route = new URL(url, 'http://_').pathname
58
- const { method } = req
59
53
 
54
+ try {
60
55
  if (config.corsAllowed)
61
56
  setCorsHeaders(req, response, config)
62
57
 
58
+ const { method } = req
59
+ const { pathname } = new URL(url, 'http://_')
60
+
63
61
  if (isPreflight(req))
64
62
  sendNoContent(response)
65
- else if (method === 'PATCH' && apiPatchRequests.has(route))
66
- await apiPatchRequests.get(route)(req, response)
67
- else if (method === 'GET' && apiGetRequests.has(route))
68
- apiGetRequests.get(route)(req, response)
69
- else if (method === 'GET' && staticCollection.brokerByRoute(route))
63
+
64
+ else if (method === 'PATCH' && apiPatchRequests.has(pathname))
65
+ await apiPatchRequests.get(pathname)(req, response)
66
+
67
+ else if (method === 'GET' && apiGetRequests.has(pathname))
68
+ apiGetRequests.get(pathname)(req, response)
69
+
70
+ else if (method === 'GET' && staticCollection.brokerByRoute(pathname))
70
71
  await dispatchStatic(req, response)
72
+
71
73
  else
72
74
  await dispatchMock(req, response)
73
75
  }
package/src/Watcher.js CHANGED
@@ -8,20 +8,34 @@ import * as staticCollection from './staticCollection.js'
8
8
  import * as mockBrokerCollection from './mockBrokersCollection.js'
9
9
 
10
10
 
11
- /** # ARR = Add, Remove, or Rename Mock Event */
11
+ /**
12
+ * ARR = Add, Remove, or Rename Mock Event
13
+ *
14
+ * The emitter is debounced so it handles e.g. bulk deletes,
15
+ * and also renames, which are two events (delete + add).
16
+ */
12
17
  export const uiSyncVersion = new class extends EventEmitter {
13
18
  version = 0
14
19
 
15
- increment() {
20
+ increment = this.#debounce(() => {
16
21
  this.version++
17
22
  super.emit('ARR')
18
- }
23
+ })
24
+
19
25
  subscribe(listener) {
20
26
  this.once('ARR', listener)
21
27
  }
22
28
  unsubscribe(listener) {
23
29
  this.removeListener('ARR', listener)
24
30
  }
31
+
32
+ #debounce(fn) {
33
+ let timer
34
+ return () => {
35
+ clearTimeout(timer)
36
+ timer = setTimeout(fn, 80)
37
+ }
38
+ }
25
39
  }
26
40
 
27
41
  export function watchMocksDir() {
@@ -29,7 +43,7 @@ export function watchMocksDir() {
29
43
  watch(dir, { recursive: true, persistent: false }, (_, file) => {
30
44
  if (!file)
31
45
  return
32
-
46
+
33
47
  const path = join(dir, file)
34
48
 
35
49
  if (isDirectory(path)) {
@@ -56,7 +70,7 @@ export function watchStaticDir() {
56
70
  watch(dir, { recursive: true, persistent: false }, (_, file) => {
57
71
  if (!file)
58
72
  return
59
-
73
+
60
74
  const path = join(dir, file)
61
75
 
62
76
  if (isDirectory(path)) {
@@ -77,4 +91,3 @@ export function watchStaticDir() {
77
91
  }
78
92
 
79
93
  // TODO config changes
80
- // TODO think about throttling e.g. bulk deletes/remove files
@@ -6,7 +6,6 @@ import { MockBroker } from './MockBroker.js'
6
6
  import { listFilesRecursively } from './utils/fs.js'
7
7
  import { config, isFileAllowed } from './config.js'
8
8
  import { parseFilename, validateFilename } from './Filename.js'
9
- import { AUTO_500_COMMENT } from './ApiConstants.js'
10
9
 
11
10
 
12
11
  /**
@@ -36,10 +35,7 @@ export function init() {
36
35
  .sort()
37
36
  .forEach(f => registerMock(f))
38
37
 
39
- forEachBroker(broker => {
40
- broker.ensureItHas500()
41
- broker.selectDefaultFile()
42
- })
38
+ forEachBroker(b => b.selectDefaultFile())
43
39
  }
44
40
 
45
41
  /** @returns {boolean} registered */
@@ -57,12 +53,9 @@ export function registerMock(file, isFromWatcher = false) {
57
53
  else
58
54
  collection[method][urlMask].register(file)
59
55
 
60
- if (isFromWatcher && !this.file)
56
+ if (isFromWatcher && !collection[method][urlMask].file) // TESTME e.g. auto500 is selected and adding a new one
61
57
  collection[method][urlMask].selectDefaultFile()
62
58
 
63
- if (isFromWatcher)
64
- collection[method][urlMask].ensureItHas500()
65
-
66
59
  return true
67
60
  }
68
61
 
@@ -117,7 +110,6 @@ export function extractAllComments() {
117
110
  comments.add(c)
118
111
  })
119
112
  return Array.from(comments)
120
- .filter(c => c !== AUTO_500_COMMENT)
121
113
  }
122
114
 
123
115
  export function setMocksMatchingComment(comment) {
@@ -50,13 +50,13 @@ export function readBody(req, parser = a => a) {
50
50
  }
51
51
 
52
52
  export const reControlAndDelChars = /[\x00-\x1f\x7f]/
53
- export function isControlCharFree(url) {
53
+ export function hasControlChars(url) {
54
54
  try {
55
55
  const decoded = decode(url)
56
- return decoded && !reControlAndDelChars.test(decoded)
56
+ return !decoded || reControlAndDelChars.test(decoded)
57
57
  }
58
58
  catch {
59
- return false
59
+ return true
60
60
  }
61
61
  }
62
62