Mercurial
view design_system/test/storybook_test.js @ 251:117c4d53c9a4
[ui] Add HTML-first Web Component system
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 04 Aug 2026 09:14:57 -0700 |
| parents | |
| children | 7a7581f040e8 |
line wrap: on
line source
const assert = require('node:assert/strict'); const http = require('node:http'); const net = require('node:net'); const path = require('node:path'); const { spawn } = require('node:child_process'); const RUNFILES = process.env.JS_BINARY__RUNFILES; const WORKSPACE = process.env.JS_BINARY__WORKSPACE; const runfilesWorkspace = path.join(RUNFILES, WORKSPACE); const playwrightPath = path.join( runfilesWorkspace, 'hg-web/e2e/node_modules/playwright-core', ); const { chromium } = require(playwrightPath); function findFreePort() { return new Promise((resolve, reject) => { const server = net.createServer(); server.once('error', reject); server.listen(0, '127.0.0.1', () => { const address = server.address(); server.close(error => { if (error) reject(error); else resolve(String(address.port)); }); }); }); } async function stopProcess(child) { if (!child || child.exitCode !== null) return; child.kill('SIGTERM'); await new Promise(resolve => { const timer = setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); }, 3000); child.once('exit', () => { clearTimeout(timer); resolve(); }); }); } async function waitForServer(server, baseUrl, logs) { const deadline = Date.now() + 15000; while (Date.now() < deadline) { if (server.exitCode !== null) { throw new Error(`Server exited with ${server.exitCode}\n${logs.join('')}`); } try { const response = await fetch(baseUrl); if (response.ok) return; } catch { // Keep waiting. } await new Promise(resolve => setTimeout(resolve, 100)); } throw new Error(`Server startup timed out\n${logs.join('')}`); } function requestRawPath(port, requestPath) { return new Promise((resolve, reject) => { const request = http.request({ host: '127.0.0.1', port, method: 'GET', path: requestPath, agent: false, }, response => { response.resume(); response.on('end', () => resolve(response.statusCode)); }); request.on('error', reject); request.end(); }); } (async () => { assert.ok(RUNFILES); assert.ok(WORKSPACE); const serverBinary = path.join( runfilesWorkspace, 'design_system/design_system_server', ); const chromiumPath = path.resolve(process.env.CHROMIUM_PATH); const port = await findFreePort(); const baseUrl = `http://127.0.0.1:${port}`; const logs = []; let server; let browser; try { const invalidServer = spawn(serverBinary, [], { cwd: runfilesWorkspace, env: { ...process.env, DESIGN_SYSTEM_PORT: 'not-a-port', }, stdio: ['ignore', 'pipe', 'pipe'], }); const invalidExit = await new Promise(resolve => { invalidServer.once('exit', (code, signal) => resolve({ code, signal })); }); assert.equal(invalidExit.code, 1); assert.equal(invalidExit.signal, null); server = spawn(serverBinary, [], { cwd: runfilesWorkspace, env: { ...process.env, DESIGN_SYSTEM_PORT: port, }, stdio: ['ignore', 'pipe', 'pipe'], }); server.stdout.on('data', chunk => logs.push(chunk.toString())); server.stderr.on('data', chunk => logs.push(chunk.toString())); await waitForServer(server, baseUrl, logs); assert.equal(await requestRawPath(port, '/../../MODULE.bazel'), 400); for (const route of [ '/', '/tokens', '/components', '/components/button', '/components/card', '/components/alert', '/components/field', '/components/stack', ]) { const response = await fetch(`${baseUrl}${route}`); assert.equal(response.status, 200, route); assert.match( response.headers.get('content-type') || '', /^text\/html/, ); } for (const asset of [ '/styles/tokens.css', '/styles/components.css', '/components/index.js', '/storybook.js', ]) { const response = await fetch(`${baseUrl}${asset}`); assert.equal(response.status, 200, asset); assert.ok((await response.text()).length > 100, asset); } browser = await chromium.launch({ executablePath: chromiumPath, headless: true, args: ['--no-sandbox'], }); const context = await browser.newContext({ colorScheme: 'light' }); const page = await context.newPage(); const errors = []; page.on('pageerror', error => errors.push(error.message)); page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); page.on('response', response => { if (response.status() >= 400) { errors.push(`${response.status()} ${response.url()}`); } }); await page.goto(`${baseUrl}/components/button`, { waitUntil: 'networkidle', }); await page.waitForFunction(() => customElements.get('zen-button') && customElements.get('zen-story') ); assert.equal( await page.locator('[data-catalog-page="button"]').isVisible(), true, ); assert.equal( await page.locator('.catalog-sidebar a[href="/components/button"]').getAttribute('aria-current'), 'page', ); assert.match( await page.locator('zen-story pre').first().textContent(), /<zen-button>/, ); assert.equal( await page.locator('zen-button[loading]').getAttribute('inert'), null, ); assert.equal( await page.locator('zen-button[loading] button').getAttribute('aria-disabled'), 'true', ); assert.equal( await page.locator('zen-button[loading] button').getAttribute('aria-busy'), 'true', ); await page.evaluate(() => { window.__loadingClicks = 0; document.querySelector('zen-button[loading] button') .addEventListener('click', () => window.__loadingClicks++); }); await page.locator('zen-button[loading] button').evaluate( button => button.click(), ); assert.equal(await page.evaluate(() => window.__loadingClicks), 0); await page.locator('zen-button[loading] button').focus(); assert.equal( await page.locator('zen-button[loading] button').evaluate( button => document.activeElement === button, ), true, ); await page.keyboard.press('Tab'); assert.equal( await page.locator('zen-button[loading] button').evaluate( button => document.activeElement === button, ), false, ); const nativeDisabledPreserved = await page.evaluate(async () => { const wrapper = document.createElement('zen-button'); const button = document.createElement('button'); button.disabled = true; button.textContent = 'Native disabled'; wrapper.append(button); document.body.append(wrapper); await customElements.whenDefined('zen-button'); wrapper.setAttribute('disabled', ''); wrapper.removeAttribute('disabled'); return button.disabled; }); assert.equal(nativeDisabledPreserved, true); const replacementButtonState = await page.evaluate(async () => { const wrapper = document.createElement('zen-button'); wrapper.setAttribute('disabled', ''); const first = document.createElement('button'); first.textContent = 'First'; wrapper.append(first); document.body.append(wrapper); await new Promise(resolve => setTimeout(resolve)); const second = document.createElement('button'); second.textContent = 'Second'; wrapper.replaceChildren(second); await new Promise(resolve => setTimeout(resolve)); return { firstDisabled: first.disabled, secondAriaDisabled: second.getAttribute('aria-disabled'), wrapperInert: wrapper.hasAttribute('inert'), }; }); assert.equal(replacementButtonState.firstDisabled, false); assert.equal(replacementButtonState.secondAriaDisabled, 'true'); assert.equal(replacementButtonState.wrapperInert, false); const modifiedClickAllowed = await page.evaluate(() => { const link = document.querySelector( '.catalog-sidebar a[href="/components/card"]', ); return link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, button: 0, ctrlKey: true, })); }); assert.equal(modifiedClickAllowed, true); await page.goto(`${baseUrl}/components/field`, { waitUntil: 'networkidle', }); const fieldWiring = await page.locator('zen-field').first().evaluate(field => { const label = field.querySelector('label'); const input = field.querySelector('input'); const help = field.querySelector('small'); input.checkValidity(); return { describedBy: input.getAttribute('aria-describedby'), helpId: help.id, inputId: input.id, invalid: field.hasAttribute('data-invalid'), labelFor: label.htmlFor, }; }); assert.ok(fieldWiring.inputId); assert.equal(fieldWiring.labelFor, fieldWiring.inputId); assert.equal(fieldWiring.describedBy, fieldWiring.helpId); assert.equal(fieldWiring.invalid, true); const replacementField = await page.locator('zen-field').first().evaluate( async field => { const label = field.querySelector('label'); const oldInput = field.querySelector('input'); const blocker = document.createElement('div'); blocker.id = 'zen-field-3'; document.body.append(blocker); const nextInput = document.createElement('input'); nextInput.required = true; oldInput.replaceWith(nextInput); await new Promise(resolve => setTimeout(resolve)); oldInput.dispatchEvent(new Event('invalid')); const help = field.querySelector('small'); nextInput.id = 'replacement-email'; help.id = 'replacement-help'; await new Promise(resolve => setTimeout(resolve)); return { describedBy: nextInput.getAttribute('aria-describedby'), helpId: help.id, labelFor: label.htmlFor, nextId: nextInput.id, blockerId: blocker.id, }; }, ); assert.ok(replacementField.nextId); assert.notEqual(replacementField.nextId, replacementField.blockerId); assert.equal(replacementField.labelFor, replacementField.nextId); assert.equal(replacementField.describedBy, replacementField.helpId); await page.goto(`${baseUrl}/components/alert`, { waitUntil: 'networkidle', }); await page.evaluate(() => { window.__dismissed = 0; document.addEventListener('zen-dismiss', () => { window.__dismissed++; }); }); const dismissible = page.locator('zen-alert[dismissible]'); assert.equal(await dismissible.getAttribute('role'), 'alert'); await dismissible.locator('[data-zen-dismiss]').click(); assert.equal(await dismissible.count(), 0); assert.equal(await page.evaluate(() => window.__dismissed), 1); const authoredDismissState = await page.evaluate(async () => { const alert = document.createElement('zen-alert'); alert.setAttribute('dismissible', ''); const message = document.createElement('p'); message.textContent = 'Authored dismiss control'; const dismiss = document.createElement('button'); dismiss.dataset.zenDismiss = ''; alert.append(message, dismiss); document.body.append(alert); await new Promise(resolve => setTimeout(resolve)); let events = 0; alert.addEventListener('zen-dismiss', () => events++); alert.removeAttribute('dismissible'); dismiss.click(); return { connected: alert.isConnected, events, }; }); assert.equal(authoredDismissState.connected, true); assert.equal(authoredDismissState.events, 0); const lightCanvas = await page.evaluate(() => getComputedStyle(document.documentElement) .getPropertyValue('--zen-color-canvas') .trim() ); await page.locator('#themeToggle').click(); const darkCanvas = await page.evaluate(() => getComputedStyle(document.documentElement) .getPropertyValue('--zen-color-canvas') .trim() ); assert.notEqual(lightCanvas, darkCanvas); assert.equal( await page.evaluate(() => localStorage.getItem('zen-theme')), 'dark', ); assert.deepEqual(errors, []); await context.close(); const darkContext = await browser.newContext({ colorScheme: 'dark' }); const darkPage = await darkContext.newPage(); await darkPage.goto(baseUrl, { waitUntil: 'networkidle' }); assert.equal( await darkPage.locator('#themeToggle').getAttribute('aria-pressed'), 'true', ); const systemDarkCanvas = await darkPage.evaluate(() => getComputedStyle(document.documentElement) .getPropertyValue('--zen-color-canvas') .trim() ); await darkPage.locator('#themeToggle').click(); const explicitLightCanvas = await darkPage.evaluate(() => getComputedStyle(document.documentElement) .getPropertyValue('--zen-color-canvas') .trim() ); assert.notEqual(systemDarkCanvas, explicitLightCanvas); await darkContext.close(); } finally { if (browser) await browser.close(); await stopProcess(server); } })().catch(error => { console.error(error.stack || error); process.exitCode = 1; });