import { describe, expect, it, vi } from "vitest"; import { POSTPONED, ConflictResolveModal } from "./ConflictResolveModal.ts"; import { CANCELLED, type diff_result, type FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types"; vi.mock("@/deps.ts", () => ({ App: class App {}, Modal: class Modal { createdButtons: string[] = []; private createElement(): Record { const element: Record = { addClass: vi.fn(), addEventListener: vi.fn(), appendText: vi.fn(), classList: { add: vi.fn(), remove: vi.fn(), }, empty: vi.fn(), querySelector: vi.fn(() => null), querySelectorAll: vi.fn(() => []), scrollIntoView: vi.fn(), setText: vi.fn(), }; element.createDiv = vi.fn(() => this.createElement()); element.createEl = vi.fn((_tag: string, _options?: unknown, callback?: (child: unknown) => void) => { if ( _tag === "button" && typeof _options === "object" && _options !== null && "text" in _options ) { this.createdButtons.push(String((_options as { text: unknown }).text)); } const child = this.createElement(); callback?.(child); return child; }); element.createSpan = vi.fn(() => this.createElement()); return element; } contentEl = this.createElement(); titleEl = { setText: vi.fn(), }; close() { (this as { onClose?: () => void }).onClose?.(); } }, })); const conflict: diff_result = { left: { rev: "2-left", data: "left", ctime: 1, mtime: 2 }, right: { rev: "2-right", data: "right", ctime: 1, mtime: 2 }, diff: [], }; describe("ConflictResolveModal result lifecycle", () => { it("returns a response which closes the dialogue before the caller begins waiting", async () => { const modal = new ConflictResolveModal({} as never, "early-response.md" as FilePathWithPrefix, conflict); modal.sendResponse(POSTPONED); const result = await Promise.race([ modal.waitForResult(), new Promise<"timed-out">((resolve) => setTimeout(() => resolve("timed-out"), 250)), ]); expect(result).toBe(POSTPONED); }); it("cancels the previous same-path dialogue without cancelling the replacement", async () => { const filename = "same-path.md" as FilePathWithPrefix; const previous = new ConflictResolveModal({} as never, filename, conflict); const replacement = new ConflictResolveModal({} as never, filename, conflict); previous.onOpen(); replacement.onOpen(); const previousResult = await Promise.race([ previous.waitForResult(), new Promise<"timed-out">((resolve) => setTimeout(() => resolve("timed-out"), 250)), ]); const replacementState = await Promise.race([ replacement.waitForResult(), new Promise<"still-open">((resolve) => setTimeout(() => resolve("still-open"), 25)), ]); previous.sendResponse(CANCELLED); replacement.sendResponse(CANCELLED); expect(previousResult).toBe(CANCELLED); expect(replacementState).toBe("still-open"); }); it("renders a read-only comparison with no resolution actions", () => { const ReadOnlyModal = ConflictResolveModal as unknown as new ( ...args: unknown[] ) => ConflictResolveModal & { createdButtons: string[] }; const modal = new ReadOnlyModal( {}, "repair-preview.md", conflict, false, undefined, { readOnly: true, title: "Vault and database revision", localName: "Vault file", remoteName: "Database revision", } ); modal.onOpen(); expect(modal.createdButtons).toContain("Close"); expect(modal.createdButtons).not.toContain("Use Vault file"); expect(modal.createdButtons).not.toContain("Use Database revision"); expect(modal.createdButtons).not.toContain("Concat both"); expect(modal.createdButtons).not.toContain("Not now"); modal.close(); }); it("does not cancel an active conflict dialogue when a read-only comparison opens for the same file", async () => { const filename = "repair-alongside-conflict.md" as FilePathWithPrefix; const previous = new ConflictResolveModal({} as never, filename, conflict); const ReadOnlyModal = ConflictResolveModal as unknown as new ( ...args: unknown[] ) => ConflictResolveModal; const comparison = new ReadOnlyModal({}, filename, conflict, false, undefined, { readOnly: true, }); previous.onOpen(); comparison.onOpen(); const previousState = await Promise.race([ previous.waitForResult(), new Promise<"still-open">((resolve) => setTimeout(() => resolve("still-open"), 25)), ]); previous.sendResponse(CANCELLED); comparison.close(); expect(previousState).toBe("still-open"); }); });