@svgrid/grid 2.2.26 → 2.2.27
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 +199 -145
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +734 -728
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +250 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.css +2723 -2719
- package/dist/SvGrid.svelte +3333 -3333
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +287 -287
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +131 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- 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 +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/a11y/dismissable.d.ts +11 -0
- package/dist/a11y/dismissable.js +21 -0
- package/dist/ai.js +3 -3
- package/dist/cdn/GridMenus-BzxEYNtW.js +421 -0
- package/dist/cdn/GridMenus-Dr-nGEXn.js +417 -0
- package/dist/cdn/{SvDateTimePicker-CYC7gRER.js → SvDateTimePicker-B1sDc9Pz.js} +451 -444
- package/dist/cdn/{SvDateTimePicker-DtQKd4Ns.js → SvDateTimePicker-Bh0FTs1E.js} +114 -107
- package/dist/cdn/{SvGridChart-BhnFdLd0.js → SvGridChart-BKQsO5fZ.js} +1 -1
- package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Bgk4hDs8.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-DSkHIQDd.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-Cmx9DHAS.js → SvGridChartPanel-DkCc2Z7b.js} +1 -1
- package/dist/cdn/{SvGridChartView-B40DTJSX.js → SvGridChartView-BrbVaELm.js} +1 -1
- package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-i9rWvSKg.js} +1 -1
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CGxxopQ4.js} +1 -1
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D08sVmAy.js} +1 -1
- package/dist/cdn/{src-DysW9qbk.js → src-Cw8d9U8j.js} +5753 -5753
- package/dist/cdn/{src-bYfr_7aR.js → src-cgs1wcUS.js} +8102 -8102
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +30 -13
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +734 -728
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +250 -250
- package/src/SvDropDownList.svelte +374 -374
- 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.controller.svelte.ts +3239 -3239
- package/src/SvGrid.css +2723 -2719
- package/src/SvGrid.svelte +3333 -3333
- package/src/SvGrid.types.ts +1613 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +287 -287
- 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 +281 -281
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +131 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- 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 +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -95
- package/src/a11y/dismissable.ts +114 -94
- 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 +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- 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/createMenu.svelte.ts +225 -225
- 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 +319 -319
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +713 -712
- package/src/js-scroller.svelte.ts +173 -173
- 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 +172 -172
- package/src/menus.test.ts +700 -700
- package/src/menus.ts +652 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-scroll.test.ts +102 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- 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 +296 -296
- package/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-GfCAHgkZ.js +0 -420
- package/dist/cdn/GridMenus-GiVNSeDU.js +0 -416
package/src/SvCalendar.test.ts
CHANGED
|
@@ -1,226 +1,226 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component tests for SvCalendar. Mounts in jsdom and drives real DOM clicks /
|
|
3
|
-
* keydowns, asserting the emitted selection + navigation. Discriminating: each
|
|
4
|
-
* checks the actual selected value(s), not just that something rendered.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, expect, it } from 'vitest'
|
|
7
|
-
import { mount, unmount, flushSync } from 'svelte'
|
|
8
|
-
import SvCalendar from './SvCalendar.svelte'
|
|
9
|
-
|
|
10
|
-
type Mounted = { target: HTMLElement; destroy: () => void }
|
|
11
|
-
|
|
12
|
-
function mountCal(props: Record<string, unknown>): Mounted {
|
|
13
|
-
const target = document.createElement('div')
|
|
14
|
-
document.body.appendChild(target)
|
|
15
|
-
const app = mount(SvCalendar, { target, props: props as any })
|
|
16
|
-
flushSync()
|
|
17
|
-
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const dayButton = (target: HTMLElement, label: string) =>
|
|
21
|
-
Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__day')).find(
|
|
22
|
-
(b) => b.textContent?.trim() === label && !b.classList.contains('is-outside'),
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
describe('SvCalendar render', () => {
|
|
26
|
-
it('renders the seeded month title and a 6x7 grid', () => {
|
|
27
|
-
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
28
|
-
try {
|
|
29
|
-
expect(target.querySelector('.sv-cal__title')?.textContent).toContain('2026')
|
|
30
|
-
const days = target.querySelectorAll('.sv-cal__day')
|
|
31
|
-
expect(days.length).toBe(42) // 6 weeks x 7
|
|
32
|
-
} finally {
|
|
33
|
-
destroy()
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('renders bare by default, but wraps in SvField chrome when label/error given', () => {
|
|
38
|
-
const bare = mountCal({ value: new Date(2026, 5, 15) })
|
|
39
|
-
try {
|
|
40
|
-
expect(bare.target.querySelector('.sv-field')).toBeNull() // bare
|
|
41
|
-
expect(bare.target.querySelector('.sv-cal')).not.toBeNull()
|
|
42
|
-
} finally { bare.destroy() }
|
|
43
|
-
const chrome = mountCal({ value: new Date(2026, 5, 15), label: 'Start date', error: 'Required', required: true })
|
|
44
|
-
try {
|
|
45
|
-
expect(chrome.target.querySelector('.sv-field')).not.toBeNull()
|
|
46
|
-
expect(chrome.target.querySelector('.sv-field__label')?.textContent).toContain('Start date')
|
|
47
|
-
expect(chrome.target.querySelector('.sv-field__error')?.textContent).toContain('Required')
|
|
48
|
-
expect(chrome.target.querySelector('.sv-cal.sv-cal--invalid')).toBeNull() // invalid not set
|
|
49
|
-
} finally { chrome.destroy() }
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('marks the selected day', () => {
|
|
53
|
-
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
54
|
-
try {
|
|
55
|
-
const sel = target.querySelector('.sv-cal__day.is-selected')
|
|
56
|
-
expect(sel?.textContent?.trim()).toBe('15')
|
|
57
|
-
} finally {
|
|
58
|
-
destroy()
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('shows week numbers when enabled', () => {
|
|
63
|
-
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), weekNumbers: true, firstDayOfWeek: 1 })
|
|
64
|
-
try {
|
|
65
|
-
expect(target.querySelectorAll('.sv-cal__wk').length).toBe(6)
|
|
66
|
-
} finally {
|
|
67
|
-
destroy()
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
describe('SvCalendar selection', () => {
|
|
73
|
-
it('emits the clicked day (mode one)', () => {
|
|
74
|
-
let got: Date[] = []
|
|
75
|
-
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), onChange: (d: Date[]) => (got = d) })
|
|
76
|
-
try {
|
|
77
|
-
dayButton(target, '20')!.click()
|
|
78
|
-
flushSync()
|
|
79
|
-
expect(got).toHaveLength(1)
|
|
80
|
-
expect(got[0]!.getDate()).toBe(20)
|
|
81
|
-
expect(got[0]!.getMonth()).toBe(5)
|
|
82
|
-
} finally {
|
|
83
|
-
destroy()
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('range mode: two clicks emit the inclusive span', () => {
|
|
88
|
-
let got: Date[] = []
|
|
89
|
-
const { target, destroy } = mountCal({
|
|
90
|
-
value: null,
|
|
91
|
-
selectionMode: 'range',
|
|
92
|
-
displayMode: 'month',
|
|
93
|
-
onChange: (d: Date[]) => (got = d),
|
|
94
|
-
})
|
|
95
|
-
try {
|
|
96
|
-
// Seed view to a known month by selecting from it.
|
|
97
|
-
const cal = mountCal({ value: new Date(2026, 5, 1), selectionMode: 'range', onChange: (d: Date[]) => (got = d) })
|
|
98
|
-
dayButton(cal.target, '3')!.click()
|
|
99
|
-
flushSync()
|
|
100
|
-
dayButton(cal.target, '6')!.click()
|
|
101
|
-
flushSync()
|
|
102
|
-
expect(got.map((d) => d.getDate())).toEqual([3, 4, 5, 6])
|
|
103
|
-
cal.destroy()
|
|
104
|
-
} finally {
|
|
105
|
-
destroy()
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('does not select a disabled (out-of-range) day', () => {
|
|
110
|
-
let calls = 0
|
|
111
|
-
const { target, destroy } = mountCal({
|
|
112
|
-
value: new Date(2026, 5, 15),
|
|
113
|
-
min: new Date(2026, 5, 10),
|
|
114
|
-
onChange: () => (calls += 1),
|
|
115
|
-
})
|
|
116
|
-
try {
|
|
117
|
-
const five = dayButton(target, '5')
|
|
118
|
-
expect(five?.disabled).toBe(true)
|
|
119
|
-
five?.click()
|
|
120
|
-
flushSync()
|
|
121
|
-
expect(calls).toBe(0)
|
|
122
|
-
} finally {
|
|
123
|
-
destroy()
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
describe('SvCalendar navigation', () => {
|
|
129
|
-
it('prev/next steps the month', () => {
|
|
130
|
-
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
131
|
-
try {
|
|
132
|
-
const [prev, , next] = target.querySelectorAll<HTMLButtonElement>('.sv-cal__nav, .sv-cal__title')
|
|
133
|
-
// next button is the 2nd .sv-cal__nav; grab by aria-label
|
|
134
|
-
target.querySelector<HTMLButtonElement>('[aria-label="Next"]')!.click()
|
|
135
|
-
flushSync()
|
|
136
|
-
expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('july')
|
|
137
|
-
target.querySelector<HTMLButtonElement>('[aria-label="Previous"]')!.click()
|
|
138
|
-
target.querySelector<HTMLButtonElement>('[aria-label="Previous"]')!.click()
|
|
139
|
-
flushSync()
|
|
140
|
-
expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('may')
|
|
141
|
-
void prev; void next
|
|
142
|
-
} finally {
|
|
143
|
-
destroy()
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('title drills month -> year -> decade', () => {
|
|
148
|
-
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
149
|
-
try {
|
|
150
|
-
const title = () => target.querySelector<HTMLButtonElement>('.sv-cal__title')!
|
|
151
|
-
title().click() // -> year
|
|
152
|
-
flushSync()
|
|
153
|
-
expect(target.querySelectorAll('.sv-cal__cell').length).toBe(12) // 12 months
|
|
154
|
-
title().click() // -> decade
|
|
155
|
-
flushSync()
|
|
156
|
-
expect(title().textContent).toContain('2020 - 2029')
|
|
157
|
-
} finally {
|
|
158
|
-
destroy()
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('year mode: clicking a month drills back to that month', () => {
|
|
163
|
-
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
164
|
-
try {
|
|
165
|
-
target.querySelector<HTMLButtonElement>('.sv-cal__title')!.click() // year mode
|
|
166
|
-
flushSync()
|
|
167
|
-
const jan = Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__cell'))[0]!
|
|
168
|
-
jan.click()
|
|
169
|
-
flushSync()
|
|
170
|
-
expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('january')
|
|
171
|
-
} finally {
|
|
172
|
-
destroy()
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('steps the month with animation enabled (animation never blocks navigation)', () => {
|
|
177
|
-
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), animate: 'slide' })
|
|
178
|
-
try {
|
|
179
|
-
target.querySelector<HTMLButtonElement>('[aria-label="Next"]')!.click()
|
|
180
|
-
flushSync()
|
|
181
|
-
expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('july')
|
|
182
|
-
} finally {
|
|
183
|
-
destroy()
|
|
184
|
-
}
|
|
185
|
-
})
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
describe('SvCalendar presets + tooltips', () => {
|
|
189
|
-
it('renders preset buttons and applies a range preset', () => {
|
|
190
|
-
let got: Date[] = []
|
|
191
|
-
const { target, destroy } = mountCal({
|
|
192
|
-
value: null,
|
|
193
|
-
selectionMode: 'range',
|
|
194
|
-
presets: [
|
|
195
|
-
{ label: 'First five', value: [new Date(2026, 5, 1), new Date(2026, 5, 5)] },
|
|
196
|
-
{ label: 'Just the 10th', value: new Date(2026, 5, 10) },
|
|
197
|
-
],
|
|
198
|
-
onChange: (d: Date[]) => (got = d),
|
|
199
|
-
})
|
|
200
|
-
try {
|
|
201
|
-
const buttons = Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__preset'))
|
|
202
|
-
expect(buttons.map((b) => b.textContent?.trim())).toEqual(['First five', 'Just the 10th'])
|
|
203
|
-
buttons[0]!.click()
|
|
204
|
-
flushSync()
|
|
205
|
-
expect(got.map((d) => d.getDate())).toEqual([1, 2, 3, 4, 5])
|
|
206
|
-
buttons[1]!.click()
|
|
207
|
-
flushSync()
|
|
208
|
-
expect(got.map((d) => d.getDate())).toEqual([10])
|
|
209
|
-
} finally {
|
|
210
|
-
destroy()
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('sets a native title tooltip only on matching days', () => {
|
|
215
|
-
const { target, destroy } = mountCal({
|
|
216
|
-
value: new Date(2026, 5, 15),
|
|
217
|
-
dateTooltip: (d: Date) => (d.getDate() === 20 && d.getMonth() === 5 ? 'Release day' : undefined),
|
|
218
|
-
})
|
|
219
|
-
try {
|
|
220
|
-
expect(dayButton(target, '20')?.title).toBe('Release day')
|
|
221
|
-
expect(dayButton(target, '10')?.title).toBe('')
|
|
222
|
-
} finally {
|
|
223
|
-
destroy()
|
|
224
|
-
}
|
|
225
|
-
})
|
|
226
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for SvCalendar. Mounts in jsdom and drives real DOM clicks /
|
|
3
|
+
* keydowns, asserting the emitted selection + navigation. Discriminating: each
|
|
4
|
+
* checks the actual selected value(s), not just that something rendered.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
8
|
+
import SvCalendar from './SvCalendar.svelte'
|
|
9
|
+
|
|
10
|
+
type Mounted = { target: HTMLElement; destroy: () => void }
|
|
11
|
+
|
|
12
|
+
function mountCal(props: Record<string, unknown>): Mounted {
|
|
13
|
+
const target = document.createElement('div')
|
|
14
|
+
document.body.appendChild(target)
|
|
15
|
+
const app = mount(SvCalendar, { target, props: props as any })
|
|
16
|
+
flushSync()
|
|
17
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const dayButton = (target: HTMLElement, label: string) =>
|
|
21
|
+
Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__day')).find(
|
|
22
|
+
(b) => b.textContent?.trim() === label && !b.classList.contains('is-outside'),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
describe('SvCalendar render', () => {
|
|
26
|
+
it('renders the seeded month title and a 6x7 grid', () => {
|
|
27
|
+
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
28
|
+
try {
|
|
29
|
+
expect(target.querySelector('.sv-cal__title')?.textContent).toContain('2026')
|
|
30
|
+
const days = target.querySelectorAll('.sv-cal__day')
|
|
31
|
+
expect(days.length).toBe(42) // 6 weeks x 7
|
|
32
|
+
} finally {
|
|
33
|
+
destroy()
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('renders bare by default, but wraps in SvField chrome when label/error given', () => {
|
|
38
|
+
const bare = mountCal({ value: new Date(2026, 5, 15) })
|
|
39
|
+
try {
|
|
40
|
+
expect(bare.target.querySelector('.sv-field')).toBeNull() // bare
|
|
41
|
+
expect(bare.target.querySelector('.sv-cal')).not.toBeNull()
|
|
42
|
+
} finally { bare.destroy() }
|
|
43
|
+
const chrome = mountCal({ value: new Date(2026, 5, 15), label: 'Start date', error: 'Required', required: true })
|
|
44
|
+
try {
|
|
45
|
+
expect(chrome.target.querySelector('.sv-field')).not.toBeNull()
|
|
46
|
+
expect(chrome.target.querySelector('.sv-field__label')?.textContent).toContain('Start date')
|
|
47
|
+
expect(chrome.target.querySelector('.sv-field__error')?.textContent).toContain('Required')
|
|
48
|
+
expect(chrome.target.querySelector('.sv-cal.sv-cal--invalid')).toBeNull() // invalid not set
|
|
49
|
+
} finally { chrome.destroy() }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('marks the selected day', () => {
|
|
53
|
+
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
54
|
+
try {
|
|
55
|
+
const sel = target.querySelector('.sv-cal__day.is-selected')
|
|
56
|
+
expect(sel?.textContent?.trim()).toBe('15')
|
|
57
|
+
} finally {
|
|
58
|
+
destroy()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('shows week numbers when enabled', () => {
|
|
63
|
+
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), weekNumbers: true, firstDayOfWeek: 1 })
|
|
64
|
+
try {
|
|
65
|
+
expect(target.querySelectorAll('.sv-cal__wk').length).toBe(6)
|
|
66
|
+
} finally {
|
|
67
|
+
destroy()
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('SvCalendar selection', () => {
|
|
73
|
+
it('emits the clicked day (mode one)', () => {
|
|
74
|
+
let got: Date[] = []
|
|
75
|
+
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), onChange: (d: Date[]) => (got = d) })
|
|
76
|
+
try {
|
|
77
|
+
dayButton(target, '20')!.click()
|
|
78
|
+
flushSync()
|
|
79
|
+
expect(got).toHaveLength(1)
|
|
80
|
+
expect(got[0]!.getDate()).toBe(20)
|
|
81
|
+
expect(got[0]!.getMonth()).toBe(5)
|
|
82
|
+
} finally {
|
|
83
|
+
destroy()
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('range mode: two clicks emit the inclusive span', () => {
|
|
88
|
+
let got: Date[] = []
|
|
89
|
+
const { target, destroy } = mountCal({
|
|
90
|
+
value: null,
|
|
91
|
+
selectionMode: 'range',
|
|
92
|
+
displayMode: 'month',
|
|
93
|
+
onChange: (d: Date[]) => (got = d),
|
|
94
|
+
})
|
|
95
|
+
try {
|
|
96
|
+
// Seed view to a known month by selecting from it.
|
|
97
|
+
const cal = mountCal({ value: new Date(2026, 5, 1), selectionMode: 'range', onChange: (d: Date[]) => (got = d) })
|
|
98
|
+
dayButton(cal.target, '3')!.click()
|
|
99
|
+
flushSync()
|
|
100
|
+
dayButton(cal.target, '6')!.click()
|
|
101
|
+
flushSync()
|
|
102
|
+
expect(got.map((d) => d.getDate())).toEqual([3, 4, 5, 6])
|
|
103
|
+
cal.destroy()
|
|
104
|
+
} finally {
|
|
105
|
+
destroy()
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('does not select a disabled (out-of-range) day', () => {
|
|
110
|
+
let calls = 0
|
|
111
|
+
const { target, destroy } = mountCal({
|
|
112
|
+
value: new Date(2026, 5, 15),
|
|
113
|
+
min: new Date(2026, 5, 10),
|
|
114
|
+
onChange: () => (calls += 1),
|
|
115
|
+
})
|
|
116
|
+
try {
|
|
117
|
+
const five = dayButton(target, '5')
|
|
118
|
+
expect(five?.disabled).toBe(true)
|
|
119
|
+
five?.click()
|
|
120
|
+
flushSync()
|
|
121
|
+
expect(calls).toBe(0)
|
|
122
|
+
} finally {
|
|
123
|
+
destroy()
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('SvCalendar navigation', () => {
|
|
129
|
+
it('prev/next steps the month', () => {
|
|
130
|
+
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
131
|
+
try {
|
|
132
|
+
const [prev, , next] = target.querySelectorAll<HTMLButtonElement>('.sv-cal__nav, .sv-cal__title')
|
|
133
|
+
// next button is the 2nd .sv-cal__nav; grab by aria-label
|
|
134
|
+
target.querySelector<HTMLButtonElement>('[aria-label="Next"]')!.click()
|
|
135
|
+
flushSync()
|
|
136
|
+
expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('july')
|
|
137
|
+
target.querySelector<HTMLButtonElement>('[aria-label="Previous"]')!.click()
|
|
138
|
+
target.querySelector<HTMLButtonElement>('[aria-label="Previous"]')!.click()
|
|
139
|
+
flushSync()
|
|
140
|
+
expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('may')
|
|
141
|
+
void prev; void next
|
|
142
|
+
} finally {
|
|
143
|
+
destroy()
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('title drills month -> year -> decade', () => {
|
|
148
|
+
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
149
|
+
try {
|
|
150
|
+
const title = () => target.querySelector<HTMLButtonElement>('.sv-cal__title')!
|
|
151
|
+
title().click() // -> year
|
|
152
|
+
flushSync()
|
|
153
|
+
expect(target.querySelectorAll('.sv-cal__cell').length).toBe(12) // 12 months
|
|
154
|
+
title().click() // -> decade
|
|
155
|
+
flushSync()
|
|
156
|
+
expect(title().textContent).toContain('2020 - 2029')
|
|
157
|
+
} finally {
|
|
158
|
+
destroy()
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('year mode: clicking a month drills back to that month', () => {
|
|
163
|
+
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15) })
|
|
164
|
+
try {
|
|
165
|
+
target.querySelector<HTMLButtonElement>('.sv-cal__title')!.click() // year mode
|
|
166
|
+
flushSync()
|
|
167
|
+
const jan = Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__cell'))[0]!
|
|
168
|
+
jan.click()
|
|
169
|
+
flushSync()
|
|
170
|
+
expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('january')
|
|
171
|
+
} finally {
|
|
172
|
+
destroy()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('steps the month with animation enabled (animation never blocks navigation)', () => {
|
|
177
|
+
const { target, destroy } = mountCal({ value: new Date(2026, 5, 15), animate: 'slide' })
|
|
178
|
+
try {
|
|
179
|
+
target.querySelector<HTMLButtonElement>('[aria-label="Next"]')!.click()
|
|
180
|
+
flushSync()
|
|
181
|
+
expect(target.querySelector('.sv-cal__title')?.textContent?.toLowerCase()).toContain('july')
|
|
182
|
+
} finally {
|
|
183
|
+
destroy()
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('SvCalendar presets + tooltips', () => {
|
|
189
|
+
it('renders preset buttons and applies a range preset', () => {
|
|
190
|
+
let got: Date[] = []
|
|
191
|
+
const { target, destroy } = mountCal({
|
|
192
|
+
value: null,
|
|
193
|
+
selectionMode: 'range',
|
|
194
|
+
presets: [
|
|
195
|
+
{ label: 'First five', value: [new Date(2026, 5, 1), new Date(2026, 5, 5)] },
|
|
196
|
+
{ label: 'Just the 10th', value: new Date(2026, 5, 10) },
|
|
197
|
+
],
|
|
198
|
+
onChange: (d: Date[]) => (got = d),
|
|
199
|
+
})
|
|
200
|
+
try {
|
|
201
|
+
const buttons = Array.from(target.querySelectorAll<HTMLButtonElement>('.sv-cal__preset'))
|
|
202
|
+
expect(buttons.map((b) => b.textContent?.trim())).toEqual(['First five', 'Just the 10th'])
|
|
203
|
+
buttons[0]!.click()
|
|
204
|
+
flushSync()
|
|
205
|
+
expect(got.map((d) => d.getDate())).toEqual([1, 2, 3, 4, 5])
|
|
206
|
+
buttons[1]!.click()
|
|
207
|
+
flushSync()
|
|
208
|
+
expect(got.map((d) => d.getDate())).toEqual([10])
|
|
209
|
+
} finally {
|
|
210
|
+
destroy()
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('sets a native title tooltip only on matching days', () => {
|
|
215
|
+
const { target, destroy } = mountCal({
|
|
216
|
+
value: new Date(2026, 5, 15),
|
|
217
|
+
dateTooltip: (d: Date) => (d.getDate() === 20 && d.getMonth() === 5 ? 'Release day' : undefined),
|
|
218
|
+
})
|
|
219
|
+
try {
|
|
220
|
+
expect(dayButton(target, '20')?.title).toBe('Release day')
|
|
221
|
+
expect(dayButton(target, '10')?.title).toBe('')
|
|
222
|
+
} finally {
|
|
223
|
+
destroy()
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
})
|