← Files DataARCHIVED FILE

templates/data-app/base/tests/chrome-layout.test.mjs

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

↓ Download file

import { stickyFilterState } from "../src/chrome-layout.js";
import assert from "node:assert/strict";
import test from "node:test";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { createServer } from "vite";
import { fileURLToPath } from "node:url";

import { dataAppChromeLayout } from "../src/chrome-layout.js";

test("dashboard and report previews show progress only while app work is active", async () => {
  const server = await createServer({ root: fileURLToPath(new URL("../", import.meta.url)), configFile: false,
    optimizeDeps: { noDiscovery: true, entries: [] }, server: { middlewareMode: true, watch: null, hmr: false }, appType: "custom" });
  try {
    const { DataAppRuntime } = await server.ssrLoadModule("/src/DataAppRuntime.jsx");
    const Content = () => React.createElement("article", null, "Reviewed inventory");
    for (const surface of ["dashboard", "report"]) {
      const snapshot = { id: `progress-${surface}`, surface, title: "Inventory review", status: "draft", queries: {}, filters: [] };
      for (const buildStatus of ["creating", "updating", "in-progress", "complete", "paused", undefined]) {
        const html = renderToStaticMarkup(React.createElement(DataAppRuntime, {
          reviewedSnapshot: { ...snapshot, buildStatus }, hosted: false,
          DashboardContent: Content, ReportContent: Content,
        }));
        assert.equal(/dashboard-authoring-status/u.test(html),
          ["creating", "updating", "in-progress", "paused"].includes(buildStatus), `${surface}/${buildStatus}: progress follows the app lifecycle, not evidence status`);
        const publish = html.match(/<button\b[^>]*aria-label="Publish"[^>]*>/u)?.[0];
        assert.ok(publish, "Draft previews retain their Publish button");
        assert.equal(publish.includes('disabled=""'), ["creating", "updating", "in-progress", "paused"].includes(buildStatus),
          `${surface}/${buildStatus}: publishing is disabled while app work is unfinished`);
        assert.match(html, /Reviewed inventory/u, "Partial content remains available while work continues");
      }
    }
  } finally { await server.close(); }
});

test("unidentified previews mount without constructing automation requests and omit unavailable suggestions", async () => {
  const server = await createServer({ root: fileURLToPath(new URL("../", import.meta.url)), configFile: false,
    optimizeDeps: { noDiscovery: true, entries: [] }, server: { middlewareMode: true, watch: null, hmr: false }, appType: "custom" });
  try {
    const { DataAppTopbar, dataAppAskSuggestions } = await server.ssrLoadModule("/src/components/DataAppChrome.jsx");
    const requests = [];
    const getActionHref = action => {
        requests.push(action);
        if (["schedule-refresh", "alert-changes"].includes(action)) throw new Error("Data app automation requires its exact project path, HTML file, or published URL.");
        return "https://chatgpt.com/";
    };
    const html = renderToStaticMarkup(React.createElement(DataAppTopbar, {
      title: "Preview", mode: "view", generatedAt: "2026-08-24T12:00:00Z", getActionHref,
    }));
    assert.match(html, /Refresh data/);
    assert.ok(!requests.includes("schedule-refresh"));
    assert.ok(!requests.includes("alert-changes"));
    for (const surface of ["dashboard", "report"]) for (const state of [
      { canEdit: false, published: false, mode: "view" },
      { canEdit: false, published: true, mode: "view" },
      { canEdit: true, published: false, mode: "view" },
      { canEdit: true, published: true, mode: "view" },
      { canEdit: true, published: true, mode: "edit", saveStatus: "saving" },
      { canEdit: true, published: false, mode: "edit", saveStatus: "error" },
    ]) {
      requests.length = 0;
      const markup = renderToStaticMarkup(React.createElement(DataAppTopbar, {
        ...state, surface, title: "A long analytical artifact title", getActionHref,
        editHistory: { canUndo: true, canRedo: false, undo() {}, redo() {} },
      }));
      assert.equal((markup.match(/aria-label="Cancel"/gu) ?? []).length, state.mode === "edit" ? 1 : 0);
      assert.equal((markup.match(/data-mode-action="secondary"/gu) ?? []).length, 1,
        "A single secondary button handles the current mode");
      if (state.mode === "edit") assert.match(markup, /aria-label="Save"/u);
      assert.equal((markup.match(/aria-label="Ask ChatGPT"/gu) ?? []).length, state.mode === "edit" ? 0 : 2,
        `${surface}/${state.mode}: Ask is available in pointer and native touch menus outside editing`);
      const moreContents = markup.match(/<button\b[^>]*aria-label="More"[^>]*>([\s\S]*?)<\/button>/u)?.[1];
      if (state.mode === "view") {
        assert.equal((markup.match(/aria-label="Publish(?: changes)?"/gu) ?? []).length, state.published ? 0 : 2,
          `${surface}: Publish is available only before hosting, independently of inline editing permissions`);
        assert.ok(moreContents, `${surface}: More must remain available`);
        assert.match(moreContents, /data-dashboard-icon="more"/u);
        assert.match(moreContents, /width:18px;height:18px/u, "Figma uses an 18px instance, not the icon asset nominal size");
        if (state.canEdit) {
          const editContents = markup.match(/<button\b[^>]*class="[^"]*dashboard-header-edit-button[^"]*"[^>]*>([\s\S]*?)<\/button>/u)?.[1];
          assert.match(editContents, /width:18px;height:18px/u);
        }
        assert.equal(moreContents.replace(/<[^>]*>/gu, ""), "", "More remains an icon-only trigger");
        assert.equal(markup.includes('aria-label="Edit text and layout"'), state.canEdit);
        assert.ok(markup.indexOf('aria-label="More"') < markup.indexOf('aria-label="Ask ChatGPT"'));
      } else {
        assert.equal(moreContents, undefined, "View-only actions are absent while editing");
        assert.match(markup, /aria-label="Cancel"/u);
        assert.match(markup, /aria-label="Save"/u);
        assert.doesNotMatch(markup, /Publish changes|aria-label="Refresh data"/u);
      }
      assert.ok(!requests.includes("edit-in-chatgpt"), "original editing is no longer duplicated in overflow");
      if (state.mode === "edit") {
        assert.match(markup, /aria-label="Undo /u);
      }
    }
    for (const canEdit of [true, false]) {
      const list = dataAppAskSuggestions({ canEdit, surface: "dashboard", getActionHref: () => "https://chatgpt.com/" });
      assert.deepEqual(list.map(item => item.action), [
        "share-summary", "alert-changes", "create-report", ...(canEdit ? ["edit-in-chatgpt"] : []),
      ], "Suggestions keep their priority order and original editing remains owner-only");
      assert.ok(!list.some(item => item.action === "duplicate"), "Create a copy belongs in the overflow menu");
      assert.ok(!list.some(item => item.action === "refresh-document"), "Updating an existing document is no longer a default suggestion");
      assert.equal(list.find(item => item.action === "create-report").label, "Create a report");
      assert.equal(list.find(item => item.action === "create-report").subtext, "Turn these findings into a report");
      if (canEdit) {
        assert.equal(list.at(-1).label, "Change this dashboard");
        assert.equal(list.at(-1).subtext, "Tell ChatGPT what to change");
      }
      assert.ok(!list.some(item => item.action === "copy-link" || item.action === "restore-hidden"));
    }
    assert.equal(dataAppAskSuggestions({ canEdit: true, surface: "report", getActionHref: () => "https://chatgpt.com/" }).at(-1).label,
      "Change this report");
    const suggestions = dataAppAskSuggestions({ getActionHref });
    assert.deepEqual(suggestions.map(item => item.action), ["share-summary", "create-report"],
      "Unavailable suggestions are omitted without changing the remaining priority order");
    const alert = suggestions.find(item => item.action === "alert-changes");
    assert.equal(alert, undefined);
    assert.equal(suggestions.find(item => item.action === "share-summary").href, "https://chatgpt.com/");
    const identified = dataAppAskSuggestions({ getActionHref: () => "https://chatgpt.com/?q=identified" });
    assert.equal(identified.find(item => item.action === "alert-changes").href, "https://chatgpt.com/?q=identified");
  } finally { await server.close(); }
});

test("protected chrome spans the viewport using ordinary dashboard gutters", () => {
  assert.deepEqual(dataAppChromeLayout({
    viewportWidth: 1440, paddingLeft: 40, paddingRight: 40,
  }), {
    "--data-app-safe-chrome-width": "100%",
    "--data-app-safe-chrome-margin": "0px",
    "--data-app-safe-chrome-inset-start": "40px",
    "--data-app-safe-chrome-inset-end": "40px",
  });
});

test("protected chrome keeps viewport gutters for full-bleed authored content", () => {
  const layout = dataAppChromeLayout({ viewportWidth: 1920 });
  assert.equal(layout["--data-app-safe-chrome-inset-start"], "32px");
  assert.equal(layout["--data-app-safe-chrome-inset-end"], "32px");
});

test("protected chrome remains full-width outside narrow editorial reports", () => {
  const layout = dataAppChromeLayout({
    viewportWidth: 1440, paddingLeft: 36, paddingRight: 36,
  });
  assert.equal(layout["--data-app-safe-chrome-inset-start"], "36px");
  assert.equal(layout["--data-app-safe-chrome-inset-end"], "36px");
});

test("protected chrome preserves usable actions on narrow and asymmetric layouts", () => {
  const narrow = dataAppChromeLayout({ viewportWidth: 320 });
  assert.equal(narrow["--data-app-safe-chrome-inset-start"], "16px");
  assert.equal(narrow["--data-app-safe-chrome-inset-end"], "16px");
  const asymmetric = dataAppChromeLayout({
    viewportWidth: 1440, paddingLeft: 24, paddingRight: 40,
  });
  assert.equal(asymmetric["--data-app-safe-chrome-inset-start"], "24px");
  assert.equal(asymmetric["--data-app-safe-chrome-inset-end"], "40px");
});


test("filter chrome sticks after scrolling without inheriting disappearing tabs", () => {
  const geometry = {top:88,bottom:138,headerBottom:88};
  assert.deepEqual(stickyFilterState({...geometry,scrollY:0}),{stuck:false});
  assert.deepEqual(stickyFilterState({...geometry,scrollY:20}),{stuck:true});
  assert.deepEqual(stickyFilterState({...geometry,scrollY:20,controlsHidden:true}),{stuck:true});
  assert.deepEqual(stickyFilterState({...geometry,scrollY:20,top:120}),{stuck:false});
  assert.deepEqual(stickyFilterState({...geometry,scrollY:20,bottom:80}),{stuck:false});
});

SHA-256: e40402cc8242df329b61ab7fd95541504e03ed39af67fb6af58ac4a36296ab38