Restore URL identity in merged rows and replace crowded document actions with a primary entry and accessible menu. Organize preview navigation and maintenance actions, and show task history as batches with matching counts and filters. Keep preview task scope consistent for multi-document sources and track pending operations independently so one source does not block another source's pause action. Include responsive Electron layout and behavioral regressions, bilingual copy, and design documentation. Validation: focused regressions, typecheck, lint, and diff checks passed. Electron fixtures cover light/dark and wide/narrow layouts. Full tests reported two previously known HeartbeatCenter assertions; real import and parsing integration was not rerun. Release note: 知识库文档列表保留来源网址,统一查看入口和更多操作,并整理解析结果页。任务历史按批次展示,筛选计数与当前对象一致,不同来源的操作互不阻塞。
120 lines
8.6 KiB
TypeScript
120 lines
8.6 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(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(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.includes("移除来源")'));
|
|
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)
|