Mercurial
diff 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 |
line wrap: on
line diff
--- a/design_system/test/storybook_test.js Tue Aug 04 12:21:19 2026 -0700 +++ b/design_system/test/storybook_test.js Tue Aug 04 15:12:09 2026 -0700 @@ -120,6 +120,7 @@ for (const route of [ '/', '/tokens', + '/icons', '/components', '/components/button', '/components/card', @@ -138,7 +139,10 @@ for (const asset of [ '/styles/tokens.css', '/styles/components.css', + '/styles/shadcn.css', '/components/index.js', + '/components/icon.js', + '/catalog.js', '/storybook.js', ]) { const response = await fetch(`${baseUrl}${asset}`); @@ -154,9 +158,23 @@ const context = await browser.newContext({ colorScheme: 'light' }); const page = await context.newPage(); const errors = []; - page.on('pageerror', error => errors.push(error.message)); + const devtools = await context.newCDPSession(page); + await devtools.send('Runtime.enable'); + devtools.on('Runtime.exceptionThrown', event => { + const exception = event.exceptionDetails; + errors.push( + `${exception.text}: ${exception.exception?.description || ''} ` + + `${exception.url || ''}:${exception.lineNumber + 1}`, + ); + }); + page.on('pageerror', error => errors.push(error.stack || error.message)); page.on('console', message => { - if (message.type() === 'error') errors.push(message.text()); + if (message.type() === 'error') { + const location = message.location(); + errors.push( + `${message.text()} ${location.url || ''}:${location.lineNumber + 1}`, + ); + } }); page.on('response', response => { if (response.status() >= 400) { @@ -167,10 +185,121 @@ await page.goto(`${baseUrl}/components/button`, { waitUntil: 'networkidle', }); - await page.waitForFunction(() => - customElements.get('zen-button') && - customElements.get('zen-story') + try { + await page.waitForFunction( + () => customElements.get('zen-button') && + customElements.get('zen-story'), + undefined, + { timeout: 5000 }, + ); + } catch (error) { + throw new Error(`${error.message}\n${errors.join('\n')}`); + } + const catalogState = await page.evaluate(async () => { + const links = [ + ...document.querySelectorAll( + '.catalog-nav[aria-label="Components"] a', + ), + ]; + const elements = [...new Set( + links.map(link => link.dataset.zenElement).filter(Boolean), + )]; + const statuses = await Promise.all( + links.map(async link => (await fetch(link.href)).status), + ); + return { + cards: document.querySelectorAll('.component-grid > a').length, + elements, + links: links.length, + missing: elements.filter(name => !customElements.get(name)), + routes: links.map(link => new URL(link.href).pathname), + statuses, + }; + }); + assert.equal(catalogState.cards, 66); + assert.equal(catalogState.links, 66); + assert.equal(catalogState.elements.length, 65); + assert.deepEqual(catalogState.missing, []); + assert.deepEqual( + [...new Set(catalogState.statuses)], + [200], ); + const brokenCatalogPages = await page.evaluate(routes => { + const failures = []; + for (const route of ['/', '/tokens', '/icons', '/components', ...routes]) { + history.replaceState({}, '', route); + window.dispatchEvent(new PopStateEvent('popstate')); + const visible = [ + ...document.querySelectorAll('[data-catalog-page]:not([hidden])'), + ]; + const expected = route === '/' || route === '/components' + ? 'overview' + : route.slice(route.lastIndexOf('/') + 1); + if (visible.length !== 1 || + visible[0].dataset.catalogPage !== expected || + !visible[0].querySelector('h1')?.textContent.trim()) { + failures.push(route); + continue; + } + if (route.startsWith('/components/')) { + const story = visible[0].querySelector('zen-story'); + const canvas = story?.querySelector('.story-canvas'); + const source = story?.querySelector('pre code'); + if (!story || !canvas?.children.length || + !source?.textContent.trim()) { + failures.push(`${route}:empty-story`); + } + } + } + history.replaceState({}, '', '/components/button'); + window.dispatchEvent(new PopStateEvent('popstate')); + return failures; + }, catalogState.routes); + assert.deepEqual(brokenCatalogPages, []); + await page.keyboard.press('/'); + assert.equal( + await page.locator('#componentSearch').evaluate( + input => document.activeElement === input, + ), + true, + ); + await page.locator('#componentSearch').fill('date'); + assert.equal( + await page.locator( + '.catalog-nav[aria-label="Components"] a:not([hidden])', + ).count(), + 2, + ); + assert.equal( + await page.locator('.component-grid > a:not([hidden])').count(), + 2, + ); + assert.match( + await page.locator('#componentSearchStatus').textContent(), + /^2 components found$/, + ); + await page.locator('#componentSearch').press('Escape'); + assert.equal(await page.locator('#componentSearch').inputValue(), ''); + assert.equal( + await page.locator( + '.catalog-nav[aria-label="Components"] a:not([hidden])', + ).count(), + 66, + ); + await page.locator('#componentSearch').fill('stack'); + assert.equal( + await page.locator( + '.catalog-nav[aria-label="Components"] a[href="/components/stack"]', + ).getAttribute('hidden'), + null, + ); + assert.equal( + await page.locator( + '.component-grid > a[href="/components/stack"]', + ).getAttribute('hidden'), + null, + ); + await page.locator('#componentSearch').press('Escape'); assert.equal( await page.locator('[data-catalog-page="button"]').isVisible(), true, @@ -265,6 +394,114 @@ }); assert.equal(modifiedClickAllowed, true); + await page.goto(`${baseUrl}/icons`, { waitUntil: 'networkidle' }); + const iconCount = await page.locator('#iconGrid figure').count(); + assert.ok(iconCount >= 30); + assert.equal( + await page.locator('#iconGrid figure zen-icon > svg').count(), + iconCount, + ); + const iconFallback = await page.evaluate(() => { + const icon = document.createElement('zen-icon'); + icon.setAttribute('name', 'not-a-real-icon'); + icon.setAttribute('label', 'Missing icon fallback'); + document.body.append(icon); + return { + fallback: icon.dataset.zenIcon, + hidden: icon.getAttribute('aria-hidden'), + label: icon.getAttribute('aria-label'), + role: icon.getAttribute('role'), + }; + }); + assert.deepEqual(iconFallback, { + fallback: 'alert', + hidden: null, + label: 'Missing icon fallback', + role: 'img', + }); + + await page.goto(`${baseUrl}/tokens`, { waitUntil: 'networkidle' }); + const colorTokenState = await page.evaluate(() => { + const root = document.documentElement; + const value = name => getComputedStyle(root) + .getPropertyValue(`--zen-color-${name}`) + .trim(); + const families = [ + 'neutral', + 'red', + 'orange', + 'amber', + 'green', + 'teal', + 'blue', + 'violet', + 'rose', + 'brown', + ]; + const steps = [ + '50', + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900', + '950', + ]; + const rampsComplete = families.every(family => + steps.every(step => value(`${family}-${step}`)) + ); + const data = Array.from( + { length: 10 }, + (_, index) => value(`data-${index + 1}`), + ); + const materials = [ + 'paper', + 'washi', + 'linen', + 'sumi', + 'brick', + 'persimmon', + 'ochre', + 'moss', + 'patina', + 'indigo', + 'plum', + 'clay', + 'wood', + ].map(value); + const lightMuted = value('surface-muted'); + root.dataset.zenTheme = 'dark'; + const darkMuted = value('surface-muted'); + const darkData = Array.from( + { length: 10 }, + (_, index) => value(`data-${index + 1}`), + ); + delete root.dataset.zenTheme; + return { + darkData, + darkMuted, + data, + lightMuted, + materials, + rampsComplete, + }; + }); + assert.equal(colorTokenState.rampsComplete, true); + assert.equal(new Set(colorTokenState.materials).size, 13); + assert.equal(new Set(colorTokenState.data).size, 10); + assert.equal(new Set(colorTokenState.darkData).size, 10); + assert.notDeepEqual(colorTokenState.data, colorTokenState.darkData); + assert.notEqual( + colorTokenState.lightMuted, + colorTokenState.darkMuted, + ); + assert.equal(await page.locator('.palette-grid > div').count(), 13); + assert.equal(await page.locator('.data-palette > span').count(), 10); + await page.goto(`${baseUrl}/components/field`, { waitUntil: 'networkidle', }); @@ -285,6 +522,13 @@ assert.equal(fieldWiring.labelFor, fieldWiring.inputId); assert.equal(fieldWiring.describedBy, fieldWiring.helpId); assert.equal(fieldWiring.invalid, true); + await page.locator('zen-field input').first().focus(); + assert.equal( + await page.locator('zen-field input').first().evaluate( + input => getComputedStyle(input).outlineStyle, + ), + 'none', + ); const replacementField = await page.locator('zen-field').first().evaluate( async field => { const label = field.querySelector('label'); @@ -351,6 +595,519 @@ assert.equal(authoredDismissState.connected, true); assert.equal(authoredDismissState.events, 0); + await page.goto(`${baseUrl}/components/accordion`, { + waitUntil: 'networkidle', + }); + const accordion = page.locator('zen-accordion').first(); + const disclosureIcon = accordion.locator( + 'summary > zen-icon[data-zen-disclosure-icon]', + ).nth(1); + const collapsedDisclosure = await disclosureIcon.evaluate(icon => { + const style = getComputedStyle(icon); + return { + duration: style.transitionDuration, + transform: style.transform, + }; + }); + assert.notEqual(collapsedDisclosure.duration, '0s'); + await accordion.locator('summary').nth(1).click(); + await page.waitForTimeout(250); + assert.notEqual( + await disclosureIcon.evaluate(icon => getComputedStyle(icon).transform), + collapsedDisclosure.transform, + ); + assert.equal( + await accordion.locator('details').first().getAttribute('open'), + null, + ); + assert.equal( + await accordion.locator('details').nth(1).getAttribute('open'), + '', + ); + await accordion.locator('summary').nth(1).press('ArrowUp'); + assert.equal( + await accordion.locator('summary').first().evaluate( + summary => document.activeElement === summary, + ), + true, + ); + + await page.goto(`${baseUrl}/components/aspect-ratio`, { + waitUntil: 'networkidle', + }); + const aspectColors = await page.locator('.demo-media').evaluate(media => { + const style = getComputedStyle(media); + const probe = document.createElement('span'); + probe.style.background = 'var(--zen-color-surface-muted)'; + document.body.append(probe); + const token = getComputedStyle(probe).backgroundColor; + probe.remove(); + return { + background: style.backgroundColor, + image: style.backgroundImage, + token, + }; + }); + assert.equal(aspectColors.image, 'none'); + assert.equal(aspectColors.background, aspectColors.token); + + await page.goto(`${baseUrl}/components/tabs`, { + waitUntil: 'networkidle', + }); + await page.evaluate(() => { + window.__tabValue = null; + document.querySelector('zen-tabs').addEventListener( + 'zen-change', + event => { + window.__tabValue = event.detail.value; + }, + ); + }); + await page.locator('zen-tabs [role="tab"]').nth(1).click(); + assert.equal( + await page.locator('zen-tabs [role="tab"]').nth(1) + .getAttribute('aria-selected'), + 'true', + ); + assert.equal( + await page.locator('zen-tabs [role="tabpanel"]').nth(1) + .getAttribute('hidden'), + null, + ); + assert.equal(await page.evaluate(() => window.__tabValue), 'history'); + + await page.goto(`${baseUrl}/components/dialog`, { + waitUntil: 'networkidle', + }); + await page.locator('zen-dialog [data-zen-trigger]').click(); + assert.equal(await page.locator('zen-dialog dialog').getAttribute('open'), ''); + await page.locator('zen-dialog [data-zen-close]').click(); + assert.equal(await page.locator('zen-dialog dialog').getAttribute('open'), null); + const dynamicOverlays = await page.evaluate(async () => { + const dialogHost = document.createElement('zen-dialog'); + const popoverHost = document.createElement('zen-popover'); + const tooltipHost = document.createElement('zen-tooltip'); + const menuHost = document.createElement('zen-dropdown-menu'); + document.body.append(dialogHost, popoverHost, tooltipHost, menuHost); + + const dialogTrigger = document.createElement('button'); + dialogTrigger.type = 'button'; + dialogTrigger.dataset.zenTrigger = ''; + const dialog = document.createElement('dialog'); + dialogHost.append(dialogTrigger, dialog); + + const popoverTrigger = document.createElement('button'); + popoverTrigger.type = 'button'; + popoverTrigger.dataset.zenTrigger = ''; + const popover = document.createElement('div'); + popover.dataset.zenContent = ''; + popoverHost.append(popoverTrigger, popover); + + const tooltipTrigger = document.createElement('button'); + tooltipTrigger.type = 'button'; + tooltipTrigger.dataset.zenTrigger = ''; + const tooltip = document.createElement('span'); + tooltip.dataset.zenContent = ''; + tooltipHost.append(tooltipTrigger, tooltip); + + const menuTrigger = document.createElement('button'); + menuTrigger.type = 'button'; + menuTrigger.dataset.zenTrigger = ''; + const menu = document.createElement('div'); + menu.setAttribute('role', 'menu'); + const item = document.createElement('button'); + item.type = 'button'; + item.setAttribute('role', 'menuitem'); + menu.append(item); + menuHost.append(menuTrigger, menu); + + await new Promise(resolve => setTimeout(resolve)); + dialogTrigger.click(); + const dialogOpen = dialog.open; + dialog.close(); + popoverTrigger.click(); + menuTrigger.click(); + tooltipTrigger.focus(); + await new Promise(resolve => setTimeout(resolve, 375)); + return { + dialogControlled: Boolean(dialogTrigger.getAttribute('aria-controls')), + dialogOpen, + menuOpen: !menu.hidden, + popoverExpanded: popoverTrigger.getAttribute('aria-expanded'), + tooltipOpen: !tooltip.hidden, + }; + }); + assert.deepEqual(dynamicOverlays, { + dialogControlled: true, + dialogOpen: true, + menuOpen: true, + popoverExpanded: 'true', + tooltipOpen: true, + }); + + await page.goto(`${baseUrl}/components/dropdown-menu`, { + waitUntil: 'networkidle', + }); + const dropdownTrigger = page.locator( + 'zen-dropdown-menu [data-zen-trigger]', + ); + await dropdownTrigger.click(); + assert.equal( + await page.locator('zen-dropdown-menu [role="menu"]') + .getAttribute('hidden'), + null, + ); + await page.keyboard.press('Escape'); + assert.equal( + await page.locator('zen-dropdown-menu [role="menu"]') + .getAttribute('hidden'), + '', + ); + assert.equal( + await dropdownTrigger.evaluate( + trigger => document.activeElement === trigger, + ), + true, + ); + await dropdownTrigger.click(); + await page.locator('zen-dropdown-menu [role="menuitem"]').first().click(); + assert.equal( + await dropdownTrigger.evaluate( + trigger => document.activeElement === trigger, + ), + true, + ); + + await page.goto(`${baseUrl}/components/combobox`, { + waitUntil: 'networkidle', + }); + const comboboxInput = page.locator('zen-combobox input'); + await comboboxInput.fill('seo'); + assert.equal( + await page.locator('zen-combobox [role="option"]:not([hidden])').count(), + 1, + ); + await comboboxInput.press('ArrowDown'); + await comboboxInput.press('Enter'); + assert.equal(await comboboxInput.inputValue(), 'seobeo'); + assert.equal( + await page.locator('zen-combobox [role="listbox"]') + .getAttribute('hidden'), + '', + ); + + await page.goto(`${baseUrl}/components/date-picker`, { + waitUntil: 'networkidle', + }); + const dateInput = page.locator('zen-date-picker input[type="date"]'); + assert.equal(await dateInput.getAttribute('aria-hidden'), 'true'); + assert.equal( + await dateInput.evaluate(input => getComputedStyle(input).position), + 'absolute', + ); + await page.evaluate(() => { + window.__dateValue = null; + document.querySelector('zen-date-picker').addEventListener( + 'zen-change', + event => { + window.__dateValue = event.detail.value; + }, + ); + }); + const dateTrigger = page.locator( + 'zen-date-picker [data-zen-date-trigger]', + ); + await dateTrigger.click(); + assert.equal( + await page.locator('zen-date-picker [data-zen-calendar-panel]') + .getAttribute('hidden'), + null, + ); + assert.equal( + await page.locator( + 'zen-date-picker [data-zen-calendar-day]', + ).count(), + 42, + ); + const chosenDate = await page.locator( + 'zen-date-picker [data-zen-calendar-day]:not([data-outside]):not(:disabled)', + ).nth(14).getAttribute('data-zen-calendar-day'); + await page.locator( + `zen-date-picker [data-zen-calendar-day="${chosenDate}"]`, + ).click(); + assert.equal(await dateInput.inputValue(), chosenDate); + assert.equal(await page.evaluate(() => window.__dateValue), chosenDate); + assert.equal(await dateTrigger.getAttribute('aria-expanded'), 'false'); + const triggerName = await dateTrigger.evaluate(trigger => + (trigger.getAttribute('aria-labelledby') || '') + .split(/\s+/) + .map(id => document.getElementById(id)?.textContent || '') + .join(' ') + ); + assert.match(triggerName, /Deploy on/); + assert.equal( + await page.locator('zen-date-picker > label').getAttribute('for'), + await dateTrigger.getAttribute('id'), + ); + const minimumDate = `${chosenDate.slice(0, 8)}10`; + await dateInput.evaluate((input, minimum) => { + input.min = minimum; + }, minimumDate); + await page.waitForTimeout(0); + await dateTrigger.click(); + const focusedBoundary = page.locator( + `zen-date-picker [data-zen-calendar-day="${chosenDate}"]`, + ); + await focusedBoundary.press('PageUp'); + assert.equal( + await page.evaluate(() => + document.activeElement?.dataset.zenCalendarDay + ), + minimumDate, + ); + assert.equal( + await page.locator( + 'zen-date-picker [data-zen-calendar-day][tabindex="0"]:not(:disabled)', + ).count(), + 1, + ); + await page.keyboard.press('Escape'); + await dateInput.evaluate(input => { + input.disabled = true; + }); + await page.waitForTimeout(0); + assert.equal(await dateTrigger.isDisabled(), true); + await dateInput.evaluate(input => { + input.disabled = false; + input.readOnly = true; + }); + await page.waitForTimeout(0); + assert.equal(await dateTrigger.isDisabled(), true); + await dateInput.evaluate(input => { + input.readOnly = false; + input.required = true; + input.value = ''; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.reportValidity(); + }); + await page.waitForTimeout(0); + assert.equal(await dateTrigger.getAttribute('aria-invalid'), 'true'); + assert.equal( + await dateTrigger.evaluate( + trigger => document.activeElement === trigger, + ), + true, + ); + assert.equal( + await page.locator('zen-date-picker').getAttribute('data-invalid'), + '', + ); + await dateInput.evaluate((input, value) => { + input.value = value; + input.dispatchEvent(new Event('input', { bubbles: true })); + }, chosenDate); + assert.equal(await dateTrigger.getAttribute('aria-invalid'), 'false'); + const earlyYear = await page.evaluate(async () => { + const calendar = document.createElement('zen-calendar'); + const input = document.createElement('input'); + input.type = 'date'; + input.value = '0001-01-01'; + input.setAttribute('aria-label', 'Early date'); + calendar.append(input); + document.body.append(calendar); + await new Promise(resolve => setTimeout(resolve)); + return { + selected: calendar.querySelector( + '[data-zen-calendar-day][aria-selected="true"]', + )?.dataset.zenCalendarDay, + value: input.value, + }; + }); + assert.deepEqual(earlyYear, { + selected: '0001-01-01', + value: '0001-01-01', + }); + const resetDateState = await page.evaluate(async () => { + const label = document.createElement('span'); + label.id = 'reset-date-label'; + label.textContent = 'Reset date'; + const form = document.createElement('form'); + const picker = document.createElement('zen-date-picker'); + const input = document.createElement('input'); + input.type = 'date'; + input.defaultValue = '2026-08-04'; + input.setAttribute('aria-labelledby', label.id); + picker.append(input); + form.append(picker); + document.body.append(label, form); + await new Promise(resolve => setTimeout(resolve)); + input.value = '2026-08-12'; + input.dispatchEvent(new Event('input', { bubbles: true })); + form.reset(); + await new Promise(resolve => setTimeout(resolve)); + const trigger = picker.querySelector('[data-zen-date-trigger]'); + return { + labelledBy: trigger.getAttribute('aria-labelledby'), + selected: picker.querySelector( + '[data-zen-calendar-day][aria-selected="true"]', + )?.dataset.zenCalendarDay, + value: input.value, + }; + }); + assert.match(resetDateState.labelledBy, /^reset-date-label /); + assert.deepEqual( + { + selected: resetDateState.selected, + value: resetDateState.value, + }, + { + selected: '2026-08-04', + value: '2026-08-04', + }, + ); + + await page.goto(`${baseUrl}/components/input-otp`, { + waitUntil: 'networkidle', + }); + await page.evaluate(() => { + window.__otpValue = null; + document.querySelector('zen-input-otp').addEventListener( + 'zen-complete', + event => { + window.__otpValue = event.detail.value; + }, + ); + }); + await page.locator('zen-input-otp input').evaluate(input => { + input.value = '12a3456'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.locator('zen-input-otp input').inputValue(), '123456'); + assert.equal( + await page.locator('zen-input-otp [data-zen-otp-slots] > span').count(), + 6, + ); + assert.equal(await page.evaluate(() => window.__otpValue), '123456'); + + await page.goto(`${baseUrl}/components/data-table`, { + waitUntil: 'networkidle', + }); + await page.locator('zen-data-table [data-zen-sort="commits"]').click(); + assert.equal( + await page.locator('zen-data-table th').nth(1).getAttribute('aria-sort'), + 'ascending', + ); + assert.equal( + await page.locator('zen-data-table tbody tr').first().locator( + '[data-key="commits"]', + ).textContent(), + '12', + ); + + await page.goto(`${baseUrl}/components/carousel`, { + waitUntil: 'networkidle', + }); + await page.evaluate(() => { + window.__carouselIndex = null; + document.querySelector('zen-carousel').addEventListener( + 'zen-change', + event => { + window.__carouselIndex = event.detail.index; + }, + ); + }); + await page.locator('zen-carousel [data-zen-next]').click(); + assert.equal(await page.evaluate(() => window.__carouselIndex), 1); + assert.equal( + await page.locator('zen-carousel [data-zen-prev]').isDisabled(), + false, + ); + + await page.goto(`${baseUrl}/components/resizable`, { + waitUntil: 'networkidle', + }); + const resizeHandle = page.locator('zen-resizable [data-zen-handle]'); + const initialPercent = Number( + await resizeHandle.getAttribute('aria-valuenow'), + ); + await resizeHandle.focus(); + await resizeHandle.press('ArrowRight'); + assert.ok( + Number(await resizeHandle.getAttribute('aria-valuenow')) > initialPercent, + ); + + await page.goto(`${baseUrl}/components/switch`, { + waitUntil: 'networkidle', + }); + const switchInput = page.locator('zen-switch input'); + assert.equal(await switchInput.getAttribute('role'), 'switch'); + assert.equal(await switchInput.getAttribute('aria-checked'), 'true'); + await switchInput.uncheck(); + assert.equal(await switchInput.getAttribute('aria-checked'), 'false'); + + await page.goto(`${baseUrl}/components/checkbox`, { + waitUntil: 'networkidle', + }); + assert.equal( + await page.locator('zen-checkbox input').evaluate( + input => getComputedStyle(input).appearance, + ), + 'none', + ); + await page.goto(`${baseUrl}/components/native-select`, { + waitUntil: 'networkidle', + }); + assert.equal( + await page.locator('zen-native-select select').evaluate( + select => getComputedStyle(select).appearance, + ), + 'none', + ); + assert.equal( + await page.locator( + 'zen-native-select > zen-icon[data-zen-select-icon]', + ).count(), + 1, + ); + await page.goto(`${baseUrl}/components/slider`, { + waitUntil: 'networkidle', + }); + assert.equal( + await page.locator('zen-slider input').evaluate( + input => getComputedStyle(input).appearance, + ), + 'none', + ); + + await page.goto(`${baseUrl}/components/toggle-group`, { + waitUntil: 'networkidle', + }); + await page.locator('zen-toggle-group button[value="right"]').click(); + assert.equal( + await page.locator('zen-toggle-group button[value="right"]') + .getAttribute('aria-pressed'), + 'true', + ); + assert.equal( + await page.locator('zen-toggle-group').getAttribute('value'), + 'right', + ); + + await page.goto(`${baseUrl}/components/sidebar`, { + waitUntil: 'networkidle', + }); + const sidebarTrigger = page.locator( + 'zen-sidebar [data-zen-sidebar-trigger]', + ); + const sidebarPanel = page.locator( + 'zen-sidebar [data-zen-sidebar-panel]', + ); + assert.equal(await sidebarTrigger.getAttribute('aria-expanded'), 'true'); + assert.equal(await sidebarPanel.getAttribute('hidden'), null); + await sidebarTrigger.click(); + assert.equal(await sidebarTrigger.getAttribute('aria-expanded'), 'false'); + assert.equal(await sidebarPanel.getAttribute('hidden'), ''); + await page.goto(`${baseUrl}/components/notifications`, { waitUntil: 'networkidle', }); @@ -846,6 +1603,74 @@ ); assert.notEqual(systemDarkCanvas, explicitLightCanvas); await darkContext.close(); + + const mobileContext = await browser.newContext({ + colorScheme: 'light', + viewport: { width: 390, height: 844 }, + }); + const mobilePage = await mobileContext.newPage(); + const mobileErrors = []; + mobilePage.on('pageerror', error => { + mobileErrors.push(error.stack || error.message); + }); + mobilePage.on('console', message => { + if (message.type() === 'error') mobileErrors.push(message.text()); + }); + await mobilePage.goto(baseUrl, { waitUntil: 'networkidle' }); + await mobilePage.waitForFunction(() => + customElements.get('zen-date-picker') && + document.querySelectorAll( + '.catalog-nav[aria-label="Components"] a', + ).length === 66 + ); + const mobileFailures = await mobilePage.evaluate(() => { + const routes = [ + '/', + '/tokens', + '/icons', + ...[...document.querySelectorAll( + '.catalog-nav[aria-label="Components"] a', + )].map(link => new URL(link.href).pathname), + ]; + const failures = []; + for (const route of routes) { + history.replaceState({}, '', route); + window.dispatchEvent(new PopStateEvent('popstate')); + const page = document.querySelector( + '[data-catalog-page]:not([hidden])', + ); + if (!page || + document.documentElement.scrollWidth > innerWidth + 1 || + page.getBoundingClientRect().right > innerWidth + 1) { + failures.push(route); + } + } + return failures; + }); + assert.deepEqual(mobileFailures, []); + await mobilePage.goto(`${baseUrl}/components/date-picker`, { + waitUntil: 'networkidle', + }); + await mobilePage.locator( + 'zen-date-picker [data-zen-date-trigger]', + ).click(); + const mobileCalendarBounds = await mobilePage.locator( + 'zen-date-picker [data-zen-calendar-panel]', + ).evaluate(panel => { + const bounds = panel.getBoundingClientRect(); + return { + bottom: bounds.bottom, + left: bounds.left, + right: bounds.right, + top: bounds.top, + }; + }); + assert.ok(mobileCalendarBounds.left >= 0); + assert.ok(mobileCalendarBounds.right <= 390); + assert.ok(mobileCalendarBounds.top >= 0); + assert.ok(mobileCalendarBounds.bottom <= 844); + assert.deepEqual(mobileErrors, []); + await mobileContext.close(); } finally { if (browser) await browser.close(); await stopProcess(server);