mockaton 13.3.2 → 13.3.3

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.3.2",
5
+ "version": "13.3.3",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -140,21 +140,7 @@ export const store = {
140
140
  store.brokersByMethod[method][urlMask] = broker
141
141
  },
142
142
 
143
- folderGroupsByMethod(method) {
144
- const groups = []
145
- let g = null
146
- for (const row of store._brokersAsRowsByMethod(method)) {
147
- const folder = row.urlMask.substring(0, row.urlMask.lastIndexOf('/') + 1)
148
- if (!g || g.folder !== folder) {
149
- g = { folder, children: [] }
150
- groups.push(g)
151
- }
152
- g.children.push(row)
153
- }
154
- return groups
155
- },
156
-
157
- _brokersAsRowsByMethod(method) {
143
+ brokersAsRowsByMethod(method) {
158
144
  const rows = store._brokersAsArray(method)
159
145
  .map(b => new BrokerRowModel(b, store.canProxy))
160
146
  .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
@@ -285,6 +271,7 @@ export class BrokerRowModel {
285
271
  method = ''
286
272
  urlMask = ''
287
273
  urlMaskDittoed = ['', '']
274
+ children = []
288
275
  #broker = /** @type ClientMockBroker */ {}
289
276
  #canProxy = false
290
277
 
@@ -12,8 +12,8 @@
12
12
  --colorLabel: light-dark(#555, #aaa);
13
13
  --colorText: light-dark(#000, #fff);
14
14
 
15
- --colorAccent: light-dark(#0059dd, #2495ff);
16
- --colorHover: light-dark(#c1e2ff, #062d59);
15
+ --colorAccent: light-dark(#0081ff, #2495ff);
16
+ --colorHover: light-dark(#dbedff, #062d59);
17
17
 
18
18
  --colorRed: light-dark(#da0f00, #f41606);
19
19
  --colorPink: light-dark(#ed206a, #f92672);
@@ -420,6 +420,7 @@ main {
420
420
  .Table {
421
421
  height: 100%;
422
422
  padding: 16px;
423
+ padding-bottom: 64px;
423
424
  padding-left: 12px;
424
425
  user-select: none;
425
426
  overflow-y: auto;
@@ -462,6 +463,7 @@ main {
462
463
  overflow: hidden;
463
464
  align-items: center;
464
465
  padding: 5px 0;
466
+ font-weight: 500;
465
467
  font-size: 12px;
466
468
  list-style: none;
467
469
  cursor: pointer;
@@ -516,7 +518,7 @@ main {
516
518
  }
517
519
 
518
520
  &[open] {
519
- &:has(summary:hover) {
521
+ &:has(summary:hover):not(:has(details:hover)) {
520
522
  background: linear-gradient(90deg, var(--colorHover), var(--colorBg));
521
523
  }
522
524
 
@@ -567,6 +569,7 @@ main {
567
569
  padding: 6px 8px;
568
570
  margin-right: -2px;
569
571
  margin-left: 4px;
572
+ font-weight: 500;
570
573
  border-radius: var(--radius);
571
574
  word-break: break-word;
572
575
 
@@ -578,7 +581,7 @@ main {
578
581
  background: var(--colorAccent);
579
582
  }
580
583
  .dittoDir {
581
- opacity: 0.9;
584
+ opacity: 0.8;
582
585
  filter: saturate(0.1);
583
586
  }
584
587
  }
package/src/client/app.js CHANGED
@@ -3,6 +3,7 @@ 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 { dirStructure } from './dirStructure.js'
6
7
  import { PayloadViewer, previewMock } from './app-payload-viewer.js'
7
8
  import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
8
9
 
@@ -100,15 +101,27 @@ function MockList() {
100
101
  r('div', {
101
102
  className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
102
103
  }, method),
103
- FolderGroups(store.folderGroupsByMethod(method))))
104
+ FolderGroups(store.brokersAsRowsByMethod(method))))
104
105
 
105
- return FolderGroups(store.folderGroupsByMethod('*'))
106
+ return FolderGroups(store.brokersAsRowsByMethod('*'))
106
107
  }
107
108
 
108
- function FolderGroups(groups) {
109
- return groups.map(({ folder, children }) => children.length === 1
110
- ? Row(children[0], 0)
111
- : r('details', {
109
+ function FolderGroups(bRows) {
110
+ const res = []
111
+ for (const b of dirStructure(bRows)) {
112
+ if (!b.children.length)
113
+ res.push(Row(b))
114
+ else
115
+ res.push(FolderGroup(b))
116
+ }
117
+ return res
118
+ }
119
+
120
+ function FolderGroup(broker) {
121
+ const folder = broker.urlMask
122
+ const children = broker.children
123
+ return (
124
+ r('details', {
112
125
  className: CSS.FolderGroup,
113
126
  open: !store.collapsedFolders.has(folder),
114
127
  onToggle() {
@@ -124,14 +137,14 @@ function FolderGroups(groups) {
124
137
  store.canProxy && CSS.canProxy)
125
138
  },
126
139
  folder + '…')),
127
- children.map(Row)))
140
+ Row(broker),
141
+ children.map(c => c.children.length
142
+ ? FolderGroup(c)
143
+ : Row(c))))
128
144
  }
129
145
 
130
- /**
131
- * @param {BrokerRowModel} row
132
- * @param {number} i
133
- */
134
- function Row(row, i) {
146
+ /** @param {BrokerRowModel} row */
147
+ function Row(row) {
135
148
  const { method, urlMask } = row
136
149
  return (
137
150
  r('div', {
@@ -159,7 +172,7 @@ function Row(row, i) {
159
172
 
160
173
  !store.groupByMethod && r('span', { className: CSS.Method }, method),
161
174
 
162
- PreviewLink(method, urlMask, row.urlMaskDittoed, i === 0),
175
+ PreviewLink(method, urlMask, row.urlMaskDittoed),
163
176
 
164
177
  MockSelector(row)))
165
178
  }
@@ -204,7 +217,7 @@ function renderRow(method, urlMask) {
204
217
 
205
218
 
206
219
 
207
- function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
220
+ function PreviewLink(method, urlMask, urlMaskDittoed) {
208
221
  function onClick(event) {
209
222
  event.preventDefault()
210
223
  store.previewLink(method, urlMask)
@@ -215,7 +228,6 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
215
228
  r('a', {
216
229
  className: classNames(CSS.PreviewLink, isChosen && CSS.chosen),
217
230
  href: urlMask,
218
- autofocus,
219
231
  onClick
220
232
  }, ditto
221
233
  ? [r('span', { className: CSS.dittoDir }, ditto), tail]
@@ -0,0 +1,83 @@
1
+ class TrieNode {
2
+ constructor() {
3
+ this.items = []
4
+ this.kids = new Map()
5
+ }
6
+ }
7
+
8
+ // TODO it should ignore query string
9
+
10
+ /**
11
+ * @typedef {{method: string, urlMask:string, children: BrokerLite[]}} BrokerLite
12
+ * @param {BrokerLite[]} brokers
13
+ * @returns {BrokerLite[]}
14
+ */
15
+ export function dirStructure(brokers) {
16
+ const root = new TrieNode()
17
+
18
+ for (let b of brokers) {
19
+ let curr = root
20
+ for (const seg of b.urlMask.split('/').filter(Boolean)) {
21
+ if (!curr.kids.has(seg))
22
+ curr.kids.set(seg, new TrieNode())
23
+ curr = curr.kids.get(seg)
24
+ }
25
+ curr.items.push(b)
26
+ }
27
+
28
+ const result = []
29
+ for (const child of root.kids.values())
30
+ result.push(...convertNode(child))
31
+
32
+ if (root.items.length) {
33
+ const elems = [root.items[0], ...result]
34
+ for (let i = 1; i < root.items.length; i++)
35
+ elems.push(root.items[i])
36
+
37
+ const parentNode = elems[0]
38
+ for (let i = 1; i < elems.length; i++)
39
+ parentNode.children.push(elems[i])
40
+
41
+ return [parentNode]
42
+ }
43
+
44
+ return result
45
+ }
46
+
47
+ /**
48
+ * Recursively converts a TrieNode into a flattened, nested array of objects.
49
+ * Flattens the tree by having the first available route at a given directory level
50
+ * act as the parent wrapper for the remaining items and subdirectories.
51
+ *
52
+ * @param {TrieNode} node
53
+ * @returns {BrokerLite[]}
54
+ */
55
+ function convertNode(node) {
56
+ const childNodes = []
57
+ for (const child of node.kids.values())
58
+ childNodes.push(...convertNode(child))
59
+
60
+ const elems = []
61
+ if (node.items.length) {
62
+ elems.push(node.items[0])
63
+ elems.push(...childNodes)
64
+ for (let i = 1; i < node.items.length; i++)
65
+ elems.push(node.items[i])
66
+ }
67
+ else
68
+ elems.push(...childNodes)
69
+
70
+ if (!elems.length)
71
+ return []
72
+
73
+ const parentNode = elems[0]
74
+
75
+ if (node.items.length || !parentNode.children.length) {
76
+ for (let i = 1; i < elems.length; i++)
77
+ parentNode.children.push(elems[i])
78
+ return [parentNode]
79
+ }
80
+
81
+ return elems
82
+ }
83
+
@@ -0,0 +1,86 @@
1
+ import { test } from 'node:test'
2
+ import { deepEqual } from 'node:assert/strict'
3
+ import { dirStructure } from './dirStructure.js'
4
+
5
+
6
+ const input = [
7
+ { children: [], method: 'GET', urlMask: '/api/user' },
8
+ { children: [], method: 'GET', urlMask: '/api/user/avatar' },
9
+ { children: [], method: 'GET', urlMask: '/api/video/[id]' },
10
+ { children: [], method: 'GET', urlMask: '/index.html' },
11
+ { children: [], method: 'GET', urlMask: '/media/file-a.txt' },
12
+ { children: [], method: 'GET', urlMask: '/media/file-b.txt' },
13
+ { children: [], method: 'GET', urlMask: '/media/sub/file-aa.txt' },
14
+ { children: [], method: 'GET', urlMask: '/media/sub/file-bb.txt' },
15
+ { children: [], method: 'POST', urlMask: '/api/user' },
16
+ { children: [], method: 'POST', urlMask: '/api/user/avatar/foo' },
17
+ { children: [], method: 'PATCH', urlMask: '/api/user' }
18
+ ]
19
+
20
+
21
+ const expected = [
22
+ {
23
+ urlMask: '/api/user',
24
+ method: 'GET',
25
+ children: [
26
+ {
27
+ urlMask: '/api/user/avatar',
28
+ method: 'GET',
29
+ children: [
30
+ {
31
+ urlMask: '/api/user/avatar/foo',
32
+ method: 'POST',
33
+ children: []
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ urlMask: '/api/user',
39
+ method: 'POST',
40
+ children: []
41
+ },
42
+ {
43
+ urlMask: '/api/user',
44
+ method: 'PATCH',
45
+ children: []
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ urlMask: '/api/video/[id]',
51
+ method: 'GET',
52
+ children: []
53
+ },
54
+ {
55
+ urlMask: '/index.html',
56
+ method: 'GET',
57
+ children: []
58
+ },
59
+ {
60
+ urlMask: '/media/file-a.txt',
61
+ method: 'GET',
62
+ children: [
63
+ {
64
+ urlMask: '/media/file-b.txt',
65
+ method: 'GET',
66
+ children: []
67
+ },
68
+ {
69
+ urlMask: '/media/sub/file-aa.txt',
70
+ method: 'GET',
71
+ children: [
72
+ {
73
+ urlMask: '/media/sub/file-bb.txt',
74
+ method: 'GET',
75
+ children: []
76
+ }
77
+ ]
78
+ }
79
+ ]
80
+ }
81
+ ]
82
+
83
+ test('acceptance', () => {
84
+ deepEqual(dirStructure(input), expected)
85
+ })
86
+
@@ -5,8 +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, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
9
8
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
9
+ import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
10
10
 
11
11
  import { API } from '../client/ApiConstants.js'
12
12
 
@@ -4,7 +4,6 @@ import { EventEmitter } from 'node:events'
4
4
 
5
5
  import { config } from './config.js'
6
6
  import { isFile, isDirectory } from './utils/fs.js'
7
-
8
7
  import * as mockBrokerCollection from './mockBrokersCollection.js'
9
8
 
10
9