e2e/html-preview-fullscreen.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test, type Locator } from "@playwright/test";
import { createTestApi, loginAsDefault } from "./helpers";
import type { TestApiClient } from "./fixtures";
const html = `<style>html,body{margin:0;padding:0}</style>
<div style="height:480px"><input aria-label="Retained input" value="initial"></div>
${"<!-- long source -->\n".repeat(100)}`;
const content = `${"Paragraph before the preview.\n\n".repeat(60)}\`\`\`html\n${html}\n\`\`\``;
// Taller than the 480px collapse point, so the block sits collapsed behind
// "Show all" — enlarging it must not re-expand the inline layout.
const tallHtml = `<style>html,body{margin:0;padding:0}</style>
<div style="height:3000px"><input aria-label="Retained input" value="initial"></div>
${"<!-- long source -->\n".repeat(100)}`;
const tallContent = `${"Paragraph before the preview.\n\n".repeat(60)}\`\`\`html\n${tallHtml}\n\`\`\``;
function readScroll(element: HTMLElement) {
return { top: element.scrollTop, height: element.scrollHeight };
}
async function scrollContainer(block: Locator) {
return block.evaluateHandle((element) => {
for (let parent = element.parentElement; parent; parent = parent.parentElement) {
if (
/auto|scroll/.test(getComputedStyle(parent).overflowY) &&
parent.scrollHeight > parent.clientHeight
) {
return parent;
}
}
throw new Error("Expected the issue's outer scroll container");
});
}
test.describe("HTML preview fullscreen", () => {
let api: TestApiClient;
let workspaceSlug: string;
test.beforeEach(async ({ page, baseURL }) => {
// These fixtures create isolated local workspaces, never shared-site data.
const local = (url: string) =>
["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname);
test.skip(
!local(baseURL!) || !local(process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"),
"Local fixtures only",
);
await page.setViewportSize({ width: 1440, height: 1080 });
api = await createTestApi();
workspaceSlug = await loginAsDefault(page);
});
test.afterEach(async () => {
await api?.cleanup();
});
for (const location of ["description", "reply"] as const) {
test(`${location} keeps its outer scroll position and loaded iframe`, async ({
page,
}, testInfo) => {
const issue = await api.createIssue(`HTML preview ${location}`, {
description: location === "description" ? content : "Reply preview regression",
});
const comment = location === "reply" ? await api.createComment(issue.id, content) : null;
await page.goto(`/${workspaceSlug}/issues/${issue.identifier}`, {
waitUntil: "domcontentloaded",
});
const outer = await scrollContainer(
page.getByText("Paragraph before the preview.", { exact: true }).first(),
);
await outer.evaluate((element) => {
element.scrollTop = element.scrollHeight - element.clientHeight;
});
const block = page.locator(
location === "description"
? ".code-block-wrapper"
: `[data-comment-content="${comment!.id}"] [data-dynamic-block="html"]`,
);
await block.scrollIntoViewIfNeeded();
const iframe = block.locator("iframe");
await expect(iframe).toBeVisible();
await expect.poll(async () => (await iframe.boundingBox())?.height).toBe(480);
const input = iframe.contentFrame().getByRole("textbox", { name: "Retained input" });
await expect(input).toBeVisible();
const frameElement = await iframe.elementHandle();
const frame = (await frameElement!.contentFrame())!;
await input.fill("Keep this document");
const timeOrigin = await frame.evaluate(() => performance.timeOrigin);
const scroller = await scrollContainer(block);
await scroller.evaluate((element) => {
element.scrollTop = element.scrollHeight - element.clientHeight;
});
const before = await scroller.evaluate(readScroll);
expect(before.top).toBeGreaterThan(480);
const blockHeight = (await block.boundingBox())!.height;
const opener = block.getByRole("button", { name: "Fullscreen" });
await block.hover();
await testInfo.attach(`${location}-inline`, {
body: await page.screenshot(),
contentType: "image/png",
});
await expect(opener).toBeInViewport();
for (let round = 0; round < 3; round++) {
await opener.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
if (round === 0)
await testInfo.attach(`${location}-enlarged`, {
body: await page.screenshot(),
contentType: "image/png",
});
expect(
await dialog.locator("iframe").evaluate((element, original) => element === original, frameElement),
).toBe(true);
expect((await dialog.boundingBox())!.height).toBe(972);
await expect.poll(async () => scroller.evaluate(readScroll)).toEqual(before);
expect((await block.boundingBox())!.height).toBe(blockHeight);
await expect(input).toHaveValue("Keep this document");
expect(await frame.evaluate(() => performance.timeOrigin)).toBe(timeOrigin);
if (round === 0) {
await page.getByRole("button", { name: "Close", exact: true }).click();
} else if (round === 1) {
await page.mouse.click(4, 4);
} else {
await page.keyboard.press("Escape");
}
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect.poll(async () => scroller.evaluate(readScroll)).toEqual(before);
expect((await block.boundingBox())!.height).toBe(blockHeight);
expect(
await iframe.evaluate((element, original) => element === original, frameElement),
).toBe(true);
expect(await frame.evaluate(() => performance.timeOrigin)).toBe(timeOrigin);
await expect(opener).toBeFocused();
}
await testInfo.attach(`${location}-restored`, {
body: await page.screenshot(),
contentType: "image/png",
});
await testInfo.attach(`${location}-geometry`, {
body: Buffer.from(
JSON.stringify({
before,
after: await scroller.evaluate(readScroll),
rounds: 3,
blockHeight,
iframeHeight: 480,
fullscreenHeight: 972,
documentRetained: true,
inputRetained: true,
}),
),
contentType: "application/json",
});
if (location === "description") {
await block.getByRole("button", { name: "Show source" }).click();
await opener.scrollIntoViewIfNeeded();
const sourceHeight = (await block.boundingBox())!.height;
const sourceScroll = await scroller.evaluate(readScroll);
await opener.click();
await expect(page.getByRole("dialog").locator("iframe")).toBeVisible();
expect((await block.boundingBox())!.height).toBe(sourceHeight);
expect(await scroller.evaluate(readScroll)).toEqual(sourceScroll);
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toHaveCount(0);
expect((await block.boundingBox())!.height).toBe(sourceHeight);
expect(await scroller.evaluate(readScroll)).toEqual(sourceScroll);
expect(await frame.evaluate(() => performance.timeOrigin)).toBe(timeOrigin);
}
});
}
// Regression: enlarging a collapsed tall block used to drop the inline clip
// and reserve the document's full height in place, so the page re-expanded
// for as long as the dialog was open (518px block growing to 3038px and
// back) — the same total-height jump the collapse clamp exists to prevent.
test("enlarging a collapsed reply block keeps its inline footprint at the clamp", async ({
page,
}, testInfo) => {
const issue = await api.createIssue("Collapsed HTML preview fullscreen");
const comment = await api.createComment(issue.id, tallContent);
await page.goto(`/${workspaceSlug}/issues/${issue.identifier}`, {
waitUntil: "domcontentloaded",
});
const outer = await scrollContainer(
page.getByText("Paragraph before the preview.", { exact: true }).first(),
);
await outer.evaluate((element) => {
element.scrollTop = element.scrollHeight - element.clientHeight;
});
const block = page.locator(
`[data-comment-content="${comment.id}"] [data-dynamic-block="html"]`,
);
await block.scrollIntoViewIfNeeded();
const iframe = block.locator("iframe");
await expect(iframe).toBeVisible();
// Collapsed, not expanded: the 480px clamp plus the frame chrome.
await expect.poll(async () => (await block.boundingBox())?.height).toBe(518);
const blockHeight = (await block.boundingBox())!.height;
const frameElement = await iframe.elementHandle();
const frame = (await frameElement!.contentFrame())!;
const input = iframe.contentFrame().getByRole("textbox", { name: "Retained input" });
await expect(input).toBeVisible();
await input.fill("Keep this document");
const timeOrigin = await frame.evaluate(() => performance.timeOrigin);
const scroller = await scrollContainer(block);
await scroller.evaluate((element) => {
element.scrollTop = element.scrollHeight - element.clientHeight;
});
const before = await scroller.evaluate(readScroll);
expect(before.top).toBeGreaterThan(480);
const opener = block.getByRole("button", { name: "Fullscreen" });
await block.hover();
await testInfo.attach("collapsed-inline", {
body: await page.screenshot(),
contentType: "image/png",
});
await expect(opener).toBeInViewport();
await opener.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await testInfo.attach("collapsed-enlarged", {
body: await page.screenshot(),
contentType: "image/png",
});
expect(
await dialog.locator("iframe").evaluate((element, original) => element === original, frameElement),
).toBe(true);
expect((await dialog.boundingBox())!.height).toBe(972);
// The in-place footprint keeps holding the clamp while enlarged.
await expect(block.locator("[data-collapsed]")).toHaveCount(1);
expect((await block.boundingBox())!.height).toBe(blockHeight);
await expect.poll(async () => scroller.evaluate(readScroll)).toEqual(before);
await expect(input).toHaveValue("Keep this document");
expect(await frame.evaluate(() => performance.timeOrigin)).toBe(timeOrigin);
await page.getByRole("button", { name: "Close", exact: true }).click();
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect(block.locator("[data-collapsed]")).toHaveCount(1);
expect((await block.boundingBox())!.height).toBe(blockHeight);
await expect.poll(async () => scroller.evaluate(readScroll)).toEqual(before);
expect(
await iframe.evaluate((element, original) => element === original, frameElement),
).toBe(true);
expect(await frame.evaluate(() => performance.timeOrigin)).toBe(timeOrigin);
await expect(opener).toBeFocused();
await testInfo.attach("collapsed-restored", {
body: await page.screenshot(),
contentType: "image/png",
});
});
test("reply remeasures content changed while enlarged, including from source", async ({ page }) => {
const interactiveHtml = `<style>html,body{margin:0;padding:0}</style>
<div id="content" style="height:200px">
<input aria-label="Retained input" value="initial">
<button onclick="document.getElementById('content').style.height='1200px'">Expand content</button>
<button onclick="document.getElementById('content').style.height='200px'">Collapse content</button>
</div>`;
const issue = await api.createIssue("HTML preview content resize");
const comment = await api.createComment(issue.id, `\`\`\`html\n${interactiveHtml}\n\`\`\``);
await page.goto(`/${workspaceSlug}/issues/${issue.identifier}`, { waitUntil: "domcontentloaded" });
const block = page.locator(`[data-comment-content="${comment.id}"] [data-dynamic-block="html"]`);
const iframe = block.locator("iframe");
const frame = (await (await iframe.elementHandle())!.contentFrame())!;
const input = iframe.contentFrame().getByRole("textbox", { name: "Retained input" });
await input.fill("Keep this document");
const timeOrigin = await frame.evaluate(() => performance.timeOrigin);
const inlineHeight = () => iframe.evaluate((element) => element.parentElement!.style.height);
await expect.poll(inlineHeight).toBe("200px");
const resizeContent = async (action: string, height: number) => {
const reported = iframe.evaluate((element, expected) => new Promise<void>((resolve) => {
const onMessage = (event: MessageEvent) => {
if (event.source !== (element as HTMLIFrameElement).contentWindow || event.data?.type !== "height" || event.data.height !== expected) return;
window.removeEventListener("message", onMessage);
resolve();
};
window.addEventListener("message", onMessage);
}), height);
await iframe.contentFrame().getByRole("button", { name: action }).click();
// Reproduce the bridge deduplication before closing the enlarged preview.
await reported;
};
for (const [action, height] of [["Expand content", 1200], ["Collapse content", 200]] as const) {
await block.hover();
await block.getByRole("button", { name: "Fullscreen" }).click();
await resizeContent(action, height);
await page.getByRole("dialog").getByRole("button", { name: "Close", exact: true }).click();
await expect.poll(inlineHeight).toBe(`${height}px`);
await expect(block.getByRole("button", { name: "Show all", exact: true })).toHaveCount(height > 480 ? 1 : 0);
await expect(input).toHaveValue("Keep this document");
expect(await frame.evaluate(() => performance.timeOrigin)).toBe(timeOrigin);
}
await block.getByRole("tab", { name: "Source" }).click();
await block.getByRole("button", { name: "Fullscreen" }).click();
await resizeContent("Expand content", 1200);
await page.getByRole("dialog").getByRole("button", { name: "Close", exact: true }).click();
await expect(block.getByRole("tab", { name: "Source" })).toHaveAttribute("aria-selected", "true");
await block.getByRole("tab", { name: "Preview" }).click();
await expect.poll(inlineHeight).toBe("1200px");
await expect(block.getByRole("button", { name: "Show all", exact: true })).toBeVisible();
await expect(input).toHaveValue("Keep this document");
expect(await frame.evaluate(() => performance.timeOrigin)).toBe(timeOrigin);
});
test("enlarging from expanded reply source preserves its panel and outer layout", async ({
page,
}, testInfo) => {
const issue = await api.createIssue("Expanded HTML source regression");
const comment = await api.createComment(issue.id, content);
await page.goto(`/${workspaceSlug}/issues/${issue.identifier}`, {
waitUntil: "domcontentloaded",
});
const outer = await scrollContainer(
page.getByText("Paragraph before the preview.", { exact: true }).first(),
);
await outer.evaluate((element) => {
element.scrollTop = element.scrollHeight - element.clientHeight;
});
const block = page.locator(
`[data-comment-content="${comment.id}"] [data-dynamic-block="html"]`,
);
await block.scrollIntoViewIfNeeded();
await block.getByRole("tab", { name: "Source" }).click();
await block.getByRole("button", { name: "Show all" }).click();
const source = block.locator("pre");
const originalSource = await source.elementHandle();
const beforeHeight = (await block.boundingBox())!.height;
expect(beforeHeight).toBeGreaterThan(480);
const scroller = await scrollContainer(block);
const opener = block.getByRole("button", { name: "Fullscreen" });
await opener.scrollIntoViewIfNeeded();
const before = await scroller.evaluate(readScroll);
await opener.click();
await expect(page.getByRole("dialog").locator("iframe")).toBeVisible();
expect(await originalSource!.evaluate((element) => element.isConnected)).toBe(true);
expect((await block.boundingBox())!.height).toBe(beforeHeight);
expect(await scroller.evaluate(readScroll)).toEqual(before);
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toHaveCount(0);
expect(await source.evaluate((element, original) => element === original, originalSource)).toBe(
true,
);
await expect(block.getByRole("tab", { name: "Source" })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(block.getByRole("button", { name: "Show all" })).toHaveCount(0);
expect((await block.boundingBox())!.height).toBe(beforeHeight);
expect(await scroller.evaluate(readScroll)).toEqual(before);
await expect(opener).toBeFocused();
await testInfo.attach("source-restored", {
body: await page.screenshot(),
contentType: "image/png",
});
await testInfo.attach("source-geometry", {
body: Buffer.from(
JSON.stringify({
before,
after: await scroller.evaluate(readScroll),
sourceHeight: beforeHeight,
expandedSourceRetained: true,
sourceElementRetained: true,
}),
),
contentType: "application/json",
});
});
});