de83757754
Implements comprehensive unit test coverage for CoeditingRegistry.syncContext, which derives context keys (cowriting.isCoediting, cowriting.baselineMode) via vscode.commands.executeCommand — previously untested. Added 8 new tests: - isCoediting: true for active editor with coediting doc - isCoediting: false for undefined editor or non-entered doc - baselineMode: uses hook result when set; defaults to "" otherwise - exit idempotency: double exit fires onDidChange only once All assertions inspect the mocked executeCommand call arguments directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
170 lines
6.3 KiB
TypeScript
170 lines
6.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
// The pure set logic lives in a vscode-free helper the class wraps, so unit-test the class
|
|
// with a Memento stub (vitest runs vscode-free; the module imports vscode only for types +
|
|
// EventEmitter — mock it).
|
|
vi.mock("vscode", () => ({
|
|
EventEmitter: class {
|
|
private handlers: Array<(e: unknown) => void> = [];
|
|
event = (h: (e: unknown) => void) => { this.handlers.push(h); return { dispose() {} }; };
|
|
fire(e: unknown) { for (const h of this.handlers) h(e); }
|
|
dispose() {}
|
|
},
|
|
Uri: { parse: (s: string) => ({ toString: () => s }) },
|
|
commands: { executeCommand: vi.fn() },
|
|
window: { activeTextEditor: undefined },
|
|
}));
|
|
import * as vscode from "vscode";
|
|
import { CoeditingRegistry } from "../src/coeditingRegistry";
|
|
|
|
function memento(): { store: Map<string, unknown> } & { get: any; update: any } {
|
|
const store = new Map<string, unknown>();
|
|
return {
|
|
store,
|
|
get: (k: string, dflt?: unknown) => (store.has(k) ? store.get(k) : dflt),
|
|
update: (k: string, v: unknown) => { store.set(k, v); return Promise.resolve(); },
|
|
} as any;
|
|
}
|
|
|
|
describe("CoeditingRegistry", () => {
|
|
it("enter/exit flips membership and fires onDidChange", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const events: Array<{ uri: string; coediting: boolean }> = [];
|
|
reg.onDidChange((e) => events.push(e as any));
|
|
const uri = { toString: () => "file:///a.md" } as any;
|
|
expect(reg.isCoediting(uri)).toBe(false);
|
|
reg.enter(uri);
|
|
expect(reg.isCoediting(uri)).toBe(true);
|
|
reg.exit(uri);
|
|
expect(reg.isCoediting(uri)).toBe(false);
|
|
expect(events).toEqual([
|
|
{ uri: "file:///a.md", coediting: true },
|
|
{ uri: "file:///a.md", coediting: false },
|
|
]);
|
|
});
|
|
|
|
it("persists the set across construction (reload survival)", () => {
|
|
const m = memento();
|
|
const reg1 = new CoeditingRegistry(m as any);
|
|
reg1.enter({ toString: () => "file:///a.md" } as any);
|
|
const reg2 = new CoeditingRegistry(m as any);
|
|
expect(reg2.isCoediting({ toString: () => "file:///a.md" } as any)).toBe(true);
|
|
expect(reg2.list()).toEqual(["file:///a.md"]);
|
|
});
|
|
|
|
it("enter is idempotent (no duplicate events)", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const events: unknown[] = [];
|
|
reg.onDidChange((e) => events.push(e));
|
|
const uri = { toString: () => "file:///a.md" } as any;
|
|
reg.enter(uri);
|
|
reg.enter(uri);
|
|
expect(events).toHaveLength(1);
|
|
});
|
|
|
|
it("exit is idempotent (no duplicate events on double exit)", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const events: unknown[] = [];
|
|
reg.onDidChange((e) => events.push(e));
|
|
const uri = { toString: () => "file:///a.md" } as any;
|
|
reg.enter(uri);
|
|
reg.exit(uri);
|
|
reg.exit(uri);
|
|
expect(events).toHaveLength(2); // enter, then exit (second exit fires nothing)
|
|
});
|
|
|
|
it("syncContext: active editor with coediting doc sets isCoediting=true", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const uri = { toString: () => "file:///a.md" } as any;
|
|
reg.enter(uri);
|
|
|
|
const mockExecuteCommand = vscode.commands.executeCommand as any;
|
|
mockExecuteCommand.mockClear();
|
|
|
|
const editor = { document: { uri } } as any;
|
|
reg.syncContext(editor);
|
|
|
|
expect(mockExecuteCommand).toHaveBeenCalledWith("setContext", "cowriting.isCoediting", true);
|
|
});
|
|
|
|
it("syncContext: undefined editor sets isCoediting=false", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const mockExecuteCommand = vscode.commands.executeCommand as any;
|
|
mockExecuteCommand.mockClear();
|
|
|
|
reg.syncContext(undefined);
|
|
|
|
expect(mockExecuteCommand).toHaveBeenCalledWith("setContext", "cowriting.isCoediting", false);
|
|
});
|
|
|
|
it("syncContext: non-coediting doc sets isCoediting=false", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const mockExecuteCommand = vscode.commands.executeCommand as any;
|
|
mockExecuteCommand.mockClear();
|
|
|
|
const uri = { toString: () => "file:///nonexistent.md" } as any;
|
|
const editor = { document: { uri } } as any;
|
|
reg.syncContext(editor);
|
|
|
|
expect(mockExecuteCommand).toHaveBeenCalledWith("setContext", "cowriting.isCoediting", false);
|
|
});
|
|
|
|
it("syncContext: baselineMode defaults to empty string when not coediting", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const mockExecuteCommand = vscode.commands.executeCommand as any;
|
|
mockExecuteCommand.mockClear();
|
|
|
|
const uri = { toString: () => "file:///a.md" } as any;
|
|
const editor = { document: { uri } } as any;
|
|
reg.syncContext(editor);
|
|
|
|
expect(mockExecuteCommand).toHaveBeenCalledWith("setContext", "cowriting.baselineMode", "");
|
|
});
|
|
|
|
it("syncContext: baselineMode uses hook result when coediting and hook is set", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const uri = { toString: () => "file:///a.md" } as any;
|
|
reg.enter(uri);
|
|
|
|
reg.baselineModeOf = vi.fn((): "head" | "snapshot" | undefined => "snapshot");
|
|
|
|
const mockExecuteCommand = vscode.commands.executeCommand as any;
|
|
mockExecuteCommand.mockClear();
|
|
|
|
const editor = { document: { uri } } as any;
|
|
reg.syncContext(editor);
|
|
|
|
expect(mockExecuteCommand).toHaveBeenCalledWith("setContext", "cowriting.baselineMode", "snapshot");
|
|
});
|
|
|
|
it("syncContext: baselineMode defaults to empty string when hook returns undefined", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const uri = { toString: () => "file:///a.md" } as any;
|
|
reg.enter(uri);
|
|
|
|
reg.baselineModeOf = vi.fn((): "head" | "snapshot" | undefined => undefined);
|
|
|
|
const mockExecuteCommand = vscode.commands.executeCommand as any;
|
|
mockExecuteCommand.mockClear();
|
|
|
|
const editor = { document: { uri } } as any;
|
|
reg.syncContext(editor);
|
|
|
|
expect(mockExecuteCommand).toHaveBeenCalledWith("setContext", "cowriting.baselineMode", "");
|
|
});
|
|
|
|
it("syncContext: calls executeCommand twice (isCoediting and baselineMode)", () => {
|
|
const reg = new CoeditingRegistry(memento() as any);
|
|
const uri = { toString: () => "file:///a.md" } as any;
|
|
reg.enter(uri);
|
|
|
|
const mockExecuteCommand = vscode.commands.executeCommand as any;
|
|
mockExecuteCommand.mockClear();
|
|
|
|
const editor = { document: { uri } } as any;
|
|
reg.syncContext(editor);
|
|
|
|
expect(mockExecuteCommand).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|