import assert from "node:assert/strict" import { readFile } from "node:fs/promises" import test from "node:test" import { deferred, loadTypeScriptModule } from "../lib/test-support/load-typescript-module.mjs" const jsx = (type, props) => ({ type, props }) const jsxRuntime = { jsx, jsxs: jsx, Fragment: "Fragment" } const i18n = { useI18n: () => (key) => key } // Minimal deterministic hook driver: render-phase resets rerender before effects, // and dependency changes/unmount run prior effect cleanup. No browser or network. function hooks() { const states = [], effects = [] let stateIndex, effectIndex, dirty, pending const react = { useState(initial) { const index = stateIndex++ if (!(index in states)) states[index] = typeof initial === "function" ? initial() : initial return [states[index], (next) => { const value = typeof next === "function" ? next(states[index]) : next if (!Object.is(value, states[index])) { states[index] = value; dirty = true } }] }, useEffect(run, deps) { const index = effectIndex++ if (!effects[index] || deps.some((dep, i) => !Object.is(dep, effects[index].deps[i]))) { pending.set(index, { run, deps }) } }, useMemo: (fn) => fn(), useCallback: (fn) => fn, useRef: (value) => ({ current: value }), createContext: () => ({ Provider: "Provider" }), } return { react, render(component, props) { let result, attempts = 0 do { assert.ok(attempts++ < 10, "render-phase state must settle") stateIndex = 0; effectIndex = 0; dirty = false; pending = new Map() result = component(props) } while (dirty) for (const [index, effect] of pending) { effects[index]?.cleanup?.() effects[index] = { deps: effect.deps, cleanup: effect.run() } } return result }, unmount() { effects.forEach((effect) => effect.cleanup?.()) }, } } function find(node, type) { if (!node || typeof node !== "object") return undefined if (node?.type === type) return node const children = node?.props?.children for (const child of Array.isArray(children) ? children : [children]) { const result = find(child, type) if (result) return result } } const flush = async () => { await Promise.resolve(); await Promise.resolve() } test("bulk move resets selection on reopen/base switch and ignores obsolete directory responses", async () => { const h = hooks(), requests = [] const { KnowledgeBulkMoveDialog } = loadTypeScriptModule(new URL("../app/(dashboard)/dashboard/knowledge/_components/knowledge-bulk-move-dialog.tsx", import.meta.url), { react: h.react, "react/jsx-runtime": jsxRuntime, "@/i18n/provider": i18n, "@/components/option-combobox": { OptionCombobox: "Combobox" }, "@/components/project-dialog": { ProjectDialog: "Dialog" }, "@/components/ui/button": { Button: "Button" }, "@/lib/api/admin": { fetchKnowledgeDirectories: (id) => { const request = deferred(); requests.push({ id, ...request }); return request.promise } }, }) let props = { open: true, knowledgeBaseId: 1, moving: false, selectedCount: 2, onOpenChange() {}, onSubmit() {} } let tree = h.render(KnowledgeBulkMoveDialog, props) find(tree, "Combobox").props.onChange("42") tree = h.render(KnowledgeBulkMoveDialog, props) assert.equal(find(tree, "Combobox").props.value, "42") props = { ...props, open: false } h.render(KnowledgeBulkMoveDialog, props) props = { ...props, open: true } tree = h.render(KnowledgeBulkMoveDialog, props) assert.equal(find(tree, "Combobox").props.value, "0") requests[0].resolve([{ id: 99, name: "obsolete" }]); await flush() tree = h.render(KnowledgeBulkMoveDialog, props) assert.equal(find(tree, "Combobox").props.options.length, 1) requests[1].resolve([{ id: 11, name: "previous base" }]); await flush() tree = h.render(KnowledgeBulkMoveDialog, props) assert.equal(find(tree, "Combobox").props.options[1].label, "previous base") find(tree, "Combobox").props.onChange("42") props = { ...props, knowledgeBaseId: 2 } tree = h.render(KnowledgeBulkMoveDialog, props) assert.equal(find(tree, "Combobox").props.value, "0") assert.equal(find(tree, "Combobox").props.options.length, 1) assert.equal(requests.at(-1).id, 2) requests.at(-1).resolve([{ id: 22, name: "current" }]); await flush() tree = h.render(KnowledgeBulkMoveDialog, props) assert.equal(find(tree, "Combobox").props.options[1].label, "current") h.unmount() requests[1].resolve([{ id: 98, name: "old reopening" }]); await flush() }) test("palette snapshots hydrate consistently and notify same-tab and storage changes", () => { const events = new EventTarget(), values = new Map([["dashboard_palette", "green"]]) const document = { documentElement: { dataset: {} } } let store, effect const { PaletteToggle } = loadTypeScriptModule(new URL("./palette-toggle.tsx", import.meta.url), { react: { useSyncExternalStore: (subscribe, getSnapshot, getServerSnapshot) => { store = { subscribe, getSnapshot, getServerSnapshot }; return getSnapshot() }, useEffect: (run) => { effect = run }, }, "react/jsx-runtime": jsxRuntime, "@/i18n/provider": i18n, "lucide-react": { CircleIcon: "Circle", DropletsIcon: "Droplets", PaletteIcon: "Palette" }, "@/components/ui/button": { Button: "Button" }, "@/components/ui/dropdown-menu": Object.fromEntries(["DropdownMenu", "DropdownMenuContent", "DropdownMenuRadioGroup", "DropdownMenuRadioItem", "DropdownMenuTrigger"].map((name) => [name, name])), }, { window: { addEventListener: events.addEventListener.bind(events), removeEventListener: events.removeEventListener.bind(events), dispatchEvent: events.dispatchEvent.bind(events), localStorage: { getItem: (key) => values.get(key) ?? null, setItem: (key, value) => values.set(key, value) } }, document, Event }) const tree = PaletteToggle() assert.equal(store.getServerSnapshot(), "plain") assert.equal(store.getSnapshot(), "green") effect(); assert.equal(document.documentElement.dataset.palette, "green") let changes = 0 const unsubscribe = store.subscribe(() => { changes++ }) find(tree, "DropdownMenuRadioGroup").props.onValueChange("blue") assert.equal(changes, 1) assert.equal(store.getSnapshot(), "blue") assert.equal(document.documentElement.dataset.palette, "blue") const storageEvent = (key) => { const event = new Event("storage"); Object.defineProperty(event, "key", { value: key }); events.dispatchEvent(event) } storageEvent("unrelated"); assert.equal(changes, 1) values.set("dashboard_palette", "gray"); storageEvent("dashboard_palette") assert.equal(changes, 2); assert.equal(store.getSnapshot(), "gray") unsubscribe(); storageEvent(null); assert.equal(changes, 2) }) test("initial notification count ignores completion after provider unmount", async () => { const h = hooks(), response = deferred() const { NotificationProvider } = loadTypeScriptModule(new URL("./notification-provider.tsx", import.meta.url), { react: h.react, "react/jsx-runtime": jsxRuntime, "@/i18n/provider": { ...i18n, useAppLocale: () => ({ locale: "en-US" }) }, "next/navigation": { useRouter: () => ({ push() {} }) }, sonner: { toast() {} }, "@/lib/auth": { readSession: () => ({ user: { id: 1 }, accessToken: "isolated-fixture" }) }, "@/lib/api/notification": { fetchNotificationUnreadCount: () => response.promise }, "@/lib/realtime-connection": { createRealtimeConnectionManager: () => ({ connect() {}, disconnect() {} }) }, "@/lib/notification-i18n": { localizeNotificationItem: (item) => item }, }) let tree = h.render(NotificationProvider, { children: null }) assert.equal(tree.props.value.unreadCount, 0) h.unmount(); response.resolve({ unreadCount: 999 }); await flush() tree = h.render(NotificationProvider, { children: null }) assert.equal(tree.props.value.unreadCount, 0) }) test("editor fullscreen and channel origin do not require mount-state effects", async () => { const editor = await readFile(new URL("./content-editor/index.tsx", import.meta.url), "utf8") assert.match(editor, /const \[fullscreen, setFullscreen\] = useState\(false\)/) assert.match(editor, /if \(fullscreen\) \{\s*return createPortal\(content, document.body\)/) assert.doesNotMatch(editor, /setMounted/) const html = await readFile(new URL("./content-editor/html-editor.tsx", import.meta.url), "utf8") assert.match(html, /allowedModes.length > 1\s*\? \[\{ key: "separator-mode", type: "separator" \} as const\]/) assert.doesNotMatch(html, /actions\.splice/) const channel = await readFile(new URL("../app/(dashboard)/dashboard/channels/_components/edit.tsx", import.meta.url), "utf8") assert.match(channel, /useSyncExternalStore\(subscribeToOrigin, readBrowserOrigin, readServerOrigin\)/) assert.match(channel, /const readServerOrigin = \(\) => ""/) })