mockaton 13.2.1 → 13.3.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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.2.1",
5
+ "version": "13.3.0",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -42,7 +42,7 @@ export class Commander {
42
42
  /** @returns {JsonPromise<ClientMockBroker>} */
43
43
  toggleStatus = (method, urlMask, status) => this.#patch(API.toggleStatus, [method, urlMask, status])
44
44
 
45
-
45
+
46
46
  /** @returns {JsonPromise<ClientMockBroker>} */
47
47
  setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
48
48
 
@@ -132,7 +132,7 @@ async function updatePayloadViewer(proxied, file, response) {
132
132
  else if (['text/xml', 'application/xml'].includes(mime))
133
133
  codeRef.elem.replaceChildren(SyntaxXML(await bodyAsText()))
134
134
 
135
- else if (mime.startsWith('text/'))
135
+ else if (mime.startsWith('text/') || mime === 'application/yaml')
136
136
  codeRef.elem.textContent = await bodyAsText()
137
137
 
138
138
  else
@@ -33,6 +33,16 @@ export const store = {
33
33
  store.render()
34
34
  },
35
35
 
36
+ collapsedFolders: new Set(JSON.parse(globalThis.localStorage?.getItem('collapsedFolders') || '[]')),
37
+ setFolderCollapsed(folder, collapsed) {
38
+ if (collapsed)
39
+ store.collapsedFolders.add(folder)
40
+ else
41
+ store.collapsedFolders.delete(folder)
42
+
43
+ globalThis.localStorage?.setItem('collapsedFolders', JSON.stringify([...store.collapsedFolders]))
44
+ },
45
+
36
46
  chosenLink: { method: '', urlMask: '' },
37
47
  setChosenLink(method, urlMask) {
38
48
  store.chosenLink = { method, urlMask }
@@ -112,27 +122,40 @@ export const store = {
112
122
  },
113
123
 
114
124
 
125
+ _dittoCache: new Map(),
126
+
115
127
  brokerFor(method, urlMask) {
116
128
  return store.brokersByMethod[method]?.[urlMask]
117
129
  },
118
130
 
131
+ brokerAsRow(method, urlMask) {
132
+ const b = store.brokerFor(method, urlMask)
133
+ const r = new BrokerRowModel(b, store.canProxy)
134
+ r.setUrlMaskDittoed(store._dittoCache.get(r.key))
135
+ return r
136
+ },
137
+
119
138
  _setBroker(broker) {
120
139
  const { method, urlMask } = parseFilename(broker.file)
121
140
  store.brokersByMethod[method] ??= {}
122
141
  store.brokersByMethod[method][urlMask] = broker
123
142
  },
124
143
 
125
- _dittoCache: new Map(),
126
-
127
- _brokersAsArray(byMethod = '*') {
128
- const arr = []
129
- for (const [method, brokers] of Object.entries(store.brokersByMethod))
130
- if (byMethod === '*' || byMethod === method)
131
- arr.push(...Object.values(brokers))
132
- return arr
144
+ folderGroupsByMethod(method) {
145
+ const groups = []
146
+ let g = null
147
+ for (const row of store._brokersAsRowsByMethod(method)) {
148
+ const folder = row.urlMask.substring(0, row.urlMask.lastIndexOf('/') + 1)
149
+ if (!g || g.folder !== folder) {
150
+ g = { folder, children: [] }
151
+ groups.push(g)
152
+ }
153
+ g.children.push(row)
154
+ }
155
+ return groups
133
156
  },
134
157
 
135
- brokersAsRowsByMethod(method) {
158
+ _brokersAsRowsByMethod(method) {
136
159
  const rows = store._brokersAsArray(method)
137
160
  .map(b => new BrokerRowModel(b, store.canProxy))
138
161
  .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
@@ -145,12 +168,14 @@ export const store = {
145
168
  return rows
146
169
  },
147
170
 
148
- brokerAsRow(method, urlMask) {
149
- const b = store.brokerFor(method, urlMask)
150
- const r = new BrokerRowModel(b, store.canProxy)
151
- r.setUrlMaskDittoed(store._dittoCache.get(r.key))
152
- return r
171
+ _brokersAsArray(byMethod = '*') {
172
+ const arr = []
173
+ for (const [method, brokers] of Object.entries(store.brokersByMethod))
174
+ if (byMethod === '*' || byMethod === method)
175
+ arr.push(...Object.values(brokers))
176
+ return arr
153
177
  },
178
+
154
179
 
155
180
  previewLink(method, urlMask) {
156
181
  store.setChosenLink(method, urlMask)
@@ -423,7 +423,7 @@ main {
423
423
  padding-bottom: 4px;
424
424
  padding-left: 1px;
425
425
  border-top: 24px solid transparent;
426
- margin-left: 71px;
426
+ margin-left: 94px;
427
427
  font-weight: bold;
428
428
  text-align: left;
429
429
 
@@ -438,7 +438,7 @@ main {
438
438
 
439
439
  .TableRow {
440
440
  display: flex;
441
- align-items: center;
441
+ margin-left: 24px;
442
442
 
443
443
  &.animIn {
444
444
  opacity: 0;
@@ -446,7 +446,79 @@ main {
446
446
  animation: _kfRowIn 180ms ease-in-out forwards;
447
447
  }
448
448
  }
449
+
450
+ .FolderGroup {
451
+ position: relative;
452
+
453
+ > summary {
454
+ left: 0;
455
+ display: flex;
456
+ overflow: hidden;
457
+ align-items: center;
458
+ padding: 5px 0;
459
+ font-size: 12px;
460
+ list-style: none;
461
+ cursor: pointer;
462
+ user-select: none;
463
+ color: var(--colorLabel);
464
+ white-space: nowrap;
465
+ text-overflow: ellipsis;
466
+ border-radius: var(--radius);
467
+
468
+
469
+ &:active {
470
+ cursor: grabbing;
471
+ }
472
+
473
+ &::-webkit-details-marker {
474
+ display: none;
475
+ }
476
+
477
+ &:hover {
478
+ color: var(--colorText);
479
+ }
480
+
481
+ .FolderName {
482
+ margin-left: 125px;
483
+ &.groupedByMethod {
484
+ margin-left: 79px;
485
+ }
486
+ }
487
+
488
+ .FolderChevron {
489
+ display: flex;
490
+ width: 16px;
491
+ height: 16px;
492
+ flex-shrink: 0;
493
+ align-items: center;
494
+ justify-content: center;
495
+ opacity: .7;
496
+ transition: transform cubic-bezier(.2, .7, .8, 1.4) 400ms;
497
+ transform: rotate(-90deg);
498
+
499
+ svg {
500
+ width: 100%;
501
+ height: 100%;
502
+ }
503
+ }
504
+ }
505
+
506
+ &[open] > summary {
507
+ position: absolute;
508
+ top: 0;
509
+ left: 0;
510
+ width: 16px;
511
+
512
+ .FolderChevron {
513
+ transform: rotate(0deg);
514
+ }
515
+ .FolderName {
516
+ display: none;
517
+ }
518
+ }
519
+ }
449
520
  }
521
+
450
522
  @keyframes _kfRowIn {
451
523
  to {
452
524
  opacity: 1;
@@ -458,6 +530,7 @@ main {
458
530
  overflow: hidden;
459
531
  min-width: 38px;
460
532
  padding: 4px 0;
533
+ margin-top: 3px;
461
534
  margin-right: 8px;
462
535
  color: var(--colorLabel);
463
536
  font-size: 11px;
@@ -523,6 +596,7 @@ main {
523
596
 
524
597
  .Toggler {
525
598
  display: flex;
599
+ margin-top: 3px;
526
600
 
527
601
  input {
528
602
  /* For click drag target */
package/src/client/app.js CHANGED
@@ -3,8 +3,8 @@ import { createElement as r, t, classNames, restoreFocus, Fragment, defineClassN
3
3
  import { store } from './app-store.js'
4
4
  import { API } from './ApiConstants.js'
5
5
  import { Header } from './app-header.js'
6
- import { TimerIcon, CloudIcon } from './graphics.js'
7
6
  import { PayloadViewer, previewMock } from './app-payload-viewer.js'
7
+ import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
8
8
 
9
9
  import CSS from './app.css' with { type: 'css' }
10
10
  document.adoptedStyleSheets.push(CSS)
@@ -100,9 +100,30 @@ function MockList() {
100
100
  r('div', {
101
101
  className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
102
102
  }, method),
103
- store.brokersAsRowsByMethod(method).map(Row)))
103
+ FolderGroups(store.folderGroupsByMethod(method))))
104
104
 
105
- return store.brokersAsRowsByMethod('*').map(Row)
105
+ return FolderGroups(store.folderGroupsByMethod('*'))
106
+ }
107
+
108
+ function FolderGroups(groups) {
109
+ return groups.map(({ folder, children }) => {
110
+ if (children.length === 1)
111
+ return Row(children[0], 0)
112
+
113
+ return r('details', {
114
+ className: CSS.FolderGroup,
115
+ open: !store.collapsedFolders.has(folder),
116
+ onToggle() {
117
+ // TODO alt+click exclusive open
118
+ store.setFolderCollapsed(folder, !this.open)
119
+ }
120
+ },
121
+ r('summary', null,
122
+ r('span', { className: CSS.FolderChevron }, ChevronDownIcon()),
123
+ r('span', { className: classNames(CSS.FolderName, store.groupByMethod && CSS.groupedByMethod) },
124
+ folder + '…')),
125
+ children.map(Row))
126
+ })
106
127
  }
107
128
 
108
129
  /**
@@ -27,3 +27,9 @@ export function HelpIcon() {
27
27
  s('svg', { viewBox: '0 0 24 24' },
28
28
  s('path', { d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25' })))
29
29
  }
30
+
31
+ export function ChevronDownIcon() {
32
+ return (
33
+ s('svg', { viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', 'stroke-width': '2' },
34
+ s('path', { d: 'M6 9l6 6 6-6' })))
35
+ }
package/src/server/Api.js CHANGED
@@ -113,7 +113,6 @@ async function setGlobalDelay(req, response) {
113
113
  response.unprocessable(`Expected non-negative integer for "delay"`)
114
114
  else {
115
115
  config.delay = delay
116
- uiSyncVersion.increment()
117
116
  response.ok()
118
117
  uiSyncVersion.increment()
119
118
  }
@@ -219,7 +218,7 @@ async function setRouteIsDelayed(req, response) {
219
218
  else {
220
219
  broker.setDelayed(delayed)
221
220
  response.json(broker)
222
- uiSyncVersion.increment()
221
+ uiSyncVersion.increment()
223
222
  }
224
223
  }
225
224
 
@@ -53,11 +53,8 @@ export async function dispatchMock(req, response) {
53
53
  response.setHeader('Content-Type', mime)
54
54
  response.setHeader('Content-Length', length(body))
55
55
 
56
- setTimeout(() =>
57
- response.end(isHead
58
- ? null
59
- : body
60
- ), Number(broker.delayed && calcDelay()))
56
+ setTimeout(() => response.end(isHead ? null : body),
57
+ Number(broker.delayed && calcDelay()))
61
58
 
62
59
  logger.accessMock(req.url, broker.file)
63
60
  }
@@ -5,9 +5,8 @@ import pkgJSON from '../../package.json' with { type: 'json' }
5
5
 
6
6
  import { logger } from './utils/logger.js'
7
7
  import { ServerResponse } from './utils/HttpServerResponse.js'
8
- import { IncomingMessage } from './utils/HttpIncomingMessage.js'
8
+ import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
9
9
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
10
- import { BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
11
10
 
12
11
  import { API } from '../client/ApiConstants.js'
13
12
 
@@ -61,29 +61,10 @@ function request(path, options = {}) {
61
61
  }
62
62
 
63
63
 
64
- class BaseFixture {
65
- dir = ''
66
- urlMask = ''
67
- method = ''
68
-
64
+ class Fixture {
69
65
  constructor(file, body = '') {
70
66
  this.file = file
71
67
  this.body = body || `Body for ${file}`
72
- }
73
- write() { return api.writeMock(this.file, this.body) }
74
- delete() { return api.deleteMock(this.file) }
75
-
76
- request(options = {}) {
77
- options.method ??= this.method
78
- return request(this.urlMask, options)
79
- }
80
- }
81
-
82
-
83
- class Fixture extends BaseFixture {
84
- constructor(file, body = '') {
85
- super(file, body)
86
- this.dir = mocksDir
87
68
  const t = parseFilename(file)
88
69
  this.urlMask = t.urlMask
89
70
  this.method = t.method
@@ -93,14 +74,12 @@ class Fixture extends BaseFixture {
93
74
  async fetchBroker() {
94
75
  return (await fetchState()).brokersByMethod?.[this.method]?.[this.urlMask]
95
76
  }
96
- }
77
+ write() { return api.writeMock(this.file, this.body) }
78
+ delete() { return api.deleteMock(this.file) }
97
79
 
98
- class FixtureStatic extends BaseFixture {
99
- constructor(file, body = '') {
100
- super(file, body)
101
- this.dir = mocksDir
102
- this.urlMask = '/' + file
103
- this.method = 'GET'
80
+ request(options = {}) {
81
+ options.method ??= this.method
82
+ return request(this.urlMask, options)
104
83
  }
105
84
  }
106
85
 
@@ -614,8 +593,8 @@ describe('Dynamic Function Mocks', () => {
614
593
 
615
594
 
616
595
  describe('Static Files', () => {
617
- const fxsIndex = new FixtureStatic('index.html', '<h1>Index</h1>')
618
- const fxsAsset = new FixtureStatic('asset-script.js', 'const a = 1')
596
+ const fxsIndex = new Fixture('index.html', '<h1>Index</h1>')
597
+ const fxsAsset = new Fixture('asset-script.js', 'const a = 1')
619
598
  before(async () => {
620
599
  await api.reset()
621
600
  await fxsIndex.write()
@@ -724,7 +703,7 @@ describe('Auto Status', () => {
724
703
  })
725
704
 
726
705
  test('toggling ON 404 for static routes', async () => {
727
- const fx = new FixtureStatic('static-404.txt')
706
+ const fx = new Fixture('static-404.txt')
728
707
  await fx.write()
729
708
  equal((await fx.request()).status, 200)
730
709
 
@@ -1040,10 +1019,6 @@ describe('Watch mocks API toggler', () => {
1040
1019
  describe('Registering Mocks', () => {
1041
1020
  // simulates user interacting with the file-system directly
1042
1021
  class FixtureExternal extends Fixture {
1043
- constructor(props) {
1044
- super(props)
1045
- }
1046
-
1047
1022
  async writeExternally() {
1048
1023
  const nextVerPromise = resolveOnNextSyncVersion()
1049
1024
  await sleep(0) // next macro task
@@ -1063,7 +1038,6 @@ describe('Registering Mocks', () => {
1063
1038
  return new Promise(resolve => setTimeout(resolve, ms))
1064
1039
  }
1065
1040
 
1066
-
1067
1041
  const fxA = new FixtureExternal('register(default).GET.200.json')
1068
1042
  const fxB = new FixtureExternal('register(alt).GET.200.json')
1069
1043
 
@@ -1138,9 +1112,9 @@ describe('Registering Mocks', () => {
1138
1112
  })
1139
1113
 
1140
1114
  test('responds when dir is renamed', async () => {
1141
- const p0 = resolveOnNextSyncVersion(version + 2)
1115
+ const prom = resolveOnNextSyncVersion(version + 2)
1142
1116
  await renameInMocksDir('reg0', 'reg1')
1143
- equal(await p0, version + 3)
1117
+ equal(await prom, version + 3)
1144
1118
 
1145
1119
  const s = await fetchState()
1146
1120
  equal(s.brokersByMethod.GET['/reg1/runtime0'].file, 'reg1/runtime0.GET.200.txt')
@@ -61,8 +61,8 @@ export function registerMock(file, isFromWatcher = false) {
61
61
 
62
62
  export function unregisterMock(file) {
63
63
  const broker = brokerByFilename(file)
64
- const hasNoMoreMocks = broker?.unregister(file)
65
- if (hasNoMoreMocks) {
64
+ const methodHasNoMoreMocks = broker?.unregister(file) // TODO or it was a directory of many mocks
65
+ if (methodHasNoMoreMocks) {
66
66
  const { method, urlMask } = parseFilename(file)
67
67
  delete collection[method][urlMask]
68
68
  if (!Object.keys(collection[method]).length)
@@ -19,14 +19,14 @@ export class ServerResponse extends http.ServerResponse {
19
19
 
20
20
  html(html, csp) {
21
21
  logger.access(this)
22
- this.setHeader('Content-Type', mimeFor('html'))
22
+ this.setHeader('Content-Type', mimeFor('.html'))
23
23
  this.setHeader('Content-Security-Policy', csp)
24
24
  this.end(html)
25
25
  }
26
26
 
27
27
  json(payload) {
28
28
  logger.access(this)
29
- this.setHeader('Content-Type', mimeFor('json'))
29
+ this.setHeader('Content-Type', mimeFor('.json'))
30
30
  this.end(JSON.stringify(payload))
31
31
  }
32
32
 
@@ -40,7 +40,6 @@ export async function resolveIn(baseDir, file) {
40
40
  : null
41
41
  }
42
42
  catch (e) {
43
- console.error('DDDDDD', e)
44
43
  return null
45
44
  }
46
45
  }
@@ -545,7 +545,7 @@
545
545
  "Filename": {
546
546
  "type": "string",
547
547
  "description": "Mock filename. The convention is UrlMask.Method.StatusCode.Extension",
548
- "example": "api/user/avatar.GET.200.svg"
548
+ "example": "api/user/friends.GET.200.json"
549
549
  },
550
550
  "ClientMockBroker": {
551
551
  "type": "object",