import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { ACCENTS, BODY_SHAPES, HAIR_COLORS, OUTFITS, SKIN_TONES, createDefaultLocalProfile, createProfileEditor, type LocalProfile, } from "../profile/index.ts"; type Listener = (event: FakeEvent) => void; class FakeEvent { defaultPrevented = false; readonly type: string; readonly target: FakeElement; readonly key: string; readonly ctrlKey: boolean; readonly metaKey: boolean; readonly shiftKey: boolean; constructor( type: string, target: FakeElement, key = "", ctrlKey = false, metaKey = false, shiftKey = false, ) { this.type = type; this.target = target; this.key = key; this.ctrlKey = ctrlKey; this.metaKey = metaKey; this.shiftKey = shiftKey; } preventDefault(): void { this.defaultPrevented = true; } } class FakeElement { readonly children: FakeElement[] = []; readonly attributes = new Map(); readonly listeners = new Map(); parentElement: FakeElement | null = null; className = ""; textContent = ""; hidden = false; value = ""; id = ""; type = ""; maxLength = 0; autocomplete = ""; disabled = false; readonly ownerDocument: FakeDocument; readonly tagName: string; constructor(ownerDocument: FakeDocument, tagName: string) { this.ownerDocument = ownerDocument; this.tagName = tagName; } append(...nodes: FakeElement[]): void { for (const node of nodes) { node.parentElement = this; this.children.push(node); } } setAttribute(name: string, value: string): void { this.attributes.set(name, value); } getAttribute(name: string): string | null { return this.attributes.get(name) ?? null; } addEventListener(type: string, listener: Listener): void { const found = this.listeners.get(type); if (found) found.push(listener); else this.listeners.set(type, [listener]); } dispatch(type: string, init: Partial> = {}): FakeEvent { const event = new FakeEvent(type, this, init.key, init.ctrlKey, init.metaKey, init.shiftKey); for (const listener of this.listeners.get(type) ?? []) listener(event); return event; } focus(): void { this.ownerDocument.activeElement = this; } remove(): void { if (!this.parentElement) return; const index = this.parentElement.children.indexOf(this); if (index >= 0) this.parentElement.children.splice(index, 1); this.parentElement = null; } find(attribute: string, value: string): FakeElement { if (this.attributes.get(attribute) === value) return this; for (const child of this.children) { try { return child.find(attribute, value); } catch { /* continue */ } } throw new Error(`missing [${attribute}=${value}]`); } descendants(tagName: string): FakeElement[] { const result: FakeElement[] = []; for (const child of this.children) { if (child.tagName === tagName.toUpperCase()) result.push(child); result.push(...child.descendants(tagName)); } return result; } } class FakeDocument { activeElement: FakeElement | null = null; createElement(tagName: string): FakeElement { return new FakeElement(this, tagName.toUpperCase()); } } function setup(profile = createDefaultLocalProfile("member-22", "Avery")) { const document = new FakeDocument(); const container = document.createElement("div"); const previews: LocalProfile[] = []; const saves: LocalProfile[] = []; let cancels = 0; const editor = createProfileEditor({ container: container as unknown as HTMLElement, profile, identityId: "member-22", onPreview: (next) => previews.push(next), onSave: (next) => saves.push(next), onCancel: () => cancels++, }); return { document, container, editor, previews, saves, cancels: () => cancels }; } describe("profile editor DOM adapter", () => { it("builds a closed labelled dialog with every enumerated appearance choice", () => { const { container, editor } = setup(); const root = editor.root as unknown as FakeElement; assert.equal(container.children.length, 1); assert.equal(root.hidden, true); assert.equal(root.getAttribute("role"), "dialog"); assert.equal(root.getAttribute("aria-modal"), "true"); assert.ok(root.getAttribute("aria-labelledby")); assert.equal(root.find("data-field", "displayName").getAttribute("aria-describedby"), root.descendants("p")[1]?.id); assert.equal(root.find("data-field", "skinTone").children.length, SKIN_TONES.length); assert.equal(root.find("data-field", "outfit").children.length, OUTFITS.length); assert.equal(root.find("data-field", "accent").children.length, ACCENTS.length); assert.equal(root.find("data-field", "hair").children.length, HAIR_COLORS.length); assert.equal(root.find("data-field", "bodyShape").children.length, BODY_SHAPES.length); assert.equal(editor.state().open, false); }); it("opens with focus, edits preview data, saves, and restores invoker focus", () => { const { document, editor, previews, saves } = setup(); const trigger = document.createElement("button"); trigger.focus(); editor.open(); const root = editor.root as unknown as FakeElement; const name = root.find("data-field", "displayName"); assert.equal(document.activeElement, name); assert.equal(root.getAttribute("aria-hidden"), "false"); name.value = "Avery Chen"; name.dispatch("input"); const outfit = root.find("data-field", "outfit"); outfit.value = "sage"; outfit.dispatch("change"); assert.equal(editor.state().draft.displayName, "Avery Chen"); assert.equal(editor.state().draft.appearance.outfit, "sage"); assert.equal(editor.state().dirty, true); assert.equal(previews.at(-1)?.appearance.outfit, "sage"); root.find("data-action", "save").dispatch("click"); assert.equal(saves.length, 1); assert.equal(saves[0]?.displayName, "Avery Chen"); assert.equal(editor.state().open, false); assert.equal(document.activeElement, trigger); }); it("blocks invalid names and exposes accessible validation", () => { const { document, editor, saves } = setup(); editor.open(); const root = editor.root as unknown as FakeElement; const name = root.find("data-field", "displayName"); const save = root.find("data-action", "save"); name.value = " "; name.dispatch("input"); assert.equal(editor.state().valid, false); assert.equal(name.getAttribute("aria-invalid"), "true"); assert.equal(save.disabled, true); // Form submit covers Enter/assistive submit even though a disabled click cannot fire in a browser. root.descendants("form")[0]?.dispatch("submit"); assert.equal(saves.length, 0); assert.equal(document.activeElement, name); }); it("resets appearance deterministically and Escape cancels the draft", () => { const profile = createDefaultLocalProfile("some-other-id", "River"); const { editor, previews, cancels } = setup(profile); editor.open(); const root = editor.root as unknown as FakeElement; const outfit = root.find("data-field", "outfit"); outfit.value = outfit.value === "ink" ? "clay" : "ink"; outfit.dispatch("change"); const name = root.find("data-field", "displayName"); name.value = "River Two"; name.dispatch("input"); root.find("data-action", "reset").dispatch("click"); assert.deepEqual(editor.state().draft.appearance, createDefaultLocalProfile("member-22", "River").appearance); assert.equal(editor.state().draft.displayName, "River Two"); const event = root.dispatch("keydown", { key: "Escape" }); assert.equal(event.defaultPrevented, true); assert.equal(cancels(), 1); assert.deepEqual(editor.state().draft, profile); assert.deepEqual(previews.at(-1), profile); }); it("traps keyboard focus and supports command-enter save", () => { const { document, editor, saves } = setup(); editor.open(); const root = editor.root as unknown as FakeElement; const first = root.find("data-field", "displayName"); const last = root.find("data-action", "save"); last.focus(); assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true); assert.equal(document.activeElement, first); first.focus(); assert.equal(root.dispatch("keydown", { key: "Tab", shiftKey: true }).defaultPrevented, true); assert.equal(document.activeElement, last); assert.equal(root.dispatch("keydown", { key: "Enter", ctrlKey: true }).defaultPrevented, true); assert.equal(saves.length, 1); }); it("includes visible contextual controls in its focus trap and skips hidden ones", () => { const { document, editor } = setup(); const cameraStart = document.createElement("button"); const cameraStop = document.createElement("button"); cameraStop.hidden = true; editor.registerFocusables([ cameraStart as unknown as HTMLElement, cameraStop as unknown as HTMLElement, ]); editor.open(); const root = editor.root as unknown as FakeElement; const first = root.find("data-field", "displayName"); cameraStart.focus(); assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true); assert.equal(document.activeElement, first); cameraStart.hidden = true; const save = root.find("data-action", "save"); save.focus(); assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true); assert.equal(document.activeElement, first); }); it("strictly updates caller data, then disposes without leaving DOM", () => { const { container, editor, previews } = setup(); const next = createDefaultLocalProfile("new-person", "Morgan"); const updated = editor.update(next); assert.deepEqual(updated.profile, next); assert.deepEqual(updated.draft, next); assert.deepEqual(previews.at(-1), next); assert.throws(() => editor.update({ ...next, displayName: "" }), RangeError); editor.open(); editor.dispose(); editor.dispose(); assert.equal(container.children.length, 0); }); });