216 lines
7.5 KiB
TypeScript
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 });
|
|
});
|
|
});
|