58 lines
5.8 KiB
JavaScript
58 lines
5.8 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { createRequire } from 'node:module'
|
|
const require = createRequire(import.meta.url)
|
|
const { JSDOM } = require(process.env.YCODE_TEST_DOM_MODULE || 'jsdom')
|
|
const output = path.resolve('.tmp/private-runtimes-dom')
|
|
fs.mkdirSync(output, { recursive: true })
|
|
fs.writeFileSync(path.join(output, 'fixture.tsx'), `import React from 'react';import {createRoot} from 'react-dom/client';import Panel from ${JSON.stringify(path.resolve('src/renderer/src/components/PrivateRuntimesSettingsPanel.tsx'))};let root;window.mount=()=>{root=createRoot(document.getElementById('root'));root.render(<React.StrictMode><Panel/></React.StrictMode>)};window.unmount=()=>root.unmount();window.mount();`)
|
|
await require('esbuild').build({entryPoints:[path.join(output,'fixture.tsx')],outfile:path.join(output,'fixture.js'),bundle:true,platform:'browser',format:'iife',jsx:'automatic',logLevel:'error'})
|
|
const dom = new JSDOM('<html><body><div id="root"></div></body></html>',{url:'https://fixture.invalid',runScripts:'outside-only',pretendToBeVisual:true})
|
|
const w=dom.window,d=w.document,checks=[],errors=[],calls=[]
|
|
const ids=['node','python','git']
|
|
let preferences={enabled:true,tools:{node:true,python:true,git:true}},supported=true,missing=[],failGet=false,failUpdate=false,hold=null,delay=false
|
|
const snapshot=()=>({platform:supported?'win32':'linux',supported,preferences:structuredClone(preferences),tools:ids.map(id=>({id,available:!missing.includes(id),enabled:preferences.tools[id],active:supported&&!missing.includes(id)&&preferences.enabled&&preferences.tools[id],path:missing.includes(id)?null:`C:\\runtimes\\${id}`}))})
|
|
w.ycode={privateRuntimes:{get:async()=>{if(failGet)throw Error('read failure');return snapshot()},update:async patch=>{calls.push(patch);if(delay)await new Promise(resolve=>hold=resolve);if(failUpdate)throw Error('write failure');preferences={...preferences,...patch,tools:{...preferences.tools,...patch.tools}};return snapshot()}}}
|
|
w.addEventListener('error',event=>errors.push(event.message))
|
|
const pause=()=>new Promise(resolve=>setTimeout(resolve,15))
|
|
const wait=async fn=>{for(let i=0;i<150;i++){if(fn())return;await pause()}throw Error('Timed out: '+fn)}
|
|
const check=(name,value)=>{assert.ok(value,name);checks.push(name)}
|
|
const control=name=>d.querySelector(`[aria-label="${name}"]`)
|
|
const master=()=>control('启用内置运行时'),node=()=>control('启用 Node.js'),python=()=>control('启用 Python')
|
|
const remount=async()=>{w.unmount();await pause();w.mount();await wait(()=>master()&&!d.body.textContent.includes('正在读取内置运行时…'));await pause()}
|
|
try {
|
|
w.eval(fs.readFileSync(path.join(output,'fixture.js'),'utf8'))
|
|
await wait(()=>master()&&!master().disabled)
|
|
check('exactly four accessible switches',d.querySelectorAll('[role=switch]').length===4)
|
|
check('ready status derives from backend',d.querySelector('[data-runtime=node] small').textContent.includes('已启用'))
|
|
delay=true;node().click();node().click();await wait(()=>hold)
|
|
check('duplicate clicks produce one patch',calls.length===1)
|
|
check('saving retains acknowledged value and disables all controls',node().checked&&[...d.querySelectorAll('input')].every(el=>el.disabled)&&d.body.textContent.includes('正在保存'))
|
|
hold();await wait(()=>!node().checked&&!node().disabled);delay=false
|
|
check('tool patch is narrow',JSON.stringify(calls[0])===JSON.stringify({tools:{node:false}}))
|
|
master().click();await wait(()=>!master().checked&&!master().disabled)
|
|
check('master off preserves individual choices',!node().checked&&python().checked&&node().disabled&&python().disabled)
|
|
master().click();await wait(()=>master().checked&&!node().disabled)
|
|
check('master on restores choices',!node().checked&&python().checked)
|
|
failUpdate=true;python().click();await wait(()=>d.querySelector('[role=alert]'))
|
|
check('failed save does not change persisted checkmark',python().checked&&preferences.tools.python&&d.body.textContent.includes('write failure'))
|
|
failUpdate=false;python().click();await wait(()=>!python().checked)
|
|
check('retry succeeds and clears error',!d.querySelector('[role=alert]'))
|
|
await remount();check('reopen reloads saved choices',!node().checked&&!python().checked&&master().checked)
|
|
missing=['git'];await remount()
|
|
check('missing bundle is disabled with honest status',control('启用 Git Bash').disabled&&d.querySelector('[data-runtime=git]').textContent.includes('未随包提供'))
|
|
supported=false;await remount()
|
|
check('non-Windows disables controls and explains support',[...d.querySelectorAll('input')].every(el=>el.disabled)&&d.body.textContent.includes('仅支持 Windows x64'))
|
|
failGet=true;await remount()
|
|
check('read failure offers recovery without enabling controls',d.body.textContent.includes('read failure')&&[...d.querySelectorAll('input')].every(el=>el.disabled))
|
|
failGet=false;supported=true;missing=[];d.querySelector('button').click();await wait(()=>!master().disabled)
|
|
check('read retry recovers actual state',!d.querySelector('[role=alert]')&&!node().checked)
|
|
delay=true;node().click();await wait(()=>hold);w.unmount();hold();await pause();delay=false;w.mount();await wait(()=>master()&&!master().disabled)
|
|
check('save acknowledged after navigation is read on reopen',node().checked)
|
|
check('no unrelated security controls',!d.body.textContent.includes('删除保护')&&!d.body.textContent.includes('审计中心'))
|
|
check('future process and system fallback warning present',d.body.textContent.includes('重新打开终端或启动新会话')&&d.body.textContent.includes('系统已安装的工具仍可能可用'))
|
|
check('no uncaught renderer errors',errors.length===0)
|
|
console.log(JSON.stringify({result:'PASS',checks,limits:'Controlled DOM/API fixture, not Windows execution or screenshot pixel parity.'},null,2))
|
|
}finally{w.unmount();dom.window.close()}
|