103 lines
4.9 KiB
JavaScript
103 lines
4.9 KiB
JavaScript
// 使用 Node 内置测试器和最小 DOM 验证真实页面脚本,无需安装浏览器或 GPU。
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
|
|
class Element {
|
|
constructor() {
|
|
this.childNodes = [];
|
|
this.className = '';
|
|
this.style = {};
|
|
this.textContent = '';
|
|
this.classList = { add() {}, remove() {}, toggle() {} };
|
|
}
|
|
append(...nodes) { nodes.forEach(node => this.appendChild(node)); }
|
|
appendChild(node) { node.parent = this; this.childNodes.push(node); }
|
|
replaceChildren() { this.childNodes = []; }
|
|
addEventListener() {}
|
|
remove() { if (this.parent) this.parent.childNodes = this.parent.childNodes.filter(n => n !== this); }
|
|
querySelector(selector) {
|
|
const name = selector.slice(1);
|
|
for (const child of this.childNodes) {
|
|
if (child.className.split(' ').includes(name)) return child;
|
|
const nested = child.querySelector(selector);
|
|
if (nested) return nested;
|
|
}
|
|
return null;
|
|
}
|
|
get firstChild() { return this.childNodes[0]; }
|
|
}
|
|
|
|
// 每个测试使用独立页面上下文,不共享会话状态。
|
|
function page() {
|
|
const elements = new Map();
|
|
const timers = [];
|
|
const context = vm.createContext({
|
|
document: {
|
|
getElementById(id) { if (!elements.has(id)) elements.set(id, new Element()); return elements.get(id); },
|
|
createElement() { return new Element(); },
|
|
body: new Element(),
|
|
},
|
|
console,
|
|
setTimeout(fn, ms) { timers.push({ fn, ms }); },
|
|
clearInterval() {},
|
|
WebSocket: { OPEN: 1 },
|
|
fetch: async () => ({ json: async () => ({ model: 'test', model_service_url: 'http://fake/v1' }) }),
|
|
});
|
|
vm.runInContext(fs.readFileSync(path.join(__dirname, '../static/app.js'), 'utf8'), context);
|
|
return { context, elements, timers };
|
|
}
|
|
|
|
const block = (id, speaker, text = 'text', reason = '') => ({
|
|
block_id: `block-${id}`, sentence: text, sentence_type: 1, start_time: id * 1000, end_time: (id + 1) * 1000,
|
|
speaker_id: speaker, speaker_name: speaker < 0 ? '' : `Person ${speaker}`,
|
|
speaker_evidence: speaker < 0 ? 'pending' : 'confirmed', speaker_reason: reason,
|
|
});
|
|
|
|
test('display snapshots update and merge pending rows; stale snapshots are ignored', () => {
|
|
const { context, elements } = page();
|
|
context.renderDisplayState({ revision: 1, display_blocks: [block(0, -1), block(1, -1)] }, true);
|
|
assert.equal(elements.get('resultArea').childNodes.length, 2);
|
|
context.renderDisplayState({ revision: 2, display_blocks: [block(0, 0, 'A B')] }, true);
|
|
assert.equal(elements.get('resultArea').childNodes.length, 1);
|
|
assert.equal(elements.get('resultArea').querySelector('.speaker-name').textContent, 'Person 0');
|
|
context.renderDisplayState({ revision: 1, display_blocks: [block(0, -1), block(1, -1)] }, true);
|
|
assert.equal(elements.get('resultArea').childNodes.length, 1);
|
|
});
|
|
|
|
test('A B A retains time order and an unknown interruption has its own bubble', () => {
|
|
const { context, elements } = page();
|
|
context.renderDisplayState({ revision: 1, display_blocks: [block(0, 0), block(1, 1), block(2, 0), block(3, -1, 'short', '音频不足')] }, true);
|
|
const rows = elements.get('resultArea').childNodes;
|
|
assert.deepEqual(rows.map(row => row.querySelector('.speaker-name').textContent), ['Person 0', 'Person 1', 'Person 0', '未知说话人']);
|
|
assert.equal(rows[3].querySelector('.speaker-name').title, '音频不足');
|
|
});
|
|
|
|
test('sentences do not duplicate display_state and end uses the final snapshot', () => {
|
|
const { context, elements } = page();
|
|
vm.runInContext('displayStateSupported = true', context);
|
|
context.handleServerMessage({ type: 'sentences', sentences: [{ ...block(0, -1), sentence_id: 0 }] }, true);
|
|
assert.equal(elements.get('resultArea').childNodes.length, 0);
|
|
context.handleServerMessage({ type: 'end', display_blocks: [block(0, 0)], sentences: [] }, true);
|
|
assert.equal(elements.get('resultArea').childNodes.length, 1);
|
|
});
|
|
|
|
test('stop sends a control message without scheduling a forced close', () => {
|
|
const { context, timers } = page();
|
|
vm.runInContext('var sent = []; ws = { readyState: 1, send(message) { sent.push(JSON.parse(message)); } }; sending = true', context);
|
|
context.stopRecognition();
|
|
assert.equal(vm.runInContext('sent[0].type', context), 'stop');
|
|
assert.equal(timers.length, 0);
|
|
assert.equal(vm.runInContext('ws !== null', context), true);
|
|
});
|
|
|
|
test('raw logs render untrusted transcript as text', () => {
|
|
const { context, elements } = page();
|
|
context.appendLog({ type: 'sentences', text: '<img src=x onerror=alert(1)>' });
|
|
const entry = elements.get('logArea').childNodes[0];
|
|
assert.equal(entry.innerHTML, undefined);
|
|
assert.ok(entry.childNodes[1].textContent.includes('<img'));
|
|
});
|