Network request not attempted
+Standards mode not detected
+ +`, + "utf8", + ); + await workspace.navigateTo(); + + const blockedRequests = watchRequestsTo(page, BLOCKED_PREVIEW_URL); + + await openWorkspaceFile(page, "probe.html"); + + const preview = htmlPreview(page); + await expect( + preview.document.getByRole("heading", { name: "Inline script ran" }), + ).toBeVisible(); + await expect( + preview.document.getByText("Standards mode enabled", { exact: true }), + ).toBeVisible(); + await expect( + preview.document.getByText("Network request blocked", { exact: true }), + ).toBeVisible(); + expect(blockedRequests).toEqual([]); + }); + + test("isolates HTML plans from the app origin and storage", async ({ page, withWorkspace }) => { + test.setTimeout(90_000); + const workspace = await withWorkspace({ prefix: "file-editing-html-origin-" }); + await writeFile( + path.join(workspace.repoPath, "origin.html"), + ` +?
?
+ +`, + "utf8", + ); + await workspace.navigateTo(); + await openWorkspaceFile(page, "origin.html"); + + const preview = htmlPreview(page); + await expect(preview.document.getByText("Parent DOM blocked", { exact: true })).toBeVisible(); + await expect(preview.document.getByText("Storage blocked", { exact: true })).toBeVisible(); + await expect(preview.document.getByText("Cookies blocked", { exact: true })).toBeVisible(); + }); + test("persists Vim keybindings and reports Vim mode with cursor position", async ({ page, withWorkspace, diff --git a/packages/app/src/components/file-pane-render-mode.test.ts b/packages/app/src/components/file-pane-render-mode.test.ts index b57b81711..7a0bcf21e 100644 --- a/packages/app/src/components/file-pane-render-mode.test.ts +++ b/packages/app/src/components/file-pane-render-mode.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { isRenderedMarkdownFile } from "@/components/file-pane-render-mode"; +import { filePreviewRenderKind, isRenderedMarkdownFile } from "@/components/file-pane-render-mode"; describe("isRenderedMarkdownFile", () => { it("detects .md files", () => { @@ -19,5 +19,22 @@ describe("isRenderedMarkdownFile", () => { it("does not treat other text files as rendered markdown", () => { expect(isRenderedMarkdownFile("src/index.ts")).toBe(false); expect(isRenderedMarkdownFile("README.md.txt")).toBe(false); + expect(isRenderedMarkdownFile("plan.html")).toBe(false); + }); +}); + +describe("filePreviewRenderKind", () => { + it("maps each renderable extension to its kind", () => { + expect(filePreviewRenderKind("README.md")).toBe("markdown"); + expect(filePreviewRenderKind("notes.markdown")).toBe("markdown"); + expect(filePreviewRenderKind("plan.html")).toBe("html"); + expect(filePreviewRenderKind("docs/PLAN.HTML")).toBe("html"); + expect(filePreviewRenderKind("plan.htm")).toBe("html"); + }); + + it("returns null for files without a rendered preview", () => { + expect(filePreviewRenderKind("src/index.ts")).toBe(null); + expect(filePreviewRenderKind("page.mdx")).toBe(null); + expect(filePreviewRenderKind("index.html.erb")).toBe(null); }); }); diff --git a/packages/app/src/components/file-pane-render-mode.ts b/packages/app/src/components/file-pane-render-mode.ts index d6f507f9e..b1c36ede4 100644 --- a/packages/app/src/components/file-pane-render-mode.ts +++ b/packages/app/src/components/file-pane-render-mode.ts @@ -1,4 +1,17 @@ +export type FilePreviewRenderKind = "markdown" | "html"; + export function isRenderedMarkdownFile(filePath: string): boolean { const normalizedPath = filePath.trim().toLowerCase(); return normalizedPath.endsWith(".md") || normalizedPath.endsWith(".markdown"); } + +function isRenderedHtmlFile(filePath: string): boolean { + const normalizedPath = filePath.trim().toLowerCase(); + return normalizedPath.endsWith(".html") || normalizedPath.endsWith(".htm"); +} + +export function filePreviewRenderKind(filePath: string): FilePreviewRenderKind | null { + if (isRenderedMarkdownFile(filePath)) return "markdown"; + if (isRenderedHtmlFile(filePath)) return "html"; + return null; +} diff --git a/packages/app/src/file-pane/bar.tsx b/packages/app/src/file-pane/bar.tsx index 5784c072f..b674cc200 100644 --- a/packages/app/src/file-pane/bar.tsx +++ b/packages/app/src/file-pane/bar.tsx @@ -30,7 +30,7 @@ export function FilePanelBar({ conflict?: FileConflictAlertState; }) { const { t } = useTranslation(); - const markdownModes = [ + const previewModes = [ { value: "preview" as const, label: t("panels.file.editor.preview"), @@ -102,8 +102,8 @@ export function FilePanelBar({ size="xs" value={mode} onValueChange={onModeChange} - testID="file-markdown-mode" - options={markdownModes} + testID="file-preview-mode" + options={previewModes} /> ) : null} diff --git a/packages/app/src/file-pane/html-preview-csp.test.ts b/packages/app/src/file-pane/html-preview-csp.test.ts new file mode 100644 index 000000000..bbf3bbb74 --- /dev/null +++ b/packages/app/src/file-pane/html-preview-csp.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { withPreviewCsp } from "@/file-pane/html-preview-csp"; + +describe("withPreviewCsp", () => { + it("places the policy before the complete source document", () => { + const source = + " "; + const output = withPreviewCsp(source); + + expect(output).toMatch(/^")); + }); + + it("keeps the original document intact", () => { + const source = "