← Files DataARCHIVED FILE
tests/data-app-image-tools-browser.smoke.mjs
17.6 KB · Sep 30, 2026 · 23:19 UTC
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 { installDashboardBrowserMocks, resolveChromiumExecutable, runDataAppFixtureBuild } from "./browser-helpers.mjs";
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const template = join(pluginRoot, "templates/data-app/base");
const project = mkdtempSync(join(tmpdir(), "data-image-tools-"));
const unusualId = 'card "quoted"]\\:雪';
const names = ["list_data_app_cards", "get_data_app_card_image", "get_data_app_card_images",
"get_data_app_context", "get_data_app_query_rows", "get_data_app_text"];
const snapshot = {
id: "image-tools-browser-fixture", surface: "dashboard", title: "Synthetic card image fixture",
generatedAt: "2026-08-28T12:00:00Z", filters: [],
queries: {
evidence: {
rows: [{ period: "First", value: 24 }, { period: "Second", value: 62 }, { period: "Third", value: 91 }],
source: { label: "Synthetic reviewed evidence", sql: "SELECT period, value FROM synthetic_fixture",
tables: ["synthetic_fixture"], executedAt: "2026-08-28T12:00:00Z" },
},
},
};
const content = `import React from "react";
import { ChartRenderer, DataComponent, DataTable, MetricCard, useDashboardTabs, useDataApp } from "../../data-app-public.jsx";
export function DashboardContent() {
const { reviewedRows, chartProps } = useDataApp();
const { activeTabId } = useDashboardTabs([{ id: "dashboard", label: "Overview" }, { id: "secondary", label: "Other cards" }]);
const rows = reviewedRows("evidence");
const chart = { type: "bar", x: "period", y: "value", showXAxisLabel: false };
if (activeTabId === "secondary") return <article className="image-fixture" data-fixture-tab="secondary">
<MetricCard id="other-tab-card" title="Other tab evidence" queryId="evidence" value="91" />
</article>;
return <article className="image-fixture" data-fixture-tab="dashboard">
<DataComponent id="chart-card" title="Shared title" queryId="evidence" kind="chart" chart={chart}
variant="card" sourceRows={rows} displayRows={rows}>
<ChartRenderer rows={rows} spec={chart} height={210} {...chartProps("chart-card")} />
</DataComponent>
<div className="image-fixture-pair">
<MetricCard id="metric-card" title="Shared title" queryId="evidence" value="91"
comparison="+29" trendValues={[24, 62, 91]} sourceRows={rows} />
<DataComponent id="table-card" title="Shared title" queryId="evidence" kind="table" variant="card"
sourceRows={rows} displayRows={rows}>
<DataTable rows={rows} columns={[{ field: "period", label: "Period" }, { field: "value", label: "Count" }]} />
</DataComponent>
</div>
<div hidden><MetricCard id="hidden-card" title="Hidden evidence" queryId="evidence" value="91" /></div>
<div className="image-fixture-offscreen">
<DataComponent id={${JSON.stringify(unusualId)}} title="Offscreen exact ID" queryId="evidence"
kind="custom" variant="card" sourceRows={rows}>
<p data-reviewed-rows>Reviewed count: <strong>91</strong></p>
<div data-fixture-scroll style={{ height: 40, overflow: "auto" }}>
<div style={{ height: 120 }}>Reviewed periods: First 24, Second 62, Third 91.</div>
</div>
</DataComponent>
</div>
<div className="image-fixture-pair">
<MetricCard id="duplicate-id" title="First duplicate" queryId="evidence" value="24" />
<MetricCard id="duplicate-id" title="Second duplicate" queryId="evidence" value="91" />
</div>
<svg width="0" height="0" aria-hidden="true"><defs>
<linearGradient id="outside-card-gradient"><stop offset="0" stopColor="red" /><stop offset="1" stopColor="red" /></linearGradient>
</defs></svg>
<DataComponent id="external-defs-card" title="Evidence with a shared paint definition" queryId="evidence"
kind="custom" variant="card">
<svg width="200" height="100"><rect width="200" height="100" fill="url(#outside-card-gradient)" /></svg>
</DataComponent>
</article>;
}`;
async function installTools(page) {
await installDashboardBrowserMocks(page);
await page.addInitScript(() => {
window.__imageTools = new Map();
window.__imageToolFetches = [];
window.__imageToolDownloads = [];
Object.defineProperty(document, "modelContext", { configurable: true, value: {
registerTool(tool, { signal } = {}) {
if (signal?.aborted) return;
window.__imageTools.set(tool.name, tool);
signal?.addEventListener("abort", () => {
if (window.__imageTools.get(tool.name) === tool) window.__imageTools.delete(tool.name);
}, { once: true });
},
} });
const fetch = window.fetch;
window.fetch = function (...args) {
window.__imageToolFetches.push({ url: String(args[0]), method: args[1]?.method ?? "GET" });
return fetch.apply(this, args);
};
const click = HTMLAnchorElement.prototype.click;
HTMLAnchorElement.prototype.click = function () {
if (this.download) window.__imageToolDownloads.push(this.download);
return click.call(this);
};
});
}
const invoke = (page, name, input = {}) => page.evaluate(({ toolName, args }) =>
window.__imageTools.get(toolName).execute(args), { toolName: name, args: input });
async function rejectTool(page, name, input, expected) {
const result = await page.evaluate(async ({ toolName, args }) => {
try { await window.__imageTools.get(toolName).execute(args); return { succeeded: true }; }
catch (error) { return { error: String(error.message) }; }
}, { toolName: name, args: input });
assert.match(result.error ?? "Unexpected successful export", expected);
}
async function readerState(page) {
return page.evaluate(() => ({
url: location.href, scroll: [scrollX, scrollY], tab: document.querySelector("[data-fixture-tab]")?.dataset.fixtureTab,
focus: { tag: document.activeElement?.tagName, id: document.activeElement?.id,
label: document.activeElement?.getAttribute("aria-label") },
storage: Object.entries(localStorage).sort(([left], [right]) => left.localeCompare(right)),
fetches: window.__imageToolFetches, downloads: window.__imageToolDownloads,
clipboard: window.__dashboardClipboard, prompts: window.__dashboardPrompts,
}));
}
async function inspectPng(page, result, { scale = 2, bodySelector } = {}) {
assert.equal(result.mimeType, "image/png");
assert.equal(result.encoding, "base64");
assert.deepEqual(result.queryIds, ["evidence"]);
assert.match(result.filename, /^[a-z0-9-]+\.png$/iu);
const bytes = Buffer.from(result.data, "base64");
assert.deepEqual([...bytes.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]);
assert.equal(bytes.readUInt32BE(16), result.width);
assert.equal(bytes.readUInt32BE(20), result.height);
const pixels = await page.evaluate(async ({ imageResult, imageScale, contentSelector }) => {
const card = [...document.querySelectorAll("main [data-component-id]")]
.find((element) => element.dataset.componentId === imageResult.cardId);
const bounds = card.getBoundingClientRect();
const image = new Image();
image.src = `data:image/png;base64,${imageResult.data}`;
await image.decode();
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
function regionStats(rect) {
const left = Math.max(0, Math.floor((rect.left - bounds.left) * imageScale));
const top = Math.max(0, Math.floor((rect.top - bounds.top) * imageScale));
const width = Math.min(canvas.width - left, Math.max(1, Math.floor(rect.width * imageScale)));
const height = Math.min(canvas.height - top, Math.max(1, Math.floor(rect.height * imageScale)));
const data = context.getImageData(left, top, width, height).data;
let opaque = 0;
let minimum = 255;
let maximum = 0;
let brightness = 0;
for (let index = 0; index < data.length; index += 4) {
if (data[index + 3] < 200) continue;
opaque += 1;
const value = (data[index] + data[index + 1] + data[index + 2]) / 3;
minimum = Math.min(minimum, value);
maximum = Math.max(maximum, value);
brightness += value;
}
return { opaque, range: maximum - minimum, brightness: opaque ? brightness / opaque : 0 };
}
return {
expectedWidth: Math.ceil(bounds.width * imageScale), expectedHeight: Math.ceil(bounds.height * imageScale),
whole: regionStats(bounds),
title: regionStats(card.querySelector(".component-title-text").getBoundingClientRect()),
body: contentSelector ? regionStats(card.querySelector(contentSelector).getBoundingClientRect()) : null,
};
}, { imageResult: result, imageScale: scale, contentSelector: bodySelector });
assert.equal(result.width, pixels.expectedWidth, "PNG width matches the exact card at requested scale");
assert.equal(result.height, pixels.expectedHeight, "PNG height matches the exact card at requested scale");
for (const [region, stats] of Object.entries({ whole: pixels.whole, title: pixels.title, ...(pixels.body && { body: pixels.body }) })) {
assert.ok(stats.opaque > 20 && stats.range > 30,
`${result.cardId} ${region} must contain visible evidence, not a blank/transparent rectangle: ${JSON.stringify(stats)}`);
}
return pixels;
}
async function ready(page) {
await page.waitForFunction((toolNames) => toolNames.every((name) => window.__imageTools.has(name)), names);
await page.locator('[data-component-id="chart-card"] .recharts-surface').waitFor();
await page.evaluate(async () => {
await document.fonts.ready;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
});
}
let browser;
try {
cpSync(template, project, { recursive: true,
filter: (path) => !path.includes("/node_modules") && !path.includes("/dist") });
writeFileSync(join(project, "src/data.json"), `${JSON.stringify(snapshot, null, 2)}\n`);
writeFileSync(join(project, "src/content/dashboard/DashboardContent.jsx"), content);
writeFileSync(join(project, "src/content/dashboard/dashboard.css"), `
.image-fixture { display: grid; gap: 24px; max-width: 680px; padding-block: 24px; }
.image-fixture-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
.image-fixture-offscreen { margin-top: 1000px; }
`);
const build = runDataAppFixtureBuild(project, { pluginRoot });
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
const html = readFileSync(join(project, "dist/index.html"), "utf8");
browser = await chromium.launch({ executablePath: resolveChromiumExecutable(), headless: true });
const failures = [];
const local = await browser.newPage({ viewport: { width: 1100, height: 850 }, colorScheme: "light" });
local.on("pageerror", (error) => failures.push(error.message));
await installTools(local);
await local.goto(pathToFileURL(join(project, "dist/index.html")).href, { waitUntil: "load" });
await ready(local);
// An unrelated element elsewhere in the host document cannot shadow a card.
await local.evaluate(() => {
const unrelated = document.createElement("section");
unrelated.dataset.componentId = "chart-card";
unrelated.textContent = "OUTSIDE_DATA_APP_ROOT";
document.body.prepend(unrelated);
});
const before = await readerState(local);
const renderedText = await invoke(local, "get_data_app_text", { id: "chart-card" });
assert.ok(renderedText.text.includes("Shared title"));
assert.doesNotMatch(renderedText.text, /OUTSIDE_DATA_APP_ROOT/u,
"Text retrieval is scoped to the Data app root");
await rejectTool(local, "get_data_app_text", { id: "hidden-card" }, /not visible/iu);
await rejectTool(local, "get_data_app_text", { id: "duplicate-id" }, /ambiguous/iu);
await rejectTool(local, "get_data_app_text", { id: "other-tab-card" }, /not visible/iu);
const listed = await invoke(local, names[0]);
assert.deepEqual(listed.cards.filter(({ title }) => title === "Shared title").map(({ cardId }) => cardId),
["chart-card", "metric-card", "table-card"], "Duplicate titles retain independent stable IDs");
assert.equal(listed.view.tabId, "dashboard");
assert.ok(!listed.cards.some(({ cardId }) => cardId === "other-tab-card"), "Unmounted tabs are not discovered");
assert.equal(listed.cards.find(({ cardId }) => cardId === "hidden-card").imageAvailable, false);
assert.equal(listed.cards.filter(({ cardId }) => cardId === "duplicate-id").length, 2);
assert.ok(listed.cards.filter(({ cardId }) => cardId === "duplicate-id").every(({ imageAvailable }) => !imageAvailable));
assert.equal(listed.cards.find(({ cardId }) => cardId === unusualId).imageAvailable, true);
const chart = await invoke(local, names[1], { cardId: "chart-card" });
assert.equal(chart.title, "Shared title");
assert.deepEqual(chart.view, listed.view);
const lightChart = await inspectPng(local, chart, { bodySelector: ".recharts-wrapper" });
const batch = await invoke(local, names[2], { cardIds: ["table-card", "metric-card", unusualId], scale: 1 });
assert.deepEqual(batch.images.map(({ cardId }) => cardId), ["table-card", "metric-card", unusualId]);
assert.deepEqual(batch.view, listed.view);
for (const [index, selector] of ["tbody", ".metric-value", "[data-reviewed-rows]"].entries()) {
await inspectPng(local, batch.images[index], { scale: 1, bodySelector: selector });
}
assert.equal(await local.evaluate((id) => [...document.querySelectorAll("main [data-component-id]")]
.find((element) => element.dataset.componentId === id).getBoundingClientRect().top > innerHeight, unusualId), true,
"The exact-ID export also captures a mounted card below the viewport");
for (const [input, error] of [
[{ cardId: "Shared title" }, /not mounted/iu],
[{ cardId: "other-tab-card" }, /not mounted/iu],
[{ cardId: "hidden-card" }, /hidden/iu],
[{ cardId: "duplicate-id" }, /ambiguous/iu],
[{ cardId: "external-defs-card" }, /definitions|references|embedded/iu],
[{ cardId: "" }, /exact cardId/iu],
[{ cardId: "chart-card", scale: 0 }, /scale/iu],
[{ cardId: "chart-card", extra: true }, /invalid/iu],
]) await rejectTool(local, names[1], input, error);
await rejectTool(local, names[2], { cardIds: ["chart-card", "missing-card"] }, /not mounted/iu);
await rejectTool(local, names[2], { cardIds: ["chart-card", "chart-card"] }, /unique/iu);
assert.deepEqual(await readerState(local), before,
"Discovery, single/batch exports and rejected requests preserve focus, view, storage and all external side effects");
await local.locator("[data-fixture-scroll]").evaluate((element) => { element.scrollTop = 30; });
await rejectTool(local, names[1], { cardId: unusualId }, /scroll/iu);
assert.equal(await local.locator("[data-fixture-scroll]").evaluate((element) => element.scrollTop), 30,
"Unsupported nested scrolling fails explicitly without changing the reader's scroll position");
await local.getByRole("tab", { name: "Other cards", exact: true }).click();
await local.locator('[data-component-id="other-tab-card"]').waitFor();
const nextView = await invoke(local, names[0]);
assert.deepEqual(nextView.cards.map(({ cardId }) => cardId), ["other-tab-card"]);
assert.equal(nextView.view.tabId, "secondary");
await rejectTool(local, names[1], { cardId: "chart-card" }, /not mounted/iu);
const viewer = await browser.newPage({ viewport: { width: 1100, height: 850 }, colorScheme: "light" });
viewer.on("pageerror", (error) => failures.push(error.message));
await installTools(viewer);
const writes = [];
await viewer.route("https://card-image-fixture.chatgpt.site/**", async (route) => {
const request = route.request();
if (request.method() !== "GET") writes.push({ method: request.method(), url: request.url() });
const { pathname } = new URL(request.url());
const json = pathname === "/api/snapshot" ? snapshot : pathname === "/api/presentation"
? { canEdit: false, revision: 0, presentation: {
appearance: "dark", componentTitles: { "chart-card": "Saved viewer chart title" },
} } : null;
return route.fulfill({ contentType: json ? "application/json" : "text/html", body: json ? JSON.stringify(json) : html });
});
await viewer.goto("https://card-image-fixture.chatgpt.site/", { waitUntil: "load" });
await ready(viewer);
assert.deepEqual(await viewer.evaluate(() => [...window.__imageTools.keys()]), names,
"Hosted viewers receive image readers without query mutation tools");
assert.equal(await viewer.getByRole("button", { name: "Edit text and layout", exact: true }).count(), 0);
const viewerBefore = await readerState(viewer);
const savedTitleImage = await invoke(viewer, names[1], { cardId: "chart-card" });
assert.equal(savedTitleImage.title, "Saved viewer chart title", "Stable IDs survive saved title edits");
const darkChart = await inspectPng(viewer, savedTitleImage, { bodySelector: ".recharts-wrapper" });
assert.ok(lightChart.whole.brightness - darkChart.whole.brightness > 80,
"The exported card retains the viewer's current dark surface and readable title/chart colors");
assert.deepEqual(await readerState(viewer), viewerBefore, "Viewer export has no write, clipboard, download or navigation effects");
assert.deepEqual(writes, []);
assert.deepEqual(failures, []);
console.log("Data app image tools: real chart/table/KPI/custom PNGs, exact IDs, current view, read-only viewer and dark-theme checks passed.");
} finally {
await browser?.close();
rmSync(project, { recursive: true, force: true });
}
SHA-256: 7660df0dfda71a8bab9769b5c7aa0e86a5bd5550105d8ff3bc8f1794044da29b