deer-flow/frontend/tests/unit/core/extensions/surfaces.dom.test.ts
Wenchao An e2f19d8335
feat(plugins): full-stack plugin APIs and bookmarks (#5647)
* feat(plugins): add full-stack contributions and bookmarks example

* ci(plugins): provision bookmark gateway for browser tests

* fix(plugins): authenticate module downloads through configured backend

* fix(plugins): isolate contributions and localize extension UI

* fix(plugins): preserve bookmark agent routing and contain async callbacks

* fix(plugins): pin durable batch workers to app extension snapshots
2026-09-22 11:18:57 +08:00

116 lines
3.1 KiB
TypeScript

import { expect, rs, test } from "@rstest/core";
import { mountSurface } from "@/core/extensions/surfaces";
test("surface cleanup aborts outstanding work and rejects late backend calls", async () => {
const container = document.createElement("div");
const dispose = rs.fn();
const backend = rs.fn(async () => ({}));
let callLater: (
name: string,
payload: Record<string, unknown>,
) => Promise<unknown> = rs.fn();
let signal: AbortSignal | undefined;
const cleanup = mountSurface(
container,
{
id: "picker",
slot: "page",
title: "Picker",
mount(root, context) {
signal = context.signal;
callLater = context.callBackend;
root.textContent = "PLUGIN UI";
return { dispose };
},
},
{
namespace: "community.example",
locale: "en",
settings: {},
threadId: "a",
callBackend: backend,
},
rs.fn(),
);
expect(container.shadowRoot?.textContent).toBe("PLUGIN UI");
await callLater("search", {});
cleanup();
cleanup();
await expect(callLater("search", {})).rejects.toThrow();
expect(backend).toHaveBeenCalledTimes(1);
expect(signal?.aborted).toBe(true);
expect(dispose).toHaveBeenCalledTimes(1);
expect(container.shadowRoot?.textContent).toBe("");
});
test("a failed mount is isolated and cannot leave its partial UI behind", () => {
const error = rs.fn();
const container = document.createElement("div");
mountSurface(
container,
{
id: "bad",
slot: "page",
title: "Broken",
mount(root) {
root.textContent = "partial";
throw new Error("broken");
},
},
{
namespace: "community.example",
locale: "en",
settings: {},
callBackend: rs.fn(),
},
error,
);
expect(error).toHaveBeenCalledTimes(1);
expect(container.shadowRoot?.textContent).toBe("");
});
for (const failMount of [false, true]) {
test(`surface ${failMount ? "mount failure" : "cleanup"} aborts navigation and rejects stale callbacks`, async () => {
let openLater!: (id: string) => Promise<void>;
let pending!: Promise<void>;
let finish!: () => void;
const navigate = rs.fn();
const open = rs.fn(async (_id: string, signal: AbortSignal) => {
await new Promise<void>((resolve) => {
finish = resolve;
});
signal.throwIfAborted();
navigate();
});
const cleanup = mountSurface(
document.createElement("div"),
{
id: "library",
slot: "page",
title: "Library",
mount(_root, context) {
openLater = context.openConversation!;
pending = openLater("thread");
if (failMount) throw new Error("mount failed");
return { dispose: rs.fn() };
},
},
{
namespace: "bookmarks",
locale: "en",
settings: {},
callBackend: rs.fn(),
openConversation: open,
},
rs.fn(),
);
if (!failMount) cleanup();
finish();
await expect(pending).rejects.toThrow();
await expect(openLater("thread")).rejects.toThrow();
expect(open).toHaveBeenCalledTimes(1);
expect(navigate).not.toHaveBeenCalled();
});
}