Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
165 lines
8.7 KiB
JavaScript
165 lines
8.7 KiB
JavaScript
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 = \(\) => ""/)
|
|
})
|