Files
docmd-io--docmd/packages/plugins/threads/e2e/reactions.e2e.spec.ts
T
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

274 lines
8.0 KiB
TypeScript

/**
* Emoji reaction tests - adapted from old reactions.e2e.spec.ts.
*
* Changes from old plugin:
* - Reactions stored in markdown (not SQLite)
* - Server-rendered reactions in .threads-reactions block
* - Client-side reactions via threads-comment Lit component (tc-* classes)
* - Toggle via docmd.call('threads:toggle-reaction')
*
* NOTE: Each docmd.call() that modifies the markdown file triggers a hot-reload
* via the dev server's file watcher. We must waitForReload() between calls to
* avoid "execution context destroyed" errors.
*/
import { test, expect } from "@playwright/test";
import { Actor } from "./screenplay.ts";
import { cleanThreads, setAuthor, seedAndReload, waitForReload } from "./helpers.ts";
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);
await waitForReload(page);
});
test.describe("emoji reactions API", () => {
test("adding a reaction persists it", async () => {
const { id: threadId, comments } = await seedAndReload(user.page, {
body: "Comment for reaction test",
});
const commentId = comments[0].id;
// Toggle a reaction via API
const reactions = await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
return docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83d\udc4d",
author: "TestUser",
});
},
{ threadId, commentId },
);
expect(reactions).toBeTruthy();
expect(reactions.length).toBe(1);
expect(reactions[0].emoji).toBe("\ud83d\udc4d");
expect(reactions[0].authors).toContain("TestUser");
});
test("toggling same reaction removes it", async () => {
const { id: threadId, comments } = await seedAndReload(user.page, {
body: "Comment for toggle test",
});
const commentId = comments[0].id;
// Add reaction
await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
await docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83d\udc4d",
author: "TestUser",
});
},
{ threadId, commentId },
);
// Wait for hot-reload after file write
await waitForReload(user.page);
// Toggle it off
const reactions = await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
return docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83d\udc4d",
author: "TestUser",
});
},
{ threadId, commentId },
);
// Reaction should be removed
const thumbsReaction = reactions.find((r: any) => r.emoji === "\ud83d\udc4d");
expect(thumbsReaction).toBeFalsy();
});
test("multiple reactions on same comment", async () => {
const { id: threadId, comments } = await seedAndReload(user.page, {
body: "Comment for multi-reaction test",
});
const commentId = comments[0].id;
// Add thumbs up
await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
await docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83d\udc4d",
author: "TestUser",
});
},
{ threadId, commentId },
);
// Wait for hot-reload
await waitForReload(user.page);
// Add party
const reactions = await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
return docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83c\udf89",
author: "TestUser",
});
},
{ threadId, commentId },
);
expect(reactions.length).toBe(2);
expect(reactions.find((r: any) => r.emoji === "\ud83d\udc4d")).toBeTruthy();
expect(reactions.find((r: any) => r.emoji === "\ud83c\udf89")).toBeTruthy();
});
test("reactions persist after re-fetching threads", async () => {
const { id: threadId, comments } = await seedAndReload(user.page, {
body: "Comment for persistence test",
});
const commentId = comments[0].id;
// Add reaction
await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
await docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83d\udc4d",
author: "TestUser",
});
},
{ threadId, commentId },
);
// Wait for hot-reload
await waitForReload(user.page);
// Re-fetch threads
const threads = await user.page.evaluate(async () => {
const file = document.body.dataset["sourceFile"];
return docmd.call("threads:get-threads", { file });
});
const thread = threads.find((t: any) => t.id === threadId);
expect(thread).toBeTruthy();
const comment = thread.comments[0];
expect(comment.reactions).toBeTruthy();
expect(comment.reactions.length).toBe(1);
expect(comment.reactions[0].emoji).toBe("\ud83d\udc4d");
expect(comment.reactions[0].authors).toContain("TestUser");
});
test("multiple authors on same reaction", async () => {
const { id: threadId, comments } = await seedAndReload(user.page, {
body: "Comment for multi-author test",
});
const commentId = comments[0].id;
// TestUser adds thumbs up
await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
await docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83d\udc4d",
author: "TestUser",
});
},
{ threadId, commentId },
);
// Wait for hot-reload
await waitForReload(user.page);
// Alice adds thumbs up
const reactions = await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
return docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83d\udc4d",
author: "Alice",
});
},
{ threadId, commentId },
);
const thumbs = reactions.find((r: any) => r.emoji === "\ud83d\udc4d");
expect(thumbs).toBeTruthy();
expect(thumbs.authors).toContain("TestUser");
expect(thumbs.authors).toContain("Alice");
expect(thumbs.authors.length).toBe(2);
});
});
test.describe("server-rendered reactions", () => {
test("reactions block rendered in server HTML when reactions exist", async () => {
// Seed a thread with a reaction, then reload to see server rendering
const { id: threadId, comments } = await seedAndReload(user.page, {
body: "Comment with reaction",
});
const commentId = comments[0].id;
// Add reaction
await user.page.evaluate(
async ({ threadId, commentId }) => {
const file = document.body.dataset["sourceFile"];
await docmd.call("threads:toggle-reaction", {
file,
threadId,
commentId,
emoji: "\ud83d\udc4d",
author: "TestUser",
});
},
{ threadId, commentId },
);
// Reload to see server-rendered HTML
await waitForReload(user.page);
const thread = user.page.locator(`.threads-thread[data-thread-id="${threadId}"]`);
const reactionsBlock = thread.locator(".threads-reactions");
// If reactions are rendered server-side, the block should be present
if ((await reactionsBlock.count()) > 0) {
await expect(reactionsBlock).toContainText("\ud83d\udc4d");
}
});
});