mockaton 13.3.4 → 13.3.5
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-header.js +3 -2
- package/src/client/app-payload-viewer.js +3 -2
- package/src/client/app-store.js +20 -47
- package/src/client/app-store.test.js +1 -25
- package/src/client/app.css +9 -5
- package/src/client/app.js +6 -7
- package/src/client/dir-tree.js +82 -0
- package/src/client/dir-tree.test.js +106 -0
- package/src/client/dom-utils-test.js +74 -0
- package/src/client/dom-utils.js +71 -4
- package/src/client/graphics.js +1 -1
- package/src/server/Mockaton.js +2 -0
- package/src/client/dirStructure.js +0 -47
- package/src/client/dirStructure.test.js +0 -81
- package/src/client/dom-utils.test.js +0 -76
package/package.json
CHANGED
package/src/client/app-header.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { createElement as r, t,
|
|
1
|
+
import { createElement as r, t, extractClassNames } from './dom-utils.js'
|
|
2
2
|
import { Logo, HelpIcon } from './graphics.js'
|
|
3
3
|
import { store } from './app-store.js'
|
|
4
4
|
|
|
5
5
|
import CSS from './app.css' with { type: 'css' }
|
|
6
|
-
defineClassNames(CSS)
|
|
7
6
|
|
|
8
7
|
|
|
8
|
+
Object.assign(CSS, extractClassNames(CSS))
|
|
9
|
+
|
|
9
10
|
export function Header() {
|
|
10
11
|
return (
|
|
11
12
|
r('header', null,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { createElement as r, t,
|
|
1
|
+
import { createElement as r, t, extractClassNames } from './dom-utils.js'
|
|
2
2
|
import { HEADER_502 } from './ApiConstants.js'
|
|
3
3
|
import { parseFilename } from './Filename.js'
|
|
4
4
|
import { store } from './app-store.js'
|
|
5
5
|
|
|
6
6
|
import CSS from './app.css' with { type: 'css' }
|
|
7
|
-
defineClassNames(CSS)
|
|
8
7
|
|
|
9
8
|
|
|
9
|
+
Object.assign(CSS, extractClassNames(CSS))
|
|
10
|
+
|
|
10
11
|
const titleRef = {}
|
|
11
12
|
const codeRef = {}
|
|
12
13
|
|
package/src/client/app-store.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Commander } from './ApiCommander.js'
|
|
2
|
+
import { dittoSplitPaths, groupByFolder } from './dir-tree.js'
|
|
2
3
|
import { parseFilename, extractComments } from './Filename.js'
|
|
4
|
+
import { QueryParamBool, LocalStorageSet } from './dom-utils.js'
|
|
3
5
|
import { EXT_UNKNOWN_MIME, EXT_EMPTY } from './ApiConstants.js'
|
|
4
6
|
|
|
5
7
|
|
|
@@ -22,25 +24,21 @@ export const store = {
|
|
|
22
24
|
collectProxied: false,
|
|
23
25
|
proxyFallback: '',
|
|
24
26
|
showProxyField: null,
|
|
25
|
-
get canProxy() {
|
|
26
|
-
return Boolean(store.proxyFallback)
|
|
27
|
-
},
|
|
27
|
+
get canProxy() { return Boolean(store.proxyFallback) },
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
_groupByMethod: new QueryParamBool('groupByMethod'),
|
|
30
|
+
get groupByMethod() { return store._groupByMethod.value },
|
|
30
31
|
toggleGroupByMethod() {
|
|
31
|
-
store.
|
|
32
|
-
togglePreference('groupByMethod', store.groupByMethod)
|
|
32
|
+
store._groupByMethod.toggle()
|
|
33
33
|
store.render()
|
|
34
34
|
},
|
|
35
35
|
|
|
36
|
-
collapsedFolders: new
|
|
36
|
+
collapsedFolders: new LocalStorageSet('collapsedFolders'),
|
|
37
37
|
setFolderCollapsed(folder, collapsed) {
|
|
38
38
|
if (collapsed)
|
|
39
39
|
store.collapsedFolders.add(folder)
|
|
40
40
|
else
|
|
41
41
|
store.collapsedFolders.delete(folder)
|
|
42
|
-
|
|
43
|
-
globalThis.localStorage?.setItem('collapsedFolders', JSON.stringify([...store.collapsedFolders]))
|
|
44
42
|
},
|
|
45
43
|
|
|
46
44
|
chosenLink: { method: '', urlMask: '' },
|
|
@@ -123,10 +121,6 @@ export const store = {
|
|
|
123
121
|
|
|
124
122
|
_dittoCache: new Map(),
|
|
125
123
|
|
|
126
|
-
brokerFor(method, urlMask) {
|
|
127
|
-
return store.brokersByMethod[method]?.[urlMask]
|
|
128
|
-
},
|
|
129
|
-
|
|
130
124
|
brokerAsRow(method, urlMask) {
|
|
131
125
|
const b = store.brokerFor(method, urlMask)
|
|
132
126
|
const r = new BrokerRowModel(b, store.canProxy)
|
|
@@ -134,13 +128,15 @@ export const store = {
|
|
|
134
128
|
return r
|
|
135
129
|
},
|
|
136
130
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
brokerFor(method, urlMask) {
|
|
132
|
+
return store.brokersByMethod[method]?.[urlMask]
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
folderGroupsByMethod(method) {
|
|
136
|
+
return groupByFolder(store._brokersAsRowsByMethod(method))
|
|
141
137
|
},
|
|
142
138
|
|
|
143
|
-
|
|
139
|
+
_brokersAsRowsByMethod(method) {
|
|
144
140
|
const rows = store._brokersAsArray(method)
|
|
145
141
|
.map(b => new BrokerRowModel(b, store.canProxy))
|
|
146
142
|
.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
@@ -200,11 +196,15 @@ export const store = {
|
|
|
200
196
|
store._request(() => api.setRouteIsDelayed(method, urlMask, checked), async response => {
|
|
201
197
|
store._setBroker(await response.json())
|
|
202
198
|
})
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
_setBroker(broker) {
|
|
202
|
+
const { method, urlMask } = parseFilename(broker.file)
|
|
203
|
+
store.brokersByMethod[method] ??= {}
|
|
204
|
+
store.brokersByMethod[method][urlMask] = broker
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
|
|
208
208
|
// When false, the URL will be updated with param=false
|
|
209
209
|
function initPreference(param) {
|
|
210
210
|
const qs = new URLSearchParams(globalThis.location?.search)
|
|
@@ -237,33 +237,6 @@ function togglePreference(param, nextVal) {
|
|
|
237
237
|
|
|
238
238
|
|
|
239
239
|
|
|
240
|
-
/**
|
|
241
|
-
* Think of this as a way of printing a directory tree in which
|
|
242
|
-
* the repeated folder paths are kept but styled differently.
|
|
243
|
-
* @param {string[]} paths - sorted
|
|
244
|
-
*/
|
|
245
|
-
export function dittoSplitPaths(paths) {
|
|
246
|
-
const pParts = paths.map(p => p.split('/').filter(Boolean))
|
|
247
|
-
return paths.map((p, i) => {
|
|
248
|
-
if (i === 0)
|
|
249
|
-
return ['', p]
|
|
250
|
-
|
|
251
|
-
const prev = pParts[i - 1]
|
|
252
|
-
const curr = pParts[i]
|
|
253
|
-
const min = Math.min(curr.length, prev.length)
|
|
254
|
-
let j = 0
|
|
255
|
-
while (j < min && curr[j] === prev[j])
|
|
256
|
-
j++
|
|
257
|
-
|
|
258
|
-
if (!j) // no common dirs
|
|
259
|
-
return ['', p]
|
|
260
|
-
|
|
261
|
-
const ditto = '/' + curr.slice(0, j).join('/') + '/'
|
|
262
|
-
return [ditto, p.slice(ditto.length)]
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
240
|
export class BrokerRowModel {
|
|
268
241
|
opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
|
|
269
242
|
isNew = false
|
|
@@ -1,31 +1,7 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import { deepEqual } from 'node:assert/strict'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
test('dittoSplitPaths', () => {
|
|
8
|
-
const input = [
|
|
9
|
-
'/api/user',
|
|
10
|
-
'/api/user/avatar',
|
|
11
|
-
'/api/user/friends',
|
|
12
|
-
'/api/vid',
|
|
13
|
-
'/api/video/id',
|
|
14
|
-
'/api/video/stats',
|
|
15
|
-
'/v2/foo',
|
|
16
|
-
'/v2/foo/bar'
|
|
17
|
-
]
|
|
18
|
-
deepEqual(dittoSplitPaths(input), [
|
|
19
|
-
['', '/api/user'],
|
|
20
|
-
['/api/user/', 'avatar'],
|
|
21
|
-
['/api/user/', 'friends'],
|
|
22
|
-
['/api/', 'vid'],
|
|
23
|
-
['/api/', 'video/id'],
|
|
24
|
-
['/api/video/', 'stats'],
|
|
25
|
-
['', '/v2/foo'],
|
|
26
|
-
['/v2/foo/', 'bar']
|
|
27
|
-
])
|
|
28
|
-
})
|
|
4
|
+
import { BrokerRowModel, t } from './app-store.js'
|
|
29
5
|
|
|
30
6
|
|
|
31
7
|
test('BrokerRowModel', () => {
|
package/src/client/app.css
CHANGED
|
@@ -185,15 +185,19 @@ header {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
.HelpLink {
|
|
188
|
-
width:
|
|
189
|
-
height:
|
|
188
|
+
width: 22px;
|
|
189
|
+
height: 22px;
|
|
190
190
|
flex-shrink: 0;
|
|
191
191
|
align-self: end;
|
|
192
|
-
margin-bottom:
|
|
192
|
+
margin-bottom: 3px;
|
|
193
193
|
margin-left: auto;
|
|
194
194
|
border-radius: 50%;
|
|
195
|
-
fill:
|
|
196
|
-
background: var(--
|
|
195
|
+
fill: var(--colorBgHeader);
|
|
196
|
+
background: var(--colorLabel);
|
|
197
|
+
|
|
198
|
+
&:hover {
|
|
199
|
+
background: var(--colorAccent);
|
|
200
|
+
}
|
|
197
201
|
|
|
198
202
|
svg {
|
|
199
203
|
transform: scale(.7);
|
package/src/client/app.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { createElement as r, t,
|
|
1
|
+
import { createElement as r, t, restoreFocus, Fragment, classNames, extractClassNames } from './dom-utils.js'
|
|
2
2
|
|
|
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'
|
|
7
6
|
import { PayloadViewer, previewMock } from './app-payload-viewer.js'
|
|
8
7
|
import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
|
|
9
8
|
|
|
10
9
|
import CSS from './app.css' with { type: 'css' }
|
|
11
10
|
document.adoptedStyleSheets.push(CSS)
|
|
12
|
-
defineClassNames(CSS)
|
|
13
11
|
|
|
12
|
+
Object.assign(CSS, extractClassNames(CSS))
|
|
14
13
|
|
|
15
14
|
store.onError = onError
|
|
16
15
|
store.render = render
|
|
@@ -100,14 +99,14 @@ function MockList() {
|
|
|
100
99
|
r('div', {
|
|
101
100
|
className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
|
|
102
101
|
}, method),
|
|
103
|
-
FolderGroups(store.
|
|
102
|
+
FolderGroups(store.folderGroupsByMethod(method))))
|
|
104
103
|
|
|
105
|
-
return FolderGroups(store.
|
|
104
|
+
return FolderGroups(store.folderGroupsByMethod('*'))
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
function FolderGroups(
|
|
107
|
+
function FolderGroups(brokersTree) {
|
|
109
108
|
const res = []
|
|
110
|
-
for (const b of
|
|
109
|
+
for (const b of brokersTree) {
|
|
111
110
|
if (!b.children.length)
|
|
112
111
|
res.push(Row(b))
|
|
113
112
|
else
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Think of this as a way of printing a directory tree in which
|
|
3
|
+
* the repeated folder paths are kept but styled differently.
|
|
4
|
+
* @param {string[]} paths - sorted
|
|
5
|
+
*/
|
|
6
|
+
export function dittoSplitPaths(paths) {
|
|
7
|
+
const pParts = paths.map(p => p.split('/').filter(Boolean))
|
|
8
|
+
return paths.map((p, i) => {
|
|
9
|
+
if (i === 0)
|
|
10
|
+
return ['', p]
|
|
11
|
+
|
|
12
|
+
const prev = pParts[i - 1]
|
|
13
|
+
const curr = pParts[i]
|
|
14
|
+
const min = Math.min(curr.length, prev.length)
|
|
15
|
+
let j = 0
|
|
16
|
+
while (j < min && curr[j] === prev[j])
|
|
17
|
+
j++
|
|
18
|
+
|
|
19
|
+
if (!j) // no common dirs
|
|
20
|
+
return ['', p]
|
|
21
|
+
|
|
22
|
+
const ditto = '/' + curr.slice(0, j).join('/') + '/'
|
|
23
|
+
return [ditto, p.slice(ditto.length)]
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Partial<BrokerRowModel>[]} brokers
|
|
31
|
+
* @returns {Partial<BrokerRowModel>[]}
|
|
32
|
+
*/
|
|
33
|
+
export function groupByFolder(brokers) {
|
|
34
|
+
return dfs(trie(brokers))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function trie(brokers) {
|
|
38
|
+
const root = new TrieNode()
|
|
39
|
+
for (const b of brokers) {
|
|
40
|
+
let node = root
|
|
41
|
+
for (const seg of b.urlMask.split('/')) { // TODO it should ignore query string
|
|
42
|
+
const segNode = node.getChild(seg) || new TrieNode()
|
|
43
|
+
node.addChild(seg, segNode)
|
|
44
|
+
node = segNode
|
|
45
|
+
}
|
|
46
|
+
node.brokers.push(b)
|
|
47
|
+
}
|
|
48
|
+
return root
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class TrieNode {
|
|
52
|
+
#children
|
|
53
|
+
constructor() {
|
|
54
|
+
this.brokers = []
|
|
55
|
+
this.#children = new Map()
|
|
56
|
+
}
|
|
57
|
+
addChild(k, v) { this.#children.set(k, v) }
|
|
58
|
+
getChild(k) { return this.#children.get(k) }
|
|
59
|
+
getChildren() { return this.#children.values() }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {TrieNode} node */
|
|
63
|
+
function dfs(node) {
|
|
64
|
+
const childBrokers = []
|
|
65
|
+
for (const tnc of node.getChildren())
|
|
66
|
+
childBrokers.push(...dfs(tnc))
|
|
67
|
+
|
|
68
|
+
const brokers = node.brokers.length
|
|
69
|
+
? [node.brokers[0], ...childBrokers, ...node.brokers.slice(1)]
|
|
70
|
+
: childBrokers
|
|
71
|
+
|
|
72
|
+
if (!brokers.length)
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
const [b0, ...rest] = brokers
|
|
76
|
+
if (node.brokers.length || !b0.children.length) {
|
|
77
|
+
b0.children.push(...rest)
|
|
78
|
+
return [b0]
|
|
79
|
+
}
|
|
80
|
+
return brokers
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { deepEqual } from 'node:assert/strict'
|
|
3
|
+
import { groupByFolder, dittoSplitPaths } from './dir-tree.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
test('dittoSplitPaths', () => {
|
|
7
|
+
const input = [
|
|
8
|
+
'/api/user',
|
|
9
|
+
'/api/user/avatar',
|
|
10
|
+
'/api/user/friends',
|
|
11
|
+
'/api/vid',
|
|
12
|
+
'/api/video/id',
|
|
13
|
+
'/api/video/stats',
|
|
14
|
+
'/v2/foo',
|
|
15
|
+
'/v2/foo/bar'
|
|
16
|
+
]
|
|
17
|
+
deepEqual(dittoSplitPaths(input), [
|
|
18
|
+
['', '/api/user'],
|
|
19
|
+
['/api/user/', 'avatar'],
|
|
20
|
+
['/api/user/', 'friends'],
|
|
21
|
+
['/api/', 'vid'],
|
|
22
|
+
['/api/', 'video/id'],
|
|
23
|
+
['/api/video/', 'stats'],
|
|
24
|
+
['', '/v2/foo'],
|
|
25
|
+
['/v2/foo/', 'bar']
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
test('dirStructure', () => {
|
|
31
|
+
const input = [
|
|
32
|
+
{ children: [], method: 'GET', urlMask: '/api/user' },
|
|
33
|
+
{ children: [], method: 'GET', urlMask: '/api/user/avatar' },
|
|
34
|
+
{ children: [], method: 'GET', urlMask: '/api/video/[id]' },
|
|
35
|
+
{ children: [], method: 'GET', urlMask: '/index.html' },
|
|
36
|
+
{ children: [], method: 'GET', urlMask: '/media/file-a.txt' },
|
|
37
|
+
{ children: [], method: 'GET', urlMask: '/media/file-b.txt' },
|
|
38
|
+
{ children: [], method: 'GET', urlMask: '/media/sub/file-aa.txt' },
|
|
39
|
+
{ children: [], method: 'GET', urlMask: '/media/sub/file-bb.txt' },
|
|
40
|
+
{ children: [], method: 'POST', urlMask: '/api/user' },
|
|
41
|
+
{ children: [], method: 'POST', urlMask: '/api/user/avatar/foo' },
|
|
42
|
+
{ children: [], method: 'PATCH', urlMask: '/api/user' }
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const expected = [
|
|
47
|
+
{
|
|
48
|
+
urlMask: '/api/user',
|
|
49
|
+
method: 'GET',
|
|
50
|
+
children: [
|
|
51
|
+
{
|
|
52
|
+
urlMask: '/api/user/avatar',
|
|
53
|
+
method: 'GET',
|
|
54
|
+
children: [
|
|
55
|
+
{
|
|
56
|
+
urlMask: '/api/user/avatar/foo',
|
|
57
|
+
method: 'POST',
|
|
58
|
+
children: []
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}, {
|
|
62
|
+
urlMask: '/api/user',
|
|
63
|
+
method: 'POST',
|
|
64
|
+
children: []
|
|
65
|
+
}, {
|
|
66
|
+
urlMask: '/api/user',
|
|
67
|
+
method: 'PATCH',
|
|
68
|
+
children: []
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
urlMask: '/api/video/[id]',
|
|
74
|
+
method: 'GET',
|
|
75
|
+
children: []
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
urlMask: '/index.html',
|
|
79
|
+
method: 'GET',
|
|
80
|
+
children: []
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
urlMask: '/media/file-a.txt',
|
|
84
|
+
method: 'GET',
|
|
85
|
+
children: [
|
|
86
|
+
{
|
|
87
|
+
urlMask: '/media/file-b.txt',
|
|
88
|
+
method: 'GET',
|
|
89
|
+
children: []
|
|
90
|
+
}, {
|
|
91
|
+
urlMask: '/media/sub/file-aa.txt',
|
|
92
|
+
method: 'GET',
|
|
93
|
+
children: [
|
|
94
|
+
{
|
|
95
|
+
urlMask: '/media/sub/file-bb.txt',
|
|
96
|
+
method: 'GET',
|
|
97
|
+
children: []
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
deepEqual(groupByFolder(input), expected)
|
|
106
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { deepEqual, equal } from 'node:assert/strict'
|
|
3
|
+
import { classNames, extractClassNames } from './dom-utils.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
test('classNames', () => equal(classNames('a', false && 'b'), 'a'))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
test('extractClassNames', () => {
|
|
10
|
+
const cssRules = [
|
|
11
|
+
{ cssText: '.TopLevelPascal { color: red; }' },
|
|
12
|
+
{ cssText: '.topLevelCamel { color: blue; }' },
|
|
13
|
+
{ cssText: '.top_level_snake { color: green; }' },
|
|
14
|
+
{ cssText: '.top-level-kebab { color: yellow; }' },
|
|
15
|
+
{ cssText: '.Level2Parent {\n & .level2ChildCamel { color: purple; }\n}' },
|
|
16
|
+
{ cssText: '.level2Base {\n &.level2ModifierCamel { font-weight: bold; }\n}' },
|
|
17
|
+
{ cssText: '.Level3Parent {\n & .level3ChildCamel {\n & .level3_grand_child_snake { color: orange; }\n}\n}' },
|
|
18
|
+
{ cssText: '.pseudoParent {\n &:hover { background: red; }\n & .pseudoNestedChild { color: pink; }\n}' },
|
|
19
|
+
{ cssText: '.multiClass1, .multi_class_2 { padding: 10px; }' },
|
|
20
|
+
{ cssText: '.combParent {\n & > .combChildDirect { margin: 5px; }\n}' },
|
|
21
|
+
{ cssText: '.siblingBase {\n & + .siblingAdjacent { border: 1px solid; }\n}' },
|
|
22
|
+
{ cssText: '@media (max-width: 768px) {\n .mediaQueryClass {\n & .mqNestedChild { display: none; }\n}\n}' },
|
|
23
|
+
{ cssText: '.class_with_123_numbers { color: cyan; }' },
|
|
24
|
+
{ cssText: '._privateStyleClass { opacity: 0.5; }' },
|
|
25
|
+
{ cssText: '.stringTest { content: ".shouldNotBeExtracted"; background: url(".alsoIgnored"); }' },
|
|
26
|
+
{ cssText: '.ComplexRoot {\n & .level2-kebab {\n &.level2ModCamel { color: red; }\n & .level3_snake {\n & .level4PascalChild { color: blue; }\n}\n}\n}' }
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const expected = {
|
|
30
|
+
TopLevelPascal: null,
|
|
31
|
+
topLevelCamel: null,
|
|
32
|
+
top_level_snake: null,
|
|
33
|
+
'top-level-kebab': null,
|
|
34
|
+
|
|
35
|
+
Level2Parent: null,
|
|
36
|
+
level2ChildCamel: null,
|
|
37
|
+
level2Base: null,
|
|
38
|
+
level2ModifierCamel: null,
|
|
39
|
+
|
|
40
|
+
Level3Parent: null,
|
|
41
|
+
level3ChildCamel: null,
|
|
42
|
+
level3_grand_child_snake: null,
|
|
43
|
+
|
|
44
|
+
pseudoParent: null,
|
|
45
|
+
pseudoNestedChild: null,
|
|
46
|
+
|
|
47
|
+
multiClass1: null,
|
|
48
|
+
multi_class_2: null,
|
|
49
|
+
|
|
50
|
+
combParent: null,
|
|
51
|
+
combChildDirect: null,
|
|
52
|
+
|
|
53
|
+
siblingBase: null,
|
|
54
|
+
siblingAdjacent: null,
|
|
55
|
+
|
|
56
|
+
mediaQueryClass: null,
|
|
57
|
+
mqNestedChild: null,
|
|
58
|
+
|
|
59
|
+
class_with_123_numbers: null,
|
|
60
|
+
_privateStyleClass: null,
|
|
61
|
+
|
|
62
|
+
stringTest: null,
|
|
63
|
+
|
|
64
|
+
ComplexRoot: null,
|
|
65
|
+
'level2-kebab': null,
|
|
66
|
+
level2ModCamel: null,
|
|
67
|
+
level3_snake: null,
|
|
68
|
+
level4PascalChild: null
|
|
69
|
+
}
|
|
70
|
+
for (const k of Object.keys(expected))
|
|
71
|
+
expected[k] = k
|
|
72
|
+
|
|
73
|
+
deepEqual(extractClassNames({ cssRules }), expected)
|
|
74
|
+
})
|
package/src/client/dom-utils.js
CHANGED
|
@@ -70,12 +70,9 @@ function selectorFor(elem) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
export function defineClassNames(sheet) {
|
|
74
|
-
Object.assign(sheet, extractClassNames(sheet))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
73
|
export function extractClassNames({ cssRules }) {
|
|
78
74
|
// Class names must begin with _ or a letter, then it can have numbers and hyphens
|
|
75
|
+
// TODO think about tag.className selectors
|
|
79
76
|
const reClassName = /(?:^|[\s,{>])&?\s*\.([a-zA-Z_][\w-]*)/g
|
|
80
77
|
const cNames = {}
|
|
81
78
|
let match
|
|
@@ -85,3 +82,73 @@ export function extractClassNames({ cssRules }) {
|
|
|
85
82
|
return cNames
|
|
86
83
|
}
|
|
87
84
|
|
|
85
|
+
|
|
86
|
+
export class QueryParamBool {
|
|
87
|
+
constructor(param) {
|
|
88
|
+
this.param = param
|
|
89
|
+
this.value = this.#init()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#init() {
|
|
93
|
+
const qs = new URLSearchParams(globalThis.location?.search)
|
|
94
|
+
if (qs.has(this.param))
|
|
95
|
+
return qs.get(this.param) !== '0'
|
|
96
|
+
const stored = globalThis.localStorage?.getItem(this.param) !== '0'
|
|
97
|
+
if (!stored)
|
|
98
|
+
this.#applyToUrl(false)
|
|
99
|
+
return stored
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
toggle() {
|
|
103
|
+
this.value = !this.value
|
|
104
|
+
if (this.value)
|
|
105
|
+
globalThis.localStorage?.removeItem(this.param)
|
|
106
|
+
else
|
|
107
|
+
globalThis.localStorage?.setItem(this.param, '0')
|
|
108
|
+
this.#applyToUrl(this.value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#applyToUrl(nextVal) {
|
|
112
|
+
const url = new URL(globalThis.location?.href)
|
|
113
|
+
if (nextVal)
|
|
114
|
+
url.searchParams.delete(this.param)
|
|
115
|
+
else
|
|
116
|
+
url.searchParams.set(this.param, '0')
|
|
117
|
+
history.replaceState(null, '', url)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
export class LocalStorageSet {
|
|
123
|
+
constructor(key) {
|
|
124
|
+
this.key = key
|
|
125
|
+
this.value = this.#parse()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
add(item) {
|
|
129
|
+
this.value.add(item)
|
|
130
|
+
this.#persist()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
delete(item) {
|
|
134
|
+
this.value.delete(item)
|
|
135
|
+
this.#persist()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
has(item) {
|
|
139
|
+
return this.value.has(item)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#parse() {
|
|
143
|
+
try {
|
|
144
|
+
return new Set(JSON.parse(globalThis.localStorage?.getItem(this.key) || '[]'))
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return new Set()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#persist() {
|
|
152
|
+
globalThis.localStorage?.setItem(this.key, JSON.stringify([...this.value]))
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/client/graphics.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createSvgElement as s } from './dom-utils.js'
|
|
|
4
4
|
export const Logo = () =>
|
|
5
5
|
s('svg', { viewBox: '0 0 556 100' },
|
|
6
6
|
s('path', { d: 'm13.75 1.8789c-5.9487 0.19352-10.865 4.5652-11.082 11.686v81.445c-1e-7 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-64.982c0.02794-3.4488 3.0988-3.5551 4.2031-1.1562l16.615 59.059c1.4393 5.3711 5.1083 7.9633 8.7656 7.9473 3.6573 0.01603 7.3263-2.5762 8.7656-7.9473l16.615-59.059c1.1043-2.3989 4.1752-2.2925 4.2031 1.1562v64.982c0 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-81.445c-0.17732-7.0807-5.1334-11.492-11.082-11.686-5.9487-0.19352-12.652 3.8309-15.609 13.619l-15.686 57.334-15.686-57.334c-2.9569-9.7882-9.6607-13.813-15.609-13.619zm239.19 0.074219c-2.216 0-4 1.784-4 4v89.057c0 2.216 1.784 4 4 4h4.793c2.216 0 3.9868-1.784 4-4l0.10644-17.94c0.0734-0.07237 12.175-13.75 12.175-13.75 5.6772 11.091 11.404 22.158 17.113 33.232 1.0168 1.9689 3.4217 2.7356 5.3906 1.7188l4.2578-2.1992c1.9689-1.0168 2.7356-3.4217 1.7188-5.3906-6.4691-12.585-12.958-25.16-19.442-37.738l17.223-19.771c1.4555-1.671 1.2803-4.189-0.39062-5.6445l-3.6133-3.1465c-0.73105-0.63679-1.6224-0.96212-2.5176-0.98633-1.151-0.03113-2.3063 0.43508-3.125 1.375l-28.896 33.174v-51.99c0-2.216-1.784-4-4-4zm-58.255 23.316c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312l-0.125-7.8457c0-2.216-1.784-4-4-4h-4.6524c-2.216 0-4 1.784-4 4l3e-3 6.7888c3e-3 3.8063-1.5601 9.3694-8.4716 9.3694h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.6937 0 8.3697 5.2207 8.4687 11.828v2.2207c0 2.216 1.784 4 4 4h4.6524c2.216 0 4-1.784 4-4l0.125-5.7363c0-10.699-8.6117-19.312-19.311-19.312zm-72.182 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }),
|
|
7
|
-
s('path', { opacity:
|
|
7
|
+
s('path', { opacity: 0.85, fill: 'currentColor', d: 'm331.9 25.27c-10.699 0-19.312 8.6137-19.312 19.312v4.3682c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-0.20414c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v7.0148h-28.059c-10.699 0-19.312 8.6117-19.312 19.311v4.0477c0 10.699 8.6137 19.313 19.312 19.312h17.812c2.216-1e-6 4-1.784 4-4v-4.7715c0-2.216-1.784-4-4-4h-13.648c-6.9115-2e-5 -12.477-1.5651-12.477-8.5649 0-6.9998 5.5651-8.5629 12.477-8.5629h23.895v25.897c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312z' }),
|
|
8
8
|
s('path', { d: 'm392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }))
|
|
9
9
|
|
|
10
10
|
export const TimerIcon = () =>
|
package/src/server/Mockaton.js
CHANGED
|
@@ -10,6 +10,7 @@ import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpI
|
|
|
10
10
|
|
|
11
11
|
import { API } from '../client/ApiConstants.js'
|
|
12
12
|
|
|
13
|
+
import { cookie } from './cookie.js'
|
|
13
14
|
import { config, setup } from './config.js'
|
|
14
15
|
import { apiPatchReqs, apiGetReqs } from './Api.js'
|
|
15
16
|
|
|
@@ -24,6 +25,7 @@ import { watchMocksDir } from './Watcher.js'
|
|
|
24
25
|
export function Mockaton(options) {
|
|
25
26
|
return new Promise((resolve, reject) => {
|
|
26
27
|
setup(options)
|
|
28
|
+
cookie.init(config.cookies)
|
|
27
29
|
mockBrokerCollection.init()
|
|
28
30
|
|
|
29
31
|
if (config.watcherEnabled) {
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
function TrieNode() {
|
|
2
|
-
this.brokers = []
|
|
3
|
-
this.tnChildren = new Map()
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {Partial<BrokerRowModel>[]} brokers
|
|
8
|
-
* @returns {Partial<BrokerRowModel>[]}
|
|
9
|
-
*/
|
|
10
|
-
export function dirStructure(brokers) {
|
|
11
|
-
return dfs(trie(brokers))
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function trie(brokers) {
|
|
15
|
-
const root = new TrieNode()
|
|
16
|
-
for (const b of brokers) {
|
|
17
|
-
let node = root
|
|
18
|
-
for (const seg of b.urlMask.split('/')) { // TODO it should ignore query string
|
|
19
|
-
const segNode = node.tnChildren.get(seg) || new TrieNode()
|
|
20
|
-
node.tnChildren.set(seg, segNode)
|
|
21
|
-
node = segNode
|
|
22
|
-
}
|
|
23
|
-
node.brokers.push(b)
|
|
24
|
-
}
|
|
25
|
-
return root
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** @param {TrieNode} node */
|
|
29
|
-
function dfs(node) {
|
|
30
|
-
const childBrokers = []
|
|
31
|
-
for (const tnc of node.tnChildren.values())
|
|
32
|
-
childBrokers.push(...dfs(tnc))
|
|
33
|
-
|
|
34
|
-
const brokers = node.brokers.length
|
|
35
|
-
? [node.brokers[0], ...childBrokers, ...node.brokers.slice(1)]
|
|
36
|
-
: childBrokers
|
|
37
|
-
|
|
38
|
-
if (!brokers.length)
|
|
39
|
-
return []
|
|
40
|
-
|
|
41
|
-
const [head, ...rest] = brokers
|
|
42
|
-
if (node.brokers.length || !head.children.length) {
|
|
43
|
-
head.children.push(...rest)
|
|
44
|
-
return [head]
|
|
45
|
-
}
|
|
46
|
-
return brokers
|
|
47
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
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
|
-
urlMask: '/api/user',
|
|
38
|
-
method: 'POST',
|
|
39
|
-
children: []
|
|
40
|
-
}, {
|
|
41
|
-
urlMask: '/api/user',
|
|
42
|
-
method: 'PATCH',
|
|
43
|
-
children: []
|
|
44
|
-
}
|
|
45
|
-
]
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
urlMask: '/api/video/[id]',
|
|
49
|
-
method: 'GET',
|
|
50
|
-
children: []
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
urlMask: '/index.html',
|
|
54
|
-
method: 'GET',
|
|
55
|
-
children: []
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
urlMask: '/media/file-a.txt',
|
|
59
|
-
method: 'GET',
|
|
60
|
-
children: [
|
|
61
|
-
{
|
|
62
|
-
urlMask: '/media/file-b.txt',
|
|
63
|
-
method: 'GET',
|
|
64
|
-
children: []
|
|
65
|
-
}, {
|
|
66
|
-
urlMask: '/media/sub/file-aa.txt',
|
|
67
|
-
method: 'GET',
|
|
68
|
-
children: [
|
|
69
|
-
{
|
|
70
|
-
urlMask: '/media/sub/file-bb.txt',
|
|
71
|
-
method: 'GET',
|
|
72
|
-
children: []
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
}
|
|
76
|
-
]
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
|
|
80
|
-
test('acceptance', () => deepEqual(dirStructure(input), expected))
|
|
81
|
-
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import { deepEqual, equal } from 'node:assert/strict'
|
|
3
|
-
import { extractClassNames, classNames } from './dom-utils.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
test('classNames', () => {
|
|
7
|
-
equal(classNames('a', false && 'b'), 'a')
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const cssRules = [
|
|
12
|
-
{ cssText: '.TopLevelPascal { color: red; }' },
|
|
13
|
-
{ cssText: '.topLevelCamel { color: blue; }' },
|
|
14
|
-
{ cssText: '.top_level_snake { color: green; }' },
|
|
15
|
-
{ cssText: '.top-level-kebab { color: yellow; }' },
|
|
16
|
-
{ cssText: '.Level2Parent {\n & .level2ChildCamel { color: purple; }\n}' },
|
|
17
|
-
{ cssText: '.level2Base {\n &.level2ModifierCamel { font-weight: bold; }\n}' },
|
|
18
|
-
{ cssText: '.Level3Parent {\n & .level3ChildCamel {\n & .level3_grand_child_snake { color: orange; }\n}\n}' },
|
|
19
|
-
{ cssText: '.pseudoParent {\n &:hover { background: red; }\n & .pseudoNestedChild { color: pink; }\n}' },
|
|
20
|
-
{ cssText: '.multiClass1, .multi_class_2 { padding: 10px; }' },
|
|
21
|
-
{ cssText: '.combParent {\n & > .combChildDirect { margin: 5px; }\n}' },
|
|
22
|
-
{ cssText: '.siblingBase {\n & + .siblingAdjacent { border: 1px solid; }\n}' },
|
|
23
|
-
{ cssText: '@media (max-width: 768px) {\n .mediaQueryClass {\n & .mqNestedChild { display: none; }\n}\n}' },
|
|
24
|
-
{ cssText: '.class_with_123_numbers { color: cyan; }' },
|
|
25
|
-
{ cssText: '._privateStyleClass { opacity: 0.5; }' },
|
|
26
|
-
{ cssText: '.stringTest { content: ".shouldNotBeExtracted"; background: url(".alsoIgnored"); }' },
|
|
27
|
-
{ cssText: '.ComplexRoot {\n & .level2-kebab {\n &.level2ModCamel { color: red; }\n & .level3_snake {\n & .level4PascalChild { color: blue; }\n}\n}\n}' }
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
const expected = {
|
|
31
|
-
TopLevelPascal: null,
|
|
32
|
-
topLevelCamel: null,
|
|
33
|
-
top_level_snake: null,
|
|
34
|
-
'top-level-kebab': null,
|
|
35
|
-
|
|
36
|
-
Level2Parent: null,
|
|
37
|
-
level2ChildCamel: null,
|
|
38
|
-
level2Base: null,
|
|
39
|
-
level2ModifierCamel: null,
|
|
40
|
-
|
|
41
|
-
Level3Parent: null,
|
|
42
|
-
level3ChildCamel: null,
|
|
43
|
-
level3_grand_child_snake: null,
|
|
44
|
-
|
|
45
|
-
pseudoParent: null,
|
|
46
|
-
pseudoNestedChild: null,
|
|
47
|
-
|
|
48
|
-
multiClass1: null,
|
|
49
|
-
multi_class_2: null,
|
|
50
|
-
|
|
51
|
-
combParent: null,
|
|
52
|
-
combChildDirect: null,
|
|
53
|
-
|
|
54
|
-
siblingBase: null,
|
|
55
|
-
siblingAdjacent: null,
|
|
56
|
-
|
|
57
|
-
mediaQueryClass: null,
|
|
58
|
-
mqNestedChild: null,
|
|
59
|
-
|
|
60
|
-
class_with_123_numbers: null,
|
|
61
|
-
_privateStyleClass: null,
|
|
62
|
-
|
|
63
|
-
stringTest: null,
|
|
64
|
-
|
|
65
|
-
ComplexRoot: null,
|
|
66
|
-
'level2-kebab': null,
|
|
67
|
-
level2ModCamel: null,
|
|
68
|
-
level3_snake: null,
|
|
69
|
-
level4PascalChild: null
|
|
70
|
-
}
|
|
71
|
-
for (const k of Object.keys(expected))
|
|
72
|
-
expected[k] = k
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
test('extracts', () => deepEqual(extractClassNames({ cssRules }), expected))
|
|
76
|
-
|