Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/views/attachments/attachment-preview-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AttachmentPreviewPage, HTML_PREVIEW_ADDRESS_KEY } from "./attachment-pr
import { hashString } from "../editor/utils/hash-string";
import { buildScrollBridge } from "../editor/utils/iframe-scroll-bridge";
import { withFragmentNavShim } from "../editor/utils/iframe-fragment-nav";
import { withWidthBridge } from "../editor/utils/iframe-width-bridge";
import { withLocationBridge } from "../editor/utils/iframe-location-bridge";

const htmlText = "<html><body><h1>Report</h1></body></html>";
Expand Down Expand Up @@ -109,7 +110,7 @@ describe("AttachmentPreviewPage", () => {
</ScrollRestorationProvider>,
);
const iframe = await screen.findByTitle("report.html");
expect(iframe.getAttribute("srcdoc")).toContain(buildScrollBridge(hashString(htmlText)));
expect(iframe.getAttribute("srcdoc")).toContain(buildScrollBridge(hashString(withWidthBridge(htmlText))));
// The iframe stays inside the sandbox the viewer uses.
expect(iframe.getAttribute("sandbox")).toBe("allow-scripts");
expect(adapter.registerExternalSource).toHaveBeenCalledWith(
Expand All @@ -128,15 +129,15 @@ describe("AttachmentPreviewPage", () => {
const iframe = await screen.findByTitle("report.html");
// The address bridge and the fragment-nav shim, but no scroll bridge.
expect(iframe.getAttribute("srcdoc")).toBe(
withLocationBridge(withFragmentNavShim(htmlText), ""),
withLocationBridge(withFragmentNavShim(withWidthBridge(htmlText)), ""),
);
});

it("opens an HTML file at the route's address", async () => {
renderPage(<AttachmentPreviewPage attachmentId="att-1" initialAddress="?s=ia" />);
const iframe = await screen.findByTitle("report.html");
expect(iframe.getAttribute("srcdoc")).toBe(
withLocationBridge(withFragmentNavShim(htmlText), "?s=ia"),
withLocationBridge(withFragmentNavShim(withWidthBridge(htmlText)), "?s=ia"),
);
expect(screen.getByRole("textbox", { name: "Address" })).toHaveValue("?s=ia");
});
Expand All @@ -155,7 +156,7 @@ describe("AttachmentPreviewPage", () => {
);
const iframe = await screen.findByTitle("report.html");
expect(iframe.getAttribute("srcdoc")).toBe(
withLocationBridge(withFragmentNavShim(htmlText), "?s=kept"),
withLocationBridge(withFragmentNavShim(withWidthBridge(htmlText)), "?s=kept"),
);

const input = screen.getByRole("textbox", { name: "Address" });
Expand Down
10 changes: 6 additions & 4 deletions packages/views/editor/attachment-preview-modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ import {
useAttachmentPreview,
} from "./attachment-preview-modal";
import { withFragmentNavShim } from "./utils/iframe-fragment-nav";
import { withWidthBridge } from "./utils/iframe-width-bridge";
import { withLocationBridge } from "./utils/iframe-location-bridge";
import { renderHook, act as hookAct } from "@testing-library/react";

Expand Down Expand Up @@ -337,11 +338,12 @@ describe("AttachmentPreviewModal — dispatch", () => {
// (MUL-2330). The combination with `allow-same-origin` would defeat
// the sandbox, so this assertion must stay exact.
expect(frame?.getAttribute("sandbox")).toBe("allow-scripts");
// srcdoc carries the original HTML between the address bridge in
// srcdoc carries the original HTML (with the width bridge,
// utils/iframe-width-bridge.ts) between the address bridge in
// front (utils/iframe-location-bridge.ts) and the fragment-nav shim
// appended at the end (utils/iframe-fragment-nav.ts).
expect(frame?.getAttribute("srcdoc")).toBe(
withLocationBridge(withFragmentNavShim("<p>hi</p>"), ""),
withLocationBridge(withFragmentNavShim(withWidthBridge("<p>hi</p>")), ""),
);
});
});
Expand All @@ -360,7 +362,7 @@ describe("AttachmentPreviewModal — dispatch", () => {

const frame = document.querySelector("iframe[sandbox]") as HTMLIFrameElement;
expect(frame.getAttribute("srcdoc")).toBe(
withLocationBridge(withFragmentNavShim("<p>hi</p>"), "?s=overview"),
withLocationBridge(withFragmentNavShim(withWidthBridge("<p>hi</p>")), "?s=overview"),
);
expect((input as HTMLInputElement).value).toBe("?s=overview");
});
Expand All @@ -385,7 +387,7 @@ describe("AttachmentPreviewModal — dispatch", () => {

expect(screen.getByLabelText("Address")).toHaveValue("?s=ia");
expect(document.querySelector("iframe[sandbox]")?.getAttribute("srcdoc")).toBe(
withLocationBridge(withFragmentNavShim("<p>hi</p>"), "?s=ia"),
withLocationBridge(withFragmentNavShim(withWidthBridge("<p>hi</p>")), "?s=ia"),
);
});

Expand Down
3 changes: 2 additions & 1 deletion packages/views/editor/attachment-preview-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import {
HtmlViewportFrame,
type HtmlViewport,
} from "./html-viewport-frame";
import { withWidthBridge } from "./utils/iframe-width-bridge";

// Preview source — full attachment, or URL-only (media types only)
//
Expand Down Expand Up @@ -1461,7 +1462,7 @@ function PreviewContent({
) : (
<HtmlViewportFrame viewport={view.htmlViewport}>
{view.renderHtmlFrame({
html: text,
html: withWidthBridge(text),
title: state.filename,
location: view.htmlLocation,
})}
Expand Down
151 changes: 149 additions & 2 deletions packages/views/editor/html-viewport-frame.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,39 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { screen } from "@testing-library/react";
import { act, screen } from "@testing-library/react";
import { renderWithI18n } from "../test/i18n";
import { fitViewport, HtmlViewportFrame } from "./html-viewport-frame";
import { fitViewport, HtmlViewportFrame, nextFitAttempt } from "./html-viewport-frame";
import { HTML_PREVIEW_WIDTH_KEY } from "./utils/iframe-width-bridge";

describe("nextFitAttempt", () => {
const at = (viewport: number, needed: number, doc = "a") => ({ doc, viewport, needed });
const trying = { doc: "a", stage: 1000, width: 1080, failed: false };

it("starts an attempt only from an overflow measured at the stage's width", () => {
expect(nextFitAttempt(null, at(1000, 1080), 1000)).toEqual(trying);
expect(nextFitAttempt(null, at(1000, 1000), 1000)).toBeNull();
expect(nextFitAttempt(null, at(1440, 1600), 1000)).toBeNull();
});

it("keeps an attempt the document fits, and fails one it still overflows", () => {
expect(nextFitAttempt(trying, at(1080, 1080), 1000)).toBe(trying);
expect(nextFitAttempt(trying, at(1080, 1128), 1000)).toEqual({ ...trying, failed: true });
});

it("ignores reports measured at any other width while trying", () => {
expect(nextFitAttempt(trying, at(1000, 1080), 1000)).toBe(trying);
expect(nextFitAttempt(trying, at(1440, 1600), 1000)).toBe(trying);
});

it("never retries a failed attempt for the same document and stage", () => {
const failed = { ...trying, failed: true };
expect(nextFitAttempt(failed, at(1000, 1080), 1000)).toBe(failed);
});

it("starts over for a new document or stage width", () => {
expect(nextFitAttempt(trying, at(1000, 1200, "b"), 1000)).toEqual({ ...trying, doc: "b", width: 1200 });
expect(nextFitAttempt(trying, at(1100, 1100), 1100)).toBeNull();
});
});

describe("fitViewport", () => {
it("keeps the device width and scales it down into a narrower area", () => {
Expand Down Expand Up @@ -60,6 +92,121 @@ describe("HtmlViewportFrame", () => {
expect(screen.getByText("390 × 800")).toBeTruthy();
});

function report(data: Record<string, unknown>, source = screen.getByTitle("doc")) {
act(() => {
window.dispatchEvent(
new MessageEvent("message", {
data: { [HTML_PREVIEW_WIDTH_KEY]: 1, type: "width", ...data },
source: (source as HTMLIFrameElement).contentWindow,
}),
);
});
}

describe("fit", () => {
it("lays a document wider than the stage out at its own width and scales it to fit", () => {
renderWithI18n(<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>);
report({ doc: "a", viewport: 1100, needed: 1320 });

expect(screen.getByTestId("html-viewport").style.width).toBe("1100px");
const layout = screen.getByTitle("doc").parentElement!;
expect(layout.style.width).toBe("1320px");
expect(layout.style.transform).toBe(`scale(${1100 / 1320})`);
expect(screen.getByText(/1320 × 960/)).toHaveTextContent("1320 × 960 · Scaled to 83%");
});

it("keeps the width once the widened document stops overflowing", () => {
renderWithI18n(<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>);
report({ doc: "a", viewport: 1100, needed: 1320 });
report({ doc: "a", viewport: 1320, needed: 1320 });

expect(screen.getByTitle("doc").parentElement!.style.width).toBe("1320px");
});

it("fills the stage again when the widened document still overflows", () => {
// A `width: 100%` box with padding overflows by the same amount at
// any width: widening can only chase it.
renderWithI18n(<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>);
report({ doc: "a", viewport: 1100, needed: 1148 });
report({ doc: "a", viewport: 1148, needed: 1196 });
report({ doc: "a", viewport: 1100, needed: 1148 });

expect(screen.getByTestId("html-viewport").style.width).toBe("100%");
expect(screen.queryByText(/×/)).toBeNull();
});

it("starts over for a new document", () => {
renderWithI18n(<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>);
report({ doc: "a", viewport: 1100, needed: 1320 });
report({ doc: "b", viewport: 1320, needed: 1320 });

expect(screen.getByTestId("html-viewport").style.width).toBe("100%");
expect(screen.queryByText(/×/)).toBeNull();
});

it("fills the stage for a document wider than twice the desktop width", () => {
renderWithI18n(<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>);
report({ doc: "a", viewport: 1100, needed: 9000 });

expect(screen.getByTestId("html-viewport").style.width).toBe("100%");
});

it("does not carry a device width's layout back into Fit", () => {
const { rerender } = renderWithI18n(
<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>,
);
report({ doc: "a", viewport: 1100, needed: 1100 });
rerender(<HtmlViewportFrame viewport="desktop">{frame}</HtmlViewportFrame>);
// Past a breakpoint the document grows wider than its viewport.
report({ doc: "a", viewport: 1440, needed: 1600 });
rerender(<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>);
report({ doc: "a", viewport: 1440, needed: 1600 });

expect(screen.getByTestId("html-viewport").style.width).toBe("100%");
expect(screen.queryByText(/×/)).toBeNull();
});

it("forgets a Fit width on leaving Fit, and measures again on return", () => {
const { rerender } = renderWithI18n(
<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>,
);
report({ doc: "a", viewport: 1100, needed: 1320 });
rerender(<HtmlViewportFrame viewport="tablet">{frame}</HtmlViewportFrame>);
const measure = vi.spyOn(
(screen.getByTitle("doc") as HTMLIFrameElement).contentWindow!,
"postMessage",
);
rerender(<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>);

expect(screen.getByTestId("html-viewport").style.width).toBe("100%");
expect(measure).toHaveBeenCalledWith(
{ [HTML_PREVIEW_WIDTH_KEY]: 1, type: "measure" },
"*",
);
report({ doc: "a", viewport: 1100, needed: 1320 });
expect(screen.getByTitle("doc").parentElement!.style.width).toBe("1320px");
});

it("ignores reports from any other window", () => {
renderWithI18n(
<>
<HtmlViewportFrame viewport="fill">{frame}</HtmlViewportFrame>
<iframe title="other" />
</>,
);
report({ doc: "a", viewport: 1100, needed: 1320 }, screen.getByTitle("other"));

expect(screen.getByTestId("html-viewport").style.width).toBe("100%");
});

it("leaves device widths alone", () => {
renderWithI18n(<HtmlViewportFrame viewport="phone">{frame}</HtmlViewportFrame>);
report({ doc: "a", viewport: 390, needed: 600 });

expect(screen.getByTestId("html-viewport").style.width).toBe("390px");
});
});

it("localizes the scale", () => {
renderWithI18n(<HtmlViewportFrame viewport="desktop">{frame}</HtmlViewportFrame>, {
locale: "zh-Hans",
Expand Down
Loading
Loading