Files
curios/shared/ui/main.js
T

185 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { trove } from './sdk/index.js';
import config from './config.js';
import { assert, equal, expectError, hostChecks, backendChecks } from './checks.js';
const $ = selector => document.querySelector(selector);
const results = [];
let running = false;
const call = (service, method, params = {}, options) => trove.call(service, method, params, options);
const report = () => ({ schemaVersion: 1, fixture: config, context: trove.context, generatedAt: new Date().toISOString(), results });
const serializeError = error => ({ code: error.code ?? 'qa.assertion_failed', message: error.message ?? String(error), data: error.data });
function refresh() {
$('#results').replaceChildren(...results.map(result => {
const row = document.createElement('li');
const heading = document.createElement('div');
heading.className = 'result-heading';
const title = document.createElement('strong');
title.className = result.status;
const labels = { pass: '通过', fail: '失败', observed: '已执行 · 请观察', skip: '跳过' };
title.textContent = `${labels[result.status]} · ${result.name}`;
const duration = document.createElement('small');
duration.textContent = `${result.durationMs} ms`;
heading.append(title, duration);
const detail = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = result.error?.message || '查看返回值';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(result.error || result.value, null, 2);
detail.append(summary, pre);
row.append(heading, detail);
return row;
}));
$('#report').textContent = JSON.stringify(report(), null, 2);
const counts = status => results.filter(result => result.status === status).length;
$('#status').textContent = `${running ? '运行中' : '已完成'} · 通过 ${counts('pass')} / 失败 ${counts('fail')} / 待观察 ${counts('observed')} / 跳过 ${counts('skip')}`;
}
async function check(name, action, status = 'pass') {
const start = performance.now();
try { results.push({ name, status, value: await action(), durationMs: Math.round(performance.now() - start) }); }
catch (error) { results.push({ name, status: 'fail', error: serializeError(error), durationMs: Math.round(performance.now() - start) }); }
refresh();
}
async function exclusive(action) {
if (running) return;
running = true;
document.querySelectorAll('button').forEach(button => { button.disabled = true; });
try { await action(); }
finally {
running = false;
document.querySelectorAll('button').forEach(button => { button.disabled = false; });
refresh();
}
}
function manual(name, action, status = 'observed') {
const button = document.createElement('button');
button.textContent = name;
button.onclick = () => exclusive(() => check(name, action, status));
$('#manual').append(button);
}
const descriptions = {
host: '检查真实 Host 桥接与数据操作。常规检查只在本插件命名空间和临时目录写入测试数据;隔离标记会保留以便复测。',
backend: `检查 ${config.service}:原生进程、并发、超时、取消、Host 回调和事件。跨插件测试需先安装公共服务提供方。公共服务返回值校验要求底座开启严格契约模式;默认未开启时,该项会报告失败。`,
dependency: `此插件声明了 ${config.service}。预期调用错误为 ${config.expectedError};“service.not_found”不能代替版本不兼容错误。`,
fault: '此后端故意无法完成启动。点击运行检查记录实际错误码;恢复与重试应在底座管理界面操作。',
upgrade: '先安装 1.0.0 并写入基线标记,再安装 1.1.0。关闭后重新打开,检查运行版本与数据保留;也可在底座回滚。',
};
$('#title').textContent = config.name;
document.title = config.name;
$('#description').textContent = descriptions[config.mode];
function configureManualActions() {
manual('读取持久化标记', () => call('host.storage', 'get', { key: config.mode === 'upgrade' ? 'qa.upgrade-marker' : 'qa.namespace-owner' }), 'pass');
if (config.mode === 'host') {
manual('读取本窗口尺寸', () => call('host.window', 'getBounds'), 'pass');
manual('设置窗口标题', () => call('host.window', 'setTitle', { title: `${config.name} · 标题测试成功` }));
manual('窗口居中', () => call('host.window', 'center'));
manual('文件选择(可取消)', () => call('host.dialog', 'openFile', { title: 'Trove QA:选择文件或取消' }, { timeoutMs: 120000 }));
manual('目录选择(可取消)', () => call('host.dialog', 'openDirectory', { title: 'Trove QA:选择目录或取消' }, { timeoutMs: 120000 }));
manual('保存对话框(仅选择路径)', () => call('host.dialog', 'saveFile', { title: 'Trove QA:只选择路径,不写入文件' }, { timeoutMs: 120000 }));
manual('确认对话框', () => call('host.dialog', 'confirm', { title: 'Trove QA', message: '确认和取消都应返回明确结果。' }, { timeoutMs: 120000 }));
manual('写入并读取测试剪贴板', async () => {
const text = 'Trove QA 剪贴板 🧪';
await call('host.clipboard', 'writeText', { text });
equal((await call('host.clipboard', 'readText')).text, text);
return { text, note: '系统剪贴板已被替换为测试文本。' };
}, 'pass');
manual('发送测试通知', () => call('host.notification', 'show', { title: 'Trove QA', body: `来自 ${config.id} 的测试通知` }));
manual('快捷键触发(15 秒)', async () => {
const events = [];
let registration;
const subscription = await trove.subscribe('host.shortcut', 'triggered', {}, event => events.push(event));
try {
registration = await call('host.shortcut', 'register', { shortcut: 'CommandOrControl+Shift+Alt+F9' });
$('#status').textContent = '请在 15 秒内按 Cmd+Shift+Option+F9(部分键盘需要 Fn)。';
const end = Date.now() + 15000;
while (!events.length && Date.now() < end) await new Promise(resolve => setTimeout(resolve, 50));
assert(events.some(event => event.registrationId === registration.registrationId), '15 秒内没有收到匹配的快捷键事件');
return events;
} finally {
try { if (registration) await call('host.shortcut', 'unregister', registration); }
finally { await subscription.close(); }
}
}, 'pass');
manual('列出正在运行的应用', () => call('host.application', 'listRunning'), 'pass');
manual('检查随底座启动的服务', async () => {
const service = 'dev.trove.qa.autostart.probe';
// Discovery does not start the backend; keep this separate from info().
return call('host.services', 'getStatus', { service });
});
manual('关闭本窗口', () => call('host.window', 'close'));
}
if (config.mode === 'backend') {
manual('读取后端 PID 与活动数', () => call(config.service, 'info'), 'pass');
if (config.service !== '@self') manual('查询提供方状态(不启动)', () => call('host.services', 'getStatus', { service: config.service }));
if (config.directory === 'test-crash') manual('触发本后端崩溃(exit 42)', async () => {
try { await call('@self', 'crash', {}, { timeoutMs: 6000 }); }
catch (error) {
assert(['provider.disconnected', 'provider.crashed'].includes(error.code), `预期进程断开,实际 ${error.code}`);
return { error: serializeError(error), next: '再次读取 PID 验证恢复;短时间反复触发可观察熔断。' };
}
throw new Error('后端未断开');
});
}
if (config.mode === 'upgrade') {
manual('写入升级基线标记', async () => {
const value = { token: crypto.randomUUID(), writtenByVersion: trove.context.curioVersion, writtenAt: new Date().toISOString() };
await call('host.storage', 'set', { key: 'qa.upgrade-marker', value });
return value;
}, 'pass');
}
}
$('#run').onclick = () => exclusive(async () => {
results.length = 0;
if (config.mode === 'host') for (const [name, action] of hostChecks(trove)) await check(name, action);
if (config.mode === 'backend') for (const [name, action] of backendChecks(trove, config.service)) await check(name, action);
if (config.mode === 'dependency') {
await check('缺失依赖时 Host 仍可用', async () => equal(await call('host.runtime', 'echo', { alive: true }), { alive: true }));
await check('依赖调用返回预期错误', () => expectError(() => call(config.service, 'echo'), config.expectedError));
}
if (config.mode === 'fault') {
await check('启动失败被限制在截止时间内', async () => {
try { await call('@self', 'info', {}, { timeoutMs: 6000 }); }
catch (error) {
const expected = config.directory === 'test-start-timeout'
? ['provider.start_timeout']
: ['provider.start_failed', 'provider.disconnected', 'provider.crashed', 'provider.start_timeout'];
assert(expected.includes(error.code), `非预期错误 ${error.code}`);
return { actual: serializeError(error), next: '查看底座后端日志确认具体启动原因。' };
}
throw new Error('故障后端意外启动成功');
});
}
if (config.mode === 'upgrade') {
await check('运行版本与打包版本一致', async () => { equal(trove.context.curioVersion, config.version); return trove.context; });
await check('升级基线标记保留', async () => {
const marker = await call('host.storage', 'get', { key: 'qa.upgrade-marker' });
assert(marker.found, '尚无基线标记,请在 1.0.0 中写入后再验证升级');
equal(marker.value.writtenByVersion, '1.0.0', '标记不是由基线版本写入');
return marker.value;
});
}
});
$('#save-report').onclick = () => exclusive(() => check('保存 JSON 报告', async () => {
const paths = await call('host.runtime', 'getPaths');
const path = `${paths.data}/qa-report-${Date.now()}.json`;
await call('host.fs', 'writeText', { path, content: JSON.stringify(report(), null, 2) });
return { path };
}));
try {
await trove.ready();
$('#context').textContent = `${trove.context.curioId} · v${trove.context.curioVersion} · ${trove.context.mode} · ${trove.context.instanceId}`;
configureManualActions();
document.querySelectorAll('button').forEach(button => { button.disabled = false; });
$('#status').textContent = '已连接真实 Trove Host,点击运行常规检查。';
} catch (error) {
$('#status').textContent = `连接失败:${error.code ?? ''} ${error.message}。请从 Trove 安装并打开本插件。`;
}