← Files DataARCHIVED FILE
tests/data-app-browser-exports.mjs
34.5 KB · Sep 30, 2026 · 23:19 UTC
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
import {
verifyEditorAskChatGPTSelectionModes,
verifyLocalAskChatGPTHandoff,
verifyViewerAskChatGPTHandoff,
} from "./data-app-browser-ask-chatgpt.mjs";
import { chooseHostedHandoff, verifyHostedHandoffChoices } from "./data-app-browser-handoff.mjs";
async function openConvertMenu(page) {
return ensureOverflowMenu(page);
}
async function selectConversion(page, name) {
const menu = await openConvertMenu(page);
const item = menu.getByRole("menuitem", { name, exact: true });
assert.equal(await item.evaluate(element => element.tagName), "A", "Exports must use genuine handoff links");
await item.focus();
await item.press("Enter");
}
async function ensureActionComposer(page) {
const composer = page.getByRole("dialog", { name: "Ask ChatGPT about this dashboard" });
if (!(await composer.isVisible())) await page.locator(".dashboard-ask-button").click();
return composer;
}
async function ensureOverflowMenu(page) {
const menu = page.getByRole("menu", { name: "More", exact: true });
if (!(await menu.isVisible())) await page.getByRole("button", { name: "More", exact: true }).click();
return menu;
}
async function verifyPromptAnchor(control, { protocol = "codex:", target = "_self" } = {}) {
const anchor = await control.evaluate((element) => ({
tagName: element.tagName,
href: element.getAttribute("href"),
target: element.getAttribute("target"),
}));
assert.equal(anchor.tagName, "A", "ChatGPT-bound actions must be real, user-activated hyperlinks");
assert.equal(anchor.target, target, "The navigation target must match the current ChatGPT surface");
const href = new URL(anchor.href);
assert.equal(href.protocol, protocol);
return href;
}
function verifyPdfHandoff(href, { viewUrl } = {}) {
const prompt = href.searchParams.get("prompt") ?? href.searchParams.get("q");
assert.match(prompt, /\[@Data\]\(plugin:\/\/data-analytics@openai-curated-remote\)/u);
assert.match(prompt, /\$data-analytics:report-to-pdf\b/u);
assert.match(prompt, /verified PDF\b/u);
if (viewUrl) {
assert.ok(prompt.includes(`](<${viewUrl}>)`), "PDF handoffs must preserve the exact selected view");
if (href.protocol === "codex:") assert.equal(href.searchParams.get("browserUrl"), viewUrl);
assert.match(prompt, /read its current Data app context/u);
}
}
function verifyPublicationPrompt(prompt, audience) {
assert.match(prompt, /\[@Sites\]\(plugin:\/\/sites@openai-curated-remote\)/u);
assert.match(prompt, /publish this dashboard/u);
assert.doesNotMatch(prompt, /read-only and does not authorize|confirmation before publishing|Unknown schedule state|permissions remain unresolved/u);
assert.match(prompt, audience === "workspace_all"
? /workspace members with the link/u
: /access limited to me until I invite others/u);
}
export async function verifyCodexEditorDashboardActions(page, { originatingThreadId }) {
assert.doesNotMatch(await page.evaluate(() => navigator.userAgent), /(?:Codex|ChatGPT)Browser/u,
"The in-app browser marker must work with the browser's ordinary user agent");
const originalMarker = await page.evaluateHandle(() => {
const descriptor = Object.getOwnPropertyDescriptor(window, "__codexBrowser");
Object.defineProperty(window, "__codexBrowser", { configurable: true, value: true });
return descriptor;
});
try {
const prompts = await verifyEditorDashboardExports(page);
await verifyEditorDashboardPublishing(page, { originatingThreadId, promptCount: prompts.length });
} finally {
await page.evaluate((descriptor) => {
if (descriptor) Object.defineProperty(window, "__codexBrowser", descriptor);
else delete window.__codexBrowser;
}, originalMarker);
await originalMarker.dispose();
}
}
async function verifyEditorDashboardExports(page) {
await page.getByRole("button", { name: "Refresh data" }).click();
const refreshAction = page.getByRole("menuitem", { name: "Refresh now" });
await verifyPromptAnchor(refreshAction);
assert.notEqual(
await page.getByRole("menuitem", { name: "Schedule refresh" }).evaluate((element) => element.tagName),
"A",
"Opening the schedule dialog must remain a local interaction",
);
await refreshAction.click();
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 1);
const actionMenu = await ensureActionComposer(page);
for (const label of ["Share key insights", "Create a change alert", "Create a report", "Change this dashboard"]) {
await verifyPromptAnchor(actionMenu.getByRole("link", { name: label }));
}
const editRequest = new URL(await actionMenu.getByRole("link", { name: "Change this dashboard" }).getAttribute("href"));
assert.match(editRequest.searchParams.get("prompt"), /Verify that I own the original/u);
assert.match(editRequest.searchParams.get("prompt"), /opening this action alone does not authorize edits/u);
assert.equal(await actionMenu.getByRole("button", { name: "Switch theme" }).count(), 0);
const report = actionMenu.getByRole("link", { name: "Create a report" });
await report.focus();
await report.press("Enter");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 2);
await actionMenu.waitFor({ state: "hidden" });
assert.equal(await actionMenu.count(), 0, "Selecting an Ask suggestion closes the composer after native link activation");
const overflowMenu = await ensureOverflowMenu(page);
assert.notEqual(
await overflowMenu.getByRole("menuitem", { name: "Switch theme" }).evaluate((element) => element.tagName),
"A",
"Theme customization must remain local instead of opening ChatGPT",
);
const copy = overflowMenu.getByRole("menuitem", { name: "Create a copy" });
await verifyPromptAnchor(copy);
await copy.focus();
await copy.press("Enter");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 3);
await page.keyboard.press("Escape");
const convertMenu = await openConvertMenu(page);
const conversionItems = convertMenu.locator(".dashboard-convert-menu-item");
const exportLabels = await conversionItems.allInnerTexts();
for (const label of exportLabels) {
const conversion = convertMenu.getByRole("menuitem", { name: label, exact: true });
await verifyPromptAnchor(conversion);
const href = new URL(await conversion.getAttribute("href"));
const prompt = href.searchParams.get("prompt") ?? href.searchParams.get("q");
assert.deepEqual(prompt.match(/\[@[^\]]+\]\(plugin:\/\/[^)]+\)/gu), [
"[@Data](plugin://data-analytics@openai-curated-remote)",
], `${label} export should prefill only the Data plugin`);
assert.doesNotMatch(prompt, /@(?:Documents|Presentations)\b/u);
}
const notebookIndex = exportLabels.indexOf("Jupyter Notebook");
assert.notEqual(notebookIndex, -1, "Jupyter Notebook must be offered in the export menu");
const notebookExport = convertMenu.getByRole("menuitem", {
name: "Jupyter Notebook",
exact: true,
});
assert.equal(
await notebookExport.locator(".dashboard-convert-icon").count(),
1,
"Jupyter Notebook must use its dedicated conversion icon",
);
await notebookExport.focus();
await notebookExport.press("Enter");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 4);
await selectConversion(page, "PDF");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 5);
verifyPdfHandoff(new URL(await page.evaluate(() => window.__dashboardDeepLinks.at(-1))));
assert.equal(
await page.evaluate(() => window.__dashboardPrints),
0,
"Dashboard PDF export must hand off without invoking browser print",
);
const prompts = (await page.evaluate(() => window.__dashboardDeepLinks)).map((href) =>
new URL(href).searchParams.get("prompt"),
);
assert.equal(prompts.length, 5, "Refresh, report creation, copying, notebook and PDF export must each activate exactly one real link");
assert.match(prompts[0], /"segment": "Studio"/);
assert.match(
prompts[0],
/"activationLift":/,
"Refresh requests must preserve the viewer's current scenario assumptions",
);
assert.match(prompts[0], /"chartOverrides"/);
assert.match(prompts[1], /new, separate report/i);
assert.match(prompts[1], /Never modify or overwrite the original Data app/i);
assert.match(prompts[1], /Do not query new data, refresh the source, publish, send, or change access unless I explicitly ask/u);
assert.doesNotMatch(prompts[1], /"activeUsers":/);
assert.match(prompts[2], /new, separate dashboard/i);
assert.match(prompts[2], /Never modify or overwrite the original dashboard/i);
assert.doesNotMatch(prompts[2], /"activeUsers":/);
assert.doesNotMatch(prompts[0], /"activeUsers":/);
const notebookPrompt = prompts[3];
assert.match(notebookPrompt, /\$data-analytics:jupyter-notebooks\b/u);
assert.match(notebookPrompt, /\.ipynb\b/u);
assert.match(notebookPrompt, /"segment": "Studio"/u, "Notebook export must preserve the viewer's current filters");
assert.match(
notebookPrompt,
/"activationLift":/u,
"Notebook export must preserve the viewer's current scenario assumptions",
);
assert.match(notebookPrompt, /"componentTitles":/u, "Notebook export must preserve edited presentation details");
assert.match(notebookPrompt, /"chartOverrides":/u, "Notebook export must preserve current chart presentation");
assert.doesNotMatch(
notebookPrompt,
/"(?:rows|sourceRows|displayRows|queries|activeUsers)"\s*:/u,
"Notebook export must not embed reviewed rows in its ChatGPT handoff",
);
const pdfPrompt = prompts[4];
assert.match(pdfPrompt, /\$data-analytics:report-to-pdf\b/u);
assert.match(pdfPrompt, /"segment": "Studio"/u, "PDF export must preserve the viewer's current filters");
assert.match(pdfPrompt, /"activationLift":/u, "PDF export must preserve current scenario assumptions");
assert.match(pdfPrompt, /"componentTitles":/u, "PDF export must preserve edited presentation details");
assert.match(pdfPrompt, /"chartOverrides":/u, "PDF export must preserve current chart presentation");
assert.doesNotMatch(pdfPrompt, /"(?:rows|sourceRows|displayRows|queries|activeUsers)"\s*:/u,
"PDF export must not embed reviewed rows in its ChatGPT handoff");
assert.deepEqual(
await page.evaluate(() => window.__dashboardPrompts),
[],
"Menu handoffs must activate trusted links instead of invoking an available host-message bridge",
);
assert.equal(
await page.getByText("Request sent to the host.", { exact: true }).count(),
0,
"Accepted app handoffs must not claim that work was sent to a host",
);
assert.deepEqual(
await page.evaluate(() => window.__dashboardNavigationEvents),
Array.from({ length: 5 }, () => ({
connected: true,
trusted: true,
defaultPrevented: false,
target: "_self",
})),
"Native mouse and keyboard links must remain mounted and unprevented through document bubble",
);
await page.getByRole("button", { name: "Refresh data" }).click();
await page.getByRole("menuitem", { name: "Schedule refresh" }).click();
const scheduleDialog = page.getByRole("dialog", { name: "Schedule refresh" });
const scheduledAction = scheduleDialog.locator(".dashboard-schedule-submit");
const validSchedule = await verifyPromptAnchor(scheduledAction);
assert.match(validSchedule.searchParams.get("prompt"), /every weekday at 09:00/u);
await scheduleDialog.getByRole("button", { name: "Repeat schedule" }).click();
await page.getByRole("menuitemradio", { name: "Custom days" }).click();
for (const day of ["Monday", "Wednesday", "Friday"]) {
await scheduleDialog.getByRole("button", { name: day }).click();
}
assert.equal(await scheduledAction.evaluate((element) => element.tagName), "BUTTON");
assert.equal(await scheduledAction.isDisabled(), true, "Invalid custom schedules must not expose active hyperlinks");
await scheduleDialog.getByRole("button", { name: "Monday" }).click();
const customSchedule = await verifyPromptAnchor(scheduledAction);
assert.match(customSchedule.searchParams.get("prompt"), /every Monday at 09:00/u);
await scheduledAction.focus();
await scheduledAction.press("Enter");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 6);
await page.keyboard.press("Escape");
return [];
}
async function verifyEditorDashboardPublishing(page, { originatingThreadId, promptCount }) {
await page.getByRole("button", { name: "Publish", exact: true }).click();
const review = page.getByRole("dialog", { name: "Publish dashboard", exact: true });
assert.equal(
await review.getByRole("radio", { name: "Invited people" }).isChecked(),
true,
"Initial publication must default to private access",
);
assert.equal(
await review.getByRole("radio", { name: "Workspace members" }).isChecked(),
false,
"Workspace sharing must be an explicit choice",
);
const confirmation = review.getByRole("link", { name: "Publish in ChatGPT" });
assert.equal(await review.getByRole("button", { name: "Cancel", exact: true }).count(), 0);
assert.equal(
await confirmation.evaluate((element) => element.tagName),
"A",
"Publication must use a native link activated directly by the user's click",
);
assert.equal(await confirmation.getAttribute("target"), "_self", "Native Codex publication must not open a new tab");
const privateLink = new URL(await confirmation.getAttribute("href"));
assert.equal(
`${privateLink.protocol}//${privateLink.host}${privateLink.pathname}`,
`codex://threads/${originatingThreadId}`,
);
assert.deepEqual([...privateLink.searchParams.keys()], ["prompt"]);
verifyPublicationPrompt(privateLink.searchParams.get("prompt"), "custom");
await review.getByRole("radio", { name: "Workspace members" }).check();
assert.equal(await review.getByRole("radio", { name: "Workspace members" }).isChecked(), true);
assert.equal(await review.getByRole("radio", { name: "Invited people" }).isChecked(), false);
const sharedLink = new URL(await confirmation.getAttribute("href"));
verifyPublicationPrompt(sharedLink.searchParams.get("prompt"), "workspace_all");
const previousLinkCount = await page.evaluate(() => window.__dashboardDeepLinks.length);
const originalConfirmation = await confirmation.elementHandle();
await confirmation.click();
await page.waitForFunction((count) => window.__dashboardDeepLinks.length === count + 1, previousLinkCount);
assert.deepEqual(await page.evaluate(() => window.__dashboardNavigationEvents.at(-1)), {
connected: true, trusted: true, defaultPrevented: false, target: "_self",
}, "The first publication remains a trusted native link activation");
const waiting = review.getByRole("link", { name: "Waiting for ChatGPT…", exact: true });
await waiting.waitFor();
assert.equal(await waiting.getAttribute("aria-disabled"), "true");
assert.equal(await originalConfirmation.evaluate(element => element.isConnected), true,
"Publishing must keep the activated link mounted through navigation");
assert.match(await review.getByRole("status").innerText(), /send.*ChatGPT.*wait/iu,
"The waiting state explains that the prepared message still needs to be sent in ChatGPT");
await waiting.evaluate(element => { element.click(); element.click(); });
await waiting.focus();
await page.keyboard.press("Enter");
assert.equal(await page.evaluate(() => window.__dashboardDeepLinks.length), previousLinkCount + 1,
"Repeated mouse and keyboard activation must not create another publication handoff");
assert.equal(await page.evaluate(() => window.__dashboardNavigationEvents.at(-1).defaultPrevented), true);
await originalConfirmation.dispose();
assert.equal(
(await page.evaluate(() => window.__dashboardPrompts)).length,
promptCount,
"Publication must use its native handoff link instead of invoking the host-message bridge",
);
await review.getByRole("button", { name: "Close", exact: true }).click();
await review.waitFor({ state: "hidden" });
await page.getByRole("button", { name: "Publish", exact: true }).click();
assert.equal(await review.getByRole("link", { name: "Publish in ChatGPT", exact: true }).isEnabled(), true,
"Closing and reopening publication lets the user start another handoff");
await review.getByRole("button", { name: "Close", exact: true }).click();
await review.waitFor({ state: "hidden" });
}
async function installNoBridgeDashboardActionMocks(page) {
await page.addInitScript(() => {
window.__dashboardDeepLinks = [];
window.__dashboardPrints = 0;
window.__dashboardTrustedActivations = [];
document.addEventListener("click", (event) => {
const link = event.target.closest('a[href^="codex://"]');
if (!link) return;
window.__dashboardTrustedActivations.push({
connected: link.isConnected,
trusted: event.isTrusted,
defaultPrevented: event.defaultPrevented,
target: link.target,
});
event.preventDefault();
window.__dashboardDeepLinks.push(link.href);
window.__dashboardLastDeepLinkTarget = link.target;
});
window.print = () => {
window.__dashboardPrints += 1;
};
});
}
async function verifyLocalDashboardActions(page, { dataAppPath, originatingThreadId }) {
await page.getByRole("button", { name: "Refresh data" }).click();
await page.getByRole("menuitem", { name: "Refresh now" }).click();
await selectConversion(page, "PDF");
await page.getByRole("button", { name: "Publish", exact: true }).click();
const review = page.getByRole("dialog", { name: "Publish dashboard", exact: true });
await review.getByRole("link", { name: "Publish in ChatGPT" }).click();
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 3);
await review.getByRole("button", { name: "Close", exact: true }).click();
await review.waitFor({ state: "hidden" });
const dashboardActionLinks = (await page.evaluate(() => window.__dashboardDeepLinks)).map((href) => new URL(href));
for (const action of dashboardActionLinks) {
assert.equal(`${action.protocol}//${action.host}${action.pathname}`, `codex://threads/${originatingThreadId}`);
assert.equal(action.searchParams.has("originUrl"), false);
assert.equal(action.searchParams.has("path"), false);
assert.equal(action.searchParams.get("prompt").includes(originatingThreadId), false);
}
assert.match(dashboardActionLinks[0].searchParams.get("prompt"), /refresh this dashboard/i);
verifyPdfHandoff(dashboardActionLinks[1]);
assert.ok(dashboardActionLinks[1].searchParams.get("prompt").includes(dataAppPath),
"File-based PDF handoffs must retain the verified local artifact path");
verifyPublicationPrompt(dashboardActionLinks[2].searchParams.get("prompt"), "custom");
assert.equal(
await page.evaluate(() => window.__dashboardPrints),
0,
"Local PDF export must use a Codex handoff without invoking browser print",
);
assert.match(
readFileSync(dataAppPath, "utf8"),
new RegExp(`<meta name="data-app-local-thread" content="${originatingThreadId}">`, "u"),
"Local previews must retain their originating task in transient build metadata",
);
}
export async function verifyLocalDashboardHandoffs(browser, { dataAppPath, title, originatingThreadId, failures }) {
const page = await browser.newPage({ viewport: { width: 1440, height: 1050 } });
page.on("pageerror", (error) => failures.push(error.message));
await installNoBridgeDashboardActionMocks(page);
await page.goto(pathToFileURL(dataAppPath).href, { waitUntil: "load" });
const dashboardTitle = page.locator(".dashboard-topbar-title");
await dashboardTitle.waitFor();
assert.equal((await dashboardTitle.innerText()).trim(), title);
assert.equal(
new URL(page.url()).hash,
"",
"A local dashboard without a host bridge must return to its build task without a URL fragment",
);
assert.equal(await page.evaluate(() => typeof window.openai), "undefined");
await verifyEditorAskChatGPTSelectionModes(page);
await verifyLocalAskChatGPTHandoff(page, { originatingThreadId });
await page.evaluate(() => {
Object.defineProperty(navigator, "userAgent", { configurable: true, value: "CodexBrowser/1.0" });
});
await verifyLocalDashboardActions(page, { dataAppPath, originatingThreadId });
const actionComposer = await ensureActionComposer(page);
const shareAction = actionComposer.getByRole("link", { name: "Share key insights" });
await verifyPromptAnchor(shareAction);
await shareAction.click();
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 4);
assert.equal(
await page.evaluate(() => window.__dashboardLastDeepLinkTarget),
"_self",
"User-activated native dashboard actions must not create empty browser tabs",
);
const share = new URL(await page.evaluate(() => window.__dashboardDeepLinks.at(-1)));
assert.equal(`${share.protocol}//${share.host}${share.pathname}`, `codex://threads/${originatingThreadId}`);
assert.match(
share.searchParams.get("prompt"),
/\[@Data\]\(plugin:\/\/data-analytics@openai-curated-remote\) and invoke \$share-artifact-summary/u,
"Sharing must explicitly invoke its dedicated Data skill in the originating task",
);
assert.equal(
page.url().includes(originatingThreadId),
false,
"The originating task must stay in transient build metadata, never the preview URL",
);
assert.deepEqual(
await page.evaluate(() => window.__dashboardTrustedActivations),
Array.from({ length: 4 }, () => ({
connected: true,
trusted: true,
defaultPrevented: false,
target: "_self",
})),
"Local refresh, PDF, publishing, and sharing links must remain mounted and unprevented through document bubble",
);
await page.close();
}
export async function installPublishedDashboardActionMocks(page) {
await page.addInitScript(() => {
window.__dashboardDeepLinks = [];
window.__dashboardTrustedActivations = [];
window.__dashboardPrints = 0;
window.__dashboardClipboard = [];
window.__dashboardScrollCalls = [];
window.__dashboardHistoryReplacements = [];
window.__dashboardStatusAnnouncements = [];
document.addEventListener("click", (event) => {
const link = event.target.closest('a[href^="codex://"], a[href^="https://chatgpt.com/"]');
if (!link || event.defaultPrevented) return;
window.__dashboardTrustedActivations.push({
connected: link.isConnected,
trusted: event.isTrusted,
defaultPrevented: event.defaultPrevented,
target: link.target,
});
event.preventDefault();
window.__dashboardDeepLinks.push(link.href);
window.__dashboardLastDeepLinkTarget = link.target;
});
const replaceState = history.replaceState;
history.replaceState = function captureDashboardHistoryReplacement(state, title, url) {
window.__dashboardHistoryReplacements.push({ state, title, url: url == null ? null : String(url) });
return replaceState.apply(this, arguments);
};
let previousStatusElement;
let previousStatusText;
new MutationObserver(() => {
const status = document.querySelector('[role="status"]');
const text = status?.textContent.trim();
if (!status) {
previousStatusElement = undefined;
previousStatusText = undefined;
} else if (status !== previousStatusElement || text !== previousStatusText) {
window.__dashboardStatusAnnouncements.push(text);
previousStatusElement = status;
previousStatusText = text;
}
}).observe(document, { childList: true, characterData: true, subtree: true });
const scrollIntoView = Element.prototype.scrollIntoView;
Element.prototype.scrollIntoView = function captureDashboardChartScroll(options) {
window.__dashboardScrollCalls.push({
componentId: this.dataset.componentId ?? this.closest("[data-component-id]")?.dataset.componentId,
options: typeof options === "object" ? { ...options } : options,
});
return scrollIntoView.call(this, options);
};
window.print = () => {
window.__dashboardPrints += 1;
};
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText: async (text) => window.__dashboardClipboard.push(text) },
});
});
}
export async function verifyPublishedDashboardActions(page) {
await page.getByRole("button", { name: "Refresh data" }).click();
const refreshAction = page.getByRole("menuitem", { name: "Refresh now" });
await refreshAction.click();
await chooseHostedHandoff(page, "web");
await selectConversion(page, "PDF");
await chooseHostedHandoff(page, "web");
await selectConversion(page, "Jupyter Notebook");
await chooseHostedHandoff(page, "web");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 3);
assert.equal(
await page.getByText("Request sent to the host.", { exact: true }).count(),
0,
"Published exports must hand off without claiming that work was sent to a host",
);
const actionMenu = await ensureOverflowMenu(page);
const copyAction = actionMenu.getByRole("menuitem", { name: "Copy link", exact: true });
assert.notEqual(await copyAction.evaluate((element) => element.tagName), "A", "Copy link must remain local");
await copyAction.click();
const deepLinks = await page.evaluate(() => window.__dashboardDeepLinks);
assert.deepEqual(
await page.evaluate(() => window.__dashboardTrustedActivations),
Array.from({ length: 3 }, () => ({
connected: true,
trusted: true,
defaultPrevented: false,
target: "_blank",
})),
"Published web refresh, PDF and notebook export links must stay mounted through browser navigation",
);
for (const href of deepLinks) {
const action = new URL(href);
assert.equal(`${action.origin}${action.pathname}`, "https://chatgpt.com/");
assert.equal(action.searchParams.get("disable_auto_send"), "1");
assert.match(action.searchParams.get("q"), /https:\/\/dashboard\.chatgpt\.site\/published/u);
assert.doesNotMatch(action.toString(), /secret|private-section|codexThreadId|550e8400/u);
}
verifyPdfHandoff(new URL(deepLinks[1]), {
viewUrl: "https://dashboard.chatgpt.site/published?view=1&tab=dashboard",
});
assert.match(
new URL(deepLinks[2]).searchParams.get("q"),
/Use \[@Data\]\(plugin:\/\/data-analytics@openai-curated-remote\)[\s\S]*\$data-analytics:jupyter-notebooks/u,
"Published notebook exports must use the Data notebook skill without exposing the originating task",
);
assert.deepEqual(await page.evaluate(() => window.__dashboardClipboard), [
"https://dashboard.chatgpt.site/published?view=1&tab=dashboard",
]);
assert.equal(
await page.evaluate(() => window.__dashboardPrints),
0,
"Published PDF export must hand off without invoking browser print",
);
await page.emulateMedia({ media: "print" });
assert.equal(
await page.locator(".dashboard-topbar").evaluate((element) => getComputedStyle(element).display),
"none",
"Printed dashboards must omit application chrome",
);
assert.notEqual(
await page
.locator('main[data-data-app-content="dashboard"]')
.evaluate((element) => getComputedStyle(element).display),
"none",
"Printed dashboards must retain the rendered dashboard content",
);
assert.match(
await page
.locator('[data-component-id="usage-trend"]')
.evaluate((element) => getComputedStyle(element).breakInside),
/avoid/u,
"Printed dashboard components must avoid splitting across pages when possible",
);
await page.emulateMedia({ media: "screen" });
assert.equal(await page.getByRole("button", { name: /^Publish(?: changes)?$/u }).count(), 0,
"Hosted dashboard owners must not see Publish");
}
export async function verifyPublishedViewerHandoffs(page, { title, originatingThreadId, sensitivePreviewPattern }) {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("https://dashboard.chatgpt.site/published?token=secret#private-section", { waitUntil: "load" });
const dashboardTitle = page.locator(".dashboard-topbar-title");
await dashboardTitle.waitFor();
assert.equal((await dashboardTitle.innerText()).trim(), title);
await page.waitForFunction(() => !document.querySelector('[aria-busy="true"]'));
await page.locator(".dashboard-topbar-title").dblclick();
assert.notEqual(
await page.locator(".dashboard-topbar-title").getAttribute("contenteditable"),
"true",
"View-only users must not be able to edit a dashboard title by double-clicking it",
);
assert.equal(
await page.getByRole("button", { name: "Edit text and layout" }).count(),
0,
"Published viewers must remain read-only when duplicating a dashboard",
);
await dashboardTitle.evaluate((element) => {
const range = document.createRange();
range.selectNodeContents(element);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
element.dispatchEvent(new PointerEvent("pointerup", { bubbles: true }));
});
await page.getByRole("dialog", { name: "Selection actions", exact: true }).waitFor();
await verifyHostedHandoffChoices(page, { originatingThreadId });
await verifyViewerAskChatGPTHandoff(page);
const viewerOverflowMenu = await ensureOverflowMenu(page);
await viewerOverflowMenu.waitFor();
assert.equal(
await viewerOverflowMenu.getByRole("menuitem", { name: "Switch theme", exact: true }).count(),
0,
"Published viewers must not be able to customize the dashboard theme",
);
await page.keyboard.press("Escape");
const webComposer = await ensureActionComposer(page);
assert.equal(await webComposer.getByRole("link", { name: "Change this dashboard" }).count(), 0,
"Viewers must not receive the owner-only original-editing handoff");
await page.keyboard.press("Escape");
await webComposer.waitFor({ state: "hidden" });
const webRemix = (await ensureOverflowMenu(page)).getByRole("menuitem", { name: "Create a copy" });
await webRemix.click();
await chooseHostedHandoff(page, "web");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 1);
assert.deepEqual(await page.evaluate(() => window.__dashboardTrustedActivations.at(-1)), {
connected: true,
trusted: true,
defaultPrevented: false,
target: "_blank",
});
const web = new URL((await page.evaluate(() => window.__dashboardDeepLinks))[0]);
assert.equal(`${web.origin}${web.pathname}`, "https://chatgpt.com/");
assert.match(web.searchParams.get("q"), /https:\/\/dashboard\.chatgpt\.site\/published/u);
assert.match(web.searchParams.get("q"), /only data I can access/u);
assert.match(web.searchParams.get("q"), /leave the original unchanged/iu);
assert.doesNotMatch(web.toString(), /secret|private-section/);
await selectConversion(page, "PDF");
const webPdf = await chooseHostedHandoff(page, "web");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 2);
verifyPdfHandoff(webPdf, { viewUrl: "https://dashboard.chatgpt.site/published?view=1&tab=dashboard" });
assert.equal(await page.evaluate(() => window.__dashboardPrints), 0);
assert.equal(await page.getByRole("button", { name: /^Publish(?: changes)?$/u }).count(), 0,
"Hosted dashboard viewers must not see Publish");
await page.addInitScript(() => {
Object.defineProperty(navigator, "userAgent", { configurable: true, value: "CodexBrowser/1.0" });
});
await page.goto(
`https://dashboard.chatgpt.site/published?token=secret&surface=desktop#codexThreadId=${originatingThreadId}`,
{
waitUntil: "load",
},
);
assert.match(await page.evaluate(() => navigator.userAgent), /^CodexBrowser/u);
await page.evaluate(() => {
window.__publishedHostPrompts = [];
window.openai = {
sendFollowUpMessage: async (message) => {
window.__publishedHostPrompts.push(message);
return { isError: false };
},
};
});
const desktopRemix = (await ensureOverflowMenu(page)).getByRole("menuitem", { name: "Create a copy" });
await desktopRemix.click();
await chooseHostedHandoff(page, "desktop");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 1);
assert.deepEqual(await page.evaluate(() => window.__dashboardTrustedActivations.at(-1)), {
connected: true,
trusted: true,
defaultPrevented: false,
target: "_self",
});
assert.equal(
await page.evaluate(() => window.__dashboardLastDeepLinkTarget),
"_self",
"Published desktop actions must open a new task without creating an empty browser tab",
);
const desktop = new URL(await page.evaluate(() => window.__dashboardDeepLinks[0]));
assert.equal(`${desktop.protocol}//${desktop.host}${desktop.pathname}`, "codex://new");
assert.deepEqual([...desktop.searchParams.keys()], ["prompt", "browserUrl"]);
assert.equal(desktop.searchParams.get("browserUrl"), "https://dashboard.chatgpt.site/published?view=1&tab=dashboard");
assert.match(desktop.searchParams.get("prompt"), /only data I can access/u);
assert.doesNotMatch(desktop.toString(), sensitivePreviewPattern);
await selectConversion(page, "PDF");
const desktopPdf = await chooseHostedHandoff(page, "desktop");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 2);
verifyPdfHandoff(desktopPdf, { viewUrl: "https://dashboard.chatgpt.site/published?view=1&tab=dashboard" });
assert.equal(`${desktopPdf.protocol}//${desktopPdf.host}${desktopPdf.pathname}`, "codex://new");
assert.doesNotMatch(desktopPdf.toString(), sensitivePreviewPattern);
assert.equal(await page.evaluate(() => window.__dashboardPrints), 0);
assert.deepEqual(
await page.evaluate(() => window.__publishedHostPrompts),
[],
"Published dashboard actions must open a new task instead of messaging the originating task",
);
assert.equal(await page.getByRole("button", { name: /^Publish(?: changes)?$/u }).count(), 0,
"Hosted dashboards must not show Publish in Codex Browser");
}
export async function verifyWidgetPermalinkPdfExport(page) {
await selectConversion(page, "PDF");
const handoff = await chooseHostedHandoff(page, "desktop");
await page.waitForFunction(() => window.__dashboardDeepLinks.length === 1);
verifyPdfHandoff(handoff, { viewUrl: "https://dashboard.chatgpt.site/?view=1&tab=dashboard" });
assert.equal(`${handoff.protocol}//${handoff.host}${handoff.pathname}`, "codex://new");
assert.match(handoff.searchParams.get("prompt"), /export the entire/u,
"A widget permalink must retain whole-dashboard PDF scope");
assert.doesNotMatch(handoff.toString(), /token=|secret|private-section|codexThreadId/u);
assert.deepEqual(await page.evaluate(() => window.__dashboardTrustedActivations.at(-1)), {
connected: true, trusted: true, defaultPrevented: false, target: "_self",
});
assert.equal(
await page.evaluate(() => window.__dashboardPrints),
0,
"PDF export from a widget permalink must hand off without invoking browser print",
);
}
SHA-256: fd95dc98d06e72fb1ed9be50892c20317b7deb0b56c184a7b3123390a9979052