48 lines
4.1 KiB
JavaScript
48 lines
4.1 KiB
JavaScript
/** Isolated DOM regression; not a browser layout/pixel or original-app acceptance test. */
|
|
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { createRequire } from 'node:module'
|
|
import { build } from 'esbuild'
|
|
const require = createRequire(import.meta.url)
|
|
const { JSDOM } = require(process.env.YCODE_TEST_DOM_MODULE || process.env.YCODE_JSDOM_PATH || 'jsdom')
|
|
const output = fs.mkdtempSync(path.resolve('.tmp/mode-chip-dom-'))
|
|
await build({ entryPoints: ['src/renderer/src/workbuddy/composer/ModeChip.tsx'], bundle: true, platform: 'node', format: 'cjs', jsx: 'automatic', packages: 'external', loader: { '.css': 'empty' }, outfile: path.join(output, 'component.cjs'), logLevel: 'error' })
|
|
const dom = new JSDOM('<html><head></head><body><div id="root"></div></body></html>', { url: 'http://localhost' })
|
|
Object.assign(globalThis, { window: dom.window, document: dom.window.document, HTMLElement: dom.window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true })
|
|
const React = require('react'), { createRoot } = require('react-dom/client')
|
|
const { ModeChip } = require(path.join(output, 'component.cjs'))
|
|
const css = fs.readFileSync('src/renderer/src/workbuddy/composer/mode-chip.css', 'utf8')
|
|
const style = document.createElement('style'); style.textContent = css; document.head.append(style)
|
|
const root = createRoot(document.getElementById('root')); let clears = 0, checks = 0
|
|
const check = (actual, expected, message) => { assert.deepEqual(actual, expected, message); checks++ }
|
|
const render = async (mode, disabled = false) => React.act(async () => root.render(React.createElement(ModeChip, { mode, disabled, onClear: () => { clears++ } })))
|
|
for (const mode of ['plan', 'ask']) {
|
|
await render(mode)
|
|
const chip = document.querySelector('.yc-mode-chip'), slot = chip.querySelector('.yc-mode-chip__icon-slot'), button = slot.querySelector('button'), icon = slot.querySelector('.yc-mode-chip__mode-icon')
|
|
check(chip.textContent, mode === 'plan' ? '计划' : '仅问答', 'mode label has no extra X text')
|
|
check(chip.getAttribute('role'), 'group', 'chip remains a group, not a menu')
|
|
check(button.type, 'button', 'clear cannot submit composer')
|
|
check(button.getAttribute('aria-label'), `退出${chip.textContent}模式`, 'accessible exit name')
|
|
check(icon.parentElement === button.parentElement, true, 'icon and close share one slot')
|
|
check(dom.window.getComputedStyle(slot).display, 'grid', 'shared grid slot')
|
|
check(dom.window.getComputedStyle(button).gridArea, '1 / 1', 'button occupies original icon cell')
|
|
check(dom.window.getComputedStyle(icon).gridArea, '1 / 1', 'mode occupies same original cell')
|
|
check(dom.window.getComputedStyle(button).position, 'static', 'no floating absolute close')
|
|
check(dom.window.getComputedStyle(chip).backgroundColor, 'rgba(0, 0, 0, 0)', 'transparent background')
|
|
await React.act(async () => button.focus())
|
|
check(document.activeElement === button, true, 'native button receives keyboard focus')
|
|
const before = clears
|
|
await React.act(async () => button.click())
|
|
check(clears, before + 1, 'activation calls existing host clear exactly once')
|
|
await render(mode, true)
|
|
check(document.querySelector('button'), null, 'disabled mode exposes no clear action')
|
|
check(document.querySelector('.yc-mode-chip').getAttribute('aria-disabled'), 'true', 'disabled state is announced')
|
|
check(document.querySelector('.yc-mode-chip').tabIndex, -1, 'disabled mode not in keyboard tab order')
|
|
}
|
|
await render('craft'); check(document.querySelector('.yc-mode-chip'), null, 'craft stays hidden')
|
|
// CSS contracts complement DOM checks; test-mode-chip-native.mjs is an unexecuted native hover/focus/layout probe.
|
|
for (const contract of ['border-radius:var(--yc-radius-full)', 'color:var(--cr-text-secondary)', ':hover .yc-mode-chip__mode-icon', ':focus-within .yc-mode-chip__mode-icon', ':hover .yc-mode-chip__clear', ':focus-within .yc-mode-chip__clear']) check(css.includes(contract), true, contract)
|
|
await React.act(async () => root.unmount()); dom.window.close()
|
|
console.log(JSON.stringify({ checks, passed: true, scope: 'Isolated React DOM and CSS contracts; no native keyboard/layout/pixel acceptance' }))
|