@classic-homes/theme-svelte 1.22.0 → 1.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/lib/components/Alert.svelte +8 -10
  2. package/dist/lib/components/DataPanel.svelte +19 -5
  3. package/dist/lib/components/HeaderSearch.svelte +18 -10
  4. package/dist/lib/components/Skeleton.svelte.d.ts +1 -1
  5. package/dist/lib/components/layout/AppShell.svelte +1 -1
  6. package/dist/lib/components/layout/DashboardLayout.svelte +15 -6
  7. package/dist/lib/components/layout/Footer.svelte +6 -6
  8. package/dist/lib/components/layout/FormPageLayout.svelte +4 -3
  9. package/dist/lib/components/layout/Sidebar.svelte +25 -21
  10. package/dist/lib/components/media/Image.svelte.d.ts +1 -1
  11. package/dist/lib/components/media/Lightbox.svelte +13 -9
  12. package/dist/lib/components/media/MediaCarousel.svelte +5 -0
  13. package/dist/lib/components/media/MediaGrid.svelte +4 -1
  14. package/dist/lib/components/notifications/NotificationDialog.svelte +8 -5
  15. package/dist/lib/components/notifications/NotificationItem.svelte +16 -6
  16. package/dist/lib/components/studio/InsertPalette.svelte +4 -1
  17. package/dist/lib/components/studio/StudioCanvas.svelte +23 -2
  18. package/dist/lib/components/studio/StudioCanvas.svelte.d.ts +5 -1
  19. package/dist/lib/composables/useFocusTrap.svelte.js +9 -2
  20. package/dist/lib/notices/sanitize.js +10 -3
  21. package/dist/lib/notices/store.d.ts +3 -3
  22. package/dist/lib/notices/store.js +14 -2
  23. package/dist/lib/notices/types.d.ts +8 -8
  24. package/dist/lib/notices/types.js +5 -1
  25. package/dist/lib/stores/sidebar.svelte.d.ts +6 -0
  26. package/dist/lib/stores/sidebar.svelte.js +61 -26
  27. package/dist/lib/utils/media.js +4 -3
  28. package/dist/lib/utils/url.d.ts +10 -0
  29. package/dist/lib/utils/url.js +21 -0
  30. package/dist/lib/utils.js +14 -1
  31. package/package.json +5 -4
  32. package/dist/__tests__/FileUpload.test.js +0 -45
  33. package/dist/__tests__/a11y/Alert.a11y.test.js +0 -84
  34. package/dist/__tests__/a11y/AlertDialog.a11y.test.js +0 -126
  35. package/dist/__tests__/a11y/Button.a11y.test.js +0 -66
  36. package/dist/__tests__/a11y/Card.a11y.test.js +0 -77
  37. package/dist/__tests__/a11y/Combobox.a11y.test.js +0 -209
  38. package/dist/__tests__/a11y/Dialog.a11y.test.js +0 -103
  39. package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +0 -132
  40. package/dist/__tests__/a11y/FileUpload.a11y.test.js +0 -158
  41. package/dist/__tests__/a11y/FilterToolbar.a11y.test.js +0 -62
  42. package/dist/__tests__/a11y/Icon.a11y.test.js +0 -119
  43. package/dist/__tests__/a11y/Input.a11y.test.js +0 -96
  44. package/dist/__tests__/a11y/LiveRegion.a11y.test.js +0 -102
  45. package/dist/__tests__/a11y/Media.a11y.test.js +0 -145
  46. package/dist/__tests__/a11y/PublicLayout.a11y.test.js +0 -139
  47. package/dist/__tests__/a11y/SegmentedControl.a11y.test.js +0 -50
  48. package/dist/__tests__/a11y/Switch.a11y.test.js +0 -95
  49. package/dist/__tests__/a11y/Tabs.a11y.test.js +0 -116
  50. package/dist/__tests__/a11y/Textarea.a11y.test.js +0 -111
  51. package/dist/__tests__/a11y/Toolbar.a11y.test.js +0 -31
  52. package/dist/__tests__/a11y/ViewToggle.a11y.test.js +0 -40
  53. package/dist/__tests__/action-header.test.js +0 -150
  54. package/dist/__tests__/alert-dialog-dismiss.test.js +0 -54
  55. package/dist/__tests__/code-field.test.js +0 -65
  56. package/dist/__tests__/control-text-color.test.js +0 -29
  57. package/dist/__tests__/data-panel-card-gestures.test.js +0 -138
  58. package/dist/__tests__/data-panel-viewport.test.js +0 -45
  59. package/dist/__tests__/dropdown-trigger.test.js +0 -39
  60. package/dist/__tests__/header-search.test.js +0 -90
  61. package/dist/__tests__/media-components.test.js +0 -206
  62. package/dist/__tests__/media-hls.test.js +0 -273
  63. package/dist/__tests__/media-matterport.test.js +0 -296
  64. package/dist/__tests__/media-players.test.js +0 -244
  65. package/dist/__tests__/media-scrubber.test.js +0 -44
  66. package/dist/__tests__/media-utils.test.js +0 -427
  67. package/dist/__tests__/nav-access.test.js +0 -145
  68. package/dist/__tests__/page-header.test.js +0 -36
  69. package/dist/__tests__/public-layout-drawer.test.js +0 -116
  70. package/dist/__tests__/setup.js +0 -56
  71. package/dist/__tests__/sidebar-collapse.test.js +0 -58
  72. package/dist/__tests__/sidebar-docs-parity.test.js +0 -134
  73. package/dist/__tests__/sidebar-expansion.test.js +0 -59
  74. package/dist/__tests__/sidebar-items.test.js +0 -59
  75. package/dist/__tests__/sidebar-mobile.test.js +0 -86
  76. package/dist/__tests__/studio-canvas-item.svelte.test.js +0 -98
  77. package/dist/__tests__/studio.test.js +0 -240
@@ -1,132 +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
- // Test wrapper component for DropdownMenu
6
- import { createRawSnippet } from 'svelte';
7
- import DropdownMenu from '../../lib/components/DropdownMenu.svelte';
8
- describe('DropdownMenu accessibility', () => {
9
- const items = [
10
- { label: 'Edit', value: 'edit', onSelect: () => { } },
11
- { label: 'Delete', value: 'delete', destructive: true, onSelect: () => { } },
12
- ];
13
- // Create a snippet for the trigger
14
- const createTriggerSnippet = () => createRawSnippet(() => ({
15
- render: () => '<span>Menu</span>',
16
- }));
17
- it('should render trigger as a button element', async () => {
18
- render(DropdownMenu, {
19
- props: {
20
- items,
21
- trigger: createTriggerSnippet(),
22
- },
23
- });
24
- // The trigger should be an actual button element, not a span with role="button"
25
- const trigger = screen.getByRole('button');
26
- expect(trigger).toBeInTheDocument();
27
- expect(trigger.tagName.toLowerCase()).toBe('button');
28
- });
29
- it('trigger button should have type="button"', async () => {
30
- render(DropdownMenu, {
31
- props: {
32
- items,
33
- trigger: createTriggerSnippet(),
34
- },
35
- });
36
- const trigger = screen.getByRole('button');
37
- expect(trigger).toHaveAttribute('type', 'button');
38
- });
39
- it('should have no axe violations when closed', async () => {
40
- const { container } = render(DropdownMenu, {
41
- props: {
42
- items,
43
- trigger: createTriggerSnippet(),
44
- },
45
- });
46
- const results = await axe(container);
47
- expect(results).toHaveNoViolations();
48
- });
49
- it('trigger should be focusable', async () => {
50
- render(DropdownMenu, {
51
- props: {
52
- items,
53
- trigger: createTriggerSnippet(),
54
- },
55
- });
56
- const trigger = screen.getByRole('button');
57
- await userEvent.tab();
58
- expect(trigger).toHaveFocus();
59
- });
60
- it('trigger should open menu on Enter key', async () => {
61
- render(DropdownMenu, {
62
- props: {
63
- items,
64
- trigger: createTriggerSnippet(),
65
- },
66
- });
67
- const trigger = screen.getByRole('button');
68
- await userEvent.tab();
69
- expect(trigger).toHaveFocus();
70
- await userEvent.keyboard('{Enter}');
71
- // Menu should be open - check for menu items
72
- expect(await screen.findByRole('menu')).toBeInTheDocument();
73
- });
74
- it('trigger should open menu on Space key', async () => {
75
- render(DropdownMenu, {
76
- props: {
77
- items,
78
- trigger: createTriggerSnippet(),
79
- },
80
- });
81
- screen.getByRole('button');
82
- await userEvent.tab();
83
- await userEvent.keyboard(' ');
84
- expect(await screen.findByRole('menu')).toBeInTheDocument();
85
- });
86
- it('should open menu with Enter key after focus', async () => {
87
- render(DropdownMenu, {
88
- props: {
89
- items,
90
- trigger: createTriggerSnippet(),
91
- },
92
- });
93
- const trigger = screen.getByRole('button');
94
- trigger.focus();
95
- // Open with Enter
96
- await userEvent.keyboard('{Enter}');
97
- // Wait for menu to open
98
- const menu = await screen.findByRole('menu');
99
- expect(menu).toBeInTheDocument();
100
- // Menu items should be present
101
- const menuItems = screen.getAllByRole('menuitem');
102
- expect(menuItems.length).toBeGreaterThan(0);
103
- });
104
- it('applies triggerLabel and triggerClass to the trigger button', () => {
105
- render(DropdownMenu, {
106
- props: {
107
- items,
108
- trigger: createTriggerSnippet(),
109
- triggerLabel: 'More actions',
110
- triggerClass: 'custom-trigger',
111
- },
112
- });
113
- const trigger = screen.getByRole('button', { name: 'More actions' });
114
- expect(trigger).toHaveClass('custom-trigger');
115
- // No nested buttons
116
- expect(trigger.querySelector('button')).toBeNull();
117
- });
118
- it('renders href items as links', async () => {
119
- render(DropdownMenu, {
120
- props: {
121
- items: [{ label: 'Open', href: '/somewhere', icon: 'external-link' }, ...items],
122
- trigger: createTriggerSnippet(),
123
- },
124
- });
125
- // Earlier tests leave Bits UI's `pointer-events: none` on <body> in jsdom.
126
- await userEvent.setup({ pointerEventsCheck: 0 }).click(screen.getByRole('button'));
127
- const link = await screen.findByRole('menuitem', { name: 'Open' });
128
- expect(link.tagName.toLowerCase()).toBe('a');
129
- expect(link).toHaveAttribute('href', '/somewhere');
130
- expect(screen.getByRole('menuitem', { name: 'Edit' }).tagName.toLowerCase()).toBe('div');
131
- });
132
- });
@@ -1,158 +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 FileUpload from '../../lib/components/FileUpload.svelte';
6
- describe('FileUpload accessibility', () => {
7
- it('should have no axe violations', async () => {
8
- const { container } = render(FileUpload, {
9
- props: {
10
- label: 'Upload documents',
11
- },
12
- });
13
- const results = await axe(container);
14
- expect(results).toHaveNoViolations();
15
- });
16
- it('should use label element for the drop zone', () => {
17
- render(FileUpload, {
18
- props: {
19
- label: 'Upload files',
20
- id: 'file-upload',
21
- },
22
- });
23
- // The drop zone should be a label wrapping the input
24
- const label = screen.getByText('Click to upload').closest('label');
25
- expect(label).toBeInTheDocument();
26
- expect(label).toHaveAttribute('for', 'file-upload');
27
- });
28
- it('should have a properly associated file input', () => {
29
- render(FileUpload, {
30
- props: {
31
- label: 'Upload files',
32
- id: 'file-upload',
33
- },
34
- });
35
- // The file input should exist and be associated with the label
36
- const input = document.getElementById('file-upload');
37
- expect(input).toBeInTheDocument();
38
- expect(input).toHaveAttribute('type', 'file');
39
- });
40
- it('should be keyboard accessible', async () => {
41
- render(FileUpload, {
42
- props: {
43
- label: 'Upload files',
44
- },
45
- });
46
- // Tab to the file input (which is inside the label)
47
- await userEvent.tab();
48
- // The file input should be focusable
49
- const input = screen.getByLabelText('Upload files');
50
- expect(input).toBeInTheDocument();
51
- });
52
- it('should have no axe violations with error state', async () => {
53
- const { container } = render(FileUpload, {
54
- props: {
55
- label: 'Upload files',
56
- error: 'File too large',
57
- },
58
- });
59
- const results = await axe(container);
60
- expect(results).toHaveNoViolations();
61
- });
62
- it('should announce errors with role="alert"', () => {
63
- render(FileUpload, {
64
- props: {
65
- label: 'Upload files',
66
- error: 'Invalid file type',
67
- },
68
- });
69
- const errorMessage = screen.getByRole('alert');
70
- expect(errorMessage).toBeInTheDocument();
71
- expect(errorMessage).toHaveTextContent('Invalid file type');
72
- });
73
- it('should have aria-describedby pointing to error message', () => {
74
- render(FileUpload, {
75
- props: {
76
- label: 'Upload files',
77
- id: 'upload',
78
- error: 'File size exceeds limit',
79
- },
80
- });
81
- const dropZone = screen.getByText('Click to upload').closest('label');
82
- expect(dropZone).toHaveAttribute('aria-describedby', 'upload-error');
83
- const errorMessage = document.getElementById('upload-error');
84
- expect(errorMessage).toHaveTextContent('File size exceeds limit');
85
- });
86
- it('should have accessible file list when files are uploaded', async () => {
87
- const files = [
88
- {
89
- id: '1',
90
- filename: 'document.pdf',
91
- contentType: 'application/pdf',
92
- size: 1024,
93
- uploading: false,
94
- progress: 100,
95
- },
96
- ];
97
- render(FileUpload, {
98
- props: {
99
- label: 'Upload files',
100
- files,
101
- },
102
- });
103
- // File list should be present with accessible label
104
- const fileList = screen.getByRole('list', { name: 'Uploaded files' });
105
- expect(fileList).toBeInTheDocument();
106
- });
107
- it('remove button should have accessible label', async () => {
108
- const files = [
109
- {
110
- id: '1',
111
- filename: 'test.pdf',
112
- contentType: 'application/pdf',
113
- size: 1024,
114
- uploading: false,
115
- progress: 100,
116
- },
117
- ];
118
- render(FileUpload, {
119
- props: {
120
- label: 'Upload files',
121
- files,
122
- },
123
- });
124
- const removeButton = screen.getByRole('button', { name: 'Remove test.pdf' });
125
- expect(removeButton).toBeInTheDocument();
126
- });
127
- it('should respect disabled state', async () => {
128
- render(FileUpload, {
129
- props: {
130
- label: 'Upload files',
131
- disabled: true,
132
- },
133
- });
134
- // Find the input
135
- const input = screen.getByLabelText('Upload files');
136
- expect(input).toBeDisabled();
137
- });
138
- it('should support multiple files when maxFiles > 1', () => {
139
- render(FileUpload, {
140
- props: {
141
- label: 'Upload files',
142
- maxFiles: 5,
143
- },
144
- });
145
- const input = screen.getByLabelText('Upload files');
146
- expect(input).toHaveAttribute('multiple');
147
- });
148
- it('should not have multiple attribute when maxFiles is 1', () => {
149
- render(FileUpload, {
150
- props: {
151
- label: 'Upload files',
152
- maxFiles: 1,
153
- },
154
- });
155
- const input = screen.getByLabelText('Upload files');
156
- expect(input).not.toHaveAttribute('multiple');
157
- });
158
- });
@@ -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
- });