/** DOM interaction regression, no Electron sandbox bypass or upstream calls. * npm install --prefix /tmp/ycode-popconfirm-test --cache /tmp/ycode-popconfirm-test/cache --ignore-scripts jsdom * esbuild scripts/task-popconfirm-regression.tsx --bundle --platform=node --format=cjs --jsx=automatic --loader:.css=empty --outfile=.tmp/task-popconfirm-regression.cjs * YCODE_TEST_DOM_MODULE=/tmp/ycode-popconfirm-test/node_modules/jsdom node .tmp/task-popconfirm-regression.cjs * jsdom verifies event/state contracts, NOT browser layout, pixels or WorkBuddy parity. */ import assert from 'node:assert/strict' import { createRequire } from 'node:module' const externalRequire = createRequire(process.cwd() + '/package.json') const { JSDOM } = externalRequire(process.env.YCODE_TEST_DOM_MODULE || 'jsdom') const dom = new JSDOM('
', { pretendToBeVisual: true, url: 'http://localhost/' }) for (const key of ['window', 'document', 'Node', 'Element', 'HTMLElement', 'HTMLInputElement', 'ShadowRoot', 'MutationObserver', 'MouseEvent', 'KeyboardEvent', 'Event', 'getComputedStyle']) Object.defineProperty(globalThis, key, { value: key === 'getComputedStyle' ? dom.window.getComputedStyle.bind(dom.window) : dom.window[key], configurable: true }) Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, configurable: true }) Object.assign(globalThis, { requestAnimationFrame: dom.window.requestAnimationFrame.bind(dom.window), cancelAnimationFrame: dom.window.cancelAnimationFrame.bind(dom.window), IS_REACT_ACT_ENVIRONMENT: true }) // Synthetic visibility is for focus semantics only, not geometry validation. dom.window.HTMLElement.prototype.getClientRects = function () { return [new dom.window.DOMRect(20, 20, 100, 30)] as unknown as DOMRectList } dom.window.matchMedia = () => ({ matches: false, addEventListener() {}, removeEventListener() {} }) const React = require('react') as typeof import('react') const { act } = React const { createRoot } = require('react-dom/client') as typeof import('react-dom/client') const { TaskPopconfirm, taskConfirmationCopy } = require('../src/renderer/src/components/TaskPopconfirm') as typeof import('../src/renderer/src/components/TaskPopconfirm') const { Popover } = require('../src/renderer/src/ui/Popover') as typeof import('../src/renderer/src/ui/Popover') const root = createRoot(document.getElementById('root')!) const anchor = document.getElementById('anchor')! const other = document.getElementById('other')! let checks = 0 function check(condition: unknown, message: string) { assert.ok(condition, message); checks++ } async function settle() { await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)) }) } function click(element: Element) { element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, button: 0 })) } async function main() { let closes = 0, calls = 0, resolve: (() => void) | undefined, reject: ((error: Error) => void) | undefined const close = () => { closes++; root.render(null) } const submit = () => { calls++; return new Promise((yes, no) => { resolve = yes; reject = no }) } const render = async (kind: 'archive' | 'delete' = 'archive') => { await act(async () => root.render()); await settle() } await render() check(!document.querySelector('.wb-modal__overlay'), 'no modal overlay') check(document.querySelector('.task-popconfirm-source'), 'optional host class') check(document.querySelector('.wb-popover__arrow'), 'arrow rendered') check(document.querySelector('[role="dialog"]')?.getAttribute('aria-labelledby'), 'accessible title') check(document.activeElement === document.querySelector('.task-popconfirm__confirm'), 'confirmation auto-focuses') check(taskConfirmationCopy('delete', true).confirm === '停止并删除', 'running delete copy') check(document.querySelector('.task-popconfirm__compatibility')?.textContent?.includes('YCode 暂无此设置入口'), 'archive destination mismatch explicit') const confirm = document.querySelector('.task-popconfirm__confirm')! await act(async () => { click(confirm); click(confirm) }) check(calls === 1, 'duplicate confirmation guarded synchronously') check(confirm.hasAttribute('disabled'), 'confirmation disabled in flight') await act(async () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }))) check(closes === 0, 'pending operation not dismissed by Escape') await act(async () => reject!(new Error('fixture failure'))) check(document.querySelector('[role="alert"]')?.textContent === 'fixture failure', 'failure remains visible') check(!confirm.hasAttribute('disabled'), 'failure enables retry') await act(async () => click(confirm)) check(calls === 2, 'retry submits once') await act(async () => resolve!()) check(closes === 1 && !document.querySelector('.task-popconfirm'), 'success closes') await render('delete') let underlyingClicks = 0 other.addEventListener('click', () => underlyingClicks++) await act(async () => { other.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, cancelable: true, button: 0 })) click(other) }) check(underlyingClicks === 0, 'outside click is consumed before underlying task handler') check(!document.querySelector('.task-popconfirm'), 'outside click closes') await act(async () => click(other)) check(underlyingClicks === 1, 'only the dismissal click is consumed') await render() anchor.focus() await act(async () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }))) check(document.activeElement === anchor, 'Escape restores anchor focus') await render() await act(async () => click(document.querySelector('.task-popconfirm__confirm')!)) const closesBeforeRemoval = closes await act(async () => anchor.remove()) await settle() check(!document.querySelector('.task-popconfirm'), 'removed anchor closes popup') check(closes === closesBeforeRemoval + 1, 'anchor disappearance closes even in flight') await act(async () => resolve!()) check(closes === closesBeforeRemoval + 1, 'late completion after unmount cannot close another popup') document.body.prepend(anchor) await act(async () => root.render(Legacy} open={false} onOpenChange={() => {}}>hidden)) check(document.getElementById('legacy')?.getAttribute('aria-expanded') === 'false', 'legacy trigger API preserved') check(!document.querySelector('.wb-popover'), 'closed legacy popover remains unmounted') let legacyOutside = 0 await act(async () => root.render(Legacy} open onOpenChange={() => legacyOutside++}>)) await settle() await act(async () => { other.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 })); click(other) }) check(legacyOutside === 1, 'legacy outside pointer still requests close') check(underlyingClicks === 2, 'legacy outside click is not consumed by default') check(!document.querySelector('.wb-popover__arrow'), 'legacy arrow remains opt-in') await act(async () => root.unmount()) dom.window.close() console.log(`task popconfirm DOM regression: ${checks} checks passed (jsdom, not pixel validation)`) } // React's scheduler MessageChannel can keep Node alive after jsdom cleanup. main().then(() => process.exit(0), error => { console.error(error); dom.window.close(); process.exit(1) })