← Files DataARCHIVED FILE

tests/data-app-theme-contrast.smoke.mjs

11 KB · Sep 30, 2026 · 23:19 UTC

↓ Download file

import assert from "node:assert/strict";
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

import { chromium } from "playwright-core";

import {
  chromeContrastRatio,
  compositeChromeColor,
  parseChromeColor,
} from "../templates/data-app/base/src/chrome-contrast.js";
import { resolveChromiumExecutable, runDataAppFixtureBuild } from "./browser-helpers.mjs";

const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const template = join(pluginRoot, "templates/data-app/base");
const workspace = mkdtempSync(join(tmpdir(), "data-app-theme-contrast-"));
const themes = [
  {
    id: "cobalt-dark-ink",
    surface: "dashboard",
    background: "#1538d4",
    foreground: "#06115f",
    secondary: "#46518a",
    page: "#f7f7f4",
    radius: "0",
    font: "Georgia, serif",
  },
  {
    id: "forest-near-invisible",
    surface: "dashboard",
    background: "#173e2f",
    foreground: "#153b2d",
    secondary: "#5d6f61",
    page: "#f5f0e4",
    radius: "2px",
    font: "Georgia, serif",
  },
  {
    id: "navy-editorial",
    surface: "report",
    background: "#193349",
    foreground: "#f8edda",
    secondary: "#536477",
    page: "#f3ead8",
    radius: "2px",
    font: "Georgia, serif",
  },
  {
    id: "pale-low-contrast",
    surface: "report",
    background: "#e5ecdc",
    foreground: "#d7decf",
    secondary: "#c0c4b7",
    page: "#f5f0e4",
    radius: "3px",
    font: "Courier New, monospace",
  },
  {
    id: "midnight-square-controls",
    surface: "dashboard",
    background: "#060811",
    foreground: "#edf7ff",
    secondary: "#1c2333",
    page: "#060811",
    radius: "0",
    font: "Courier New, monospace",
  },
  {
    id: "coherent-whole-dashboard-dark",
    surface: "dashboard",
    background: "#080b12",
    foreground: "#f4f7fb",
    secondary: "#93a0b7",
    page: "#080b12",
    control: "#151b28",
    radius: "10px",
    font: "Georgia, serif",
    coherent: true,
  },
  {
    id: "light-controls-inside-dark-filter-region",
    surface: "dashboard",
    background: "#ffffff",
    foreground: "#1a1c1f",
    secondary: "#646464",
    page: "#ffffff",
    control: "#ffffff",
    radius: "10px",
    font: "Georgia, serif",
    inherited: "#f3f7ff",
  },
];

function assertContrast(name, foreground, background, minimum = 4.5) {
  const ratio = chromeContrastRatio(parseChromeColor(foreground), parseChromeColor(background));
  assert.ok(ratio >= minimum, `${name} contrast ${ratio.toFixed(2)}:1 is below ${minimum}:1`);
  return Number(ratio.toFixed(2));
}

const browser = await chromium.launch({
  executablePath: resolveChromiumExecutable(),
  headless: true,
});
const results = [];

try {
  for (const theme of themes) {
    const project = join(workspace, theme.id);
    cpSync(template, project, {
      recursive: true,
      filter: (path) => !path.includes("/node_modules") && !path.includes("/dist"),
    });
    const dataPath = join(project, "src/data.json");
    const snapshot = JSON.parse(readFileSync(dataPath, "utf8"));
    snapshot.surface = theme.surface;
    writeFileSync(dataPath, `${JSON.stringify(snapshot, null, 2)}\n`);
    const themePath = join(project, "src/theme.css");
    writeFileSync(
      themePath,
      `${readFileSync(themePath, "utf8")}\n:root {\n` +
        `  --background: ${theme.page};\n` +
        `  --text: ${theme.foreground};\n` +
        `  --secondary: ${theme.secondary};\n` +
        (theme.control ? `  --control: ${theme.control};\n` : "") +
        `  --control-radius: ${theme.radius};\n` +
        `  --font-sans: ${theme.font};\n` +
        `  --data-app-chrome-background: ${theme.background};\n` +
        `  --data-app-chrome-text: ${theme.foreground};\n}\n`,
    );
    if (theme.inherited) {
      const contentStyles = join(project, "src/content/dashboard/dashboard.css");
      writeFileSync(
        contentStyles,
        `${readFileSync(contentStyles, "utf8")}\n` +
          `.filter-bar { color: ${theme.inherited}; background: #0b1220; }\n`,
      );
    }
    const build = runDataAppFixtureBuild(project, { pluginRoot });
    assert.equal(build.status, 0, `${theme.id}: ${build.stdout}\n${build.stderr}`);

    const page = await browser.newPage({
      viewport: { width: 1440, height: 1000 },
    });
    await page.goto(pathToFileURL(join(project, "dist/index.html")).href, {
      waitUntil: "load",
    });
    await page.locator(".dashboard-topbar-title").waitFor();
    const measured = await page.locator('[data-data-app-chrome="topbar"]').evaluate((header) => {
      const get = (selector) => {
        const element = header.querySelector(selector);
        const style = getComputedStyle(element);
        const bounds = element.getBoundingClientRect();
        return {
          color: style.color,
          background: style.backgroundColor,
          radius: style.borderRadius,
          width: bounds.width,
          height: bounds.height,
          font: style.fontFamily,
          x: bounds.x,
          y: bounds.y,
        };
      };
      return {
        background: getComputedStyle(header).backgroundColor,
        underlay: getComputedStyle(document.body).backgroundColor,
        title: get(".dashboard-topbar-title"),
        freshness: get(".freshness"),
        action: get(".dashboard-header-action-button"),
        overflow: get(".dashboard-header-overflow-button"),
        publish: get(".dashboard-publish-button"),
        toggle: get(".topbar-mode-switcher"),
        indicator: get(".topbar-mode-indicator"),
        selected: get('.topbar-mode-switcher button[aria-pressed="true"]'),
        unselected: get('.topbar-mode-switcher button[aria-pressed="false"]'),
      };
    });
    const actualBackground = compositeChromeColor(
      parseChromeColor(measured.background),
      parseChromeColor(measured.underlay),
    );
    const background = `rgb(${actualBackground.slice(0, 3).join(", ")})`;
    const contrast = {};
    for (const item of ["title", "freshness", "action", "overflow"]) {
      contrast[item] = assertContrast(`${theme.id} ${item}`, measured[item].color, background);
    }
    contrast.publish = assertContrast(`${theme.id} Publish`, measured.publish.color, measured.publish.background);
    contrast.publishAgainstHeader = assertContrast(
      `${theme.id} Publish surface`,
      measured.publish.background,
      background,
    );
    contrast.selected = assertContrast(
      `${theme.id} selected icon`,
      measured.selected.color,
      measured.indicator.background,
    );
    contrast.unselected = assertContrast(
      `${theme.id} unselected icon`,
      measured.unselected.color,
      measured.toggle.background,
    );

    if (theme.control) {
      const filters = await page.locator(".filter-trigger").evaluateAll((buttons) =>
        buttons.map((button) => {
          const style = getComputedStyle(button);
          const label = button.querySelector(".filter-label");
          return {
            color: style.color,
            background: style.backgroundColor,
            label: label ? getComputedStyle(label).color : style.color,
          };
        }),
      );
      assert.ok(filters.length > 0, `${theme.id} did not render shared filter controls`);
      filters.forEach((filter, index) => {
        contrast[`filter${index}Value`] = assertContrast(`${theme.id} filter value`, filter.color, filter.background);
        contrast[`filter${index}Label`] = assertContrast(`${theme.id} filter label`, filter.label, filter.background);
      });
    }
    if (theme.coherent) {
      const surfaces = await page.evaluate(() => ({
        body: getComputedStyle(document.body).backgroundColor,
        header: getComputedStyle(document.querySelector(".dashboard-topbar")).backgroundColor,
        main: getComputedStyle(document.querySelector("main")).backgroundColor,
      }));
      const body = parseChromeColor(surfaces.body);
      const header = compositeChromeColor(parseChromeColor(surfaces.header), body);
      const main = compositeChromeColor(parseChromeColor(surfaces.main), body);
      assert.ok(
        chromeContrastRatio(body, header) < 1.5,
        `${theme.id} disconnected protected chrome from the authored dark theme`,
      );
      assert.ok(
        chromeContrastRatio(body, main) < 1.5,
        `${theme.id} left contrasting page gutters around the authored dark theme`,
      );
    }

    assert.deepEqual(
      [measured.toggle.width, measured.toggle.height],
      [84, 32],
      `${theme.id} changed the protected segmented-control geometry`,
    );
    assert.deepEqual([measured.indicator.width, measured.indicator.height], [40, 28]);
    for (const [item, radius] of Object.entries({
      toggle: "12px", indicator: "10px", selected: "10px", unselected: "10px", publish: "12px",
    })) {
      assert.equal(measured[item].radius, radius, `${theme.id} distorted the protected ${item} button shape`);
    }
    for (const item of ["title", "publish", "selected", "unselected", "action"]) {
      assert.match(measured[item].font, /(?:-apple-system|system-ui)/u,
        `${theme.id} should preserve the protected host/system font for ${item}`);
      assert.doesNotMatch(measured[item].font, /OpenAI Sans|Georgia|Courier New/u,
        `${theme.id} leaked custom authored typography into protected ${item}`);
    }
    await page.getByRole("button", { name: "Edit mode" }).click();
    await page.waitForFunction(
      () =>
        document.querySelector('.topbar-mode-switcher button[aria-label="Edit mode"]')?.getAttribute("aria-pressed") ===
        "true",
    );
    await page.waitForFunction(() => {
      const track = document.querySelector(".topbar-mode-switcher");
      const indicator = track?.querySelector(".topbar-mode-indicator");
      return indicator && indicator.getBoundingClientRect().left - track.getBoundingClientRect().left >= 41.9;
    });
    const geometry = await page.locator(".topbar-mode-switcher").evaluate((track) => {
      const outer = track.getBoundingClientRect();
      const indicator = track.querySelector(".topbar-mode-indicator").getBoundingClientRect();
      return {
        left: indicator.left - outer.left,
        right: outer.right - indicator.right,
        top: indicator.top - outer.top,
        bottom: outer.bottom - indicator.bottom,
      };
    });
    for (const [side, expected] of Object.entries({
      left: 42,
      right: 2,
      top: 2,
      bottom: 2,
    })) {
      assert.ok(
        Math.abs(geometry[side] - expected) <= 0.15,
        `${theme.id} clips or misaligns the selected edit-mode indicator: ${JSON.stringify(geometry)}`,
      );
    }
    await page.getByRole("button", { name: "More", exact: true }).click();
    await page.getByRole("menuitem", { name: "Edit theme" }).waitFor();
    results.push({ id: theme.id, surface: theme.surface, contrast, geometry });
    await page.close();
  }
} finally {
  await browser.close();
  rmSync(workspace, { recursive: true, force: true });
}

console.log(JSON.stringify({ status: "passed", themes: results }, null, 2));

SHA-256: 361f8ec8ad68070c4acaf5c57cb8ae78286dcf0ed416b461abd58b3b93bdde1b