← Files DataARCHIVED FILE
tests/mobile-deep-links-browser.smoke.mjs
9.76 KB · Oct 5, 2026 · 12:04 UTC
import assert from 'node:assert/strict';
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright-core';
import { forceLocalDataAppMount, installDashboardBrowserMocks, resolveChromiumExecutable, runDataAppFixtureBuild } from './browser-helpers.mjs';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const workspace = mkdtempSync(join(tmpdir(), 'data-mobile-links-'));
const snapshot = JSON.parse(readFileSync(join(root, 'templates/data-app/base/src/data.json'), 'utf8'));
const html = {};
let browser;
async function open(surface, hosted = true) {
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true });
const page = await context.newPage();
const errors = [];
page.on('pageerror', error => errors.push(error.message));
await installDashboardBrowserMocks(page);
await context.route('https://mobile-links.workspace-a.chatgpt.site/**', route => {
const path = new URL(route.request().url()).pathname;
const payload = path === '/api/snapshot' ? { ...snapshot, surface }
: path === '/api/presentation' ? { canEdit: true, presentation: {}, revision: 0 } : null;
return route.fulfill(payload
? { contentType: 'application/json', body: JSON.stringify(payload) }
: { contentType: 'text/html', body: html[surface][hosted ? 'hosted' : 'static'] });
});
await page.goto(`https://mobile-links.workspace-a.chatgpt.site/${surface}`, { waitUntil: 'load' });
await page.locator('.dashboard-topbar-title').waitFor();
return { page, context, errors };
}
async function chooseNative(page, label) {
const select = page.locator('.dashboard-native-overflow select');
const value = await select.locator('option').evaluateAll((options, text) => options
.find(option => option.textContent.trim() === text)?.value, label);
assert.ok(value, `Missing ${label} from the mobile action menu`);
await select.selectOption(value);
assert.equal(await select.evaluate(element => element.selectedIndex), -1);
}
async function checkChooser(page, action, expectedPrompt) {
const dialog = page.getByRole('dialog', { name: 'Open in ChatGPT', exact: true });
await dialog.waitFor();
const desktop = new URL(await dialog.getByRole('link', { name: 'Open in desktop' }).getAttribute('href'));
const web = new URL(await dialog.getByRole('link', { name: 'Open on web' }).getAttribute('href'));
assert.equal(desktop.protocol, 'codex:', action);
assert.equal(web.origin, 'https://chatgpt.com', action);
assert.equal(web.searchParams.get('disable_auto_send'), '1', action);
assert.match(web.searchParams.get('q'), expectedPrompt, action);
const before = await page.evaluate(() => window.__dashboardDeepLinks.length);
await dialog.getByRole('link', { name: 'Open on web' }).click();
await page.waitForFunction(count => window.__dashboardDeepLinks.length === count + 1, before);
const event = await page.evaluate(() => window.__dashboardNavigationEvents.at(-1));
assert.equal(event.trusted, true, `${action}: destination must open from a real tap, not a synthetic click`);
await dialog.waitFor({ state: 'hidden' });
}
try {
for (const surface of ['dashboard', 'report']) {
const project = join(workspace, surface);
cpSync(join(root, 'templates/data-app/base'), project, { recursive: true,
filter: path => !path.includes('/node_modules') && !path.includes('/dist') });
if (surface === 'report') writeFileSync(join(project, 'src/data.json'), JSON.stringify({ ...snapshot, surface }));
const build = runDataAppFixtureBuild(project, { pluginRoot: root });
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
const built = readFileSync(join(project, 'dist/index.html'), 'utf8');
html[surface] = { hosted: built, static: forceLocalDataAppMount(built) };
}
browser = await chromium.launch({ executablePath: resolveChromiumExecutable(), headless: true });
for (const hosted of [true, false]) {
for (const surface of ['dashboard', 'report']) {
const { page, context, errors } = await open(surface, hosted);
let directCount = 0;
const checkHandoff = hosted ? checkChooser : async (page, action, expectedPrompt) => {
await page.waitForFunction(count => window.__dashboardDeepLinks.length === count + 1, directCount);
directCount += 1;
const href = new URL(await page.evaluate(() => window.__dashboardDeepLinks.at(-1)));
assert.equal(href.origin, 'https://chatgpt.com', `${action}: static preview opens directly on web`);
assert.equal(href.searchParams.get('disable_auto_send'), '1', action);
assert.match(href.searchParams.get('q'), expectedPrompt, action);
const event = await page.evaluate(() => window.__dashboardNavigationEvents.at(-1));
assert.equal(event.connected, true, `${action}: the destination anchor must remain connected during activation`);
};
for (const [action, pattern] of [
...(surface === 'dashboard' ? [['Refresh now', /refresh this existing dashboard/u],
['Create a copy', /create a new, separate dashboard/u]] : []),
['PDF', /\$data-analytics:report-to-pdf\b/u],
['Word document', /DOCX/u], ['PowerPoint', /PPTX/u], ['Google Docs', /Google Doc/u],
['Google Slides', /Google Slides/u], ['Jupyter Notebook', /ipynb/u],
]) {
await chooseNative(page, action);
await checkHandoff(page, `${surface} ${hosted ? 'hosted' : 'static'} ${action}`, pattern);
}
assert.equal(await page.evaluate(() => window.__dashboardPrints), 0,
`${surface}: native PDF export must hand off to the agent without opening browser print`);
await chooseNative(page, 'Ask ChatGPT');
const composer = page.getByRole('dialog', { name: 'Ask ChatGPT about this dashboard' });
await composer.waitFor();
await composer.getByRole('textbox', { name: 'Question for ChatGPT' }).fill('Explain this view.');
await composer.getByRole('link', { name: 'Send to ChatGPT' }).click();
await checkHandoff(page, `${surface} Ask`, /Explain this view\./u);
await chooseNative(page, 'Ask ChatGPT');
const suggestion = page.getByRole('dialog', { name: 'Ask ChatGPT about this dashboard' })
.getByRole('link', { name: 'Share Key Insights' });
await suggestion.click();
await checkHandoff(page, `${surface} suggestion`, /summary/u);
if (surface === 'report') {
const followUp = page.locator('a.report-task-link').first();
assert.ok(await followUp.count(), 'The report has an authored follow-up link');
await followUp.click();
await checkHandoff(page, `${surface} follow-up`, /report/u);
}
if (hosted) {
const chart = page.locator('.dashboard-component[data-component-kind="chart"] .component-native-actions select').first();
const copy = await chart.locator('option').evaluateAll(options => options.find(option => option.textContent === 'Copy link')?.value);
assert.ok(copy, `${surface}: published chart has a mobile Copy link command`);
const before = await page.evaluate(() => window.__dashboardClipboard.length);
await chart.selectOption(copy);
await page.waitForFunction(count => window.__dashboardClipboard.length === count + 1, before);
const copied = await page.evaluate(() => window.__dashboardClipboard.at(-1));
assert.match(copied, /^https:\/\/mobile-links\.workspace-a\.chatgpt\.site\/_data\/charts\//u);
await page.goto(copied, { waitUntil: 'load' });
await page.locator('[data-permalink-target="true"]').waitFor();
await page.goto(`https://mobile-links.workspace-a.chatgpt.site/${surface}`, { waitUntil: 'load' });
await page.locator('.dashboard-topbar-title').waitFor();
}
if (surface === 'dashboard' && hosted) {
await chooseNative(page, 'Schedule refresh');
const schedule = page.getByRole('dialog', { name: 'Schedule refresh' });
await schedule.waitFor();
await schedule.getByRole('button', { name: 'Schedule refresh' }).click();
await checkHandoff(page, 'Schedule refresh', /schedule refreshes/u);
await schedule.getByRole('button', { name: 'Close' }).click();
const metric = page.locator('.dashboard-component[data-component-kind="metric"]').first();
const select = metric.locator('.component-native-actions select');
const value = await select.locator('option').evaluateAll(options => options.find(option => option.textContent === 'Copy link')?.value);
assert.ok(value, 'Published metric has a mobile Copy link command');
await select.selectOption(value);
await page.waitForFunction(() => window.__dashboardClipboard.length > 0);
const copied = await page.evaluate(() => window.__dashboardClipboard.at(-1));
assert.match(copied, /^https:\/\/mobile-links\.workspace-a\.chatgpt\.site\/_data\/components\//u);
await page.goto(copied, { waitUntil: 'load' });
await page.locator('[data-permalink-target="true"]').waitFor();
await page.evaluate(() => { document.cookie = 'data_app_handoff_destination_v1=web; Domain=.workspace-a.chatgpt.site; Path=/; Secure; SameSite=Lax'; });
await chooseNative(page, 'Refresh now');
await checkHandoff(page, 'Remembered web choice from native picker', /refresh this existing dashboard/u);
}
assert.deepEqual(errors, [], `${surface} ${hosted ? 'hosted' : 'static'} browser errors`);
await context.close();
}
}
console.log('PASS: mobile native handoffs, Ask, report follow-ups, refresh schedule, chart/metric permalinks, hosted and static examples');
} finally {
await browser?.close();
rmSync(workspace, { recursive: true, force: true });
}
SHA-256: 58cc1fa1a00b8a8d4840f3a90122d71d94c9bb01025400c491587d454d847747