import * as assert from "assert"; import * as fs from "fs"; import * as path from "path"; import * as vscode from "vscode"; import type { CowritingApi } from "../../../src/extension"; const WS = process.env.E2E_WORKSPACE!; const settle = () => new Promise((r) => setTimeout(r, 400)); async function getApi(): Promise { const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!; const api = (await ext.activate()) as CowritingApi; assert.ok(api?.coeditingRegistry, "exports coeditingRegistry"); return api; } /** Write a markdown file under WS and return its uri (without opening it). */ function writeFile(rel: string, body: string): vscode.Uri { const abs = path.join(WS, rel); fs.mkdirSync(path.dirname(abs), { recursive: true }); fs.writeFileSync(abs, body, "utf8"); return vscode.Uri.file(abs); } function pkg(): any { return JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8")); } // Task 8 (native-surfaces migration, spec §6.10): the review-webview's #41 // right-click entries ("Open Cowriting Review Panel") are replaced by // `cowriting.openReviewPreview` — a minimal wrapper that resolves the clicked // document (or falls back to the active editor, mirroring #41's original // behavior), enters coediting if not already (so there is a baseline to review // against), and hands off to VS Code's OWN "Open Preview to the Side" // (`markdown.showPreviewToSide`) — the built-in preview IS the review surface // now (Task 7's annotations render inside it). There is no controller-level // `isOpen` seam anymore (no bespoke webview to introspect); these tests observe // the command's OWN side effects instead — which document it focused/entered // coediting on — the same signal #41's original suite checked via `isOpen`. suite("Open Cowriting Review Preview (host E2E — menu wiring + clicked-doc resolution, Task 8)", () => { test("command with a Uri targets the CLICKED doc, not the active editor", async () => { // Two markdown docs; A is the active editor, B is the right-clicked target. const aUri = writeFile("docs/menu-active.md", "# Active\n\nThe active editor's document.\n"); const bUri = writeFile("docs/menu-clicked.md", "# Clicked\n\nThe right-clicked document.\n"); const aDoc = await vscode.workspace.openTextDocument(aUri); await vscode.window.showTextDocument(aDoc); const bDoc = await vscode.workspace.openTextDocument(bUri); // open but NOT active await settle(); const api = await getApi(); await vscode.commands.executeCommand("cowriting.openReviewPreview", bUri); await settle(); // `markdown.showPreviewToSide` itself ends with the preview WEBVIEW focused // (not a text editor), so `activeTextEditor` is not a stable post-command // signal — `isCoediting` (set by the command's OWN resolve-and-enter step, // before it hands off to the preview) is: the clicked doc B entered, the // still-open-but-not-clicked doc A did not. assert.strictEqual(api.coeditingRegistry.isCoediting(bUri), true, "the clicked doc B entered coediting (a baseline to review against)"); assert.strictEqual(api.coeditingRegistry.isCoediting(aUri), false, "the active-but-not-clicked doc A did NOT enter coediting"); assert.ok(bDoc, "clicked doc handle held"); }); test("command with a Uri for a not-yet-open file opens it and focuses it (explorer case)", async () => { // Write a file but do not openTextDocument it — mimics an Explorer right-click. const uri = writeFile("docs/menu-unopened.md", "# Unopened\n\nNever opened before the right-click.\n"); const key = uri.toString(); const api = await getApi(); assert.ok( !vscode.workspace.textDocuments.some((d) => d.uri.toString() === key), "precondition: the file is not an open document", ); await vscode.commands.executeCommand("cowriting.openReviewPreview", uri); await settle(); assert.ok( vscode.workspace.textDocuments.some((d) => d.uri.toString() === key), "the clicked-but-unopened file was opened", ); assert.strictEqual(api.coeditingRegistry.isCoediting(uri), true, "it entered coediting"); }); test("no-arg invocation still falls back to the active editor (palette / keybinding unchanged)", async () => { const uri = writeFile("docs/menu-noarg.md", "# No-arg\n\nActive editor for the no-arg path.\n"); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); await settle(); const api = await getApi(); await vscode.commands.executeCommand("cowriting.openReviewPreview"); await settle(); assert.strictEqual(api.coeditingRegistry.isCoediting(uri), true, "no-arg targets the active editor"); }); test("a non-markdown doc: command warns, no coediting entered", async () => { const uri = writeFile("docs/menu-notmd.txt", "Not markdown.\n"); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); await settle(); const api = await getApi(); assert.notStrictEqual(doc.languageId, "markdown", "fixture is not markdown"); await vscode.commands.executeCommand("cowriting.openReviewPreview"); await settle(); assert.strictEqual(api.coeditingRegistry.isCoediting(uri), false, "a non-markdown doc never enters coediting via this gateway"); }); test("explorer/context contributes the review-preview item for markdown only", () => { const items = (pkg().contributes.menus["explorer/context"] ?? []) as Array<{ command: string; when?: string }>; const entry = items.find((m) => m.command === "cowriting.openReviewPreview"); assert.ok(entry, "explorer/context has an openReviewPreview entry"); assert.match(entry!.when ?? "", /resourceLangId == markdown/, "gated on markdown resources"); }); test("editor/title/context (tab right-click) contributes the review-preview item for markdown only", () => { const items = (pkg().contributes.menus["editor/title/context"] ?? []) as Array<{ command: string; when?: string }>; const entry = items.find((m) => m.command === "cowriting.openReviewPreview"); assert.ok(entry, "editor/title/context has an openReviewPreview entry"); assert.match(entry!.when ?? "", /resourceLangId == markdown/, "gated on markdown tabs"); }); test("editor/title (toolbar icon) contributes the review-preview item for markdown only", () => { const items = (pkg().contributes.menus["editor/title"] ?? []) as Array<{ command: string; when?: string }>; const entry = items.find((m) => m.command === "cowriting.openReviewPreview"); assert.ok(entry, "editor/title has an openReviewPreview entry"); assert.match(entry!.when ?? "", /resourceLangId == markdown/, "gated on markdown"); }); test("the command title reads 'Open Cowriting Review Preview'", () => { const cmd = (pkg().contributes.commands as Array<{ command: string; title: string }>).find( (c) => c.command === "cowriting.openReviewPreview", ); assert.ok(cmd, "command is contributed"); assert.strictEqual(cmd!.title, "Open Cowriting Review Preview", "menus render this title"); }); test("the old cowriting.showTrackChangesPreview command/menus/keybinding are fully gone (Task 8 exit criterion)", () => { const p = pkg(); const commands = p.contributes.commands as Array<{ command: string }>; assert.ok(!commands.some((c) => c.command === "cowriting.showTrackChangesPreview"), "command contribution removed"); for (const menuId of ["commandPalette", "editor/title", "editor/title/context", "explorer/context"]) { const items = (p.contributes.menus[menuId] ?? []) as Array<{ command: string }>; assert.ok(!items.some((m) => m.command === "cowriting.showTrackChangesPreview"), `${menuId} no longer references it`); } const kb = (p.contributes.keybindings as Array<{ command: string }>).find( (k) => k.command === "cowriting.showTrackChangesPreview", ); assert.ok(!kb, "keybinding removed"); }); test("ctrl+alt+r now targets cowriting.reviewChanges (the native diff), not the retired preview command", () => { const kb = (pkg().contributes.keybindings as Array<{ command: string; key: string; when?: string }>).find( (k) => k.key === "ctrl+alt+r", ); assert.ok(kb, "ctrl+alt+r keybinding still present"); assert.strictEqual(kb!.command, "cowriting.reviewChanges", "the chord now opens the native diff (Step 1 re-point)"); }); });