166 lines
7.8 KiB
JavaScript
166 lines
7.8 KiB
JavaScript
|
|
// 运行:node --test scripts/eq-save-regression.test.mjs
|
||
|
|
// 无硬件测试:执行实际 API 和从 EQPage 提取的提交函数,不复制待测实现。
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { readFileSync } from 'node:fs';
|
||
|
|
import { test } from 'node:test';
|
||
|
|
import ts from 'typescript';
|
||
|
|
import { build } from 'esbuild';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
const pageText = readFileSync(new URL('../client/src/pages/EQPage.tsx', import.meta.url), 'utf8');
|
||
|
|
const transpile = text => ts.transpileModule(text, {
|
||
|
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS },
|
||
|
|
}).outputText;
|
||
|
|
const bundle = await build({
|
||
|
|
entryPoints: [fileURLToPath(new URL('../client/src/lib/luxsinApi.ts', import.meta.url))],
|
||
|
|
bundle: true, write: false, format: 'esm', platform: 'node',
|
||
|
|
define: { 'import.meta.env.DEV': 'false' },
|
||
|
|
alias: { '@': fileURLToPath(new URL('../client/src', import.meta.url)) },
|
||
|
|
});
|
||
|
|
const { LuxsinAPI, encodeCustomBase64, decodeCustomBase64 } = await import(
|
||
|
|
'data:text/javascript;base64,' + Buffer.from(bundle.outputFiles[0].text).toString('base64')
|
||
|
|
);
|
||
|
|
const band = gain => ({ freq: 100, gain, q: 1, type: 0, enabled: true });
|
||
|
|
const preset = gain => ({ name: 'Copy', preamp: -2, autoPre: 0, canDel: 1,
|
||
|
|
filters: [{ fc: 100, gain, q: 1, type: 0 }] });
|
||
|
|
const flushMicrotasks = async () => { for (let i = 0; i < 30; i++) await Promise.resolve(); };
|
||
|
|
|
||
|
|
function scheduler() {
|
||
|
|
const source = ts.createSourceFile('EQPage.tsx', pageText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||
|
|
const component = source.statements.find(n => ts.isFunctionDeclaration(n) && n.name?.text === 'EQPage');
|
||
|
|
const names = ['peqSyncTimerRef', 'pendingPeqSyncRef', 'inFlightPeqSyncRef', 'peqPresetCacheRef',
|
||
|
|
'cancelPendingPeqSync', 'flushPendingPeqSync', 'rememberPeqPresetCache',
|
||
|
|
'persistPresetEditsToPeqItem', 'schedulePeqSync'];
|
||
|
|
const definitions = names.map(name => {
|
||
|
|
const node = component.body.statements.find(n => ts.isVariableStatement(n)
|
||
|
|
&& n.declarationList.declarations.some(d => ts.isIdentifier(d.name) && d.name.text === name));
|
||
|
|
assert.ok(node, `缺少待测定义 ${name}`);
|
||
|
|
return node.getText(source);
|
||
|
|
}).join('\n');
|
||
|
|
const timers = new Map();
|
||
|
|
let sequence = 0;
|
||
|
|
const requests = [];
|
||
|
|
const errors = [];
|
||
|
|
const window = {
|
||
|
|
setTimeout: fn => { const id = ++sequence; timers.set(id, fn); return id; },
|
||
|
|
clearTimeout: id => timers.delete(id),
|
||
|
|
};
|
||
|
|
const setup = `
|
||
|
|
const useRef = current => ({current});
|
||
|
|
const useCallback = fn => fn;
|
||
|
|
const api = {}, isDemoMode = false;
|
||
|
|
let peqItems = [${JSON.stringify(preset(0))}], updates = [];
|
||
|
|
const setPeqItems = updater => updates.push(updater);
|
||
|
|
const bandToPeqFilter = b => ({fc: b.freq, gain: b.gain, q: b.q, type: b.type});
|
||
|
|
const buildPeqPresetBody = (peq, filters) => ({...peq, filters});
|
||
|
|
const upgradePeqChange = async payload => { requests.push(payload); };
|
||
|
|
const upgradePeqApply = async payload => { requests.push(payload); };
|
||
|
|
const toast = {error: message => errors.push(message)};
|
||
|
|
${definitions}
|
||
|
|
return {
|
||
|
|
schedule: schedulePeqSync, flush: flushPendingPeqSync,
|
||
|
|
cache: peqPresetCacheRef.current,
|
||
|
|
render: () => { for (const update of updates) peqItems = update(peqItems); updates = []; return peqItems; }
|
||
|
|
};`;
|
||
|
|
return { ...new Function('window', 'requests', 'errors', transpile(setup))(window, requests, errors), timers, requests, errors };
|
||
|
|
}
|
||
|
|
|
||
|
|
test('A 显式保存立即发送,不依赖 React updater 求值或定时器', async () => {
|
||
|
|
const eq = scheduler();
|
||
|
|
await eq.schedule(preset(0), [band(3)], 'A', 0);
|
||
|
|
assert.equal(eq.requests.length, 1);
|
||
|
|
assert.equal(eq.requests[0].peqChange.filters[0].gain, 3);
|
||
|
|
assert.equal(eq.timers.size, 0);
|
||
|
|
assert.equal(eq.cache.Copy.filters[0].gain, 3);
|
||
|
|
assert.equal(eq.render()[0].filters[0].gain, 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('单次编辑在防抖窗口内切换/卸载时补发,且不重复发送', async () => {
|
||
|
|
const eq = scheduler();
|
||
|
|
await eq.schedule(preset(0), [band(4)], 'A');
|
||
|
|
assert.equal(eq.requests.length, 0);
|
||
|
|
await eq.flush();
|
||
|
|
await eq.flush();
|
||
|
|
assert.equal(eq.requests.length, 1);
|
||
|
|
assert.equal(eq.requests[0].peqChange.filters[0].gain, 4);
|
||
|
|
assert.equal(eq.timers.size, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('连续编辑合并为最新快照;元数据也进入载荷', async () => {
|
||
|
|
const eq = scheduler();
|
||
|
|
await eq.schedule(preset(0), [band(1)], 'A');
|
||
|
|
await eq.schedule({ ...preset(0), preamp: -7, autoPre: 1 }, [band(2)], 'A');
|
||
|
|
assert.equal(eq.timers.size, 1);
|
||
|
|
await eq.flush();
|
||
|
|
assert.equal(eq.requests.length, 1);
|
||
|
|
assert.deepEqual([eq.requests[0].peqChange.filters[0].gain, eq.requests[0].peqChange.preamp, eq.requests[0].peqChange.autoPre], [2, -7, 1]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('B 试听不能替换尚未发送的 A 保存', async () => {
|
||
|
|
const eq = scheduler();
|
||
|
|
await eq.schedule(preset(0), [band(1)], 'A');
|
||
|
|
await eq.schedule({ ...preset(0), preamp: -8 }, [band(5)], 'B', 0);
|
||
|
|
assert.deepEqual(eq.requests.map(p => Object.keys(p)[0]), ['peqChange', 'peqApply']);
|
||
|
|
assert.equal(eq.cache.Copy.filters[0].gain, 1);
|
||
|
|
assert.equal(eq.cache.Copy.preamp, -2);
|
||
|
|
assert.equal(eq.requests[1].peqApply.preamp, -8);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('B 的待发试听在离开时取消,不写入预设缓存', async () => {
|
||
|
|
const eq = scheduler();
|
||
|
|
await eq.schedule(preset(0), [band(5)], 'B');
|
||
|
|
await eq.flush();
|
||
|
|
assert.equal(eq.requests.length, 0);
|
||
|
|
assert.equal(eq.timers.size, 0);
|
||
|
|
assert.deepEqual(eq.cache, {});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('切换调度目标时先提交原预设,而不是覆盖它的保存', async () => {
|
||
|
|
const eq = scheduler();
|
||
|
|
await eq.schedule(preset(0), [band(3)], 'A');
|
||
|
|
await eq.schedule({ ...preset(0), name: 'Other' }, [band(4)], 'A', 0);
|
||
|
|
assert.deepEqual(eq.requests.map(p => p.peqChange.name), ['Copy', 'Other']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('真实 API 串行执行保存、试听、选择、目录操作及读取', async t => {
|
||
|
|
const started = [], release = [];
|
||
|
|
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
||
|
|
assert.equal(new URL(url).hostname, 'eq-test.invalid');
|
||
|
|
const payload = options?.body ? JSON.parse(decodeCustomBase64(new URLSearchParams(options.body).get('json'))) : null;
|
||
|
|
started.push(payload ? Object.keys(payload)[0] : new URL(url).searchParams.get('action'));
|
||
|
|
await new Promise(resolve => release.push(resolve));
|
||
|
|
return new Response(encodeCustomBase64(JSON.stringify({ peq: [], filters: [] })));
|
||
|
|
});
|
||
|
|
const api = new LuxsinAPI('eq-test.invalid');
|
||
|
|
const calls = [api.upgradePeqChange({ peqChange: preset(1) }), api.upgradePeqApply({ peqApply: preset(2) }),
|
||
|
|
api.setPeqSelect(1), api.renamePeq('Copy', 'Renamed'), api.sortPeq(['Renamed'], 0), api.removePeq(['Renamed']), api.getPeqState()];
|
||
|
|
await flushMicrotasks();
|
||
|
|
assert.deepEqual(started, ['peqChange']);
|
||
|
|
for (let i = 0; i < calls.length; i++) {
|
||
|
|
assert.equal(started.length, i + 1, '前一个响应完成之前不应启动下一个请求');
|
||
|
|
release[i]();
|
||
|
|
await flushMicrotasks();
|
||
|
|
}
|
||
|
|
await Promise.all(calls);
|
||
|
|
assert.deepEqual(started, ['peqChange', 'peqApply', 'setting', 'peqRename', 'peqSoft', 'peqRemove', 'syncPeq']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('HTTP 500 必须失败,后续重试不被失败队列阻塞', async t => {
|
||
|
|
let count = 0;
|
||
|
|
t.mock.method(globalThis, 'fetch', async () => new Response('', { status: ++count === 1 ? 500 : 200 }));
|
||
|
|
const api = new LuxsinAPI('eq-test.invalid');
|
||
|
|
await assert.rejects(api.upgradePeqChange({ peqChange: preset(1) }), /500/);
|
||
|
|
await api.upgradePeqChange({ peqChange: preset(2) });
|
||
|
|
assert.equal(count, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('保存失败不会变成静默成功;网络异常之后可重试', async t => {
|
||
|
|
let count = 0;
|
||
|
|
t.mock.method(globalThis, 'fetch', async () => {
|
||
|
|
if (++count === 1) throw new TypeError('mock network failure');
|
||
|
|
return new Response('');
|
||
|
|
});
|
||
|
|
const api = new LuxsinAPI('eq-test.invalid');
|
||
|
|
await assert.rejects(api.upgradePeqChange({ peqChange: preset(1) }), /network/);
|
||
|
|
await api.upgradePeqChange({ peqChange: preset(2) });
|
||
|
|
});
|