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;
});