12ed7123ab
* 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
410 lines
14 KiB
TypeScript
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);
|
|
});
|
|
});
|