view mrjunejune/test/theme_and_webp_test.js @ 266:efaf4c63cc94

fix clean production bundle staging Recreate deployment staging before copying the Bazel bundle and verify required inference runtime paths before promoting it. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 12:52:30 -0700
parents 056790c4fb0d
children 41a49c29a28f
line wrap: on
line source

const assert = require('node:assert/strict');
const fs = require('node:fs');
const net = require('node:net');
const os = require('node:os');
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;
let port;
let baseUrl;
const playwrightPath = path.join(
  RUNFILES,
  WORKSPACE,
  'hg-web/e2e/node_modules/playwright-core',
);
const { chromium } = require(playwrightPath);
const browserSuite = process.env.MJJ_BROWSER_SUITE || 'all';
const runsSuite = name => browserSuite === 'all' || browserSuite === name;

function stopProcess(child) {
  if (!child || child.exitCode !== null) return Promise.resolve();
  child.kill('SIGTERM');
  return new Promise(resolve => {
    const timer = setTimeout(() => {
      if (child.exitCode === null) child.kill('SIGKILL');
    }, 3000);
    child.once('exit', () => {
      clearTimeout(timer);
      resolve();
    });
  });
}

async function waitForServer(child, logs) {
  const deadline = Date.now() + 15000;
  while (Date.now() < deadline) {
    if (child.exitCode !== null) {
      throw new Error(`Server exited with ${child.exitCode}\n${logs.join('')}`);
    }
    try {
      const response = await fetch(baseUrl);
      if (response.ok) return;
    } catch {
      // Keep waiting for startup.
    }
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  throw new Error(`Server startup timed out\n${logs.join('')}`);
}

function listFiles(root) {
  const files = [];
  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
    const absolute = path.join(root, entry.name);
    if (entry.isDirectory()) files.push(...listFiles(absolute));
    else files.push(absolute);
  }
  return files;
}

function primitiveOwnershipViolations(source) {
  const violations = [];
  const stack = [];
  const voidElements = new Set([
    'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
    'meta', 'param', 'source', 'track', 'wbr',
  ]);
  const ownerMap = {
    button: new Set(['zen-button']),
    select: new Set(['zen-field', 'zen-native-select', 'zen-select']),
    textarea: new Set(['zen-field', 'zen-textarea']),
  };

  for (const token of source.matchAll(
    /<!--[\s\S]*?-->|<\/?([a-zA-Z][\w-]*)\b([^>]*)>/g,
  )) {
    if (!token[1]) continue;
    const name = token[1].toLowerCase();
    if (token[0].startsWith('</')) {
      while (stack.length) {
        if (stack.pop() === name) break;
      }
      continue;
    }

    let owners = ownerMap[name];
    if (name === 'input') {
      const type = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(token[2])?.[1]
        ?.toLowerCase() || 'text';
      if (type === 'checkbox') {
        owners = new Set(['zen-checkbox', 'zen-field', 'zen-switch']);
      } else if (type === 'radio') {
        owners = new Set(['zen-field', 'zen-radio-group']);
      } else if (type === 'range') {
        owners = new Set(['zen-field', 'zen-slider']);
      } else if (type === 'date') {
        owners = new Set(['zen-calendar', 'zen-date-picker', 'zen-field']);
      } else {
        owners = new Set([
          'zen-combobox',
          'zen-command',
          'zen-field',
          'zen-input',
          'zen-input-group',
          'zen-input-otp',
        ]);
      }
    }
    if (owners) {
      const matchingOwners = stack.filter(ancestor => owners.has(ancestor));
      if (matchingOwners.length !== 1) {
        violations.push({
          control: name,
          owners: matchingOwners,
          source: token[0],
        });
      }
    }

    if (!voidElements.has(name) && !token[0].endsWith('/>')) {
      stack.push(name);
    }
  }
  return violations;
}

async function sampleTheme(browser, theme) {
  const context = await browser.newContext({ colorScheme: 'dark' });
  await context.addInitScript(value => {
    localStorage.setItem('theme-preference', value);
  }, theme);
  const page = await context.newPage();
  const errors = [];
  await page.route('**/api/conversations**', async route => {
    const request = route.request();
    const url = new URL(request.url());
    if (request.method() === 'POST' &&
        url.pathname === '/api/conversations') {
      await route.fulfill({
        status: 201,
        contentType: 'application/json',
        body: JSON.stringify({
          id: '11111111-1111-4111-8111-111111111111',
        }),
      });
      return;
    }
    if (request.method() === 'POST' && url.pathname.endsWith('/turns')) {
      await route.fulfill({
        status: 200,
        contentType: 'text/event-stream',
        body: [
          'event: turn.accepted',
          'data: {"request_id":"request-1"}',
          '',
          'event: assistant.delta',
          'data: {"request_id":"request-1","delta":"Start with "}',
          '',
          'event: assistant.delta',
          'data: {"request_id":"request-1","delta":"the tools."}',
          '',
          'event: assistant.completed',
          'data: {"request_id":"request-1","content":"Start with the tools."}',
          '',
          'event: turn.done',
          'data: {"request_id":"request-1","failed":false,"aborted":false}',
          '',
          '',
        ].join('\n'),
      });
      return;
    }
    await route.fulfill({
      status: 404,
      contentType: 'application/json',
      body: JSON.stringify({ error: { message: 'Not found' } }),
    });
  });
  page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));
  page.on('console', message => {
    if (message.type() === 'error' &&
        !message.text().startsWith('Failed to load resource:')) {
      errors.push(`console: ${message.text()}`);
    }
  });
  page.on('requestfailed', request => {
    if (request.url().startsWith('blob:') &&
        request.failure()?.errorText === 'net::ERR_ABORTED') {
      return;
    }
    if (request.method() === 'DELETE' &&
        request.url().includes('/api/download/') &&
        request.failure()?.errorText === 'net::ERR_ABORTED') {
      return;
    }
    if (request.method() === 'POST' &&
        request.url().endsWith('/api/latex/render') &&
        request.failure()?.errorText === 'net::ERR_ABORTED') {
      return;
    }
    errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
  });
  page.on('response', response => {
    if (response.status() === 404 &&
        response.url().endsWith(
          '/api/conversations/00000000-0000-4000-8000-000000000000'
        )) {
      return;
    }
    if (response.status() >= 400) {
      errors.push(`response: ${response.status()} ${response.url()}`);
    }
  });

  await page.goto(baseUrl, { waitUntil: 'networkidle' });
  await page.waitForFunction(() => {
    const canvas = document.querySelector('#background');
    if (!canvas) return false;
    const data = canvas.getContext('2d').getImageData(
      0,
      0,
      canvas.width,
      canvas.height,
    ).data;
    for (let index = 3; index < data.length; index += 4) {
      if (data[index] >= 20) return true;
    }
    return false;
  });

  const sample = await page.evaluate(() => {
    const canvas = document.querySelector('#background');
    const data = canvas.getContext('2d').getImageData(
      0,
      0,
      canvas.width,
      canvas.height,
    ).data;
    let count = 0;
    let luminance = 0;
    for (let index = 0; index < data.length; index += 4) {
      if (data[index + 3] < 20) continue;
      count++;
      luminance +=
        data[index] * 0.2126 +
        data[index + 1] * 0.7152 +
        data[index + 2] * 0.0722;
    }
    const channels = value => value.match(/\d+(?:\.\d+)?/g)
      .slice(0, 3)
      .map(Number);
    const colorLuminance = value => {
      const converted = channels(value).map(channel => {
        const normalized = channel / 255;
        return normalized <= 0.04045
          ? normalized / 12.92
          : ((normalized + 0.055) / 1.055) ** 2.4;
      });
      return converted[0] * 0.2126 +
        converted[1] * 0.7152 +
        converted[2] * 0.0722;
    };
    const bodyStyle = getComputedStyle(document.body);
    const themeButtonStyle = getComputedStyle(
      document.querySelector('#themeToggle'),
    );
    const foreground = colorLuminance(bodyStyle.color);
    const contrastProbe = document.createElement('span');
    contrastProbe.style.background = 'var(--zen-color-surface)';
    document.body.append(contrastProbe);
    const background = colorLuminance(
      getComputedStyle(contrastProbe).backgroundColor,
    );
    contrastProbe.remove();
    return {
      backgroundRepeat: bodyStyle.backgroundRepeat,
      backgroundSize: bodyStyle.backgroundSize,
      bodyCoversViewport: document.body.getBoundingClientRect().height >=
        innerHeight,
      componentReady: Boolean(customElements.get('zen-button')),
      count,
      fontFamily: getComputedStyle(document.body).fontFamily,
      headerPaws: document.querySelectorAll(
        'header [data-zen-link-decoration]',
      ).length,
      luminance: count ? luminance / count : 0,
      textContrast: (Math.max(foreground, background) + 0.05) /
        (Math.min(foreground, background) + 0.05),
      rootTheme: document.documentElement.dataset.zenTheme || 'auto',
      enhancedLinks: document.querySelectorAll(
        'zen-link[effect="paw"] > a > zen-icon[name="paw"]',
      ).length,
      links: document.querySelectorAll('a').length,
      pawLinks: [...document.querySelectorAll('a')].filter(link =>
        !link.closest('zen-button') &&
        link.dataset.zenLinkEffect !== 'none'
      ).length,
      mainBackground: getComputedStyle(
        document.querySelector('main'),
      ).backgroundColor,
      themeButtonBackground: themeButtonStyle.backgroundColor,
      themeButtonBorder: themeButtonStyle.borderTopWidth,
      themeButtonShadow: themeButtonStyle.boxShadow,
      themeLabel: document.querySelector('#themeName')?.textContent,
    };
  });

  if (errors.length) throw new Error(`${theme}\n${errors.join('\n')}`);
  await context.close();
  return sample;
}

async function testThemeCycle(browser) {
  const context = await browser.newContext({ colorScheme: 'light' });
  const page = await context.newPage();
  await page.goto(baseUrl, { waitUntil: 'networkidle' });
  const expected = ['paper', 'ink', 'playful', 'auto'];
  for (const theme of expected) {
    await page.locator('#themeToggle').click();
    assert.equal(
      await page.evaluate(() =>
        document.documentElement.dataset.zenTheme || 'auto'
      ),
      theme,
    );
    assert.equal(
      await page.evaluate(() => localStorage.getItem('theme-preference')),
      theme,
    );
  }
  await context.close();
}

async function testPlainField(browser) {
  const page = await browser.newPage();
  await page.goto(`${baseUrl}/tools/markdown_to_html`, {
    waitUntil: 'networkidle',
  });
  await page.waitForFunction(() => customElements.get('zen-field'));
  const state = await page.locator('zen-field[appearance="plain"]').evaluate(
    field => {
      const label = field.querySelector('label');
      const textarea = field.querySelector('textarea');
      const probe = document.createElement('span');
      probe.style.border = '1px solid var(--accent)';
      document.body.append(probe);
      const accent = getComputedStyle(probe).borderColor;
      probe.remove();
      textarea.focus();
      return {
        borderColor: getComputedStyle(textarea).borderColor,
        display: getComputedStyle(field).display,
        labelFor: label.htmlFor,
        accent,
        textareaId: textarea.id,
      };
    },
  );
  assert.deepEqual(state, {
    accent: state.borderColor,
    borderColor: state.borderColor,
    display: 'contents',
    labelFor: 'input',
    textareaId: 'input',
  });
  await page.close();
}

async function testButtonScale(browser) {
  const page = await browser.newPage();
  for (const route of [
    '/talk',
    '/notes/login',
    '/tools/hls_player',
    '/tools/latex_editor',
    '/tools/markdown_to_html',
    '/tools/file_converter',
    '/offline.html',
  ]) {
    await page.goto(`${baseUrl}${route}`, { waitUntil: 'networkidle' });
    await page.waitForFunction(() => customElements.get('zen-button'));
    const controls = await page.locator(
      'zen-button[size] > :is(button, a):not(#themeToggle)',
    ).evaluateAll(elements => elements.map(control => {
      const host = control.parentElement;
      const style = getComputedStyle(control);
      const hostStyle = getComputedStyle(host);
      return {
        height: control.getBoundingClientRect().height,
        minimum: Number.parseFloat(
          hostStyle.getPropertyValue('--zenbu-control-height'),
        ) * Number.parseFloat(getComputedStyle(document.documentElement).fontSize),
        paddingBlock: style.paddingBlockStart,
        paddingVariable: Number.parseFloat(
          hostStyle.getPropertyValue('--zenbu-control-padding-block'),
        ) * Number.parseFloat(getComputedStyle(document.documentElement).fontSize),
        size: host.getAttribute('size'),
      };
    }));
    assert.ok(controls.length > 0, route);
    for (const control of controls) {
      assert.match(control.size, /^(?:xs|sm|md|lg|xl)$/);
      assert.ok(control.height >= control.minimum - 1, JSON.stringify(control));
      assert.ok(
        Math.abs(Number.parseFloat(control.paddingBlock) -
          control.paddingVariable) < 1,
        JSON.stringify(control),
      );
    }
  }
  await page.close();
}

async function testDynamicButtonOwnership(browser) {
  const page = await browser.newPage();
  await page.goto(baseUrl, { waitUntil: 'networkidle' });
  await page.evaluate(() => {
    window.dispatchEvent(new Event('beforeinstallprompt', {
      cancelable: true,
    }));
  });
  const state = await page.locator('#pwa-install-btn').evaluate(button => ({
    ownerCount: button.closest('zen-button') ? 1 : 0,
    appearance: button.closest('zen-button')?.getAttribute('appearance'),
    size: button.closest('zen-button')?.getAttribute('size'),
  }));
  assert.deepEqual(state, {
    appearance: 'plain',
    ownerCount: 1,
    size: 'md',
  });
  await page.close();
}

async function testResumePrint(browser) {
  const page = await browser.newPage();
  await page.emulateMedia({ media: 'print' });
  await page.goto(`${baseUrl}/resume`, { waitUntil: 'networkidle' });
  assert.equal(
    await page.locator('.info > a[href="/public/resume.pdf"]').isVisible(),
    false,
  );
  await page.close();
}

async function testJrpgPage(browser) {
  const context = await browser.newContext({
    colorScheme: 'light',
    viewport: { width: 1280, height: 900 },
  });
  await context.addInitScript(key => {
    sessionStorage.setItem(
      key,
      '00000000-0000-4000-8000-000000000000',
    );
  }, 'mjj-jrpg-conversation-id');
  const page = await context.newPage();
  const errors = [];
  page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));
  page.on('console', message => {
    if (message.type() === 'error' &&
        !message.text().startsWith('Failed to load resource:')) {
      errors.push(`console: ${message.text()}`);
    }
  });
  page.on('requestfailed', request => {
    if (request.url().startsWith('blob:') &&
        request.failure()?.errorText === 'net::ERR_ABORTED') {
      return;
    }
    if (request.method() === 'DELETE' &&
        request.url().includes('/api/download/') &&
        request.failure()?.errorText === 'net::ERR_ABORTED') {
      return;
    }
    if (request.method() === 'POST' &&
        request.url().endsWith('/api/latex/render') &&
        request.failure()?.errorText === 'net::ERR_ABORTED') {
      return;
    }
    if (request.url().includes('/api/auth/login') &&
        request.failure()?.errorText === 'net::ERR_ABORTED') {
      return;
    }
    errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
  });
  page.on('response', response => {
    if (response.status() === 404 &&
        response.url().endsWith(
          '/api/conversations/00000000-0000-4000-8000-000000000000'
        )) {
      return;
    }
    if (response.url().includes('/api/auth/login') && response.status() >= 400) {
      return; /* expected: login attempt with invalid credentials in modal test */
    }
    if (response.status() >= 400) {
      errors.push(`response: ${response.status()} ${response.url()}`);
    }
  });

  await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
  await page.waitForFunction(() =>
    customElements.get('mjj-jrpg-composer') &&
    customElements.get('mjj-jrpg-menu') &&
    customElements.get('mjj-jrpg-chat') &&
    customElements.get('mjj-jrpg-preview')
  );
  await page.evaluate(() => document.fonts.ready);
  const desktop = await page.evaluate(() => {
    const scene = document.querySelector('.jrpg-scene').getBoundingClientRect();
    const utility = document.querySelector('.jrpg-utility').getBoundingClientRect();
    const composer = document.querySelector('mjj-jrpg-composer')
      .getBoundingClientRect();
    const composerField = document.querySelector('.jrpg-composer zen-field')
      .getBoundingClientRect();
    const composerFieldStyle = getComputedStyle(
      document.querySelector('.jrpg-composer zen-field'),
    );
    const composerLabel = document.querySelector('.jrpg-composer label')
      .getBoundingClientRect();
    const composerTextarea = document.querySelector('#jrpg-message')
      .getBoundingClientRect();
    const menu = document.querySelector('mjj-jrpg-menu').getBoundingClientRect();
    const character = document.querySelector('[data-character]');
    const characterBox = character.getBoundingClientRect();
    const sceneBarStyle = getComputedStyle(
      document.querySelector('.jrpg-scene'),
      '::before',
    );
    const messageScroller = document.querySelector('zen-message-scroller')
      .getBoundingClientRect();
    const messageViewport = document.querySelector('.jrpg-message-viewport')
      .getBoundingClientRect();
    const workspace = document.querySelector('.jrpg-workspace')
      .getBoundingClientRect();
    const brand = document.querySelector('.jrpg-brand').getBoundingClientRect();
    const frameControls = document.querySelector('.jrpg-frame-controls')
      .getBoundingClientRect();
    const frameTelemetry = document.querySelector('.jrpg-frame-telemetry')
      .getBoundingClientRect();
    const telemetryValues = [
      ...document.querySelectorAll('.jrpg-frame-telemetry > *'),
    ].map(value => value.getBoundingClientRect());
    const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
    const main = document.querySelector('.jrpg-main').getBoundingClientRect();
    const colorProbe = document.createElement('span');
    colorProbe.style.background = 'var(--zenbu-sys-color-surface-page)';
    colorProbe.style.color =
      'var(--zenbu-sys-color-action-primary-background)';
    colorProbe.style.borderColor = 'var(--zenbu-sys-color-focus-outer)';
    const surfaceProbe = document.createElement('span');
    surfaceProbe.style.background =
      'var(--zenbu-sys-color-surface-default)';
    surfaceProbe.style.color = 'var(--zenbu-sys-color-text-primary)';
    surfaceProbe.style.borderColor = 'var(--zenbu-sys-color-border-strong)';
    const sunkenProbe = document.createElement('span');
    sunkenProbe.style.background =
      'var(--zenbu-sys-color-surface-sunken)';
    const panelProbe = document.createElement('span');
    panelProbe.style.background = 'var(--mjj-jrpg-panel-strong)';
    document.body.append(colorProbe, surfaceProbe, sunkenProbe, panelProbe);
    const probeStyle = getComputedStyle(colorProbe);
    const surfaceProbeStyle = getComputedStyle(surfaceProbe);
    const sunkenProbeStyle = getComputedStyle(sunkenProbe);
    const panelProbeStyle = getComputedStyle(panelProbe);
    const computedFonts = [
      document.body,
      document.querySelector('.jrpg-brand h1'),
      document.querySelector('.jrpg-message p'),
      document.querySelector('#jrpg-message'),
      document.querySelector('.jrpg-composer button[type="submit"]'),
    ].map(element => getComputedStyle(element).fontFamily);
    const nonPixelText = [document.body, ...document.body.querySelectorAll('*')]
      .filter(element => [...element.childNodes].some(node =>
        node.nodeType === Node.TEXT_NODE && node.textContent.trim()
      ))
      .filter(element =>
        getComputedStyle(element).fontFamily !== '"Pixel Mplus"'
      )
      .map(element => element.localName);
    const pageStyle = getComputedStyle(document.body);
    const submitStyle = getComputedStyle(
      document.querySelector('.jrpg-composer button[type="submit"]'),
    );
    const colors = {
      canvas: probeStyle.backgroundColor,
      conversation: getComputedStyle(
        document.querySelector('mjj-jrpg-chat'),
      ).backgroundColor,
      conversationBorder: getComputedStyle(
        document.querySelector('mjj-jrpg-chat'),
      ).borderTopColor,
      conversationPanel: panelProbeStyle.backgroundColor,
      conversationText: getComputedStyle(
        document.querySelector('.jrpg-message p'),
      ).color,
      focus: probeStyle.borderColor,
      frame: surfaceProbeStyle.borderColor,
      menu: getComputedStyle(
        document.querySelector('mjj-jrpg-menu'),
      ).backgroundColor,
      page: pageStyle.backgroundColor,
      primary: probeStyle.color,
      primaryText: getComputedStyle(
        document.querySelector('.jrpg-preview-panel h2'),
      ).color,
      sunken: sunkenProbeStyle.backgroundColor,
      surface: surfaceProbeStyle.backgroundColor,
      submit: submitStyle.backgroundColor,
      text: surfaceProbeStyle.color,
      utility: getComputedStyle(
        document.querySelector('.jrpg-utility'),
      ).backgroundColor,
    };
    colorProbe.remove();
    surfaceProbe.remove();
    sunkenProbe.remove();
    panelProbe.remove();
    const closeTo = (value, expected) => Math.abs(value - expected) < 0.01;
    const frameAlignment = [
      closeTo((scene.left - workspace.left) / workspace.width, 0.032),
      closeTo((scene.top - workspace.top) / workspace.height, 0.094),
      closeTo(scene.width / workspace.width, 0.548),
      closeTo(scene.height / workspace.height, 0.57),
      closeTo((utility.left - workspace.left) / workspace.width, 0.604),
      closeTo(utility.width / workspace.width, 0.365),
      closeTo((composer.top - workspace.top) / workspace.height, 0.72),
      closeTo(composer.width / workspace.width, 0.682),
      closeTo((menu.left - workspace.left) / workspace.width, 0.734),
      closeTo(menu.width / workspace.width, 0.234),
    ].every(Boolean);
    const borderless = [
      '.jrpg-shell',
      '.jrpg-scene',
      '.jrpg-utility',
      'mjj-jrpg-chat',
      'mjj-jrpg-composer',
      'mjj-jrpg-menu',
      '.jrpg-preview-panel',
      '.jrpg-message',
      '.jrpg-work-showcase a',
      '.jrpg-composer textarea',
      '.jrpg-composer button[type="submit"]',
      'mjj-jrpg-menu button',
    ].every(selector => {
      const style = getComputedStyle(document.querySelector(selector));
      return style.borderTopWidth === '0px' &&
        style.borderRightWidth === '0px' &&
        style.borderBottomWidth === '0px' &&
        style.borderLeftWidth === '0px';
    });
    return {
      art: getComputedStyle(document.querySelector('.jrpg-workspace'))
        .backgroundImage,
      borderless,
      brandInSystemSlot:
        closeTo((brand.left - workspace.left) / workspace.width, 0.054) &&
        closeTo((brand.top - workspace.top) / workspace.height, 0.022) &&
        document.querySelector('.jrpg-brand').parentElement.classList
          .contains('jrpg-workspace'),
      brandCoversSystemLabel:
        brand.left <= workspace.left + workspace.width * 0.062 &&
        brand.right >= workspace.left + workspace.width * 0.102 &&
        brand.top <= workspace.top + workspace.height * 0.03 &&
        brand.bottom >= workspace.top + workspace.height * 0.06,
      characterLoaded: character.complete && character.naturalWidth > 0,
      colors,
      composerBelowScene: composer.top >= scene.bottom,
      composerTextareaFills: (
        Math.abs(composerTextarea.left - composerField.left) < 1 &&
        Math.abs(composerTextarea.right - composerField.right) < 1 &&
        Math.abs(composerTextarea.bottom - composerField.bottom) < 1 &&
        composerTextarea.height > 60
      ),
      composerTextareaGeometry: {
        fieldBottom: composerField.bottom,
        fieldDisplay: composerFieldStyle.display,
        fieldGridRows: composerFieldStyle.gridTemplateRows,
        fieldLeft: composerField.left,
        fieldRight: composerField.right,
        fieldTop: composerField.top,
        labelBottom: composerLabel.bottom,
        labelTop: composerLabel.top,
        textareaBottom: composerTextarea.bottom,
        textareaHeight: composerTextarea.height,
        textareaLeft: composerTextarea.left,
        textareaRight: composerTextarea.right,
        textareaTop: composerTextarea.top,
      },
      fillsViewport: shell.left === 0 &&
        shell.top === 0 &&
        Math.abs(shell.right - innerWidth) < 1 &&
        Math.abs(shell.bottom - innerHeight) < 1 &&
        main.left === 0 &&
        main.top === 0 &&
        Math.abs(main.right - innerWidth) < 1 &&
        Math.abs(main.bottom - innerHeight) < 1,
      frameAlignment,
      frameChromeAligned:
        closeTo((frameControls.left - workspace.left) / workspace.width, 0.866) &&
        closeTo((frameTelemetry.left - workspace.left) / workspace.width, 0.085) &&
        closeTo((frameTelemetry.top - workspace.top) / workspace.height, 0.928),
      fonts: computedFonts,
      fontLoaded: [...document.fonts].some(font =>
        font.family.replaceAll('"', '') === 'Pixel Mplus' &&
        font.status === 'loaded'
      ),
      menuRightOfComposer: menu.left >= composer.right,
      messageViewportFills: (
        Math.abs(messageViewport.top - messageScroller.top) < 1 &&
        Math.abs(messageViewport.bottom - messageScroller.bottom) < 1 &&
        messageViewport.height > 192
      ),
      noOverflow: document.documentElement.scrollWidth <= innerWidth,
      nonPixelText,
      questLabelAbsent: !document.body.textContent.includes('QUEST 01'),
      previewLabelsRemoved:
        !/CHARACTER RECORD|ITEM INVENTORY|QUEST ARCHIVE/.test(
          document.querySelector('.jrpg-preview-panel').textContent,
        ),
      statsRemoved:
        !document.querySelector('.jrpg-preview-stats') &&
        !document.querySelector('[data-preview-type]'),
      telemetryCovered:
        document.querySelectorAll('.jrpg-frame-telemetry > *').length === 4 &&
        document.querySelector('[data-frame-network]').textContent
          .includes('ONLINE') &&
        document.querySelector('[data-frame-uptime]').textContent
          .match(/^\d{2}:\d{2}:\d{2}$/) &&
        [...document.querySelectorAll('.jrpg-frame-telemetry > *')]
          .every(item => !/TEMP:|USER:|STATUS:|UPTIME:/.test(item.textContent)),
      telemetryValuesAligned:
        closeTo(
          (telemetryValues[0].left + telemetryValues[0].width / 2 -
            frameTelemetry.left) / frameTelemetry.width,
          0.12,
        ) &&
        closeTo(
          (telemetryValues[1].left + telemetryValues[1].width / 2 -
            frameTelemetry.left) / frameTelemetry.width,
          0.325,
        ) &&
        closeTo(
          (telemetryValues[2].left + telemetryValues[2].width / 2 -
            frameTelemetry.left) / frameTelemetry.width,
          0.605,
        ) &&
        closeTo(
          (telemetryValues[3].left + telemetryValues[3].width / 2 -
            frameTelemetry.left) / frameTelemetry.width,
          0.88,
        ),
      shibaInFrontOfBar:
        sceneBarStyle.backgroundImage.includes('bar-ink.webp') &&
        sceneBarStyle.backgroundSize === 'cover' &&
        Math.abs(parseFloat(sceneBarStyle.height) - scene.height) < 1 &&
        Number(getComputedStyle(character.parentElement).zIndex) >
          Number(sceneBarStyle.zIndex) &&
        characterBox.bottom > scene.top + scene.height * 0.65,
      terminalScrollbar: (
        getComputedStyle(
          document.querySelector('.jrpg-message-viewport'),
        ).scrollbarWidth === 'thin' &&
        getComputedStyle(
          document.querySelector('.jrpg-message-viewport'),
        ).scrollbarColor !== 'auto'
      ),
      theme: document.documentElement.dataset.zenTheme,
      utilityRightOfScene: utility.left >= scene.right,
      workspaceFillsViewport:
        Math.abs(workspace.left) < 1 &&
        Math.abs(workspace.top) < 1 &&
        Math.abs(workspace.right - innerWidth) < 1 &&
        Math.abs(workspace.bottom - innerHeight) < 1,
    };
  });
  assert.match(desktop.art, /background-frame-2\.webp/);
  assert.equal(desktop.borderless, true);
  assert.equal(desktop.brandInSystemSlot, true);
  assert.equal(desktop.brandCoversSystemLabel, true);
  assert.equal(desktop.characterLoaded, true);
  assert.equal(desktop.colors.page, desktop.colors.canvas);
  assert.equal(desktop.colors.conversation, 'rgba(0, 0, 0, 0)');
  assert.equal(desktop.colors.conversationText, desktop.colors.text);
  assert.equal(desktop.colors.menu, 'rgba(0, 0, 0, 0)');
  assert.equal(desktop.colors.primaryText, desktop.colors.primary);
  assert.equal(desktop.colors.submit, desktop.colors.primary);
  assert.equal(desktop.colors.utility, 'rgba(0, 0, 0, 0)');
  assert.equal(desktop.composerBelowScene, true);
  assert.equal(
    desktop.composerTextareaFills,
    true,
    JSON.stringify(desktop.composerTextareaGeometry),
  );
  assert.equal(desktop.fillsViewport, true);
  assert.equal(desktop.frameAlignment, true);
  assert.equal(desktop.frameChromeAligned, true);
  assert.ok(desktop.fonts.every(font => font === '"Pixel Mplus"'));
  assert.equal(desktop.fontLoaded, true);
  assert.equal(desktop.menuRightOfComposer, true);
  assert.equal(desktop.messageViewportFills, true);
  assert.equal(desktop.noOverflow, true);
  assert.deepEqual(desktop.nonPixelText, []);
  assert.equal(desktop.questLabelAbsent, true);
  assert.equal(desktop.previewLabelsRemoved, true);
  assert.equal(desktop.statsRemoved, true);
  assert.equal(desktop.telemetryCovered, true);
  assert.equal(desktop.telemetryValuesAligned, true);
  assert.equal(desktop.shibaInFrontOfBar, true);
  assert.equal(desktop.terminalScrollbar, true);
  assert.equal(desktop.theme, 'cyberpunk');
  assert.equal(desktop.utilityRightOfScene, true);
  assert.equal(desktop.workspaceFillsViewport, true);
  await page.locator('.jrpg-brand a').focus();
  assert.equal(
    await page.locator('.jrpg-brand a').evaluate(element =>
      getComputedStyle(element).outlineColor
    ),
    desktop.colors.focus,
  );
  await page.locator('.jrpg-composer button[type="submit"]').focus();
  assert.equal(
    await page.locator('.jrpg-composer button[type="submit"]').evaluate(element =>
      getComputedStyle(element).outlineColor
    ),
    desktop.colors.focus,
  );
  assert.equal(
    await page.locator('meta[name="theme-color"]').first().getAttribute('content'),
    desktop.colors.canvas,
  );
  assert.equal(
    await page.locator('.jrpg-frame-controls :is(button, a)').count(),
    3,
  );
  assert.equal(
    await page.locator('.jrpg-frame-controls a').getAttribute('href'),
    '/',
  );

  // Account slot: guest session shows a login button (modal trigger)
  assert.ok(
    await page.locator('[data-frame-account]').count() > 0,
    'account slot exists in telemetry',
  );
  await page.waitForFunction(() =>
    document.querySelector('[data-frame-account]')?.querySelector('button[data-login-open]') !== null
  );
  const loginBtnState = await page.locator('[data-frame-account] button[data-login-open]').evaluate(btn => ({
    text: btn.textContent.trim(),
    ownerCount: btn.closest('zen-button') ? 1 : 0,
    appearance: btn.closest('zen-button')?.getAttribute('appearance'),
    size: btn.closest('zen-button')?.getAttribute('size'),
  }));
  assert.match(loginBtnState.text, /LOGIN/i);
  assert.equal(loginBtnState.ownerCount, 1, 'login button has zen-button owner');
  assert.equal(loginBtnState.appearance, 'plain');
  assert.equal(loginBtnState.size, 'xs');

  // Login modal: opens on button click, focuses username, closes on cancel
  await page.locator('[data-frame-account] button[data-login-open]').click();
  await page.locator('[data-login-dialog]').waitFor({ state: 'visible' });
  assert.equal(
    await page.locator('[data-login-dialog]').getAttribute('aria-modal'),
    'true',
    'login dialog is modal',
  );
  // Username input should receive initial focus
  await page.waitForFunction(() => document.activeElement?.id === 'jrpg-login-username');
  // Cancel closes dialog and clears password
  await page.locator('#jrpg-login-password').fill('dummypassword');
  await page.locator('[data-login-cancel]').click();
  await page.locator('[data-login-dialog]').waitFor({ state: 'hidden' });
  await page.waitForFunction(() => !document.querySelector('#jrpg-login-password')?.value);
  assert.equal(
    await page.locator('#jrpg-login-password').inputValue(),
    '',
    'password cleared on close',
  );

  // Invalid credentials show error message
  await page.locator('[data-frame-account] button[data-login-open]').click();
  await page.locator('[data-login-dialog]').waitFor({ state: 'visible' });
  await page.locator('#jrpg-login-username').fill('nosuchuser');
  await page.locator('#jrpg-login-password').fill('nosuchpassword123');
  let loginErrorShown = false;
  const loginResponsePromise = page.waitForResponse(resp =>
    resp.url().includes('/api/auth/login')
  );
  await page.locator('[data-login-submit]').click();
  const loginResp = await loginResponsePromise;
  if (loginResp.status() >= 400) {
    await page.waitForFunction(() => {
      const el = document.querySelector('[data-login-error]');
      return el && !el.hidden && el.textContent.trim().length > 0;
    });
    loginErrorShown = true;
  }
  assert.equal(loginErrorShown, true, 'error element shown on bad credentials');
  assert.equal(
    await page.locator('#jrpg-login-password').inputValue(),
    '',
    'password cleared after failed login attempt',
  );
  // Close modal via X button (data-zen-close)
  await page.getByRole('button', { name: 'Close sign-in dialog' }).click();
  await page.locator('[data-login-dialog]').waitFor({ state: 'hidden' });

  // Archive opens only from the bottom destination menu.
  assert.equal(
    await page.locator('[data-toggle-archive]').count(),
    0,
    'redundant chat-heading archive shortcut is removed',
  );
  await page.locator('button[data-preview="conversations"]').click();
  await page.locator('.jrpg-utility mjj-conversation-archive').waitFor({ state: 'visible' });
  const archiveGeometry = await page.evaluate(() => {
    const utility = document.querySelector('.jrpg-utility').getBoundingClientRect();
    const archive = document.querySelector(
      '.jrpg-utility mjj-conversation-archive',
    ).getBoundingClientRect();
    return {
      archive: {
        bottom: archive.bottom,
        height: archive.height,
        left: archive.left,
        right: archive.right,
        top: archive.top,
        width: archive.width,
      },
      fits: archive.left >= utility.left &&
      archive.top >= utility.top &&
      archive.right <= utility.right + 1 &&
      archive.bottom <= utility.bottom + 1 &&
      archive.width / utility.width > 0.85 &&
      archive.height / utility.height > 0.85,
      utility: {
        bottom: utility.bottom,
        height: utility.height,
        left: utility.left,
        right: utility.right,
        top: utility.top,
        width: utility.width,
      },
    };
  });
  assert.equal(
    archiveGeometry.fits,
    true,
    `conversation archive fills the top-right utility aperture: ${JSON.stringify(archiveGeometry)}`,
  );
  // Archive status or empty message should be visible
  await page.waitForFunction(() => {
    const status = document.querySelector('[data-archive-status]');
    return status && !status.hidden;
  });
  // New conversation button exists inside archive
  assert.ok(
    await page.locator('[data-archive-new]').isVisible(),
    'archive new button visible',
  );
  // Conversations menu button should be pressed
  assert.equal(
    await page.locator('button[data-preview="conversations"]').getAttribute('aria-pressed'),
    'true',
  );

  // Archive: close via close button, navigates back to resume panel
  await page.locator('[data-archive-close]').click();
  await page.locator('.jrpg-utility mjj-conversation-archive').waitFor({ state: 'hidden' });
  // mjj-jrpg-chat must remain visible (always in scene)
  assert.equal(
    await page.locator('mjj-jrpg-chat').isVisible(),
    true,
  );

  // Quota element is present (may be shown for guest session)
  assert.ok(
    await page.locator('[data-quota]').count() > 0,
    'quota element exists',
  );

  await page.getByRole('button', { name: 'Minimize interface' }).click();
  assert.equal(
    await page.locator('.jrpg-scene').evaluate(
      scene => getComputedStyle(scene).visibility,
    ),
    'hidden',
  );
  await page.getByRole('button', { name: 'Restore interface' }).click();
  assert.equal(
    await page.locator('.jrpg-scene').evaluate(
      scene => getComputedStyle(scene).visibility,
    ),
    'visible',
  );
  assert.equal(await page.locator('[data-work-showcase] a').count(), 8);
  assert.equal(
    await page.getByRole('button', { name: /Inspect/ }).count(),
    0,
  );
  assert.match(
    await page.locator('[data-work-showcase]').textContent(),
    /Copilot Tasks/,
  );
  assert.match(
    await page.locator('[data-work-showcase]').textContent(),
    /AIX Harness \/ Copilot/,
  );
  assert.equal(
    await page.locator('[data-turn-position]').textContent(),
    'START',
  );
  assert.equal(
    await page.getByRole('button', { name: 'Previous turn' }).isDisabled(),
    true,
  );
  assert.equal(
    await page.getByRole('button', { name: 'Next turn' }).isDisabled(),
    true,
  );

  await page.getByRole('button', { name: 'Tools', exact: true }).click();
  assert.equal(
    await page.locator('[data-preview-title]').textContent(),
    'Tools',
  );
  assert.equal(
    await page.locator('[data-preview-link]').getAttribute('href'),
    '/tools',
  );
  assert.equal(
    await page.getByRole('button', { name: 'Tools', exact: true })
      .getAttribute('aria-pressed'),
    'true',
  );
  await page.waitForFunction(() =>
    document.querySelectorAll('[data-latest-tool]').length === 5
  );
  assert.match(
    await page.locator('[data-work-showcase]').textContent(),
    /Online LaTeX Editor/,
  );
  assert.match(
    await page.locator('[data-work-showcase]').textContent(),
    /Personal Notes/,
  );
  await page.locator('[data-latest-tool]').first().click();
  await page.locator('[data-functional-tool="markdown"]').waitFor();
  assert.equal(
    await page.locator('[data-dialog-title]').textContent(),
    'MarkDown to HTML',
  );
  assert.equal(
    await page.locator('[data-preview-link]').getAttribute('href'),
    '/tools/markdown_to_html',
  );
  await page.locator('[data-markdown-source]').fill('# Modal Markdown');
  await page.waitForFunction(() =>
    document.querySelector('[data-markdown-output] h1')?.textContent ===
      'Modal Markdown'
  );
  assert.equal(await page.locator('.jrpg-tool-pane').count(), 2);
  const toolWorkspace = await page.evaluate(() => {
    const dialogElement = document.querySelector(
      '.jrpg-detail-dialog[data-detail-mode="tools"]',
    );
    const dialog = dialogElement.getBoundingClientRect();
    const windows = [...document.querySelectorAll('.jrpg-tool-window')]
      .map(window => window.getBoundingClientRect());
    const windowShadows = [...document.querySelectorAll('.jrpg-tool-window')]
      .map(window => getComputedStyle(window).boxShadow);
    const rail = getComputedStyle(
      document.querySelector('.jrpg-tool-panes'),
      '::before',
    );
    return {
      dialogHeight: dialogElement.offsetHeight,
      dialogWidth: dialogElement.offsetWidth,
      fillsViewport: dialogElement.offsetWidth > innerWidth * 0.9 &&
        dialogElement.offsetHeight > innerHeight * 0.9,
      viewportHeight: innerHeight,
      viewportWidth: innerWidth,
      sideBySide: windows.length === 2 &&
        windows[0].right < windows[1].left &&
        Math.abs(windows[0].top - windows[1].top) < 1,
      fullPageInHeader: Boolean(
        document.querySelector('[data-preview-link]')
          .closest('.jrpg-dialog-heading-actions'),
      ),
      footerHidden: document.querySelector('[data-dialog-actions]').hidden,
      metalRail: rail.content !== 'none' &&
        rail.backgroundImage.includes('bar-ink.webp'),
      pixelIcons: document.querySelectorAll('.jrpg-tool-window zen-icon').length >= 2 &&
        [...document.querySelectorAll('.jrpg-tool-window zen-icon svg')]
          .every(icon =>
            getComputedStyle(icon).shapeRendering.toLowerCase() === 'crispedges'
          ),
      separateGlows: windowShadows.length === 2 &&
        windowShadows.every(shadow => shadow !== 'none'),
      wideWindows: windows.every(window => window.width > dialog.width * 0.35),
    };
  });
  assert.equal(
    toolWorkspace.fillsViewport,
    true,
    JSON.stringify(toolWorkspace),
  );
  assert.equal(toolWorkspace.sideBySide, true);
  assert.equal(toolWorkspace.fullPageInHeader, true);
  assert.equal(toolWorkspace.footerHidden, true);
  assert.equal(toolWorkspace.metalRail, true);
  assert.equal(toolWorkspace.pixelIcons, true);
  assert.equal(toolWorkspace.separateGlows, true);
  assert.equal(toolWorkspace.wideWindows, true);
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.locator('[data-latest-tool]').first().click();
  await page.locator('[data-functional-tool="markdown"]').waitFor();
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.locator('[data-latest-tool]').nth(1).click();
  await page.locator('[data-functional-tool="converter"]').waitFor();
  assert.equal(
    await page.locator('[data-converter-input]').count(),
    2,
  );
  const converterFixture = path.join(
    RUNFILES,
    WORKSPACE,
    'mrjunejune/test/shiba.webp',
  );
  assert.equal(fs.existsSync(converterFixture), true);
  await page.locator('[data-converter-input="image"]').setInputFiles(
    converterFixture,
  );
  const conversionResponse = page.waitForResponse(response =>
    response.url().endsWith('/api/convert/image-to-webp') &&
    response.request().method() === 'POST'
  );
  await page.locator('[data-convert-kind="image"]').click();
  assert.equal((await conversionResponse).status(), 200);
  await page.locator('[data-converter-download="image"]').waitFor();
  const cleanupResponse = page.waitForResponse(response =>
    response.url().includes('/api/download/') &&
    response.request().method() === 'DELETE'
  );
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  assert.equal((await cleanupResponse).status(), 204);
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.locator('[data-latest-tool]').nth(1).click();
  await page.locator('[data-functional-tool="converter"]').waitFor();
  await page.locator('[data-converter-input="image"]').setInputFiles(
    converterFixture,
  );
  const lateConversionResponse = page.waitForResponse(response =>
    response.url().endsWith('/api/convert/image-to-webp') &&
    response.request().method() === 'POST'
  );
  const lateCleanupResponse = page.waitForResponse(response =>
    response.url().includes('/api/download/') &&
    response.request().method() === 'DELETE'
  );
  await page.locator('[data-convert-kind="image"]').click();
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  assert.equal((await lateConversionResponse).status(), 200);
  assert.equal((await lateCleanupResponse).status(), 204);
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.locator('[data-latest-tool]').nth(2).click();
  await page.locator('[data-functional-tool="hls"]').waitFor();
  assert.equal(
    await page.locator('[data-preview-link]').getAttribute('href'),
    '/tools/hls_player',
  );
  await page.locator('[data-hls-sample]').click();
  await page.waitForFunction(() =>
    ['ready', 'error'].includes(
      document.querySelector('[data-hls-status]')?.dataset.state,
    )
  );
  assert.ok(
    ['ready', 'error'].includes(
      await page.locator('[data-hls-status]').getAttribute('data-state'),
    ),
  );
  assert.match(
    await page.locator('[data-hls-status]').textContent(),
    /ready|unsupported|failed/i,
  );
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.locator('[data-latest-tool]').nth(3).click();
  await page.locator('[data-functional-tool="latex"]').waitFor();
  await page.locator('[data-latex-compile]').click();
  await page.locator('[data-latex-source]').fill(
    '\\documentclass{article}\\begin{document}Updated modal PDF\\end{document}',
  );
  assert.equal(await page.locator('[data-latex-download]').isHidden(), true);
  await page.locator('[data-latex-compile]').click();
  await page.waitForFunction(() =>
    document.querySelector('[data-latex-status]')?.dataset.state === 'ready'
  );
  assert.match(
    await page.locator('[data-latex-preview]').getAttribute('src'),
    /^blob:/,
  );
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.locator('[data-latest-tool]').nth(4).click();
  await page.waitForFunction(() =>
    document.querySelector('[data-tool-content] h1')?.textContent ===
      'Personal Notes'
  );
  assert.match(
    await page.locator('[data-tool-content]').textContent(),
    /private browser-based writing workspace/,
  );
  assert.equal(
    await page.locator('[data-preview-link]').getAttribute('href'),
    '/notes',
  );
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.getByRole('button', { name: 'Resume', exact: true }).click();
  await page.locator('[data-resume-modal]').click();
  await page.locator('[data-resume-content] .sub-header').first().waitFor();
  const resumeDossier = await page.locator('[data-resume-dossier]').evaluate(
    dossier => ({
      fontFamily: getComputedStyle(dossier).fontFamily,
      text: dossier.textContent,
    }),
  );
  assert.equal(resumeDossier.fontFamily, '"Pixel Mplus"');
  assert.match(resumeDossier.text, /Copilot Tasks/);
  assert.match(resumeDossier.text, /Microsoft/);
  assert.match(resumeDossier.text, /Meta/);
  assert.equal(
    await page.locator('[data-resume-content] a[target="_blank"]').count() > 0,
    true,
  );
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.getByRole('button', { name: 'Blogs', exact: true }).click();
  await page.waitForFunction(() =>
    document.querySelectorAll('[data-latest-blog]').length === 4
  );
  assert.match(
    await page.locator('[data-work-showcase]').textContent(),
    /Websocket Demystified/,
  );
  assert.match(
    await page.locator('[data-work-showcase]').textContent(),
    /Creating Network Library in C/,
  );
  await page.locator('[data-latest-blog]').first().click();
  await page.locator('[data-blog-content] h1').waitFor();
  assert.equal(
    await page.locator('[data-blog-content] h1').textContent(),
    'WebSocket Demystified',
  );
  assert.equal(
    await page.locator('[data-preview-link]').getAttribute('href'),
    '/blog/websocket-demystified',
  );
  assert.equal(
    await page.locator('[data-blog-content]').evaluate(
      article => getComputedStyle(article).fontFamily,
    ),
    '"Pixel Mplus"',
  );
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();
  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });

  await page.locator('[data-blog-archive]').click();
  await page.waitForFunction(() =>
    document.querySelectorAll('[data-blog-entry]').length === 9
  );
  assert.equal(await page.locator('[data-blog-entry]').count(), 9);
  await page.locator('[data-blog-entry]').nth(1).click();
  await page.waitForFunction(() =>
    document.querySelector('[data-blog-content] h1')?.textContent ===
      'Creating Network Library in C'
  );
  assert.equal(
    await page.locator('[data-preview-link]').getAttribute('href'),
    '/blog/my-seobeo-journey',
  );
  await page.getByRole('button', {
    name: 'Close destination details',
  }).click();

  await page.evaluate(() => {
    window.__jrpgStreamEvents = [];
    document.querySelector('mjj-jrpg-shell').addEventListener(
      'mjj-jrpg-stream-event',
      event => window.__jrpgStreamEvents.push(event.detail),
    );
  });
  await page.locator('#jrpg-message').fill('Where should I begin?');
  await page.locator('#jrpg-message').press('Enter');
  await page.locator('.jrpg-message[data-speaker="June"]').last().waitFor();
  const streamingMessage = page.locator(
    '.jrpg-message[data-speaker="Epi"][data-streaming="true"]',
  ).last();
  await streamingMessage.waitFor();
  await page.locator('[data-cancel-control]').waitFor({ state: 'hidden' });
  await streamingMessage.waitFor();
  assert.notEqual(
    await streamingMessage.locator('p').textContent(),
    'hello traveler',
  );
  await page.getByRole('button', { name: 'Blogs', exact: true }).click();
  assert.equal(
    await page.locator('.jrpg-message[data-speaker="June"] p').last()
      .textContent(),
    'Where should I begin?',
  );
  await page.waitForFunction(() =>
    document.querySelector('mjj-jrpg-character')?.getAttribute('state') ===
      'default' &&
    document.querySelectorAll('.jrpg-message[data-speaker="Epi"]').length > 1
  );
  assert.equal(
    await page.locator('.jrpg-message[data-speaker="Epi"] p').last()
      .textContent(),
    'hello traveler',
  );
  assert.equal(
    await page.locator(
      '.jrpg-message[data-speaker="Epi"][data-streaming="true"]',
    ).count(),
    0,
  );
  assert.deepEqual(
    await page.evaluate(() => {
      const event = window.__jrpgStreamEvents.find(
        item => item.type === 'tool.started',
      );
      return {
        name: event?.data.tool?.name,
        type: event?.type,
      };
    }),
    {
      name: 'mock-search',
      type: 'tool.started',
    },
  );
  assert.equal(
    await page.locator('[data-turn-position]').textContent(),
    'TURN 1 / 1',
  );
  assert.equal(
    await page.getByRole('button', { name: 'Previous turn' }).isEnabled(),
    true,
  );
  assert.equal(
    await page.getByRole('button', { name: 'Next turn' }).isDisabled(),
    true,
  );
  await page.getByRole('button', { name: 'Previous turn' }).click();
  assert.equal(
    await page.locator('[data-turn-position]').textContent(),
    'START',
  );
  assert.equal(
    await page.locator('.jrpg-message[data-speaker="June"]:visible').count(),
    0,
  );
  assert.equal(
    await page.locator('.jrpg-message[data-speaker="System"]:visible').count(),
    1,
  );
  await page.getByRole('button', { name: 'Next turn' }).click();
  assert.equal(
    await page.locator('[data-turn-position]').textContent(),
    'TURN 1 / 1',
  );
  assert.equal(
    await page.locator('.jrpg-message[data-speaker="June"]:visible').count(),
    1,
  );
  assert.equal(await page.locator('#jrpg-message').isEnabled(), true);

  await page.waitForFunction(() => navigator.serviceWorker?.controller);
  await page.reload({ waitUntil: 'networkidle' });
  await page.evaluate(() => document.fonts.ready);
  const controlledReload = await page.evaluate(() => {
    const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
    return {
      art: getComputedStyle(document.querySelector('.jrpg-workspace'))
        .backgroundImage,
      fillsViewport: shell.left === 0 &&
        shell.top === 0 &&
        Math.abs(shell.right - innerWidth) < 1 &&
        Math.abs(shell.bottom - innerHeight) < 1,
      fontLoaded: document.fonts.check('16px "Pixel Mplus"'),
      fontFamily: getComputedStyle(
        document.querySelector('.jrpg-brand h1'),
      ).fontFamily,
      theme: document.documentElement.dataset.zenTheme,
    };
  });
  assert.match(controlledReload.art, /background-frame-2\.webp/);
  assert.equal(controlledReload.fillsViewport, true);
  assert.equal(controlledReload.fontLoaded, true);
  assert.equal(controlledReload.fontFamily, '"Pixel Mplus"');
  assert.equal(controlledReload.theme, 'cyberpunk');
  assert.deepEqual(errors, []);
  await context.close();

  const mobileContext = await browser.newContext({
    colorScheme: 'light',
    viewport: { width: 360, height: 640 },
  });
  const mobilePage = await mobileContext.newPage();
  await mobilePage.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
  await mobilePage.waitForFunction(() =>
    customElements.get('mjj-jrpg-composer') &&
    customElements.get('mjj-jrpg-menu')
  );
  const mobile = await mobilePage.evaluate(() => {
    const workspace = document.querySelector('.jrpg-workspace')
      .getBoundingClientRect();
    const scene = document.querySelector('.jrpg-scene').getBoundingClientRect();
    const utility = document.querySelector('.jrpg-utility').getBoundingClientRect();
    const composer = document.querySelector('mjj-jrpg-composer')
      .getBoundingClientRect();
    const menu = document.querySelector('mjj-jrpg-menu').getBoundingClientRect();
    const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
    const hamburger = document.querySelector('[data-mobile-menu-toggle]');
    const hamburgerBox = hamburger?.getBoundingClientRect();
    const menuButtons = [...document.querySelectorAll(
      'mjj-jrpg-menu button[data-preview]',
    )];
    const telemetryItems = [
      ...document.querySelectorAll('.jrpg-frame-telemetry > *'),
    ];
    const closeTo = (value, expected, tol = 0.025) =>
      Math.abs(value - expected) < tol;
    const wsW = workspace.width;
    const wsH = workspace.height;
    const mobileArt = getComputedStyle(
      document.querySelector('.jrpg-workspace'),
    ).backgroundImage;
    const mobileArtIsCorrect = mobileArt.includes('background-frame-mobile.webp');
    const aperturesTolerance = [
      closeTo((scene.left - workspace.left) / wsW, 0.045),
      closeTo((scene.top - workspace.top) / wsH, 0.075),
      closeTo(scene.width / wsW, 0.91),
      closeTo(scene.height / wsH, 0.455),
      closeTo((utility.left - workspace.left) / wsW, 0.045),
      closeTo((utility.top - workspace.top) / wsH, 0.547),
      closeTo(utility.width / wsW, 0.91),
      closeTo(utility.height / wsH, 0.171),
      closeTo((composer.left - workspace.left) / wsW, 0.045),
      closeTo((composer.top - workspace.top) / wsH, 0.728),
      closeTo(composer.width / wsW, 0.603),
      closeTo(composer.height / wsH, 0.192),
      closeTo((menu.left - workspace.left) / wsW, 0.665),
      closeTo((menu.top - workspace.top) / wsH, 0.728),
      closeTo(menu.width / wsW, 0.29),
      closeTo(menu.height / wsH, 0.192),
    ].every(Boolean);
    const hamburgerPresent = hamburger !== null &&
      hamburger.getAttribute('aria-expanded') === 'true' &&
      hamburgerBox !== null &&
      hamburgerBox.width > 0 &&
      hamburgerBox.height > 0;
    const telemetryCount = telemetryItems.length === 4;
    const composerVisible = composer.width > 0 && composer.height > 0;
    const menuVisible = menu.width > 0 && menu.height > 0;
    const conversationsButton = document.querySelector(
      'button[data-preview="conversations"]',
    );
    return {
      aperturesTolerance,
      conversationsButtonVisible: conversationsButton !== null &&
        conversationsButton.offsetParent !== null,
      buttonsFit: menuButtons.every(button =>
        button.getBoundingClientRect().right <= innerWidth + 1
      ),
      composerVisible,
      fillsViewport: shell.left === 0 &&
        shell.top === 0 &&
        Math.abs(shell.right - innerWidth) < 1 &&
        Math.abs(shell.bottom - innerHeight) < 1,
      fillsWidth: shell.left === 0 &&
        Math.abs(shell.right - innerWidth) < 1,
      hamburgerPresent,
      initialScroll: scrollY,
      mobileArtIsCorrect,
      menuVisible,
      noOverflow: document.documentElement.scrollWidth <= innerWidth,
      telemetryCount,
      utilityBelowScene: utility.top >= scene.bottom - 1,
      workspaceFills: Math.abs(workspace.left) < 1 &&
        Math.abs(workspace.top) < 1 &&
        Math.abs(workspace.right - innerWidth) < 1 &&
        Math.abs(workspace.bottom - innerHeight) < 1,
    };
  });
  assert.equal(mobile.aperturesTolerance, true, 'mobile apertures within tolerance at 360x640');
  assert.equal(
    mobile.conversationsButtonVisible,
    true,
    'Conversations destination visible on mobile',
  );
  assert.equal(mobile.buttonsFit, true, 'menu buttons fit within viewport width');
  assert.equal(mobile.composerVisible, true, 'composer visible');
  assert.equal(mobile.fillsViewport, true, 'shell fills viewport');
  assert.equal(mobile.fillsWidth, true, 'shell fills width');
  assert.equal(mobile.hamburgerPresent, true, 'hamburger button present and has aria-expanded=true');
  assert.equal(mobile.initialScroll, 0, 'no initial scroll');
  assert.equal(mobile.mobileArtIsCorrect, true, 'mobile background URL is background-frame-mobile.webp');
  assert.equal(mobile.menuVisible, true, 'menu visible');
  assert.equal(mobile.noOverflow, true, 'no horizontal overflow at 360x640');
  assert.equal(mobile.telemetryCount, true, 'four telemetry items present');
  assert.equal(mobile.utilityBelowScene, true, 'utility aperture below scene');
  assert.equal(mobile.workspaceFills, true, 'workspace fills full viewport');

  // Test hamburger disclosure behavior.
  await mobilePage.locator('[data-mobile-menu-toggle]').click();
  assert.equal(
    await mobilePage.locator('[data-mobile-menu-toggle]')
      .getAttribute('aria-expanded'),
    'false',
    'hamburger collapses the destination menu',
  );
  assert.equal(
    await mobilePage.locator('mjj-jrpg-menu')
      .getAttribute('data-mobile-menu-collapsed'),
    '',
    'menu exposes its collapsed state',
  );
  // Second click expands.
  await mobilePage.locator('[data-mobile-menu-toggle]').click();
  assert.equal(
    await mobilePage.locator('[data-mobile-menu-toggle]')
      .getAttribute('aria-expanded'),
    'true',
    'hamburger expands the destination menu',
  );
  // Hamburger keyboard accessible
  assert.ok(
    await mobilePage.locator('[data-mobile-menu-toggle]').evaluate(el =>
      el.tabIndex >= 0 || el.tabIndex === -1
    ),
    'hamburger button is in document',
  );
  await mobileContext.close();

  // Test at 390x844
  const mobile390Context = await browser.newContext({
    colorScheme: 'light',
    viewport: { width: 390, height: 844 },
  });
  const mobile390Page = await mobile390Context.newPage();
  await mobile390Page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
  const mobile390 = await mobile390Page.evaluate(() => {
    const workspace = document.querySelector('.jrpg-workspace')
      .getBoundingClientRect();
    const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
    const mobileArt = getComputedStyle(
      document.querySelector('.jrpg-workspace'),
    ).backgroundImage;
    return {
      fillsViewport: shell.left === 0 &&
        shell.top === 0 &&
        Math.abs(shell.right - innerWidth) < 1 &&
        Math.abs(shell.bottom - innerHeight) < 1,
      mobileArtIsCorrect: mobileArt.includes('background-frame-mobile.webp'),
      noOverflow: document.documentElement.scrollWidth <= innerWidth,
      workspaceFills: Math.abs(workspace.left) < 1 &&
        Math.abs(workspace.top) < 1 &&
        Math.abs(workspace.right - innerWidth) < 1 &&
        Math.abs(workspace.bottom - innerHeight) < 1,
    };
  });
  assert.equal(mobile390.fillsViewport, true, 'fills viewport at 390x844');
  assert.equal(mobile390.mobileArtIsCorrect, true, 'mobile art at 390x844');
  assert.equal(mobile390.noOverflow, true, 'no overflow at 390x844');
  assert.equal(mobile390.workspaceFills, true, 'workspace fills at 390x844');
  await mobile390Context.close();

  // Test at 320x568 (narrow)
  const mobile320Context = await browser.newContext({
    colorScheme: 'light',
    viewport: { width: 320, height: 568 },
  });
  const mobile320Page = await mobile320Context.newPage();
  await mobile320Page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
  const mobile320 = await mobile320Page.evaluate(() => {
    const workspace = document.querySelector('.jrpg-workspace')
      .getBoundingClientRect();
    const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
    const composer = document.querySelector('mjj-jrpg-composer')
      .getBoundingClientRect();
    const menu = document.querySelector('mjj-jrpg-menu').getBoundingClientRect();
    const mobileArt = getComputedStyle(
      document.querySelector('.jrpg-workspace'),
    ).backgroundImage;
    return {
      composerPresent: composer.width > 0 && composer.height > 0,
      fillsViewport: shell.left === 0 &&
        shell.top === 0 &&
        Math.abs(shell.right - innerWidth) < 1 &&
        Math.abs(shell.bottom - innerHeight) < 1,
      menuPresent: menu.width > 0 && menu.height > 0,
      mobileArtIsCorrect: mobileArt.includes('background-frame-mobile.webp'),
      noOverflow: document.documentElement.scrollWidth <= innerWidth,
      workspaceFills: Math.abs(workspace.left) < 1 &&
        Math.abs(workspace.top) < 1 &&
        Math.abs(workspace.right - innerWidth) < 1 &&
        Math.abs(workspace.bottom - innerHeight) < 1,
    };
  });
  assert.equal(mobile320.composerPresent, true, 'composer present at 320x568');
  assert.equal(mobile320.fillsViewport, true, 'fills viewport at 320x568');
  assert.equal(mobile320.menuPresent, true, 'menu present at 320x568');
  assert.equal(mobile320.mobileArtIsCorrect, true, 'mobile art at 320x568');
  assert.equal(mobile320.noOverflow, true, 'no overflow at 320x568');
  assert.equal(mobile320.workspaceFills, true, 'workspace fills at 320x568');
  await mobile320Context.close();

  const shortContext = await browser.newContext({
    colorScheme: 'dark',
    viewport: { width: 900, height: 500 },
  });
  const shortPage = await shortContext.newPage();
  await shortPage.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
  const shortLayout = await shortPage.evaluate(() => {
    const scene = document.querySelector('.jrpg-scene').getBoundingClientRect();
    const character = document.querySelector('mjj-jrpg-character')
      .getBoundingClientRect();
    const chat = document.querySelector('mjj-jrpg-chat')
      .getBoundingClientRect();
    return {
      characterInside: character.left >= scene.left &&
        character.right <= scene.right,
      chatInside: chat.left >= scene.left && chat.right <= scene.right,
      noSceneOverflow: document.querySelector('.jrpg-scene').scrollWidth <=
        document.querySelector('.jrpg-scene').clientWidth,
    };
  });
  assert.deepEqual(shortLayout, {
    characterInside: true,
    chatInside: true,
    noSceneOverflow: true,
  });
  await shortContext.close();

  const forcedColorsContext = await browser.newContext({
    forcedColors: 'active',
    viewport: { width: 1280, height: 900 },
  });
  const forcedColorsPage = await forcedColorsContext.newPage();
  await forcedColorsPage.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
  assert.deepEqual(
    await forcedColorsPage.evaluate(() => ({
      sceneBar: getComputedStyle(
        document.querySelector('.jrpg-scene'),
        '::before',
      ).backgroundImage,
      workspace: getComputedStyle(
        document.querySelector('.jrpg-workspace'),
      ).backgroundImage,
    })),
    {
      sceneBar: 'none',
      workspace: 'none',
    },
  );
  await forcedColorsContext.close();
}

async function testHlsPlayer(browser, siteRoot) {
  const page = await browser.newPage();
  const errors = [];
  const mediaRequests = [];
  let testingExpectedFailure = false;
  let testingExpectedReload = false;
  page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));
  page.on('console', message => {
    if (message.type() !== 'error') return;
    if (testingExpectedFailure &&
        message.text().startsWith('Failed to load resource:')) return;
    errors.push(`console: ${message.text()}`);
  });
  page.on('requestfailed', request => {
    if (testingExpectedReload &&
        request.failure()?.errorText === 'net::ERR_ABORTED') return;
    errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
  });
  page.on('request', request => {
    if (/\.(?:m3u8|m4s|mp4)(?:\?|$)/.test(request.url())) {
      mediaRequests.push(request.url());
    }
  });

  const playlistResponse = await fetch(
    `${baseUrl}/public/hls-sample/master.m3u8`,
  );
  assert.equal(playlistResponse.status, 200);
  assert.match(
    playlistResponse.headers.get('content-type') || '',
    /^application\/vnd\.apple\.mpegurl/,
  );
  assert.match(
    await playlistResponse.text(),
    /#EXT-X-STREAM-INF:.*CODECS="vp09\.00\.10\.08,opus"/,
  );

  const variantResponse = await fetch(
    `${baseUrl}/public/hls-sample/vp9-stream.m3u8`,
  );
  assert.equal(variantResponse.status, 200);
  assert.match(
    await variantResponse.text(),
    /#EXT-X-MAP:URI="vp9-init\.mp4"/,
  );

  const segmentResponse = await fetch(
    `${baseUrl}/public/hls-sample/vp9-segment000.m4s`,
  );
  assert.equal(segmentResponse.status, 200);
  assert.match(
    segmentResponse.headers.get('content-type') || '',
    /^video\/iso\.segment/,
  );
  const transportStreamResponse = await fetch(
    `${baseUrl}/public/hls-sample/h264-ts-segment000.ts`,
  );
  assert.equal(transportStreamResponse.status, 200);
  assert.match(
    transportStreamResponse.headers.get('content-type') || '',
    /^video\/mp2t/,
  );
  assert.ok((await transportStreamResponse.arrayBuffer()).byteLength > 0);

  await page.goto(
    `${baseUrl}/tools/hls_player?url=${encodeURIComponent('/public/hls-sample/master.m3u8')}`,
    {
      waitUntil: 'networkidle',
    },
  );
  await page.getByRole('button', { name: 'Sample', exact: true }).waitFor();
  assert.equal(
    await page.locator('#hlsUrl').evaluate(input => input.defaultValue),
    '/public/hls-sample/h264-ts-stream.m3u8',
  );
  await page.waitForFunction(() => {
    const status = document.querySelector('#hlsStatus');
    return status?.dataset.state === 'ready' ||
      status?.dataset.state === 'error';
  }, null, { timeout: 15000 });
  const terminalState = await page.locator('#hlsStatus').getAttribute('data-state');
  if (terminalState !== 'ready') {
    throw new Error(
      `HLS player failed: ${await page.locator('#hlsStatus').textContent()}\n${errors.join('\n')}`,
    );
  }
  assert.match(await page.locator('#hlsStatus').textContent(), /Stream ready/);
  assert.equal(
    await page.locator('[data-detail="mode"]').textContent(),
    'hls.js',
  );
  assert.match(
    await page.locator('[data-detail="segments"]').textContent(),
    /Managed by hls\.js|adaptive levels/,
  );
  await page.waitForFunction(() => {
    const video = document.querySelector('#hlsVideo');
    return video.readyState >= HTMLMediaElement.HAVE_METADATA &&
      Number.isFinite(video.duration) &&
      video.duration >= 14;
  });
  const firstFrame = await page.locator('#hlsVideo').evaluate(video => {
    const canvas = document.createElement('canvas');
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const context = canvas.getContext('2d');
    context.drawImage(video, 0, 0);
    const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
    let total = 0;
    for (let index = 0; index < pixels.length; index += 4) {
      total += pixels[index] + pixels[index + 1] + pixels[index + 2];
    }
    return total / (canvas.width * canvas.height * 3);
  });
  assert.ok(firstFrame > 10, `Initial HLS frame is black: ${firstFrame}`);
  await page.locator('#hlsVideo').evaluate(video => video.play());
  await page.waitForFunction(() => document.querySelector('#hlsVideo').currentTime > 0.2);
  await page.locator('#hlsVideo').evaluate(video => video.pause());
  assert.ok(mediaRequests.some(url => url.endsWith('/hls-sample/master.m3u8')));
  assert.ok(mediaRequests.some(url => url.endsWith('-stream.m3u8')));
  assert.ok(mediaRequests.some(url => url.endsWith('-init.mp4')));
  assert.ok(mediaRequests.some(url => /-segment\d+\.m4s$/.test(url)));

  await page.evaluate(() => {
    window.hlsPlayer.hls.trigger(window.Hls.Events.ERROR, {
      fatal: true,
      type: window.Hls.ErrorTypes.MEDIA_ERROR,
      details: 'testRuntimeFailure',
      error: new Error('runtime segment failed'),
    });
  });
  await page.locator('#hlsStatus[data-state="error"]').waitFor();
  assert.match(
    await page.locator('#hlsStatus').textContent(),
    /runtime segment failed/,
  );
  await page.locator('#hlsUrl').fill('/public/hls-sample/master.m3u8');
  await page.getByRole('button', { name: 'Load', exact: true }).click();
  await page.locator('#hlsStatus[data-state="ready"]').waitFor();

  let delayedSegment = true;
  await page.route('**/hls-sample/vp9-segment000.m4s', async route => {
    if (delayedSegment) {
      delayedSegment = false;
      await new Promise(resolve => setTimeout(resolve, 250));
    }
    await route.continue();
  });
  testingExpectedReload = true;
  await page.locator('#hlsUrl').fill('/public/hls-sample/master.m3u8');
  await page.getByRole('button', { name: 'Load', exact: true }).click();
  await page.waitForTimeout(25);
  await page.getByRole('button', { name: 'Load', exact: true }).click();
  await page.locator('#hlsStatus[data-state="ready"]').waitFor();
  testingExpectedReload = false;
  assert.match(await page.locator('#hlsStatus').textContent(), /Stream ready/);

  await page.evaluate(() => {
    window.__hlsObjectUrls = { created: [], revoked: [] };
    const createObjectURL = URL.createObjectURL.bind(URL);
    const revokeObjectURL = URL.revokeObjectURL.bind(URL);
    URL.createObjectURL = value => {
      const url = createObjectURL(value);
      window.__hlsObjectUrls.created.push(url);
      return url;
    };
    URL.revokeObjectURL = url => {
      window.__hlsObjectUrls.revoked.push(url);
      revokeObjectURL(url);
    };
  });
  const localFiles = listFiles(path.join(siteRoot, 'public/hls-sample'));
  await page.locator('#hlsFiles').setInputFiles(localFiles);
  await page.waitForFunction(() =>
    document.querySelector('#hlsUrl')?.value.startsWith('Local: ')
  );
  await page.locator('#hlsStatus[data-state="ready"]').waitFor();
  assert.match(await page.locator('#hlsUrl').inputValue(), /^Local: /);
  assert.equal(
    await page.locator('[data-detail="mode"]').textContent(),
    'hls.js',
  );
  await page.locator('#hlsVideo').evaluate(video => video.play());
  await page.waitForFunction(() => document.querySelector('#hlsVideo').currentTime > 0.3);
  await page.locator('#hlsVideo').evaluate(video => video.pause());
  const incompleteLocalFiles = localFiles.filter(file =>
    file.endsWith('master.m3u8') ||
    file.endsWith('vp9-stream.m3u8')
  );
  await page.locator('#hlsFiles').setInputFiles(incompleteLocalFiles);
  await page.locator('#hlsStatus[data-state="error"]').waitFor();
  assert.match(
    await page.locator('#hlsStatus').textContent(),
    /Local HLS file is missing/,
  );
  const objectUrlCounts = await page.evaluate(() => ({
    created: window.__hlsObjectUrls.created.length,
    revoked: window.__hlsObjectUrls.revoked.length,
  }));
  assert.ok(objectUrlCounts.created > 10, JSON.stringify(objectUrlCounts));
  assert.ok(
    objectUrlCounts.revoked >= objectUrlCounts.created,
    JSON.stringify(objectUrlCounts),
  );

  await page.route('**/invalid.m3u8', route => route.fulfill({
    status: 404,
    body: 'not found',
  }));
  testingExpectedFailure = true;
  await page.locator('#hlsUrl').fill(`${baseUrl}/invalid.m3u8`);
  await page.getByRole('button', { name: 'Load', exact: true }).click();
  await page.locator('#hlsStatus[data-state="error"]').waitFor();
  assert.match(
    await page.locator('#hlsStatus').textContent(),
    /404|manifestLoadError|Playlist request failed/,
  );

  if (errors.length) throw new Error(errors.join('\n'));
  await page.close();
}

function findFreePort() {
  return new Promise((resolve, reject) => {
    const socket = net.createServer();
    socket.once('error', reject);
    socket.listen(0, '127.0.0.1', () => {
      const address = socket.address();
      socket.close(error => {
        if (error) reject(error);
        else resolve(String(address.port));
      });
    });
  });
}

/* ------------------------------------------------------------------ */
/* Conversation URL routing regression tests (Fixes 1–4)              */
/* ------------------------------------------------------------------ */

async function testRoleGreetings(browser) {
  const cases = [
    {
      name: 'guest',
      session: {
        kind: 'guest',
        csrfToken: 'guest-greeting-csrf',
        quota: null,
      },
      expected: 'Hi, my name is Epi. June usually calls me Epi-chan.',
    },
    {
      name: 'invited',
      session: {
        kind: 'user',
        username: 'friend',
        role: 'member',
        mustChangePassword: false,
        csrfToken: 'friend-greeting-csrf',
        quota: null,
      },
      expected: 'Since June invited you, I can be a little more casual.',
    },
    {
      name: 'admin',
      session: {
        kind: 'user',
        username: 'june',
        role: 'admin',
        mustChangePassword: false,
        csrfToken: 'admin-greeting-csrf',
        quota: null,
      },
      expected: 'Hi June! I’m Epi-chan.',
    },
  ];

  for (const testCase of cases) {
    const context = await browser.newContext({ serviceWorkers: 'block' });
    const page = await context.newPage();
    await page.route('**/api/auth/session', route => route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify(testCase.session),
    }));
    await page.route('**/api/conversations**', route => route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ conversations: [], cursor: null }),
    }));
    await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'domcontentloaded' });
    const greeting = page.locator('[data-greeting]');
    await greeting.waitFor({ state: 'visible' });
    if (testCase.name === 'guest') {
      await page.locator('[data-greeting][data-streaming="true"]').waitFor({
        state: 'visible',
      });
    }
    await page.waitForFunction(() => {
      const item = document.querySelector('[data-greeting]');
      return item && !item.hasAttribute('data-streaming');
    });
    assert.match(
      await greeting.locator('p').textContent(),
      new RegExp(testCase.expected.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
      `${testCase.name} receives its role-specific Epi greeting`,
    );
    await context.close();
  }
}

async function testConversationRouting(browser) {
  const CONV_A = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
  const CONV_B = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
  const GUEST_SESSION = {
    kind: 'guest', csrfToken: 'test-csrf',
    quota: {
      turnsLimit: 10, turnsUsed: 0, turnsRemaining: 10,
      outputTokensLimit: 10000, outputTokensUsed: 0,
      outputTokensReserved: 0, outputTokensRemaining: 10000,
      resetsAt: Math.floor(Date.now() / 1000) + 86400,
    },
  };

  /* ---- Fix 3: malformed ?conversation param must not fall back to sessionStorage ---- */
  {
    const storedId = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
    const context = await browser.newContext({ serviceWorkers: 'block' });
    await context.addInitScript(id => {
      sessionStorage.setItem('mjj-jrpg-conversation-id', id);
    }, storedId);
    const page = await context.newPage();
    const storedIdFetched = [];
    await page.route('**/api/auth/session', route => route.fulfill({
      status: 200, contentType: 'application/json',
      body: JSON.stringify(GUEST_SESSION),
    }));
    await page.route('**/api/conversations**', route => {
      const url = new URL(route.request().url());
      if (url.pathname.endsWith(`/${storedId}`)) {
        storedIdFetched.push(route.request().url());
      }
      if (url.pathname === '/api/conversations') {
        return route.fulfill({
          status: 200, contentType: 'application/json',
          body: JSON.stringify({ conversations: [], cursor: null }),
        });
      }
      return route.fulfill({
        status: 404, contentType: 'application/json',
        body: JSON.stringify({ error: { message: 'Not found' } }),
      });
    });
    await page.route('**/api/conversations/claim', route => route.fulfill({
      status: 503,
      contentType: 'application/json',
      body: JSON.stringify({ error: { message: 'Service unavailable' } }),
    }));
    await page.goto(`${baseUrl}/jrpg?conversation=not-a-valid-uuid`, {
      waitUntil: 'networkidle',
    });
    assert.equal(
      storedIdFetched.length, 0,
      'Fix 3: malformed ?conversation param must not trigger sessionStorage fallback',
    );
    await context.close();
  }

  /* ---- Fix 4: legacy claim: key retained on 5xx, button re-renders and re-fires ---- */
  {
    const legacyStoredId = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd';
    const context = await browser.newContext({ serviceWorkers: 'block' });
    /* Seed CONVERSATION_STORAGE_KEY so init migrates it to CONVERSATION_LEGACY_KEY */
    await context.addInitScript(id => {
      sessionStorage.setItem('mjj-jrpg-conversation-id', id);
    }, legacyStoredId);
    const page = await context.newPage();
    const pageErrors = [];
    page.on('pageerror', e => {
      if (!e.message.startsWith('Failed to load resource:')) {
        pageErrors.push(`pageerror: ${e.message}`);
      }
    });
    await page.route('**/api/auth/session', route => route.fulfill({
      status: 200, contentType: 'application/json',
      body: JSON.stringify({
        kind: 'user', username: 'tester', role: 'member',
        csrfToken: 'test-csrf', quota: null,
      }),
    }));
    await page.route('**/api/conversations**', route => {
      const url = new URL(route.request().url());
      const req = route.request();
      if (url.pathname === '/api/conversations' && req.method() === 'GET') {
        return route.fulfill({
          status: 200, contentType: 'application/json',
          body: JSON.stringify({ conversations: [], cursor: null }),
        });
      }
      if (url.pathname === `/api/conversations/${legacyStoredId}` && req.method() === 'GET') {
        return route.fulfill({
          status: 404, contentType: 'application/json',
          body: JSON.stringify({ error: { message: 'Not found' } }),
        });
      }
      if (url.pathname === '/api/conversations/claim' && req.method() === 'POST') {
        return route.fulfill({
          status: 503, contentType: 'application/json',
          body: JSON.stringify({ error: { message: 'Service unavailable' } }),
        });
      }
      return route.fulfill({
        status: 404, contentType: 'application/json',
        body: JSON.stringify({ error: { message: 'Not found' } }),
      });
    });
    await page.route('**/api/conversations/claim', route => route.fulfill({
      status: 503,
      contentType: 'application/json',
      body: JSON.stringify({ error: { message: 'Service unavailable' } }),
    }));
    await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });

    /* Open archive panel so the legacy claim status is visible */
    await page.locator('button[data-preview="conversations"]').click();
    await page.locator('mjj-conversation-archive').waitFor({ state: 'visible' });

    /* Init should have called showLegacyClaim() — wait for the button */
    await page.waitForFunction(
      () => document.querySelector('[data-archive-claim]') !== null,
      null, { timeout: 5000 },
    );

    /* First claim attempt — returns 503 */
    const firstClaimResponse = page.waitForResponse(
      response => response.url().includes('/api/conversations/claim'),
    );
    await page.locator('[data-archive-claim]').click();
    const firstClaimStatus = (await firstClaimResponse).status();
    assert.equal(
      firstClaimStatus,
      503,
      `claim fixture returns the mocked service failure, got ${firstClaimStatus}`,
    );

    /* Wait for the retry state to appear: showLegacyClaim re-rendered with error msg */
    await page.waitForFunction(
      () => {
        const status = document.querySelector('[data-archive-status]');
        return status && !status.hidden && status.textContent.includes('Claim failed');
      },
      null, { timeout: 5000 },
    );

    /* Key must be retained in sessionStorage on 5xx */
    const keyAfterFail = await page.evaluate(
      () => sessionStorage.getItem('mjj-jrpg-legacy-claim'),
    );
    assert.equal(
      keyAfterFail, legacyStoredId,
      'Fix 4: CONVERSATION_LEGACY_KEY must be retained after 5xx failure',
    );

    /* Button must be present and enabled for retry */
    await page.locator('[data-archive-claim]').waitFor({ state: 'visible' });

    /* Tag the current button so we can detect when it's replaced by a second re-render */
    await page.locator('[data-archive-claim]').evaluate(btn => {
      btn.dataset.claimGen = '1';
    });

    /* Second click: must dispatch event again — proves no {once:true} on button */
    await page.locator('[data-archive-claim]').click();

    /* Wait for the button to be replaced (showLegacyClaim called again = handler ran) */
    await page.waitForFunction(
      () => {
        const btn = document.querySelector('[data-archive-claim]');
        return btn !== null && btn.dataset.claimGen !== '1';
      },
      null, { timeout: 5000 },
    );

    assert.deepEqual(pageErrors, [], 'no page errors in legacy claim test');
    await context.close();
  }

  /* ---- Fix 1: popstate during active stream converges URL and UI ---- */
  {
    let releaseStream = () => {};
    const streamGate = new Promise(resolve => { releaseStream = resolve; });

    const context = await browser.newContext();
    const page = await context.newPage();
    const pageErrors = [];
    page.on('pageerror', e => {
      if (!e.message.startsWith('Failed to load resource:')) {
        pageErrors.push(`pageerror: ${e.message}`);
      }
    });
    /* Suppress expected abort-network errors from the held-then-aborted stream */
    page.on('requestfailed', request => {
      if (request.failure()?.errorText === 'net::ERR_ABORTED') return;
      pageErrors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
    });

    await page.route('**/api/auth/session', route => route.fulfill({
      status: 200, contentType: 'application/json',
      body: JSON.stringify(GUEST_SESSION),
    }));
    await page.route('**/api/conversations**', async route => {
      const url = new URL(route.request().url());
      const req = route.request();
      if (url.pathname === '/api/conversations' && req.method() === 'GET') {
        return route.fulfill({
          status: 200, contentType: 'application/json',
          body: JSON.stringify({ conversations: [], cursor: null }),
        });
      }
      if (url.pathname === '/api/conversations' && req.method() === 'POST') {
        return route.fulfill({
          status: 201, contentType: 'application/json',
          body: JSON.stringify({ id: CONV_A }),
        });
      }
      if (url.pathname.endsWith('/turns') && req.method() === 'POST') {
        /* Hold the SSE stream until the test releases it */
        await streamGate;
        try { await route.abort(); } catch { /* request may already be aborted */ }
        return;
      }
      return route.fulfill({
        status: 404, contentType: 'application/json',
        body: JSON.stringify({ error: { message: 'Not found' } }),
      });
    });

    await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
    await page.waitForFunction(
      () => !document.querySelector('mjj-jrpg-composer textarea')?.disabled,
    );

    /* Submit — creates conversation via pushState and starts stream */
    await page.locator('#jrpg-message').fill('routing race test');
    await page.locator('#jrpg-message').press('Enter');

    /* Stream is now active */
    await page.locator('[data-cancel-control]').waitFor({ state: 'visible' });

    const urlDuringStream = page.url();
    assert.ok(
      urlDuringStream.includes('conversation='),
      `Fix 1: URL must have ?conversation during stream: ${urlDuringStream}`,
    );

    /* Navigate back while stream is active — URL already changes here */
    await page.goBack();

    /* Stream abort + pending-popstate application: cancel control disappears */
    await page.locator('[data-cancel-control]').waitFor({ state: 'hidden' });

    /* URL and UI must converge to the pre-stream state */
    const finalUrl = page.url();
    assert.ok(
      !finalUrl.includes('conversation='),
      `Fix 1: URL must not have ?conversation after popstate back: ${finalUrl}`,
    );
    assert.equal(
      await page.locator(
        '.jrpg-message[data-streaming="true"]:not([data-greeting])',
      ).count(),
      0,
      'Fix 1: no streaming message must remain after popstate back',
    );

    releaseStream(); /* clean up the held route */
    assert.deepEqual(pageErrors, [], 'no page errors in popstate-during-stream test');
    await context.close();
  }

  /* ---- Fix 2: openSeq incremented on no-conv popstate, invalidates pending open ---- */
  {
    let resolveConvBFetch = () => {};
    const convBFetchGate = new Promise(resolve => { resolveConvBFetch = resolve; });

    const context = await browser.newContext();
    const page = await context.newPage();
    const pageErrors = [];
    page.on('pageerror', e => {
      if (!e.message.startsWith('Failed to load resource:')) {
        pageErrors.push(`pageerror: ${e.message}`);
      }
    });

    await page.route('**/api/auth/session', route => route.fulfill({
      status: 200, contentType: 'application/json',
      body: JSON.stringify(GUEST_SESSION),
    }));
    await page.route('**/api/conversations**', async route => {
      const url = new URL(route.request().url());
      const req = route.request();
      /* Empty archive: no auto-open, no gate deadlock */
      if (url.pathname === '/api/conversations' && req.method() === 'GET') {
        return route.fulfill({
          status: 200, contentType: 'application/json',
          body: JSON.stringify({ conversations: [], cursor: null }),
        });
      }
      /* The archive-open click will trigger this; hold it with the gate */
      if (url.pathname === `/api/conversations/${CONV_B}` && req.method() === 'GET') {
        await convBFetchGate;
        return route.fulfill({
          status: 200, contentType: 'application/json',
          body: JSON.stringify({ id: CONV_B, title: 'Conv B', turns: [] }),
        });
      }
      return route.fulfill({
        status: 404, contentType: 'application/json',
        body: JSON.stringify({ error: { message: 'Not found' } }),
      });
    });

    /* Empty archive so init completes without auto-opening any conversation */
    await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });

    /* Open archive panel — this pushes a history entry (conversations panel) */
    await page.locator('button[data-preview="conversations"]').click();
    await page.locator('mjj-conversation-archive').waitFor({ state: 'visible' });

    /* Inject Conv B into the archive list without triggering an API fetch */
    await page.evaluate(id => {
      document.querySelector('mjj-conversation-archive')
        ?.addConversation({ id, title: 'Conv B', turn_count: 0 });
    }, CONV_B);

    await page.waitForFunction(
      id => [...document.querySelectorAll('[data-conv-item]')]
        .some(item => item._convId === id),
      CONV_B,
    );

    /* Click Conv B — archive-open fires, fetch held by convBFetchGate */
    await page.evaluate(id => {
      for (const item of document.querySelectorAll('[data-conv-item]')) {
        if (item._convId === id) {
          item.querySelector('[data-conv-open]')?.click();
          break;
        }
      }
    }, CONV_B);

    /* Brief pause so archive-open can increment openSeq and start the fetch */
    await page.waitForTimeout(80);

    /* Navigate back — popstate fires with no-conv state, openSeq is incremented */
    await page.goBack();
    await page.waitForTimeout(80);

    /* Release the held fetch — the archive-open handler should ignore it */
    resolveConvBFetch();
    await page.waitForTimeout(300);

    /* currentConversationId must remain null (no aria-current item) */
    const currentAriaItem = await page.evaluate(() => {
      const btn = document.querySelector('[data-conv-open][aria-current="true"]');
      return btn ? (btn.closest('[data-conv-item]')?._convId ?? null) : null;
    });
    assert.equal(
      currentAriaItem, null,
      'Fix 2: pending archive-open must not overwrite no-conv popstate',
    );

    assert.deepEqual(pageErrors, [], 'no page errors in openSeq test');
    await context.close();
  }
}

(async () => {
  assert.ok(RUNFILES);
  assert.ok(WORKSPACE);
  const runfilesWorkspace = path.join(RUNFILES, WORKSPACE);
  const serverBinary = path.join(
    runfilesWorkspace,
    'mrjunejune/mrjunejune_server_debug',
  );
  const siteRoot = path.join(runfilesWorkspace, 'mrjunejune/src');
  const fakeSidecar = path.join(
    runfilesWorkspace,
    'mrjunejune/test/inference_bridge_fake_sidecar',
  );
  const tempDirectory = fs.mkdtempSync(
    path.join(os.tmpdir(), 'mrjunejune-theme-test-'),
  );
  const chromiumPath = path.resolve(process.env.CHROMIUM_PATH);
  const serverLogs = [];
  let server;
  let browser;

  try {
    port = await findFreePort();
    baseUrl = `http://127.0.0.1:${port}`;
    const pngFiles = listFiles(siteRoot).filter(file => file.endsWith('.png'));
    assert.deepEqual(pngFiles, [], `PNG files leaked into runfiles: ${pngFiles}`);
    for (const file of listFiles(siteRoot).filter(file =>
      file.endsWith('.html')
    )) {
      const source = fs.readFileSync(file, 'utf8');
      for (const primitive of source.matchAll(/<zen-button\b([^>]*)>/g)) {
        assert.match(
          primitive[1],
          /\bsize=["'](?:xs|sm|md|lg|xl)["']/,
          `${path.relative(siteRoot, file)} has an unsized zen-button`,
        );
      }
      assert.deepEqual(
        primitiveOwnershipViolations(source),
        [],
        `${path.relative(siteRoot, file)} violates primitive ownership`,
      );
    }
    for (const file of listFiles(siteRoot).filter(file =>
      /\.(?:css|html|js)$/.test(file) &&
      !file.includes(`${path.sep}public${path.sep}design-system${path.sep}`)
    )) {
      const source = fs.readFileSync(file, 'utf8');
      assert.doesNotMatch(
        source,
        /var\(\s*--zen-/,
        `${path.relative(siteRoot, file)} consumes a deprecated token`,
      );
    }

    server = spawn(serverBinary, [], {
      cwd: runfilesWorkspace,
      env: {
        ...process.env,
        MRJUNEJUNE_PORT: port,
        MRJUNEJUNE_DB_PATH: path.join(tempDirectory, 'site.db'),
        MRJUNEJUNE_INFERENCE_SIDECAR_PATH: fakeSidecar,
        MRJUNEJUNE_COPILOT_CLI_PATH: fakeSidecar,
        MRJUNEJUNE_ALLOW_GUEST_INFERENCE: '1',
      },
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    server.stdout.on('data', chunk => serverLogs.push(chunk.toString()));
    server.stderr.on('data', chunk => serverLogs.push(chunk.toString()));
    await waitForServer(server, serverLogs);

    const home = await (await fetch(baseUrl)).text();
    const dogGame = await (
      await fetch(`${baseUrl}/public/dog-game.js`)
    ).text();
    const manifest = await (
      await fetch(`${baseUrl}/public/manifest.json`)
    ).text();
    const serviceWorker = await (
      await fetch(`${baseUrl}/sw.js`)
    ).text();
    for (const source of [home, dogGame, manifest]) {
      assert.doesNotMatch(source, /\.png(?:["')]|$)/i);
    }
    assert.match(serviceWorker, /v35-role-greeting/);
    assert.match(
      await (
        await fetch(`${baseUrl}/public/pwa-register.js`)
      ).text(),
      /register\('\/sw\.js', \{ scope: '\/' \}\)/,
    );
    assert.ok(serviceWorker.includes("startsWith('/tools/hls_player')"));
    assert.ok(serviceWorker.includes("url.hostname === 'localhost'"));
    assert.ok(serviceWorker.includes("url.hostname === '127.0.0.1'"));
    assert.ok(!serviceWorker.includes("startsWith('/jrpg')"));

    for (const asset of [
      'sprite_shiba0.webp',
      'dog-treat.webp',
      'start-large.webp',
      'icon-192.webp',
      'vscode.webp',
    ]) {
      const response = await fetch(`${baseUrl}/public/${asset}`);
      assert.equal(response.status, 200, asset);
      assert.ok((await response.arrayBuffer()).byteLength > 0, asset);
    }

    browser = await chromium.launch({
      executablePath: chromiumPath,
      headless: true,
      args: ['--no-sandbox'],
    });
    if (runsSuite('core')) {
      const paper = await sampleTheme(browser, 'paper');
      const ink = await sampleTheme(browser, 'ink');
      const playful = await sampleTheme(browser, 'playful');
      const automatic = await sampleTheme(browser, 'auto');
      assert.equal(paper.rootTheme, 'paper');
      assert.equal(ink.rootTheme, 'ink');
      assert.equal(playful.rootTheme, 'playful');
      assert.equal(automatic.rootTheme, 'auto');
      assert.equal(paper.themeLabel, 'Paper');
      assert.equal(ink.themeLabel, 'Ink');
      assert.equal(playful.themeLabel, 'Playful');
      assert.equal(automatic.themeLabel, 'Auto');
      for (const sample of [paper, ink, playful, automatic]) {
        assert.ok(sample.count > 0);
        assert.equal(sample.backgroundRepeat, 'no-repeat');
        assert.equal(sample.backgroundSize, 'cover');
        assert.equal(sample.bodyCoversViewport, true);
        assert.equal(sample.componentReady, true);
        assert.ok(sample.links > 0);
        assert.equal(sample.enhancedLinks, sample.pawLinks);
        assert.match(sample.fontFamily, /More/);
        assert.equal(sample.headerPaws, 0);
        assert.ok(sample.textContrast >= 4.5, JSON.stringify(sample));
        assert.equal(sample.mainBackground, 'rgba(0, 0, 0, 0)');
        assert.equal(sample.themeButtonBackground, 'rgba(0, 0, 0, 0)');
        assert.equal(sample.themeButtonBorder, '0px');
        assert.equal(sample.themeButtonShadow, 'none');
      }
      assert.ok(paper.luminance < 175, JSON.stringify(paper));
      assert.ok(playful.luminance < 175, JSON.stringify(playful));
      assert.ok(ink.luminance > 200, JSON.stringify(ink));
      await testThemeCycle(browser);
      await testPlainField(browser);
      await testButtonScale(browser);
      await testDynamicButtonOwnership(browser);
      await testResumePrint(browser);
    }
    if (runsSuite('jrpg')) await testJrpgPage(browser);
    if (runsSuite('routing')) await testConversationRouting(browser);
    if (runsSuite('greeting')) await testRoleGreetings(browser);
    if (runsSuite('hls')) await testHlsPlayer(browser, siteRoot);
  } finally {
    if (browser) await browser.close();
    await stopProcess(server);
    fs.rmSync(tempDirectory, { recursive: true, force: true });
  }
})().catch(error => {
  console.error(error.stack || error);
  process.exitCode = 1;
});