mockaton 10.6.4 → 10.6.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/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/Api.js +2 -4
- package/src/ApiCommander.js +9 -2
- package/src/Dashboard.css +1 -2
- package/src/Dashboard.js +322 -291
- package/src/MockBroker.js +9 -14
- package/src/MockDispatcher.js +10 -6
- package/src/mockBrokersCollection.js +0 -7
package/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -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
|
-
|
|
120
|
+
sendJSON(response, broker.currentMock)
|
|
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
|
-
|
|
133
|
+
sendJSON(response, broker.currentMock)
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -176,8 +176,6 @@ async function updateProxyFallback(req, response) {
|
|
|
176
176
|
sendUnprocessableContent(response, `Invalid Proxy Fallback URL`)
|
|
177
177
|
return
|
|
178
178
|
}
|
|
179
|
-
if (!fallback)
|
|
180
|
-
mockBrokersCollection.ensureAllRoutesHaveSelectedMock()
|
|
181
179
|
config.proxyFallback = fallback
|
|
182
180
|
sendOK(response)
|
|
183
181
|
}
|
package/src/ApiCommander.js
CHANGED
|
@@ -4,8 +4,13 @@ import { API, DF, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
|
|
|
4
4
|
/** Client for controlling Mockaton via its HTTP API */
|
|
5
5
|
export class Commander {
|
|
6
6
|
#addr = ''
|
|
7
|
-
|
|
7
|
+
#then = a => a
|
|
8
|
+
#catch = e => { throw e }
|
|
9
|
+
|
|
10
|
+
constructor(addr, _then = undefined, _catch = undefined) {
|
|
8
11
|
this.#addr = addr
|
|
12
|
+
if (_then) this.#then = _then
|
|
13
|
+
if (_catch) this.#catch = _catch
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
#patch(api, body) {
|
|
@@ -13,6 +18,8 @@ export class Commander {
|
|
|
13
18
|
method: 'PATCH',
|
|
14
19
|
body: JSON.stringify(body)
|
|
15
20
|
})
|
|
21
|
+
.then(this.#then)
|
|
22
|
+
.catch(this.#catch)
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/** @returns {JsonPromise<State>} */
|
|
@@ -42,7 +49,7 @@ export class Commander {
|
|
|
42
49
|
toggle500(routeMethod, routeUrlMask) {
|
|
43
50
|
return this.#patch(API.toggle500, {
|
|
44
51
|
[DF.routeMethod]: routeMethod,
|
|
45
|
-
[DF.routeUrlMask]: routeUrlMask
|
|
52
|
+
[DF.routeUrlMask]: routeUrlMask
|
|
46
53
|
})
|
|
47
54
|
}
|
|
48
55
|
|
package/src/Dashboard.css
CHANGED
package/src/Dashboard.js
CHANGED
|
@@ -56,7 +56,9 @@ const FocusGroup = {
|
|
|
56
56
|
PreviewLink: 3
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const
|
|
59
|
+
const mockaton = new Commander(location.origin, parseError, onError)
|
|
60
|
+
|
|
61
|
+
const store = /** @type {State} */ {
|
|
60
62
|
brokersByMethod: {},
|
|
61
63
|
staticBrokers: {},
|
|
62
64
|
cookies: [],
|
|
@@ -66,58 +68,145 @@ const state = /** @type {State} */ {
|
|
|
66
68
|
collectProxied: false,
|
|
67
69
|
proxyFallback: '',
|
|
68
70
|
get canProxy() {
|
|
69
|
-
return Boolean(
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
fileFor(method, urlMask) {
|
|
73
|
-
return state.brokersByMethod[method]?.[urlMask]?.currentMock.file
|
|
71
|
+
return Boolean(store.proxyFallback)
|
|
74
72
|
},
|
|
75
73
|
|
|
76
74
|
leftSideWidth: window.innerWidth / 2,
|
|
77
75
|
|
|
78
76
|
groupByMethod: initPreference('groupByMethod'),
|
|
79
77
|
toggleGroupByMethod() {
|
|
80
|
-
|
|
81
|
-
togglePreference('groupByMethod',
|
|
82
|
-
|
|
78
|
+
store.groupByMethod = !store.groupByMethod
|
|
79
|
+
togglePreference('groupByMethod', store.groupByMethod)
|
|
80
|
+
render()
|
|
83
81
|
},
|
|
84
82
|
|
|
85
|
-
chosenLink: {
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
chosenLink: {
|
|
84
|
+
method: '',
|
|
85
|
+
urlMask: ''
|
|
88
86
|
},
|
|
89
87
|
get hasChosenLink() {
|
|
90
|
-
return
|
|
88
|
+
return store.chosenLink.method
|
|
89
|
+
&& store.chosenLink.urlMask
|
|
90
|
+
},
|
|
91
|
+
setChosenLink(method, urlMask) {
|
|
92
|
+
store.chosenLink = { method, urlMask }
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
reset() {
|
|
96
|
+
store.setChosenLink('', '')
|
|
97
|
+
mockaton.reset()
|
|
98
|
+
.then(fetchState)
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
bulkSelectByComment(value) {
|
|
102
|
+
mockaton.bulkSelectByComment(value)
|
|
103
|
+
.then(fetchState)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
setGlobalDelay(value) {
|
|
108
|
+
store.delay = value
|
|
109
|
+
mockaton.setGlobalDelay(value)
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
selectCookie(name) {
|
|
113
|
+
store.cookies = store.cookies.map(([n]) => [n, n === name])
|
|
114
|
+
mockaton.selectCookie(name)
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
setProxyFallback(value) {
|
|
118
|
+
store.proxyFallback = value
|
|
119
|
+
mockaton.setProxyFallback(value)
|
|
120
|
+
.then(render)
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
setCollectProxied(checked) {
|
|
124
|
+
store.collectProxied = checked
|
|
125
|
+
mockaton.setCollectProxied(checked)
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
brokerFor(method, urlMask) { return store.brokersByMethod[method]?.[urlMask] },
|
|
129
|
+
staticBrokerFor(route) { return store.staticBrokers[route] },
|
|
130
|
+
|
|
131
|
+
previewLink(method, urlMask) {
|
|
132
|
+
store.setChosenLink(method, urlMask)
|
|
133
|
+
renderRow(method, urlMask)
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
selectFile(file) {
|
|
137
|
+
mockaton.select(file).then(async response => {
|
|
138
|
+
const { method, urlMask } = parseFilename(file)
|
|
139
|
+
store.brokerFor(method, urlMask).currentMock = await response.json()
|
|
140
|
+
store.setChosenLink(method, urlMask)
|
|
141
|
+
renderRow(method, urlMask)
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
toggle500(method, urlMask) {
|
|
146
|
+
mockaton.toggle500(method, urlMask).then(async response => {
|
|
147
|
+
store.brokerFor(method, urlMask).currentMock = await response.json()
|
|
148
|
+
store.setChosenLink(method, urlMask)
|
|
149
|
+
renderRow(method, urlMask)
|
|
150
|
+
})
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
toggleProxied(method, urlMask, checked) {
|
|
154
|
+
mockaton.setRouteIsProxied(method, urlMask, checked).then(() => {
|
|
155
|
+
store.brokerFor(method, urlMask).currentMock.proxied = checked
|
|
156
|
+
store.setChosenLink(method, urlMask)
|
|
157
|
+
renderRow(method, urlMask)
|
|
158
|
+
})
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
setDelayed(method, urlMask, checked) {
|
|
162
|
+
mockaton.setRouteIsDelayed(method, urlMask, checked).then(() => {
|
|
163
|
+
store.brokerFor(method, urlMask).currentMock.delayed = checked
|
|
164
|
+
})
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
setDelayedStatic(route, checked) {
|
|
169
|
+
store.staticBrokerFor(route).delayed = checked
|
|
170
|
+
mockaton.setStaticRouteIsDelayed(route, checked)
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
setStaticRouteStatus(route, status) {
|
|
174
|
+
store.staticBrokerFor(route).status = status
|
|
175
|
+
mockaton.setStaticRouteStatus(route, status)
|
|
91
176
|
}
|
|
92
177
|
}
|
|
93
178
|
|
|
94
179
|
|
|
95
|
-
|
|
96
|
-
updateState()
|
|
180
|
+
fetchState()
|
|
97
181
|
initRealTimeUpdates()
|
|
98
182
|
initKeyboardNavigation()
|
|
99
183
|
|
|
100
|
-
async function
|
|
184
|
+
async function fetchState() {
|
|
101
185
|
try {
|
|
102
186
|
const response = await mockaton.getState()
|
|
103
187
|
if (!response.ok)
|
|
104
188
|
throw response.status
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const focusedElem = selectorFor(document.activeElement)
|
|
109
|
-
document.body.replaceChildren(...App())
|
|
110
|
-
if (focusedElem)
|
|
111
|
-
document.querySelector(focusedElem)?.focus()
|
|
112
|
-
|
|
113
|
-
if (state.hasChosenLink)
|
|
114
|
-
await previewMock(state.chosenLink.method, state.chosenLink.urlMask)
|
|
189
|
+
Object.assign(store, await response.json())
|
|
190
|
+
render()
|
|
115
191
|
}
|
|
116
192
|
catch (error) {
|
|
117
193
|
onError(error)
|
|
118
194
|
}
|
|
119
195
|
}
|
|
120
196
|
|
|
197
|
+
function render() {
|
|
198
|
+
restoreFocus(() => document.body.replaceChildren(...App()))
|
|
199
|
+
if (store.hasChosenLink)
|
|
200
|
+
previewMock(store.chosenLink.method, store.chosenLink.urlMask)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function restoreFocus(cb) {
|
|
204
|
+
const focusQuery = selectorFor(document.activeElement)
|
|
205
|
+
cb()
|
|
206
|
+
if (focusQuery)
|
|
207
|
+
document.querySelector(focusQuery)?.focus()
|
|
208
|
+
}
|
|
209
|
+
|
|
121
210
|
const r = createElement
|
|
122
211
|
const s = createSvgElement
|
|
123
212
|
const t = translation => translation[0]
|
|
@@ -125,7 +214,7 @@ const t = translation => translation[0]
|
|
|
125
214
|
const leftSideRef = useRef()
|
|
126
215
|
|
|
127
216
|
function App() {
|
|
128
|
-
const { leftSideWidth } =
|
|
217
|
+
const { leftSideWidth } = store
|
|
129
218
|
return [
|
|
130
219
|
Header(),
|
|
131
220
|
r('main', null,
|
|
@@ -162,84 +251,41 @@ function Header() {
|
|
|
162
251
|
SettingsMenuTrigger())))
|
|
163
252
|
}
|
|
164
253
|
|
|
165
|
-
function
|
|
166
|
-
const id = '_settings_menu_'
|
|
167
|
-
return (
|
|
168
|
-
r('button', {
|
|
169
|
-
title: t`Settings`,
|
|
170
|
-
popovertarget: id,
|
|
171
|
-
className: CSS.MenuTrigger
|
|
172
|
-
},
|
|
173
|
-
SettingsIcon(),
|
|
174
|
-
Defer(() => SettingsMenu(id))))
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function SettingsMenu(id) {
|
|
178
|
-
const { groupByMethod, toggleGroupByMethod } = state
|
|
179
|
-
|
|
180
|
-
const firstInputRef = useRef()
|
|
181
|
-
function onToggle(event) {
|
|
182
|
-
if (event.newState === 'open')
|
|
183
|
-
firstInputRef.current.focus()
|
|
184
|
-
}
|
|
185
|
-
return (
|
|
186
|
-
r('menu', {
|
|
187
|
-
id,
|
|
188
|
-
popover: '',
|
|
189
|
-
className: CSS.SettingsMenu,
|
|
190
|
-
onToggle
|
|
191
|
-
},
|
|
192
|
-
|
|
193
|
-
r('label', className(CSS.GroupByMethod),
|
|
194
|
-
r('input', {
|
|
195
|
-
ref: firstInputRef,
|
|
196
|
-
type: 'checkbox',
|
|
197
|
-
checked: groupByMethod,
|
|
198
|
-
onChange: toggleGroupByMethod
|
|
199
|
-
}),
|
|
200
|
-
r('span', null, t`Group by Method`)),
|
|
201
|
-
|
|
202
|
-
r('a', {
|
|
203
|
-
href: 'https://github.com/ericfortis/mockaton',
|
|
204
|
-
target: '_blank',
|
|
205
|
-
rel: 'noopener noreferrer'
|
|
206
|
-
}, t`Documentation`)))
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
function CookieSelector() {
|
|
211
|
-
const { cookies } = state
|
|
254
|
+
function GlobalDelayField() {
|
|
212
255
|
function onChange() {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
256
|
+
store.setGlobalDelay(this.valueAsNumber)
|
|
257
|
+
}
|
|
258
|
+
function onWheel(event) {
|
|
259
|
+
if (event.deltaY > 0)
|
|
260
|
+
this.stepUp()
|
|
261
|
+
else
|
|
262
|
+
this.stepDown()
|
|
263
|
+
clearTimeout(onWheel.timer)
|
|
264
|
+
onWheel.timer = setTimeout(onChange.bind(this), 300)
|
|
216
265
|
}
|
|
217
|
-
const disabled = cookies.length <= 1
|
|
218
|
-
const list = cookies.length ? cookies : [[t`None`, true]]
|
|
219
266
|
return (
|
|
220
|
-
r('label', className(CSS.Field, CSS.
|
|
221
|
-
r('span', null, t`
|
|
222
|
-
r('
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
267
|
+
r('label', className(CSS.Field, CSS.GlobalDelayField),
|
|
268
|
+
r('span', null, t`Delay (ms)`),
|
|
269
|
+
r('input', {
|
|
270
|
+
type: 'number',
|
|
271
|
+
min: 0,
|
|
272
|
+
step: 100,
|
|
273
|
+
autocomplete: 'none',
|
|
274
|
+
value: store.delay,
|
|
275
|
+
onChange,
|
|
276
|
+
onWheel: [onWheel, { passive: true }]
|
|
277
|
+
})))
|
|
229
278
|
}
|
|
230
279
|
|
|
231
280
|
function BulkSelector() {
|
|
232
|
-
const { comments } =
|
|
281
|
+
const { comments } = store
|
|
233
282
|
// UX wise this should be a menu instead of this `select`.
|
|
234
283
|
// But this way is easier to implement, with a few hacks.
|
|
235
284
|
const firstOption = t`Pick Comment…`
|
|
236
285
|
function onChange() {
|
|
237
286
|
const value = this.value
|
|
238
287
|
this.value = firstOption // Hack
|
|
239
|
-
|
|
240
|
-
.then(parseError)
|
|
241
|
-
.then(updateState)
|
|
242
|
-
.catch(onError)
|
|
288
|
+
store.bulkSelectByComment(value)
|
|
243
289
|
}
|
|
244
290
|
const disabled = !comments.length
|
|
245
291
|
return (
|
|
@@ -254,45 +300,29 @@ function BulkSelector() {
|
|
|
254
300
|
},
|
|
255
301
|
r('option', { value: firstOption }, firstOption),
|
|
256
302
|
r('hr'),
|
|
257
|
-
comments.map(value =>
|
|
258
|
-
r('option', { value }, value)),
|
|
303
|
+
comments.map(value => r('option', { value }, value)),
|
|
259
304
|
r('hr'),
|
|
260
305
|
r('option', { value: AUTOGENERATED_500_COMMENT }, t`Auto500`)
|
|
261
306
|
)))
|
|
262
307
|
}
|
|
263
308
|
|
|
264
|
-
function
|
|
265
|
-
const {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
mockaton.setGlobalDelay(state.delay)
|
|
269
|
-
.then(parseError)
|
|
270
|
-
.catch(onError)
|
|
271
|
-
}
|
|
272
|
-
function onWheel(event) {
|
|
273
|
-
if (event.deltaY > 0)
|
|
274
|
-
this.stepUp()
|
|
275
|
-
else
|
|
276
|
-
this.stepDown()
|
|
277
|
-
clearTimeout(onWheel.timer)
|
|
278
|
-
onWheel.timer = setTimeout(onChange.bind(this), 300)
|
|
279
|
-
}
|
|
309
|
+
function CookieSelector() {
|
|
310
|
+
const { cookies } = store
|
|
311
|
+
const disabled = cookies.length <= 1
|
|
312
|
+
const list = cookies.length ? cookies : [[t`None`, true]]
|
|
280
313
|
return (
|
|
281
|
-
r('label', className(CSS.Field, CSS.
|
|
282
|
-
r('span', null, t`
|
|
283
|
-
r('
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
onWheel: [onWheel, { passive: true }]
|
|
291
|
-
})))
|
|
314
|
+
r('label', className(CSS.Field, CSS.CookieSelector),
|
|
315
|
+
r('span', null, t`Cookie`),
|
|
316
|
+
r('select', {
|
|
317
|
+
autocomplete: 'off',
|
|
318
|
+
disabled,
|
|
319
|
+
title: disabled ? t`No cookies specified in config.cookies` : '',
|
|
320
|
+
onChange() { store.selectCookie(this.value) }
|
|
321
|
+
}, list.map(([value, selected]) =>
|
|
322
|
+
r('option', { value, selected }, value)))))
|
|
292
323
|
}
|
|
293
324
|
|
|
294
325
|
function ProxyFallbackField() {
|
|
295
|
-
const { proxyFallback } = state
|
|
296
326
|
const checkboxRef = useRef()
|
|
297
327
|
function onChange() {
|
|
298
328
|
checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
|
|
@@ -300,10 +330,7 @@ function ProxyFallbackField() {
|
|
|
300
330
|
if (!this.validity.valid)
|
|
301
331
|
this.reportValidity()
|
|
302
332
|
else
|
|
303
|
-
|
|
304
|
-
.then(parseError)
|
|
305
|
-
.then(updateState)
|
|
306
|
-
.catch(onError)
|
|
333
|
+
store.setProxyFallback(this.value.trim())
|
|
307
334
|
}
|
|
308
335
|
return (
|
|
309
336
|
r('div', className(CSS.Field, CSS.FallbackBackend),
|
|
@@ -313,92 +340,103 @@ function ProxyFallbackField() {
|
|
|
313
340
|
type: 'url',
|
|
314
341
|
autocomplete: 'none',
|
|
315
342
|
placeholder: t`Type backend address`,
|
|
316
|
-
value: proxyFallback,
|
|
343
|
+
value: store.proxyFallback,
|
|
317
344
|
onChange
|
|
318
345
|
})),
|
|
319
346
|
SaveProxiedCheckbox(checkboxRef)))
|
|
320
347
|
}
|
|
321
348
|
|
|
322
349
|
function SaveProxiedCheckbox(ref) {
|
|
323
|
-
const { collectProxied, canProxy } = state
|
|
324
|
-
function onChange() {
|
|
325
|
-
mockaton.setCollectProxied(this.checked)
|
|
326
|
-
.then(parseError)
|
|
327
|
-
.catch(onError)
|
|
328
|
-
}
|
|
329
350
|
return (
|
|
330
351
|
r('label', className(CSS.SaveProxiedCheckbox),
|
|
331
352
|
r('input', {
|
|
332
353
|
ref,
|
|
333
354
|
type: 'checkbox',
|
|
334
|
-
disabled: !canProxy,
|
|
335
|
-
checked: collectProxied,
|
|
336
|
-
onChange
|
|
355
|
+
disabled: !store.canProxy,
|
|
356
|
+
checked: store.collectProxied,
|
|
357
|
+
onChange() { store.setCollectProxied(this.checked) }
|
|
337
358
|
}),
|
|
338
359
|
r('span', null, t`Save Mocks`)))
|
|
339
360
|
}
|
|
340
361
|
|
|
341
362
|
function ResetButton() {
|
|
342
|
-
function onClick() {
|
|
343
|
-
state.setChosenLink('', '')
|
|
344
|
-
mockaton.reset()
|
|
345
|
-
.then(parseError)
|
|
346
|
-
.then(updateState)
|
|
347
|
-
.catch(onError)
|
|
348
|
-
}
|
|
349
363
|
return (
|
|
350
364
|
r('button', {
|
|
351
365
|
className: CSS.ResetButton,
|
|
352
|
-
onClick
|
|
366
|
+
onClick: store.reset
|
|
353
367
|
}, t`Reset`))
|
|
354
368
|
}
|
|
355
369
|
|
|
370
|
+
function SettingsMenuTrigger() {
|
|
371
|
+
const id = '_settings_menu_'
|
|
372
|
+
return (
|
|
373
|
+
r('button', {
|
|
374
|
+
title: t`Settings`,
|
|
375
|
+
popovertarget: id,
|
|
376
|
+
className: CSS.MenuTrigger
|
|
377
|
+
},
|
|
378
|
+
SettingsIcon(),
|
|
379
|
+
Defer(() => SettingsMenu(id))))
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function SettingsMenu(id) {
|
|
383
|
+
const firstInputRef = useRef()
|
|
384
|
+
function onToggle(event) {
|
|
385
|
+
if (event.newState === 'open')
|
|
386
|
+
firstInputRef.current.focus()
|
|
387
|
+
}
|
|
388
|
+
return (
|
|
389
|
+
r('menu', {
|
|
390
|
+
id,
|
|
391
|
+
popover: '',
|
|
392
|
+
className: CSS.SettingsMenu,
|
|
393
|
+
onToggle
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
r('label', className(CSS.GroupByMethod),
|
|
397
|
+
r('input', {
|
|
398
|
+
ref: firstInputRef,
|
|
399
|
+
type: 'checkbox',
|
|
400
|
+
checked: store.groupByMethod,
|
|
401
|
+
onChange: store.toggleGroupByMethod
|
|
402
|
+
}),
|
|
403
|
+
r('span', null, t`Group by Method`)),
|
|
404
|
+
|
|
405
|
+
r('a', {
|
|
406
|
+
href: 'https://github.com/ericfortis/mockaton',
|
|
407
|
+
target: '_blank',
|
|
408
|
+
rel: 'noopener noreferrer'
|
|
409
|
+
}, t`Documentation`)))
|
|
410
|
+
}
|
|
411
|
+
|
|
356
412
|
|
|
357
413
|
|
|
358
414
|
/** # MockList */
|
|
359
415
|
|
|
360
416
|
function MockList() {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (!Object.keys(brokersByMethod).length)
|
|
417
|
+
if (!Object.keys(store.brokersByMethod).length)
|
|
364
418
|
return (
|
|
365
419
|
r('div', className(CSS.empty),
|
|
366
420
|
t`No mocks found`))
|
|
367
421
|
|
|
368
|
-
if (groupByMethod)
|
|
369
|
-
return Object.keys(brokersByMethod).map(method => Fragment(
|
|
422
|
+
if (store.groupByMethod)
|
|
423
|
+
return Object.keys(store.brokersByMethod).map(method => Fragment(
|
|
370
424
|
r('tr', null,
|
|
371
|
-
r('th', { colspan: 2 + Number(canProxy) }),
|
|
425
|
+
r('th', { colspan: 2 + Number(store.canProxy) }),
|
|
372
426
|
r('th', null, method)),
|
|
373
427
|
rowsFor(method).map(Row)))
|
|
374
428
|
|
|
375
429
|
return rowsFor('*').map(Row)
|
|
376
430
|
}
|
|
377
431
|
|
|
378
|
-
|
|
379
|
-
function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
|
|
380
|
-
const { canProxy, groupByMethod } = state
|
|
381
|
-
return (
|
|
382
|
-
r('tr', { key: method + '::' + urlMask },
|
|
383
|
-
canProxy && r('td', null, ProxyToggler(broker)),
|
|
384
|
-
r('td', null, DelayRouteToggler(broker)),
|
|
385
|
-
r('td', null, InternalServerErrorToggler(broker)),
|
|
386
|
-
!groupByMethod && r('td', className(CSS.Method), method),
|
|
387
|
-
r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
|
|
388
|
-
r('td', null, MockSelector(broker))))
|
|
389
|
-
}
|
|
390
|
-
|
|
391
432
|
function rowsFor(targetMethod) {
|
|
392
|
-
const { brokersByMethod } = state
|
|
393
|
-
|
|
394
433
|
const rows = []
|
|
395
|
-
for (const [method, brokers] of Object.entries(brokersByMethod))
|
|
434
|
+
for (const [method, brokers] of Object.entries(store.brokersByMethod))
|
|
396
435
|
if (targetMethod === '*' || targetMethod === method)
|
|
397
436
|
for (const [urlMask, broker] of Object.entries(brokers))
|
|
398
437
|
rows.push({ method, urlMask, broker })
|
|
399
438
|
|
|
400
439
|
const sorted = rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
401
|
-
|
|
402
440
|
const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
|
|
403
441
|
return sorted.map((r, i) => ({
|
|
404
442
|
...r,
|
|
@@ -406,22 +444,50 @@ function rowsFor(targetMethod) {
|
|
|
406
444
|
}))
|
|
407
445
|
}
|
|
408
446
|
|
|
447
|
+
function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
|
|
448
|
+
const key = Row.key(method, urlMask)
|
|
449
|
+
Row.ditto.set(key, urlMaskDittoed)
|
|
450
|
+
return (
|
|
451
|
+
r('tr', { key },
|
|
452
|
+
store.canProxy && r('td', null, ProxyToggler(broker)),
|
|
453
|
+
r('td', null, DelayRouteToggler(broker)),
|
|
454
|
+
r('td', null, InternalServerErrorToggler(broker)),
|
|
455
|
+
!store.groupByMethod && r('td', className(CSS.Method), method),
|
|
456
|
+
r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
|
|
457
|
+
r('td', null, MockSelector(broker))))
|
|
458
|
+
}
|
|
459
|
+
Row.key = (method, urlMask) => method + '::' + urlMask
|
|
460
|
+
Row.ditto = new Map()
|
|
461
|
+
|
|
462
|
+
function renderRow(method, urlMask) {
|
|
463
|
+
restoreFocus(() => {
|
|
464
|
+
unChooseOld()
|
|
465
|
+
const key = Row.key(method, urlMask)
|
|
466
|
+
trFor(key).replaceWith(Row({
|
|
467
|
+
method,
|
|
468
|
+
urlMask,
|
|
469
|
+
urlMaskDittoed: Row.ditto.get(key),
|
|
470
|
+
broker: store.brokerFor(method, urlMask)
|
|
471
|
+
}))
|
|
472
|
+
previewMock(method, urlMask)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
function trFor(key) {
|
|
476
|
+
return document.querySelector(`body > main > .${CSS.leftSide} tr[key="${key}"]`)
|
|
477
|
+
}
|
|
478
|
+
function unChooseOld() {
|
|
479
|
+
return document.querySelector(`body > main > .${CSS.leftSide} tr .${CSS.chosen}`)
|
|
480
|
+
?.classList.remove(CSS.chosen)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
409
484
|
|
|
410
485
|
function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
|
|
411
|
-
|
|
486
|
+
function onClick(event) {
|
|
412
487
|
event.preventDefault()
|
|
413
|
-
|
|
414
|
-
findChosenLink()?.classList.remove(CSS.chosen)
|
|
415
|
-
this.classList.add(CSS.chosen)
|
|
416
|
-
state.setChosenLink(method, urlMask)
|
|
417
|
-
await previewMock(method, urlMask)
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
onError(error)
|
|
421
|
-
}
|
|
488
|
+
store.previewLink(method, urlMask)
|
|
422
489
|
}
|
|
423
|
-
const
|
|
424
|
-
const isChosen = chosenLink.method === method && chosenLink.urlMask === urlMask
|
|
490
|
+
const isChosen = store.chosenLink.method === method && store.chosenLink.urlMask === urlMask
|
|
425
491
|
const [ditto, tail] = urlMaskDittoed
|
|
426
492
|
return (
|
|
427
493
|
r('a', {
|
|
@@ -435,30 +501,18 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
|
|
|
435
501
|
: tail))
|
|
436
502
|
}
|
|
437
503
|
|
|
438
|
-
function findChosenLink() {
|
|
439
|
-
return document.querySelector(
|
|
440
|
-
`body > main > .${CSS.leftSide} .${CSS.PreviewLink}.${CSS.chosen}`)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const STR_PROXIED = t`Proxied`
|
|
444
504
|
|
|
445
505
|
/** @param {ClientMockBroker} broker */
|
|
446
506
|
function MockSelector(broker) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
state.setChosenLink(method, urlMask)
|
|
450
|
-
mockaton.select(this.value)
|
|
451
|
-
.then(parseError)
|
|
452
|
-
.then(updateState)
|
|
453
|
-
.catch(onError)
|
|
454
|
-
}
|
|
455
|
-
|
|
507
|
+
const STR_PROXIED = t`Proxied`
|
|
508
|
+
|
|
456
509
|
let selected = broker.currentMock.file
|
|
457
510
|
const { status } = parseFilename(selected)
|
|
458
511
|
const files = broker.mocks.filter(item =>
|
|
459
512
|
status === 500 ||
|
|
460
513
|
!item.includes(AUTOGENERATED_500_COMMENT))
|
|
461
|
-
|
|
514
|
+
|
|
515
|
+
if (store.canProxy && broker.currentMock.proxied) {
|
|
462
516
|
selected = STR_PROXIED
|
|
463
517
|
files.push(selected)
|
|
464
518
|
}
|
|
@@ -478,7 +532,7 @@ function MockSelector(broker) {
|
|
|
478
532
|
|
|
479
533
|
return (
|
|
480
534
|
r('select', {
|
|
481
|
-
onChange,
|
|
535
|
+
onChange() { store.selectFile(this.value) },
|
|
482
536
|
autocomplete: 'off',
|
|
483
537
|
'aria-label': t`Mock Selector`,
|
|
484
538
|
disabled: files.length <= 1,
|
|
@@ -497,9 +551,7 @@ function MockSelector(broker) {
|
|
|
497
551
|
function DelayRouteToggler(broker) {
|
|
498
552
|
function commit(checked) {
|
|
499
553
|
const { method, urlMask } = parseFilename(broker.mocks[0])
|
|
500
|
-
|
|
501
|
-
.then(parseError)
|
|
502
|
-
.catch(onError)
|
|
554
|
+
store.setDelayed(method, urlMask, checked)
|
|
503
555
|
}
|
|
504
556
|
return ClickDragToggler({
|
|
505
557
|
checked: broker.currentMock.delayed,
|
|
@@ -513,11 +565,7 @@ function DelayRouteToggler(broker) {
|
|
|
513
565
|
function InternalServerErrorToggler(broker) {
|
|
514
566
|
function onChange() {
|
|
515
567
|
const { method, urlMask } = parseFilename(broker.mocks[0])
|
|
516
|
-
|
|
517
|
-
mockaton.toggle500(method, urlMask)
|
|
518
|
-
.then(parseError)
|
|
519
|
-
.then(updateState)
|
|
520
|
-
.catch(onError)
|
|
568
|
+
store.toggle500(method, urlMask)
|
|
521
569
|
}
|
|
522
570
|
return (
|
|
523
571
|
r('label', {
|
|
@@ -527,7 +575,6 @@ function InternalServerErrorToggler(broker) {
|
|
|
527
575
|
r('input', {
|
|
528
576
|
type: 'checkbox',
|
|
529
577
|
'data-focus-group': FocusGroup.StatusToggler,
|
|
530
|
-
name: broker.currentMock.file,
|
|
531
578
|
checked: parseFilename(broker.currentMock.file).status === 500,
|
|
532
579
|
onChange
|
|
533
580
|
}),
|
|
@@ -538,11 +585,7 @@ function InternalServerErrorToggler(broker) {
|
|
|
538
585
|
function ProxyToggler(broker) {
|
|
539
586
|
function onChange() {
|
|
540
587
|
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
541
|
-
|
|
542
|
-
mockaton.setRouteIsProxied(method, urlMask, this.checked)
|
|
543
|
-
.then(parseError)
|
|
544
|
-
.then(updateState)
|
|
545
|
-
.catch(onError)
|
|
588
|
+
store.toggleProxied(method, urlMask, this.checked)
|
|
546
589
|
}
|
|
547
590
|
return (
|
|
548
591
|
r('label', {
|
|
@@ -551,7 +594,7 @@ function ProxyToggler(broker) {
|
|
|
551
594
|
},
|
|
552
595
|
r('input', {
|
|
553
596
|
type: 'checkbox',
|
|
554
|
-
checked:
|
|
597
|
+
checked: broker.currentMock.proxied,
|
|
555
598
|
onChange,
|
|
556
599
|
'data-focus-group': FocusGroup.ProxyToggler
|
|
557
600
|
}),
|
|
@@ -563,7 +606,7 @@ function ProxyToggler(broker) {
|
|
|
563
606
|
/** # StaticFilesList */
|
|
564
607
|
|
|
565
608
|
function StaticFilesList() {
|
|
566
|
-
const { staticBrokers, canProxy, groupByMethod } =
|
|
609
|
+
const { staticBrokers, canProxy, groupByMethod } = store
|
|
567
610
|
if (!Object.keys(staticBrokers).length)
|
|
568
611
|
return null
|
|
569
612
|
const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
|
|
@@ -591,25 +634,17 @@ function StaticFilesList() {
|
|
|
591
634
|
|
|
592
635
|
/** @param {ClientStaticBroker} broker */
|
|
593
636
|
function DelayStaticRouteToggler(broker) {
|
|
594
|
-
function commit(checked) {
|
|
595
|
-
mockaton.setStaticRouteIsDelayed(broker.route, checked)
|
|
596
|
-
.then(parseError)
|
|
597
|
-
.catch(onError)
|
|
598
|
-
}
|
|
599
637
|
return ClickDragToggler({
|
|
600
638
|
checked: broker.delayed,
|
|
601
|
-
|
|
602
|
-
|
|
639
|
+
focusGroup: FocusGroup.DelayToggler,
|
|
640
|
+
commit(checked) {
|
|
641
|
+
store.setDelayedStatic(broker.route, checked)
|
|
642
|
+
}
|
|
603
643
|
})
|
|
604
644
|
}
|
|
605
645
|
|
|
606
646
|
/** @param {ClientStaticBroker} broker */
|
|
607
647
|
function NotFoundToggler(broker) {
|
|
608
|
-
function onChange() {
|
|
609
|
-
mockaton.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
|
|
610
|
-
.then(parseError)
|
|
611
|
-
.catch(onError)
|
|
612
|
-
}
|
|
613
648
|
return (
|
|
614
649
|
r('label', {
|
|
615
650
|
className: CSS.NotFoundToggler,
|
|
@@ -618,8 +653,10 @@ function NotFoundToggler(broker) {
|
|
|
618
653
|
r('input', {
|
|
619
654
|
type: 'checkbox',
|
|
620
655
|
checked: broker.status === 404,
|
|
621
|
-
|
|
622
|
-
|
|
656
|
+
'data-focus-group': FocusGroup.StatusToggler,
|
|
657
|
+
onChange() {
|
|
658
|
+
store.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
|
|
659
|
+
}
|
|
623
660
|
}),
|
|
624
661
|
r('span', null, t`404`)))
|
|
625
662
|
}
|
|
@@ -659,7 +696,6 @@ function ClickDragToggler({ checked, commit, focusGroup }) {
|
|
|
659
696
|
TimerIcon()))
|
|
660
697
|
}
|
|
661
698
|
|
|
662
|
-
|
|
663
699
|
function Resizer() {
|
|
664
700
|
let raf = 0
|
|
665
701
|
let initialX = 0
|
|
@@ -680,8 +716,8 @@ function Resizer() {
|
|
|
680
716
|
function onMove(event) {
|
|
681
717
|
const MIN_LEFT_WIDTH = 380
|
|
682
718
|
raf = raf || requestAnimationFrame(() => {
|
|
683
|
-
|
|
684
|
-
leftSideRef.current.style.width =
|
|
719
|
+
store.leftSideWidth = Math.max(panelWidth - (initialX - event.clientX), MIN_LEFT_WIDTH)
|
|
720
|
+
leftSideRef.current.style.width = store.leftSideWidth + 'px'
|
|
685
721
|
raf = 0
|
|
686
722
|
})
|
|
687
723
|
}
|
|
@@ -711,14 +747,13 @@ const payloadViewerTitleRef = useRef()
|
|
|
711
747
|
const payloadViewerCodeRef = useRef()
|
|
712
748
|
|
|
713
749
|
function PayloadViewer() {
|
|
714
|
-
const { hasChosenLink } = state
|
|
715
750
|
return (
|
|
716
751
|
r('div', className(CSS.PayloadViewer),
|
|
717
752
|
r('h2', { ref: payloadViewerTitleRef },
|
|
718
|
-
!hasChosenLink && t`Preview`),
|
|
753
|
+
!store.hasChosenLink && t`Preview`),
|
|
719
754
|
r('pre', null,
|
|
720
755
|
r('code', { ref: payloadViewerCodeRef },
|
|
721
|
-
!hasChosenLink && t`Click a link to preview it`))))
|
|
756
|
+
!store.hasChosenLink && t`Click a link to preview it`))))
|
|
722
757
|
}
|
|
723
758
|
|
|
724
759
|
function PayloadViewerTitle({ file, statusText }) {
|
|
@@ -745,7 +780,7 @@ const SPINNER_DELAY = 80
|
|
|
745
780
|
function PayloadViewerProgressBar() {
|
|
746
781
|
return (
|
|
747
782
|
r('div', className(CSS.ProgressBar),
|
|
748
|
-
r('div', { style: { animationDuration:
|
|
783
|
+
r('div', { style: { animationDuration: store.delay - SPINNER_DELAY + 'ms' } })))
|
|
749
784
|
}
|
|
750
785
|
|
|
751
786
|
async function previewMock(method, urlMask) {
|
|
@@ -763,11 +798,11 @@ async function previewMock(method, urlMask) {
|
|
|
763
798
|
signal: previewMock.controller.signal
|
|
764
799
|
})
|
|
765
800
|
clearTimeout(spinnerTimer)
|
|
766
|
-
const file =
|
|
767
|
-
if (
|
|
768
|
-
await updatePayloadViewer(
|
|
801
|
+
const { proxied, file } = store.brokerFor(method, urlMask).currentMock
|
|
802
|
+
if (proxied)
|
|
803
|
+
await updatePayloadViewer(true, '', response)
|
|
769
804
|
else if (file)
|
|
770
|
-
await updatePayloadViewer(file, response)
|
|
805
|
+
await updatePayloadViewer(false, file, response)
|
|
771
806
|
else {/* e.g. selected was deleted */}
|
|
772
807
|
}
|
|
773
808
|
catch (err) {
|
|
@@ -776,11 +811,11 @@ async function previewMock(method, urlMask) {
|
|
|
776
811
|
}
|
|
777
812
|
}
|
|
778
813
|
|
|
779
|
-
async function updatePayloadViewer(file, response) {
|
|
814
|
+
async function updatePayloadViewer(proxied, file, response) {
|
|
780
815
|
const mime = response.headers.get('content-type') || ''
|
|
781
816
|
|
|
782
817
|
payloadViewerTitleRef.current.replaceChildren(
|
|
783
|
-
|
|
818
|
+
proxied
|
|
784
819
|
? PayloadViewerTitleWhenProxied({
|
|
785
820
|
mime,
|
|
786
821
|
status: response.status,
|
|
@@ -794,9 +829,7 @@ async function updatePayloadViewer(file, response) {
|
|
|
794
829
|
|
|
795
830
|
if (mime.startsWith('image/')) // Naively assumes GET.200
|
|
796
831
|
payloadViewerCodeRef.current.replaceChildren(
|
|
797
|
-
r('img', {
|
|
798
|
-
src: URL.createObjectURL(await response.blob())
|
|
799
|
-
}))
|
|
832
|
+
r('img', { src: URL.createObjectURL(await response.blob()) }))
|
|
800
833
|
else {
|
|
801
834
|
const body = await response.text() || t`/* Empty Response Body */`
|
|
802
835
|
if (mime === 'application/json')
|
|
@@ -814,55 +847,11 @@ function isXML(mime) {
|
|
|
814
847
|
}
|
|
815
848
|
|
|
816
849
|
|
|
817
|
-
function initKeyboardNavigation() {
|
|
818
|
-
addEventListener('keydown', onKeyDown)
|
|
819
|
-
|
|
820
|
-
function onKeyDown(event) {
|
|
821
|
-
const pivot = document.activeElement
|
|
822
|
-
switch (event.key) {
|
|
823
|
-
case 'ArrowDown':
|
|
824
|
-
case 'ArrowUp': {
|
|
825
|
-
let fg = pivot.getAttribute('data-focus-group')
|
|
826
|
-
if (fg !== null) {
|
|
827
|
-
const offset = event.key === 'ArrowDown' ? +1 : -1
|
|
828
|
-
circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
|
|
829
|
-
}
|
|
830
|
-
break
|
|
831
|
-
}
|
|
832
|
-
case 'ArrowRight':
|
|
833
|
-
case 'ArrowLeft': {
|
|
834
|
-
if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
|
|
835
|
-
const offset = event.key === 'ArrowRight' ? +1 : -1
|
|
836
|
-
rowFocusable(pivot, offset).focus()
|
|
837
|
-
}
|
|
838
|
-
break
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
function rowFocusable(el, step) {
|
|
844
|
-
const row = el.closest('tr')
|
|
845
|
-
if (row) {
|
|
846
|
-
const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
|
|
847
|
-
return circularAdjacent(step, focusables, el)
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function allInFocusGroup(focusGroup) {
|
|
852
|
-
return Array.from(document.querySelectorAll(
|
|
853
|
-
`body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function circularAdjacent(step = 1, arr, pivot) {
|
|
857
|
-
return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
850
|
/** # Error */
|
|
862
851
|
|
|
863
852
|
async function parseError(response) {
|
|
864
853
|
if (response.ok)
|
|
865
|
-
return
|
|
854
|
+
return response
|
|
866
855
|
if (response.status === 422)
|
|
867
856
|
throw await response.text()
|
|
868
857
|
throw response.statusText
|
|
@@ -939,7 +928,7 @@ function initRealTimeUpdates() {
|
|
|
939
928
|
if (oldSyncVersion !== syncVersion) { // because it could be < or >
|
|
940
929
|
oldSyncVersion = syncVersion
|
|
941
930
|
if (!skipUpdate)
|
|
942
|
-
await
|
|
931
|
+
await fetchState()
|
|
943
932
|
}
|
|
944
933
|
poll()
|
|
945
934
|
}
|
|
@@ -953,6 +942,50 @@ function initRealTimeUpdates() {
|
|
|
953
942
|
}
|
|
954
943
|
}
|
|
955
944
|
|
|
945
|
+
function initKeyboardNavigation() {
|
|
946
|
+
addEventListener('keydown', onKeyDown)
|
|
947
|
+
|
|
948
|
+
function onKeyDown(event) {
|
|
949
|
+
const pivot = document.activeElement
|
|
950
|
+
switch (event.key) {
|
|
951
|
+
case 'ArrowDown':
|
|
952
|
+
case 'ArrowUp': {
|
|
953
|
+
let fg = pivot.getAttribute('data-focus-group')
|
|
954
|
+
if (fg !== null) {
|
|
955
|
+
const offset = event.key === 'ArrowDown' ? +1 : -1
|
|
956
|
+
circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
|
|
957
|
+
}
|
|
958
|
+
break
|
|
959
|
+
}
|
|
960
|
+
case 'ArrowRight':
|
|
961
|
+
case 'ArrowLeft': {
|
|
962
|
+
if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
|
|
963
|
+
const offset = event.key === 'ArrowRight' ? +1 : -1
|
|
964
|
+
rowFocusable(pivot, offset).focus()
|
|
965
|
+
}
|
|
966
|
+
break
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function rowFocusable(el, step) {
|
|
972
|
+
const row = el.closest('tr')
|
|
973
|
+
if (row) {
|
|
974
|
+
const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
|
|
975
|
+
return circularAdjacent(step, focusables, el)
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function allInFocusGroup(focusGroup) {
|
|
980
|
+
return Array.from(document.querySelectorAll(
|
|
981
|
+
`body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function circularAdjacent(step = 1, arr, pivot) {
|
|
985
|
+
return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
956
989
|
|
|
957
990
|
/** # Utils */
|
|
958
991
|
|
|
@@ -1119,7 +1152,6 @@ dittoSplitPaths.test = function () {
|
|
|
1119
1152
|
}
|
|
1120
1153
|
|
|
1121
1154
|
|
|
1122
|
-
|
|
1123
1155
|
function syntaxJSON(json) {
|
|
1124
1156
|
const MAX_NODES = 50_000
|
|
1125
1157
|
let nNodes = 0
|
|
@@ -1167,7 +1199,6 @@ syntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s
|
|
|
1167
1199
|
// Capture group order: [string, optional colon, punc]
|
|
1168
1200
|
|
|
1169
1201
|
|
|
1170
|
-
|
|
1171
1202
|
function syntaxXML(xml) {
|
|
1172
1203
|
const MAX_NODES = 50_000
|
|
1173
1204
|
let nNodes = 0
|
package/src/MockBroker.js
CHANGED
|
@@ -10,15 +10,17 @@ export class MockBroker {
|
|
|
10
10
|
this.mocks = []
|
|
11
11
|
this.currentMock = {
|
|
12
12
|
file: '',
|
|
13
|
-
delayed: false
|
|
13
|
+
delayed: false,
|
|
14
|
+
proxied: false
|
|
14
15
|
}
|
|
15
16
|
this.register(file)
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
get file() { return this.currentMock.file }
|
|
19
|
-
get status() { return parseFilename(this.file).status }
|
|
20
20
|
get delayed() { return this.currentMock.delayed }
|
|
21
|
-
get proxied() { return
|
|
21
|
+
get proxied() { return this.currentMock.proxied }
|
|
22
|
+
|
|
23
|
+
get status() { return parseFilename(this.file).status }
|
|
22
24
|
get temp500IsSelected() { return this.#isTemp500(this.file) }
|
|
23
25
|
|
|
24
26
|
hasMock(file) { return this.mocks.includes(file) }
|
|
@@ -80,25 +82,18 @@ export class MockBroker {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
selectFile(filename) {
|
|
85
|
+
this.currentMock.proxied = false
|
|
83
86
|
this.currentMock.file = filename
|
|
84
87
|
}
|
|
85
|
-
|
|
88
|
+
|
|
86
89
|
toggle500() {
|
|
87
90
|
this.#is500(this.currentMock.file)
|
|
88
91
|
? this.selectDefaultFile()
|
|
89
92
|
: this.selectFile(this.mocks.find(this.#is500))
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
setDelayed(delayed) {
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
setProxied(proxied) {
|
|
97
|
-
if (proxied)
|
|
98
|
-
this.selectFile('')
|
|
99
|
-
else
|
|
100
|
-
this.selectDefaultFile()
|
|
101
|
-
}
|
|
95
|
+
setDelayed(delayed) { this.currentMock.delayed = delayed }
|
|
96
|
+
setProxied(proxied) { this.currentMock.proxied = proxied }
|
|
102
97
|
|
|
103
98
|
setByMatchingComment(comment) {
|
|
104
99
|
for (const file of this.mocks)
|
package/src/MockDispatcher.js
CHANGED
|
@@ -13,15 +13,19 @@ import { sendInternalServerError, sendMockNotFound } from './utils/http-response
|
|
|
13
13
|
|
|
14
14
|
export async function dispatchMock(req, response) {
|
|
15
15
|
try {
|
|
16
|
-
let broker = mockBrokerCollection.brokerByRoute(req.method, req.url)
|
|
17
16
|
const isHead = req.method === 'HEAD'
|
|
17
|
+
|
|
18
|
+
let broker = mockBrokerCollection.brokerByRoute(req.method, req.url)
|
|
18
19
|
if (!broker && isHead)
|
|
19
20
|
broker = mockBrokerCollection.brokerByRoute('GET', req.url)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
|
|
22
|
+
if (config.proxyFallback && (!broker || broker.proxied)) {
|
|
23
|
+
await proxy(req, response, broker?.delayed ? calcDelay() : 0)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!broker) {
|
|
28
|
+
sendMockNotFound(response)
|
|
25
29
|
return
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -125,13 +125,6 @@ export function setMocksMatchingComment(comment) {
|
|
|
125
125
|
broker.setByMatchingComment(comment))
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export function ensureAllRoutesHaveSelectedMock() {
|
|
129
|
-
forEachBroker(broker => {
|
|
130
|
-
if (broker.proxied)
|
|
131
|
-
broker.selectDefaultFile()
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
128
|
function forEachBroker(fn) {
|
|
136
129
|
for (const brokers of Object.values(collection))
|
|
137
130
|
Object.values(brokers).forEach(fn)
|