Files
goodbuddy/tests/knowledge-layout.electron.test.ts
mesalogo 67dedc2ed2 fix: simplify knowledge action menus
Remove repeated document names from action labels, shorten the reparse label, and narrow knowledge menus to 220px while preserving other menu widths and viewport bounds. Keep object context on menu triggers and removal confirmations.

Validation: 72 focused and Electron layout tests, typecheck, lint, and diff checks passed.

Release note: 知识库操作菜单去掉重复文件名,将操作简写为重新解析,并收窄菜单宽度,减少换行。
2026-09-27 13:11:09 +08:00

127 lines
9.0 KiB
TypeScript

// @vitest-environment node
import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { createServer } from 'vite'
import react from '@vitejs/plugin-react'
import { expect, it } from 'vitest'
it('renders knowledge rows, menus, preview and task history in wide and narrow Electron windows', async () => {
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-knowledge-layout-'))
const server = await createServer({ configFile: false, root: resolve('.'), cacheDir: join(directory, 'vite'),
plugins: [react(), { name: 'knowledge-layout', configureServer(server) {
server.middlewares.use('/knowledge.html', async (_request, response) => {
response.setHeader('Content-Type', 'text/html')
response.end(await server.transformIndexHtml('/knowledge.html',
'<html><body><div id="root"></div><script type="module" src="/tests/support/knowledge-layout-regression.tsx"></script></body></html>'))
})
} }], server: { host: '127.0.0.1', port: 0, watch: null } })
try {
await server.listen()
const driver = join(directory, 'driver.cjs')
await writeFile(driver, `
const { app, BrowserWindow } = require('electron');
const assert = require('node:assert/strict');
const { writeFile } = require('node:fs/promises');
const { join } = require('node:path');
app.commandLine.appendSwitch('force-device-scale-factor', '1');
app.setPath('userData', ${JSON.stringify(join(directory, 'profile'))});
app.whenReady().then(async () => {
const win = new BrowserWindow({ show: true, width: 1100, height: 720, useContentSize: true,
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false, backgroundThrottling: false } });
const js = code => win.webContents.executeJavaScript(code);
const settle = () => js('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
const wait = async code => { for (let i = 0; i < 400; i++) {
if (await js(code)) return; await new Promise(resolve => setTimeout(resolve, 25));
} throw new Error('Timeout: ' + code); };
const click = async text => { await js('Array.from(document.querySelectorAll("button")).find(b => b.textContent === ' + JSON.stringify(text) + ').click()'); await settle(); };
const capture = async name => { if (process.env.GOODBUDDY_LAYOUT_ARTIFACTS) await writeFile(join(process.env.GOODBUDDY_LAYOUT_ARTIFACTS, name + '.png'), (await win.webContents.capturePage()).toPNG()); };
console.log('Loading knowledge fixture');
await win.loadURL(${JSON.stringify(server.resolvedUrls!.local[0] + 'knowledge.html')});
await wait('document.querySelectorAll("tbody tr").length === 2');
console.log('Knowledge fixture ready');
for (const theme of ['light', 'dark']) {
await js('document.documentElement.dataset.theme = ' + JSON.stringify(theme));
for (const width of [1100, 720, 420]) {
win.setContentSize(width, width === 420 ? 480 : 720); await settle();
console.log('Checking ' + theme + ' ' + width);
assert(await js('document.documentElement.scrollWidth <= innerWidth'));
assert(await js('document.querySelector(".workspace-panel-scroll").scrollWidth <= document.querySelector(".workspace-panel-scroll").clientWidth'));
assert(await js('Array.from(document.querySelectorAll("tbody tr")).every(r => r.querySelectorAll("button").length === 2)'));
assert(await js('Array.from(document.querySelectorAll(".knowledge-document-path")).map(e => e.textContent).join(" ").includes("edition=2")'));
await capture('knowledge-list-' + theme + '-' + width);
console.log('Opening list menu');
const height = await js('document.querySelector("tbody tr").getBoundingClientRect().height');
await js('document.querySelector("tbody [aria-haspopup=menu]").scrollIntoView({block:"center",inline:"end"}); document.querySelector("tbody [aria-haspopup=menu]").click()');
await wait('!!document.querySelector("[role=menuitem]")'); await settle();
assert.equal(await js('document.querySelector("tbody tr").getBoundingClientRect().height'), height);
const menu = await js('document.querySelector("[role=menu]").getBoundingClientRect().toJSON()');
assert.equal(menu.width, 220);
assert(menu.left >= 15 && menu.right <= width - 15 && menu.top >= 15 && menu.height >= 32, JSON.stringify(menu));
await capture('knowledge-menu-' + theme + '-' + width);
console.log('Closing list menu');
win.focus(); win.webContents.focus();
await wait('document.hasFocus()');
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' }); await settle();
assert(await js('!document.querySelector("[role=menu]") && document.activeElement.matches("[aria-haspopup=menu]")'));
await click('查看解析结果');
console.log('Opening preview');
await wait('!!document.querySelector(".document-result-preview")');
assert(await js('document.activeElement.textContent === "返回文档列表"'));
assert(await js('document.documentElement.scrollWidth <= innerWidth'));
assert(await js('document.querySelector(".workspace-panel-scroll").scrollWidth <= document.querySelector(".workspace-panel-scroll").clientWidth'));
assert(await js('!Array.from(document.querySelectorAll("button")).some(b => b.textContent === "添加所选图片")'));
await capture('knowledge-preview-' + theme + '-' + width);
console.log('Opening preview menu');
await js('document.querySelector(".knowledge-document-preview__header [aria-haspopup=menu]").click()'); await settle();
const previewMenu = await js('document.querySelector("[role=menu]").getBoundingClientRect().toJSON()');
assert.equal(previewMenu.width, 220);
assert(previewMenu.height >= 100 && previewMenu.top >= 16 && previewMenu.bottom <= (width === 420 ? 480 : 720) - 15, JSON.stringify(previewMenu));
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'End' });
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'End' }); await settle();
assert(await js('document.activeElement.textContent === "移除来源"'));
for (let i = 0; i < 2; i++) {
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Up' });
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Up' }); await settle();
}
assert(await js('document.activeElement.textContent === "重新解析"'));
await capture('knowledge-preview-menu-' + theme + '-' + width);
console.log('Closing preview menu');
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' }); await settle();
await click('返回文档列表');
console.log('Returned to list');
assert(await js('document.activeElement.textContent === "查看解析结果"'));
await js('document.querySelector("tbody [aria-haspopup=menu]").click()'); await settle();
console.log('Opened task navigation menu');
await click('查看任务');
console.log('Navigated to tasks');
await click('历史');
console.log('Opening history');
assert(await js('document.querySelectorAll(".knowledge-task-list > li").length === 1 && !document.querySelector("progress") && !document.querySelector(".knowledge-task__children")'));
assert(await js('document.documentElement.scrollWidth <= innerWidth'));
await capture('knowledge-history-' + theme + '-' + width);
await click('Documents');
}
}
console.log('Knowledge layout passed: light/dark, 1100/720/420px, stable row height, bounded menus, native Escape, preview focus, compact history');
app.exit(0);
}).catch(error => { console.error(error); app.exit(1); });
`)
const env = { ...process.env }
delete env.ELECTRON_RUN_AS_NODE
const child = spawn(createRequire(import.meta.url)('electron'), [driver], { env, stdio: ['ignore', 'pipe', 'pipe'] })
let output = ''
child.stdout.on('data', data => { output += data })
child.stderr.on('data', data => { output += data })
const timeout = setTimeout(() => child.kill(), 60000)
try {
const code = await new Promise((resolve, reject) => { child.once('exit', resolve); child.once('error', reject) })
expect(code, output).toBe(0)
} finally { clearTimeout(timeout) }
} finally { await server.close(); await rm(directory, { recursive: true, force: true }) }
}, 90000)