@classic-homes/theme-svelte 1.22.0 → 1.22.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/dist/lib/components/Alert.svelte +8 -10
- package/dist/lib/components/Combobox.svelte +2 -2
- package/dist/lib/components/DataPanel.svelte +19 -5
- package/dist/lib/components/DataTable.svelte +15 -6
- package/dist/lib/components/DataTable.svelte.d.ts +1 -1
- package/dist/lib/components/DateTimePicker.svelte +31 -34
- package/dist/lib/components/DropdownMenu.svelte +52 -30
- package/dist/lib/components/DropdownMenu.svelte.d.ts +5 -0
- package/dist/lib/components/FileUpload.svelte +5 -19
- package/dist/lib/components/HeaderSearch.svelte +18 -10
- package/dist/lib/components/MultiSelect.svelte +2 -2
- package/dist/lib/components/NumberInput.svelte +15 -2
- package/dist/lib/components/Select.svelte +19 -5
- package/dist/lib/components/Select.svelte.d.ts +4 -0
- package/dist/lib/components/Signature.svelte +15 -4
- package/dist/lib/components/Signature.svelte.d.ts +1 -1
- package/dist/lib/components/Skeleton.svelte.d.ts +1 -1
- package/dist/lib/components/Slider.svelte +8 -6
- package/dist/lib/components/Tabs.svelte +10 -12
- package/dist/lib/components/Toast.svelte +12 -12
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +24 -26
- package/dist/lib/components/data-table/DataTableEditableCell.svelte +31 -18
- package/dist/lib/components/data-table/DataTableEditableCell.svelte.d.ts +0 -7
- package/dist/lib/components/data-table/DataTablePagination.svelte +5 -1
- package/dist/lib/components/data-table/DataTableVirtual.svelte +4 -7
- package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +3 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +6 -1
- package/dist/lib/components/data-table/createDataTable.svelte.js +22 -9
- package/dist/lib/components/layout/AppShell.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +15 -6
- package/dist/lib/components/layout/Footer.svelte +6 -6
- package/dist/lib/components/layout/FormPageLayout.svelte +4 -3
- package/dist/lib/components/layout/Sidebar.svelte +53 -21
- package/dist/lib/components/media/Image.svelte.d.ts +1 -1
- package/dist/lib/components/media/Lightbox.svelte +13 -9
- package/dist/lib/components/media/MediaCarousel.svelte +5 -0
- package/dist/lib/components/media/MediaGrid.svelte +4 -1
- package/dist/lib/components/notifications/NotificationDialog.svelte +8 -5
- package/dist/lib/components/notifications/NotificationItem.svelte +16 -6
- package/dist/lib/components/studio/InsertPalette.svelte +4 -1
- package/dist/lib/components/studio/StudioCanvas.svelte +23 -2
- package/dist/lib/components/studio/StudioCanvas.svelte.d.ts +5 -1
- package/dist/lib/composables/useAsync.svelte.d.ts +1 -1
- package/dist/lib/composables/useAsync.svelte.js +33 -14
- package/dist/lib/composables/useFocusTrap.svelte.js +9 -2
- package/dist/lib/composables/useForm.svelte.js +19 -7
- package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
- package/dist/lib/notices/sanitize.js +10 -3
- package/dist/lib/notices/store.d.ts +3 -3
- package/dist/lib/notices/store.js +14 -2
- package/dist/lib/notices/types.d.ts +8 -8
- package/dist/lib/notices/types.js +5 -1
- package/dist/lib/stores/sidebar.svelte.d.ts +12 -0
- package/dist/lib/stores/sidebar.svelte.js +76 -26
- package/dist/lib/stores/toast.svelte.js +12 -1
- package/dist/lib/utils/date.d.ts +3 -1
- package/dist/lib/utils/date.js +54 -34
- package/dist/lib/utils/form.d.ts +2 -1
- package/dist/lib/utils/form.js +3 -2
- package/dist/lib/utils/media.js +4 -3
- package/dist/lib/utils/url.d.ts +10 -0
- package/dist/lib/utils/url.js +21 -0
- package/dist/lib/utils.js +14 -1
- package/package.json +5 -4
- package/dist/__tests__/FileUpload.test.js +0 -45
- package/dist/__tests__/a11y/Alert.a11y.test.js +0 -84
- package/dist/__tests__/a11y/AlertDialog.a11y.test.js +0 -126
- package/dist/__tests__/a11y/Button.a11y.test.js +0 -66
- package/dist/__tests__/a11y/Card.a11y.test.js +0 -77
- package/dist/__tests__/a11y/Combobox.a11y.test.js +0 -209
- package/dist/__tests__/a11y/Dialog.a11y.test.js +0 -103
- package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +0 -132
- package/dist/__tests__/a11y/FileUpload.a11y.test.js +0 -158
- package/dist/__tests__/a11y/FilterToolbar.a11y.test.js +0 -62
- package/dist/__tests__/a11y/Icon.a11y.test.js +0 -119
- package/dist/__tests__/a11y/Input.a11y.test.js +0 -96
- package/dist/__tests__/a11y/LiveRegion.a11y.test.js +0 -102
- package/dist/__tests__/a11y/Media.a11y.test.js +0 -145
- package/dist/__tests__/a11y/PublicLayout.a11y.test.js +0 -139
- package/dist/__tests__/a11y/SegmentedControl.a11y.test.js +0 -50
- package/dist/__tests__/a11y/Switch.a11y.test.js +0 -95
- package/dist/__tests__/a11y/Tabs.a11y.test.js +0 -116
- package/dist/__tests__/a11y/Textarea.a11y.test.js +0 -111
- package/dist/__tests__/a11y/Toolbar.a11y.test.js +0 -31
- package/dist/__tests__/a11y/ViewToggle.a11y.test.js +0 -40
- package/dist/__tests__/action-header.test.js +0 -150
- package/dist/__tests__/alert-dialog-dismiss.test.js +0 -54
- package/dist/__tests__/code-field.test.js +0 -65
- package/dist/__tests__/control-text-color.test.js +0 -29
- package/dist/__tests__/data-panel-card-gestures.test.js +0 -138
- package/dist/__tests__/data-panel-viewport.test.js +0 -45
- package/dist/__tests__/dropdown-trigger.test.js +0 -39
- package/dist/__tests__/header-search.test.js +0 -90
- package/dist/__tests__/media-components.test.js +0 -206
- package/dist/__tests__/media-hls.test.js +0 -273
- package/dist/__tests__/media-matterport.test.js +0 -296
- package/dist/__tests__/media-players.test.js +0 -244
- package/dist/__tests__/media-scrubber.test.js +0 -44
- package/dist/__tests__/media-utils.test.js +0 -427
- package/dist/__tests__/nav-access.test.js +0 -145
- package/dist/__tests__/page-header.test.js +0 -36
- package/dist/__tests__/public-layout-drawer.test.js +0 -116
- package/dist/__tests__/setup.js +0 -56
- package/dist/__tests__/sidebar-collapse.test.js +0 -58
- package/dist/__tests__/sidebar-docs-parity.test.js +0 -134
- package/dist/__tests__/sidebar-expansion.test.js +0 -59
- package/dist/__tests__/sidebar-items.test.js +0 -59
- package/dist/__tests__/sidebar-mobile.test.js +0 -86
- package/dist/__tests__/studio-canvas-item.svelte.test.js +0 -98
- package/dist/__tests__/studio.test.js +0 -240
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { render, screen, fireEvent } from '@testing-library/svelte';
|
|
3
|
-
import { axe } from 'vitest-axe';
|
|
4
|
-
import { createRawSnippet } from 'svelte';
|
|
5
|
-
import FilterToolbar from '../../lib/components/FilterToolbar.svelte';
|
|
6
|
-
import ActiveFilterChips from '../../lib/components/ActiveFilterChips.svelte';
|
|
7
|
-
const snippet = (html) => createRawSnippet(() => ({ render: () => html }));
|
|
8
|
-
/** Two labelled selects; labels are sr-only when inline. */
|
|
9
|
-
const filters = createRawSnippet((args) => ({
|
|
10
|
-
render: () => {
|
|
11
|
-
const labelClass = args().inline ? ' class="sr-only"' : '';
|
|
12
|
-
return `<div>
|
|
13
|
-
<label for="f-status"${labelClass}>Status</label><select id="f-status"><option>Any</option></select>
|
|
14
|
-
<label for="f-type"${labelClass}>Type</label><select id="f-type"><option>Any</option></select>
|
|
15
|
-
</div>`;
|
|
16
|
-
},
|
|
17
|
-
}));
|
|
18
|
-
const search = snippet('<input type="search" aria-label="Search signs" />');
|
|
19
|
-
describe('FilterToolbar', () => {
|
|
20
|
-
it('keeps filters inline at or under maxInline controls', () => {
|
|
21
|
-
render(FilterToolbar, {
|
|
22
|
-
props: { label: 'Sign tools', search, filters, filterCount: 2 },
|
|
23
|
-
});
|
|
24
|
-
expect(screen.queryByRole('button', { name: /Filters/ })).not.toBeInTheDocument();
|
|
25
|
-
expect(screen.getByLabelText('Status')).toBeInTheDocument();
|
|
26
|
-
});
|
|
27
|
-
it('folds filters behind a Filters button past maxInline', async () => {
|
|
28
|
-
const { container } = render(FilterToolbar, {
|
|
29
|
-
props: { label: 'Sign tools', search, filters, filterCount: 3, activeCount: 2 },
|
|
30
|
-
});
|
|
31
|
-
const button = screen.getByRole('button', { name: 'Filters, 2 active' });
|
|
32
|
-
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
33
|
-
const panel = screen.getByRole('region', { hidden: true });
|
|
34
|
-
expect(button).toHaveAttribute('aria-controls', panel.id);
|
|
35
|
-
// Svelte sets `inert` as a property; jsdom doesn't reflect it to an attribute.
|
|
36
|
-
const content = panel.firstElementChild;
|
|
37
|
-
expect(content.inert).toBe(true);
|
|
38
|
-
await fireEvent.click(button);
|
|
39
|
-
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
40
|
-
expect(content.inert).toBe(false);
|
|
41
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe('ActiveFilterChips', () => {
|
|
45
|
-
it('removes a single filter or all of them', async () => {
|
|
46
|
-
const onRemove = vi.fn();
|
|
47
|
-
const onClearAll = vi.fn();
|
|
48
|
-
const { container } = render(ActiveFilterChips, {
|
|
49
|
-
props: { chips: [{ key: 'status', label: 'Status: Installed', onRemove }], onClearAll },
|
|
50
|
-
});
|
|
51
|
-
expect(screen.getByRole('group', { name: 'Active filters' })).toBeInTheDocument();
|
|
52
|
-
await fireEvent.click(screen.getByRole('button', { name: 'Remove filter Status: Installed' }));
|
|
53
|
-
expect(onRemove).toHaveBeenCalledOnce();
|
|
54
|
-
await fireEvent.click(screen.getByRole('button', { name: 'Clear all' }));
|
|
55
|
-
expect(onClearAll).toHaveBeenCalledOnce();
|
|
56
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
57
|
-
});
|
|
58
|
-
it('renders nothing without chips', () => {
|
|
59
|
-
const { container } = render(ActiveFilterChips, { props: { chips: [] } });
|
|
60
|
-
expect(container.querySelector('[role="group"]')).toBeNull();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/svelte';
|
|
3
|
-
import { axe } from 'vitest-axe';
|
|
4
|
-
import Icon from '../../lib/components/Icon.svelte';
|
|
5
|
-
describe('Icon accessibility', () => {
|
|
6
|
-
it('should have no axe violations for decorative icons', async () => {
|
|
7
|
-
const { container } = render(Icon, {
|
|
8
|
-
props: {
|
|
9
|
-
name: 'check',
|
|
10
|
-
},
|
|
11
|
-
});
|
|
12
|
-
const results = await axe(container);
|
|
13
|
-
expect(results).toHaveNoViolations();
|
|
14
|
-
});
|
|
15
|
-
it('should always have aria-hidden="true" on the SVG element', () => {
|
|
16
|
-
render(Icon, {
|
|
17
|
-
props: {
|
|
18
|
-
name: 'check',
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
const svg = document.querySelector('svg');
|
|
22
|
-
expect(svg).toBeInTheDocument();
|
|
23
|
-
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
|
24
|
-
});
|
|
25
|
-
it('should have aria-hidden="true" even when label is provided', () => {
|
|
26
|
-
render(Icon, {
|
|
27
|
-
props: {
|
|
28
|
-
name: 'check',
|
|
29
|
-
label: 'Success',
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
const svg = document.querySelector('svg');
|
|
33
|
-
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
|
34
|
-
});
|
|
35
|
-
it('should render visually hidden text when label is provided', () => {
|
|
36
|
-
render(Icon, {
|
|
37
|
-
props: {
|
|
38
|
-
name: 'alert-circle',
|
|
39
|
-
label: 'Warning',
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
// Check for sr-only text
|
|
43
|
-
const srOnlyText = screen.getByText('Warning');
|
|
44
|
-
expect(srOnlyText).toBeInTheDocument();
|
|
45
|
-
expect(srOnlyText).toHaveClass('sr-only');
|
|
46
|
-
});
|
|
47
|
-
it('should not render sr-only text when no label is provided', () => {
|
|
48
|
-
const { container } = render(Icon, {
|
|
49
|
-
props: {
|
|
50
|
-
name: 'check',
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
const srOnlyElements = container.querySelectorAll('.sr-only');
|
|
54
|
-
expect(srOnlyElements.length).toBe(0);
|
|
55
|
-
});
|
|
56
|
-
it('should have no axe violations when used with label', async () => {
|
|
57
|
-
const { container } = render(Icon, {
|
|
58
|
-
props: {
|
|
59
|
-
name: 'trash',
|
|
60
|
-
label: 'Delete item',
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
const results = await axe(container);
|
|
64
|
-
expect(results).toHaveNoViolations();
|
|
65
|
-
});
|
|
66
|
-
it('should work correctly for loading spinner icon', () => {
|
|
67
|
-
render(Icon, {
|
|
68
|
-
props: {
|
|
69
|
-
name: 'loading',
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
const svg = document.querySelector('svg');
|
|
73
|
-
expect(svg).toBeInTheDocument();
|
|
74
|
-
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
|
75
|
-
});
|
|
76
|
-
it('should work correctly for more (vertical dots) icon', () => {
|
|
77
|
-
render(Icon, {
|
|
78
|
-
props: {
|
|
79
|
-
name: 'more',
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
const svg = document.querySelector('svg');
|
|
83
|
-
expect(svg).toBeInTheDocument();
|
|
84
|
-
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
|
85
|
-
});
|
|
86
|
-
it('should work correctly for more-horizontal icon', () => {
|
|
87
|
-
render(Icon, {
|
|
88
|
-
props: {
|
|
89
|
-
name: 'more-horizontal',
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
const svg = document.querySelector('svg');
|
|
93
|
-
expect(svg).toBeInTheDocument();
|
|
94
|
-
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
|
95
|
-
});
|
|
96
|
-
it('should work correctly for grip icon', () => {
|
|
97
|
-
render(Icon, {
|
|
98
|
-
props: {
|
|
99
|
-
name: 'grip',
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
const svg = document.querySelector('svg');
|
|
103
|
-
expect(svg).toBeInTheDocument();
|
|
104
|
-
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
|
105
|
-
});
|
|
106
|
-
it('icon with label should be accessible to screen readers', async () => {
|
|
107
|
-
const { container } = render(Icon, {
|
|
108
|
-
props: {
|
|
109
|
-
name: 'info',
|
|
110
|
-
label: 'Information',
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
// The accessible text should be present
|
|
114
|
-
expect(screen.getByText('Information')).toBeInTheDocument();
|
|
115
|
-
// No axe violations
|
|
116
|
-
const results = await axe(container);
|
|
117
|
-
expect(results).toHaveNoViolations();
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { axe } from 'vitest-axe';
|
|
5
|
-
import Input from '../../lib/components/Input.svelte';
|
|
6
|
-
describe('Input accessibility', () => {
|
|
7
|
-
it('should have no axe violations with aria-label', async () => {
|
|
8
|
-
const { container } = render(Input, {
|
|
9
|
-
props: { 'aria-label': 'Email address', type: 'email' },
|
|
10
|
-
});
|
|
11
|
-
const results = await axe(container);
|
|
12
|
-
expect(results).toHaveNoViolations();
|
|
13
|
-
});
|
|
14
|
-
it('should have no axe violations with aria-invalid', async () => {
|
|
15
|
-
const { container } = render(Input, {
|
|
16
|
-
props: {
|
|
17
|
-
'aria-label': 'Email',
|
|
18
|
-
'aria-invalid': true,
|
|
19
|
-
'aria-describedby': 'error',
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
// Add error element to container
|
|
23
|
-
const errorSpan = document.createElement('span');
|
|
24
|
-
errorSpan.id = 'error';
|
|
25
|
-
errorSpan.textContent = 'Please enter a valid email';
|
|
26
|
-
container.appendChild(errorSpan);
|
|
27
|
-
const results = await axe(container);
|
|
28
|
-
expect(results).toHaveNoViolations();
|
|
29
|
-
});
|
|
30
|
-
it('should be focusable', async () => {
|
|
31
|
-
render(Input, {
|
|
32
|
-
props: { 'aria-label': 'Test input' },
|
|
33
|
-
});
|
|
34
|
-
const input = screen.getByRole('textbox');
|
|
35
|
-
await userEvent.tab();
|
|
36
|
-
expect(input).toHaveFocus();
|
|
37
|
-
});
|
|
38
|
-
it('should support aria-describedby for help text', () => {
|
|
39
|
-
const { container } = render(Input, {
|
|
40
|
-
props: { 'aria-label': 'Username', 'aria-describedby': 'help' },
|
|
41
|
-
});
|
|
42
|
-
const helpSpan = document.createElement('span');
|
|
43
|
-
helpSpan.id = 'help';
|
|
44
|
-
helpSpan.textContent = 'Enter your username';
|
|
45
|
-
container.appendChild(helpSpan);
|
|
46
|
-
const input = screen.getByRole('textbox');
|
|
47
|
-
expect(input).toHaveAttribute('aria-describedby', 'help');
|
|
48
|
-
});
|
|
49
|
-
it('should support aria-labelledby', () => {
|
|
50
|
-
const { container } = render(Input, {
|
|
51
|
-
props: { 'aria-labelledby': 'label' },
|
|
52
|
-
});
|
|
53
|
-
const labelSpan = document.createElement('span');
|
|
54
|
-
labelSpan.id = 'label';
|
|
55
|
-
labelSpan.textContent = 'Custom Label';
|
|
56
|
-
container.insertBefore(labelSpan, container.firstChild);
|
|
57
|
-
const input = screen.getByRole('textbox');
|
|
58
|
-
expect(input).toHaveAttribute('aria-labelledby', 'label');
|
|
59
|
-
});
|
|
60
|
-
it('should apply error styling when aria-invalid is true', () => {
|
|
61
|
-
render(Input, {
|
|
62
|
-
props: { 'aria-label': 'Email', 'aria-invalid': true },
|
|
63
|
-
});
|
|
64
|
-
const input = screen.getByRole('textbox');
|
|
65
|
-
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
66
|
-
expect(input.className).toContain('border-destructive');
|
|
67
|
-
});
|
|
68
|
-
it('should apply error styling when aria-invalid is "true" string', () => {
|
|
69
|
-
render(Input, {
|
|
70
|
-
props: { 'aria-label': 'Email', 'aria-invalid': 'true' },
|
|
71
|
-
});
|
|
72
|
-
const input = screen.getByRole('textbox');
|
|
73
|
-
expect(input.className).toContain('border-destructive');
|
|
74
|
-
});
|
|
75
|
-
it('should not apply error styling when aria-invalid is false', () => {
|
|
76
|
-
render(Input, {
|
|
77
|
-
props: { 'aria-label': 'Email', 'aria-invalid': false },
|
|
78
|
-
});
|
|
79
|
-
const input = screen.getByRole('textbox');
|
|
80
|
-
expect(input.className).not.toContain('border-destructive');
|
|
81
|
-
});
|
|
82
|
-
it('should be disabled when disabled prop is true', () => {
|
|
83
|
-
render(Input, {
|
|
84
|
-
props: { 'aria-label': 'Disabled input', disabled: true },
|
|
85
|
-
});
|
|
86
|
-
const input = screen.getByRole('textbox');
|
|
87
|
-
expect(input).toBeDisabled();
|
|
88
|
-
});
|
|
89
|
-
it('should support required attribute', () => {
|
|
90
|
-
render(Input, {
|
|
91
|
-
props: { 'aria-label': 'Required field', required: true },
|
|
92
|
-
});
|
|
93
|
-
const input = screen.getByRole('textbox');
|
|
94
|
-
expect(input).toBeRequired();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/svelte';
|
|
3
|
-
import { axe } from 'vitest-axe';
|
|
4
|
-
import LiveRegion from '../../lib/components/LiveRegion.svelte';
|
|
5
|
-
describe('LiveRegion accessibility', () => {
|
|
6
|
-
it('should have no axe violations', async () => {
|
|
7
|
-
const { container } = render(LiveRegion, {
|
|
8
|
-
props: { message: 'Status update' },
|
|
9
|
-
});
|
|
10
|
-
const results = await axe(container);
|
|
11
|
-
expect(results).toHaveNoViolations();
|
|
12
|
-
});
|
|
13
|
-
it('should have no axe violations with assertive politeness', async () => {
|
|
14
|
-
const { container } = render(LiveRegion, {
|
|
15
|
-
props: { message: 'Error occurred', politeness: 'assertive' },
|
|
16
|
-
});
|
|
17
|
-
const results = await axe(container);
|
|
18
|
-
expect(results).toHaveNoViolations();
|
|
19
|
-
});
|
|
20
|
-
it('should have role="status"', () => {
|
|
21
|
-
render(LiveRegion, {
|
|
22
|
-
props: { message: 'Test message' },
|
|
23
|
-
});
|
|
24
|
-
const region = screen.getByRole('status');
|
|
25
|
-
expect(region).toBeInTheDocument();
|
|
26
|
-
});
|
|
27
|
-
it('should have aria-live="polite" by default', () => {
|
|
28
|
-
render(LiveRegion, {
|
|
29
|
-
props: { message: 'Test message' },
|
|
30
|
-
});
|
|
31
|
-
const region = screen.getByRole('status');
|
|
32
|
-
expect(region).toHaveAttribute('aria-live', 'polite');
|
|
33
|
-
});
|
|
34
|
-
it('should have aria-live="assertive" when specified', () => {
|
|
35
|
-
render(LiveRegion, {
|
|
36
|
-
props: { message: 'Urgent message', politeness: 'assertive' },
|
|
37
|
-
});
|
|
38
|
-
const region = screen.getByRole('status');
|
|
39
|
-
expect(region).toHaveAttribute('aria-live', 'assertive');
|
|
40
|
-
});
|
|
41
|
-
it('should have aria-atomic="true" by default', () => {
|
|
42
|
-
render(LiveRegion, {
|
|
43
|
-
props: { message: 'Test message' },
|
|
44
|
-
});
|
|
45
|
-
const region = screen.getByRole('status');
|
|
46
|
-
expect(region).toHaveAttribute('aria-atomic', 'true');
|
|
47
|
-
});
|
|
48
|
-
it('should support aria-atomic="false"', () => {
|
|
49
|
-
render(LiveRegion, {
|
|
50
|
-
props: { message: 'Test message', atomic: false },
|
|
51
|
-
});
|
|
52
|
-
const region = screen.getByRole('status');
|
|
53
|
-
expect(region).toHaveAttribute('aria-atomic', 'false');
|
|
54
|
-
});
|
|
55
|
-
it('should be visually hidden by default', () => {
|
|
56
|
-
render(LiveRegion, {
|
|
57
|
-
props: { message: 'Hidden message' },
|
|
58
|
-
});
|
|
59
|
-
const region = screen.getByRole('status');
|
|
60
|
-
expect(region).toHaveClass('sr-only');
|
|
61
|
-
});
|
|
62
|
-
it('should be visible when visuallyHidden is false', () => {
|
|
63
|
-
render(LiveRegion, {
|
|
64
|
-
props: { message: 'Visible message', visuallyHidden: false },
|
|
65
|
-
});
|
|
66
|
-
const region = screen.getByRole('status');
|
|
67
|
-
expect(region).not.toHaveClass('sr-only');
|
|
68
|
-
});
|
|
69
|
-
it('should render message content', () => {
|
|
70
|
-
render(LiveRegion, {
|
|
71
|
-
props: { message: 'Status: Loading' },
|
|
72
|
-
});
|
|
73
|
-
expect(screen.getByText('Status: Loading')).toBeInTheDocument();
|
|
74
|
-
});
|
|
75
|
-
it('should render children content', () => {
|
|
76
|
-
render(LiveRegion, {
|
|
77
|
-
props: {
|
|
78
|
-
children: () => {
|
|
79
|
-
const span = document.createElement('span');
|
|
80
|
-
span.textContent = 'Dynamic content';
|
|
81
|
-
return span;
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
// Children rendering in Svelte tests is different - verify region exists
|
|
86
|
-
const region = screen.getByRole('status');
|
|
87
|
-
expect(region).toBeInTheDocument();
|
|
88
|
-
});
|
|
89
|
-
it('should prefer message over children when both provided', () => {
|
|
90
|
-
render(LiveRegion, {
|
|
91
|
-
props: {
|
|
92
|
-
message: 'Message text',
|
|
93
|
-
children: () => {
|
|
94
|
-
const span = document.createElement('span');
|
|
95
|
-
span.textContent = 'Children text';
|
|
96
|
-
return span;
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
expect(screen.getByText('Message text')).toBeInTheDocument();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/svelte';
|
|
3
|
-
import { axe } from 'vitest-axe';
|
|
4
|
-
import Audio from '../../lib/components/media/Audio.svelte';
|
|
5
|
-
import Image from '../../lib/components/media/Image.svelte';
|
|
6
|
-
import Lightbox from '../../lib/components/media/Lightbox.svelte';
|
|
7
|
-
import MediaCarousel from '../../lib/components/media/MediaCarousel.svelte';
|
|
8
|
-
import MediaEmbed from '../../lib/components/media/MediaEmbed.svelte';
|
|
9
|
-
import MediaGrid from '../../lib/components/media/MediaGrid.svelte';
|
|
10
|
-
import Video from '../../lib/components/media/Video.svelte';
|
|
11
|
-
const items = [
|
|
12
|
-
{ id: '1', kind: 'image', src: '/one.jpg', alt: 'A harbour at dawn', caption: 'Dawn' },
|
|
13
|
-
{ id: '2', kind: 'image', src: '/two.jpg', alt: 'A field of wheat' },
|
|
14
|
-
{ id: '3', kind: 'video', src: '/clip.mp4', poster: '/clip.jpg', title: 'Walkthrough' },
|
|
15
|
-
];
|
|
16
|
-
const captions = [{ src: '/clip.en.vtt', srclang: 'en', label: 'English', default: true }];
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
// Video warns when captions are missing; those cases are covered elsewhere.
|
|
19
|
-
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
20
|
-
});
|
|
21
|
-
describe('Image accessibility', () => {
|
|
22
|
-
it('has no axe violations', async () => {
|
|
23
|
-
const { container } = render(Image, { props: { src: '/photo.jpg', alt: 'A landscape' } });
|
|
24
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
25
|
-
});
|
|
26
|
-
it('has no violations with a decorative empty alt', async () => {
|
|
27
|
-
const { container } = render(Image, { props: { src: '/divider.png', alt: '' } });
|
|
28
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
29
|
-
});
|
|
30
|
-
it('exposes the alt text as the image name', () => {
|
|
31
|
-
render(Image, { props: { src: '/photo.jpg', alt: 'A landscape' } });
|
|
32
|
-
expect(screen.getByRole('img', { name: 'A landscape' })).toBeInTheDocument();
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
describe('Video accessibility', () => {
|
|
36
|
-
it('has no axe violations', async () => {
|
|
37
|
-
const { container } = render(Video, {
|
|
38
|
-
props: { src: '/clip.mp4', label: 'Product tour', tracks: captions },
|
|
39
|
-
});
|
|
40
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
41
|
-
});
|
|
42
|
-
it('names the region and every transport control', () => {
|
|
43
|
-
render(Video, { props: { src: '/clip.mp4', label: 'Product tour', tracks: captions } });
|
|
44
|
-
expect(screen.getByRole('region')).toHaveAccessibleName('Product tour');
|
|
45
|
-
expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument();
|
|
46
|
-
expect(screen.getByRole('slider', { name: 'Seek' })).toBeInTheDocument();
|
|
47
|
-
expect(screen.getByRole('slider', { name: 'Volume' })).toBeInTheDocument();
|
|
48
|
-
});
|
|
49
|
-
it('announces the seek position as a spoken timestamp, not raw seconds', () => {
|
|
50
|
-
render(Video, { props: { src: '/clip.mp4', tracks: captions } });
|
|
51
|
-
expect(screen.getByRole('slider', { name: 'Seek' })).toHaveAttribute('aria-valuetext', '0 seconds of 0 seconds');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
describe('Audio accessibility', () => {
|
|
55
|
-
it('has no axe violations', async () => {
|
|
56
|
-
const { container } = render(Audio, {
|
|
57
|
-
props: { src: '/episode.mp3', title: 'Episode 12', artist: 'Design Notes' },
|
|
58
|
-
});
|
|
59
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
describe('MediaEmbed accessibility', () => {
|
|
63
|
-
it('has no axe violations in its facade state', async () => {
|
|
64
|
-
const { container } = render(MediaEmbed, {
|
|
65
|
-
props: { url: 'https://youtu.be/dQw4w9WgXcQ', title: 'A song' },
|
|
66
|
-
});
|
|
67
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
68
|
-
});
|
|
69
|
-
it('names the facade with the provider and title', () => {
|
|
70
|
-
render(MediaEmbed, { props: { url: 'https://youtu.be/dQw4w9WgXcQ', title: 'A song' } });
|
|
71
|
-
expect(screen.getByRole('button')).toHaveAccessibleName('Play A song on YouTube');
|
|
72
|
-
});
|
|
73
|
-
it('has no axe violations for a Matterport tour facade', async () => {
|
|
74
|
-
const { container } = render(MediaEmbed, {
|
|
75
|
-
props: { provider: 'matterport', modelId: 'SxQL3iGyoDo', title: 'Show home' },
|
|
76
|
-
});
|
|
77
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
78
|
-
});
|
|
79
|
-
it('says a tour opens rather than plays', () => {
|
|
80
|
-
render(MediaEmbed, {
|
|
81
|
-
props: { provider: 'matterport', modelId: 'SxQL3iGyoDo', title: 'Show home' },
|
|
82
|
-
});
|
|
83
|
-
expect(screen.getByRole('button')).toHaveAccessibleName('Open Show home as a 3D tour on Matterport');
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe('MediaGrid accessibility', () => {
|
|
87
|
-
it('has no axe violations', async () => {
|
|
88
|
-
const { container } = render(MediaGrid, { props: { items, label: 'Project photos' } });
|
|
89
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
90
|
-
});
|
|
91
|
-
it('names the list and every tile', () => {
|
|
92
|
-
render(MediaGrid, { props: { items, label: 'Project photos' } });
|
|
93
|
-
expect(screen.getByRole('list')).toHaveAccessibleName('Project photos');
|
|
94
|
-
expect(screen.getByRole('button', { name: 'A harbour at dawn' })).toBeInTheDocument();
|
|
95
|
-
expect(screen.getByRole('button', { name: 'Walkthrough' })).toBeInTheDocument();
|
|
96
|
-
});
|
|
97
|
-
it('has no violations in the masonry variant', async () => {
|
|
98
|
-
const { container } = render(MediaGrid, { props: { items, variant: 'masonry' } });
|
|
99
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe('MediaCarousel accessibility', () => {
|
|
103
|
-
it('has no axe violations', async () => {
|
|
104
|
-
const { container } = render(MediaCarousel, {
|
|
105
|
-
props: { items, label: 'Project gallery' },
|
|
106
|
-
});
|
|
107
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
108
|
-
});
|
|
109
|
-
it('carries the carousel and slide role descriptions', () => {
|
|
110
|
-
render(MediaCarousel, { props: { items, label: 'Project gallery' } });
|
|
111
|
-
const carousel = screen.getByRole('region', { name: 'Project gallery' });
|
|
112
|
-
expect(carousel).toHaveAttribute('aria-roledescription', 'carousel');
|
|
113
|
-
const slides = screen.getAllByRole('group');
|
|
114
|
-
expect(slides[0]).toHaveAttribute('aria-roledescription', 'slide');
|
|
115
|
-
expect(slides[0]).toHaveAccessibleName('1 of 3');
|
|
116
|
-
});
|
|
117
|
-
it('offers a pause control whenever autoplay is enabled (WCAG 2.2.2)', () => {
|
|
118
|
-
render(MediaCarousel, { props: { items, autoplay: true } });
|
|
119
|
-
expect(screen.getByRole('button', { name: 'Stop automatic slideshow' })).toBeInTheDocument();
|
|
120
|
-
});
|
|
121
|
-
it('has no violations with the thumbnail strip shown', async () => {
|
|
122
|
-
const { container } = render(MediaCarousel, { props: { items, showThumbnails: true } });
|
|
123
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
describe('Lightbox accessibility', () => {
|
|
127
|
-
it('has no axe violations when open', async () => {
|
|
128
|
-
render(Lightbox, { props: { items, open: true } });
|
|
129
|
-
await screen.findByRole('dialog');
|
|
130
|
-
// The dialog is portalled to the body, so axe has to run against it.
|
|
131
|
-
expect(await axe(document.body)).toHaveNoViolations();
|
|
132
|
-
});
|
|
133
|
-
it('is a modal dialog with an accessible name and description', async () => {
|
|
134
|
-
render(Lightbox, { props: { items, open: true } });
|
|
135
|
-
const dialog = await screen.findByRole('dialog');
|
|
136
|
-
expect(dialog).toHaveAttribute('aria-modal', 'true');
|
|
137
|
-
expect(dialog).toHaveAccessibleName('A harbour at dawn');
|
|
138
|
-
expect(dialog).toHaveAccessibleDescription('Dawn');
|
|
139
|
-
});
|
|
140
|
-
it('has no violations with the thumbnail strip shown', async () => {
|
|
141
|
-
render(Lightbox, { props: { items, open: true, showThumbnails: true } });
|
|
142
|
-
await screen.findByRole('dialog');
|
|
143
|
-
expect(await axe(document.body)).toHaveNoViolations();
|
|
144
|
-
});
|
|
145
|
-
});
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { axe } from 'vitest-axe';
|
|
5
|
-
import { createRawSnippet } from 'svelte';
|
|
6
|
-
import PublicLayout from '../../lib/components/layout/PublicLayout.svelte';
|
|
7
|
-
function textSnippet(text) {
|
|
8
|
-
return createRawSnippet(() => ({
|
|
9
|
-
render: () => `<span>${text}</span>`,
|
|
10
|
-
}));
|
|
11
|
-
}
|
|
12
|
-
const navigation = [
|
|
13
|
-
{ name: 'Orientation', href: '/orientation' },
|
|
14
|
-
{ name: 'Home Info', href: '/home-information-sheet' },
|
|
15
|
-
{ name: 'Warranty Transfer', href: '/warranty-transfer', active: true },
|
|
16
|
-
];
|
|
17
|
-
const footerLinks = [
|
|
18
|
-
{
|
|
19
|
-
title: 'Resources',
|
|
20
|
-
items: [
|
|
21
|
-
{ name: 'Documentation', href: '/docs' },
|
|
22
|
-
{ name: 'GitHub', href: 'https://github.com', external: true },
|
|
23
|
-
],
|
|
24
|
-
},
|
|
25
|
-
];
|
|
26
|
-
function renderLayout(extra = {}) {
|
|
27
|
-
return render(PublicLayout, {
|
|
28
|
-
props: { navigation, footerLinks, children: textSnippet('Page content'), ...extra },
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
/** A drawer CTA that calls the `close` argument PublicLayout hands to mobileNavEnd. */
|
|
32
|
-
function ctaSnippet(label) {
|
|
33
|
-
return createRawSnippet((args) => ({
|
|
34
|
-
render: () => `<button type="button">${label}</button>`,
|
|
35
|
-
setup: (el) => {
|
|
36
|
-
el.addEventListener('click', () => args().close());
|
|
37
|
-
},
|
|
38
|
-
}));
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* jsdom has no layout engine, so getBoundingClientRect() returns zeros and target
|
|
42
|
-
* size cannot be measured here. These assert the classes that produce a 44x44px
|
|
43
|
-
* target (WCAG 2.5.5); the geometry itself is verified in a real browser.
|
|
44
|
-
*/
|
|
45
|
-
describe('PublicLayout accessibility', () => {
|
|
46
|
-
it('should have no axe violations', async () => {
|
|
47
|
-
const { container } = renderLayout();
|
|
48
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
49
|
-
});
|
|
50
|
-
it('should expand desktop nav links to a 44px touch target', () => {
|
|
51
|
-
renderLayout();
|
|
52
|
-
const nav = screen.getByRole('navigation', { name: 'Main navigation' });
|
|
53
|
-
const links = [...nav.querySelectorAll(':scope > a')];
|
|
54
|
-
expect(links).toHaveLength(navigation.length);
|
|
55
|
-
for (const link of links) {
|
|
56
|
-
// 20px text line box + 2 x 12px padding = 44px border box; -m-3 cancels
|
|
57
|
-
// it in layout so the nav's gap-6 spacing is unchanged.
|
|
58
|
-
expect(link.className).toContain('p-3');
|
|
59
|
-
expect(link.className).toContain('-m-3');
|
|
60
|
-
expect(link.className).not.toContain('after:');
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
it('should give the logo link a 44px-tall touch target', () => {
|
|
64
|
-
renderLayout();
|
|
65
|
-
const logo = screen.getByRole('banner').querySelector('a[href="/"]');
|
|
66
|
-
expect(logo?.className).toContain('relative');
|
|
67
|
-
expect(logo?.className).toContain('after:h-11');
|
|
68
|
-
});
|
|
69
|
-
it('should give mobile drawer links a 44px touch target', async () => {
|
|
70
|
-
renderLayout();
|
|
71
|
-
// The drawer is only in the DOM while the menu is open.
|
|
72
|
-
await userEvent.click(screen.getByRole('button', { name: 'Open menu' }));
|
|
73
|
-
const drawerNav = screen.getByRole('navigation', { name: 'Mobile navigation' });
|
|
74
|
-
for (const link of drawerNav.querySelectorAll('a')) {
|
|
75
|
-
expect(link.className).toContain('py-3');
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
it('should give footer links a 44px-tall full-width touch target', () => {
|
|
79
|
-
renderLayout();
|
|
80
|
-
const footerNav = screen.getByRole('navigation', { name: 'Footer navigation' });
|
|
81
|
-
for (const link of footerNav.querySelectorAll('a')) {
|
|
82
|
-
// block + py-3 = 12 + 20 + 12; the list carries no space-y, so rows never overlap
|
|
83
|
-
expect(link.className).toContain('block');
|
|
84
|
-
expect(link.className).toContain('py-3');
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
it('should render headerEnd unwrapped when there is no mobileNavEnd', () => {
|
|
88
|
-
renderLayout({ headerEnd: textSnippet('Service Request') });
|
|
89
|
-
const cta = screen.getByText('Service Request');
|
|
90
|
-
expect(cta.parentElement?.className).not.toContain('hidden');
|
|
91
|
-
});
|
|
92
|
-
it('should hide headerEnd below the mobile breakpoint when mobileNavEnd is given', () => {
|
|
93
|
-
renderLayout({
|
|
94
|
-
headerEnd: textSnippet('Service Request'),
|
|
95
|
-
mobileNavEnd: ctaSnippet('Drawer CTA'),
|
|
96
|
-
});
|
|
97
|
-
const cta = screen.getByText('Service Request');
|
|
98
|
-
expect(cta.parentElement?.className).toContain('hidden');
|
|
99
|
-
expect(cta.parentElement?.className).toContain('md:contents');
|
|
100
|
-
});
|
|
101
|
-
it('should switch the header to the drawer at the given mobileBreakpoint', () => {
|
|
102
|
-
renderLayout({
|
|
103
|
-
mobileBreakpoint: 'lg',
|
|
104
|
-
headerEnd: textSnippet('Service Request'),
|
|
105
|
-
mobileNavEnd: ctaSnippet('Drawer CTA'),
|
|
106
|
-
});
|
|
107
|
-
const nav = screen.getByRole('navigation', { name: 'Main navigation' });
|
|
108
|
-
expect(nav.className).toContain('lg:flex');
|
|
109
|
-
expect(nav.className).not.toContain('md:flex');
|
|
110
|
-
expect(screen.getByRole('button', { name: 'Open menu' }).className).toContain('lg:hidden');
|
|
111
|
-
expect(screen.getByText('Service Request').parentElement?.className).toContain('lg:contents');
|
|
112
|
-
});
|
|
113
|
-
it('should render mobileNavEnd in the drawer and close it via the close argument', async () => {
|
|
114
|
-
renderLayout({
|
|
115
|
-
headerEnd: textSnippet('Service Request'),
|
|
116
|
-
mobileNavEnd: ctaSnippet('Drawer CTA'),
|
|
117
|
-
});
|
|
118
|
-
expect(screen.queryByRole('button', { name: 'Drawer CTA' })).toBeNull();
|
|
119
|
-
await userEvent.click(screen.getByRole('button', { name: 'Open menu' }));
|
|
120
|
-
const drawerNav = screen.getByRole('navigation', { name: 'Mobile navigation' });
|
|
121
|
-
const cta = screen.getByRole('button', { name: 'Drawer CTA' });
|
|
122
|
-
expect(drawerNav.contains(cta)).toBe(true);
|
|
123
|
-
await userEvent.click(cta);
|
|
124
|
-
expect(screen.queryByRole('navigation', { name: 'Mobile navigation' })).toBeNull();
|
|
125
|
-
});
|
|
126
|
-
it('should cue hover for a trackpad pointer that reports hover: none', () => {
|
|
127
|
-
renderLayout();
|
|
128
|
-
const nav = screen.getByRole('navigation', { name: 'Main navigation' });
|
|
129
|
-
const footerNav = screen.getByRole('navigation', { name: 'Footer navigation' });
|
|
130
|
-
// hoverOnlyWhenSupported gates hover: behind (hover: hover) and (pointer: fine),
|
|
131
|
-
// which iPadOS never matches even with a trackpad attached.
|
|
132
|
-
for (const link of [
|
|
133
|
-
...nav.querySelectorAll(':scope > a'),
|
|
134
|
-
...footerNav.querySelectorAll('a'),
|
|
135
|
-
]) {
|
|
136
|
-
expect(link.className).toContain('[@media(any-pointer:fine)]:[&:hover]:');
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
});
|