← Files DataARCHIVED FILE

tests/data-app-permalink-tabs-browser.smoke.mjs

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

↓ Download file

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 } from "node:url";

import { chromium } from "playwright-core";

import { componentPermalinkId, componentPermalinkShortId } from "../templates/data-app/base/src/chart-permalink.js";
import { resolveChromiumExecutable, runDataAppFixtureBuild } from "./browser-helpers.mjs";

const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const templateRoot = join(pluginRoot, "templates/data-app/base");
const projectRoot = mkdtempSync(join(tmpdir(), "data-permalink-tabs-"));
const snapshot = JSON.parse(readFileSync(join(templateRoot, "src/data.json"), "utf8"));
const origin = "https://permalink-tabs.chatgpt.site";
const tabs = [
  { id: "dashboard", label: "Overview" },
  { id: "segments", label: "Segments" },
];
const hostHistoryState = { host: { frame: "permalink-regression", scroll: 17 } };
const aliases = {
  readable: (id) => id,
  uuid: (id) => componentPermalinkId(origin, id),
  short: (id) => componentPermalinkShortId(origin, id),
};
const failures = [];
const passed = [];
let browser;

function permalink(kind, id, { alias = "short", tab, detail = false } = {}) {
  const url = new URL(`/_data/${kind}/${encodeURIComponent(aliases[alias](id))}${detail ? "/detail" : ""}`, origin);
  if (tab) url.searchParams.set("tab", tab);
  url.searchParams.set("f.segment", "Studio");
  return url.href;
}

function withHostContext(link) {
  const url = new URL(link);
  url.searchParams.set("host_context", "frame-7");
  url.hash = "host-anchor";
  return url.href;
}

async function assertHostContext(page) {
  const url = new URL(page.url());
  assert.equal(url.searchParams.get("host_context"), "frame-7");
  assert.equal(url.hash, "#host-anchor");
  const { dataAppView, ...hostState } = await page.evaluate(() => history.state);
  assert.deepEqual(
    hostState,
    hostHistoryState,
    "Dashboard navigation must preserve the host's existing history state.",
  );
  return dataAppView;
}

async function historyState(page) {
  return page.evaluate(() => ({ length: history.length, pushes: window.__permalinkHistoryPushes }));
}

async function targetState(page, id) {
  return page.evaluate((componentId) => {
    const target = [...document.querySelectorAll("[data-component-id]")].find(
      (element) => element.dataset.componentId === componentId,
    );
    return {
      url: location.href,
      activeTab: document.querySelector("[data-fixture-view]")?.dataset.fixtureView,
      selectedTabs: [...document.querySelectorAll('[role="tab"][aria-selected="true"]')].map((element) =>
        element.textContent.trim(),
      ),
      targetExists: Boolean(target),
      targetVisible: Boolean(target?.getClientRects().length),
      targetFocused: document.activeElement === target,
      targetHighlighted: target?.getAttribute("data-permalink-target") === "true",
      focusEvents: window.__permalinkFocus,
      highlightEvents: window.__permalinkHighlights,
    };
  }, id);
}

async function assertSelectedTab(page, tabId) {
  await page.waitForFunction(
    (expected) => document.querySelector("[data-fixture-view]")?.dataset.fixtureView === expected,
    tabId,
  );
  const label = tabs.find(({ id }) => id === tabId).label;
  assert.equal(
    await page.getByRole("tab", { name: label, exact: true, includeHidden: true }).getAttribute("aria-selected"),
    "true",
  );
  assert.equal(new URL(page.url()).searchParams.get("view"), "1");
  assert.equal(new URL(page.url()).searchParams.get("tab"), tabId);
  assert.equal(new URL(page.url()).searchParams.get("f.segment"), "Studio");
  assert.equal(await page.locator("[data-fixture-filter]").innerText(), "Studio");
}

async function assertResolved(page, id, { tab = "segments", detail = false } = {}) {
  await page.waitForFunction(
    ({ componentId, tabId }) => {
      // An invisible highlight is a useful immediate failure, not a reason to wait
      // for the highlight timeout and lose the evidence of the incorrect resolution.
      if (window.__permalinkHighlights.some((event) => event.id === componentId && !event.visible)) return true;
      const target = [...document.querySelectorAll("[data-component-id]")].find(
        (element) => element.dataset.componentId === componentId,
      );
      return (
        document.querySelector("[data-fixture-view]")?.dataset.fixtureView === tabId &&
        target?.getClientRects().length > 0 &&
        target.getAttribute("data-permalink-target") === "true" &&
        window.__permalinkFocus.includes(componentId)
      );
    },
    { componentId: id, tabId: tab },
  );
  const state = await targetState(page, id);
  assert.equal(state.activeTab, tab, `The permalink selected the wrong tab: ${JSON.stringify(state)}`);
  assert.equal(state.targetVisible, true, `The linked component is still hidden: ${JSON.stringify(state)}`);
  assert.equal(state.targetHighlighted, true, `The linked component was not highlighted: ${JSON.stringify(state)}`);
  assert.ok(
    state.focusEvents.includes(id),
    `The linked component never received real browser focus: ${JSON.stringify(state)}`,
  );
  assert.deepEqual(
    state.highlightEvents.filter((event) => !event.visible),
    [],
    `Permalinks must not resolve hidden elements: ${JSON.stringify(state)}`,
  );
  await assertSelectedTab(page, tab);
  if (detail) {
    const dialog = page.getByRole("dialog", { name: "Segment trend", exact: true });
    await dialog.waitFor();
    assert.equal(
      await dialog.evaluate((element) => element.contains(document.activeElement)),
      true,
      "Chart details should move focus from the visible chart into the real dialog.",
    );
  } else {
    assert.equal(state.targetFocused, true, `Focus did not reach the linked component: ${JSON.stringify(state)}`);
    assert.equal(await page.getByRole("dialog").count(), 0, "Plain component/chart links must not open chart details.");
  }
}

async function assertOverviewNavigation(page, id) {
  const linkedUrl = page.url();
  const before = await historyState(page);
  await page.getByRole("tab", { name: "Overview", exact: true }).click();
  await assertSelectedTab(page, "dashboard");
  const overviewUrl = page.url();
  assert.equal(new URL(overviewUrl).pathname, "/", "A deliberate tab change must leave the component route.");
  const overviewView = {
    artifactId: snapshot.id,
    tabId: "dashboard",
    filters: { ...Object.fromEntries(snapshot.filters.map(({ id, defaultValue }) => [id, defaultValue])), segment: "Studio" },
    focus: {},
    returnTo: null,
  };
  assert.deepEqual(await assertHostContext(page), overviewView);
  const after = await historyState(page);
  assert.equal(after.length, before.length + 1, "A deliberate tab change must add exactly one history entry.");
  assert.deepEqual(
    after.pushes.slice(before.pushes.length),
    [{ url: overviewUrl, state: { ...hostHistoryState, dataAppView: overviewView } }],
    "Tab navigation must push the canonical destination exactly once, preserving host state.",
  );

  await page.goBack();
  await assertResolved(page, id);
  assert.equal(page.url(), linkedUrl, "Back must restore the original component link.");
  const { scrollY, ...linkedView } = await assertHostContext(page);
  assert.deepEqual(linkedView, { ...overviewView, tabId: "segments" });
  assert.ok(Number.isFinite(scrollY) && scrollY >= 0, "The linked history entry must retain its scroll position.");
  await page.goForward();
  await assertSelectedTab(page, "dashboard");
  assert.equal(page.url(), overviewUrl, "Forward must restore the manually selected view's URL.");
  assert.deepEqual(await assertHostContext(page), overviewView);
  assert.deepEqual(await historyState(page), after, "Back/Forward resolution must not push extra history entries.");
  const state = await targetState(page, id);
  assert.equal(
    state.targetHighlighted,
    false,
    `Forward must not re-resolve a permalink hidden by a manual tab choice: ${JSON.stringify(state)}`,
  );
  assert.deepEqual(
    state.highlightEvents.filter((event) => !event.visible),
    [],
  );
  assert.equal(await page.getByRole("dialog").count(), 0);

  await page.reload();
  await assertSelectedTab(page, "dashboard");
  assert.equal(page.url(), overviewUrl, "Reload must stay on the manually chosen dashboard tab.");
  assert.deepEqual(await assertHostContext(page), overviewView);
  const reloaded = await targetState(page, id);
  assert.equal(reloaded.targetVisible, false);
  assert.deepEqual(reloaded.highlightEvents, [], "Reloading a canonical dashboard URL must not re-arm the old target.");
  assert.equal(await page.getByRole("dialog").count(), 0);
}

async function scenario(name, run, { canEdit = false, savedTabs = tabs, hiddenBlocks = [] } = {}) {
  const page = await browser.newPage({ viewport: { width: 1280, height: 900 }, reducedMotion: "reduce" });
  const errors = [];
  const presentationWrites = [];
  page.setDefaultTimeout(4000);
  page.on("pageerror", (error) => errors.push(error.message));
  await page.route(`${origin}/**`, (route) => {
    const request = route.request();
    const { pathname } = new URL(request.url());
    if (pathname === "/api/presentation" && request.method() === "PUT") {
      presentationWrites.push(request.postData());
    }
    const payload =
      pathname === "/api/snapshot"
        ? snapshot
        : pathname === "/api/presentation"
          ? {
              canEdit,
              presentation: {
                ...(savedTabs ? { tabs: savedTabs } : {}),
                ...(hiddenBlocks.length ? { hiddenBlocks } : {}),
              },
              revision: 0,
            }
          : null;
    return route.fulfill(
      payload
        ? { contentType: "application/json", body: JSON.stringify(payload) }
        : { contentType: "text/html", body: html },
    );
  });
  await page.addInitScript(() => {
    window.__permalinkFocus = [];
    window.__permalinkHighlights = [];
    window.__permalinkTabChanges = [];
    window.__permalinkHistoryPushes = [];
    const pushState = history.pushState;
    history.pushState = function capturePermalinkPush(...args) {
      window.__permalinkHistoryPushes.push({
        url: new URL(args[2] ?? location.href, location.href).href,
        state: args[0],
      });
      return pushState.apply(this, args);
    };
    document.addEventListener("focusin", (event) => {
      if (event.target.matches?.("[data-component-id]")) {
        window.__permalinkFocus.push(event.target.dataset.componentId);
      }
    });
    new MutationObserver((records) => {
      for (const { target, attributeName, oldValue } of records) {
        if (attributeName === "data-fixture-view") {
          if (oldValue !== null)
            window.__permalinkTabChanges.push({
              from: oldValue,
              to: target.getAttribute("data-fixture-view"),
            });
          continue;
        }
        if (target.getAttribute("data-permalink-target") !== "true") continue;
        window.__permalinkHighlights.push({
          id: target.dataset.componentId,
          visible: target.getClientRects().length > 0 && getComputedStyle(target).visibility !== "hidden",
        });
      }
    }).observe(document, {
      subtree: true,
      attributes: true,
      attributeOldValue: true,
      attributeFilter: ["data-permalink-target", "data-fixture-view"],
    });
  });
  try {
    await run(page);
    // Owner presentation persistence is debounced by 300 ms. Keep the page alive
    // long enough to catch an accidental shared write caused by tab discovery.
    await page.waitForTimeout(350);
    assert.deepEqual(presentationWrites, [], "Resolving a permalink must not write shared presentation.");
    assert.deepEqual(errors, [], "The source-authored dashboard must not report browser errors.");
    passed.push(name);
  } catch (error) {
    failures.push({ scenario: name, message: error.message, errors, presentationWrites });
  } finally {
    await page.close();
  }
}

let html;
try {
  cpSync(templateRoot, projectRoot, {
    recursive: true,
    filter: (path) => !path.includes("/node_modules") && !path.includes("/dist"),
  });
  // Only authored content changes. Cover conditionally mounted targets and
  // mounted-but-hidden targets: a registered ref is not necessarily visible.
  writeFileSync(
    join(projectRoot, "src/content/dashboard/DashboardContent.jsx"),
    `
import React from "react";
import { ChartRenderer, DataComponent, useDataApp, useDashboardTabs } from "../../data-app-public.jsx";
const tabs = ${JSON.stringify(tabs)};
const trend = { type: "line", x: "week", y: "activeUsers", showXAxisLabel: false };
export function DashboardContent() {
  const { activeTabId } = useDashboardTabs(tabs);
  const { filters, reviewedRows, visible, chartProps } = useDataApp();
  const rows = reviewedRows("usage_summary", ["week"]);
  return <section data-fixture-view={activeTabId}>
    <output data-fixture-filter>{filters.segment}</output>
    <section data-fixture-panel="dashboard" hidden={activeTabId !== "dashboard"}>
      {activeTabId === "dashboard" && visible("overview-widget") && <DataComponent id="overview-widget" title="Overview metric"
        queryId="usage_summary" kind="metric"><p data-reviewed-rows>{rows.at(-1)?.activeUsers}</p></DataComponent>}
    </section>
    <section data-fixture-panel="segments" style={{ display: activeTabId === "segments" ? "block" : "none" }}>
      {visible("segments-widget") && <DataComponent id="segments-widget" title="Segment metric"
        queryId="usage_summary" kind="metric"><p data-reviewed-rows>{rows.at(-1)?.activeUsers}</p></DataComponent>}
      {visible("segments-chart") && <DataComponent id="segments-chart" title="Segment trend"
        queryId="usage_summary" kind="chart" chart={trend} displayRows={rows}>
        <ChartRenderer spec={trend} rows={rows} height={180} {...chartProps("segments-chart")} />
      </DataComponent>}
    </section>
  </section>;
}`,
  );
  const build = runDataAppFixtureBuild(projectRoot, { pluginRoot });
  assert.equal(build.status, 0, build.stdout + build.stderr);
  html = readFileSync(join(projectRoot, "dist/index.html"), "utf8");
  browser = await chromium.launch({ executablePath: resolveChromiumExecutable(), headless: true });

  for (const [alias, tab] of [
    ["readable", "dashboard"],
    ["uuid", undefined],
    ["short", "deleted"],
  ]) {
    await scenario(
      `${alias} widget link discovers an inactive mounted panel`,
      async (page) => {
        await page.goto(permalink("components", "segments-widget", { alias, tab }));
        await assertResolved(page, "segments-widget");
      },
      { canEdit: alias === "readable", savedTabs: alias === "uuid" ? null : tabs },
    );
  }

  await scenario("a conditionally mounted target is discovered in the other direction", async (page) => {
    await page.goto(permalink("components", "overview-widget", { tab: "segments" }));
    await assertResolved(page, "overview-widget", { tab: "dashboard" });
  });

  await scenario("a readable chart link selects its tab without opening details", async (page) => {
    await page.goto(permalink("charts", "segments-chart", { alias: "readable" }));
    await assertResolved(page, "segments-chart");
  });

  await scenario("a component link to a chart does not open details", async (page) => {
    await page.goto(permalink("components", "segments-chart", { tab: "dashboard" }));
    await assertResolved(page, "segments-chart");
  });

  for (const [alias, tab] of [
    ["uuid", "dashboard"],
    ["short", "segments"],
  ]) {
    await scenario(`${alias} chart details retain the selected tab and filters on close`, async (page) => {
      const link = permalink("charts", "segments-chart", { alias, tab, detail: true });
      await page.goto(link);
      await assertResolved(page, "segments-chart", { detail: true });
      const dialog = page.getByRole("dialog", { name: "Segment trend", exact: true });
      await dialog.getByRole("button", { name: "Close", exact: true }).click();
      await dialog.waitFor({ state: "hidden" });
      await assertResolved(page, "segments-chart");
      const closed = new URL(page.url());
      assert.equal(closed.pathname, new URL(link).pathname.replace(/\/detail$/u, ""));
    });
  }

  await scenario("chart routes reject metric targets and restore the starting view", async (page) => {
    await page.goto(permalink("charts", "segments-widget", { tab: "dashboard" }));
    await page.getByText("The linked chart is unavailable", { exact: true }).waitFor();
    await assertSelectedTab(page, "dashboard");
    assert.equal(await page.getByRole("dialog").count(), 0);
    assert.deepEqual((await targetState(page, "segments-widget")).highlightEvents, []);
  });

  await scenario(
    "saved-hidden targets stay hidden without probing other tabs",
    async (page) => {
      await page.goto(permalink("components", "segments-widget", { tab: "dashboard" }));
      await page.getByText("The linked component is unavailable", { exact: true }).waitFor();
      await assertSelectedTab(page, "dashboard");
      assert.deepEqual(
        await page.evaluate(() => window.__permalinkTabChanges),
        [],
        "A saved-hidden component must be rejected before exposing another tab.",
      );
      const state = await targetState(page, "segments-widget");
      assert.equal(state.targetExists, false);
      assert.deepEqual(state.highlightEvents, []);
      assert.deepEqual(state.focusEvents, []);
      assert.deepEqual((await historyState(page)).pushes, []);
      await page.getByRole("tab", { name: "Segments", exact: true }).click();
      await assertSelectedTab(page, "segments");
      assert.equal(
        (await targetState(page, "segments-widget")).targetExists,
        false,
        "Choosing the target's tab must not restore an owner-hidden component.",
      );
    },
    { canEdit: true, hiddenBlocks: ["segments-widget"] },
  );

  await scenario(
    "a genuinely missing target restores its original nondefault tab",
    async (page) => {
      const link = permalink("components", "missing-widget", { tab: "segments" });
      await page.goto(link);
      await page.getByText("The linked component is unavailable", { exact: true }).waitFor();
      await assertSelectedTab(page, "segments");
      const expected = new URL(link);
      expected.search = new URLSearchParams({ view: "1", ...Object.fromEntries(expected.searchParams) }).toString();
      assert.equal(page.url(), expected.href);
      const state = await targetState(page, "missing-widget");
      assert.equal(state.targetExists, false);
      assert.deepEqual(state.highlightEvents, []);
      assert.deepEqual(state.focusEvents, []);
      assert.deepEqual((await historyState(page)).pushes, [], "Automatic discovery must not add history entries.");
    },
    { canEdit: true },
  );

  await scenario(
    "Back and Forward preserve manual navigation away from a resolved link",
    async (page) => {
      await page.goto(withHostContext(permalink("components", "segments-widget", { tab: "segments" })));
      await assertResolved(page, "segments-widget");
      await page.evaluate((state) => history.replaceState(state, "", location.href), hostHistoryState);
      await assertOverviewNavigation(page, "segments-widget");
    },
    { canEdit: true },
  );

  await scenario(
    "closing chart details then leaving its tab preserves navigation history",
    async (page) => {
      const link = withHostContext(
        permalink("charts", "segments-chart", {
          alias: "uuid",
          tab: "segments",
          detail: true,
        }),
      );
      await page.goto(link);
      await assertResolved(page, "segments-chart", { detail: true });
      await page.evaluate((state) => history.replaceState(state, "", location.href), hostHistoryState);
      const beforeClose = await historyState(page);
      const dialog = page.getByRole("dialog", { name: "Segment trend", exact: true });
      await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
      await dialog.waitFor({ state: "hidden" });
      await assertResolved(page, "segments-chart");
      assert.equal(new URL(page.url()).pathname, new URL(link).pathname.replace(/\/detail$/u, ""));
      await assertHostContext(page);
      assert.deepEqual(
        await historyState(page),
        beforeClose,
        "Closing linked details must replace, not push, history.",
      );
      await assertOverviewNavigation(page, "segments-chart");
    },
    { canEdit: true },
  );

  assert.deepEqual(failures, [], `${JSON.stringify({ passed, failures }, null, 2)}`);
  process.stdout.write(`${JSON.stringify({ status: "passed", scenarios: passed }, null, 2)}\n`);
} finally {
  await browser?.close();
  rmSync(projectRoot, { recursive: true, force: true });
}

SHA-256: d54c7640023c2e4e5a10af7a5bdc7f5078386fcb1459a0ec778ca34714a41874