import { afterEach, describe, expect, it, vi } from "vitest"; import { act, render, waitFor } from "@testing-library/react"; import type { PluginInfo, ViewEvent } from "@/bindings"; import { addFileIconProvider, fileIconName, registerExternalIcons } from "@/ui"; import { provideEditorService, type EditorInput, type EditorService } from "@/workbench/contributions"; import { addContributedFileTemplate, fileTemplates, isBuiltinFileTemplate, mergeFileTemplates, } from "@/features/templates/files/store"; import { createMemoryPluginsBackend, setPluginsBackend, webSchemeUrl } from "../backend"; import { defineViewUiPoints } from "../uiPoints"; import { clearUiContributions, syncUiContributions } from "../projection"; import { usePlugins } from "../store"; import * as ptyModule from "../views/ptyBridge"; import { FRAME_TAG, frameDocument } from "../views/frame"; import { WebView, minimalChange, validChanges, type DocumentState, type FrameDocument, type TextChange, } from "../views/WebView"; afterEach(() => { setPluginsBackend(null); usePlugins.setState({ plugins: [] }); }); /** An in-memory document with the custom editor session's versioning. */ function memoryDocument(text: string) { let version = 0; const listeners = new Set<(s: DocumentState) => void>(); const apply = (changes: readonly TextChange[]) => { for (const c of [...changes].reverse()) text = text.slice(0, c.from) + c.insert + text.slice(c.to); version++; }; const doc: FrameDocument & { external(changes: TextChange[]): void; text(): string } = { read: () => ({ version, text, readOnly: false, name: "a.draw" }), edit: (at, changes) => { if (at !== version) throw new Error("stale"); apply(changes); return version; }, subscribe: (l) => { listeners.add(l); return () => listeners.delete(l); }, external: (changes) => { apply(changes); for (const l of listeners) l({ version, text }); }, text: () => text, }; return doc; } describe("web view helpers", () => { it("turns a replacement into the smallest change", () => { expect(minimalChange("abcdef", "abXYef")).toEqual({ from: 2, to: 4, insert: "XY" }); expect(minimalChange("aaa", "aaaa")).toEqual({ from: 3, to: 3, insert: "a" }); expect(minimalChange("abc", "")).toEqual({ from: 0, to: 3, insert: "" }); expect(minimalChange("same", "same")).toBeNull(); }); it("accepts only sorted, in-range, non-overlapping edits", () => { expect( validChanges( [ { from: 0, to: 1, insert: "x" }, { from: 2, to: 2, insert: "y" }, ], 3, ), ).toHaveLength(2); expect(() => validChanges( [ { from: 2, to: 2, insert: "" }, { from: 0, to: 1, insert: "" }, ], 3, ), ).toThrow(); expect(() => validChanges([{ from: 0, to: 4, insert: "" }], 3)).toThrow(); expect(() => validChanges([{ from: 0.5, to: 1, insert: "" }], 3)).toThrow(); expect(() => validChanges("nope", 3)).toThrow(); }); it("spells scheme URLs the way each webview expects", () => { expect(webSchemeUrl("acme.draw/abc/web/index.html", false)).toBe( "openidea-plugin://localhost/acme.draw/abc/web/index.html", ); expect(webSchemeUrl("p/r/web/a b.html", true)).toBe("http://openidea-plugin.localhost/p/r/web/a%20b.html"); }); }); describe("frame bridge", () => { it("asks the workbench for the document and edits it, and hears of changes made elsewhere", async () => { const script = /