← Files DataARCHIVED FILE

tests/data-app-handoff-preference-browser.smoke.mjs

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

↓ Download file

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

import { chromium } from "playwright-core";

import { installDashboardBrowserMocks, resolveChromiumExecutable, runDataAppFixtureBuild } from "./browser-helpers.mjs";
import { chooseHostedHandoff } from "./data-app-browser-handoff.mjs";

const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const template = join(pluginRoot, "templates/data-app/base");
const project = mkdtempSync(join(tmpdir(), "data-handoff-preference-"));
const snapshot = JSON.parse(readFileSync(join(template, "src/data.json"), "utf8"));
const cookieName = "data_app_handoff_destination_v1";
const unsavedMessage = "Your choice couldn't be saved in this browser. You'll be asked again next time";
const screenshotDirectory = process.env.DATA_APP_HANDOFF_SCREENSHOT_DIR;
const failures = [];
const presentationWrites = [];
let browser;
let html;

async function createContext(options = {}) {
  const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, ...options });
  await context.route(/^https:\/\/[^/]+\.chatgpt\.site\//u, route => {
    const request = route.request();
    const { hostname, pathname } = new URL(request.url());
    if (request.method() !== "GET") presentationWrites.push({ url: request.url(), method: request.method() });
    const payload = pathname === "/api/snapshot" ? { ...snapshot, id: `dashboard:${hostname}` }
      : pathname === "/api/presentation" ? { canEdit: true, presentation: {}, revision: 0 } : null;
    return route.fulfill(payload
      ? { contentType: "application/json", body: JSON.stringify(payload) }
      : { contentType: "text/html", body: html });
  });
  return context;
}

async function openDashboard(context, hostname) {
  const page = await context.newPage();
  page.setDefaultTimeout(6000);
  page.on("pageerror", error => failures.push(`${hostname}: ${error.message}`));
  await installDashboardBrowserMocks(page);
  await page.goto(`https://${hostname}/`, { waitUntil: "load" });
  await page.locator(".dashboard-topbar-title").waitFor();
  return page;
}

const chooser = page => page.getByRole("dialog", { name: "Open in ChatGPT", exact: true });
const navigationCount = page => page.evaluate(() => window.__dashboardDeepLinks.length);

async function openCopy(page) {
  const menu = page.getByRole("menu", { name: "More", exact: true });
  if (!(await menu.isVisible())) await page.getByRole("button", { name: "More", exact: true }).click();
  await page.getByRole("menuitem", { name: "Create a copy", exact: true }).click();
}

async function expectRememberedCopy(page, destination) {
  const before = await navigationCount(page);
  const originalUrl = page.url();
  await openCopy(page);
  await page.waitForFunction(count => window.__dashboardDeepLinks.length === count + 1, before);
  assert.equal(await chooser(page).count(), 0, "A saved preference must bypass the chooser");
  const href = new URL(await page.evaluate(() => window.__dashboardDeepLinks.at(-1)));
  assert.equal(href.protocol, destination === "desktop" ? "codex:" : "https:");
  if (destination === "web") {
    assert.equal(href.origin, "https://chatgpt.com");
    assert.equal(href.searchParams.get("disable_auto_send"), "1");
  }
  assert.equal(page.url(), originalUrl, "The test must intercept all handoff navigation");
  assert.deepEqual(await page.evaluate(() => window.__dashboardPrompts), [], "Handoffs must not submit a host prompt");
}

async function expectChooser(page) {
  const before = await navigationCount(page);
  await openCopy(page);
  await chooser(page).waitFor();
  assert.equal(await navigationCount(page), before, "Showing the chooser must not navigate");
}

async function cancelChooser(page) {
  await chooser(page).getByRole("button", { name: "Cancel", exact: true }).click();
  await chooser(page).waitFor({ state: "hidden" });
}

async function openSettings(page, selected) {
  const before = await navigationCount(page);
  const more = page.getByRole("button", { name: "More", exact: true });
  if (await page.getByRole("menu", { name: "More", exact: true }).isVisible()) await page.keyboard.press("Escape");
  await more.focus();
  await more.press("Enter");
  const action = page.getByRole("menuitem", { name: "Open ChatGPT in…", exact: true });
  await action.focus();
  await action.press("ArrowRight");
  const menu = page.getByRole("menu", { name: "Open ChatGPT in…", exact: true });
  await menu.waitFor();
  assert.equal(await menu.getByRole("menuitemradio", { name: selected, exact: true }).getAttribute("aria-checked"), "true");
  assert.equal(await navigationCount(page), before, "Opening preferences must not activate a handoff");
  return menu;
}

async function closeSettings(page, menu) {
  await page.keyboard.press("Escape");
  await menu.waitFor({ state: "hidden" });
  await page.keyboard.press("Escape");
  await page.waitForFunction(() => document.activeElement?.getAttribute("aria-label") === "More");
}

async function saveSettings(page, before, after) {
  const menu = await openSettings(page, before);
  const option = menu.getByRole("menuitemradio", { name: after, exact: true });
  await option.focus();
  await option.press("Enter");
  await menu.waitFor({ state: "hidden" });
}

async function blockCookieWrites(page, blocked) {
  await page.evaluate(value => {
    if (!value) {
      delete document.cookie;
      return;
    }
    const descriptor = Object.getOwnPropertyDescriptor(Document.prototype, "cookie");
    Object.defineProperty(document, "cookie", {
      configurable: true,
      get() { return descriptor.get.call(this); },
      set() {},
    });
  }, blocked);
}

try {
  cpSync(template, project, { recursive: true,
    filter: path => !path.includes("/node_modules") && !path.includes("/dist") });
  const build = runDataAppFixtureBuild(project, { pluginRoot });
  assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
  html = readFileSync(join(project, "dist/index.html"), "utf8");
  browser = await chromium.launch({ executablePath: resolveChromiumExecutable(), headless: true });
  const touchContext = await createContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
  const touch = await openDashboard(touchContext, "pdf-mobile.openai.chatgpt.site");
  for (const destination of ["desktop", "web"]) {
    const count = await navigationCount(touch);
    const picker = touch.locator(".dashboard-native-overflow select");
    await picker.waitFor({ state: "visible" });
    await picker.selectOption("export:pdf");
    assert.equal(await picker.evaluate(element => element.selectedIndex), -1);
    await chooser(touch).waitFor();
    assert.equal(await navigationCount(touch), count, "The native picker waits for a user-activated handoff link");
    const href = await chooseHostedHandoff(touch, destination);
    assert.equal(await navigationCount(touch), count + 1);
    const viewUrl = "https://pdf-mobile.openai.chatgpt.site/?view=1&tab=dashboard";
    const prompt = href.searchParams.get(destination === "desktop" ? "prompt" : "q");
    assert.ok(prompt.includes(`](<${viewUrl}>)`));
    assert.match(prompt, /\$data-analytics:report-to-pdf\b/u);
    if (destination === "desktop") assert.equal(href.searchParams.get("browserUrl"), viewUrl);
    assert.equal(await touch.evaluate(() => window.__dashboardPrints), 0);
    assert.deepEqual(await touch.evaluate(() => window.__dashboardPrompts), []);
  }
  await touchContext.close();
  console.log("PASS: mobile PDF picker preserves the exact view through desktop/web handoffs without printing or host prompts.");
  const context = await createContext();
  const first = await openDashboard(context, "first.workspace-a.chatgpt.site");
  const sibling = await openDashboard(context, "second.workspace-a.chatgpt.site");
  const otherWorkspace = await openDashboard(context, "first.workspace-b.chatgpt.site");

  await expectChooser(first);
  if (screenshotDirectory) await first.screenshot({ path: join(screenshotDirectory, "handoff-chooser.png"), animations: "disabled" });
  await chooseHostedHandoff(first, "desktop", { remember: true });
  const savedCookies = (await context.cookies()).filter(cookie => cookie.name === cookieName);
  assert.equal(savedCookies.length, 1);
  assert.equal(savedCookies[0].value, "desktop");
  assert.equal(savedCookies[0].domain, ".workspace-a.chatgpt.site");
  assert.equal(savedCookies[0].path, "/");
  assert.equal(savedCookies[0].secure, true);
  assert.ok(savedCookies[0].expires > Date.now() / 1000, "Remembered choice must use persistent browser storage");
  await first.evaluate(() => {
    const click = HTMLAnchorElement.prototype.click;
    HTMLAnchorElement.prototype.click = function () {
      if (this.hasAttribute("data-data-app-handoff-navigation")) {
        HTMLAnchorElement.prototype.click = click;
        throw new Error("Navigation temporarily unavailable");
      }
      return click.call(this);
    };
  });
  await expectChooser(first);
  await chooseHostedHandoff(first, "desktop");
  await expectRememberedCopy(sibling, "desktop");
  await first.reload({ waitUntil: "load" });
  await first.locator(".dashboard-topbar-title").waitFor();
  await expectRememberedCopy(first, "desktop");
  const newTab = await openDashboard(context, "third.workspace-a.chatgpt.site");
  await expectRememberedCopy(newTab, "desktop");
  await expectChooser(otherWorkspace);
  await cancelChooser(otherWorkspace);

  const separateProfile = await createContext();
  const isolated = await openDashboard(separateProfile, "first.workspace-a.chatgpt.site");
  await expectChooser(isolated);
  await cancelChooser(isolated);
  await separateProfile.close();

  const canceled = await openSettings(first, "Desktop app");
  if (screenshotDirectory) await first.screenshot({ path: join(screenshotDirectory, "handoff-settings.png"), animations: "disabled" });
  await closeSettings(first, canceled);
  await first.setViewportSize({ width: 390, height: 844 });
  const mobileMenu = await openSettings(first, "Desktop app");
  await mobileMenu.evaluate(async element => { await Promise.all(element.getAnimations().map(animation => animation.finished)); });
  const mobileBounds = await mobileMenu.boundingBox();
  if (screenshotDirectory) await first.screenshot({ path: join(screenshotDirectory, "handoff-mobile-bounds.png"), animations: "disabled" });
  assert.ok(mobileBounds && mobileBounds.x >= 0 && mobileBounds.x + mobileBounds.width <= 390,
    `Preferences must fit the mobile viewport without horizontal clipping: ${JSON.stringify(mobileBounds)}`);
  if (screenshotDirectory) await first.screenshot({ path: join(screenshotDirectory, "handoff-settings-mobile.png"), animations: "disabled" });
  await closeSettings(first, mobileMenu);
  await first.setViewportSize({ width: 1280, height: 900 });
  await expectRememberedCopy(sibling, "desktop");
  await saveSettings(first, "Desktop app", "Web browser");
  await expectRememberedCopy(sibling, "web");
  await expectRememberedCopy(newTab, "web");

  await blockCookieWrites(first, true);
  for (const rejectedValue of ["Desktop app", "Always ask"]) {
    await saveSettings(first, "Web browser", rejectedValue);
    await first.getByRole("status").filter({ hasText: "Your ChatGPT opening preference couldn't be saved in this browser" }).waitFor();
    const unchanged = await openSettings(first, "Web browser");
    await closeSettings(first, unchanged);
    await expectRememberedCopy(sibling, "web");
  }
  await blockCookieWrites(first, false);
  await saveSettings(first, "Web browser", "Always ask");
  assert.deepEqual((await context.cookies()).filter(cookie => cookie.name === cookieName), []);
  await expectChooser(sibling);
  await chooseHostedHandoff(sibling, "desktop");
  await expectChooser(sibling);
  await cancelChooser(sibling);

  await blockCookieWrites(first, true);
  await expectChooser(first);
  await chooseHostedHandoff(first, "web", { remember: true });
  await first.getByRole("status").filter({ hasText: unsavedMessage }).waitFor();
  await expectChooser(first);
  await cancelChooser(first);
  await blockCookieWrites(first, false);

  const personal = await openDashboard(context, "dashboard.chatgpt.site");
  await expectChooser(personal);
  assert.equal(await chooser(personal).getByRole("checkbox").count(), 0, "Unsupported origins must not offer a preference they cannot save");
  await chooseHostedHandoff(personal, "desktop");
  await expectChooser(personal);
  await cancelChooser(personal);
  await personal.reload({ waitUntil: "load" });
  await personal.locator(".dashboard-topbar-title").waitFor();
  await expectChooser(personal);
  await cancelChooser(personal);
  assert.deepEqual((await context.cookies()).filter(cookie => cookie.name === cookieName), []);
  assert.deepEqual(presentationWrites, [], "Browser destination choices must never write dashboard presentation");
  assert.deepEqual(failures, [], "Handoff preference flows must not report browser errors");
  console.log("PASS: handoff cookies persist across reloads and sibling dashboards; settings, reset, isolation, blocked writes and mobile PDF handoffs verified.");
} finally {
  await browser?.close();
  rmSync(project, { recursive: true, force: true });
}

SHA-256: 64bdd383ea536fb5a75fb8df327f62c3c4d6b4f9ce9e2533fc7f02549dbde78f