656 lines
30 KiB
JavaScript
656 lines
30 KiB
JavaScript
// Isolated production YCode verification for N01, N03 and N07.
|
|
// No remote model request, notification fetch, or system integration is performed.
|
|
const fs = require('node:fs/promises')
|
|
const path = require('node:path')
|
|
const assert = require('node:assert/strict')
|
|
|
|
if (!process.versions.electron) {
|
|
;(async () => {
|
|
const directory = await fs.mkdtemp(path.resolve('.tmp/seven-ui-fixes-'))
|
|
const data = path.join(directory, 'data')
|
|
const home = path.join(directory, 'home')
|
|
const workspace = path.join(directory, 'workspace')
|
|
await Promise.all([
|
|
fs.mkdir(data, { recursive: true }),
|
|
fs.mkdir(home, { recursive: true }),
|
|
fs.mkdir(workspace, { recursive: true })
|
|
])
|
|
|
|
const now = Date.now()
|
|
const modelId = 'seven-ui-model'
|
|
const workspaceId = 'seven-ui-workspace'
|
|
const sessionId = 'seven-ui-session'
|
|
await fs.writeFile(path.join(data, 'models.json'), JSON.stringify({
|
|
version: 1,
|
|
selectedModelId: modelId,
|
|
models: [{
|
|
id: modelId,
|
|
name: 'UI Fixture Model',
|
|
providerId: 'custom',
|
|
model: 'fixture-model',
|
|
baseUrl: 'http://127.0.0.1:1/v1',
|
|
authMode: 'none',
|
|
supportsToolCall: false
|
|
}]
|
|
}))
|
|
await fs.writeFile(path.join(data, 'workspaces.json'), JSON.stringify({
|
|
selectedWorkspaceId: workspaceId,
|
|
workspaces: [{ id: workspaceId, name: 'Seven UI Fixture', root: workspace, createdAt: now }]
|
|
}))
|
|
await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({
|
|
sessions: [{
|
|
id: sessionId,
|
|
title: '七项 UI 修复验收',
|
|
cwd: workspace,
|
|
workspaceId,
|
|
modelId,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
messages: [
|
|
{ id: 'seven-ui-user', role: 'user', content: '只做本地 UI 验收', createdAt: now - 1, status: 'done' },
|
|
{ id: 'seven-ui-assistant', role: 'assistant', content: '已准备本地 UI 验证。', createdAt: now, status: 'done', parts: [], tools: [] }
|
|
]
|
|
}]
|
|
}))
|
|
|
|
const env = {
|
|
...process.env,
|
|
YCODE_QA_DIRECTORY: directory,
|
|
YCODE_USER_DATA_DIR: data, YCODE_DISABLE_GPU: '1',
|
|
YCODE_QA_OFFSCREEN: '1',
|
|
YCODE_DISABLE_SYSTEM_INTEGRATION: '1'
|
|
}
|
|
delete env.ELECTRON_RUN_AS_NODE
|
|
delete env.ELECTRON_RENDERER_URL
|
|
const child = require('node:child_process').spawn(
|
|
require('electron'),
|
|
['--no-sandbox', '--disable-gpu-sandbox', __filename],
|
|
{ env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
)
|
|
let log = ''
|
|
child.stdout.on('data', (bytes) => {
|
|
log += bytes
|
|
if (bytes.toString().includes('SEVEN_UI_FIXES_')) process.stdout.write(bytes)
|
|
})
|
|
child.stderr.on('data', (bytes) => { log += bytes })
|
|
const timer = setTimeout(() => child.kill(), 180_000)
|
|
child.once('exit', async (code) => {
|
|
clearTimeout(timer)
|
|
await fs.writeFile(path.join(directory, 'process.log'), log)
|
|
console.log(JSON.stringify({ directory, exitCode: code }))
|
|
if (code) console.error(log.slice(-12_000))
|
|
process.exitCode = code ?? 1
|
|
})
|
|
})().catch((error) => {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
})
|
|
} else {
|
|
const { app, BrowserWindow } = require('electron')
|
|
const { pathToFileURL } = require('node:url')
|
|
const directory = process.env.YCODE_QA_DIRECTORY
|
|
const data = process.env.YCODE_USER_DATA_DIR
|
|
assert.ok(directory)
|
|
assert.ok(data)
|
|
const relative = path.relative(path.resolve('.tmp'), directory)
|
|
assert.ok(relative.startsWith('seven-ui-fixes-') && !relative.includes(path.sep))
|
|
|
|
app.setPath('userData', data)
|
|
app.setPath('sessionData', data)
|
|
app.setPath('home', path.join(directory, 'home'))
|
|
|
|
let main
|
|
let phase = 'boot'
|
|
const checks = []
|
|
const geometry = {}
|
|
const snapshots = {}
|
|
const screenshots = []
|
|
const rendererErrors = []
|
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
|
async function read(source) {
|
|
let timer
|
|
try {
|
|
return await Promise.race([
|
|
main.webContents.executeJavaScript(source),
|
|
new Promise((_, reject) => {
|
|
timer = setTimeout(() => reject(new Error(`Renderer timeout: ${source.slice(0, 140)}`)), 20_000)
|
|
})
|
|
])
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
async function wait(source, label) {
|
|
for (let index = 0; index < 400; index += 1) {
|
|
const result = await read(source)
|
|
if (result) return result
|
|
await pause(40)
|
|
}
|
|
throw new Error(`Timeout: ${label}`)
|
|
}
|
|
|
|
async function step(label) {
|
|
phase = label
|
|
await fs.writeFile(path.join(directory, 'phase.txt'), label)
|
|
}
|
|
|
|
function check(id, label, actual, expected) {
|
|
const pass = typeof expected === 'function'
|
|
? Boolean(expected(actual))
|
|
: JSON.stringify(actual) === JSON.stringify(expected)
|
|
checks.push({
|
|
id,
|
|
label,
|
|
pass,
|
|
actual,
|
|
expected: typeof expected === 'function' ? expected.toString() : expected
|
|
})
|
|
}
|
|
|
|
function opaqueColor(value) {
|
|
if (!value || value === 'transparent') return false
|
|
const match = /^rgba?\((.+)\)$/i.exec(value)
|
|
if (!match) return true
|
|
const parts = match[1].split(',').map((part) => part.trim())
|
|
return parts.length < 4 || Number(parts[3]) >= 0.999
|
|
}
|
|
|
|
function insideViewport(box, viewport) {
|
|
return box[0] >= -0.5
|
|
&& box[1] >= -0.5
|
|
&& box[0] + box[2] <= viewport.width + 0.5
|
|
&& box[1] + box[3] <= viewport.height + 0.5
|
|
}
|
|
|
|
async function capture(name) {
|
|
await pause(160)
|
|
const image = (await main.webContents.capturePage()).toPNG()
|
|
const filename = `${name}.png`
|
|
await fs.writeFile(path.join(directory, filename), image)
|
|
screenshots.push(filename)
|
|
}
|
|
|
|
async function setTheme(theme) {
|
|
await read(`(() => {
|
|
localStorage.setItem('ycode:theme', ${JSON.stringify(theme)})
|
|
window.dispatchEvent(new StorageEvent('storage', {
|
|
key: 'ycode:theme',
|
|
newValue: ${JSON.stringify(theme)}
|
|
}))
|
|
return true
|
|
})()`)
|
|
await wait(
|
|
`document.documentElement.dataset.theme === ${JSON.stringify(theme)}`,
|
|
`${theme} theme`
|
|
)
|
|
await pause(180)
|
|
}
|
|
|
|
async function pressEscape() {
|
|
await read(`(() => {
|
|
document.dispatchEvent(new KeyboardEvent('keydown', {
|
|
key: 'Escape',
|
|
code: 'Escape',
|
|
bubbles: true,
|
|
cancelable: true
|
|
}))
|
|
return true
|
|
})()`)
|
|
}
|
|
|
|
async function verifyProjectMenu(theme) {
|
|
await setTheme(theme)
|
|
await step(`N01-project-menu-${theme}`)
|
|
const trigger = await read(`(() => {
|
|
const node = document.querySelector('.conversation-list-tab-button-more')
|
|
if (!node) return null
|
|
node.click()
|
|
return true
|
|
})()`)
|
|
assert.ok(trigger, 'project menu trigger')
|
|
await wait(`!!document.querySelector('.yc-project-more')`, 'project menu')
|
|
const snapshot = await read(`(() => {
|
|
const menu = document.querySelector('.yc-project-more')
|
|
const style = getComputedStyle(menu)
|
|
const rect = menu.getBoundingClientRect()
|
|
return {
|
|
box: [rect.x, rect.y, rect.width, rect.height],
|
|
background: style.backgroundColor,
|
|
opacity: style.opacity,
|
|
parentIsBody: menu.parentElement === document.body,
|
|
viewport: { width: innerWidth, height: innerHeight },
|
|
text: menu.textContent
|
|
}
|
|
})()`)
|
|
snapshots[`projectMenu-${theme}`] = snapshot
|
|
geometry[`projectMenu-${theme}`] = snapshot
|
|
check(`n01-${theme}-background`, 'project menu uses an opaque theme surface', opaqueColor(snapshot.background), true)
|
|
check(`n01-${theme}-viewport`, 'project menu stays inside the viewport', insideViewport(snapshot.box, snapshot.viewport), true)
|
|
check(`n01-${theme}-portal`, 'project menu is portaled to body', snapshot.parentIsBody, true)
|
|
check(`n01-${theme}-visible`, 'project menu is fully visible', snapshot.opacity, '1')
|
|
await capture(`n01-project-menu-${theme}`)
|
|
await pressEscape()
|
|
await wait(`!document.querySelector('.yc-project-more')`, 'project menu closes')
|
|
}
|
|
|
|
async function verifyNotifications(theme, closeWithEscape) {
|
|
await setTheme(theme)
|
|
await step(`N03-notifications-${theme}`)
|
|
await read(`document.querySelector('.user-menu-trigger-msg-center').click()`)
|
|
await wait(
|
|
`!!document.querySelector('.notif-panel[data-positioned="true"]')`,
|
|
'notification panel'
|
|
)
|
|
const snapshot = await read(`(() => {
|
|
const panel = document.querySelector('.notif-panel')
|
|
const bell = document.querySelector('.user-menu-trigger-msg-center')
|
|
const style = getComputedStyle(panel)
|
|
const rect = panel.getBoundingClientRect()
|
|
const points = [
|
|
[rect.left + rect.width * 0.25, rect.top + rect.height * 0.25],
|
|
[rect.left + rect.width * 0.75, rect.top + rect.height * 0.25],
|
|
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.5],
|
|
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.75]
|
|
]
|
|
return {
|
|
box: [rect.x, rect.y, rect.width, rect.height],
|
|
background: style.backgroundColor,
|
|
opacity: style.opacity,
|
|
parentIsBody: panel.parentElement === document.body,
|
|
viewport: { width: innerWidth, height: innerHeight },
|
|
bellExpanded: bell.getAttribute('aria-expanded'),
|
|
bellControls: bell.getAttribute('aria-controls'),
|
|
unobstructed: points.every(([x, y]) => {
|
|
const hit = document.elementFromPoint(x, y)
|
|
return !!hit && panel.contains(hit)
|
|
})
|
|
}
|
|
})()`)
|
|
snapshots[`notifications-${theme}`] = snapshot
|
|
geometry[`notifications-${theme}`] = snapshot
|
|
check(`n03-${theme}-background`, 'notification panel uses an opaque theme surface', opaqueColor(snapshot.background), true)
|
|
check(`n03-${theme}-viewport`, 'notification panel stays inside the viewport', insideViewport(snapshot.box, snapshot.viewport), true)
|
|
check(`n03-${theme}-portal`, 'notification panel is portaled to body', snapshot.parentIsBody, true)
|
|
check(`n03-${theme}-unobstructed`, 'notification content is not covered', snapshot.unobstructed, true)
|
|
check(`n03-${theme}-aria`, 'bell exposes the open dialog state', snapshot.bellExpanded, 'true')
|
|
check(`n03-${theme}-controls`, 'bell controls the notification dialog', snapshot.bellControls, 'ycode-notifications-panel')
|
|
await capture(`n03-notifications-${theme}`)
|
|
if (closeWithEscape) await pressEscape()
|
|
else await read(`document.querySelector('.notif-panel__close').click()`)
|
|
await wait(`!document.querySelector('.notif-panel')`, 'notification panel closes')
|
|
await pause(80)
|
|
check(
|
|
`n03-${theme}-focus`,
|
|
'closing the notification restores bell focus',
|
|
await read(`document.activeElement?.classList.contains('user-menu-trigger-msg-center')`),
|
|
true
|
|
)
|
|
check(
|
|
`n03-${theme}-closed`,
|
|
'bell reports the closed state',
|
|
await read(`document.querySelector('.user-menu-trigger-msg-center').getAttribute('aria-expanded')`),
|
|
'false'
|
|
)
|
|
}
|
|
|
|
async function openPermissionPanel() {
|
|
const opened = await read(`(() => {
|
|
const trigger = [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
if (!trigger) return false
|
|
trigger.click()
|
|
return true
|
|
})()`)
|
|
assert.ok(opened, 'visible permission trigger')
|
|
await wait(`!!document.querySelector('.cr-permission-setting-popover .cr-permission-panel')`, 'permission panel')
|
|
}
|
|
|
|
async function verifyPermissionPanel(theme) {
|
|
await setTheme(theme)
|
|
await step(`N07-permission-panel-${theme}`)
|
|
await openPermissionPanel()
|
|
const snapshot = await read(`(() => {
|
|
const popover = document.querySelector('.cr-permission-setting-popover')
|
|
const panel = popover.querySelector('.cr-permission-panel')
|
|
const description = panel.querySelector('.cr-permission-panel__desc')
|
|
const rect = panel.getBoundingClientRect()
|
|
const style = getComputedStyle(panel)
|
|
const trigger = [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
return {
|
|
box: [rect.x, rect.y, rect.width, rect.height],
|
|
popoverWidth: popover.getBoundingClientRect().width,
|
|
triggerLeft: trigger.getBoundingClientRect().left,
|
|
viewport: { width: innerWidth, height: innerHeight },
|
|
background: style.backgroundColor,
|
|
opacity: style.opacity,
|
|
outsideComposer: !document.querySelector('.conversation-input')?.contains(popover)
|
|
&& !document.querySelector('.chat-pane')?.contains(popover),
|
|
role: panel.getAttribute('role'),
|
|
switchCount: panel.querySelectorAll('[role="switch"]').length,
|
|
rowCount: panel.querySelectorAll('.cr-permission-panel__row').length,
|
|
description: description?.textContent ?? '',
|
|
triggerLabel: trigger?.querySelector('.cr-input-footer-item__label')?.textContent ?? ''
|
|
}
|
|
})()`)
|
|
snapshots[`permissionPanel-${theme}`] = snapshot
|
|
geometry[`permissionPanel-${theme}`] = snapshot
|
|
check(`n07-${theme}-width`, 'permission panel keeps the recovered 200px width', Math.round(snapshot.box[2]), 200)
|
|
check(`n07-${theme}-outer-width`, 'permission popover adds no second padded surface', Math.round(snapshot.popoverWidth), 200)
|
|
check(`n07-${theme}-alignment`, 'permission panel starts at the trigger edge', Math.abs(snapshot.box[0] - snapshot.triggerLeft) < 1, true)
|
|
check(`n07-${theme}-background`, 'permission panel uses an opaque theme surface', opaqueColor(snapshot.background), true)
|
|
check(`n07-${theme}-viewport`, 'permission panel stays inside the viewport', insideViewport(snapshot.box, snapshot.viewport), true)
|
|
check(`n07-${theme}-portal`, 'permission panel is outside the composer portal ancestor', snapshot.outsideComposer, true)
|
|
check(`n07-${theme}-switch-count`, 'permission panel has one full-access switch', snapshot.switchCount, 1)
|
|
check(`n07-${theme}-row-count`, 'permission panel has one permission row', snapshot.rowCount, 1)
|
|
check(
|
|
`n07-${theme}-description`,
|
|
'permission panel describes local default permissions without promising an OS sandbox',
|
|
snapshot.description,
|
|
'当前为默认权限,所有操作都会在默认权限约束内进行,超出范围会请求你的允许。'
|
|
)
|
|
check(`n07-${theme}-default-label`, 'permission trigger defaults to 默认权限', snapshot.triggerLabel, '默认权限')
|
|
await capture(`n07-permission-panel-${theme}`)
|
|
await pressEscape()
|
|
await wait(`!document.querySelector('.cr-permission-setting-popover')`, 'permission panel closes')
|
|
}
|
|
|
|
async function openPermissionConfirmation() {
|
|
if (!(await read(`(() => {
|
|
const panel = document.querySelector('.cr-permission-setting-popover .cr-permission-panel')
|
|
return !!panel && panel.getClientRects().length > 0
|
|
})()`))) {
|
|
await openPermissionPanel()
|
|
}
|
|
await read(`document.querySelector('.cr-permission-setting-popover .wb-switch__input').click()`)
|
|
await wait(`!!document.querySelector('.permission-confirm-dialog')`, 'permission confirmation')
|
|
}
|
|
|
|
async function verifyPermissionConfirmation(theme) {
|
|
await setTheme(theme)
|
|
await step(`N07-permission-confirm-${theme}`)
|
|
await openPermissionConfirmation()
|
|
const snapshot = await read(`(() => {
|
|
const dialog = document.querySelector('.permission-confirm-dialog')
|
|
const style = getComputedStyle(dialog)
|
|
const overlay = dialog.closest('.wb-modal__overlay')
|
|
const popover = document.querySelector('.cr-permission-setting-popover')
|
|
const panel = popover?.querySelector('.cr-permission-panel')
|
|
const overlayZIndex = overlay ? Number(getComputedStyle(overlay).zIndex) : Number.NaN
|
|
const panelZIndex = popover ? Number(getComputedStyle(popover).zIndex) : Number.NaN
|
|
const rect = dialog.getBoundingClientRect()
|
|
const buttons = [...dialog.querySelectorAll('button')]
|
|
const confirm = buttons.find((button) => button.textContent.trim() === '允许完全访问')
|
|
const cancel = buttons.find((button) => button.textContent.trim() === '取消')
|
|
return {
|
|
box: [rect.x, rect.y, rect.width, rect.height],
|
|
viewport: { width: innerWidth, height: innerHeight },
|
|
background: style.backgroundColor,
|
|
overlayZIndex,
|
|
panelZIndex,
|
|
modalAbovePanel: Number.isFinite(overlayZIndex)
|
|
&& Number.isFinite(panelZIndex)
|
|
&& overlayZIndex > panelZIndex,
|
|
description: dialog.querySelector('.cr-permission-confirm__desc')?.textContent ?? '',
|
|
acknowledgement: dialog.querySelector('.permission-confirm-acknowledgement')?.checked ?? null,
|
|
confirmDisabled: confirm?.disabled ?? null,
|
|
confirmDanger: confirm?.classList.contains('wb-composer__danger-button') ?? false,
|
|
hasCancel: !!cancel,
|
|
panelOpen: !!panel && panel.getClientRects().length > 0,
|
|
outsideComposer: !document.querySelector('.conversation-input')?.contains(dialog)
|
|
&& !document.querySelector('.chat-pane')?.contains(dialog),
|
|
triggerLabel: [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
?.querySelector('.cr-input-footer-item__label')?.textContent ?? ''
|
|
}
|
|
})()`)
|
|
snapshots[`permissionConfirm-${theme}`] = snapshot
|
|
geometry[`permissionConfirm-${theme}`] = snapshot
|
|
check(`n07-${theme}-confirm-width`, 'permission confirmation keeps the recovered 480px width', Math.round(snapshot.box[2]), 480)
|
|
check(`n07-${theme}-confirm-viewport`, 'permission confirmation stays inside the viewport', insideViewport(snapshot.box, snapshot.viewport), true)
|
|
check(`n07-${theme}-confirm-background`, 'permission confirmation uses an opaque surface', opaqueColor(snapshot.background), true)
|
|
check(`n07-${theme}-confirm-portal`, 'permission confirmation is outside the composer portal ancestor', snapshot.outsideComposer, true)
|
|
check(`n07-${theme}-confirm-layer`, 'permission confirmation uses the recovered body portal layer', snapshot.overlayZIndex, 100001)
|
|
check(`n07-${theme}-confirm-above-panel`, 'permission confirmation stays above the open panel', snapshot.modalAbovePanel, true)
|
|
check(`n07-${theme}-panel-remains-open`, 'permission panel remains open behind confirmation', snapshot.panelOpen, true)
|
|
check(`n07-${theme}-confirm-description`, 'permission confirmation states the implemented task scope',
|
|
snapshot.description,
|
|
'开启完全访问后,当前发送的任务将减少确认步骤并直接操作您的电脑,请谨慎操作。包括以下内容:')
|
|
check(`n07-${theme}-ack-empty`, 'risk acknowledgement starts unchecked', snapshot.acknowledgement, false)
|
|
check(`n07-${theme}-danger-disabled`, 'full-access confirmation is disabled before acknowledgement', snapshot.confirmDisabled, true)
|
|
check(`n07-${theme}-danger-button`, 'full-access confirmation uses the dangerous action button', snapshot.confirmDanger, true)
|
|
check(`n07-${theme}-cancel`, 'permission confirmation exposes cancel', snapshot.hasCancel, true)
|
|
await capture(`n07-permission-confirm-${theme}`)
|
|
await read(`(() => {
|
|
const dialog = document.querySelector('.permission-confirm-dialog')
|
|
const cancel = [...dialog.querySelectorAll('button')]
|
|
.find((button) => button.textContent.trim() === '取消')
|
|
cancel.click()
|
|
return true
|
|
})()`)
|
|
await wait(`!document.querySelector('.permission-confirm-dialog')`, 'permission confirmation closes')
|
|
check(`n07-${theme}-cancel-keeps-default`, 'cancelling confirmation keeps default permission',
|
|
await read(`(() => {
|
|
const trigger = [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
return {
|
|
label: trigger.querySelector('.cr-input-footer-item__label').textContent,
|
|
danger: trigger.classList.contains('cr-permission-setting--danger')
|
|
}
|
|
})()`),
|
|
{ label: '默认权限', danger: false })
|
|
const panelAfterCancel = await read(`(() => {
|
|
const panel = document.querySelector('.cr-permission-setting-popover .cr-permission-panel')
|
|
return !!panel && panel.getClientRects().length > 0
|
|
})()`)
|
|
check(`n07-${theme}-cancel-panel-open`, 'cancelling confirmation keeps the permission panel open', panelAfterCancel, true)
|
|
check(`n07-${theme}-cancel-label`, 'default label remains after cancellation', snapshot.triggerLabel, '默认权限')
|
|
|
|
await read(`document.querySelector('.cr-permission-setting-popover .wb-switch__input').click()`)
|
|
await wait(`!!document.querySelector('.permission-confirm-dialog')`, 'permission confirmation reopens from the same panel')
|
|
const reopened = await read(`(() => {
|
|
const dialog = document.querySelector('.permission-confirm-dialog')
|
|
const acknowledgement = dialog.querySelector('.permission-confirm-acknowledgement')
|
|
const confirm = [...dialog.querySelectorAll('button')]
|
|
.find((button) => button.textContent.trim() === '允许完全访问')
|
|
const panel = document.querySelector('.cr-permission-setting-popover .cr-permission-panel')
|
|
return {
|
|
panelOpen: !!panel && panel.getClientRects().length > 0,
|
|
acknowledgement: acknowledgement?.checked ?? null,
|
|
confirmDisabled: confirm?.disabled ?? null
|
|
}
|
|
})()`)
|
|
snapshots[`permissionConfirmReopened-${theme}`] = reopened
|
|
check(`n07-${theme}-cancel-direct-reopen`, 'the switch can reopen confirmation without reopening the trigger', reopened.panelOpen, true)
|
|
check(`n07-${theme}-reopen-ack-empty`, 'reopened confirmation still requires risk acknowledgement', reopened.acknowledgement, false)
|
|
check(`n07-${theme}-reopen-disabled`, 'reopened confirmation stays disabled until risk acknowledgement',
|
|
reopened.confirmDisabled, true)
|
|
await read(`(() => {
|
|
const dialog = document.querySelector('.permission-confirm-dialog')
|
|
const cancel = [...dialog.querySelectorAll('button')]
|
|
.find((button) => button.textContent.trim() === '取消')
|
|
cancel.click()
|
|
return true
|
|
})()`)
|
|
await wait(`!document.querySelector('.permission-confirm-dialog')`, 'permission confirmation closes after direct reopen')
|
|
await pressEscape()
|
|
await wait(`!document.querySelector('.cr-permission-setting-popover')`, 'permission panel closes after cancellation checks')
|
|
}
|
|
|
|
async function verifyFullAccessRoundTrip() {
|
|
await setTheme('light')
|
|
await step('N07-full-access-confirm')
|
|
await openPermissionConfirmation()
|
|
const confirmation = await read(`(() => {
|
|
const dialog = document.querySelector('.permission-confirm-dialog')
|
|
const acknowledgement = dialog.querySelector('.permission-confirm-acknowledgement')
|
|
acknowledgement.click()
|
|
const confirm = [...dialog.querySelectorAll('button')]
|
|
.find((button) => button.textContent.trim() === '允许完全访问')
|
|
return {
|
|
acknowledgement: acknowledgement.checked,
|
|
enabled: !confirm.disabled
|
|
}
|
|
})()`)
|
|
snapshots.fullAccessConfirmation = confirmation
|
|
check('n07-full-ack-checked', 'acknowledging the risk checks the source checkbox', confirmation.acknowledgement, true)
|
|
check('n07-full-confirm-enabled', 'acknowledgement enables full-access confirmation', confirmation.enabled, true)
|
|
await capture('n07-full-access-acknowledged')
|
|
await read(`(() => {
|
|
const dialog = document.querySelector('.permission-confirm-dialog')
|
|
const confirm = [...dialog.querySelectorAll('button')]
|
|
.find((button) => button.textContent.trim() === '允许完全访问')
|
|
confirm.click()
|
|
return true
|
|
})()`)
|
|
await wait(`(() => {
|
|
const trigger = [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
return trigger?.classList.contains('cr-permission-setting--danger')
|
|
&& trigger.querySelector('.cr-input-footer-item__label')?.textContent === '允许完全访问'
|
|
})()`, 'full-access mode')
|
|
check('n07-full-dialog-closed', 'confirming full access closes the dialog', await read(`!document.querySelector('.permission-confirm-dialog')`), true)
|
|
check('n07-full-panel-closed', 'confirming full access closes the permission panel', await read(`!document.querySelector('.cr-permission-setting-popover')`), true)
|
|
snapshots.fullAccess = await read(`(() => {
|
|
const trigger = [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
return {
|
|
label: trigger.querySelector('.cr-input-footer-item__label')?.textContent,
|
|
danger: trigger.classList.contains('cr-permission-setting--danger')
|
|
}
|
|
})()`)
|
|
check('n07-full-label', 'full access updates the visible trigger', snapshots.fullAccess,
|
|
{ label: '允许完全访问', danger: true })
|
|
await capture('n07-full-access')
|
|
|
|
await step('N07-restore-default')
|
|
await openPermissionPanel()
|
|
const reopened = await read(`(() => ({
|
|
switchCount: document.querySelectorAll('.cr-permission-setting-popover [role="switch"]').length,
|
|
checked: document.querySelector('.cr-permission-setting-popover .wb-switch__input').checked
|
|
}))()`)
|
|
snapshots.fullAccessReopened = reopened
|
|
check('n07-full-reopen-switch', 'reopened permission panel shows one enabled full-access switch', reopened, {
|
|
switchCount: 1,
|
|
checked: true
|
|
})
|
|
await read(`document.querySelector('.cr-permission-setting-popover .wb-switch__input').click()`)
|
|
await wait(`!document.querySelector('.cr-permission-setting-popover')`, 'permission panel closes after restoring default')
|
|
await wait(`(() => {
|
|
const trigger = [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
return trigger.querySelector('.cr-input-footer-item__label')?.textContent === '默认权限'
|
|
&& !trigger.classList.contains('cr-permission-setting--danger')
|
|
})()`, 'default permission restored')
|
|
snapshots.restoredDefault = await read(`(() => {
|
|
const trigger = [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
return {
|
|
label: trigger.querySelector('.cr-input-footer-item__label')?.textContent,
|
|
danger: trigger.classList.contains('cr-permission-setting--danger')
|
|
}
|
|
})()`)
|
|
check('n07-default-restored', 'turning the reopened switch off restores default permission',
|
|
snapshots.restoredDefault, { label: '默认权限', danger: false })
|
|
await capture('n07-default-restored')
|
|
}
|
|
|
|
;(async () => {
|
|
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
|
|
await app.whenReady()
|
|
for (let index = 0; index < 300; index += 1) {
|
|
main = BrowserWindow.getAllWindows().find((window) => window.getTitle() === 'YCode')
|
|
if (main && !main.webContents.isLoading()) break
|
|
await pause(40)
|
|
}
|
|
assert.ok(main, 'YCode main window')
|
|
main.setSize(1336, 902)
|
|
main.showInactive()
|
|
main.webContents.on('console-message', (...args) => {
|
|
const event = args[0]
|
|
const level = typeof event === 'object' ? event.level : args[1]
|
|
const message = typeof event === 'object' ? event.message : args[2]
|
|
if (level === 'error' || Number(level) >= 3) rendererErrors.push(message)
|
|
})
|
|
await wait(
|
|
`!!window.ycode && !!document.querySelector('.sidebar') && !!document.querySelector('.chat-pane')`,
|
|
'isolated production renderer'
|
|
)
|
|
await read(`document.querySelector('[data-session-id="seven-ui-session"]')?.click()`)
|
|
await wait(
|
|
`(() => {
|
|
const trigger = [...document.querySelectorAll('.cr-permission-setting')]
|
|
.find((node) => node.offsetParent !== null)
|
|
return !!trigger
|
|
})()`,
|
|
'visible permission setting'
|
|
)
|
|
|
|
await verifyProjectMenu('light')
|
|
await verifyNotifications('light', false)
|
|
await verifyPermissionPanel('light')
|
|
await verifyPermissionConfirmation('light')
|
|
|
|
await verifyProjectMenu('dark')
|
|
await verifyNotifications('dark', true)
|
|
await verifyPermissionPanel('dark')
|
|
await verifyPermissionConfirmation('dark')
|
|
|
|
await verifyFullAccessRoundTrip()
|
|
check('console-errors', 'renderer has no console errors', rendererErrors, [])
|
|
|
|
const report = {
|
|
result: checks.every((item) => item.pass) ? 'PASS' : 'FAIL',
|
|
scope: 'Isolated production YCode renderer/preload/IPC with local model, workspace and session fixtures. No remote model, notification, connector, or system requests are issued.',
|
|
fixture: {
|
|
directory,
|
|
modelId: 'seven-ui-model',
|
|
workspaceId: 'seven-ui-workspace',
|
|
sessionId: 'seven-ui-session',
|
|
viewport: await read(`({ width: innerWidth, height: innerHeight, theme: document.documentElement.dataset.theme })`)
|
|
},
|
|
sourceEvidence: [
|
|
'ev_d1a438b97437b2f6a98f5dddcc5c3385184e24a439c5a351365fbbefc39e457d',
|
|
'ev_f5e9d71a676c453daab40928acfb33aeabae68ba57ffda342380cead7f1a1fe2',
|
|
'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94',
|
|
'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
|
],
|
|
checks,
|
|
snapshots,
|
|
geometry,
|
|
screenshots,
|
|
rendererErrors,
|
|
limitations: [
|
|
'The local model fixture is deliberately unreachable and no model request is made.',
|
|
'Notification content remains mock data; this verifier checks the shell, portal, geometry, and close behavior.',
|
|
'Project menu still contains only the currently implemented Project entry.'
|
|
]
|
|
}
|
|
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
|
console.log(`SEVEN_UI_FIXES_${report.result} ${directory}`)
|
|
for (const failure of checks.filter((item) => !item.pass)) {
|
|
console.log(`SEVEN_UI_FIXES_FAILURE ${JSON.stringify(failure)}`)
|
|
}
|
|
app.exit(report.result === 'PASS' ? 0 : 1)
|
|
})().catch(async (error) => {
|
|
if (main && !main.isDestroyed()) {
|
|
await fs.writeFile(
|
|
path.join(directory, 'failure.png'),
|
|
(await main.webContents.capturePage()).toPNG()
|
|
).catch(() => undefined)
|
|
}
|
|
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({
|
|
phase,
|
|
error: String(error?.stack || error),
|
|
checks,
|
|
snapshots,
|
|
geometry,
|
|
rendererErrors
|
|
}, null, 2)).catch(() => undefined)
|
|
console.error(error)
|
|
app.exit(1)
|
|
})
|
|
}
|