@svgrid/grid 3.0.0 → 3.0.1
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/CHANGELOG.md +116 -88
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +181 -181
- package/dist/SvAutoComplete.svelte +169 -169
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +279 -279
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +163 -163
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +378 -378
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGridCellEditor.svelte +748 -744
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +270 -270
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvListBox.svelte +334 -334
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +293 -293
- package/dist/SvNumberInput.svelte +172 -172
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +203 -203
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +115 -115
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +239 -239
- package/dist/cdn/{GridMenus-DMihUtma.js → GridMenus-CYzOvmNQ.js} +3 -3
- package/dist/cdn/{GridMenus-nmNDj1a3.js → GridMenus-DFB6iwF8.js} +3 -3
- package/dist/cdn/{SvDateRangeInput-DflbiP7N.js → SvDateRangeInput-CaOuMs8O.js} +1 -1
- package/dist/cdn/{SvDateRangeInput-Bh0A0JkF.js → SvDateRangeInput-DMLKmGEc.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-Bivn8dAP.js → SvDateTimePicker-CCbDZNZB.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-BWwpfB_o.js → SvDateTimePicker-sonaH0oh.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-Ba7rY3eu.js → SvGridCellEditor-BWnTi2N7.js} +2 -2
- package/dist/cdn/{SvGridCellEditor-AXL8cHGO.js → SvGridCellEditor-CjEJMJKc.js} +2 -2
- package/dist/cdn/{SvGridChart-CwhFz7GV.js → SvGridChart-BEJmNNx9.js} +1 -1
- package/dist/cdn/{SvGridChart-Bs2GIR2Q.js → SvGridChart-BwVos976.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-1WXuStzM.js → SvGridChartPanel-BOknOkrP.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-e5_nqNP9.js → SvGridChartPanel-D2PjII4O.js} +1 -1
- package/dist/cdn/{SvGridChartView-SmPW10dI.js → SvGridChartView-00LPUHL1.js} +1 -1
- package/dist/cdn/{SvGridChartView-eSPuJE6g.js → SvGridChartView-BhUEJ5ki.js} +1 -1
- package/dist/cdn/{SvGridDropdown-B1ZcLpgs.js → SvGridDropdown-D0VdjeR8.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CtdbHcIS.js → SvGridDropdown-D13MtJ2j.js} +1 -1
- package/dist/cdn/{date-format-DZT7T1wf.js → date-format-BNii4zeD.js} +2 -2
- package/dist/cdn/{date-format-CcP1tafP.js → date-format-BnnHlqGw.js} +2 -2
- package/dist/cdn/{src-B_YS5AOc.js → src-C8QjSaq-.js} +42 -44
- package/dist/cdn/{src-DYXXpuSk.js → src-D0hO2eYN.js} +41 -43
- package/dist/cdn/svgrid.js +7 -7
- package/dist/cdn/svgrid.svelte-external.js +7 -7
- package/dist/chart-export.js +8 -8
- package/dist/editing.js +22 -13
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +181 -181
- package/src/SvAutoComplete.svelte +169 -169
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +279 -279
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +163 -163
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +378 -378
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.types.ts +2092 -2092
- package/src/SvGridCellEditor.svelte +748 -744
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +270 -270
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvListBox.svelte +334 -334
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +293 -293
- package/src/SvNumberInput.svelte +172 -172
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +203 -203
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +115 -115
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +239 -239
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1419 -1419
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +846 -846
- package/src/builtin-editors.grid.test.ts +83 -83
- package/src/cell-formatting.ts +171 -171
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +148 -148
- package/src/chart-export.ts +202 -202
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2321 -2321
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.ts +78 -78
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1865 -1865
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +250 -250
- package/src/createMenu.svelte.ts +224 -224
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +322 -322
- package/src/datetime/date-core.ts +206 -206
- package/src/datetime/date-restrict.ts +61 -61
- package/src/datetime/timezone.ts +135 -135
- package/src/dock-manager-model.ts +596 -596
- package/src/dock-model.ts +374 -374
- package/src/editing.test.ts +974 -974
- package/src/editing.ts +618 -609
- package/src/editor-contract.ts +171 -171
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +160 -160
- package/src/filtering/excel-filters.ts +325 -325
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/headless.ts +87 -87
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +179 -179
- package/src/menus.ts +597 -597
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/positioning.ts +268 -268
- package/src/render-component.ts +28 -28
- package/src/row-drag.test.ts +401 -401
- package/src/row-drag.ts +419 -419
- package/src/row-resize.test.ts +524 -524
- package/src/row-resize.ts +228 -228
- package/src/scheduler-ical.ts +181 -181
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +873 -873
- package/src/selection.test.ts +885 -885
- package/src/server-data-source.test.ts +383 -383
- package/src/server-data-source.ts +469 -469
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +113 -113
- package/src/svgrid-wrapper.types.ts +563 -563
- package/src/svgrid.async-editor-options.test.ts +273 -273
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +910 -910
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.comments-autocomplete.test.ts +127 -127
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +382 -377
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.wrapper.test.ts +63 -63
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +288 -288
- package/src/toast-store.svelte.ts +250 -250
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +322 -322
package/src/toast-store.test.ts
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the headless toast store: queueing, variants, auto-dismiss timers,
|
|
3
|
-
* manual dismiss, sticky toasts, and the SR announcement side effect.
|
|
4
|
-
*/
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
-
import { toast, dismissToast, updateToast, clearToasts, pauseToast, resumeToast, toastStore } from './toast-store.svelte'
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
vi.useFakeTimers()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
clearToasts()
|
|
14
|
-
vi.useRealTimers()
|
|
15
|
-
document.body.innerHTML = ''
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('toast store', () => {
|
|
19
|
-
it('queues a toast with defaults', () => {
|
|
20
|
-
const id = toast('Hello')
|
|
21
|
-
expect(toastStore.toasts).toHaveLength(1)
|
|
22
|
-
const t = toastStore.toasts[0]!
|
|
23
|
-
expect(t.id).toBe(id)
|
|
24
|
-
expect(t.message).toBe('Hello')
|
|
25
|
-
expect(t.variant).toBe('info')
|
|
26
|
-
expect(t.dismissible).toBe(true)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('variant helpers set the variant', () => {
|
|
30
|
-
toast.success('ok')
|
|
31
|
-
toast.error('bad')
|
|
32
|
-
toast.warning('hmm')
|
|
33
|
-
expect(toastStore.toasts.map((t) => t.variant)).toEqual(['success', 'error', 'warning'])
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('auto-dismisses after its duration', () => {
|
|
37
|
-
toast('bye', { duration: 3000 })
|
|
38
|
-
expect(toastStore.toasts).toHaveLength(1)
|
|
39
|
-
vi.advanceTimersByTime(2999)
|
|
40
|
-
expect(toastStore.toasts).toHaveLength(1)
|
|
41
|
-
vi.advanceTimersByTime(1)
|
|
42
|
-
expect(toastStore.toasts).toHaveLength(0)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('duration 0 is sticky (no auto-dismiss)', () => {
|
|
46
|
-
toast('stay', { duration: 0 })
|
|
47
|
-
vi.advanceTimersByTime(60_000)
|
|
48
|
-
expect(toastStore.toasts).toHaveLength(1)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('dismissToast removes by id and cancels its timer', () => {
|
|
52
|
-
const id = toast('x', { duration: 5000 })
|
|
53
|
-
dismissToast(id)
|
|
54
|
-
expect(toastStore.toasts).toHaveLength(0)
|
|
55
|
-
// Advancing past the original duration must not throw or double-remove.
|
|
56
|
-
vi.advanceTimersByTime(5000)
|
|
57
|
-
expect(toastStore.toasts).toHaveLength(0)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('announces via a live region (assertive for errors)', () => {
|
|
61
|
-
toast.error('boom')
|
|
62
|
-
vi.runOnlyPendingTimers()
|
|
63
|
-
// The store routes errors to the assertive live region.
|
|
64
|
-
expect(document.querySelector('[aria-live="assertive"]')).not.toBeNull()
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('pause banks the remaining time and resume finishes it (hover to keep)', () => {
|
|
68
|
-
const id = toast('hover me', { duration: 3000 })
|
|
69
|
-
vi.advanceTimersByTime(1000) // 2000 left
|
|
70
|
-
pauseToast(id)
|
|
71
|
-
vi.advanceTimersByTime(10_000) // stays while paused
|
|
72
|
-
expect(toastStore.toasts).toHaveLength(1)
|
|
73
|
-
resumeToast(id)
|
|
74
|
-
vi.advanceTimersByTime(1999)
|
|
75
|
-
expect(toastStore.toasts).toHaveLength(1)
|
|
76
|
-
vi.advanceTimersByTime(1) // banked 2000ms elapsed
|
|
77
|
-
expect(toastStore.toasts).toHaveLength(0)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('clearToasts empties the queue', () => {
|
|
81
|
-
toast('a'); toast('b'); toast('c')
|
|
82
|
-
expect(toastStore.toasts).toHaveLength(3)
|
|
83
|
-
clearToasts()
|
|
84
|
-
expect(toastStore.toasts).toHaveLength(0)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('update patches a toast in place (same id) and restarts its timer', () => {
|
|
88
|
-
const id = toast('working', { duration: 0 })
|
|
89
|
-
updateToast(id, { message: 'done', variant: 'success', duration: 3000 })
|
|
90
|
-
expect(toastStore.toasts).toHaveLength(1)
|
|
91
|
-
const t = toastStore.toasts[0]!
|
|
92
|
-
expect(t.id).toBe(id)
|
|
93
|
-
expect(t.message).toBe('done')
|
|
94
|
-
expect(t.variant).toBe('success')
|
|
95
|
-
// The new duration now applies.
|
|
96
|
-
vi.advanceTimersByTime(3000)
|
|
97
|
-
expect(toastStore.toasts).toHaveLength(0)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('update is a no-op for an unknown id', () => {
|
|
101
|
-
updateToast(999, { message: 'nope' })
|
|
102
|
-
expect(toastStore.toasts).toHaveLength(0)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('promise shows a sticky loader, then updates in place to success', async () => {
|
|
106
|
-
vi.useRealTimers() // real microtask flush for the promise
|
|
107
|
-
let resolve!: (v: string) => void
|
|
108
|
-
const p = new Promise<string>((r) => (resolve = r))
|
|
109
|
-
const ret = toast.promise(p, { loading: 'Saving...', success: (v) => `Saved ${v}`, error: 'Failed' })
|
|
110
|
-
expect(ret).toBe(p) // returns the original promise
|
|
111
|
-
expect(toastStore.toasts).toHaveLength(1)
|
|
112
|
-
const id = toastStore.toasts[0]!.id
|
|
113
|
-
expect(toastStore.toasts[0]!.message).toBe('Saving...')
|
|
114
|
-
expect(toastStore.toasts[0]!.variant).toBe('info')
|
|
115
|
-
resolve('now')
|
|
116
|
-
await p
|
|
117
|
-
await Promise.resolve()
|
|
118
|
-
const t = toastStore.toasts.find((x) => x.id === id)!
|
|
119
|
-
expect(t.message).toBe('Saved now')
|
|
120
|
-
expect(t.variant).toBe('success')
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('promise updates in place to error on rejection', async () => {
|
|
124
|
-
vi.useRealTimers()
|
|
125
|
-
const p = Promise.reject(new Error('x'))
|
|
126
|
-
toast.promise(p, { loading: 'Load', success: 'ok', error: (e) => `Err: ${(e as Error).message}` })
|
|
127
|
-
await p.catch(() => {})
|
|
128
|
-
await Promise.resolve()
|
|
129
|
-
const t = toastStore.toasts[0]!
|
|
130
|
-
expect(t.variant).toBe('error')
|
|
131
|
-
expect(t.message).toBe('Err: x')
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('action + cancel are stored on the toast', () => {
|
|
135
|
-
const onClick = vi.fn()
|
|
136
|
-
toast('Undo?', { action: { label: 'Undo', onClick }, cancel: { label: 'Dismiss' } })
|
|
137
|
-
const t = toastStore.toasts[0]!
|
|
138
|
-
expect(t.action?.label).toBe('Undo')
|
|
139
|
-
expect(t.cancel?.label).toBe('Dismiss')
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('custom carries a render snippet', () => {
|
|
143
|
-
const render = (() => {}) as never
|
|
144
|
-
const id = toast.custom(render, { duration: 0 })
|
|
145
|
-
expect(toastStore.toasts.find((x) => x.id === id)!.render).toBe(render)
|
|
146
|
-
})
|
|
147
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the headless toast store: queueing, variants, auto-dismiss timers,
|
|
3
|
+
* manual dismiss, sticky toasts, and the SR announcement side effect.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { toast, dismissToast, updateToast, clearToasts, pauseToast, resumeToast, toastStore } from './toast-store.svelte'
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.useFakeTimers()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
clearToasts()
|
|
14
|
+
vi.useRealTimers()
|
|
15
|
+
document.body.innerHTML = ''
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('toast store', () => {
|
|
19
|
+
it('queues a toast with defaults', () => {
|
|
20
|
+
const id = toast('Hello')
|
|
21
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
22
|
+
const t = toastStore.toasts[0]!
|
|
23
|
+
expect(t.id).toBe(id)
|
|
24
|
+
expect(t.message).toBe('Hello')
|
|
25
|
+
expect(t.variant).toBe('info')
|
|
26
|
+
expect(t.dismissible).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('variant helpers set the variant', () => {
|
|
30
|
+
toast.success('ok')
|
|
31
|
+
toast.error('bad')
|
|
32
|
+
toast.warning('hmm')
|
|
33
|
+
expect(toastStore.toasts.map((t) => t.variant)).toEqual(['success', 'error', 'warning'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('auto-dismisses after its duration', () => {
|
|
37
|
+
toast('bye', { duration: 3000 })
|
|
38
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
39
|
+
vi.advanceTimersByTime(2999)
|
|
40
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
41
|
+
vi.advanceTimersByTime(1)
|
|
42
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('duration 0 is sticky (no auto-dismiss)', () => {
|
|
46
|
+
toast('stay', { duration: 0 })
|
|
47
|
+
vi.advanceTimersByTime(60_000)
|
|
48
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('dismissToast removes by id and cancels its timer', () => {
|
|
52
|
+
const id = toast('x', { duration: 5000 })
|
|
53
|
+
dismissToast(id)
|
|
54
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
55
|
+
// Advancing past the original duration must not throw or double-remove.
|
|
56
|
+
vi.advanceTimersByTime(5000)
|
|
57
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('announces via a live region (assertive for errors)', () => {
|
|
61
|
+
toast.error('boom')
|
|
62
|
+
vi.runOnlyPendingTimers()
|
|
63
|
+
// The store routes errors to the assertive live region.
|
|
64
|
+
expect(document.querySelector('[aria-live="assertive"]')).not.toBeNull()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('pause banks the remaining time and resume finishes it (hover to keep)', () => {
|
|
68
|
+
const id = toast('hover me', { duration: 3000 })
|
|
69
|
+
vi.advanceTimersByTime(1000) // 2000 left
|
|
70
|
+
pauseToast(id)
|
|
71
|
+
vi.advanceTimersByTime(10_000) // stays while paused
|
|
72
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
73
|
+
resumeToast(id)
|
|
74
|
+
vi.advanceTimersByTime(1999)
|
|
75
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
76
|
+
vi.advanceTimersByTime(1) // banked 2000ms elapsed
|
|
77
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('clearToasts empties the queue', () => {
|
|
81
|
+
toast('a'); toast('b'); toast('c')
|
|
82
|
+
expect(toastStore.toasts).toHaveLength(3)
|
|
83
|
+
clearToasts()
|
|
84
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('update patches a toast in place (same id) and restarts its timer', () => {
|
|
88
|
+
const id = toast('working', { duration: 0 })
|
|
89
|
+
updateToast(id, { message: 'done', variant: 'success', duration: 3000 })
|
|
90
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
91
|
+
const t = toastStore.toasts[0]!
|
|
92
|
+
expect(t.id).toBe(id)
|
|
93
|
+
expect(t.message).toBe('done')
|
|
94
|
+
expect(t.variant).toBe('success')
|
|
95
|
+
// The new duration now applies.
|
|
96
|
+
vi.advanceTimersByTime(3000)
|
|
97
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('update is a no-op for an unknown id', () => {
|
|
101
|
+
updateToast(999, { message: 'nope' })
|
|
102
|
+
expect(toastStore.toasts).toHaveLength(0)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('promise shows a sticky loader, then updates in place to success', async () => {
|
|
106
|
+
vi.useRealTimers() // real microtask flush for the promise
|
|
107
|
+
let resolve!: (v: string) => void
|
|
108
|
+
const p = new Promise<string>((r) => (resolve = r))
|
|
109
|
+
const ret = toast.promise(p, { loading: 'Saving...', success: (v) => `Saved ${v}`, error: 'Failed' })
|
|
110
|
+
expect(ret).toBe(p) // returns the original promise
|
|
111
|
+
expect(toastStore.toasts).toHaveLength(1)
|
|
112
|
+
const id = toastStore.toasts[0]!.id
|
|
113
|
+
expect(toastStore.toasts[0]!.message).toBe('Saving...')
|
|
114
|
+
expect(toastStore.toasts[0]!.variant).toBe('info')
|
|
115
|
+
resolve('now')
|
|
116
|
+
await p
|
|
117
|
+
await Promise.resolve()
|
|
118
|
+
const t = toastStore.toasts.find((x) => x.id === id)!
|
|
119
|
+
expect(t.message).toBe('Saved now')
|
|
120
|
+
expect(t.variant).toBe('success')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('promise updates in place to error on rejection', async () => {
|
|
124
|
+
vi.useRealTimers()
|
|
125
|
+
const p = Promise.reject(new Error('x'))
|
|
126
|
+
toast.promise(p, { loading: 'Load', success: 'ok', error: (e) => `Err: ${(e as Error).message}` })
|
|
127
|
+
await p.catch(() => {})
|
|
128
|
+
await Promise.resolve()
|
|
129
|
+
const t = toastStore.toasts[0]!
|
|
130
|
+
expect(t.variant).toBe('error')
|
|
131
|
+
expect(t.message).toBe('Err: x')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('action + cancel are stored on the toast', () => {
|
|
135
|
+
const onClick = vi.fn()
|
|
136
|
+
toast('Undo?', { action: { label: 'Undo', onClick }, cancel: { label: 'Dismiss' } })
|
|
137
|
+
const t = toastStore.toasts[0]!
|
|
138
|
+
expect(t.action?.label).toBe('Undo')
|
|
139
|
+
expect(t.cancel?.label).toBe('Dismiss')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('custom carries a render snippet', () => {
|
|
143
|
+
const render = (() => {}) as never
|
|
144
|
+
const id = toast.custom(render, { duration: 0 })
|
|
145
|
+
expect(toastStore.toasts.find((x) => x.id === id)!.render).toBe(render)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createTreeRowModel, flattenTreeData } from './core'
|
|
3
|
-
|
|
4
|
-
type Node = { id: number; parent: number | null; name: string }
|
|
5
|
-
|
|
6
|
-
/** Minimal Row stand-ins - the model only reads `original`, `id` and spreads. */
|
|
7
|
-
function rowsFrom(data: Node[]) {
|
|
8
|
-
return data.map((original, index) => ({
|
|
9
|
-
id: String(original.id),
|
|
10
|
-
index,
|
|
11
|
-
original,
|
|
12
|
-
depth: 0,
|
|
13
|
-
getCanExpand: () => false,
|
|
14
|
-
getIsExpanded: () => false,
|
|
15
|
-
toggleExpanded: () => {},
|
|
16
|
-
getIsSelected: () => false,
|
|
17
|
-
toggleSelected: () => {},
|
|
18
|
-
getAllCells: () => [],
|
|
19
|
-
getCellValueByColumnId: (c: string) => (original as never as Record<string, unknown>)[c],
|
|
20
|
-
})) as never[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let expandedState: Record<string, boolean> = {}
|
|
24
|
-
const table = {
|
|
25
|
-
getState: () => ({ expanded: expandedState }),
|
|
26
|
-
setExpanded: (fn: (p: Record<string, boolean>) => Record<string, boolean>) => {
|
|
27
|
-
expandedState = fn(expandedState)
|
|
28
|
-
},
|
|
29
|
-
} as never
|
|
30
|
-
|
|
31
|
-
const run = (data: Node[], opts = { parentField: 'parent' }) =>
|
|
32
|
-
createTreeRowModel<never>(opts)({ table, rows: rowsFrom(data) } as never)
|
|
33
|
-
|
|
34
|
-
const ids = (rows: readonly { id: string }[]) => rows.map((r) => r.id)
|
|
35
|
-
|
|
36
|
-
describe('createTreeRowModel', () => {
|
|
37
|
-
const data: Node[] = [
|
|
38
|
-
{ id: 1, parent: null, name: 'root' },
|
|
39
|
-
{ id: 2, parent: 1, name: 'child a' },
|
|
40
|
-
{ id: 3, parent: 1, name: 'child b' },
|
|
41
|
-
{ id: 4, parent: 2, name: 'grandchild' },
|
|
42
|
-
{ id: 5, parent: null, name: 'other root' },
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
it('nests by parent id and returns only roots', () => {
|
|
46
|
-
const out = run(data)
|
|
47
|
-
expect(ids(out)).toEqual(['1', '5'])
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('records depth down the chain', () => {
|
|
51
|
-
const [root] = run(data) as never as Array<{ depth: number; subRows: never[] }>
|
|
52
|
-
expect(root!.depth).toBe(0)
|
|
53
|
-
const child = (root!.subRows as never as Array<{ id: string; depth: number; subRows: never[] }>)[0]!
|
|
54
|
-
expect(child.depth).toBe(1)
|
|
55
|
-
expect((child.subRows as never as Array<{ depth: number }>)[0]!.depth).toBe(2)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('marks rows with children expandable and leaves not', () => {
|
|
59
|
-
const [root, other] = run(data) as never as Array<{ getCanExpand: () => boolean }>
|
|
60
|
-
expect(root!.getCanExpand()).toBe(true)
|
|
61
|
-
expect(other!.getCanExpand()).toBe(false)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('counts descendants, not just direct children', () => {
|
|
65
|
-
const [root] = run(data) as never as Array<{ leafCount: number }>
|
|
66
|
-
// children a + b, plus a's grandchild
|
|
67
|
-
expect(root!.leafCount).toBe(3)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('tags rows so they are not mistaken for group banners', async () => {
|
|
71
|
-
const { isGroupRow } = await import('./cell-values')
|
|
72
|
-
const [root] = run(data) as never as Array<never>
|
|
73
|
-
expect((root as never as { __treeRow: boolean }).__treeRow).toBe(true)
|
|
74
|
-
// Expandable, but must NOT render as a full-width banner.
|
|
75
|
-
expect(isGroupRow(root!)).toBe(false)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('drives expansion through the table state', () => {
|
|
79
|
-
expandedState = {}
|
|
80
|
-
const [root] = run(data) as never as Array<{
|
|
81
|
-
getIsExpanded: () => boolean
|
|
82
|
-
toggleExpanded: () => void
|
|
83
|
-
}>
|
|
84
|
-
expect(root!.getIsExpanded()).toBe(false)
|
|
85
|
-
root!.toggleExpanded()
|
|
86
|
-
expect(expandedState['1']).toBe(true)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('promotes a row whose parent is missing rather than dropping it', () => {
|
|
90
|
-
// Parent 99 does not exist - e.g. the filter removed it. The row must still
|
|
91
|
-
// be reachable.
|
|
92
|
-
const orphaned: Node[] = [
|
|
93
|
-
{ id: 1, parent: null, name: 'root' },
|
|
94
|
-
{ id: 2, parent: 99, name: 'orphan' },
|
|
95
|
-
]
|
|
96
|
-
expect(ids(run(orphaned))).toEqual(['1', '2'])
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('survives a parent cycle', () => {
|
|
100
|
-
const cyclic: Node[] = [
|
|
101
|
-
{ id: 1, parent: 2, name: 'a' },
|
|
102
|
-
{ id: 2, parent: 1, name: 'b' },
|
|
103
|
-
]
|
|
104
|
-
// Neither is a root by parent link, so nothing is emitted rather than
|
|
105
|
-
// recursing forever. The guard is what matters.
|
|
106
|
-
expect(() => run(cyclic)).not.toThrow()
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('treats a self-parenting row as a root', () => {
|
|
110
|
-
expect(ids(run([{ id: 1, parent: 1, name: 'self' }]))).toEqual(['1'])
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('passes an empty set straight through', () => {
|
|
114
|
-
expect(run([])).toEqual([])
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('honours a custom idField', () => {
|
|
118
|
-
const keyed = [
|
|
119
|
-
{ key: 10, parent: null },
|
|
120
|
-
{ key: 11, parent: 10 },
|
|
121
|
-
] as never as Node[]
|
|
122
|
-
const out = createTreeRowModel<never>({ parentField: 'parent', idField: 'key' })({
|
|
123
|
-
table,
|
|
124
|
-
rows: rowsFrom(keyed),
|
|
125
|
-
} as never) as never as Array<{ subRows: never[] }>
|
|
126
|
-
expect(out).toHaveLength(1)
|
|
127
|
-
expect(out[0]!.subRows).toHaveLength(1)
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
describe('flattenTreeData', () => {
|
|
132
|
-
const nested = [
|
|
133
|
-
{ id: 1, name: 'a', kids: [{ id: 2, name: 'a1', kids: [{ id: 3, name: 'a1a' }] }] },
|
|
134
|
-
{ id: 4, name: 'b' },
|
|
135
|
-
]
|
|
136
|
-
|
|
137
|
-
it('emits children directly after their parent', () => {
|
|
138
|
-
const flat = flattenTreeData(nested as never, { childrenField: 'kids' })
|
|
139
|
-
expect(flat.map((r) => (r as never as { id: number }).id)).toEqual([1, 2, 3, 4])
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('stamps the parent link the tree model reads', () => {
|
|
143
|
-
const flat = flattenTreeData(nested as never, { childrenField: 'kids' }) as never as Array<
|
|
144
|
-
Record<string, unknown>
|
|
145
|
-
>
|
|
146
|
-
expect(flat[0]!.__parentId).toBeNull()
|
|
147
|
-
expect(flat[1]!.__parentId).toBe(1)
|
|
148
|
-
expect(flat[2]!.__parentId).toBe(2)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('round-trips into createTreeRowModel', () => {
|
|
152
|
-
const flat = flattenTreeData(nested as never, { childrenField: 'kids' })
|
|
153
|
-
const out = createTreeRowModel<never>({ parentField: '__parentId' })({
|
|
154
|
-
table,
|
|
155
|
-
rows: rowsFrom(flat as never as Node[]),
|
|
156
|
-
} as never) as never as Array<{ id: string; subRows: never[] }>
|
|
157
|
-
expect(out.map((r) => r.id)).toEqual(['1', '4'])
|
|
158
|
-
expect(out[0]!.subRows).toHaveLength(1)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('accepts a custom parent field name', () => {
|
|
162
|
-
const flat = flattenTreeData(nested as never, {
|
|
163
|
-
childrenField: 'kids',
|
|
164
|
-
parentField: 'pid',
|
|
165
|
-
}) as never as Array<Record<string, unknown>>
|
|
166
|
-
expect(flat[1]!.pid).toBe(1)
|
|
167
|
-
})
|
|
168
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createTreeRowModel, flattenTreeData } from './core'
|
|
3
|
+
|
|
4
|
+
type Node = { id: number; parent: number | null; name: string }
|
|
5
|
+
|
|
6
|
+
/** Minimal Row stand-ins - the model only reads `original`, `id` and spreads. */
|
|
7
|
+
function rowsFrom(data: Node[]) {
|
|
8
|
+
return data.map((original, index) => ({
|
|
9
|
+
id: String(original.id),
|
|
10
|
+
index,
|
|
11
|
+
original,
|
|
12
|
+
depth: 0,
|
|
13
|
+
getCanExpand: () => false,
|
|
14
|
+
getIsExpanded: () => false,
|
|
15
|
+
toggleExpanded: () => {},
|
|
16
|
+
getIsSelected: () => false,
|
|
17
|
+
toggleSelected: () => {},
|
|
18
|
+
getAllCells: () => [],
|
|
19
|
+
getCellValueByColumnId: (c: string) => (original as never as Record<string, unknown>)[c],
|
|
20
|
+
})) as never[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let expandedState: Record<string, boolean> = {}
|
|
24
|
+
const table = {
|
|
25
|
+
getState: () => ({ expanded: expandedState }),
|
|
26
|
+
setExpanded: (fn: (p: Record<string, boolean>) => Record<string, boolean>) => {
|
|
27
|
+
expandedState = fn(expandedState)
|
|
28
|
+
},
|
|
29
|
+
} as never
|
|
30
|
+
|
|
31
|
+
const run = (data: Node[], opts = { parentField: 'parent' }) =>
|
|
32
|
+
createTreeRowModel<never>(opts)({ table, rows: rowsFrom(data) } as never)
|
|
33
|
+
|
|
34
|
+
const ids = (rows: readonly { id: string }[]) => rows.map((r) => r.id)
|
|
35
|
+
|
|
36
|
+
describe('createTreeRowModel', () => {
|
|
37
|
+
const data: Node[] = [
|
|
38
|
+
{ id: 1, parent: null, name: 'root' },
|
|
39
|
+
{ id: 2, parent: 1, name: 'child a' },
|
|
40
|
+
{ id: 3, parent: 1, name: 'child b' },
|
|
41
|
+
{ id: 4, parent: 2, name: 'grandchild' },
|
|
42
|
+
{ id: 5, parent: null, name: 'other root' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
it('nests by parent id and returns only roots', () => {
|
|
46
|
+
const out = run(data)
|
|
47
|
+
expect(ids(out)).toEqual(['1', '5'])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('records depth down the chain', () => {
|
|
51
|
+
const [root] = run(data) as never as Array<{ depth: number; subRows: never[] }>
|
|
52
|
+
expect(root!.depth).toBe(0)
|
|
53
|
+
const child = (root!.subRows as never as Array<{ id: string; depth: number; subRows: never[] }>)[0]!
|
|
54
|
+
expect(child.depth).toBe(1)
|
|
55
|
+
expect((child.subRows as never as Array<{ depth: number }>)[0]!.depth).toBe(2)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('marks rows with children expandable and leaves not', () => {
|
|
59
|
+
const [root, other] = run(data) as never as Array<{ getCanExpand: () => boolean }>
|
|
60
|
+
expect(root!.getCanExpand()).toBe(true)
|
|
61
|
+
expect(other!.getCanExpand()).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('counts descendants, not just direct children', () => {
|
|
65
|
+
const [root] = run(data) as never as Array<{ leafCount: number }>
|
|
66
|
+
// children a + b, plus a's grandchild
|
|
67
|
+
expect(root!.leafCount).toBe(3)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('tags rows so they are not mistaken for group banners', async () => {
|
|
71
|
+
const { isGroupRow } = await import('./cell-values')
|
|
72
|
+
const [root] = run(data) as never as Array<never>
|
|
73
|
+
expect((root as never as { __treeRow: boolean }).__treeRow).toBe(true)
|
|
74
|
+
// Expandable, but must NOT render as a full-width banner.
|
|
75
|
+
expect(isGroupRow(root!)).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('drives expansion through the table state', () => {
|
|
79
|
+
expandedState = {}
|
|
80
|
+
const [root] = run(data) as never as Array<{
|
|
81
|
+
getIsExpanded: () => boolean
|
|
82
|
+
toggleExpanded: () => void
|
|
83
|
+
}>
|
|
84
|
+
expect(root!.getIsExpanded()).toBe(false)
|
|
85
|
+
root!.toggleExpanded()
|
|
86
|
+
expect(expandedState['1']).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('promotes a row whose parent is missing rather than dropping it', () => {
|
|
90
|
+
// Parent 99 does not exist - e.g. the filter removed it. The row must still
|
|
91
|
+
// be reachable.
|
|
92
|
+
const orphaned: Node[] = [
|
|
93
|
+
{ id: 1, parent: null, name: 'root' },
|
|
94
|
+
{ id: 2, parent: 99, name: 'orphan' },
|
|
95
|
+
]
|
|
96
|
+
expect(ids(run(orphaned))).toEqual(['1', '2'])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('survives a parent cycle', () => {
|
|
100
|
+
const cyclic: Node[] = [
|
|
101
|
+
{ id: 1, parent: 2, name: 'a' },
|
|
102
|
+
{ id: 2, parent: 1, name: 'b' },
|
|
103
|
+
]
|
|
104
|
+
// Neither is a root by parent link, so nothing is emitted rather than
|
|
105
|
+
// recursing forever. The guard is what matters.
|
|
106
|
+
expect(() => run(cyclic)).not.toThrow()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('treats a self-parenting row as a root', () => {
|
|
110
|
+
expect(ids(run([{ id: 1, parent: 1, name: 'self' }]))).toEqual(['1'])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('passes an empty set straight through', () => {
|
|
114
|
+
expect(run([])).toEqual([])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('honours a custom idField', () => {
|
|
118
|
+
const keyed = [
|
|
119
|
+
{ key: 10, parent: null },
|
|
120
|
+
{ key: 11, parent: 10 },
|
|
121
|
+
] as never as Node[]
|
|
122
|
+
const out = createTreeRowModel<never>({ parentField: 'parent', idField: 'key' })({
|
|
123
|
+
table,
|
|
124
|
+
rows: rowsFrom(keyed),
|
|
125
|
+
} as never) as never as Array<{ subRows: never[] }>
|
|
126
|
+
expect(out).toHaveLength(1)
|
|
127
|
+
expect(out[0]!.subRows).toHaveLength(1)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('flattenTreeData', () => {
|
|
132
|
+
const nested = [
|
|
133
|
+
{ id: 1, name: 'a', kids: [{ id: 2, name: 'a1', kids: [{ id: 3, name: 'a1a' }] }] },
|
|
134
|
+
{ id: 4, name: 'b' },
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
it('emits children directly after their parent', () => {
|
|
138
|
+
const flat = flattenTreeData(nested as never, { childrenField: 'kids' })
|
|
139
|
+
expect(flat.map((r) => (r as never as { id: number }).id)).toEqual([1, 2, 3, 4])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('stamps the parent link the tree model reads', () => {
|
|
143
|
+
const flat = flattenTreeData(nested as never, { childrenField: 'kids' }) as never as Array<
|
|
144
|
+
Record<string, unknown>
|
|
145
|
+
>
|
|
146
|
+
expect(flat[0]!.__parentId).toBeNull()
|
|
147
|
+
expect(flat[1]!.__parentId).toBe(1)
|
|
148
|
+
expect(flat[2]!.__parentId).toBe(2)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('round-trips into createTreeRowModel', () => {
|
|
152
|
+
const flat = flattenTreeData(nested as never, { childrenField: 'kids' })
|
|
153
|
+
const out = createTreeRowModel<never>({ parentField: '__parentId' })({
|
|
154
|
+
table,
|
|
155
|
+
rows: rowsFrom(flat as never as Node[]),
|
|
156
|
+
} as never) as never as Array<{ id: string; subRows: never[] }>
|
|
157
|
+
expect(out.map((r) => r.id)).toEqual(['1', '4'])
|
|
158
|
+
expect(out[0]!.subRows).toHaveLength(1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('accepts a custom parent field name', () => {
|
|
162
|
+
const flat = flattenTreeData(nested as never, {
|
|
163
|
+
childrenField: 'kids',
|
|
164
|
+
parentField: 'pid',
|
|
165
|
+
}) as never as Array<Record<string, unknown>>
|
|
166
|
+
expect(flat[1]!.pid).toBe(1)
|
|
167
|
+
})
|
|
168
|
+
})
|