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 +1 -1
- package/src/client/app-store.js +2 -15
- package/src/client/app.css +7 -4
- package/src/client/app.js +27 -15
- package/src/client/dirStructure.js +83 -0
- package/src/client/dirStructure.test.js +86 -0
- package/src/server/Mockaton.js +1 -1
- package/src/server/Watcher.js +0 -1
package/package.json
CHANGED
package/src/client/app-store.js
CHANGED
|
@@ -140,21 +140,7 @@ export const store = {
|
|
|
140
140
|
store.brokersByMethod[method][urlMask] = broker
|
|
141
141
|
},
|
|
142
142
|
|
|
143
|
-
|
|
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
|
|
package/src/client/app.css
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
--colorLabel: light-dark(#555, #aaa);
|
|
13
13
|
--colorText: light-dark(#000, #fff);
|
|
14
14
|
|
|
15
|
-
--colorAccent: light-dark(#
|
|
16
|
-
--colorHover: light-dark(#
|
|
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.
|
|
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.
|
|
104
|
+
FolderGroups(store.brokersAsRowsByMethod(method))))
|
|
104
105
|
|
|
105
|
-
return FolderGroups(store.
|
|
106
|
+
return FolderGroups(store.brokersAsRowsByMethod('*'))
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
function FolderGroups(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
140
|
+
Row(broker),
|
|
141
|
+
children.map(c => c.children.length
|
|
142
|
+
? FolderGroup(c)
|
|
143
|
+
: Row(c))))
|
|
128
144
|
}
|
|
129
145
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
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
|
|
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
|
|
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
|
+
|
package/src/server/Mockaton.js
CHANGED
|
@@ -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
|
|