126 lines
4.7 KiB
JavaScript
126 lines
4.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { after, test } from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { createServer } from 'vite';
|
|
|
|
const vite = await createServer({
|
|
root: fileURLToPath(new URL('..', import.meta.url)),
|
|
configFile: false,
|
|
optimizeDeps: { noDiscovery: true },
|
|
server: { middlewareMode: true, hmr: false },
|
|
appType: 'custom'
|
|
});
|
|
|
|
after(async () => {
|
|
await vite.close();
|
|
delete globalThis.plus;
|
|
});
|
|
|
|
const load = () => vite.ssrLoadModule('/src/utils/microphone.ts');
|
|
|
|
test('底层音频错误一律翻译为中文,英文原文不得透给用户', async () => {
|
|
const { describeMicrophoneError } = await load();
|
|
const fallback = '实时陪练连接失败,请使用录音对练';
|
|
|
|
// 这些是 Chromium / WebRTC 的真实原文,真机上曾直接展示给用户。
|
|
const rawErrors = [
|
|
new Error('Could not start audio source'),
|
|
Object.assign(new Error('Permission denied'), { name: 'NotAllowedError' }),
|
|
Object.assign(new Error('Requested device not found'), { name: 'NotFoundError' }),
|
|
Object.assign(new Error('Could not start audio source'), { name: 'NotReadableError' }),
|
|
'getUserMedia requires a secure context'
|
|
];
|
|
|
|
for (const error of rawErrors) {
|
|
const text = describeMicrophoneError(error, fallback);
|
|
assert.match(text, /[一-龥]/, `应翻译为中文: ${error}`);
|
|
assert.ok(!/[A-Za-z]{6,}/.test(text), `不应残留英文原文: ${text}`);
|
|
}
|
|
});
|
|
|
|
test('业务层自抛的中文错误原样透出,不被兜底文案吞掉', async () => {
|
|
const { describeMicrophoneError } = await load();
|
|
const own = '实时语音模块尚未就绪,请稍后重试';
|
|
assert.equal(describeMicrophoneError(new Error(own), '兜底文案'), own);
|
|
});
|
|
|
|
test('无法识别的错误回落到兜底文案', async () => {
|
|
const { describeMicrophoneError } = await load();
|
|
assert.equal(describeMicrophoneError(new Error('zzz unknown'), '兜底文案'), '兜底文案');
|
|
assert.equal(describeMicrophoneError(undefined, '兜底文案'), '兜底文案');
|
|
});
|
|
|
|
test('Android 运行时主动申请 RECORD_AUDIO 并区分拒绝与永久拒绝', async () => {
|
|
const requested = [];
|
|
globalThis.plus = {
|
|
android: {
|
|
runtimeMainActivity: () => ({ hasWindowFocus: () => true }),
|
|
requestPermissions: (permissions, success) => {
|
|
requested.push(...permissions);
|
|
success(pendingResult);
|
|
}
|
|
}
|
|
};
|
|
let pendingResult = { granted: ['android.permission.RECORD_AUDIO'] };
|
|
const { requestMicrophonePermission } = await load();
|
|
|
|
assert.equal(await requestMicrophonePermission(), 'granted');
|
|
assert.deepEqual(requested, ['android.permission.RECORD_AUDIO']);
|
|
|
|
pendingResult = { deniedPresent: ['android.permission.RECORD_AUDIO'] };
|
|
assert.equal(await requestMicrophonePermission(), 'denied');
|
|
|
|
pendingResult = { deniedAlways: ['android.permission.RECORD_AUDIO'] };
|
|
assert.equal(await requestMicrophonePermission(), 'denied-always');
|
|
|
|
delete globalThis.plus;
|
|
});
|
|
|
|
test('权限 Activity 未归还窗口焦点前不结束授权流程,避免文件选择器被系统吞掉', async () => {
|
|
let focused = false;
|
|
let importedActivity = false;
|
|
const activity = { hasWindowFocus: () => focused };
|
|
globalThis.plus = {
|
|
android: {
|
|
importClass: (target) => {
|
|
assert.equal(target, activity);
|
|
importedActivity = true;
|
|
},
|
|
runtimeMainActivity: () => activity,
|
|
requestPermissions: (_permissions, success) => {
|
|
success({ deniedAlways: ['android.permission.RECORD_AUDIO'] });
|
|
}
|
|
}
|
|
};
|
|
const { requestMicrophonePermission } = await load();
|
|
const lifecycle = await vite.ssrLoadModule('/src/services/file-selection-lifecycle.ts');
|
|
let resolved = false;
|
|
const permission = requestMicrophonePermission().then((result) => {
|
|
resolved = true;
|
|
return result;
|
|
});
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
assert.equal(importedActivity, true);
|
|
assert.equal(lifecycle.isPlatformNativeActivityActive(), true);
|
|
assert.equal(lifecycle.isPlatformFileSelectionActive(), false);
|
|
assert.equal(resolved, false);
|
|
|
|
focused = true;
|
|
assert.equal(await permission, 'denied-always');
|
|
assert.equal(resolved, true);
|
|
assert.equal(lifecycle.isPlatformNativeActivityActive(), false);
|
|
});
|
|
|
|
test('非 App 运行时返回 unsupported,交由平台原生授权流程处理', async () => {
|
|
delete globalThis.plus;
|
|
const { requestMicrophonePermission } = await load();
|
|
assert.equal(await requestMicrophonePermission(), 'unsupported');
|
|
});
|
|
|
|
test('永久拒绝的引导文案指向系统设置,普通拒绝不误导', async () => {
|
|
const { microphoneDeniedMessage } = await load();
|
|
assert.match(microphoneDeniedMessage('denied-always'), /系统设置/);
|
|
assert.ok(!/系统设置/.test(microphoneDeniedMessage('denied')));
|
|
});
|