← Files DataARCHIVED FILE

tests/data-app-browser-ask-chatgpt.mjs

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

↓ Download file

import assert from "node:assert/strict";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { chooseHostedHandoff } from "./data-app-browser-handoff.mjs";

async function settleSelectionUi(page) {
  await page.evaluate(
    () =>
      new Promise((resolveFrame) => {
        requestAnimationFrame(() => requestAnimationFrame(resolveFrame));
      }),
  );
}

export async function verifyAskChatGPTSelectionContext(page, { componentTitle, selectedContextIncludes }) {
  const actions = page.getByRole("dialog", { name: "Selection actions" });
  await actions.getByRole("button", { name: "Ask ChatGPT", exact: true }).click();
  const composer = page.getByRole("dialog", { name: "Ask ChatGPT about this dashboard" });
  const questionInput = composer.getByRole("textbox", { name: "Question for ChatGPT" });
  const question = "Explain this selected dashboard evidence.";
  await questionInput.fill(question);
  const sendLink = composer.getByRole("link", { name: "Send to ChatGPT" });
  await sendLink.waitFor();
  const href = new URL(await sendLink.getAttribute("href"));
  const prompt = href.searchParams.get("prompt") ?? href.searchParams.get("q");
  assert.ok(prompt, "Selection actions must construct a real question handoff");
  assert.equal(
    prompt.split("\n").at(-1),
    `Question: ${question}`,
    "The selected-context handoff must retain the entered question as its final labeled line",
  );
  assert.ok(
    prompt.includes(`Chart: ${componentTitle}\n`),
    "The question must refer to the selected component, not the entire dashboard",
  );
  const selectedContext = /^Selected (?:point|context): (.+)$/mu.exec(prompt)?.[1];
  assert.ok(
    selectedContext && selectedContext !== "Entire dashboard",
    "Selection actions must retain nonempty selected evidence",
  );
  if (selectedContextIncludes) {
    assert.ok(selectedContext.includes(selectedContextIncludes), "The handoff must retain the selected label or text");
  }
  await questionInput.fill("");
  await page.keyboard.press("Escape");
  await composer.waitFor({ state: "hidden" });
}

export async function verifyEditorAskChatGPTSelectionModes(page) {
  const selectedBar = page.locator('[data-component-id="segment-breakdown"] .chart-ranked-list-row').first();
  await selectedBar.scrollIntoViewIfNeeded();
  await page.getByRole("button", { name: "Edit text and layout" }).click();
  await selectedBar.click();
  await settleSelectionUi(page);
  assert.equal(
    await page.getByRole("dialog", { name: "Selection actions" }).count(),
    0,
    "Ask ChatGPT must stay hidden in edit mode",
  );
  assert.equal(
    await page.locator(".dashboard-ask-selected-region").count(),
    0,
    "Edit mode must not show chart-selection regions",
  );
  await page.getByRole("button", { name: "Cancel", exact: true }).click();
  await page.getByRole("button", { name: "Edit text and layout" }).waitFor();

  await selectedBar.click();
  await settleSelectionUi(page);
  await page.getByRole("dialog", { name: "Selection actions" }).waitFor();
  assert.equal(
    await page.getByRole("dialog", { name: "Selection actions" }).count(),
    1,
    "Ordinary file/browser viewers may select marks regardless of edit permission",
  );
  await page.keyboard.press("Escape");
}

export async function verifyLocalAskChatGPTHandoff(page, {
  originatingThreadId,
  projectRoot = dirname(dirname(fileURLToPath(page.url()))).replaceAll("\\", "/"),
}) {
  const original = await page.evaluate(() => ({
    markerPresent: Object.hasOwn(window, "__codexBrowser"),
    marker: window.__codexBrowser,
    userAgent: navigator.userAgent,
    threadId: document.querySelector('meta[name="data-app-local-thread"]')?.content,
    linkCount: window.__dashboardDeepLinks.length,
  }));
  assert.doesNotMatch(original.userAgent, /^(?:CodexBrowser|ChatGPTBrowser)(?:\s|\/|$)/u);
  assert.equal(original.threadId, originatingThreadId);
  const assertLocalDestination = (href, threadId) => {
    assert.equal(`${href.protocol}//${href.host}${href.pathname}`, threadId ? `codex://threads/${threadId}` : "codex://new");
    assert.deepEqual([...href.searchParams.keys()], threadId ? ["prompt"] : ["prompt", "path"]);
    assert.equal(href.searchParams.get("path"), threadId ? null : projectRoot);
  };
  try {
    for (const { codexMarker, threadId } of [
      { codexMarker: false, threadId: originatingThreadId },
      { codexMarker: true, threadId: originatingThreadId },
      { codexMarker: false, threadId: null },
    ]) {
      await page.evaluate(({ codexMarker, threadId }) => {
        if (codexMarker) window.__codexBrowser = true;
        else delete window.__codexBrowser;
        const meta = document.querySelector('meta[name="data-app-local-thread"]');
        if (threadId == null) meta.remove();
        else meta.content = threadId;
      }, { codexMarker, threadId });
      await page.locator(".dashboard-ask-button").click();
      const composer = page.getByRole("dialog", { name: "Ask ChatGPT about this dashboard", exact: true });
      const suggestions = await composer.getByRole("group", { name: "Suggested actions", exact: true })
        .getByRole("link").evaluateAll(links => links.map(link => ({ href: link.href, target: link.target })));
      assert.ok(suggestions.length > 0);
      for (const suggestion of suggestions) {
        assertLocalDestination(new URL(suggestion.href), threadId);
        assert.equal(suggestion.target, "_self");
      }
      const questionInput = composer.getByRole("textbox", { name: "Question for ChatGPT", exact: true });
      const question = "Explain this dashboard.";
      await questionInput.fill(question);
      const send = composer.getByRole("link", { name: "Send to ChatGPT", exact: true });
      const href = new URL(await send.getAttribute("href"));
      assertLocalDestination(href, threadId);
      assert.equal(await send.getAttribute("target"), "_self");
      assert.ok(href.searchParams.get("prompt").endsWith(`Question: ${question}`));
      assert.equal(await page.getByRole("dialog", { name: "Open in ChatGPT", exact: true }).count(), 0);
      assert.equal(await page.evaluate(() => typeof window.openai), "undefined");
      await questionInput.fill("");
      await page.keyboard.press("Escape");
      await composer.waitFor({ state: "hidden" });
    }
    assert.equal(await page.evaluate(() => window.__dashboardDeepLinks.length), original.linkCount,
      "Preparing an Ask link must not activate a handoff");
  } finally {
    await page.evaluate(({ markerPresent, marker, threadId }) => {
      if (markerPresent) window.__codexBrowser = marker;
      else delete window.__codexBrowser;
      const meta = document.querySelector('meta[name="data-app-local-thread"]') ?? document.createElement("meta");
      meta.name = "data-app-local-thread";
      meta.content = threadId;
      if (!meta.isConnected) document.head.append(meta);
    }, original);
  }
}

export async function verifyViewerAskChatGPTHandoff(page) {
  await page.evaluate(() => {
    const marker = document.createElement("meta");
    marker.name = "data-app-sites-project";
    marker.content = "appgprj_browser_handoff";
    document.head.append(marker);
  });
  const selectedBar = page.locator('[data-component-id="segment-breakdown"] .chart-ranked-list-row').first();
  await selectedBar.scrollIntoViewIfNeeded();
  await selectedBar.click();
  await settleSelectionUi(page);

  const selectionActions = page.getByRole("dialog", {
    name: "Selection actions",
  });
  await selectionActions.waitFor();
  await selectionActions.getByRole("button", { name: "Ask ChatGPT" }).click();
  const composer = page.getByRole("dialog", {
    name: "Ask ChatGPT about this dashboard",
  });
  await composer.waitFor();
  assert.equal(
    await composer.getByText("Send to ChatGPT", { exact: true }).count(),
    0,
    "The handoff label must stay hidden until the viewer enters a question",
  );

  const viewerQuestion = "What explains this segment?";
  await composer.getByRole("textbox", { name: "Question for ChatGPT" }).fill(viewerQuestion);
  const sendLink = composer.getByRole("link", { name: "Send to ChatGPT" });
  assert.deepEqual(
    await sendLink.locator(".dashboard-ask-primary").evaluate((button) => {
      const bounds = button.getBoundingClientRect();
      return {
        width: bounds.width,
        height: bounds.height,
        radius: getComputedStyle(button).borderRadius,
      };
    }),
    { width: 28, height: 28, radius: "50%" },
    "The handoff action must use the 28px circular system button",
  );

  await sendLink.click();
  await chooseHostedHandoff(page, "web");
  await page.waitForFunction(() => window.__dashboardDeepLinks.length === 1);
  const chatGPTLink = new URL((await page.evaluate(() => window.__dashboardDeepLinks))[0]);
  const chatGPTPrompt = chatGPTLink.searchParams.get("q");
  assert.equal(`${chatGPTLink.origin}${chatGPTLink.pathname}`, "https://chatgpt.com/");
  assert.deepEqual([...chatGPTLink.searchParams.keys()], ["q", "disable_auto_send"]);
  assert.equal(chatGPTLink.searchParams.get("disable_auto_send"), "1");
  assert.ok(chatGPTPrompt.startsWith(viewerQuestion));
  assert.ok(chatGPTPrompt.includes("https://dashboard.chatgpt.site/published?view=1&tab=dashboard"));
  assert.match(chatGPTPrompt, /read its current Data app context/u);
  assert.match(chatGPTPrompt, /Chart: Active accounts by feature/u);
  assert.match(chatGPTPrompt, /Selected point: .+/u);
  assert.doesNotMatch(chatGPTPrompt, /Dashboard URL:|token=secret|#private-section/u);
  await composer.waitFor({ state: "hidden" });
  assert.equal(await composer.count(), 0, "The composer must close after handing the question to ChatGPT");
  await page.evaluate(() => {
    window.__dashboardDeepLinks = [];
  });

  const externalUserAgent = await page.evaluate(() => navigator.userAgent);
  try {
    for (const userAgent of [externalUserAgent, "CodexBrowser/1.0"]) {
      const desktop = userAgent === "CodexBrowser/1.0";
      await page.evaluate((value) => Object.defineProperty(navigator, "userAgent", { configurable: true, value }), userAgent);
      await page.locator(".dashboard-ask-button").click();
      const dashboardComposer = page.getByRole("dialog", {
        name: "Ask ChatGPT about this dashboard",
      });
      const dashboardQuestion = "What should I know about this dashboard?";
      await dashboardComposer.getByRole("textbox", { name: "Question for ChatGPT" }).fill(dashboardQuestion);
      const dashboardSend = dashboardComposer.getByRole("link", { name: "Send to ChatGPT" });
      await dashboardSend.click();
      await chooseHostedHandoff(page, desktop ? "desktop" : "web");
      await page.waitForFunction(() => window.__dashboardDeepLinks.length === 1);
      const dashboardLink = new URL((await page.evaluate(() => window.__dashboardDeepLinks))[0]);
      assert.equal(`${dashboardLink.protocol}//${dashboardLink.host}${dashboardLink.pathname}`,
        desktop ? "codex://new" : "https://chatgpt.com/");
      assert.deepEqual([...dashboardLink.searchParams.keys()], desktop ? ["prompt", "browserUrl"] : ["q", "disable_auto_send"]);
      if (!desktop) assert.equal(dashboardLink.searchParams.get("disable_auto_send"), "1");
      if (desktop) assert.equal(dashboardLink.searchParams.get("browserUrl"), "https://dashboard.chatgpt.site/published?view=1&tab=dashboard");
      const prompt = dashboardLink.searchParams.get(desktop ? "prompt" : "q");
      assert.ok(prompt.startsWith(dashboardQuestion));
      assert.ok(prompt.includes("https://dashboard.chatgpt.site/published?view=1&tab=dashboard"));
      assert.match(prompt, /read its current Data app context/u);
      assert.match(prompt, /Selected context: Entire dashboard/u);
      await dashboardComposer.waitFor({ state: "hidden" });
      await page.evaluate(() => { window.__dashboardDeepLinks = []; });
    }
  } finally {
    await page.evaluate((value) => Object.defineProperty(navigator, "userAgent", { configurable: true, value }), externalUserAgent);
  }
}

SHA-256: 4019bbf528a58576d5c3ae3d1122510df5cb4156b2e75a69f1131b30f1a904d1