mockaton 13.11.0 → 13.11.2

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.
@@ -0,0 +1,340 @@
1
+ .SubToolbar {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: space-between;
5
+ padding-right: 14px;
6
+ padding-left: 16px;
7
+ border-bottom: 1px solid var(--colorBorder);
8
+ background: var(--colorBgHeader);
9
+ }
10
+
11
+ .GroupByMethod {
12
+ display: flex;
13
+ align-items: center;
14
+ gap: 6px;
15
+ cursor: pointer;
16
+ }
17
+
18
+ .BulkSelector {
19
+ span {
20
+ color: var(--colorLabel);
21
+ }
22
+ select {
23
+ width: 110px;
24
+ padding: 6px 8px;
25
+ margin-left: 4px;
26
+ background-image: none;
27
+ text-align-last: center;
28
+ color: var(--colorText);
29
+ font-size: 11px;
30
+ background-color: var(--colorBgHeaderField);
31
+ border-radius: var(--radius);
32
+ box-shadow: var(--boxShadowRimDrop);
33
+ transition: background-color ease-in-out 120ms;
34
+
35
+ &:hover {
36
+ background-color: var(--colorBgFieldHover);
37
+ }
38
+ }
39
+ }
40
+
41
+ .Table {
42
+ height: 100%;
43
+ padding: 16px 16px 64px 15px;
44
+ user-select: none;
45
+ overflow-y: auto;
46
+
47
+ .TableHeading {
48
+ padding-bottom: 4px;
49
+ padding-left: 1px;
50
+ border-top: 24px solid transparent;
51
+ margin-left: 94px;
52
+ font-weight: bold;
53
+ text-align: left;
54
+
55
+ &:first-of-type {
56
+ border-top: 0;
57
+ }
58
+
59
+ &.canProxy {
60
+ margin-left: 124px;
61
+ }
62
+ }
63
+
64
+ .TableRow {
65
+ display: flex;
66
+ margin-left: 24px;
67
+
68
+ &.animIn {
69
+ opacity: 0;
70
+ transform: scaleY(0);
71
+ animation: _kfRowIn 180ms ease-in-out forwards;
72
+ }
73
+ }
74
+
75
+ .FolderGroup {
76
+ position: relative;
77
+ border-radius: var(--radius);
78
+
79
+ > summary {
80
+ left: 0;
81
+ display: flex;
82
+ overflow: hidden;
83
+ align-items: center;
84
+ padding: 5px 0;
85
+ font-weight: 500;
86
+ font-size: 12px;
87
+ list-style: none;
88
+ cursor: pointer;
89
+ user-select: none;
90
+ color: var(--colorLabel);
91
+ white-space: nowrap;
92
+ text-overflow: ellipsis;
93
+ border-radius: var(--radius);
94
+
95
+ &:active {
96
+ cursor: grabbing;
97
+ }
98
+
99
+ &::-webkit-details-marker,
100
+ &::marker {
101
+ display: none;
102
+ }
103
+
104
+ &:hover {
105
+ background: var(--colorHover);
106
+ }
107
+
108
+ .FolderName {
109
+ margin-left: 125px;
110
+ &.canProxy {
111
+ margin-left: 155px;
112
+ }
113
+
114
+ &.groupedByMethod {
115
+ margin-left: 79px;
116
+ &.canProxy {
117
+ margin-left: 109px;
118
+ }
119
+ }
120
+ }
121
+
122
+ .FolderChevron {
123
+ display: flex;
124
+ width: 16px;
125
+ height: 16px;
126
+ flex-shrink: 0;
127
+ align-items: center;
128
+ justify-content: center;
129
+ opacity: .7;
130
+ transition: transform cubic-bezier(.2, .7, .8, 1.4) 400ms;
131
+ transform: rotate(-90deg);
132
+
133
+ svg {
134
+ width: 100%;
135
+ height: 100%;
136
+ }
137
+ }
138
+ }
139
+
140
+ &[open] {
141
+ &:has(summary:hover):not(:has(details:hover)) {
142
+ background: linear-gradient(90deg, var(--colorHover), var(--colorBg));
143
+ }
144
+
145
+ > summary {
146
+ position: absolute;
147
+ top: 0;
148
+ left: 0;
149
+ width: 16px;
150
+
151
+ .FolderChevron {
152
+ transform: rotate(0deg);
153
+ }
154
+ .FolderName {
155
+ display: none;
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ @keyframes _kfRowIn {
163
+ to {
164
+ opacity: 1;
165
+ transform: scaleY(1);
166
+ }
167
+ }
168
+
169
+ .Method {
170
+ overflow: hidden;
171
+ min-width: 38px;
172
+ padding: 4px 0;
173
+ margin-top: 3px;
174
+ margin-right: 8px;
175
+ color: var(--colorLabel);
176
+ font-size: 11px;
177
+ text-align: center;
178
+ white-space: nowrap;
179
+ text-overflow: ellipsis;
180
+ }
181
+
182
+ .PreviewLink {
183
+ position: relative;
184
+ left: -8px;
185
+ display: inline-block;
186
+ width: auto;
187
+ min-width: 140px;
188
+ flex-grow: 1;
189
+ padding: 6px 8px;
190
+ margin-right: -2px;
191
+ margin-left: 4px;
192
+ font-weight: 500;
193
+ border-radius: var(--radius);
194
+ word-break: break-word;
195
+
196
+ &:hover {
197
+ background: var(--colorHover);
198
+ }
199
+ &.chosen {
200
+ color: white;
201
+ background: var(--colorAccent);
202
+ }
203
+ .dittoDir {
204
+ opacity: 0.8;
205
+ filter: saturate(0.1);
206
+ }
207
+ }
208
+
209
+
210
+ .MockSelector {
211
+ min-width: 58px;
212
+ height: 24px;
213
+ padding-right: 20px;
214
+ padding-left: 8px;
215
+ text-overflow: ellipsis;
216
+ text-align: right;
217
+
218
+ &:enabled {
219
+ box-shadow: var(--boxShadowRimDrop);
220
+ &:hover {
221
+ background-color: var(--colorBgFieldHover);
222
+ }
223
+ }
224
+
225
+ &.nonDefault {
226
+ font-weight: bold;
227
+ font-size: 11px;
228
+ }
229
+ &.status4xx {
230
+ background: var(--colorBg4xx);
231
+ }
232
+ &:disabled {
233
+ padding-right: 4px;
234
+ padding-left: 0;
235
+ border-color: transparent;
236
+ appearance: none;
237
+ background: transparent;
238
+ cursor: default;
239
+ color: var(--colorLabel);
240
+ opacity: 0.8;
241
+ }
242
+ }
243
+
244
+
245
+ .Toggler {
246
+ display: flex;
247
+ margin-top: 2px;
248
+
249
+ input {
250
+ /* For click drag target */
251
+ position: absolute;
252
+ width: 22px;
253
+ height: 22px;
254
+ opacity: 0;
255
+
256
+ &:focus-visible {
257
+ outline: 0;
258
+ & + .checkboxBody {
259
+ outline: 2px solid var(--colorAccent);
260
+ }
261
+ }
262
+
263
+ &:disabled + .checkboxBody {
264
+ cursor: not-allowed;
265
+ opacity: 0.7;
266
+ }
267
+
268
+ &:checked + .checkboxBody {
269
+ border-color: var(--colorAccent);
270
+ fill: var(--colorAccent);
271
+ background: var(--colorAccent);
272
+ stroke: var(--colorBg);
273
+ }
274
+
275
+ &:enabled:hover:not(:checked) + .checkboxBody {
276
+ border-color: var(--colorHover);
277
+ background: var(--colorHover);
278
+ stroke: var(--colorText);
279
+ }
280
+ }
281
+
282
+ .checkboxBody {
283
+ display: flex;
284
+ width: 22px;
285
+ height: 21px;
286
+ align-items: center;
287
+ justify-content: center;
288
+ fill: none;
289
+ stroke: var(--colorLabel);
290
+ stroke-width: 2.5px;
291
+ border-radius: 50%;
292
+ box-shadow: var(--boxShadowRimDrop);
293
+ background-color: var(--colorBgHeaderField);
294
+ }
295
+
296
+ &.canProxy {
297
+ margin-left: 30px;
298
+ }
299
+ }
300
+
301
+ .DelayToggler {
302
+ }
303
+
304
+ .ProxyToggler {
305
+ margin-right: 8px;
306
+
307
+ .checkboxBody {
308
+ svg {
309
+ width: 15px;
310
+ }
311
+ }
312
+ }
313
+
314
+ .StatusCodeToggler {
315
+ margin-right: 10px;
316
+ margin-left: 8px;
317
+
318
+ input {
319
+ width: 26px;
320
+
321
+ &:not(:checked):enabled:hover + .checkboxBody {
322
+ border-color: var(--colorRed);
323
+ color: var(--colorRed);
324
+ background: transparent;
325
+ }
326
+ &:checked + .checkboxBody {
327
+ border-color: var(--colorRed);
328
+ color: white;
329
+ background: var(--colorRed);
330
+ }
331
+ }
332
+
333
+ .checkboxBody {
334
+ width: 27px;
335
+ padding: 4px;
336
+ font-size: 10px;
337
+ font-weight: bold;
338
+ color: var(--colorLabel);
339
+ }
340
+ }
@@ -0,0 +1,370 @@
1
+ import { createElement as r, t, Fragment } from './utils/dom.js'
2
+
3
+ import { store } from './app-store.js'
4
+ import { previewMock } from './app-payload-viewer.js'
5
+ import { classNames, adoptSheet } from './utils/css.js'
6
+ import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
7
+
8
+ import CSS from './app-mock-list.css' with { type: 'css' }
9
+ adoptSheet(CSS, './app-mock-list.css')
10
+
11
+
12
+ export function MockList() {
13
+ return Fragment(
14
+ r('div', { className: CSS.SubToolbar },
15
+ GroupByMethod(),
16
+ BulkSelector()),
17
+ Table())
18
+ }
19
+
20
+
21
+ function GroupByMethod() {
22
+ return (
23
+ r('label', { className: CSS.GroupByMethod },
24
+ r('input', {
25
+ type: 'checkbox',
26
+ checked: store.groupByMethod,
27
+ onChange: store.toggleGroupByMethod
28
+ }),
29
+ r('span', { className: CSS.checkboxBody }, t`Group by Method`)))
30
+ }
31
+
32
+
33
+ function BulkSelector() {
34
+ const { comments } = store
35
+ const firstOption = t`Pick Comment…`
36
+ function onChange() {
37
+ const value = this.value
38
+ this.value = firstOption // hack so it’s always selected
39
+ store.bulkSelectByComment(value)
40
+ }
41
+ const disabled = !comments.length
42
+ return (
43
+ r('label', { className: CSS.BulkSelector },
44
+ r('span', null, t`Bulk Select`),
45
+ r('select', {
46
+ disabled,
47
+ autocomplete: 'off',
48
+ title: disabled
49
+ ? t`No mock files have comments which are anything within parentheses on the filename.`
50
+ : undefined,
51
+ onChange
52
+ },
53
+ r('option', { value: firstOption }, firstOption),
54
+ r('hr'),
55
+ comments.map(value => r('option', { value }, value)))))
56
+ }
57
+
58
+ function Table() {
59
+ return (
60
+ r('div', {
61
+ ref: Table.ref,
62
+ className: CSS.Table
63
+ },
64
+ TableContent()))
65
+ }
66
+ Table.ref = { width: undefined }
67
+ Table.$ = selector => Table.ref.elem.querySelector(selector)
68
+ Table.$$ = selector => Table.ref.elem.querySelectorAll(selector)
69
+
70
+
71
+
72
+
73
+ function TableContent() {
74
+ if (!Object.keys(store.brokersByMethod).length)
75
+ return r('div', null, t`No mocks found`)
76
+
77
+ if (store.groupByMethod)
78
+ return Object.keys(store.brokersByMethod).map(method => Fragment(
79
+ r('div', {
80
+ className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
81
+ }, method),
82
+ FolderGroups(store.folderGroupsByMethod(method))))
83
+
84
+ return FolderGroups(store.folderGroupsByMethod('*'))
85
+ }
86
+
87
+
88
+ function FolderGroups(brokersTree) {
89
+ const res = []
90
+ for (const b of brokersTree) {
91
+ if (!b.children.length)
92
+ res.push(Row(b))
93
+ else
94
+ res.push(FolderGroup(b))
95
+ }
96
+ return res
97
+ }
98
+
99
+ function FolderGroup(broker) {
100
+ const folder = broker.urlMask
101
+ const children = broker.children
102
+ return (
103
+ r('details', {
104
+ className: CSS.FolderGroup,
105
+ open: !store.collapsedFolders.has(folder),
106
+ onToggle() {
107
+ store.setFolderCollapsed(folder, !this.open)
108
+ }
109
+ },
110
+ r('summary', null,
111
+ r('span', { className: CSS.FolderChevron }, ChevronDownIcon()),
112
+ r('span', {
113
+ className: classNames(
114
+ CSS.FolderName,
115
+ store.groupByMethod && CSS.groupedByMethod,
116
+ store.canProxy && CSS.canProxy)
117
+ },
118
+ folder + '…')),
119
+ Row(broker),
120
+ children.map(c => c.children.length
121
+ ? FolderGroup(c)
122
+ : Row(c))))
123
+ }
124
+
125
+ /** @param {BrokerRowModel} row */
126
+ function Row(row) {
127
+ const { method, urlMask } = row
128
+ return (
129
+ r('div', {
130
+ key: row.key,
131
+ className: classNames(CSS.TableRow, store.mounted && row.isNew && CSS.animIn)
132
+ },
133
+
134
+ store.canProxy && ClickDragToggler({
135
+ className: CSS.ProxyToggler,
136
+ title: t`Proxy Toggler`,
137
+ body: CloudIcon(),
138
+ checked: row.proxied,
139
+ commit(checked) {
140
+ store.setProxied(method, urlMask, checked)
141
+ }
142
+ }),
143
+
144
+ ClickDragToggler({
145
+ className: CSS.DelayToggler,
146
+ title: t`Delay`,
147
+ body: TimerIcon(),
148
+ checked: row.delayed,
149
+ commit(checked) {
150
+ store.setDelayed(method, urlMask, checked)
151
+ }
152
+ }),
153
+
154
+ ClickDragToggler(
155
+ row.isStatic
156
+ ? {
157
+ className: CSS.StatusCodeToggler,
158
+ title: t`Not Found`,
159
+ body: t`404`,
160
+ disabled: row.opts.length === 1 && row.status === 404,
161
+ checked: !row.proxied && row.status === 404,
162
+ commit() { store.toggleStatus(method, urlMask, 404) }
163
+ }
164
+ : {
165
+ className: CSS.StatusCodeToggler,
166
+ title: t`Internal Server Error`,
167
+ body: t`500`,
168
+ disabled: row.opts.length === 1 && row.status === 500,
169
+ checked: !row.proxied && row.status === 500,
170
+ commit() { store.toggleStatus(method, urlMask, 500) }
171
+ }),
172
+
173
+ !store.groupByMethod && r('span', { className: CSS.Method }, method),
174
+
175
+ PreviewLink(method, urlMask, row.urlMaskDittoed),
176
+
177
+ MockSelector(row)))
178
+ }
179
+
180
+
181
+ export function renderRow(method, urlMask) {
182
+ unChooseOld()
183
+ const row = store.brokerAsRow(method, urlMask)
184
+ const tr = Table.$(`.${CSS.TableRow}[key="${row.key}"]`)
185
+ mergeTableRow(tr, Row(row))
186
+ previewMock()
187
+
188
+ function unChooseOld() {
189
+ return Table.$(`a.${CSS.chosen}`)
190
+ ?.classList.remove(CSS.chosen)
191
+ }
192
+
193
+ function mergeTableRow(oldRow, newRow) {
194
+ for (let i = 0; i < newRow.children.length; i++) {
195
+ const oldEl = oldRow.children[i]
196
+ const newEl = newRow.children[i]
197
+ switch (newEl.tagName) {
198
+ case 'LABEL': {
199
+ const oldInput = oldEl.querySelector('[type="checkbox"]')
200
+ const newInput = newEl.querySelector('[type="checkbox"]')
201
+ oldInput.checked = newInput.checked
202
+ oldInput.disabled = newInput.disabled
203
+ break
204
+ }
205
+ case 'A':
206
+ oldEl.className = newEl.className
207
+ break
208
+ case 'SELECT':
209
+ oldEl.replaceChildren(...newEl.cloneNode(true).children)
210
+ oldEl.className = newEl.className
211
+ oldEl.disabled = newEl.disabled
212
+ oldEl.value = newEl.value
213
+ break
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+
220
+ function PreviewLink(method, urlMask, urlMaskDittoed) {
221
+ function onClick(event) {
222
+ event.preventDefault()
223
+ store.previewLink(method, urlMask)
224
+ }
225
+ const isChosen = store.chosenLink.method === method && store.chosenLink.urlMask === urlMask
226
+ const [ditto, tail] = urlMaskDittoed
227
+ return (
228
+ r('a', {
229
+ className: classNames(CSS.PreviewLink, isChosen && CSS.chosen),
230
+ href: urlMask,
231
+ onClick
232
+ }, ditto
233
+ ? [r('span', { className: CSS.dittoDir }, ditto), tail]
234
+ : tail))
235
+ }
236
+
237
+
238
+ /** @param {BrokerRowModel} row */
239
+ function MockSelector(row) {
240
+ return (
241
+ r('select', {
242
+ onChange() {
243
+ store.selectFile(this.value)
244
+ },
245
+ onKeyDown(event) {
246
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft')
247
+ event.preventDefault()
248
+ // Because in Firefox they change the select.option, and
249
+ // we use those keys for spreadsheet-like navigation.
250
+ },
251
+ 'aria-label': t`Mock Selector`,
252
+ disabled: row.opts.length < 2,
253
+ className: classNames(
254
+ CSS.MockSelector,
255
+ row.selectedIdx > 0 && CSS.nonDefault,
256
+ row.selectedFileIs4xx && CSS.status4xx)
257
+ }, row.opts.map(([value, label, selected]) =>
258
+ r('option', { value, selected }, label))))
259
+ }
260
+
261
+
262
+ function ClickDragToggler({ checked, commit, className, title, body }) {
263
+ function onPointerEnter(event) {
264
+ if (event.buttons === 1)
265
+ onPointerDown.call(this, event)
266
+ }
267
+
268
+ function onPointerDown(event) {
269
+ if (event.altKey) {
270
+ onExclusiveClick.call(this)
271
+ return
272
+ }
273
+ this.checked = !this.checked
274
+ this.focus()
275
+ commit(this.checked)
276
+ }
277
+
278
+ function onExclusiveClick() {
279
+ const selector = selectorForColumnOf(this)
280
+ if (!selector)
281
+ return
282
+
283
+ // Uncheck all other in the column.
284
+ for (const elem of Table.$$(selector))
285
+ if (elem !== this && elem.checked && !elem.disabled) {
286
+ elem.checked = false
287
+ elem.dispatchEvent(new Event('change'))
288
+ }
289
+
290
+ if (!this.checked) {
291
+ this.checked = true
292
+ this.dispatchEvent(new Event('change'))
293
+ }
294
+ this.focus()
295
+ }
296
+
297
+ function onClick(event) {
298
+ if (event.pointerType === 'mouse')
299
+ event.preventDefault()
300
+ }
301
+ function onChange() {
302
+ commit(this.checked)
303
+ }
304
+ return (
305
+ r('label', { className: classNames(CSS.Toggler, className), title },
306
+ r('input', {
307
+ type: 'checkbox',
308
+ checked,
309
+ onPointerEnter,
310
+ onPointerDown,
311
+ onClick,
312
+ onChange
313
+ }),
314
+ r('span', { className: CSS.checkboxBody }, body)))
315
+ }
316
+
317
+
318
+
319
+ function selectorForColumnOf(elem) {
320
+ return columnSelectors().find(s => elem?.matches(s))
321
+ }
322
+
323
+ function columnSelectors() {
324
+ return [
325
+ `.${CSS.TableRow} .${CSS.ProxyToggler} input`,
326
+ `.${CSS.TableRow} .${CSS.DelayToggler} input`,
327
+ `.${CSS.TableRow} .${CSS.StatusCodeToggler} input`,
328
+ `.${CSS.TableRow} .${CSS.PreviewLink}`,
329
+ // No .MockSelector because down/up arrows have native behavior on them
330
+ ]
331
+ }
332
+
333
+ export function initKeyboardNavigation() {
334
+ const rowSelectors = [
335
+ ...columnSelectors(),
336
+ `.${CSS.TableRow} .${CSS.MockSelector}:enabled`,
337
+ ]
338
+
339
+ addEventListener('keydown', function ({ key }) {
340
+ switch (key) {
341
+ case 'ArrowDown':
342
+ case 'ArrowUp': {
343
+ const pivot = document.activeElement
344
+ const sel = selectorForColumnOf(pivot)
345
+ if (sel) {
346
+ const offset = key === 'ArrowDown' ? +1 : -1
347
+ const siblings = Table.$$(sel)
348
+ circularAdjacent(offset, siblings, pivot).focus()
349
+ }
350
+ break
351
+ }
352
+ case 'ArrowRight':
353
+ case 'ArrowLeft': {
354
+ const pivot = document.activeElement
355
+ const sel = rowSelectors.find(s => pivot?.matches(s))
356
+ if (sel) {
357
+ const offset = key === 'ArrowRight' ? +1 : -1
358
+ const siblings = pivot.closest(`.${CSS.TableRow}`).querySelectorAll(rowSelectors.join(','))
359
+ circularAdjacent(offset, siblings, pivot).focus()
360
+ }
361
+ break
362
+ }
363
+ }
364
+ })
365
+
366
+ function circularAdjacent(step, siblings, pivot) {
367
+ const arr = Array.from(siblings)
368
+ return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
369
+ }
370
+ }