diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 8409d58d..8918cc19 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -19526,5 +19526,22 @@ - 复发自:BUG-1029。上次是组件搬家后文件清单没跟上;这次是入口删了,断言还锁着旧标签。 - 修复版本:分支 `codex/gate-birth-journey-contract-20261025`。 - 断言三栏:原值是扫描面必须出现 ` readFileSync(new URL(path, import.meta.url), "utf8"); const migration = read("../supabase/migrations/20260930020000_account_deletion_requests.sql"); +type HostNode = { tagName: string; text: string; childNodes: HostNode[] }; +function walkHost(node: HostNode): HostNode[] { + return node.childNodes.flatMap((child) => [child, ...walkHost(child)]); +} + test("status parsing, confirmation word, date and error codes", () => { assert.deepEqual(parseAccountDeletionStatus(null), { status: "none" }); assert.deepEqual(parseAccountDeletionStatus({ status: "none" }), { status: "none" }); @@ -152,10 +157,15 @@ test("a pending account sees 「账号注销中」 with the date and a way back" try { await h.render(); await h.idle(); - assert.match(h.container.text, /账号注销中/); - assert.match(h.container.text, /10 月 7 日 永久删除/); - assert.ok(h.elements().some((node) => node.tagName === "BUTTON" && node.text === "撤销注销")); - assert.ok(h.elements().some((node) => node.tagName === "BUTTON" && node.text === "退出登录")); + // 原值:这句话和两个按钮在挂载点里面。 + // 新值:到文档体里找。挂载点本身是空的。 + // 原因:注销确认改成组件库对话框,内容画在页面主体上,不留在原来的格子里。 + const surface = h.document.body; + const surfaceNodes = [surface, ...walkHost(surface)]; + assert.match(surface.text, /账号注销中/); + assert.match(surface.text, /10 月 7 日 永久删除/); + assert.ok(surfaceNodes.some((node) => node.tagName === "BUTTON" && node.text === "撤销注销")); + assert.ok(surfaceNodes.some((node) => node.tagName === "BUTTON" && node.text === "退出登录")); await h.render(); } finally { globalThis.fetch = originalFetch; diff --git a/frontend/tests/new-chat-from-people-lifecycle.test.tsx b/frontend/tests/new-chat-from-people-lifecycle.test.tsx index 9e47caf8..c50748d4 100644 --- a/frontend/tests/new-chat-from-people-lifecycle.test.tsx +++ b/frontend/tests/new-chat-from-people-lifecycle.test.tsx @@ -275,7 +275,11 @@ async function openDocument(tab: Tab, href: string, options: { mobile?: boolean // 原值:排除类名 `profile-trigger` // 新值:排除类名 `sidebar-account-trigger` // 原因:K1 账户按钮改名。页面上同文案的按钮仍可点,不被页脚账户按钮抢走 - const found = nodes().find((node) => node.tagName === "BUTTON" && node.text.includes(label) + // 原值:按钮的可见文字里要有名字。 + // 新值:打开档案的按钮没有可见文字,名字在 aria-label「某某 打开档案」上。 + // 原因:K4 把名字放在卡片里。同名的「更多操作」不能被当成点开这个人。 + const found = nodes().find((node) => node.tagName === "BUTTON" + && (node.text.includes(label) || String(node.props["aria-label"] ?? "") === `${label} 打开档案`) && !String(node.props.className ?? "").split(/\s+/).includes("sidebar-account-trigger")); assert.ok(found, `button ${label} is rendered`); return found; @@ -406,7 +410,7 @@ for (const person of [ let doc: Awaited> | undefined; try { doc = await openRectificationThenGoTo(tab, api, "星盘档案"); - await doc.settle(() => doc!.h.elements().some((node) => node.tagName === "BUTTON" && node.text.includes("阿澈")), "the people list"); + await doc.settle(() => doc!.h.elements().some((node) => node.tagName === "BUTTON" && String(node.props["aria-label"] ?? node.text).includes("阿澈")), "the people list"); await doc.h.event(doc.button(person.row)); await doc.h.event(doc.button("和 TA 对话")); const assigned = tab.location.assigned.at(-1); @@ -445,7 +449,7 @@ test("a stashed rectification session of the person you left is not reopened as doc = await openRectificationThenGoTo(tab, api, "星盘档案"); // 原值: === RECT(离开时存根)。新值: === null。原因: BUG-1052 D3 不再存根;落点断言不变。 assert.equal(tab.session.getItem("jyotisha.session-url-return"), null); - await doc.settle(() => doc!.h.elements().some((node) => node.tagName === "BUTTON" && node.text.includes("阿澈")), "the people list"); + await doc.settle(() => doc!.h.elements().some((node) => node.tagName === "BUTTON" && String(node.props["aria-label"] ?? node.text).includes("阿澈")), "the people list"); await doc.h.event(doc.button("阿澈")); await doc.h.event(doc.button("看星盘")); const assigned = tab.location.assigned.at(-1); diff --git a/frontend/tests/react-client-lifecycle-test-support.ts b/frontend/tests/react-client-lifecycle-test-support.ts index 25e5531a..ad0af15f 100644 --- a/frontend/tests/react-client-lifecycle-test-support.ts +++ b/frontend/tests/react-client-lifecycle-test-support.ts @@ -73,7 +73,18 @@ class HostElement { return this.parentNode?.closest(selector) ?? null; } get parentElement() { return this.parentNode; } - get children() { return this.childNodes.filter((child) => child.nodeType === 1); } + private hasAssignedChildren = false; + private assignedChildren: unknown = undefined; + get children() { + // A test or a library may replace `children`. Until that happens, the live + // element list is the child nodes. + if (this.hasAssignedChildren) return this.assignedChildren; + return this.childNodes.filter((child) => child.nodeType === 1); + } + set children(value: unknown) { + this.hasAssignedChildren = true; + this.assignedChildren = value; + } matches() { return false; } querySelector() { return null; } querySelectorAll() { return []; } @@ -158,6 +169,17 @@ class HostDocument { createElementNS(_namespace: string, tag: string) { return this.createElement(tag); } } +// Base UI chooses `useLayoutEffect` or a no-op once, the first time it loads, +// by reading `typeof document`. Tests import this harness before those +// components so the choice sticks. Each test still installs its own document. +if (typeof globalThis.document === "undefined") { + Object.defineProperty(globalThis, "document", { + value: new HostDocument(), + writable: true, + configurable: true, + }); +} + export function createClientLifecycleHarness() { const document = new HostDocument(); const frames = new Map>(); @@ -184,6 +206,10 @@ export function createClientLifecycleHarness() { filter: "none", willChange: "auto", contain: "none", + getPropertyValue(name: string) { + if (name === "--space-4") return "16px"; + return ""; + }, }; class KeyboardEvent {} class FocusEvent {}