← Files DataARCHIVED FILE

tests/data-app-table-delta-browser.smoke.mjs

8.14 KB · Oct 3, 2026 · 06:03 UTC

↓ Download file

import assert from "node:assert/strict";
import { cpSync, mkdtempSync, 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 { resolveChromiumExecutable, runDataAppFixtureBuild } from "./browser-helpers.mjs";
import { chromeContrastRatio, parseChromeColor } from "../templates/data-app/base/src/chrome-contrast.js";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const template = join(root, "templates/data-app/base");
const project = mkdtempSync(join(tmpdir(), "data-app-table-delta-"));
const rows = [
  { metric: "Adoption", change: "+12.34%", tone: "positive" },
  { metric: "Reach", change: "+0.0007%", tone: "neutral" },
  { metric: "Lower cost", change: "−4.25%", tone: "positive" },
  { metric: "More errors", change: "+8.12%", tone: "negative" },
  { metric: "Trade-off", change: "−0.45%", tone: "neutral" },
  { metric: "Unknown", change: "+1.00%", tone: "invalid" },
  { metric: "Broken rule", change: "+2.00%", tone: "throws" },
  { metric: "No change", change: "0.00%", tone: "neutral" },
  { metric: "Last page", change: "−3.00%", tone: "negative" },
];
const authored = `import React from "react";
import { Chart, DataTable } from "../../data-app-public.jsx";
const rows = ${JSON.stringify(rows)};
const columns = [{field:"metric",label:"Metric"},{field:"change",label:"Relative change",
  deltaTone: (value,row) => {
    (window.__deltaToneCalls ??= []).push({metric:row.metric,value});
    if(row.tone === "throws") throw new Error("Bad color rule"); return row.tone;
  }}];
export function ReportContent(){return <article className="report-content"><h1>Delta presentation fixture</h1>
<DataTable rows={rows} columns={columns} signedDeltas label="Semantic deltas" />
<section id="explicit-neutral"><Chart height={180} spec={{type:"horizontalBar",x:"category",y:"change",colorBySign:false,
  colors:{A:"var(--secondary)",B:"var(--secondary)"},showLegend:false,showXAxisLabel:false,showYAxisLabel:false}}
  rows={[{category:"A",change:-2},{category:"B",change:3}]} /></section>
<section id="all-negative"><Chart height={180} spec={{type:"horizontalBar",x:"category",y:"change",colorBySign:true,
  startAtZero:true,showLegend:false,showXAxisLabel:false,showYAxisLabel:false}}
  rows={[{category:"A",change:-2},{category:"B",change:-3}]} /></section>
</article>;}`;
let browser;
try {
  cpSync(template, project, { recursive: true, filter: (path) => !path.includes("/node_modules") && !path.includes("/dist") });
  writeFileSync(join(project, "src/content/report/ReportContent.jsx"), authored);
  writeFileSync(join(project, "src/data.json"), JSON.stringify({ id: "delta-tone-contract", surface: "report",
    title: "Delta presentation fixture", generatedAt: "2026-08-20T12:00:00Z", filters: [], queries: {} }));
  const build = runDataAppFixtureBuild(project, { pluginRoot: root });
  assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
  browser = await chromium.launch({ executablePath: resolveChromiumExecutable(), headless: true });
  const errors = [];
  for (const colorScheme of ["light", "dark"]) for (const width of [1100, 390]) {
    const context = await browser.newContext({ colorScheme, viewport: { width, height: 900 } });
    const page = await context.newPage();
    page.on("pageerror", error => errors.push(error.message));
    await page.goto(pathToFileURL(join(project, "dist/index.html")).href);
    const table = page.getByRole("table", { name: "Semantic deltas", exact: true });
    await table.waitFor();
    await page.waitForFunction(scheme => document.documentElement.dataset.colorScheme === scheme, colorScheme);
    for (const [id, fill] of [["explicit-neutral", "var(--secondary)"], ["all-negative", "var(--negative)"]]) {
      const bars = page.locator(`#${id} .recharts-bar-rectangle path`);
      await bars.nth(1).waitFor();
      const rendered = await bars.evaluateAll(elements => elements.map(element => ({
        fill: element.getAttribute("fill"), width: element.getBoundingClientRect().width,
      })));
      assert.equal(rendered.length, 2, "All-negative bars retain a zero baseline and remain visible");
      assert.ok(rendered.every(bar => bar.fill === fill && bar.width > 0), "Explicit semantic color overrides survive rendering");
    }
    const checkVisible = async () => {
      const cells = await table.locator("tbody tr").evaluateAll(elements => elements.map(element => {
        const [metric, change] = element.querySelectorAll("td");
        const probe = document.createElement("span");
        probe.style.color = change.dataset.delta === "positive" ? "var(--movement-positive)"
          : change.dataset.delta === "negative" ? "var(--movement-negative)" : "var(--secondary)";
        change.append(probe);
        const tokenColor = getComputedStyle(probe).color;
        probe.style.color = "var(--background)";
        const background = getComputedStyle(probe).color;
        probe.remove();
        return { metric: metric.textContent, change: change.textContent,
          tone: change.dataset.delta, color: getComputedStyle(change).color, tokenColor, background };
      }));
      for (const cell of cells) {
        const expected = rows.find(row => row.metric === cell.metric);
        assert.equal(cell.change, expected.change, "Color must not rewrite the signed value");
        assert.equal(cell.tone, ["positive", "negative"].includes(expected.tone) ? expected.tone : "neutral");
        assert.equal(cell.color, cell.tokenColor, "Semantic colors use the active theme; neutral retains secondary text");
        assert.ok(chromeContrastRatio(parseChromeColor(cell.color), parseChromeColor(cell.background)) >= 4.5,
          `${colorScheme} ${cell.tone} delta text must meet 4.5:1 contrast`);
      }
      return cells;
    };
    assert.equal((await checkVisible()).length, 8);
    await page.getByRole("button", { name: "Next page", exact: true }).click();
    assert.equal((await checkVisible())[0].metric, "Last page");
    await page.getByRole("button", { name: "Previous page", exact: true }).click();
    for (let direction = 0; direction < 2; direction++) {
      await table.getByRole("button", { name: /^Metric/u }).click();
      await checkVisible();
      await page.getByRole("button", { name: "Next page", exact: true }).click();
      await checkVisible();
      await page.getByRole("button", { name: "Previous page", exact: true }).click();
    }
    const ascending = ["Lower cost", "Last page", "Trade-off", "No change", "Reach",
      "Unknown", "Broken rule", "More errors", "Adoption"];
    for (const expected of [ascending, [...ascending].reverse()]) {
      await table.getByRole("button", { name: /^Relative change/u }).click();
      const firstPage = await checkVisible();
      await page.getByRole("button", { name: "Next page", exact: true }).click();
      const secondPage = await checkVisible();
      assert.deepEqual([...firstPage, ...secondPage].map(cell => cell.metric), expected,
        "Signed percentages sort by numeric value in both directions across pages");
      await page.getByRole("button", { name: "Previous page", exact: true }).click();
    }
    await page.getByRole("textbox", { name: "Search data", exact: true }).fill("Lower cost");
    assert.equal((await checkVisible())[0].tone, "positive", "A negative value can represent an improvement");
    await page.getByRole("textbox", { name: "Search data", exact: true }).fill("");
    await page.emulateMedia({ media: "print" });
    await checkVisible();
    const deltaToneCalls = await page.evaluate(() => window.__deltaToneCalls);
    assert.ok(deltaToneCalls.length > 0);
    for (const call of deltaToneCalls) assert.equal(call.value, rows.find(row => row.metric === call.metric).change,
      "Delta callbacks receive the original reviewed value, not a numeric sort key or formatted replacement");
    await context.close();
  }
  assert.deepEqual(errors, []);
  console.log("Shared delta tones pass desktop/mobile light/dark, numeric sorting, original-value callbacks, pagination, search, and print-media checks.");
} finally {
  await browser?.close();
  rmSync(project, { recursive: true, force: true });
}

SHA-256: fa6df254fbb43d27345a0dcaee335023d5926448d23b4d509128b5ca24a9b9be