← Files DataARCHIVED FILE
tests/data-app-browser-text-editing.mjs
12.7 KB · Sep 30, 2026 · 23:19 UTC
import assert from "node:assert/strict";
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runDataAppFixtureBuild } from "./browser-helpers.mjs";
export async function verifyHostedTextEditing({ browser, pluginRoot }) {
const project = mkdtempSync(join(tmpdir(), "data-hosted-text-"));
const template = join(pluginRoot, "templates/data-app/base");
const contexts = [];
const releaseContentRequests = [];
const errors = [];
const writes = [];
const reads = [];
let record = { canEdit: true, revision: 0, presentation: {} };
try {
cpSync(template, project, {
recursive: true,
filter: (path) => !["node_modules", "dist", "examples"].some((part) =>
path === join(template, part) || path.startsWith(`${join(template, part)}/`)),
});
const snapshotPath = join(project, "src/data.json");
const snapshot = JSON.parse(readFileSync(snapshotPath, "utf8"));
snapshot.id = "hosted-text-regression";
snapshot.title = "Hosted text regression";
snapshot.filters = [];
writeFileSync(snapshotPath, `${JSON.stringify(snapshot)}\n`);
writeFileSync(join(project, "src/content/dashboard/DashboardContent.jsx"), `
import React, { useEffect, useState } from "react";
import { EditableText } from "../../data-app-public.jsx";
export function DashboardContent() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
let active = true;
fetch("/fixture/content-ready").then((response) => response.json()).then(() => {
if (active) setLoaded(true);
});
return () => { active = false; };
}, []);
return <article>
<EditableText data-editable-id="fixture:caption" data-testid="caption">
Original caption
</EditableText>
<EditableText data-editable-id="fixture:formatted" data-testid="formatted-copy">
Original <strong>formatted</strong> statement
</EditableText>
{loaded && <EditableText data-editable-id="fixture:late" data-testid="late-copy">
Original delayed paragraph
</EditableText>}
</article>;
}
`);
const build = runDataAppFixtureBuild(project, { pluginRoot });
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
const html = readFileSync(join(project, "dist/index.html"), "utf8")
.replace(/<meta name="data-app-local-thread"[^>]*>\s*/gu, "");
async function openPublishedPage() {
const context = await browser.newContext({ viewport: { width: 1100, height: 850 } });
contexts.push(context);
const page = await context.newPage();
page.on("pageerror", (error) => errors.push(error.message));
let releaseContent;
const contentReady = new Promise((resolve) => { releaseContent = resolve; });
releaseContentRequests.push(releaseContent);
await page.route("**/*", async (route) => {
const { hostname, pathname } = new URL(route.request().url());
if (hostname !== "text-regression.chatgpt.site") return route.abort();
if (pathname === "/fixture/content-ready") {
await contentReady;
return route.fulfill({ contentType: "application/json", body: "{}" });
}
if (pathname === "/api/presentation") {
if (route.request().method() === "PUT") {
const submitted = route.request().postDataJSON();
assert.equal(submitted.revision, record.revision, "The owner saves against the acknowledged revision");
writes.push(submitted);
record = { canEdit: true, revision: record.revision + 1, presentation: submitted.presentation };
} else {
reads.push(structuredClone(record));
}
return route.fulfill({ contentType: "application/json", body: JSON.stringify(record) });
}
return route.fulfill(pathname === "/api/snapshot"
? { contentType: "application/json", body: JSON.stringify(snapshot) }
: { contentType: "text/html", body: html });
});
await page.goto("https://text-regression.chatgpt.site/published", { waitUntil: "load" });
await page.getByRole("button", { name: "Edit text and layout", exact: true }).waitFor();
assert.equal(await page.getByTestId("late-copy").count(), 0,
"The async fixture must remain unmounted until its response is released");
return { page, releaseContent };
}
async function saveText(page, id, value) {
const saved = page.waitForResponse((response) =>
new URL(response.url()).pathname === "/api/presentation"
&& response.request().method() === "PUT" && response.ok());
await page.getByRole("button", { name: "Save", exact: true }).click();
const response = await saved;
assert.equal(response.request().postDataJSON().presentation.textEdits[id], value);
assert.equal((await response.json()).presentation.textEdits[id], value);
await page.getByRole("button", { name: "Edit text and layout", exact: true }).waitFor();
assert.equal(record.presentation.textEdits[id], value, "Saved text must reach the hosted presentation record");
}
const first = await openPublishedPage();
first.releaseContent();
await first.page.getByTestId("late-copy").waitFor();
await first.page.getByRole("button", { name: "Edit text and layout", exact: true }).click();
const edited = "Saved delayed paragraph";
await first.page.getByTestId("late-copy").fill(edited);
const formattedEdit = "Saved formatted statement";
await first.page.getByTestId("formatted-copy").fill(formattedEdit);
await saveText(first.page, "fixture:late", edited);
assert.equal(record.presentation.textEdits["fixture:formatted"], formattedEdit);
assert.equal(await first.page.getByTestId("formatted-copy").locator("strong").innerText(), "formatted",
"Committing an inline edit must retain authored emphasis");
assert.equal(writes.length, 1);
await first.page.close();
// A fresh browser context proves restoration comes from the server response,
// without any prior DOM, React state, or viewer-local presentation storage.
const restored = await openPublishedPage();
assert.equal(reads.at(-1).presentation.textEdits["fixture:late"], edited);
assert.equal(await restored.page.getByTestId("formatted-copy").innerText(), formattedEdit);
assert.equal(await restored.page.getByTestId("formatted-copy").locator("strong").innerText(), "formatted",
"Restoring saved text in a fresh context must preserve its authored strong element");
restored.releaseContent();
await restored.page.getByTestId("late-copy").waitFor();
assert.equal(await restored.page.getByTestId("late-copy").innerText(), edited,
"Text mounted after the initial hosted render must display its acknowledged saved edit");
await restored.page.close();
const editing = await openPublishedPage();
const page = editing.page;
const caption = page.getByTestId("caption");
await page.getByRole("button", { name: "Edit text and layout", exact: true }).click();
await caption.fill("");
editing.releaseContent();
await page.getByTestId("late-copy").waitFor();
await page.waitForFunction(() => document.querySelector('[data-testid="late-copy"]')?.isContentEditable);
assert.equal(await page.getByTestId("late-copy").innerText(), edited,
"Late content must restore saved text when it joins an active edit session");
assert.equal(await caption.textContent(), "",
"Mounting unrelated async content must not replace a temporarily empty focused draft");
assert.equal(await caption.evaluate((element) => element === document.activeElement), true,
"Discovering late content must preserve the active text editor's focus");
await page.keyboard.insertText("Original caption");
const editedAgain = "Saved after mounting in Edit mode";
await page.getByTestId("late-copy").fill(editedAgain);
await saveText(page, "fixture:late", editedAgain);
const writesBeforeCancel = writes.length;
await page.getByRole("button", { name: "Edit text and layout", exact: true }).click();
await caption.fill("Unsaved first caption edit");
await page.getByRole("button", { name: "Cancel", exact: true }).click();
await page.getByRole("button", { name: "Edit text and layout", exact: true }).waitFor();
assert.equal(await caption.innerText(), "Original caption",
"Cancel must restore authored plain text when no saved override exists");
assert.equal(writes.length, writesBeforeCancel, "Cancel must not save its discarded text");
const savedCaption = "Saved caption baseline";
await page.getByRole("button", { name: "Edit text and layout", exact: true }).click();
await caption.fill(savedCaption);
await saveText(page, "fixture:caption", savedCaption);
await page.getByRole("button", { name: "Edit text and layout", exact: true }).click();
await caption.fill("Unsaved second caption edit");
await caption.press("Tab");
await page.getByRole("button", { name: "Save", exact: true }).focus();
await page.keyboard.press(`${process.platform === "darwin" ? "Meta" : "Control"}+z`);
await page.waitForFunction(() =>
document.querySelector('[aria-label="Redo dashboard change"]')?.disabled === false);
assert.equal(await caption.innerText(), savedCaption,
"Undo must update visible plain text while the edit session stays open");
await page.getByRole("button", { name: "Cancel", exact: true }).click();
await page.getByRole("button", { name: "Edit text and layout", exact: true }).waitFor();
assert.equal(await caption.innerText(), savedCaption);
const writesBeforeRoundTrip = writes.length;
await page.getByRole("button", { name: "Edit text and layout", exact: true }).click();
await caption.fill("Intermediate committed caption");
await caption.press("Tab");
await caption.fill(savedCaption);
await caption.press("Tab");
await page.getByRole("button", { name: "Save", exact: true }).click();
await page.getByRole("button", { name: "Edit text and layout", exact: true }).waitFor();
assert.equal(await caption.innerText(), savedCaption);
assert.equal(record.presentation.textEdits["fixture:caption"], savedCaption,
"Editing A to B, then refocusing and committing A, must not save the intermediate B");
assert.equal(writes.length, writesBeforeRoundTrip, "Returning to the saved text is a net unchanged edit");
await page.getByRole("button", { name: "Edit text and layout", exact: true }).click();
const committedCaption = "Most recently committed caption";
await caption.fill(committedCaption);
await caption.press("Tab");
await caption.fill("Uncommitted caption draft");
await caption.press("Escape");
assert.equal(await caption.innerText(), committedCaption,
"Escape must discard only the focused draft, preserving the most recent committed text");
await saveText(page, "fixture:caption", committedCaption);
await page.getByRole("button", { name: "Edit text and layout", exact: true }).click();
const formatted = page.getByTestId("formatted-copy");
const pastedCaption = "Pasted formatted statement";
const inserted = await formatted.evaluate((element, value) => {
element.focus();
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
const accepted = document.execCommand("insertHTML", false, `<a href="#pasted-draft">${value}</a>`);
return { accepted, pastedLink: Boolean(element.querySelector("a")) };
}, pastedCaption);
assert.deepEqual(inserted, { accepted: true, pastedLink: true },
"The browser must insert a real formatted draft before reconciliation runs");
await page.evaluate(() => new Promise((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(resolve));
}));
assert.equal(await formatted.evaluate((element) => element.isContentEditable && element === document.activeElement), true,
"Temporary pasted descendants must not disable or blur the focused editor");
await formatted.press("Tab");
await saveText(page, "fixture:formatted", pastedCaption);
assert.equal(await formatted.innerText(), pastedCaption);
assert.equal(await formatted.locator("a").count(), 0, "Committing pasted text must remove unauthored links");
assert.equal(await formatted.locator("strong").innerText(), "formatted",
"Committing a formatted paste must retain the paragraph's authored emphasis");
assert.deepEqual(errors, [], "Hosted text editing must not emit browser errors");
} finally {
releaseContentRequests.forEach((release) => release());
await Promise.all(contexts.map((context) => context.close()));
rmSync(project, { recursive: true, force: true });
}
}
SHA-256: d469b68a14926a90ced84f6354cf4e4a398759532e1562a0cd0db5d4aad2c41f