diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 1447852b..97bfceef 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/block-editor", - "version": "0.1.6", + "version": "0.1.7", "type": "module", "description": "Hydrated accessible editor runtime for WRNexus block documents.", "exports": { diff --git a/packages/block-editor/src/runtime.ts b/packages/block-editor/src/runtime.ts index 0c59b744..c26019c9 100644 --- a/packages/block-editor/src/runtime.ts +++ b/packages/block-editor/src/runtime.ts @@ -31,8 +31,20 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions) live.style.cssText = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0"; root.append(live); + const destroy = () => { + if (destroyed) return; + destroyed = true; + generation++; + try { + cleanups.forEach((cleanup) => cleanup()); + } finally { + cleanups = []; + try { manager?.destroy(); } finally { live.remove(); } + } + }; const refresh = () => { if (destroyed) return; + try { const currentGeneration = ++generation; cleanups.forEach((cleanup) => cleanup()); cleanups = []; @@ -145,18 +157,13 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions) }), ); } + } catch (error) { + try { destroy(); } finally { throw error; } + } }; refresh(); return { refresh, - destroy() { - if (destroyed) return; - destroyed = true; - generation++; - cleanups.forEach((cleanup) => cleanup()); - cleanups = []; - manager?.destroy(); - live.remove(); - }, + destroy, }; } diff --git a/packages/block-editor/test/runtime.test.ts b/packages/block-editor/test/runtime.test.ts index 94ca6ec1..e5f599ff 100644 --- a/packages/block-editor/test/runtime.test.ts +++ b/packages/block-editor/test/runtime.test.ts @@ -9,6 +9,28 @@ const code = new Bun.Transpiler({ loader: "ts" }) .replace(/^import .*;\s*$/gm, "") .replace("export function mountBlockEditor", "function mountBlockEditor"); +test("partial registration failure cleans the backend and live region on mount and refresh", () => { + for (const failingMount of [1, 2]) { + const window = new Window(), root = window.document.createElement("div"); + window.document.body.append(root); + root.innerHTML = '
'; + let mounts = 0, destroyed = 0, cleaned = 0; + const mount = new Function("createDndManager", code + ";return mountBlockEditor;")(() => { + const number = ++mounts; let registrations = 0; + return { registerSortable() { if (++registrations === 2 && number === failingMount) throw new Error("registration failed"); return () => { cleaned++; }; }, destroy() { destroyed++; } }; + }); + let editor: MountedEditor | undefined; + if (failingMount === 1) expect(() => mount(root, { onMove() {} })).toThrow("registration failed"); + else { editor = mount(root, { onMove() {} }); expect(() => editor!.refresh()).toThrow("registration failed"); } + expect(root.querySelector('[role="status"]')).toBeNull(); + expect(root.querySelectorAll('[data-block-id]')).toHaveLength(2); + expect(destroyed).toBe(failingMount); + expect(cleaned).toBe(failingMount === 1 ? 1 : 3); + editor?.destroy(); editor?.refresh(); + expect(destroyed).toBe(failingMount); + } +}); + function fixture(onMove: MountEditorOptions["onMove"] = () => false) { const window = new Window(); const document = window.document;