Mercurial
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 => { |