187 lines
7.3 KiB
TypeScript
187 lines
7.3 KiB
TypeScript
import { expect, mod, test } from "./fixtures.ts";
|
|
|
|
// Inline comments: signed HTML-comment notes (`<!-- message — author, YYYY-MM-DD -->`),
|
|
// optionally `@addressee:`-addressed to hand a question off, that render as a
|
|
// card widget when the cursor is outside them, and as raw markdown when the
|
|
// cursor overlaps the block. See plug-api/lib/comments.ts,
|
|
// plugs/index/comment.ts, client/codemirror/comment_widget.ts, and
|
|
// plugs/editor/comments.ts.
|
|
|
|
test.describe("Comment card rendering", () => {
|
|
test.use({
|
|
spaceFiles: {
|
|
"index.md":
|
|
"A claim.\n<!-- @pete: verify — john, 2026-08-04 -->\n",
|
|
},
|
|
});
|
|
|
|
test("conforming comment renders as card; cursor reveals raw", async ({
|
|
sbPage,
|
|
}) => {
|
|
const editor = sbPage.locator("#sb-editor .cm-content");
|
|
const card = editor.locator(".sb-comment-card");
|
|
|
|
// Cursor starts at doc position 0, outside the comment block, so it
|
|
// should already render as a card.
|
|
await expect(card).toBeVisible();
|
|
await expect(card).toContainText("@pete");
|
|
await expect(card).toContainText("verify");
|
|
await expect(editor).not.toContainText("<!-- @pete:");
|
|
|
|
// Move the cursor into the paragraph line (still outside the block) --
|
|
// an explicit "elsewhere" placement before we walk into the block.
|
|
await editor.locator(".cm-line").first().click();
|
|
await expect(card).toBeVisible();
|
|
|
|
// Mouse clicks on the widget itself don't move the *document* cursor
|
|
// into it (CodeMirror's own click-to-cursor mapping on a block-replace
|
|
// widget lands at its edge, and the decorator field explicitly skips
|
|
// recomputation for "select.pointer" transactions -- see
|
|
// decoratorStateField in client/codemirror/util.ts -- to avoid the card
|
|
// flickering away under a plain click). Walking in with the keyboard is
|
|
// a real, distinct transaction and does trigger recomputation.
|
|
await sbPage.keyboard.press("End");
|
|
await sbPage.keyboard.press("ArrowDown");
|
|
|
|
await expect(card).toHaveCount(0);
|
|
await expect(editor).toContainText("<!-- @pete:");
|
|
await expect(editor).toContainText("verify");
|
|
});
|
|
|
|
test("alt-click on the card reveals raw source", async ({ sbPage }) => {
|
|
const editor = sbPage.locator("#sb-editor .cm-content");
|
|
const card = editor.locator(".sb-comment-card");
|
|
await expect(card).toBeVisible();
|
|
|
|
await card.click({ modifiers: ["Alt"] });
|
|
|
|
await expect(card).toHaveCount(0);
|
|
await expect(editor).toContainText("<!-- @pete:");
|
|
});
|
|
});
|
|
|
|
test.describe("Comment: Add command", () => {
|
|
test.use({
|
|
spaceFiles: {
|
|
"index.md": "Some claim worth quoting.\n",
|
|
},
|
|
});
|
|
|
|
test("Comment: Add inserts scaffold with quoted selection", async ({
|
|
sbPage,
|
|
}) => {
|
|
const editor = sbPage.locator("#sb-editor .cm-content");
|
|
await expect(editor).toContainText("Some claim worth quoting.");
|
|
|
|
// Select "claim worth" (offsets 5..16 in "Some claim worth quoting.").
|
|
await editor.click();
|
|
await sbPage.evaluate(() => {
|
|
const view = (globalThis as any).client.editorView;
|
|
view.dispatch({ selection: { anchor: 5, head: 16 } });
|
|
});
|
|
|
|
// Run "Comment: Add" via its keybinding (Cmd-Alt-c on mac, Ctrl-Alt-c
|
|
// elsewhere -- see plugs/editor/editor.plug.yaml).
|
|
await sbPage.keyboard.press(`${mod}+Alt+c`);
|
|
|
|
await expect(editor).toContainText('<!-- re: "claim worth"');
|
|
|
|
const result = await sbPage.evaluate(() => {
|
|
const view = (globalThis as any).client.editorView;
|
|
const pos = view.state.selection.main.head;
|
|
return {
|
|
// Nothing has been typed yet, so the message position sits right
|
|
// before the signature's leading " — ".
|
|
afterCursor: view.state.doc.sliceString(pos, pos + 15),
|
|
};
|
|
});
|
|
// Cursor lands at the message position, before the " — <date>" signature
|
|
// -- there's no "@" addressee slot to land in any more (addressing is
|
|
// optional; see plug-api/lib/comments.ts buildCommentScaffold).
|
|
expect(result.afterCursor.trimStart().startsWith("—")).toBe(true);
|
|
await expect(editor).not.toContainText("@:");
|
|
});
|
|
});
|
|
|
|
test.describe("Comment reply/resolve round-trip", () => {
|
|
test.use({
|
|
spaceFiles: {
|
|
"index.md":
|
|
"Some text.\n<!-- @pete: verify — john, 2026-08-04 -->\n",
|
|
},
|
|
});
|
|
|
|
test("reply and resolve round-trip with undo", async ({ sbPage }) => {
|
|
const editor = sbPage.locator("#sb-editor .cm-content");
|
|
const card = editor.locator(".sb-comment-card");
|
|
await expect(card).toBeVisible();
|
|
|
|
const docText = () =>
|
|
sbPage.evaluate(() =>
|
|
(globalThis as any).client.editorView.state.doc.toString()
|
|
);
|
|
|
|
// Click Reply -> a pre-addressed line is inserted and the cursor lands
|
|
// ready to type the reply body (see CommentCardWidget.reply()).
|
|
await card.locator("button", { hasText: "Reply" }).click();
|
|
await sbPage.keyboard.type("on it");
|
|
|
|
// Move the cursor back out of the block to collapse it to a card again.
|
|
await sbPage.evaluate(() => {
|
|
(globalThis as any).client.editorView.dispatch({
|
|
selection: { anchor: 0 },
|
|
});
|
|
});
|
|
|
|
await expect(card).toBeVisible();
|
|
await expect(card.locator(".sb-comment-message")).toHaveCount(2);
|
|
await expect(card).toContainText("on it");
|
|
|
|
// Click Resolve -> the whole comment block is removed from the document.
|
|
await card.locator("button", { hasText: "Resolve" }).click();
|
|
await expect.poll(docText).not.toContain("<!-- @pete:");
|
|
|
|
// Undo restores the comment block into the document model (whether it
|
|
// re-renders as a card or as raw text depends on where undo leaves the
|
|
// cursor, so assert on the document content rather than the DOM).
|
|
await editor.click();
|
|
await sbPage.keyboard.press(`${mod}+z`);
|
|
await expect.poll(docText).toContain("<!-- @pete:");
|
|
await expect.poll(docText).toContain("on it");
|
|
});
|
|
});
|
|
|
|
test.describe("Comment on an unaddressed note", () => {
|
|
test.use({
|
|
spaceFiles: {
|
|
"index.md":
|
|
'A claim.\n<!-- re: "the surrounding text"\n Commenting on a specific phrase — 2026-08-05 -->\n',
|
|
},
|
|
});
|
|
|
|
// Reply only makes sense when there's someone to reply to -- an
|
|
// unaddressed note offers Resolve but no Reply button (see
|
|
// CommentCardWidget.toDOM() in client/codemirror/comment_widget.ts).
|
|
// buildReplyInsertion's unaddressed fallback path stays in place as a
|
|
// safeguard for mixed threads (addressed earlier, unaddressed last), but
|
|
// that path is no longer reachable from this card's UI.
|
|
test("an unaddressed note's card offers Resolve but no Reply button", async ({
|
|
sbPage,
|
|
}) => {
|
|
const editor = sbPage.locator("#sb-editor .cm-content");
|
|
const card = editor.locator(".sb-comment-card");
|
|
await expect(card).toBeVisible();
|
|
await expect(card.locator(".sb-comment-message")).toHaveCount(1);
|
|
|
|
await expect(card.locator("button", { hasText: "Resolve" })).toBeVisible();
|
|
await expect(card.locator("button", { hasText: "Reply" })).toHaveCount(0);
|
|
});
|
|
});
|
|
|
|
// "published render hides comments": there is no e2e-reachable surface for
|
|
// rendered/published HTML in this app (no share/preview route -- the client
|
|
// only ever shows the editable CodeMirror view). Per the brief, the fallback
|
|
// lives as a vitest case in client/markdown_renderer/markdown_render.test.ts
|
|
// ("Conforming inline comment renders to nothing"), asserting CommentBlock
|
|
// renders to null (empty HTML) for a conforming comment.
|