/** 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) })