Files
wehub-resource-sync 6db8fca185
docmd CI verification / verify (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:31:55 +08:00

216 lines
7.5 KiB
TypeScript

/**
* Thread creation tests - adapted from old devtalk.e2e.spec.ts "discussions" block
* and filters-general.e2e.spec.ts "general comments" block.
*
* Changes from old plugin:
* - No sidebar panel; threads are inline in the content area
* - "Add comment" popover opens inline editor (not a panel compose form)
* - "New Thread" button replaces "Add general comment" panel button
* - WebSocket actions instead of HTTP API for verification
*/
import { test, expect } from "@playwright/test";
import {
Actor,
NavigateTo,
StartThreadOn,
AddNewThread,
SelectText,
TheHighlightCount,
} from "./screenplay.ts";
import { cleanThreads, setAuthor, waitForReload } from "./helpers.ts";
const contentSelector = "[data-docmd-content] p, .docmd-content p, article p, main p";
let user: Actor;
test.beforeEach(async ({ page }) => {
user = new Actor("TestUser", page);
await page.goto("/");
await page.waitForSelector("threads-app", { state: "attached", timeout: 10_000 });
await setAuthor(page, "TestUser");
await cleanThreads(page);
// Reload to reflect the clean state
await waitForReload(page);
});
test.describe("creating threads via text selection", () => {
test("start a thread by selecting text", async () => {
await user.perform(NavigateTo.path("/"));
await user.page.waitForSelector(contentSelector, { timeout: 5_000 });
const firstParagraph = user.page.locator(contentSelector).first();
const paragraphText = await firstParagraph.textContent();
if (!paragraphText || paragraphText.trim().length < 5) {
test.skip(true, "No suitable text content found to select");
return;
}
const textToSelect = paragraphText.trim().slice(0, 20);
await user.perform(
StartThreadOn.on(contentSelector, textToSelect, "This needs clarification"),
);
// Wait for page to reload with the new thread
await waitForReload(user.page);
// Thread should appear in the content area
const threads = user.page.locator(".threads-thread[data-thread-id]");
// Count may include pre-existing threads from markdown, so just check we have at least 1
const count = await threads.count();
expect(count).toBeGreaterThanOrEqual(1);
});
test("highlights appear after creating a thread", async () => {
await user.perform(NavigateTo.path("/"));
await user.page.waitForSelector(contentSelector, { timeout: 5_000 });
const firstParagraph = user.page.locator(contentSelector).first();
const paragraphText = await firstParagraph.textContent();
if (!paragraphText || paragraphText.trim().length < 5) {
test.skip(true, "No suitable text content found");
return;
}
const highlightsBefore = await user.ask(TheHighlightCount.count());
const textToSelect = paragraphText.trim().slice(0, 15);
await user.perform(
StartThreadOn.on(contentSelector, textToSelect, "Highlight test"),
);
await waitForReload(user.page);
// At least one new highlight should have appeared
const highlightsAfter = await user.ask(TheHighlightCount.count());
expect(highlightsAfter).toBeGreaterThan(highlightsBefore);
});
test("popover shows 'Add comment' after text selection", async () => {
await user.perform(NavigateTo.path("/"));
await user.page.waitForSelector(contentSelector, { timeout: 5_000 });
const firstParagraph = user.page.locator(contentSelector).first();
const paragraphText = await firstParagraph.textContent();
if (!paragraphText || paragraphText.trim().length < 5) {
test.skip(true, "No suitable text content found");
return;
}
const textToSelect = paragraphText.trim().slice(0, 15);
await user.perform(SelectText.within(contentSelector, textToSelect));
const popoverBtn = user.page.locator("threads-popover").getByText("Add comment");
await expect(popoverBtn).toBeVisible({ timeout: 5_000 });
});
});
test.describe("creating general (unanchored) threads", () => {
test("clicking New Thread opens inline editor", async () => {
await user.perform(NavigateTo.path("/"));
const newThreadBtn = user.page.locator(".threads-new-thread-btn").first();
await newThreadBtn.click();
const editor = user.page.locator("threads-inline-editor");
await expect(editor).toBeAttached({ timeout: 5_000 });
});
test("submitting New Thread creates unanchored thread", async () => {
await user.perform(NavigateTo.path("/"));
await user.perform(AddNewThread.withBody("A general observation"));
// After submission, the page should reload and show the new thread
await waitForReload(user.page);
// Verify thread was persisted via WebSocket API
const threads = await user.page.evaluate(async () => {
const file = document.body.dataset["sourceFile"];
return docmd.call("threads:get-threads", { file });
});
const generalThread = threads.find(
(t: any) => t.comments[0]?.body === "A general observation",
);
expect(generalThread).toBeTruthy();
// Unanchored threads have no ==highlight== markup on the page
const highlightForThread = await user.page.locator(
`mark.threads-highlight[data-thread-id="${generalThread.id}"]`,
).count();
expect(highlightForThread).toBe(0);
});
test("general thread has no highlight on page", async () => {
await user.perform(NavigateTo.path("/"));
const highlightsBefore = await user.ask(TheHighlightCount.count());
await user.perform(AddNewThread.withBody("No highlight expected"));
await waitForReload(user.page);
// No new highlights should have appeared
const highlightsAfter = await user.ask(TheHighlightCount.count());
expect(highlightsAfter).toBe(highlightsBefore);
});
});
test.describe("popover behavior", () => {
test("mousedown outside popover dismisses it", async () => {
await user.perform(NavigateTo.path("/"));
await user.page.waitForSelector(contentSelector, { timeout: 5_000 });
const firstParagraph = user.page.locator(contentSelector).first();
const paragraphText = await firstParagraph.textContent();
if (!paragraphText || paragraphText.trim().length < 5) {
test.skip(true, "No suitable text content");
return;
}
const textToSelect = paragraphText.trim().slice(0, 15);
await user.perform(SelectText.within(contentSelector, textToSelect));
await expect(
user.page.locator("threads-popover").getByText("Add comment"),
).toBeVisible({ timeout: 5_000 });
// Click outside
await user.page.locator("h1, body").first().click();
await expect(
user.page.locator("threads-popover").getByText("Add comment"),
).not.toBeVisible({ timeout: 5_000 });
});
test("collapsing text selection hides popover", async () => {
await user.perform(NavigateTo.path("/"));
await user.page.waitForSelector(contentSelector, { timeout: 5_000 });
const firstParagraph = user.page.locator(contentSelector).first();
const paragraphText = await firstParagraph.textContent();
if (!paragraphText || paragraphText.trim().length < 5) {
test.skip(true, "No suitable text content");
return;
}
const textToSelect = paragraphText.trim().slice(0, 15);
await user.perform(SelectText.within(contentSelector, textToSelect));
await expect(
user.page.locator("threads-popover").getByText("Add comment"),
).toBeVisible({ timeout: 5_000 });
// Click to collapse selection
await user.page.mouse.click(10, 10);
await expect(
user.page.locator("threads-popover").getByText("Add comment"),
).not.toBeVisible({ timeout: 5_000 });
});
});