comparison design_system/test/storybook_test.js @ 254:2b6e732087ff

[ui] Add complete native component catalog Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 15:12:09 -0700
parents fdf3816959cb
children 30c2196d03d4
comparison
equal deleted inserted replaced
253:fdf3816959cb 254:2b6e732087ff
118 assert.equal(await requestRawPath(port, '/../../MODULE.bazel'), 400); 118 assert.equal(await requestRawPath(port, '/../../MODULE.bazel'), 400);
119 119
120 for (const route of [ 120 for (const route of [
121 '/', 121 '/',
122 '/tokens', 122 '/tokens',
123 '/icons',
123 '/components', 124 '/components',
124 '/components/button', 125 '/components/button',
125 '/components/card', 126 '/components/card',
126 '/components/alert', 127 '/components/alert',
127 '/components/field', 128 '/components/field',
136 ); 137 );
137 } 138 }
138 for (const asset of [ 139 for (const asset of [
139 '/styles/tokens.css', 140 '/styles/tokens.css',
140 '/styles/components.css', 141 '/styles/components.css',
142 '/styles/shadcn.css',
141 '/components/index.js', 143 '/components/index.js',
144 '/components/icon.js',
145 '/catalog.js',
142 '/storybook.js', 146 '/storybook.js',
143 ]) { 147 ]) {
144 const response = await fetch(`${baseUrl}${asset}`); 148 const response = await fetch(`${baseUrl}${asset}`);
145 assert.equal(response.status, 200, asset); 149 assert.equal(response.status, 200, asset);
146 assert.ok((await response.text()).length > 100, asset); 150 assert.ok((await response.text()).length > 100, asset);
152 args: ['--no-sandbox'], 156 args: ['--no-sandbox'],
153 }); 157 });
154 const context = await browser.newContext({ colorScheme: 'light' }); 158 const context = await browser.newContext({ colorScheme: 'light' });
155 const page = await context.newPage(); 159 const page = await context.newPage();
156 const errors = []; 160 const errors = [];
157 page.on('pageerror', error => errors.push(error.message)); 161 const devtools = await context.newCDPSession(page);
162 await devtools.send('Runtime.enable');
163 devtools.on('Runtime.exceptionThrown', event => {
164 const exception = event.exceptionDetails;
165 errors.push(
166 `${exception.text}: ${exception.exception?.description || ''} ` +
167 `${exception.url || ''}:${exception.lineNumber + 1}`,
168 );
169 });
170 page.on('pageerror', error => errors.push(error.stack || error.message));
158 page.on('console', message => { 171 page.on('console', message => {
159 if (message.type() === 'error') errors.push(message.text()); 172 if (message.type() === 'error') {
173 const location = message.location();
174 errors.push(
175 `${message.text()} ${location.url || ''}:${location.lineNumber + 1}`,
176 );
177 }
160 }); 178 });
161 page.on('response', response => { 179 page.on('response', response => {
162 if (response.status() >= 400) { 180 if (response.status() >= 400) {
163 errors.push(`${response.status()} ${response.url()}`); 181 errors.push(`${response.status()} ${response.url()}`);
164 } 182 }
165 }); 183 });
166 184
167 await page.goto(`${baseUrl}/components/button`, { 185 await page.goto(`${baseUrl}/components/button`, {
168 waitUntil: 'networkidle', 186 waitUntil: 'networkidle',
169 }); 187 });
170 await page.waitForFunction(() => 188 try {
171 customElements.get('zen-button') && 189 await page.waitForFunction(
172 customElements.get('zen-story') 190 () => customElements.get('zen-button') &&
173 ); 191 customElements.get('zen-story'),
192 undefined,
193 { timeout: 5000 },
194 );
195 } catch (error) {
196 throw new Error(`${error.message}\n${errors.join('\n')}`);
197 }
198 const catalogState = await page.evaluate(async () => {
199 const links = [
200 ...document.querySelectorAll(
201 '.catalog-nav[aria-label="Components"] a',
202 ),
203 ];
204 const elements = [...new Set(
205 links.map(link => link.dataset.zenElement).filter(Boolean),
206 )];
207 const statuses = await Promise.all(
208 links.map(async link => (await fetch(link.href)).status),
209 );
210 return {
211 cards: document.querySelectorAll('.component-grid > a').length,
212 elements,
213 links: links.length,
214 missing: elements.filter(name => !customElements.get(name)),
215 routes: links.map(link => new URL(link.href).pathname),
216 statuses,
217 };
218 });
219 assert.equal(catalogState.cards, 66);
220 assert.equal(catalogState.links, 66);
221 assert.equal(catalogState.elements.length, 65);
222 assert.deepEqual(catalogState.missing, []);
223 assert.deepEqual(
224 [...new Set(catalogState.statuses)],
225 [200],
226 );
227 const brokenCatalogPages = await page.evaluate(routes => {
228 const failures = [];
229 for (const route of ['/', '/tokens', '/icons', '/components', ...routes]) {
230 history.replaceState({}, '', route);
231 window.dispatchEvent(new PopStateEvent('popstate'));
232 const visible = [
233 ...document.querySelectorAll('[data-catalog-page]:not([hidden])'),
234 ];
235 const expected = route === '/' || route === '/components'
236 ? 'overview'
237 : route.slice(route.lastIndexOf('/') + 1);
238 if (visible.length !== 1 ||
239 visible[0].dataset.catalogPage !== expected ||
240 !visible[0].querySelector('h1')?.textContent.trim()) {
241 failures.push(route);
242 continue;
243 }
244 if (route.startsWith('/components/')) {
245 const story = visible[0].querySelector('zen-story');
246 const canvas = story?.querySelector('.story-canvas');
247 const source = story?.querySelector('pre code');
248 if (!story || !canvas?.children.length ||
249 !source?.textContent.trim()) {
250 failures.push(`${route}:empty-story`);
251 }
252 }
253 }
254 history.replaceState({}, '', '/components/button');
255 window.dispatchEvent(new PopStateEvent('popstate'));
256 return failures;
257 }, catalogState.routes);
258 assert.deepEqual(brokenCatalogPages, []);
259 await page.keyboard.press('/');
260 assert.equal(
261 await page.locator('#componentSearch').evaluate(
262 input => document.activeElement === input,
263 ),
264 true,
265 );
266 await page.locator('#componentSearch').fill('date');
267 assert.equal(
268 await page.locator(
269 '.catalog-nav[aria-label="Components"] a:not([hidden])',
270 ).count(),
271 2,
272 );
273 assert.equal(
274 await page.locator('.component-grid > a:not([hidden])').count(),
275 2,
276 );
277 assert.match(
278 await page.locator('#componentSearchStatus').textContent(),
279 /^2 components found$/,
280 );
281 await page.locator('#componentSearch').press('Escape');
282 assert.equal(await page.locator('#componentSearch').inputValue(), '');
283 assert.equal(
284 await page.locator(
285 '.catalog-nav[aria-label="Components"] a:not([hidden])',
286 ).count(),
287 66,
288 );
289 await page.locator('#componentSearch').fill('stack');
290 assert.equal(
291 await page.locator(
292 '.catalog-nav[aria-label="Components"] a[href="/components/stack"]',
293 ).getAttribute('hidden'),
294 null,
295 );
296 assert.equal(
297 await page.locator(
298 '.component-grid > a[href="/components/stack"]',
299 ).getAttribute('hidden'),
300 null,
301 );
302 await page.locator('#componentSearch').press('Escape');
174 assert.equal( 303 assert.equal(
175 await page.locator('[data-catalog-page="button"]').isVisible(), 304 await page.locator('[data-catalog-page="button"]').isVisible(),
176 true, 305 true,
177 ); 306 );
178 assert.equal( 307 assert.equal(
263 ctrlKey: true, 392 ctrlKey: true,
264 })); 393 }));
265 }); 394 });
266 assert.equal(modifiedClickAllowed, true); 395 assert.equal(modifiedClickAllowed, true);
267 396
397 await page.goto(`${baseUrl}/icons`, { waitUntil: 'networkidle' });
398 const iconCount = await page.locator('#iconGrid figure').count();
399 assert.ok(iconCount >= 30);
400 assert.equal(
401 await page.locator('#iconGrid figure zen-icon > svg').count(),
402 iconCount,
403 );
404 const iconFallback = await page.evaluate(() => {
405 const icon = document.createElement('zen-icon');
406 icon.setAttribute('name', 'not-a-real-icon');
407 icon.setAttribute('label', 'Missing icon fallback');
408 document.body.append(icon);
409 return {
410 fallback: icon.dataset.zenIcon,
411 hidden: icon.getAttribute('aria-hidden'),
412 label: icon.getAttribute('aria-label'),
413 role: icon.getAttribute('role'),
414 };
415 });
416 assert.deepEqual(iconFallback, {
417 fallback: 'alert',
418 hidden: null,
419 label: 'Missing icon fallback',
420 role: 'img',
421 });
422
423 await page.goto(`${baseUrl}/tokens`, { waitUntil: 'networkidle' });
424 const colorTokenState = await page.evaluate(() => {
425 const root = document.documentElement;
426 const value = name => getComputedStyle(root)
427 .getPropertyValue(`--zen-color-${name}`)
428 .trim();
429 const families = [
430 'neutral',
431 'red',
432 'orange',
433 'amber',
434 'green',
435 'teal',
436 'blue',
437 'violet',
438 'rose',
439 'brown',
440 ];
441 const steps = [
442 '50',
443 '100',
444 '200',
445 '300',
446 '400',
447 '500',
448 '600',
449 '700',
450 '800',
451 '900',
452 '950',
453 ];
454 const rampsComplete = families.every(family =>
455 steps.every(step => value(`${family}-${step}`))
456 );
457 const data = Array.from(
458 { length: 10 },
459 (_, index) => value(`data-${index + 1}`),
460 );
461 const materials = [
462 'paper',
463 'washi',
464 'linen',
465 'sumi',
466 'brick',
467 'persimmon',
468 'ochre',
469 'moss',
470 'patina',
471 'indigo',
472 'plum',
473 'clay',
474 'wood',
475 ].map(value);
476 const lightMuted = value('surface-muted');
477 root.dataset.zenTheme = 'dark';
478 const darkMuted = value('surface-muted');
479 const darkData = Array.from(
480 { length: 10 },
481 (_, index) => value(`data-${index + 1}`),
482 );
483 delete root.dataset.zenTheme;
484 return {
485 darkData,
486 darkMuted,
487 data,
488 lightMuted,
489 materials,
490 rampsComplete,
491 };
492 });
493 assert.equal(colorTokenState.rampsComplete, true);
494 assert.equal(new Set(colorTokenState.materials).size, 13);
495 assert.equal(new Set(colorTokenState.data).size, 10);
496 assert.equal(new Set(colorTokenState.darkData).size, 10);
497 assert.notDeepEqual(colorTokenState.data, colorTokenState.darkData);
498 assert.notEqual(
499 colorTokenState.lightMuted,
500 colorTokenState.darkMuted,
501 );
502 assert.equal(await page.locator('.palette-grid > div').count(), 13);
503 assert.equal(await page.locator('.data-palette > span').count(), 10);
504
268 await page.goto(`${baseUrl}/components/field`, { 505 await page.goto(`${baseUrl}/components/field`, {
269 waitUntil: 'networkidle', 506 waitUntil: 'networkidle',
270 }); 507 });
271 const fieldWiring = await page.locator('zen-field').first().evaluate(field => { 508 const fieldWiring = await page.locator('zen-field').first().evaluate(field => {
272 const label = field.querySelector('label'); 509 const label = field.querySelector('label');
283 }); 520 });
284 assert.ok(fieldWiring.inputId); 521 assert.ok(fieldWiring.inputId);
285 assert.equal(fieldWiring.labelFor, fieldWiring.inputId); 522 assert.equal(fieldWiring.labelFor, fieldWiring.inputId);
286 assert.equal(fieldWiring.describedBy, fieldWiring.helpId); 523 assert.equal(fieldWiring.describedBy, fieldWiring.helpId);
287 assert.equal(fieldWiring.invalid, true); 524 assert.equal(fieldWiring.invalid, true);
525 await page.locator('zen-field input').first().focus();
526 assert.equal(
527 await page.locator('zen-field input').first().evaluate(
528 input => getComputedStyle(input).outlineStyle,
529 ),
530 'none',
531 );
288 const replacementField = await page.locator('zen-field').first().evaluate( 532 const replacementField = await page.locator('zen-field').first().evaluate(
289 async field => { 533 async field => {
290 const label = field.querySelector('label'); 534 const label = field.querySelector('label');
291 const oldInput = field.querySelector('input'); 535 const oldInput = field.querySelector('input');
292 const blocker = document.createElement('div'); 536 const blocker = document.createElement('div');
348 events, 592 events,
349 }; 593 };
350 }); 594 });
351 assert.equal(authoredDismissState.connected, true); 595 assert.equal(authoredDismissState.connected, true);
352 assert.equal(authoredDismissState.events, 0); 596 assert.equal(authoredDismissState.events, 0);
597
598 await page.goto(`${baseUrl}/components/accordion`, {
599 waitUntil: 'networkidle',
600 });
601 const accordion = page.locator('zen-accordion').first();
602 const disclosureIcon = accordion.locator(
603 'summary > zen-icon[data-zen-disclosure-icon]',
604 ).nth(1);
605 const collapsedDisclosure = await disclosureIcon.evaluate(icon => {
606 const style = getComputedStyle(icon);
607 return {
608 duration: style.transitionDuration,
609 transform: style.transform,
610 };
611 });
612 assert.notEqual(collapsedDisclosure.duration, '0s');
613 await accordion.locator('summary').nth(1).click();
614 await page.waitForTimeout(250);
615 assert.notEqual(
616 await disclosureIcon.evaluate(icon => getComputedStyle(icon).transform),
617 collapsedDisclosure.transform,
618 );
619 assert.equal(
620 await accordion.locator('details').first().getAttribute('open'),
621 null,
622 );
623 assert.equal(
624 await accordion.locator('details').nth(1).getAttribute('open'),
625 '',
626 );
627 await accordion.locator('summary').nth(1).press('ArrowUp');
628 assert.equal(
629 await accordion.locator('summary').first().evaluate(
630 summary => document.activeElement === summary,
631 ),
632 true,
633 );
634
635 await page.goto(`${baseUrl}/components/aspect-ratio`, {
636 waitUntil: 'networkidle',
637 });
638 const aspectColors = await page.locator('.demo-media').evaluate(media => {
639 const style = getComputedStyle(media);
640 const probe = document.createElement('span');
641 probe.style.background = 'var(--zen-color-surface-muted)';
642 document.body.append(probe);
643 const token = getComputedStyle(probe).backgroundColor;
644 probe.remove();
645 return {
646 background: style.backgroundColor,
647 image: style.backgroundImage,
648 token,
649 };
650 });
651 assert.equal(aspectColors.image, 'none');
652 assert.equal(aspectColors.background, aspectColors.token);
653
654 await page.goto(`${baseUrl}/components/tabs`, {
655 waitUntil: 'networkidle',
656 });
657 await page.evaluate(() => {
658 window.__tabValue = null;
659 document.querySelector('zen-tabs').addEventListener(
660 'zen-change',
661 event => {
662 window.__tabValue = event.detail.value;
663 },
664 );
665 });
666 await page.locator('zen-tabs [role="tab"]').nth(1).click();
667 assert.equal(
668 await page.locator('zen-tabs [role="tab"]').nth(1)
669 .getAttribute('aria-selected'),
670 'true',
671 );
672 assert.equal(
673 await page.locator('zen-tabs [role="tabpanel"]').nth(1)
674 .getAttribute('hidden'),
675 null,
676 );
677 assert.equal(await page.evaluate(() => window.__tabValue), 'history');
678
679 await page.goto(`${baseUrl}/components/dialog`, {
680 waitUntil: 'networkidle',
681 });
682 await page.locator('zen-dialog [data-zen-trigger]').click();
683 assert.equal(await page.locator('zen-dialog dialog').getAttribute('open'), '');
684 await page.locator('zen-dialog [data-zen-close]').click();
685 assert.equal(await page.locator('zen-dialog dialog').getAttribute('open'), null);
686 const dynamicOverlays = await page.evaluate(async () => {
687 const dialogHost = document.createElement('zen-dialog');
688 const popoverHost = document.createElement('zen-popover');
689 const tooltipHost = document.createElement('zen-tooltip');
690 const menuHost = document.createElement('zen-dropdown-menu');
691 document.body.append(dialogHost, popoverHost, tooltipHost, menuHost);
692
693 const dialogTrigger = document.createElement('button');
694 dialogTrigger.type = 'button';
695 dialogTrigger.dataset.zenTrigger = '';
696 const dialog = document.createElement('dialog');
697 dialogHost.append(dialogTrigger, dialog);
698
699 const popoverTrigger = document.createElement('button');
700 popoverTrigger.type = 'button';
701 popoverTrigger.dataset.zenTrigger = '';
702 const popover = document.createElement('div');
703 popover.dataset.zenContent = '';
704 popoverHost.append(popoverTrigger, popover);
705
706 const tooltipTrigger = document.createElement('button');
707 tooltipTrigger.type = 'button';
708 tooltipTrigger.dataset.zenTrigger = '';
709 const tooltip = document.createElement('span');
710 tooltip.dataset.zenContent = '';
711 tooltipHost.append(tooltipTrigger, tooltip);
712
713 const menuTrigger = document.createElement('button');
714 menuTrigger.type = 'button';
715 menuTrigger.dataset.zenTrigger = '';
716 const menu = document.createElement('div');
717 menu.setAttribute('role', 'menu');
718 const item = document.createElement('button');
719 item.type = 'button';
720 item.setAttribute('role', 'menuitem');
721 menu.append(item);
722 menuHost.append(menuTrigger, menu);
723
724 await new Promise(resolve => setTimeout(resolve));
725 dialogTrigger.click();
726 const dialogOpen = dialog.open;
727 dialog.close();
728 popoverTrigger.click();
729 menuTrigger.click();
730 tooltipTrigger.focus();
731 await new Promise(resolve => setTimeout(resolve, 375));
732 return {
733 dialogControlled: Boolean(dialogTrigger.getAttribute('aria-controls')),
734 dialogOpen,
735 menuOpen: !menu.hidden,
736 popoverExpanded: popoverTrigger.getAttribute('aria-expanded'),
737 tooltipOpen: !tooltip.hidden,
738 };
739 });
740 assert.deepEqual(dynamicOverlays, {
741 dialogControlled: true,
742 dialogOpen: true,
743 menuOpen: true,
744 popoverExpanded: 'true',
745 tooltipOpen: true,
746 });
747
748 await page.goto(`${baseUrl}/components/dropdown-menu`, {
749 waitUntil: 'networkidle',
750 });
751 const dropdownTrigger = page.locator(
752 'zen-dropdown-menu [data-zen-trigger]',
753 );
754 await dropdownTrigger.click();
755 assert.equal(
756 await page.locator('zen-dropdown-menu [role="menu"]')
757 .getAttribute('hidden'),
758 null,
759 );
760 await page.keyboard.press('Escape');
761 assert.equal(
762 await page.locator('zen-dropdown-menu [role="menu"]')
763 .getAttribute('hidden'),
764 '',
765 );
766 assert.equal(
767 await dropdownTrigger.evaluate(
768 trigger => document.activeElement === trigger,
769 ),
770 true,
771 );
772 await dropdownTrigger.click();
773 await page.locator('zen-dropdown-menu [role="menuitem"]').first().click();
774 assert.equal(
775 await dropdownTrigger.evaluate(
776 trigger => document.activeElement === trigger,
777 ),
778 true,
779 );
780
781 await page.goto(`${baseUrl}/components/combobox`, {
782 waitUntil: 'networkidle',
783 });
784 const comboboxInput = page.locator('zen-combobox input');
785 await comboboxInput.fill('seo');
786 assert.equal(
787 await page.locator('zen-combobox [role="option"]:not([hidden])').count(),
788 1,
789 );
790 await comboboxInput.press('ArrowDown');
791 await comboboxInput.press('Enter');
792 assert.equal(await comboboxInput.inputValue(), 'seobeo');
793 assert.equal(
794 await page.locator('zen-combobox [role="listbox"]')
795 .getAttribute('hidden'),
796 '',
797 );
798
799 await page.goto(`${baseUrl}/components/date-picker`, {
800 waitUntil: 'networkidle',
801 });
802 const dateInput = page.locator('zen-date-picker input[type="date"]');
803 assert.equal(await dateInput.getAttribute('aria-hidden'), 'true');
804 assert.equal(
805 await dateInput.evaluate(input => getComputedStyle(input).position),
806 'absolute',
807 );
808 await page.evaluate(() => {
809 window.__dateValue = null;
810 document.querySelector('zen-date-picker').addEventListener(
811 'zen-change',
812 event => {
813 window.__dateValue = event.detail.value;
814 },
815 );
816 });
817 const dateTrigger = page.locator(
818 'zen-date-picker [data-zen-date-trigger]',
819 );
820 await dateTrigger.click();
821 assert.equal(
822 await page.locator('zen-date-picker [data-zen-calendar-panel]')
823 .getAttribute('hidden'),
824 null,
825 );
826 assert.equal(
827 await page.locator(
828 'zen-date-picker [data-zen-calendar-day]',
829 ).count(),
830 42,
831 );
832 const chosenDate = await page.locator(
833 'zen-date-picker [data-zen-calendar-day]:not([data-outside]):not(:disabled)',
834 ).nth(14).getAttribute('data-zen-calendar-day');
835 await page.locator(
836 `zen-date-picker [data-zen-calendar-day="${chosenDate}"]`,
837 ).click();
838 assert.equal(await dateInput.inputValue(), chosenDate);
839 assert.equal(await page.evaluate(() => window.__dateValue), chosenDate);
840 assert.equal(await dateTrigger.getAttribute('aria-expanded'), 'false');
841 const triggerName = await dateTrigger.evaluate(trigger =>
842 (trigger.getAttribute('aria-labelledby') || '')
843 .split(/\s+/)
844 .map(id => document.getElementById(id)?.textContent || '')
845 .join(' ')
846 );
847 assert.match(triggerName, /Deploy on/);
848 assert.equal(
849 await page.locator('zen-date-picker > label').getAttribute('for'),
850 await dateTrigger.getAttribute('id'),
851 );
852 const minimumDate = `${chosenDate.slice(0, 8)}10`;
853 await dateInput.evaluate((input, minimum) => {
854 input.min = minimum;
855 }, minimumDate);
856 await page.waitForTimeout(0);
857 await dateTrigger.click();
858 const focusedBoundary = page.locator(
859 `zen-date-picker [data-zen-calendar-day="${chosenDate}"]`,
860 );
861 await focusedBoundary.press('PageUp');
862 assert.equal(
863 await page.evaluate(() =>
864 document.activeElement?.dataset.zenCalendarDay
865 ),
866 minimumDate,
867 );
868 assert.equal(
869 await page.locator(
870 'zen-date-picker [data-zen-calendar-day][tabindex="0"]:not(:disabled)',
871 ).count(),
872 1,
873 );
874 await page.keyboard.press('Escape');
875 await dateInput.evaluate(input => {
876 input.disabled = true;
877 });
878 await page.waitForTimeout(0);
879 assert.equal(await dateTrigger.isDisabled(), true);
880 await dateInput.evaluate(input => {
881 input.disabled = false;
882 input.readOnly = true;
883 });
884 await page.waitForTimeout(0);
885 assert.equal(await dateTrigger.isDisabled(), true);
886 await dateInput.evaluate(input => {
887 input.readOnly = false;
888 input.required = true;
889 input.value = '';
890 input.dispatchEvent(new Event('input', { bubbles: true }));
891 input.reportValidity();
892 });
893 await page.waitForTimeout(0);
894 assert.equal(await dateTrigger.getAttribute('aria-invalid'), 'true');
895 assert.equal(
896 await dateTrigger.evaluate(
897 trigger => document.activeElement === trigger,
898 ),
899 true,
900 );
901 assert.equal(
902 await page.locator('zen-date-picker').getAttribute('data-invalid'),
903 '',
904 );
905 await dateInput.evaluate((input, value) => {
906 input.value = value;
907 input.dispatchEvent(new Event('input', { bubbles: true }));
908 }, chosenDate);
909 assert.equal(await dateTrigger.getAttribute('aria-invalid'), 'false');
910 const earlyYear = await page.evaluate(async () => {
911 const calendar = document.createElement('zen-calendar');
912 const input = document.createElement('input');
913 input.type = 'date';
914 input.value = '0001-01-01';
915 input.setAttribute('aria-label', 'Early date');
916 calendar.append(input);
917 document.body.append(calendar);
918 await new Promise(resolve => setTimeout(resolve));
919 return {
920 selected: calendar.querySelector(
921 '[data-zen-calendar-day][aria-selected="true"]',
922 )?.dataset.zenCalendarDay,
923 value: input.value,
924 };
925 });
926 assert.deepEqual(earlyYear, {
927 selected: '0001-01-01',
928 value: '0001-01-01',
929 });
930 const resetDateState = await page.evaluate(async () => {
931 const label = document.createElement('span');
932 label.id = 'reset-date-label';
933 label.textContent = 'Reset date';
934 const form = document.createElement('form');
935 const picker = document.createElement('zen-date-picker');
936 const input = document.createElement('input');
937 input.type = 'date';
938 input.defaultValue = '2026-08-04';
939 input.setAttribute('aria-labelledby', label.id);
940 picker.append(input);
941 form.append(picker);
942 document.body.append(label, form);
943 await new Promise(resolve => setTimeout(resolve));
944 input.value = '2026-08-12';
945 input.dispatchEvent(new Event('input', { bubbles: true }));
946 form.reset();
947 await new Promise(resolve => setTimeout(resolve));
948 const trigger = picker.querySelector('[data-zen-date-trigger]');
949 return {
950 labelledBy: trigger.getAttribute('aria-labelledby'),
951 selected: picker.querySelector(
952 '[data-zen-calendar-day][aria-selected="true"]',
953 )?.dataset.zenCalendarDay,
954 value: input.value,
955 };
956 });
957 assert.match(resetDateState.labelledBy, /^reset-date-label /);
958 assert.deepEqual(
959 {
960 selected: resetDateState.selected,
961 value: resetDateState.value,
962 },
963 {
964 selected: '2026-08-04',
965 value: '2026-08-04',
966 },
967 );
968
969 await page.goto(`${baseUrl}/components/input-otp`, {
970 waitUntil: 'networkidle',
971 });
972 await page.evaluate(() => {
973 window.__otpValue = null;
974 document.querySelector('zen-input-otp').addEventListener(
975 'zen-complete',
976 event => {
977 window.__otpValue = event.detail.value;
978 },
979 );
980 });
981 await page.locator('zen-input-otp input').evaluate(input => {
982 input.value = '12a3456';
983 input.dispatchEvent(new Event('input', { bubbles: true }));
984 });
985 assert.equal(await page.locator('zen-input-otp input').inputValue(), '123456');
986 assert.equal(
987 await page.locator('zen-input-otp [data-zen-otp-slots] > span').count(),
988 6,
989 );
990 assert.equal(await page.evaluate(() => window.__otpValue), '123456');
991
992 await page.goto(`${baseUrl}/components/data-table`, {
993 waitUntil: 'networkidle',
994 });
995 await page.locator('zen-data-table [data-zen-sort="commits"]').click();
996 assert.equal(
997 await page.locator('zen-data-table th').nth(1).getAttribute('aria-sort'),
998 'ascending',
999 );
1000 assert.equal(
1001 await page.locator('zen-data-table tbody tr').first().locator(
1002 '[data-key="commits"]',
1003 ).textContent(),
1004 '12',
1005 );
1006
1007 await page.goto(`${baseUrl}/components/carousel`, {
1008 waitUntil: 'networkidle',
1009 });
1010 await page.evaluate(() => {
1011 window.__carouselIndex = null;
1012 document.querySelector('zen-carousel').addEventListener(
1013 'zen-change',
1014 event => {
1015 window.__carouselIndex = event.detail.index;
1016 },
1017 );
1018 });
1019 await page.locator('zen-carousel [data-zen-next]').click();
1020 assert.equal(await page.evaluate(() => window.__carouselIndex), 1);
1021 assert.equal(
1022 await page.locator('zen-carousel [data-zen-prev]').isDisabled(),
1023 false,
1024 );
1025
1026 await page.goto(`${baseUrl}/components/resizable`, {
1027 waitUntil: 'networkidle',
1028 });
1029 const resizeHandle = page.locator('zen-resizable [data-zen-handle]');
1030 const initialPercent = Number(
1031 await resizeHandle.getAttribute('aria-valuenow'),
1032 );
1033 await resizeHandle.focus();
1034 await resizeHandle.press('ArrowRight');
1035 assert.ok(
1036 Number(await resizeHandle.getAttribute('aria-valuenow')) > initialPercent,
1037 );
1038
1039 await page.goto(`${baseUrl}/components/switch`, {
1040 waitUntil: 'networkidle',
1041 });
1042 const switchInput = page.locator('zen-switch input');
1043 assert.equal(await switchInput.getAttribute('role'), 'switch');
1044 assert.equal(await switchInput.getAttribute('aria-checked'), 'true');
1045 await switchInput.uncheck();
1046 assert.equal(await switchInput.getAttribute('aria-checked'), 'false');
1047
1048 await page.goto(`${baseUrl}/components/checkbox`, {
1049 waitUntil: 'networkidle',
1050 });
1051 assert.equal(
1052 await page.locator('zen-checkbox input').evaluate(
1053 input => getComputedStyle(input).appearance,
1054 ),
1055 'none',
1056 );
1057 await page.goto(`${baseUrl}/components/native-select`, {
1058 waitUntil: 'networkidle',
1059 });
1060 assert.equal(
1061 await page.locator('zen-native-select select').evaluate(
1062 select => getComputedStyle(select).appearance,
1063 ),
1064 'none',
1065 );
1066 assert.equal(
1067 await page.locator(
1068 'zen-native-select > zen-icon[data-zen-select-icon]',
1069 ).count(),
1070 1,
1071 );
1072 await page.goto(`${baseUrl}/components/slider`, {
1073 waitUntil: 'networkidle',
1074 });
1075 assert.equal(
1076 await page.locator('zen-slider input').evaluate(
1077 input => getComputedStyle(input).appearance,
1078 ),
1079 'none',
1080 );
1081
1082 await page.goto(`${baseUrl}/components/toggle-group`, {
1083 waitUntil: 'networkidle',
1084 });
1085 await page.locator('zen-toggle-group button[value="right"]').click();
1086 assert.equal(
1087 await page.locator('zen-toggle-group button[value="right"]')
1088 .getAttribute('aria-pressed'),
1089 'true',
1090 );
1091 assert.equal(
1092 await page.locator('zen-toggle-group').getAttribute('value'),
1093 'right',
1094 );
1095
1096 await page.goto(`${baseUrl}/components/sidebar`, {
1097 waitUntil: 'networkidle',
1098 });
1099 const sidebarTrigger = page.locator(
1100 'zen-sidebar [data-zen-sidebar-trigger]',
1101 );
1102 const sidebarPanel = page.locator(
1103 'zen-sidebar [data-zen-sidebar-panel]',
1104 );
1105 assert.equal(await sidebarTrigger.getAttribute('aria-expanded'), 'true');
1106 assert.equal(await sidebarPanel.getAttribute('hidden'), null);
1107 await sidebarTrigger.click();
1108 assert.equal(await sidebarTrigger.getAttribute('aria-expanded'), 'false');
1109 assert.equal(await sidebarPanel.getAttribute('hidden'), '');
353 1110
354 await page.goto(`${baseUrl}/components/notifications`, { 1111 await page.goto(`${baseUrl}/components/notifications`, {
355 waitUntil: 'networkidle', 1112 waitUntil: 'networkidle',
356 }); 1113 });
357 const notificationScope = page.locator('zen-notifications').first(); 1114 const notificationScope = page.locator('zen-notifications').first();
844 .getPropertyValue('--zen-color-canvas') 1601 .getPropertyValue('--zen-color-canvas')
845 .trim() 1602 .trim()
846 ); 1603 );
847 assert.notEqual(systemDarkCanvas, explicitLightCanvas); 1604 assert.notEqual(systemDarkCanvas, explicitLightCanvas);
848 await darkContext.close(); 1605 await darkContext.close();
1606
1607 const mobileContext = await browser.newContext({
1608 colorScheme: 'light',
1609 viewport: { width: 390, height: 844 },
1610 });
1611 const mobilePage = await mobileContext.newPage();
1612 const mobileErrors = [];
1613 mobilePage.on('pageerror', error => {
1614 mobileErrors.push(error.stack || error.message);
1615 });
1616 mobilePage.on('console', message => {
1617 if (message.type() === 'error') mobileErrors.push(message.text());
1618 });
1619 await mobilePage.goto(baseUrl, { waitUntil: 'networkidle' });
1620 await mobilePage.waitForFunction(() =>
1621 customElements.get('zen-date-picker') &&
1622 document.querySelectorAll(
1623 '.catalog-nav[aria-label="Components"] a',
1624 ).length === 66
1625 );
1626 const mobileFailures = await mobilePage.evaluate(() => {
1627 const routes = [
1628 '/',
1629 '/tokens',
1630 '/icons',
1631 ...[...document.querySelectorAll(
1632 '.catalog-nav[aria-label="Components"] a',
1633 )].map(link => new URL(link.href).pathname),
1634 ];
1635 const failures = [];
1636 for (const route of routes) {
1637 history.replaceState({}, '', route);
1638 window.dispatchEvent(new PopStateEvent('popstate'));
1639 const page = document.querySelector(
1640 '[data-catalog-page]:not([hidden])',
1641 );
1642 if (!page ||
1643 document.documentElement.scrollWidth > innerWidth + 1 ||
1644 page.getBoundingClientRect().right > innerWidth + 1) {
1645 failures.push(route);
1646 }
1647 }
1648 return failures;
1649 });
1650 assert.deepEqual(mobileFailures, []);
1651 await mobilePage.goto(`${baseUrl}/components/date-picker`, {
1652 waitUntil: 'networkidle',
1653 });
1654 await mobilePage.locator(
1655 'zen-date-picker [data-zen-date-trigger]',
1656 ).click();
1657 const mobileCalendarBounds = await mobilePage.locator(
1658 'zen-date-picker [data-zen-calendar-panel]',
1659 ).evaluate(panel => {
1660 const bounds = panel.getBoundingClientRect();
1661 return {
1662 bottom: bounds.bottom,
1663 left: bounds.left,
1664 right: bounds.right,
1665 top: bounds.top,
1666 };
1667 });
1668 assert.ok(mobileCalendarBounds.left >= 0);
1669 assert.ok(mobileCalendarBounds.right <= 390);
1670 assert.ok(mobileCalendarBounds.top >= 0);
1671 assert.ok(mobileCalendarBounds.bottom <= 844);
1672 assert.deepEqual(mobileErrors, []);
1673 await mobileContext.close();
849 } finally { 1674 } finally {
850 if (browser) await browser.close(); 1675 if (browser) await browser.close();
851 await stopProcess(server); 1676 await stopProcess(server);
852 } 1677 }
853 })().catch(error => { 1678 })().catch(error => {