Files
kandev/apps/web/e2e/tests/chat/markdown-preview.spec.ts
T
José Almeida 12ed7123ab fix: support markdown preview comments (#1648)
* fix: support markdown preview comments

* fix: address markdown preview review feedback

* fix: address markdown preview comment scope

* fix: address markdown preview follow-up feedback

* fix: preserve markdown preview run action in file tabs

* fix: ignore svg clicks in markdown preview controls

* fix: ignore svg markdown preview selection targets
2026-07-10 14:03:32 +01:00

410 lines
14 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { type Page } from "@playwright/test";
import { test, expect } from "../../fixtures/test-base";
import type { SeedData } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
import {
selectMarkdownPreviewRange,
selectMarkdownPreviewText,
} from "../../helpers/markdown-preview";
import { SessionPage } from "../../pages/session-page";
const MARKDOWN_CONTENT = `# Hello World
This is a **markdown** file with some content.
- Item 1
- Item 2
- Item 3
\`\`\`js
console.log("hello");
\`\`\`
`;
async function seedTaskWithSession(
testPage: Page,
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<{ session: SessionPage; sessionId: string }> {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
title,
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.waitForChatIdle({ timeout: 30_000 });
return { session, sessionId: task.session_id };
}
/** Open a markdown file from the Files panel and enable preview mode. */
async function openFileInPreview(
testPage: Page,
session: SessionPage,
fileName: string,
): Promise<void> {
await session.clickTab("Files");
await expect(session.files).toBeVisible({ timeout: 5_000 });
const fileRow = session.files.getByText(fileName);
await expect(fileRow).toBeVisible({ timeout: 10_000 });
await fileRow.click();
const editorTab = testPage.locator(`.dv-default-tab:has-text('${fileName}')`);
await expect(editorTab).toBeVisible({ timeout: 10_000 });
const previewToggle = testPage.getByTestId("markdown-preview-toggle").first();
await expect(previewToggle).toBeVisible({ timeout: 10_000 });
await previewToggle.click();
await expect(testPage.getByTestId("markdown-preview")).toBeVisible({ timeout: 5_000 });
}
/** Open a markdown file from the Files panel and leave it in code mode. */
async function openFileInCode(
testPage: Page,
session: SessionPage,
fileName: string,
): Promise<void> {
await session.clickTab("Files");
await expect(session.files).toBeVisible({ timeout: 5_000 });
const fileRow = session.files.getByText(fileName);
await expect(fileRow).toBeVisible({ timeout: 10_000 });
await fileRow.click();
const editorTab = testPage.locator(`.dv-default-tab:has-text('${fileName}')`);
await expect(editorTab).toBeVisible({ timeout: 10_000 });
await expect(testPage.locator(".monaco-editor").first()).toBeVisible({ timeout: 10_000 });
}
test.describe("Markdown preview", () => {
test.describe.configure({ retries: 1, timeout: 120_000 });
test("toggle markdown preview in file editor", async ({
testPage,
apiClient,
seedData,
backend,
prCapture,
}) => {
// Create a markdown file in the workspace repo before navigating
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
const filePath = path.join(repoDir, "readme.md");
fs.writeFileSync(filePath, MARKDOWN_CONTENT);
const { session } = await seedTaskWithSession(
testPage,
apiClient,
seedData,
"Markdown Preview Test",
);
// Open the Files panel and click on the markdown file
await session.clickTab("Files");
await expect(session.files).toBeVisible({ timeout: 5_000 });
const fileRow = session.files.getByText("readme.md");
await expect(fileRow).toBeVisible({ timeout: 10_000 });
await fileRow.click();
// Wait for the file editor tab to appear
const editorTab = testPage.locator(".dv-default-tab:has-text('readme.md')");
await expect(editorTab).toBeVisible({ timeout: 10_000 });
// The preview toggle button should be visible (only for markdown files)
const previewToggle = testPage.getByTestId("markdown-preview-toggle").first();
await expect(previewToggle).toBeVisible({ timeout: 10_000 });
// Click to enable markdown preview
await previewToggle.click();
// The markdown preview should be visible with rendered content
const preview = testPage.getByTestId("markdown-preview");
await expect(preview).toBeVisible({ timeout: 5_000 });
// Check that markdown is rendered (heading should be an <h1>)
await expect(preview.locator("h1")).toContainText("Hello World");
// Check that list items are rendered
await expect(preview.locator("li")).toHaveCount(3);
// Capture screenshot for PR description (only when CAPTURE_PR_ASSETS=true)
await prCapture.screenshot("markdown-preview-on", {
caption: "Markdown file rendered in preview mode",
});
// Toggle back to code view
const codeToggle = testPage.getByTestId("markdown-preview-toggle").first();
await codeToggle.click();
// Preview should be gone
await expect(preview).not.toBeVisible({ timeout: 5_000 });
});
test("markdown preview persists across page refresh", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
// Create a markdown file
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
const filePath = path.join(repoDir, "persist-test.md");
fs.writeFileSync(filePath, "# Persist Test\n\nSome content here.");
const { session, sessionId } = await seedTaskWithSession(
testPage,
apiClient,
seedData,
"Markdown Persist Test",
);
// Open file and enable preview
await openFileInPreview(testPage, session, "persist-test.md");
await expect(testPage.getByTestId("markdown-preview").locator("h1")).toContainText(
"Persist Test",
);
// Verify the markdownPreview flag is in sessionStorage
const storedTabs = await testPage.evaluate((sid) => {
const raw = window.sessionStorage.getItem(`kandev.openFiles.${sid}`);
return raw ? JSON.parse(raw) : null;
}, sessionId);
expect(storedTabs).not.toBeNull();
const mdTab = storedTabs.find((t: { path: string }) => t.path.endsWith("persist-test.md"));
expect(mdTab).toBeTruthy();
expect(mdTab.markdownPreview).toBe(true);
// Brief pause to let the sessionStorage write settle before reload
await testPage.waitForTimeout(500);
// Reload the page — sessionStorage survives same-URL reload.
// After reload, the restored file tab becomes active (not the chat),
// so we wait for the sidebar instead of the chat panel.
await testPage.reload();
const sessionAfter = new SessionPage(testPage);
await expect(sessionAfter.sidebar).toBeVisible({ timeout: 30_000 });
// The file tab should be restored with preview still active
const editorTabAfter = testPage.locator(".dv-default-tab:has-text('persist-test.md')");
await expect(editorTabAfter).toBeVisible({ timeout: 15_000 });
await editorTabAfter.click();
const previewAfter = testPage.getByTestId("markdown-preview");
await expect(previewAfter).toBeVisible({ timeout: 10_000 });
await expect(previewAfter.locator("h1")).toContainText("Persist Test");
});
test("open markdown preview from diff toolbar", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
// Create a markdown file as an untracked file — it will show in Changes
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
const filePath = path.join(repoDir, "preview-from-diff.md");
fs.writeFileSync(filePath, "# Preview From Diff\n\nThis file was created for the diff test.");
const { session } = await seedTaskWithSession(
testPage,
apiClient,
seedData,
"Markdown Diff Preview Test",
);
// Open Changes panel — the untracked .md file should appear in the file list.
// Click "Changes" tab first, then click the file to open its diff.
await session.clickTab("Changes");
await expect(session.changes).toBeVisible({ timeout: 5_000 });
// Wait for git status to detect the file and show it in the changes list
const fileEntry = session.changes.getByText("preview-from-diff.md");
await expect(fileEntry).toBeVisible({ timeout: 15_000 });
await fileEntry.click();
// The diff opens as a center panel tab "Diff [preview-from-diff.md]".
// Wait for the diff tab and its content to render.
const diffTab = testPage.locator(".dv-default-tab:has-text('preview-from-diff.md')");
await expect(diffTab).toBeVisible({ timeout: 10_000 });
// The diff content should show our markdown text
await expect(testPage.getByText("# Preview From Diff")).toBeVisible({ timeout: 15_000 });
// Click the "Preview markdown" button (eye icon) in the diff header toolbar.
// The button is in the center diff panel, not the right-side changes panel.
const previewBtn = testPage.getByRole("button", { name: "Preview markdown" }).first();
await expect(previewBtn).toBeVisible({ timeout: 10_000 });
await previewBtn.click();
// The file editor should open in preview mode (not code mode).
// The markdown preview should be active immediately (no need to toggle).
const preview = testPage.getByTestId("markdown-preview");
await expect(preview).toBeVisible({ timeout: 10_000 });
await expect(preview.locator("h1")).toContainText("Preview From Diff");
});
test("can add a pending comment from markdown preview selection", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
const filePath = path.join(repoDir, "comment-preview.md");
fs.writeFileSync(filePath, MARKDOWN_CONTENT);
const { session } = await seedTaskWithSession(
testPage,
apiClient,
seedData,
"Markdown Preview Comment Test",
);
await openFileInPreview(testPage, session, "comment-preview.md");
const preview = testPage.getByTestId("markdown-preview");
await selectMarkdownPreviewText(preview.locator("p").first());
const commentButton = testPage.getByTestId("markdown-preview-comment-button");
await expect(commentButton).toBeVisible({ timeout: 5_000 });
await commentButton.click();
await expect(testPage.getByRole("button", { name: "Run" })).toBeVisible();
await testPage
.locator('textarea[placeholder="Add your comment or instruction..."]')
.fill("Please tighten this paragraph.");
await testPage.getByRole("button", { name: "Add", exact: true }).click();
await expect(testPage.getByTestId("markdown-preview-commented-range").first()).toBeVisible({
timeout: 5_000,
});
await session.clickSessionChatTab();
await expect(session.activeChat().getByText("comment-preview.md (1)")).toBeVisible({
timeout: 5_000,
});
});
test("shows one editable badge for a multiline markdown preview comment", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
const filePath = path.join(repoDir, "multiline-preview-comment.md");
fs.writeFileSync(
filePath,
[
"# Multiline Preview Comment",
"",
"First paragraph for the preview selection.",
"",
"Second paragraph for the preview selection.",
"",
"Third paragraph for the preview selection.",
"",
].join("\n"),
);
const { session } = await seedTaskWithSession(
testPage,
apiClient,
seedData,
"Markdown Preview Multiline Comment Test",
);
await openFileInPreview(testPage, session, "multiline-preview-comment.md");
const preview = testPage.getByTestId("markdown-preview");
await selectMarkdownPreviewRange(preview.locator("p").nth(0), preview.locator("p").nth(2));
const commentButton = testPage.getByTestId("markdown-preview-comment-button");
await expect(commentButton).toBeVisible({ timeout: 5_000 });
await commentButton.click();
await testPage
.locator('textarea[placeholder="Add your comment or instruction..."]')
.fill("Please revise these paragraphs.");
await testPage.getByRole("button", { name: "Add", exact: true }).click();
const badges = preview.getByTestId("markdown-preview-comment-badge");
await expect(badges).toHaveCount(1);
await badges.first().click();
await testPage.getByText("Please revise these paragraphs.").hover();
await testPage.getByRole("button", { name: "Edit comment" }).click();
const editTextarea = testPage.locator('textarea[placeholder="Add a comment..."]').last();
await editTextarea.fill("Please tighten these paragraphs.");
await testPage.getByRole("button", { name: "Update", exact: true }).click();
await expect(testPage.getByText("Please tighten these paragraphs.")).toBeVisible({
timeout: 5_000,
});
});
test("shows one code comment marker on the first visual row of a wrapped line", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
const fileName = "wrapped-code-comment.md";
const wrappedLine = Array.from(
{ length: 80 },
(_, i) => `wrapped-word-${i.toString().padStart(2, "0")}`,
).join(" ");
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
fs.writeFileSync(path.join(repoDir, fileName), `${wrappedLine}\n`);
const { session, sessionId } = await seedTaskWithSession(
testPage,
apiClient,
seedData,
"Markdown Code Wrapped Comment Test",
);
await testPage.evaluate(
({ sid, pathName, codeContent }) => {
window.sessionStorage.setItem(
`kandev.comments.${sid}`,
JSON.stringify([
{
id: "wrapped-code-comment",
source: "diff",
sessionId: sid,
filePath: pathName,
startLine: 1,
endLine: 1,
side: "additions",
codeContent,
text: "Review this wrapped line.",
createdAt: new Date().toISOString(),
status: "pending",
},
]),
);
},
{ sid: sessionId, pathName: fileName, codeContent: wrappedLine },
);
await openFileInCode(testPage, session, fileName);
await expect(testPage.locator(".view-line").first()).toContainText("wrapped-word-00", {
timeout: 10_000,
});
await expect
.poll(() => testPage.locator(".view-line").count(), { timeout: 10_000 })
.toBeGreaterThan(1);
await expect
.poll(() => testPage.locator(".monaco-comment-bar-icon").count(), {
timeout: 10_000,
})
.toBe(1);
});
});