mockaton 11.2.5 → 11.2.6

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/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  ![NPM Version](https://img.shields.io/npm/v/mockaton)
4
4
  [![Test](https://github.com/ericfortis/mockaton/actions/workflows/test.yml/badge.svg)](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
5
- [![CodeQL](https://github.com/ericfortis/mockaton/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/ericfortis/mockaton/actions/workflows/github-code-scanning/codeql)
6
5
  [![codecov](https://codecov.io/github/ericfortis/mockaton/graph/badge.svg?token=90NYLMMG1J)](https://codecov.io/github/ericfortis/mockaton)
7
6
 
8
7
  An HTTP mock server for simulating APIs with minimal setup — ideal
@@ -46,15 +45,19 @@ curl localhost:2020/api/user
46
45
 
47
46
  ## Dashboard
48
47
 
49
- On the dashboard you can select a mock variant for a particular route,
50
- 🕓 delaying responses, or triggering an autogenerated `500` error, among
51
- other features. Nonetheless, there’s a programmatic API, which is
52
- handy for setting up tests (see **Commander API** section below).
48
+ On the dashboard you can:
49
+ - Select a mock variant for a particular route
50
+ - 🕓 Delay responses
51
+ - Trigger an autogenerated `500` error
52
+ - …and cycle it off (for testing retries)
53
+
54
+ Nonetheless, there’s a programmatic API, which is handy for
55
+ setting up tests (see **Commander API** section below).
53
56
 
54
57
  <picture>
55
- <source media="(prefers-color-scheme: light)" srcset="pixaton-tests/tests/macos/pic-for-readme.vp761x720.light.gold.png">
56
- <source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/tests/macos/pic-for-readme.vp761x720.dark.gold.png">
57
- <img alt="Mockaton Dashboard" src="pixaton-tests/tests/macos/pic-for-readme.vp761x720.light.gold.png">
58
+ <source media="(prefers-color-scheme: light)" srcset="pixaton-tests/tests/macos/pic-for-readme.vp761x740.light.gold.png">
59
+ <source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/tests/macos/pic-for-readme.vp761x740.dark.gold.png">
60
+ <img alt="Mockaton Dashboard" src="pixaton-tests/tests/macos/pic-for-readme.vp761x740.light.gold.png">
58
61
  </picture>
59
62
 
60
63
 
@@ -76,7 +79,7 @@ api/login<b>(invalid login attempt)</b>.POST.401.json
76
79
  You can **Bulk Select** mocks by comments to simulate the complete states
77
80
  you want. For example:
78
81
 
79
- <img src="docs/bulk-select.png" width="180px">
82
+ <img src="docs/changelog/bulk-select.png" width="180px">
80
83
 
81
84
 
82
85
  ### Different response status code
@@ -95,10 +98,11 @@ api/videos.GET.<b>500</b>.txt # Internal Server Error
95
98
  ## Scraping Mocks from your Backend
96
99
 
97
100
  ### Option 1: Browser extension
98
- The companion Chrome [devtools
99
- extension](https://github.com/ericfortis/download-http-requests-browser-ext)
101
+ The companion Chrome [devtools extension](https://chromewebstore.google.com/detail/mockaton-downloader/babjpljmacbefcmlomjedmgmkecnmlaa)
100
102
  lets you download all the HTTP responses and
101
- save them following Mockaton’s filename convention.
103
+ save them in bulk following Mockaton’s filename convention.
104
+
105
+ ![](browser-extension/README-overview.png)
102
106
 
103
107
  ### Option 2: Fallback to your backend
104
108
  <details>
@@ -127,11 +131,10 @@ Sometimes the flow you need to test is
127
131
  too difficult to reproduce from the actual backend.
128
132
 
129
133
  - Demo edge cases to PMs, Design, and clients
130
- - Set up screenshot tests, e.g., with [pixaton](https://github.com/ericfortis/pixaton)
131
- - Spot inadvertent regressions during development. For example, the demo app in
132
- this repo has a list of colors containing all of their possible states, such as
133
- permutations for out-of-stock, new-arrival, and discontinued. This way you’ll
134
- indirectly notice if something else broke.
134
+ - Set up screenshot tests (see [pixaton-tests/](pixaton-tests) in this repo)
135
+ - Spot inadvertent regressions during development. For example, the demo
136
+ app in this repo has a list of colors containing all of their possible
137
+ states. This way you’ll indirectly notice if something broke.
135
138
 
136
139
  <img src="./demo-app-vite/pixaton-tests/pic-for-readme.vp740x880.light.gold.png" alt="Mockaton Demo App Screenshot" width="740" />
137
140
 
@@ -154,11 +157,10 @@ make run-standalone-demo
154
157
  - Dashboard: http://localhost:4040/mockaton
155
158
 
156
159
  ### Testing scenarios that would otherwise be skipped
157
- - Trigger dynamic states on an API. You can do this by using comments on mock filenames, for example, for polled alerts or notifications.
160
+ - Trigger dynamic states on an API. For example, for polled alerts or notifications.
158
161
  - Testing retries, you can change an endpoint from a 500 to a 200 on the fly.
159
- - Simulate errors on third-party APIs, or on your project’s backend (if you are a frontend dev, or unfamiliar with that code)
160
- - Generating dynamic responses. Mockaton lets you use Node’s HTTP handlers (see function mocks) when using function mocks.
161
- So you can, e.g.:
162
+ - Simulate errors on third-party APIs, or on your project’s backend.
163
+ - Generate dynamic responses. Use Node’s HTTP handlers (see function mocks below). So you can, e.g.:
162
164
  - have an in-memory database
163
165
  - read from disk
164
166
  - read query string
@@ -755,6 +757,8 @@ api/foo/bar.GET.200.json
755
757
  <br/>
756
758
 
757
759
  ## Commander API
760
+ [openapi.yaml](docs/openapi.yaml)
761
+
758
762
  `Commander` is a JavaScript client for Mockaton’s HTTP API.
759
763
  All of its methods return their `fetch` response promise.
760
764
  ```js
@@ -773,8 +777,8 @@ await mockaton.select('api/foo.200.GET.json')
773
777
  ```
774
778
 
775
779
  ### Toggle 500
776
- Either selects the first found 500, which could be
777
- the autogenerated one, or selects the default file.
780
+ - Selects the first found 500, which could be the autogenerated one.
781
+ - Or, selects the default file.
778
782
  ```js
779
783
  await mockaton.toggle500('GET', '/api/foo')
780
784
  ```
@@ -848,7 +852,7 @@ await mockaton.setCorsAllowed(true)
848
852
 
849
853
  ### Reset
850
854
  Re-initialize the collection. The selected mocks, cookies, and delays go back to
851
- default, but the `proxyFallback`, `colledProxied`, and `corsAllowed` are not affected.
855
+ default, but the `proxyFallback`, `collectProxied`, and `corsAllowed` are not affected.
852
856
  ```js
853
857
  await mockaton.reset()
854
858
  ```
@@ -873,15 +877,13 @@ example, if you are polling, and you want to test the state change.
873
877
 
874
878
  ### Client Side
875
879
  In contrast to Mockaton, which is an HTTP Server, these
876
- programs hijack your browser’s HTTP client (and Node’s).
877
-
880
+ programs hijack your browser’s HTTP client (or Node’s).
878
881
  - [Mock Server Worker (MSW)](https://mswjs.io)
879
882
  - [Nock](https://github.com/nock/nock)
880
883
  - [Fetch Mock](https://github.com/wheresrhys/fetch-mock)
881
- - [Mentoss](https://github.com/humanwhocodes/mentoss) Has a server side too
884
+ - [Mentoss](https://github.com/humanwhocodes/mentoss)
882
885
 
883
886
  ### Server Side
884
-
885
887
  - [Wire Mock](https://github.com/wiremock/wiremock)
886
888
  - [Mock](https://github.com/dhuan/mock)
887
889
  - [Swagger](https://swagger.io/)
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "11.2.5",
5
+ "version": "11.2.6",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -1,4 +1,8 @@
1
- import { API, LONG_POLL_SERVER_TIMEOUT, HEADER_SYNC_VERSION } from './ApiConstants.js'
1
+ import {
2
+ API,
3
+ HEADER_SYNC_VERSION,
4
+ LONG_POLL_SERVER_TIMEOUT
5
+ } from './ApiConstants.js'
2
6
 
3
7
 
4
8
  /** Client for controlling Mockaton via its HTTP API */
@@ -9,61 +13,72 @@ export class Commander {
9
13
  this.addr = addr
10
14
  }
11
15
 
12
- /** @returns {JsonPromise<State>} */
13
- getState = () =>
14
- fetch(this.addr + API.state)
15
-
16
- /**
17
- * @param {number?} currSyncVer - On mismatch, it responds immediately. Otherwise, long polls.
18
- * @param {AbortSignal} abortSignal
19
- * @returns {JsonPromise<number>}
20
- */
21
- getSyncVersion = (currSyncVer = undefined, abortSignal = undefined) =>
22
- fetch(this.addr + API.syncVersion, {
23
- signal: AbortSignal.any([
24
- abortSignal,
25
- AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
26
- ].filter(Boolean)),
27
- headers: currSyncVer !== undefined
28
- ? { [HEADER_SYNC_VERSION]: currSyncVer }
29
- : {}
30
- })
31
-
32
-
33
- #patch(api, body) {
34
- return fetch(this.addr + api, {
35
- method: 'PATCH',
36
- body: JSON.stringify(body)
37
- })
38
- }
16
+ #patch = (api, body) => fetch(this.addr + api, {
17
+ method: 'PATCH',
18
+ body: JSON.stringify(body)
19
+ })
39
20
 
21
+ /** @returns {Promise<Response>} */
40
22
  reset = () => this.#patch(API.reset)
41
23
 
42
- selectCookie = label => this.#patch(API.cookies, label)
24
+ /** @returns {Promise<Response>} */
43
25
  setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
26
+
27
+ /** @returns {Promise<Response>} */
44
28
  setCorsAllowed = value => this.#patch(API.cors, value)
29
+
30
+ /** @returns {Promise<Response>} */
45
31
  setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
32
+
33
+ /** @returns {Promise<Response>} */
46
34
  setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
47
35
 
36
+ /** @returns {JsonPromise<State.cookies>} */
37
+ selectCookie = label => this.#patch(API.cookies, label)
38
+
39
+
40
+ /** @returns {Promise<Response>} */
41
+ bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
42
+
48
43
  /** @returns {JsonPromise<ClientMockBroker>} */
49
- select = file =>
50
- this.#patch(API.select, file)
44
+ select = file => this.#patch(API.select, file)
51
45
 
52
- bulkSelectByComment = comment =>
53
- this.#patch(API.bulkSelect, comment)
54
46
 
55
47
  /** @returns {JsonPromise<ClientMockBroker>} */
56
- toggle500 = (method, urlMask) =>
57
- this.#patch(API.toggle500, [method, urlMask])
48
+ toggle500 = (method, urlMask) => this.#patch(API.toggle500, [method, urlMask])
58
49
 
59
50
  /** @returns {JsonPromise<ClientMockBroker>} */
60
- setRouteIsProxied = (method, urlMask, proxied) =>
61
- this.#patch(API.proxied, [method, urlMask, proxied])
51
+ setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
62
52
 
63
53
  /** @returns {JsonPromise<ClientMockBroker>} */
64
- setRouteIsDelayed = (method, urlMask, delayed) =>
65
- this.#patch(API.delay, [method, urlMask, delayed])
54
+ setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
55
+
66
56
 
57
+ /** @returns {Promise<Response>} */
67
58
  setStaticRouteStatus = (urlMask, status) => this.#patch(API.staticStatus, [urlMask, status])
59
+
60
+ /** @returns {Promise<Response>} */
68
61
  setStaticRouteIsDelayed = (urlMask, delayed) => this.#patch(API.delayStatic, [urlMask, delayed])
62
+
63
+
64
+
65
+ /** @returns {JsonPromise<State>} */
66
+ getState = () => fetch(this.addr + API.state)
67
+
68
+ /**
69
+ * This is for listening to real-time updates. It responds when a new mock is added, deleted, or renamed.
70
+ * @param {number?} currSyncVer - On mismatch, it responds immediately. Otherwise, long polls.
71
+ * @param {AbortSignal} abortSignal
72
+ * @returns {JsonPromise<number>}
73
+ */
74
+ getSyncVersion = (currSyncVer = undefined, abortSignal = undefined) =>
75
+ fetch(this.addr + API.syncVersion, {
76
+ signal: AbortSignal.any([
77
+ abortSignal,
78
+ AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
79
+ ].filter(Boolean)),
80
+ headers: currSyncVer !== undefined
81
+ ? { [HEADER_SYNC_VERSION]: currSyncVer }
82
+ : {}
83
+ })
69
84
  }
@@ -1,4 +1,4 @@
1
- // @KeepSync src/server/ApiConstants.js
1
+ /** @KeepSync src/server/ApiConstants.js */
2
2
 
3
3
  const MOUNT = '/mockaton'
4
4
  export const API = {
@@ -2,9 +2,9 @@
2
2
 
3
3
  const reComments = /\(.*?\)/g // Anything within parentheses
4
4
 
5
- export const extractComments = file =>
6
- Array.from(file.matchAll(reComments), ([c]) => c)
7
-
5
+ export function extractComments(file) {
6
+ return Array.from(file.matchAll(reComments), ([c]) => c)
7
+ }
8
8
 
9
9
  export function parseFilename(file) {
10
10
  const tokens = file.replace(reComments, '').split('.')
package/src/client/app.js CHANGED
@@ -1,7 +1,12 @@
1
- import { createElement as r, createSvgElement as s, className, restoreFocus, Defer, Fragment } from './dom-utils.js'
2
- import { HEADER_502 } from './ApiConstants.js'
3
- import { parseFilename } from './Filename.js'
1
+ import {
2
+ createElement as r,
3
+ createSvgElement as s,
4
+ className, restoreFocus, Defer, Fragment
5
+ } from './dom-utils.js'
6
+
4
7
  import { store } from './app-store.js'
8
+ import { parseFilename } from './Filename.js'
9
+ import { HEADER_502 } from './ApiConstants.js'
5
10
 
6
11
 
7
12
  const CSS = {
@@ -27,12 +32,18 @@ const CSS = {
27
32
  Resizer: null,
28
33
  SaveProxiedCheckbox: null,
29
34
  SettingsMenu: null,
35
+ Table: null,
36
+ TableHeading: null,
37
+ TableRow: null,
30
38
 
31
39
  animIn: null,
40
+ canProxy: null,
41
+ checkboxBody: null,
32
42
  chosen: null,
33
43
  dittoDir: null,
34
44
  leftSide: null,
35
45
  nonDefault: null,
46
+ nonGroupedByMethod: null,
36
47
  rightSide: null,
37
48
  status4xx: null,
38
49
 
@@ -84,7 +95,7 @@ function App() {
84
95
  style: { width: leftSideRef.width },
85
96
  className: CSS.leftSide
86
97
  },
87
- r('table', null,
98
+ r('div', className(CSS.Table),
88
99
  MockList(),
89
100
  StaticFilesList())),
90
101
  r('div', { className: CSS.rightSide },
@@ -212,7 +223,7 @@ function SaveProxiedCheckbox(ref) {
212
223
  checked: store.collectProxied,
213
224
  onChange() { store.setCollectProxied(this.checked) }
214
225
  }),
215
- r('span', null, t`Save Mocks`)))
226
+ r('span', className(CSS.checkboxBody), t`Save Mocks`)))
216
227
  }
217
228
 
218
229
 
@@ -258,7 +269,7 @@ function SettingsMenu(id) {
258
269
  checked: store.groupByMethod,
259
270
  onChange: store.toggleGroupByMethod
260
271
  }),
261
- r('span', null, t`Group by Method`)),
272
+ r('span', className(CSS.checkboxBody), t`Group by Method`)),
262
273
 
263
274
  r('a', {
264
275
  href: 'https://github.com/ericfortis/mockaton',
@@ -280,9 +291,8 @@ function MockList() {
280
291
 
281
292
  if (store.groupByMethod)
282
293
  return Object.keys(store.brokersByMethod).map(method => Fragment(
283
- r('tr', null,
284
- r('th', { colspan: 2 + Number(store.canProxy) }),
285
- r('th', { colspan: 2 }, method)),
294
+ r('div', className(CSS.TableHeading, store.canProxy && CSS.canProxy),
295
+ method),
286
296
  store.brokersAsRowsByMethod(method).map(Row)))
287
297
 
288
298
  return store.brokersAsRowsByMethod('*').map(Row)
@@ -293,33 +303,26 @@ function MockList() {
293
303
  * @param {number} i
294
304
  */
295
305
  function Row(row, i) {
296
- const { key, method, urlMask } = row
306
+ const { method, urlMask } = row
297
307
  return (
298
- r('tr', {
299
- key,
300
- ...className(render.count > 1 && row.isNew && CSS.animIn)
308
+ r('div', {
309
+ key: row.key,
310
+ ...className(CSS.TableRow,
311
+ render.count > 1 && row.isNew && CSS.animIn)
301
312
  },
302
- store.canProxy && r('td', null,
303
- ProxyToggler(method, urlMask, row.proxied)),
313
+ store.canProxy && ProxyToggler(method, urlMask, row.proxied),
304
314
 
305
- r('td', null,
306
- DelayRouteToggler(method, urlMask, row.delayed)),
315
+ DelayRouteToggler(method, urlMask, row.delayed),
307
316
 
308
- r('td', null,
309
- InternalServerErrorToggler(
310
- method,
311
- urlMask,
312
- !row.proxied && row.status === 500, // checked
313
- row.opts.length === 1 && row.status === 500)), // disabled
317
+ InternalServerErrorToggler(method, urlMask,
318
+ !row.proxied && row.status === 500, // checked
319
+ row.opts.length === 1 && row.status === 500), // disabled
314
320
 
315
- !store.groupByMethod && r('td', className(CSS.Method),
316
- method),
321
+ !store.groupByMethod && r('span', className(CSS.Method), method),
317
322
 
318
- r('td', null,
319
- PreviewLink(method, urlMask, row.urlMaskDittoed, i === 0)),
323
+ PreviewLink(method, urlMask, row.urlMaskDittoed, i === 0),
320
324
 
321
- r('td', null,
322
- MockSelector(row))))
325
+ MockSelector(row)))
323
326
  }
324
327
 
325
328
  function renderRow(method, urlMask) {
@@ -331,10 +334,10 @@ function renderRow(method, urlMask) {
331
334
  })
332
335
 
333
336
  function trFor(key) {
334
- return leftSideRef.elem.querySelector(`tr[key="${key}"]`)
337
+ return leftSideRef.elem.querySelector(`.${CSS.TableRow}[key="${key}"]`)
335
338
  }
336
339
  function unChooseOld() {
337
- return leftSideRef.elem.querySelector(`td > a.${CSS.chosen}`)
340
+ return leftSideRef.elem.querySelector(`a.${CSS.chosen}`)
338
341
  ?.classList.remove(CSS.chosen)
339
342
  }
340
343
  }
@@ -398,7 +401,7 @@ function InternalServerErrorToggler(method, urlMask, checked, disabled) {
398
401
  onChange() { store.toggle500(method, urlMask) },
399
402
  'data-focus-group': FocusGroup.StatusToggler
400
403
  }),
401
- r('span', null, t`500`)))
404
+ r('span', className(CSS.checkboxBody), t`500`)))
402
405
  }
403
406
 
404
407
  function ProxyToggler(method, urlMask, checked) {
@@ -421,49 +424,47 @@ function ProxyToggler(method, urlMask, checked) {
421
424
  /** # StaticFilesList */
422
425
 
423
426
  function StaticFilesList() {
424
- const { canProxy, groupByMethod } = store
425
427
  const rows = store.staticBrokersAsRows()
426
428
  return !rows.length
427
429
  ? null
428
430
  : Fragment(
429
- r('tr', null,
430
- r('th', { colspan: (2 + Number(!groupByMethod)) + Number(canProxy) }),
431
- r('th', { colspan: 2 }, t`Static GET`)),
431
+ r('div',
432
+ className(CSS.TableHeading,
433
+ store.canProxy && CSS.canProxy,
434
+ !store.groupByMethod && CSS.nonGroupedByMethod),
435
+ store.groupByMethod ? t`Static GET` : t`Static`),
432
436
  rows.map(StaticRow))
433
437
  }
434
438
 
435
439
  /** @param {StaticBrokerRowModel} row */
436
440
  function StaticRow(row) {
437
- const { canProxy, groupByMethod } = store
441
+ const { groupByMethod } = store
438
442
  const [ditto, tail] = row.urlMaskDittoed
439
443
  return (
440
- r('tr', {
444
+ r('div', {
441
445
  key: row.key,
442
- ...className(render.count > 1 && row.isNew && CSS.animIn)
446
+ ...className(CSS.TableRow,
447
+ render.count > 1 && row.isNew && CSS.animIn)
443
448
  },
444
- canProxy && r('td'),
445
- r('td', null,
446
- DelayStaticRouteToggler(row.urlMask, row.delayed)),
449
+ DelayStaticRouteToggler(row.urlMask, row.delayed),
447
450
 
448
- r('td', null,
449
- NotFoundToggler(row.urlMask, row.status === 404)),
451
+ NotFoundToggler(row.urlMask, row.status === 404),
450
452
 
451
- !groupByMethod && r('td', className(CSS.Method),
452
- 'GET'),
453
+ !groupByMethod && r('span', className(CSS.Method), 'GET'),
453
454
 
454
- r('td', { colspan: 2 },
455
- r('a', {
456
- href: row.urlMask,
457
- target: '_blank',
458
- className: CSS.PreviewLink,
459
- 'data-focus-group': FocusGroup.PreviewLink
460
- }, ditto
461
- ? [r('span', className(CSS.dittoDir), ditto), tail]
462
- : tail))))
455
+ r('a', {
456
+ href: row.urlMask,
457
+ target: '_blank',
458
+ className: CSS.PreviewLink,
459
+ 'data-focus-group': FocusGroup.PreviewLink
460
+ }, ditto
461
+ ? [r('span', className(CSS.dittoDir), ditto), tail]
462
+ : tail)))
463
463
  }
464
464
 
465
465
  function DelayStaticRouteToggler(route, checked) {
466
466
  return ClickDragToggler({
467
+ optClassName: store.canProxy && CSS.canProxy,
467
468
  checked,
468
469
  focusGroup: FocusGroup.DelayToggler,
469
470
  commit(checked) {
@@ -486,11 +487,11 @@ function NotFoundToggler(route, checked) {
486
487
  store.setStaticRouteStatus(route, this.checked ? 404 : 200)
487
488
  }
488
489
  }),
489
- r('span', null, t`404`)))
490
+ r('span', className(CSS.checkboxBody), t`404`)))
490
491
  }
491
492
 
492
493
 
493
- function ClickDragToggler({ checked, commit, focusGroup }) {
494
+ function ClickDragToggler({ checked, commit, focusGroup, optClassName }) {
494
495
  function onPointerEnter(event) {
495
496
  if (event.buttons === 1)
496
497
  onPointerDown.call(this)
@@ -509,7 +510,7 @@ function ClickDragToggler({ checked, commit, focusGroup }) {
509
510
  }
510
511
  return (
511
512
  r('label', {
512
- className: CSS.DelayToggler,
513
+ ...className(CSS.DelayToggler, optClassName),
513
514
  title: t`Delay`
514
515
  },
515
516
  r('input', {
@@ -542,7 +543,7 @@ function Resizer(ref) {
542
543
  }
543
544
 
544
545
  function onMove(event) {
545
- const MIN_LEFT_WIDTH = 380
546
+ const MIN_LEFT_WIDTH = 340
546
547
  raf = raf || requestAnimationFrame(() => {
547
548
  ref.width = Math.max(initialWidth - (initialX - event.clientX), MIN_LEFT_WIDTH) + 'px'
548
549
  ref.elem.style.width = ref.width
@@ -804,7 +805,7 @@ function initKeyboardNavigation() {
804
805
  }
805
806
 
806
807
  function rowFocusable(el, step) {
807
- const row = el.closest('tr')
808
+ const row = el.closest(`.${CSS.TableRow}`)
808
809
  if (row) {
809
810
  const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
810
811
  return circularAdjacent(step, focusables, el)
@@ -813,7 +814,7 @@ function initKeyboardNavigation() {
813
814
 
814
815
  function allInFocusGroup(focusGroup) {
815
816
  return Array.from(leftSideRef.elem.querySelectorAll(
816
- `tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
817
+ `.${CSS.TableRow} [data-focus-group="${focusGroup}"]:is(input, a)`))
817
818
  }
818
819
 
819
820
  function circularAdjacent(step = 1, arr, pivot) {
@@ -1,5 +1,7 @@
1
1
  export function className(...args) {
2
- return { className: args.filter(Boolean).join(' ') }
2
+ return {
3
+ className: args.filter(Boolean).join(' ')
4
+ }
3
5
  }
4
6
 
5
7
  export function createElement(tag, props, ...children) {
@@ -14,8 +16,8 @@ export function createElement(tag, props, ...children) {
14
16
  return elem
15
17
  }
16
18
 
17
- export function createSvgElement(tagName, props, ...children) {
18
- const elem = document.createElementNS('http://www.w3.org/2000/svg', tagName)
19
+ export function createSvgElement(tag, props, ...children) {
20
+ const elem = document.createElementNS('http://www.w3.org/2000/svg', tag)
19
21
  for (const [k, v] of Object.entries(props))
20
22
  elem.setAttribute(k, v)
21
23
  elem.append(...children.flat().filter(Boolean))