@happyvertical/smrt-ui 0.53.1 → 0.53.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/dist/actions/__tests__/ripple.test.js +0 -82
- package/dist/actions/__tests__/swipe-dismiss.test.js +0 -69
- package/dist/components/calendar/__tests__/CalendarView.test.js +0 -226
- package/dist/components/calendar/__tests__/calendar-model.test.js +0 -138
- package/dist/components/chat/__tests__/chat-primitives.test.js +0 -118
- package/dist/components/data/__tests__/CollectionList.test.js +0 -37
- package/dist/components/data/__tests__/DataTable.test.js +0 -807
- package/dist/components/data/__tests__/DataTableConformance.test.js +0 -212
- package/dist/components/data/__tests__/DataTableController.test.js +0 -371
- package/dist/components/data/__tests__/DataTableIdentity.test.js +0 -29
- package/dist/components/data/__tests__/DataTableLayout.test.js +0 -195
- package/dist/components/data/__tests__/DataTablePerformance.test.js +0 -21
- package/dist/components/data/__tests__/DataTableResponsive.test.js +0 -80
- package/dist/components/data/__tests__/DataTableVirtualization.test.js +0 -80
- package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +0 -255
- package/dist/components/data/__tests__/SortableHeader.test.js +0 -101
- package/dist/components/data/__tests__/data-surface.test.js +0 -746
- package/dist/components/data/__tests__/data-table-helpers.test.js +0 -36
- package/dist/components/data/__tests__/data-table-surface.test.js +0 -49
- package/dist/components/data/__tests__/link-step-surface.test.js +0 -172
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +0 -71
- package/dist/components/data/__tests__/list-sort.test.js +0 -91
- package/dist/components/data/__tests__/mounted-data-surface.test.js +0 -794
- package/dist/components/data/__tests__/responsive-table.fixture.svelte +0 -29
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +0 -7
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +0 -1
- package/dist/components/data/__tests__/surface-navigation.test.js +0 -121
- package/dist/components/display/__tests__/ConfidenceBadge.test.js +0 -109
- package/dist/components/display/__tests__/CurrencyDisplay.test.js +0 -304
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +0 -20
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +0 -19
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +0 -1
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +0 -7
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +0 -19
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +0 -1
- package/dist/components/display/__tests__/DateDisplay.test.js +0 -195
- package/dist/components/display/__tests__/Icon.test.js +0 -223
- package/dist/components/display/__tests__/StatusBadge.test.js +0 -191
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +0 -181
- package/dist/components/feedback/__tests__/LoadingOverlay.test.js +0 -113
- package/dist/components/feedback/__tests__/Modal.test.js +0 -72
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +0 -101
- package/dist/components/feedback/__tests__/ProgressBar.test.js +0 -89
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +0 -172
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +0 -9
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +0 -19
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +0 -1
- package/dist/components/feedback/__tests__/toast.test.js +0 -190
- package/dist/components/forms/__tests__/CameraCapture.test.js +0 -504
- package/dist/components/forms/__tests__/Combobox.test.js +0 -354
- package/dist/components/forms/__tests__/ControlHitArea.test.js +0 -73
- package/dist/components/forms/__tests__/DictationButton.test.js +0 -176
- package/dist/components/forms/__tests__/Fieldset.test.js +0 -37
- package/dist/components/forms/__tests__/Form.test.js +0 -201
- package/dist/components/forms/__tests__/FormActionBar.test.js +0 -22
- package/dist/components/forms/__tests__/FormGroup.test.js +0 -48
- package/dist/components/forms/__tests__/Input.test.js +0 -49
- package/dist/components/forms/__tests__/Listbox.test.js +0 -303
- package/dist/components/forms/__tests__/MultiSelect.test.js +0 -281
- package/dist/components/forms/__tests__/SearchInput.test.js +0 -92
- package/dist/components/forms/__tests__/Select.test.js +0 -37
- package/dist/components/forms/__tests__/SignaturePad.test.js +0 -338
- package/dist/components/forms/__tests__/StagedControlReview.test.js +0 -702
- package/dist/components/forms/__tests__/Textarea.test.js +0 -39
- package/dist/components/forms/__tests__/Toggle.test.js +0 -87
- package/dist/components/forms/__tests__/camera-capture-session.test.js +0 -207
- package/dist/components/forms/__tests__/capture-form-retry.test.js +0 -166
- package/dist/components/forms/__tests__/capture-form.fixture.svelte +0 -38
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +0 -10
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/capture-test-env.d.ts +0 -70
- package/dist/components/forms/__tests__/capture-test-env.d.ts.map +0 -1
- package/dist/components/forms/__tests__/capture-test-env.js +0 -122
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte +0 -15
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +0 -14
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +0 -19
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +0 -11
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte +0 -47
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-controls.test.js +0 -128
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +0 -41
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/control-interaction.test.js +0 -3421
- package/dist/components/forms/__tests__/control-proposals.test.js +0 -63
- package/dist/components/forms/__tests__/core-controls.fixture.svelte +0 -50
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/core-controls.test.js +0 -311
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +0 -25
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/dictation-recording.test.js +0 -582
- package/dist/components/forms/__tests__/dictation.test.js +0 -486
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte +0 -28
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +0 -11
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte +0 -16
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +0 -31
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-retry-form.test.js +0 -107
- package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +0 -69
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte +0 -27
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/long-press.test.js +0 -175
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +0 -27
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/native-file-field.test.js +0 -177
- package/dist/components/forms/__tests__/place-picker.fixture.svelte +0 -33
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +0 -28
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/segmented-posting.test.js +0 -92
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte +0 -25
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/signature-pad-logic.test.js +0 -54
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +0 -21
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +0 -23
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/staged-review.fixture.svelte +0 -56
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/touch-density.fixture.svelte +0 -34
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/touch-density.test.js +0 -66
- package/dist/components/forms/form-retry/__tests__/boundaries.test.js +0 -23
- package/dist/components/forms/form-retry/__tests__/content-digest.test.js +0 -70
- package/dist/components/forms/form-retry/__tests__/controller.test.js +0 -802
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +0 -82
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +0 -1
- package/dist/components/forms/form-retry/__tests__/fake-kit.js +0 -180
- package/dist/components/forms/form-retry/__tests__/form-draft.test.js +0 -169
- package/dist/components/forms/form-retry/__tests__/ssr.test.js +0 -37
- package/dist/components/forms/form-retry/__tests__/storage.d.ts +0 -26
- package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +0 -1
- package/dist/components/forms/form-retry/__tests__/storage.js +0 -63
- package/dist/components/forms/form-retry/__tests__/submission-key.test.js +0 -236
- package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +0 -334
- package/dist/components/layout/__tests__/Container.test.js +0 -62
- package/dist/components/layout/__tests__/EmptyState.test.js +0 -83
- package/dist/components/layout/__tests__/Footer.test.js +0 -50
- package/dist/components/layout/__tests__/Grid.test.js +0 -121
- package/dist/components/layout/__tests__/Header.test.js +0 -48
- package/dist/components/layout/__tests__/Masthead.test.js +0 -93
- package/dist/components/layout/__tests__/PageHeader.test.js +0 -166
- package/dist/components/layout/__tests__/SummaryCard.test.js +0 -104
- package/dist/components/nav/__tests__/FilterChips.test.js +0 -94
- package/dist/components/nav/__tests__/LinkTabs.test.js +0 -117
- package/dist/components/nav/__tests__/Tabs.test.js +0 -128
- package/dist/components/roles/__tests__/RoleSelector.test.js +0 -134
- package/dist/components/ui/__tests__/Badge.test.js +0 -76
- package/dist/components/ui/__tests__/Button.test.js +0 -166
- package/dist/components/ui/__tests__/Card.test.js +0 -103
- package/dist/components/ui/__tests__/IconToggle.test.js +0 -59
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +0 -30
- package/dist/components/ui/__tests__/Pagination.test.js +0 -113
- package/dist/components/ui/__tests__/Popover.test.js +0 -44
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +0 -125
- package/dist/components/ui/__tests__/gap-primitives-interactive.test.js +0 -112
- package/dist/components/ui/__tests__/gap-primitives.test.js +0 -84
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte +0 -13
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts +0 -19
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts.map +0 -1
- package/dist/components/ui/__tests__/interaction-primitives.test.js +0 -31
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +0 -39
- package/dist/i18n/__tests__/i18n.test.js +0 -74
- package/dist/i18n/__tests__/render-parity.spec.js +0 -37
- package/dist/svelte/__tests__/playground.test.js +0 -20
- package/dist/themes/__tests__/control-boundary-contrast.test.js +0 -112
- package/dist/themes/__tests__/create-theme.test.js +0 -79
- package/dist/themes/__tests__/css-generator.test.js +0 -129
- package/dist/themes/__tests__/happyvertical-theme.test.js +0 -231
- package/dist/themes/__tests__/registry.test.js +0 -43
- package/dist/themes/__tests__/styles-lockstep.test.js +0 -32
- package/dist/themes/__tests__/theme-provider.test.js +0 -147
- package/dist/themes/__tests__/theme-script.test.js +0 -130
- package/dist/themes/__tests__/token-aliases.test.js +0 -178
- package/dist/utils/forms/__tests__/formatters.test.js +0 -27
- package/dist/utils/theme/__tests__/color.test.js +0 -89
- package/dist/utils/theme/__tests__/typography.test.js +0 -11
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component tests for DateDisplay (Sweep S11, #1416).
|
|
3
|
-
*
|
|
4
|
-
* DateDisplay parses a Date/ISO/timestamp and renders a <time datetime> element
|
|
5
|
-
* (or a fallback <span> for null/invalid input). Expected formatted strings are
|
|
6
|
-
* derived via Intl.DateTimeFormat with the SAME options the component uses, so
|
|
7
|
-
* assertions stay locale/timezone-robust. Relative mode is exercised with
|
|
8
|
-
* offsets from `now`. Tests assert the real API from DateDisplay.svelte.
|
|
9
|
-
*/
|
|
10
|
-
import { render, screen } from '@testing-library/svelte';
|
|
11
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
-
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
13
|
-
import DateDisplay from '../DateDisplay.svelte';
|
|
14
|
-
const LOCALE = 'en-CA';
|
|
15
|
-
/** Build the component's expected output for an absolute format. */
|
|
16
|
-
function expected(date, options, locale = LOCALE) {
|
|
17
|
-
return new Intl.DateTimeFormat(locale, options).format(date);
|
|
18
|
-
}
|
|
19
|
-
const SHORT = { month: 'short', day: 'numeric' };
|
|
20
|
-
const MEDIUM = { year: 'numeric', month: 'short', day: 'numeric' };
|
|
21
|
-
const LONG = {
|
|
22
|
-
weekday: 'long',
|
|
23
|
-
year: 'numeric',
|
|
24
|
-
month: 'long',
|
|
25
|
-
day: 'numeric',
|
|
26
|
-
};
|
|
27
|
-
describe('DateDisplay', () => {
|
|
28
|
-
afterEach(() => vi.useRealTimers());
|
|
29
|
-
it.each([
|
|
30
|
-
'America/Edmonton',
|
|
31
|
-
'UTC',
|
|
32
|
-
'Asia/Tokyo',
|
|
33
|
-
])('formats timestamp dates and times in %s', (timeZone) => {
|
|
34
|
-
const instant = '2026-10-01T04:00:00Z';
|
|
35
|
-
const { container } = render(DateDisplay, {
|
|
36
|
-
props: { date: instant, timeZone, showTime: true },
|
|
37
|
-
});
|
|
38
|
-
expect(container.querySelector('time')?.textContent?.trim()).toBe(expected(new Date(instant), {
|
|
39
|
-
...MEDIUM,
|
|
40
|
-
timeZone,
|
|
41
|
-
hour: 'numeric',
|
|
42
|
-
minute: '2-digit',
|
|
43
|
-
}));
|
|
44
|
-
});
|
|
45
|
-
it.each([
|
|
46
|
-
'America/Edmonton',
|
|
47
|
-
'Asia/Tokyo',
|
|
48
|
-
])('keeps a bare calendar date in %s', (timeZone) => {
|
|
49
|
-
const { container } = render(DateDisplay, {
|
|
50
|
-
props: { date: '2026-09-30', timeZone },
|
|
51
|
-
});
|
|
52
|
-
expect(container.querySelector('time')).toHaveAttribute('datetime', '2026-09-30');
|
|
53
|
-
expect(container.querySelector('time')?.textContent?.trim()).toBe(expected(new Date('2026-09-30T00:00:00Z'), {
|
|
54
|
-
...MEDIUM,
|
|
55
|
-
timeZone: 'UTC',
|
|
56
|
-
}));
|
|
57
|
-
});
|
|
58
|
-
it('keeps date-only values stable in the default browser zone', () => {
|
|
59
|
-
render(DateDisplay, { props: { date: '2026-09-30' } });
|
|
60
|
-
expect(screen.getByText(expected(new Date('2026-09-30T00:00:00Z'), {
|
|
61
|
-
...MEDIUM,
|
|
62
|
-
timeZone: 'UTC',
|
|
63
|
-
}))).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
it.each([
|
|
66
|
-
'2026-02-30',
|
|
67
|
-
'2025-02-29',
|
|
68
|
-
'2026-13-01',
|
|
69
|
-
])('rejects impossible calendar date %s', (date) => {
|
|
70
|
-
render(DateDisplay, { props: { date, fallback: 'Invalid date' } });
|
|
71
|
-
expect(screen.getByText('Invalid date')).toBeInTheDocument();
|
|
72
|
-
});
|
|
73
|
-
it('accepts a leap day', () => {
|
|
74
|
-
const { container } = render(DateDisplay, {
|
|
75
|
-
props: { date: '2028-02-29' },
|
|
76
|
-
});
|
|
77
|
-
expect(container.querySelector('time')).toHaveAttribute('datetime', '2028-02-29');
|
|
78
|
-
});
|
|
79
|
-
it.each([
|
|
80
|
-
['2026-10-01T06:30:00Z', '2026-10-01T05:30:00Z', 'yesterday'],
|
|
81
|
-
['2026-10-01T05:30:00Z', '2026-10-01T06:30:00Z', 'tomorrow'],
|
|
82
|
-
['2026-10-01T06:30:00Z', '2026-10-01T07:30:00Z', 'today'],
|
|
83
|
-
['2026-03-09T06:30:00Z', '2026-03-08T07:30:00Z', 'yesterday'],
|
|
84
|
-
['2026-10-01T05:30:00Z', '2026-09-30', 'today'],
|
|
85
|
-
['2026-10-01T05:30:00Z', '2026-10-01', 'tomorrow'],
|
|
86
|
-
['2026-10-01T05:30:00Z', '2026-09-29', 'yesterday'],
|
|
87
|
-
])('uses calendar boundaries for %s → %s', (now, date, text) => {
|
|
88
|
-
vi.useFakeTimers({ toFake: ['Date'] });
|
|
89
|
-
vi.setSystemTime(new Date(now));
|
|
90
|
-
render(DateDisplay, {
|
|
91
|
-
props: { date, format: 'relative', timeZone: 'America/Edmonton' },
|
|
92
|
-
});
|
|
93
|
-
expect(screen.getByText(text)).toBeInTheDocument();
|
|
94
|
-
});
|
|
95
|
-
it('uses the fallback for an invalid explicit zone', () => {
|
|
96
|
-
render(DateDisplay, {
|
|
97
|
-
props: {
|
|
98
|
-
date: '2026-09-30T00:00:00Z',
|
|
99
|
-
timeZone: 'not/a/zone',
|
|
100
|
-
fallback: 'Invalid date',
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
expect(screen.getByText('Invalid date')).toBeInTheDocument();
|
|
104
|
-
});
|
|
105
|
-
// Use a fixed local-noon date so date-part is timezone-stable.
|
|
106
|
-
const sample = new Date(2024, 0, 15, 12, 0, 0); // 15 Jan 2024, local noon
|
|
107
|
-
it('renders a <time> element with the ISO datetime attribute', () => {
|
|
108
|
-
const { container } = render(DateDisplay, { props: { date: sample } });
|
|
109
|
-
const time = container.querySelector('time');
|
|
110
|
-
expect(time).toBeInTheDocument();
|
|
111
|
-
expect(time).toHaveAttribute('datetime', sample.toISOString());
|
|
112
|
-
});
|
|
113
|
-
it('formats with medium options by default', () => {
|
|
114
|
-
render(DateDisplay, { props: { date: sample } });
|
|
115
|
-
expect(screen.getByText(expected(sample, MEDIUM))).toBeInTheDocument();
|
|
116
|
-
});
|
|
117
|
-
it('formats with short options', () => {
|
|
118
|
-
render(DateDisplay, { props: { date: sample, format: 'short' } });
|
|
119
|
-
expect(screen.getByText(expected(sample, SHORT))).toBeInTheDocument();
|
|
120
|
-
});
|
|
121
|
-
it('formats with long options', () => {
|
|
122
|
-
render(DateDisplay, { props: { date: sample, format: 'long' } });
|
|
123
|
-
expect(screen.getByText(expected(sample, LONG))).toBeInTheDocument();
|
|
124
|
-
});
|
|
125
|
-
it('includes time when showTime is set', () => {
|
|
126
|
-
render(DateDisplay, { props: { date: sample, showTime: true } });
|
|
127
|
-
const withTime = expected(sample, {
|
|
128
|
-
...MEDIUM,
|
|
129
|
-
hour: 'numeric',
|
|
130
|
-
minute: '2-digit',
|
|
131
|
-
});
|
|
132
|
-
expect(screen.getByText(withTime)).toBeInTheDocument();
|
|
133
|
-
});
|
|
134
|
-
it('respects a custom locale', () => {
|
|
135
|
-
render(DateDisplay, {
|
|
136
|
-
props: { date: sample, format: 'long', locale: 'fr-CA' },
|
|
137
|
-
});
|
|
138
|
-
expect(screen.getByText(expected(sample, LONG, 'fr-CA'))).toBeInTheDocument();
|
|
139
|
-
});
|
|
140
|
-
it('parses an ISO string input', () => {
|
|
141
|
-
const iso = '2024-01-15T12:00:00.000Z';
|
|
142
|
-
const d = new Date(iso);
|
|
143
|
-
const { container } = render(DateDisplay, { props: { date: iso } });
|
|
144
|
-
expect(container.querySelector('time')).toHaveAttribute('datetime', d.toISOString());
|
|
145
|
-
});
|
|
146
|
-
it('parses a numeric timestamp input', () => {
|
|
147
|
-
const ts = sample.getTime();
|
|
148
|
-
const { container } = render(DateDisplay, { props: { date: ts } });
|
|
149
|
-
expect(container.querySelector('time')).toHaveAttribute('datetime', new Date(ts).toISOString());
|
|
150
|
-
});
|
|
151
|
-
it('renders the fallback span for null input', () => {
|
|
152
|
-
const { container } = render(DateDisplay, {
|
|
153
|
-
props: { date: null, fallback: 'No date' },
|
|
154
|
-
});
|
|
155
|
-
expect(container.querySelector('time')).not.toBeInTheDocument();
|
|
156
|
-
const span = screen.getByText('No date');
|
|
157
|
-
expect(span).toHaveClass('date-fallback');
|
|
158
|
-
});
|
|
159
|
-
it('renders the fallback for an invalid Date instance', () => {
|
|
160
|
-
render(DateDisplay, { props: { date: new Date(Number.NaN) } });
|
|
161
|
-
expect(screen.getByText('N/A')).toBeInTheDocument();
|
|
162
|
-
});
|
|
163
|
-
it('renders the default fallback for an invalid date string', () => {
|
|
164
|
-
render(DateDisplay, { props: { date: 'not-a-date' } });
|
|
165
|
-
expect(screen.getByText('N/A')).toBeInTheDocument();
|
|
166
|
-
});
|
|
167
|
-
it('renders "just now" for the current time in relative mode', () => {
|
|
168
|
-
render(DateDisplay, { props: { date: new Date(), format: 'relative' } });
|
|
169
|
-
expect(screen.getByText('just now')).toBeInTheDocument();
|
|
170
|
-
});
|
|
171
|
-
it('renders "yesterday" for ~1 day ago in relative mode', () => {
|
|
172
|
-
const d = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25h ago → 1 day
|
|
173
|
-
render(DateDisplay, { props: { date: d, format: 'relative' } });
|
|
174
|
-
expect(screen.getByText('yesterday')).toBeInTheDocument();
|
|
175
|
-
});
|
|
176
|
-
it('renders "tomorrow" for a near-future date in relative mode', () => {
|
|
177
|
-
const d = new Date(Date.now() + 23 * 60 * 60 * 1000); // 23h ahead → tomorrow
|
|
178
|
-
render(DateDisplay, { props: { date: d, format: 'relative' } });
|
|
179
|
-
expect(screen.getByText('tomorrow')).toBeInTheDocument();
|
|
180
|
-
});
|
|
181
|
-
it('appends a custom class to the time element', () => {
|
|
182
|
-
const { container } = render(DateDisplay, {
|
|
183
|
-
props: { date: sample, class: 'my-date' },
|
|
184
|
-
});
|
|
185
|
-
expect(container.querySelector('time')).toHaveClass('my-date');
|
|
186
|
-
});
|
|
187
|
-
it('is axe-clean with a date', async () => {
|
|
188
|
-
const { container } = render(DateDisplay, { props: { date: sample } });
|
|
189
|
-
await expectNoA11yViolations(container);
|
|
190
|
-
});
|
|
191
|
-
it('is axe-clean with the fallback', async () => {
|
|
192
|
-
const { container } = render(DateDisplay, { props: { date: null } });
|
|
193
|
-
await expectNoA11yViolations(container);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component tests for Icon (Sweep S11, #1416).
|
|
3
|
-
*
|
|
4
|
-
* Icon is a presentational SVG primitive: decorative by default (aria-hidden),
|
|
5
|
-
* informative when given an aria-label (role="img"). Tests assert the real API
|
|
6
|
-
* from Icon.svelte — preset/path resolution, size/color, and axe-cleanliness in
|
|
7
|
-
* both decorative and labelled modes.
|
|
8
|
-
*/
|
|
9
|
-
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
10
|
-
import { act, render } from '@testing-library/svelte';
|
|
11
|
-
import { hydrate, tick, unmount } from 'svelte';
|
|
12
|
-
import { createServer } from 'vite';
|
|
13
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
14
|
-
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
15
|
-
import Icon from '../Icon.svelte';
|
|
16
|
-
import { registerIcons } from '../icons.svelte.js';
|
|
17
|
-
/** The 'check' preset path, used to assert preset resolution. */
|
|
18
|
-
const CHECK_PATH = 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z';
|
|
19
|
-
describe('Icon', () => {
|
|
20
|
-
it('renders an svg that is decorative (aria-hidden) by default', () => {
|
|
21
|
-
const { container } = render(Icon, { props: { name: 'check' } });
|
|
22
|
-
const svg = container.querySelector('svg');
|
|
23
|
-
expect(svg).toBeInTheDocument();
|
|
24
|
-
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
|
25
|
-
expect(svg).not.toHaveAttribute('role');
|
|
26
|
-
});
|
|
27
|
-
it('resolves a named preset to its path', () => {
|
|
28
|
-
const { container } = render(Icon, { props: { name: 'check' } });
|
|
29
|
-
const path = container.querySelector('svg path');
|
|
30
|
-
expect(path).toHaveAttribute('d', CHECK_PATH);
|
|
31
|
-
});
|
|
32
|
-
it('renders a custom path verbatim, taking precedence over name', () => {
|
|
33
|
-
const custom = 'M0 0h10v10H0z';
|
|
34
|
-
const { container } = render(Icon, {
|
|
35
|
-
props: { name: 'check', path: custom },
|
|
36
|
-
});
|
|
37
|
-
const path = container.querySelector('svg path');
|
|
38
|
-
expect(path).toHaveAttribute('d', custom);
|
|
39
|
-
});
|
|
40
|
-
it('renders a path with no shape for an unknown preset name', () => {
|
|
41
|
-
// finalPath resolves to '' for an unknown preset; Svelte omits the empty
|
|
42
|
-
// `d` attribute entirely, so the path element renders without a shape.
|
|
43
|
-
const { container } = render(Icon, { props: { name: 'does-not-exist' } });
|
|
44
|
-
const path = container.querySelector('svg path');
|
|
45
|
-
expect(path).toBeInTheDocument();
|
|
46
|
-
expect(path).not.toHaveAttribute('d');
|
|
47
|
-
});
|
|
48
|
-
it('becomes informative (role=img + aria-label) when given an aria-label', () => {
|
|
49
|
-
const { container } = render(Icon, {
|
|
50
|
-
props: { name: 'search', 'aria-label': 'Search' },
|
|
51
|
-
});
|
|
52
|
-
const svg = container.querySelector('svg');
|
|
53
|
-
expect(svg).toHaveAttribute('role', 'img');
|
|
54
|
-
expect(svg).toHaveAttribute('aria-label', 'Search');
|
|
55
|
-
expect(svg).toHaveAttribute('aria-hidden', 'false');
|
|
56
|
-
});
|
|
57
|
-
it('converts a numeric size to a px width/height', () => {
|
|
58
|
-
const { container } = render(Icon, { props: { name: 'menu', size: 32 } });
|
|
59
|
-
const svg = container.querySelector('svg');
|
|
60
|
-
expect(svg).toHaveAttribute('width', '32px');
|
|
61
|
-
expect(svg).toHaveAttribute('height', '32px');
|
|
62
|
-
});
|
|
63
|
-
it('passes a string size through unchanged', () => {
|
|
64
|
-
const { container } = render(Icon, {
|
|
65
|
-
props: { name: 'menu', size: '1.5em' },
|
|
66
|
-
});
|
|
67
|
-
const svg = container.querySelector('svg');
|
|
68
|
-
expect(svg).toHaveAttribute('width', '1.5em');
|
|
69
|
-
expect(svg).toHaveAttribute('height', '1.5em');
|
|
70
|
-
});
|
|
71
|
-
it('applies the color prop as the svg fill', () => {
|
|
72
|
-
const { container } = render(Icon, {
|
|
73
|
-
props: { name: 'menu', color: '#ff0000' },
|
|
74
|
-
});
|
|
75
|
-
expect(container.querySelector('svg')).toHaveAttribute('fill', '#ff0000');
|
|
76
|
-
});
|
|
77
|
-
it('uses currentColor as the default fill', () => {
|
|
78
|
-
const { container } = render(Icon, { props: { name: 'menu' } });
|
|
79
|
-
expect(container.querySelector('svg')).toHaveAttribute('fill', 'currentColor');
|
|
80
|
-
});
|
|
81
|
-
it('applies a custom viewBox', () => {
|
|
82
|
-
const { container } = render(Icon, {
|
|
83
|
-
props: { name: 'menu', viewBox: '0 0 48 48' },
|
|
84
|
-
});
|
|
85
|
-
expect(container.querySelector('svg')).toHaveAttribute('viewBox', '0 0 48 48');
|
|
86
|
-
});
|
|
87
|
-
it('is axe-clean when decorative', async () => {
|
|
88
|
-
const { container } = render(Icon, { props: { name: 'close' } });
|
|
89
|
-
await expectNoA11yViolations(container);
|
|
90
|
-
});
|
|
91
|
-
it('is axe-clean when labelled', async () => {
|
|
92
|
-
const { container } = render(Icon, {
|
|
93
|
-
props: { name: 'close', 'aria-label': 'Close dialog' },
|
|
94
|
-
});
|
|
95
|
-
await expectNoA11yViolations(container);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
const cleanupRegistrations = [];
|
|
99
|
-
afterEach(() => {
|
|
100
|
-
for (const cleanup of cleanupRegistrations.splice(0))
|
|
101
|
-
cleanup();
|
|
102
|
-
});
|
|
103
|
-
describe('Icon catalog and application registration', () => {
|
|
104
|
-
it.each([
|
|
105
|
-
'alert',
|
|
106
|
-
'warning',
|
|
107
|
-
'info',
|
|
108
|
-
'home',
|
|
109
|
-
'user',
|
|
110
|
-
'settings',
|
|
111
|
-
'trash',
|
|
112
|
-
'edit',
|
|
113
|
-
'calendar',
|
|
114
|
-
'clock',
|
|
115
|
-
'camera',
|
|
116
|
-
'upload',
|
|
117
|
-
'download',
|
|
118
|
-
])('ships a nonempty %s glyph', (name) => {
|
|
119
|
-
const { container } = render(Icon, { props: { name } });
|
|
120
|
-
expect(container.querySelector('path')?.getAttribute('d')).toMatch(/^M/);
|
|
121
|
-
});
|
|
122
|
-
it('updates already mounted icons when an application set is registered and removed', async () => {
|
|
123
|
-
const { container } = render(Icon, { props: { name: 'shop-machine' } });
|
|
124
|
-
const path = container.querySelector('path');
|
|
125
|
-
expect(path).not.toHaveAttribute('d');
|
|
126
|
-
let cleanup = () => { };
|
|
127
|
-
await act(() => {
|
|
128
|
-
cleanup = registerIcons({ 'shop-machine': 'M2 2h20v20H2z' });
|
|
129
|
-
});
|
|
130
|
-
cleanupRegistrations.push(cleanup);
|
|
131
|
-
expect(path).toHaveAttribute('d', 'M2 2h20v20H2z');
|
|
132
|
-
await act(cleanup);
|
|
133
|
-
expect(path).not.toHaveAttribute('d');
|
|
134
|
-
});
|
|
135
|
-
it('preserves override precedence and supports out-of-order, repeated cleanup', async () => {
|
|
136
|
-
const { container } = render(Icon, { props: { name: 'check' } });
|
|
137
|
-
const path = container.querySelector('path');
|
|
138
|
-
const first = registerIcons({ check: 'M1 1h2v2H1z' });
|
|
139
|
-
const second = registerIcons({ check: 'M3 3h4v4H3z' });
|
|
140
|
-
cleanupRegistrations.push(first, second);
|
|
141
|
-
await act(() => { });
|
|
142
|
-
expect(path).toHaveAttribute('d', 'M3 3h4v4H3z');
|
|
143
|
-
await act(first);
|
|
144
|
-
expect(path).toHaveAttribute('d', 'M3 3h4v4H3z');
|
|
145
|
-
await act(first);
|
|
146
|
-
expect(path).toHaveAttribute('d', 'M3 3h4v4H3z');
|
|
147
|
-
await act(second);
|
|
148
|
-
expect(path).toHaveAttribute('d', CHECK_PATH);
|
|
149
|
-
});
|
|
150
|
-
it('keeps explicit path higher priority than a registered name', () => {
|
|
151
|
-
cleanupRegistrations.push(registerIcons({ check: 'M1 1h2v2H1z' }));
|
|
152
|
-
const { container } = render(Icon, {
|
|
153
|
-
props: {
|
|
154
|
-
name: 'check',
|
|
155
|
-
path: 'M0 0h10v10H0z',
|
|
156
|
-
'aria-label': 'Custom check',
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
expect(container.querySelector('path')).toHaveAttribute('d', 'M0 0h10v10H0z');
|
|
160
|
-
expect(container.querySelector('svg')).toHaveAccessibleName('Custom check');
|
|
161
|
-
});
|
|
162
|
-
it('snapshots caller maps and rejects malformed mixed sets atomically', () => {
|
|
163
|
-
const icons = { brand: 'M1 1h2v2H1z' };
|
|
164
|
-
cleanupRegistrations.push(registerIcons(icons));
|
|
165
|
-
icons.brand = 'M3 3h4v4H3z';
|
|
166
|
-
expect(() => registerIcons({ brand: 'M5 5h6v6H5z', bad: 3 })).toThrow(TypeError);
|
|
167
|
-
expect(() => registerIcons({ '': 'M1 1h2v2H1z' })).toThrow(TypeError);
|
|
168
|
-
expect(() => registerIcons({ empty: '' })).toThrow(TypeError);
|
|
169
|
-
const { container } = render(Icon, { props: { name: 'brand' } });
|
|
170
|
-
expect(container.querySelector('path')).toHaveAttribute('d', 'M1 1h2v2H1z');
|
|
171
|
-
});
|
|
172
|
-
it('does not resolve inherited map properties or builtin prototype names', () => {
|
|
173
|
-
cleanupRegistrations.push(registerIcons(Object.create({ inherited: 'M1 1h2v2H1z' })));
|
|
174
|
-
for (const name of ['inherited', 'toString', '__proto__', 'constructor']) {
|
|
175
|
-
const { container } = render(Icon, { props: { name } });
|
|
176
|
-
expect(container.querySelector('path')).not.toHaveAttribute('d');
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
it('renders registered icons on the server and hydrates with the same application set', async () => {
|
|
181
|
-
const vite = await createServer({
|
|
182
|
-
configFile: false,
|
|
183
|
-
plugins: [svelte()],
|
|
184
|
-
root: process.cwd(),
|
|
185
|
-
server: { middlewareMode: true },
|
|
186
|
-
});
|
|
187
|
-
const custom = 'M4 4h16v16H4z';
|
|
188
|
-
const unregisterClient = registerIcons({ brand: custom });
|
|
189
|
-
cleanupRegistrations.push(unregisterClient);
|
|
190
|
-
try {
|
|
191
|
-
const { default: ServerIcon } = await vite.ssrLoadModule('/src/components/display/Icon.svelte');
|
|
192
|
-
const { registerIcons: registerServerIcons } = await vite.ssrLoadModule('/src/components/display/icons.svelte.ts');
|
|
193
|
-
const unregisterServer = registerServerIcons({ brand: custom });
|
|
194
|
-
try {
|
|
195
|
-
const { render: renderSsr } = await vite.ssrLoadModule('svelte/server');
|
|
196
|
-
const props = { name: 'brand', 'aria-label': 'Brand' };
|
|
197
|
-
const result = await renderSsr(ServerIcon, { props });
|
|
198
|
-
expect(result.body).toContain(`d="${custom}"`);
|
|
199
|
-
const host = document.createElement('div');
|
|
200
|
-
host.innerHTML = result.body;
|
|
201
|
-
document.body.append(host);
|
|
202
|
-
const instance = hydrate(Icon, { target: host, props });
|
|
203
|
-
try {
|
|
204
|
-
expect(host.querySelector('svg')).toHaveAccessibleName('Brand');
|
|
205
|
-
expect(host.querySelector('path')).toHaveAttribute('d', custom);
|
|
206
|
-
const override = registerIcons({ brand: 'M2 2h20v20H2z' });
|
|
207
|
-
cleanupRegistrations.push(override);
|
|
208
|
-
await tick();
|
|
209
|
-
expect(host.querySelector('path')).toHaveAttribute('d', 'M2 2h20v20H2z');
|
|
210
|
-
}
|
|
211
|
-
finally {
|
|
212
|
-
await unmount(instance);
|
|
213
|
-
host.remove();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
finally {
|
|
217
|
-
unregisterServer();
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
finally {
|
|
221
|
-
await vite.close();
|
|
222
|
-
}
|
|
223
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component tests for StatusBadge (Sweep S11, #1416).
|
|
3
|
-
*
|
|
4
|
-
* StatusBadge is a presentational chip: it normalizes a status string, maps it
|
|
5
|
-
* to a color scheme per domain `type`, and renders a humanized label. Tests
|
|
6
|
-
* assert the real API from StatusBadge.svelte — label formatting, custom label
|
|
7
|
-
* override, size/variant classes, scheme-driven CSS custom properties, and
|
|
8
|
-
* axe-cleanliness across variants.
|
|
9
|
-
*/
|
|
10
|
-
import { render, screen } from '@testing-library/svelte';
|
|
11
|
-
import { describe, expect, it } from 'vitest';
|
|
12
|
-
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
13
|
-
import StatusBadge from '../StatusBadge.svelte';
|
|
14
|
-
describe('StatusBadge', () => {
|
|
15
|
-
it.each([
|
|
16
|
-
[
|
|
17
|
-
'success',
|
|
18
|
-
'var(--smrt-color-success-container)',
|
|
19
|
-
'var(--smrt-color-on-success-container)',
|
|
20
|
-
],
|
|
21
|
-
[
|
|
22
|
-
'warning',
|
|
23
|
-
'var(--smrt-color-warning-container)',
|
|
24
|
-
'var(--smrt-color-on-warning-container)',
|
|
25
|
-
],
|
|
26
|
-
[
|
|
27
|
-
'danger',
|
|
28
|
-
'var(--smrt-color-error-container)',
|
|
29
|
-
'var(--smrt-color-on-error-container)',
|
|
30
|
-
],
|
|
31
|
-
[
|
|
32
|
-
'info',
|
|
33
|
-
'var(--smrt-color-primary-container)',
|
|
34
|
-
'var(--smrt-color-on-primary-container)',
|
|
35
|
-
],
|
|
36
|
-
[
|
|
37
|
-
'neutral',
|
|
38
|
-
'var(--smrt-color-surface-container-highest)',
|
|
39
|
-
'var(--smrt-color-on-surface-variant)',
|
|
40
|
-
],
|
|
41
|
-
])('applies %s tone to a custom status', (tone, background, foreground) => {
|
|
42
|
-
const { container } = render(StatusBadge, {
|
|
43
|
-
props: { status: 'awaiting_cert', tone },
|
|
44
|
-
});
|
|
45
|
-
const span = container.querySelector('span');
|
|
46
|
-
expect(span.style.getPropertyValue('--badge-bg')).toBe(background);
|
|
47
|
-
expect(span.style.getPropertyValue('--badge-text')).toBe(foreground);
|
|
48
|
-
expect(screen.getByText('awaiting cert')).toBeInTheDocument();
|
|
49
|
-
});
|
|
50
|
-
it('lets explicit tone override a domain scheme and updates reactively', async () => {
|
|
51
|
-
const { container, rerender } = render(StatusBadge, {
|
|
52
|
-
props: {
|
|
53
|
-
status: 'overdue',
|
|
54
|
-
type: 'invoice',
|
|
55
|
-
tone: 'success',
|
|
56
|
-
variant: 'outline',
|
|
57
|
-
size: 'sm',
|
|
58
|
-
label: 'Reviewed',
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
const span = container.querySelector('span');
|
|
62
|
-
expect(span.style.getPropertyValue('--badge-bg')).toBe('var(--smrt-color-success-container)');
|
|
63
|
-
expect(span).toHaveClass('outline', 'sm');
|
|
64
|
-
expect(screen.getByText('Reviewed')).toBeInTheDocument();
|
|
65
|
-
await rerender({ status: 'overdue', type: 'invoice', tone: 'warning' });
|
|
66
|
-
expect(span.style.getPropertyValue('--badge-bg')).toBe('var(--smrt-color-warning-container)');
|
|
67
|
-
await rerender({ status: 'overdue', type: 'invoice', tone: undefined });
|
|
68
|
-
expect(span.style.getPropertyValue('--badge-bg')).toBe('var(--smrt-color-error-container)');
|
|
69
|
-
});
|
|
70
|
-
it.each([
|
|
71
|
-
'unknown',
|
|
72
|
-
'__proto__',
|
|
73
|
-
'constructor',
|
|
74
|
-
])('safely ignores invalid runtime tone %s', (tone) => {
|
|
75
|
-
const { container } = render(StatusBadge, {
|
|
76
|
-
props: { status: 'paid', type: 'invoice', tone: tone },
|
|
77
|
-
});
|
|
78
|
-
const span = container.querySelector('span');
|
|
79
|
-
expect(span.style.getPropertyValue('--badge-bg')).toBe('var(--smrt-color-primary-container)');
|
|
80
|
-
});
|
|
81
|
-
it.each([
|
|
82
|
-
'success',
|
|
83
|
-
'warning',
|
|
84
|
-
'danger',
|
|
85
|
-
'info',
|
|
86
|
-
'neutral',
|
|
87
|
-
])('is axe-clean for custom status tone=%s', async (tone) => {
|
|
88
|
-
const { container } = render(StatusBadge, {
|
|
89
|
-
props: { status: 'awaiting_cert', tone },
|
|
90
|
-
});
|
|
91
|
-
await expectNoA11yViolations(container);
|
|
92
|
-
});
|
|
93
|
-
it('renders the status value as its label', () => {
|
|
94
|
-
render(StatusBadge, { props: { status: 'active' } });
|
|
95
|
-
expect(screen.getByText('active')).toBeInTheDocument();
|
|
96
|
-
});
|
|
97
|
-
it('humanizes underscores in the status into spaces', () => {
|
|
98
|
-
render(StatusBadge, { props: { status: 'on_hold', type: 'project' } });
|
|
99
|
-
expect(screen.getByText('on hold')).toBeInTheDocument();
|
|
100
|
-
});
|
|
101
|
-
it('prefers an explicit label over the status value', () => {
|
|
102
|
-
render(StatusBadge, {
|
|
103
|
-
props: { status: 'paid', type: 'invoice', label: 'Paid in full' },
|
|
104
|
-
});
|
|
105
|
-
expect(screen.getByText('Paid in full')).toBeInTheDocument();
|
|
106
|
-
expect(screen.queryByText('paid')).not.toBeInTheDocument();
|
|
107
|
-
});
|
|
108
|
-
it('always carries the base status-badge class', () => {
|
|
109
|
-
const { container } = render(StatusBadge, { props: { status: 'active' } });
|
|
110
|
-
expect(container.querySelector('span')).toHaveClass('status-badge');
|
|
111
|
-
});
|
|
112
|
-
it.each([
|
|
113
|
-
['sm', 'sm'],
|
|
114
|
-
['lg', 'lg'],
|
|
115
|
-
])('applies the %s size class', (size, cls) => {
|
|
116
|
-
const { container } = render(StatusBadge, {
|
|
117
|
-
props: { status: 'active', size },
|
|
118
|
-
});
|
|
119
|
-
expect(container.querySelector('span')).toHaveClass(cls);
|
|
120
|
-
});
|
|
121
|
-
it('omits a size class for the default md size', () => {
|
|
122
|
-
const { container } = render(StatusBadge, { props: { status: 'active' } });
|
|
123
|
-
const span = container.querySelector('span');
|
|
124
|
-
expect(span).not.toHaveClass('sm');
|
|
125
|
-
expect(span).not.toHaveClass('lg');
|
|
126
|
-
});
|
|
127
|
-
it('applies the outline class for the outline variant', () => {
|
|
128
|
-
const { container } = render(StatusBadge, {
|
|
129
|
-
props: { status: 'active', variant: 'outline' },
|
|
130
|
-
});
|
|
131
|
-
expect(container.querySelector('span')).toHaveClass('outline');
|
|
132
|
-
});
|
|
133
|
-
it('maps a known status to its scheme colors via CSS custom properties', () => {
|
|
134
|
-
const { container } = render(StatusBadge, {
|
|
135
|
-
props: { status: 'overdue', type: 'invoice' },
|
|
136
|
-
});
|
|
137
|
-
const span = container.querySelector('span');
|
|
138
|
-
// invoice/overdue → error container scheme
|
|
139
|
-
expect(span.style.getPropertyValue('--badge-bg')).toContain('smrt-color-error-container');
|
|
140
|
-
expect(span.style.getPropertyValue('--badge-text')).toContain('smrt-color-on-error-container');
|
|
141
|
-
});
|
|
142
|
-
it('falls back to a neutral scheme for an unknown status', () => {
|
|
143
|
-
const { container } = render(StatusBadge, {
|
|
144
|
-
props: { status: 'totally-unknown', type: 'invoice' },
|
|
145
|
-
});
|
|
146
|
-
const span = container.querySelector('span');
|
|
147
|
-
// The neutral fallback resolves to canonical surface tokens, not a literal
|
|
148
|
-
// hex. smrt-ui owns the theme system, so the tokens are always defined; a
|
|
149
|
-
// hardcoded fallback would freeze the wrong color across presets (#1586).
|
|
150
|
-
expect(span.style.getPropertyValue('--badge-bg')).toBe('var(--smrt-color-surface-container-highest)');
|
|
151
|
-
expect(span.style.getPropertyValue('--badge-text')).toBe('var(--smrt-color-on-surface-variant)');
|
|
152
|
-
});
|
|
153
|
-
it('uses bare design tokens with no literal hex fallback in scheme colors', () => {
|
|
154
|
-
// Regression for #1586: the `var(--token, #hex)` fallbacks were the wrong
|
|
155
|
-
// colors (e.g. primary-container → green #dcfce7 vs real material light blue
|
|
156
|
-
// #d3e3fd). Every emitted custom property must be a bare token reference.
|
|
157
|
-
const { container } = render(StatusBadge, {
|
|
158
|
-
props: { status: 'paid', type: 'invoice' },
|
|
159
|
-
});
|
|
160
|
-
const span = container.querySelector('span');
|
|
161
|
-
expect(span.style.getPropertyValue('--badge-bg')).toBe('var(--smrt-color-primary-container)');
|
|
162
|
-
expect(span.style.getPropertyValue('--badge-bg')).not.toContain('#');
|
|
163
|
-
expect(span.style.getPropertyValue('--badge-text')).not.toContain('#');
|
|
164
|
-
});
|
|
165
|
-
it('normalizes spaces and hyphens when matching a scheme key', () => {
|
|
166
|
-
// 'On-Hold' normalizes to 'on_hold', a known project status.
|
|
167
|
-
const { container } = render(StatusBadge, {
|
|
168
|
-
props: { status: 'On-Hold', type: 'project' },
|
|
169
|
-
});
|
|
170
|
-
const span = container.querySelector('span');
|
|
171
|
-
expect(span.style.getPropertyValue('--badge-bg')).toContain('smrt-color-secondary-container');
|
|
172
|
-
});
|
|
173
|
-
it.each([
|
|
174
|
-
['default', 'active'],
|
|
175
|
-
['invoice', 'paid'],
|
|
176
|
-
['project', 'completed'],
|
|
177
|
-
['expense', 'reimbursed'],
|
|
178
|
-
['time', 'approved'],
|
|
179
|
-
['compliance', 'valid'],
|
|
180
|
-
['estimate', 'accepted'],
|
|
181
|
-
])('is axe-clean for type=%s status=%s', async (type, status) => {
|
|
182
|
-
const { container } = render(StatusBadge, { props: { status, type } });
|
|
183
|
-
await expectNoA11yViolations(container);
|
|
184
|
-
});
|
|
185
|
-
it('is axe-clean for the outline variant', async () => {
|
|
186
|
-
const { container } = render(StatusBadge, {
|
|
187
|
-
props: { status: 'active', variant: 'outline' },
|
|
188
|
-
});
|
|
189
|
-
await expectNoA11yViolations(container);
|
|
190
|
-
});
|
|
191
|
-
});
|