@floegence/redevplugin-ui 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ import { type MessagePortLike, type MessageEventLike, type PluginJSONObject, type PluginUIElementVNode, type PluginSurfaceContext, type PluginUIActionEvent, type PluginBridgeClientOptions } from "./surface.js";
2
+ /** Business/bridge fixture only. It does not certify Host permissions or WASM execution. */
3
+ export type PluginTestCall = {
4
+ method: string;
5
+ input?: PluginJSONObject;
6
+ tree?: PluginUIElementVNode;
7
+ cancelled: boolean;
8
+ resolve(value: unknown): void;
9
+ reject(error: Error): void;
10
+ };
11
+ export type PluginTestHarnessOptions = {
12
+ context?: PluginSurfaceContext;
13
+ call?: (method: string, input?: PluginJSONObject) => unknown | Promise<unknown>;
14
+ };
15
+ export declare function createPluginTestContext(languageTag?: string, revision?: number): PluginSurfaceContext;
16
+ /** A deterministic transport adapter around the actual production bridge client. */
17
+ export declare function createPluginTestHarness(options?: PluginTestHarnessOptions): {
18
+ mode: "fixture";
19
+ BridgeClient: {
20
+ new (clientOptions?: PluginBridgeClientOptions): {
21
+ render(next: PluginUIElementVNode): Promise<void>;
22
+ readonly surfaceHandle: string;
23
+ readonly timeoutMs: number;
24
+ "__#private@#nextID": number;
25
+ "__#private@#port": MessagePortLike;
26
+ "__#private@#pending": Map<string, {
27
+ resolve: (value: unknown) => void;
28
+ reject: (reason: unknown) => void;
29
+ timer: ReturnType<typeof setTimeout> | undefined;
30
+ abortCleanup?: () => void;
31
+ kind: "json" | "canvas" | "asset";
32
+ identifier?: string;
33
+ }>;
34
+ "__#private@#actionHandlers": Map<string, Set<(event: PluginUIActionEvent) => void>>;
35
+ "__#private@#canvasInputHandlers": Map<string, Set<(event: import("./surface.js").PluginCanvasInputEvent) => void>>;
36
+ "__#private@#keyboardInputHandlers": Set<(event: import("./surface.js").PluginSurfaceKeyboardEvent) => void>;
37
+ "__#private@#lifecycleHandlers": Set<(event: import("./surface.js").BridgeLifecycleEvent) => Promise<void> | void>;
38
+ "__#private@#contextHandlers": Set<(context: PluginSurfaceContext) => void>;
39
+ "__#private@#context"?: PluginSurfaceContext;
40
+ "__#private@#ready": boolean;
41
+ "__#private@#readyPromise": Promise<void>;
42
+ "__#private@#resolveReady": () => void;
43
+ "__#private@#rejectReady": (reason: unknown) => void;
44
+ "__#private@#lifecycleState": "active" | "quiescing" | "quiesced" | "disposed";
45
+ "__#private@#handlingLifecycle": boolean;
46
+ "__#private@#renderRevision": number;
47
+ "__#private@#committedTree"?: PluginUIElementVNode;
48
+ "__#private@#pendingRender"?: {
49
+ tree: PluginUIElementVNode;
50
+ waiters: Array<{
51
+ resolve: () => void;
52
+ reject: (reason: unknown) => void;
53
+ }>;
54
+ };
55
+ "__#private@#renderLoop"?: Promise<void>;
56
+ "__#private@#controlEditRevisions": Map<string, number>;
57
+ "__#private@#onMessage": (event: MessageEventLike) => void;
58
+ ready(): Promise<void>;
59
+ call<T = unknown>(method: string, params?: PluginJSONObject, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<T>;
60
+ "__#private@#call"<T>(method: string, params: PluginJSONObject | undefined, options: import("./surface.js").PluginBridgeRequestOptions, mutation: boolean): Promise<T>;
61
+ executionEvents(executionID: string, afterCursor: number, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<import("./surface.js").PluginExecutionEventList>;
62
+ cancelExecution(executionID: string, reason?: string, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<void>;
63
+ executionSnapshot(executionID: string, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<import("./surface.js").PluginExecutionSnapshot>;
64
+ context(): PluginSurfaceContext | undefined;
65
+ onContext(handler: (context: PluginSurfaceContext) => void): () => void;
66
+ openCanvas(canvasId: string): Promise<import("./surface.js").PluginCanvasSurface>;
67
+ updateCanvasAccessibility(canvasId: string, state: import("./surface.js").PluginCanvasAccessibilityState): Promise<void>;
68
+ setKeyboardBindings(bindings: readonly import("./surface.js").PluginSurfaceKeyboardBinding[]): Promise<void>;
69
+ onKeyboardInput(handler: (event: import("./surface.js").PluginSurfaceKeyboardEvent) => void): () => void;
70
+ loadImageAsset(assetId: string): Promise<ImageBitmap>;
71
+ exportFile(request: import("./surface.js").PluginFileExportRequest, options?: import("./surface.js").PluginBridgeRequestOptions): Promise<void>;
72
+ onAction(action: string, handler: (event: PluginUIActionEvent) => void): () => void;
73
+ onCanvasInput(canvasId: string, handler: (event: import("./surface.js").PluginCanvasInputEvent) => void): () => void;
74
+ onLifecycle(handler: (event: import("./surface.js").BridgeLifecycleEvent) => Promise<void> | void): () => void;
75
+ dispose(): void;
76
+ "__#private@#drainRenders"(): Promise<void>;
77
+ "__#private@#startRenderLoop"(): void;
78
+ "__#private@#request"<T>(id: string, message: unknown, options?: {
79
+ kind?: "canvas" | "json" | "asset";
80
+ identifier?: string;
81
+ cancellationKind?: "bridge" | "stream";
82
+ mutation?: boolean;
83
+ signal?: AbortSignal;
84
+ }): Promise<T>;
85
+ "__#private@#dispatchRequest"<T>(id: string, message: unknown, options: {
86
+ kind?: "canvas" | "json" | "asset";
87
+ identifier?: string;
88
+ cancellationKind?: "bridge" | "stream";
89
+ mutation?: boolean;
90
+ signal?: AbortSignal;
91
+ }, transfer?: readonly unknown[]): Promise<T>;
92
+ "__#private@#takePending"(id: string): {
93
+ resolve: (value: unknown) => void;
94
+ reject: (reason: unknown) => void;
95
+ timer: ReturnType<typeof setTimeout> | undefined;
96
+ abortCleanup?: () => void;
97
+ kind: "json" | "canvas" | "asset";
98
+ identifier?: string;
99
+ } | undefined;
100
+ "__#private@#handleMessage"(event: MessageEventLike): Promise<void>;
101
+ "__#private@#requestID"(prefix: string): string;
102
+ "__#private@#assertActive"(): void;
103
+ "__#private@#acceptsPluginWork"(): boolean;
104
+ };
105
+ };
106
+ calls: PluginTestCall[];
107
+ readonly tree: PluginUIElementVNode | undefined;
108
+ context(next: PluginSurfaceContext): void;
109
+ action(action: string, event?: Partial<PluginUIActionEvent>): void;
110
+ dispose(): void;
111
+ };
@@ -0,0 +1,83 @@
1
+ import { PluginBridgeClient } from "./surface.js";
2
+ import { PluginBridgeError } from "./errors.js";
3
+ class FixturePort {
4
+ dispatch;
5
+ listeners = new Set();
6
+ closed = false;
7
+ constructor(dispatch) {
8
+ this.dispatch = dispatch;
9
+ }
10
+ postMessage(message) { if (this.closed)
11
+ throw new Error("Fixture port is closed"); queueMicrotask(() => { if (!this.closed)
12
+ this.dispatch(message); }); }
13
+ addEventListener(_type, listener) { this.listeners.add(listener); }
14
+ removeEventListener(_type, listener) { this.listeners.delete(listener); }
15
+ start() { }
16
+ close() { this.closed = true; this.listeners.clear(); }
17
+ emit(data) { if (!this.closed)
18
+ for (const listener of this.listeners)
19
+ listener({ origin: "null", data }); }
20
+ }
21
+ export function createPluginTestContext(languageTag = "en-US", revision = 1) {
22
+ return { schema_version: "redevplugin.surface_context.v1", revision, appearance: { color_scheme: "dark", colors: {
23
+ canvas: "#101010", surface: "#181818", surface_elevated: "#202020", text: "#ffffff", text_muted: "#aaaaaa", border: "#444444", accent: "#66aaff", accent_text: "#000000", success: "#44cc88", warning: "#ffcc44", danger: "#ff6677", focus: "#88bbff",
24
+ } }, locale: { language_tag: languageTag, direction: "ltr" } };
25
+ }
26
+ /** A deterministic transport adapter around the actual production bridge client. */
27
+ export function createPluginTestHarness(options = {}) {
28
+ const calls = [], pending = new Map();
29
+ const clients = new Set();
30
+ let tree, disposed = false;
31
+ let context = options.context ?? createPluginTestContext();
32
+ const port = new FixturePort(message => {
33
+ const value = message;
34
+ if (value.type === "redevplugin.bridge.cancel") {
35
+ const call = pending.get(value.id);
36
+ if (call)
37
+ call.cancelled = true;
38
+ pending.delete(value.id);
39
+ return;
40
+ }
41
+ if (value.type === "redevplugin.ui.mount" || value.type === "redevplugin.ui.patch") {
42
+ port.emit({ type: "redevplugin.bridge.response", id: value.id, ok: true });
43
+ return;
44
+ }
45
+ if (value.type !== "redevplugin.bridge.call" || !value.request)
46
+ return;
47
+ const request = value.request;
48
+ if (calls.length >= 1024) {
49
+ port.emit({ type: "redevplugin.bridge.response", id: request.id, ok: false, error_code: "PLUGIN_INVALID_REQUEST", error: "Fixture call history exceeds 1024 entries" });
50
+ return;
51
+ }
52
+ const finish = (response) => { if (!pending.delete(request.id))
53
+ return; port.emit({ type: "redevplugin.bridge.response", id: request.id, ...response }); };
54
+ const call = { method: request.method, input: request.params, tree, cancelled: false,
55
+ resolve: value => finish({ ok: true, data: value }),
56
+ reject: failure => finish({ ok: false, error_code: failure instanceof PluginBridgeError ? failure.errorCode : "PLUGIN_CAPABILITY_ERROR", error: failure.message }),
57
+ };
58
+ calls.push(call);
59
+ pending.set(request.id, call);
60
+ if (options.call)
61
+ void Promise.resolve().then(() => options.call(call.method, call.input)).then(call.resolve, failure => call.reject(failure instanceof Error ? failure : new Error(String(failure))));
62
+ });
63
+ class BridgeClient extends PluginBridgeClient {
64
+ constructor(clientOptions = {}) {
65
+ if (clients.size || disposed)
66
+ throw new Error("A fixture owns exactly one bridge client");
67
+ super({ ...clientOptions, port, surfaceHandle: "surface_author_fixture" });
68
+ clients.add(this);
69
+ queueMicrotask(() => { port.emit({ type: "redevplugin.bridge.context", context }); port.emit({ type: "redevplugin.bridge.lifecycle", event: { type: "ready" } }); });
70
+ }
71
+ async render(next) { await super.render(next); tree = structuredClone(next); }
72
+ }
73
+ return {
74
+ mode: "fixture", BridgeClient, calls,
75
+ get tree() { return tree; },
76
+ context(next) { context = next; port.emit({ type: "redevplugin.bridge.context", context }); },
77
+ action(action, event = {}) {
78
+ port.emit({ type: "redevplugin.ui.action", action, event: event.event ?? "click", target_key: event.targetKey ?? "fixture-target", edit_revision: event.editRevision ?? 0, is_composing: event.isComposing ?? false, ...(event.value !== undefined ? { value: event.value } : {}), ...(event.checked !== undefined ? { checked: event.checked } : {}), ...(event.form_data ? { form_data: event.form_data } : {}) });
79
+ },
80
+ dispose() { disposed = true; for (const client of clients)
81
+ client.dispose(); clients.clear(); port.close(); pending.clear(); },
82
+ };
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/redevplugin-ui",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -34,13 +34,17 @@
34
34
  "./local-import": {
35
35
  "types": "./dist/local-import.d.ts",
36
36
  "default": "./dist/local-import.js"
37
+ },
38
+ "./testing": {
39
+ "types": "./dist/testing.d.ts",
40
+ "default": "./dist/testing.js"
37
41
  }
38
42
  },
39
43
  "files": [
40
44
  "dist"
41
45
  ],
42
46
  "dependencies": {
43
- "@floegence/redevplugin-contracts": "3.1.0",
47
+ "@floegence/redevplugin-contracts": "3.2.0",
44
48
  "@noble/hashes": "2.4.0"
45
49
  },
46
50
  "publishConfig": {