主要变更: 1. 新增多风格音效资源与管理文档 2. 修复翻译服务空响应处理与Dio日志异常捕获 3. 完善Web端平台适配与路径获取Stub 4. 优化设备配对与文件传输功能 5. 新增角色命名常量与摇一摇检测器 6. 修复Riverpod dispose与鸿蒙导航路由 7. 新增每日通知服务与流体着色器 8. 优化备份服务与数据管理页面 9. 新增隐私设置附近设备发现选项 10. 重构诗词提供者支持历史记录 11. 完善桌面端构建配置与开发脚本 12. 清理旧版工具部署脚本
382 lines
15 KiB
JavaScript
382 lines
15 KiB
JavaScript
// ============================================================
|
|
// 闲言APP — 本地逻辑测试脚本
|
|
// 创建时间: 2026-05-20
|
|
// 更新时间: 2026-05-20
|
|
// 作用: 测试配对码生成/设备名称/画布样式等本地逻辑
|
|
// 上次更新: 初始创建
|
|
// ============================================================
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const results = [];
|
|
|
|
function assert(condition, testName, detail) {
|
|
if (condition) {
|
|
passed++;
|
|
results.push({ name: testName, status: 'PASS', detail: '' });
|
|
console.log(` ✅ ${testName}`);
|
|
} else {
|
|
failed++;
|
|
results.push({ name: testName, status: 'FAIL', detail: detail || '' });
|
|
console.log(` ❌ ${testName} — ${detail || ''}`);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 1: Pairing Code Generation (4-digit numeric)
|
|
// ============================================================
|
|
function testPairingCodeGeneration() {
|
|
console.log('\n=== Test 1: Pairing Code Generation ===');
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
let code = '';
|
|
for (let j = 0; j < 4; j++) {
|
|
code += Math.floor(Math.random() * 10).toString();
|
|
}
|
|
assert(/^\d{4}$/.test(code), `Pairing code #${i + 1} is 4-digit numeric`, `code=${code}`);
|
|
if (i >= 9) break;
|
|
}
|
|
|
|
assert(!/^[0-9]{6}$/.test('1234'), '6-digit code does not match 4-digit pattern', '');
|
|
assert(!/^[0-9]{4}$/.test('abcd'), 'Non-numeric code rejected by pattern', '');
|
|
assert(!/^[0-9]{4}$/.test('12a4'), 'Mixed alphanumeric rejected by pattern', '');
|
|
assert(/^[0-9]{4}$/.test('0000'), 'All-zero code is valid 4-digit', '');
|
|
assert(/^[0-9]{4}$/.test('9999'), 'All-nine code is valid 4-digit', '');
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 2: Device Name Display Logic
|
|
// ============================================================
|
|
function testDeviceNameDisplay() {
|
|
console.log('\n=== Test 2: Device Name Display Logic ===');
|
|
|
|
function displayAlias({ accountAlias, alias, deviceType, deviceModel }) {
|
|
if (accountAlias && accountAlias.isNotEmpty !== false) return accountAlias;
|
|
if (deviceType === 'web') return alias && alias.length > 0 ? alias : 'Web浏览器';
|
|
if (alias !== '闲言设备' && alias !== '未知设备') return alias;
|
|
const parts = [];
|
|
if (deviceModel && deviceModel.length > 0 && deviceModel !== 'localhost') {
|
|
parts.push(deviceModel);
|
|
}
|
|
if (parts.isNotEmpty !== false && parts.length > 0) return parts.join(' · ');
|
|
return alias;
|
|
}
|
|
|
|
assert(
|
|
displayAlias({ accountAlias: '我的iPhone', alias: '闲言设备', deviceType: 'mobile', deviceModel: 'localhost' }) === '我的iPhone',
|
|
'accountAlias takes priority',
|
|
''
|
|
);
|
|
|
|
assert(
|
|
displayAlias({ accountAlias: null, alias: '闲言设备', deviceType: 'mobile', deviceModel: 'localhost' }) === '闲言设备',
|
|
'localhost deviceModel filtered, falls back to alias',
|
|
''
|
|
);
|
|
|
|
assert(
|
|
displayAlias({ accountAlias: null, alias: '闲言设备', deviceType: 'mobile', deviceModel: 'Samsung Galaxy S24' }) === 'Samsung Galaxy S24',
|
|
'Real deviceModel used instead of alias',
|
|
''
|
|
);
|
|
|
|
assert(
|
|
displayAlias({ accountAlias: null, alias: 'Web浏览器', deviceType: 'web', deviceModel: '' }) === 'Web浏览器',
|
|
'Web device type returns alias',
|
|
''
|
|
);
|
|
|
|
assert(
|
|
displayAlias({ accountAlias: null, alias: '闲言设备', deviceType: 'mobile', deviceModel: '' }) === '闲言设备',
|
|
'Empty deviceModel falls back to alias',
|
|
''
|
|
);
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 3: Canvas Style Rendering Order
|
|
// ============================================================
|
|
function testCanvasStyleRenderingOrder() {
|
|
console.log('\n=== Test 3: Canvas Style Rendering Order ===');
|
|
|
|
const correctOrder = ['child', 'clipRadius', 'border', 'shadow', 'stackLayers', 'outerMargin'];
|
|
const oldOrder = ['outerMargin', 'stackLayers', 'shadow', 'clipRadius', 'border', 'child'];
|
|
|
|
assert(correctOrder.indexOf('clipRadius') < correctOrder.indexOf('border'), 'ClipRRect before border', '');
|
|
assert(correctOrder.indexOf('clipRadius') < correctOrder.indexOf('shadow'), 'ClipRRect before shadow', '');
|
|
assert(correctOrder.indexOf('child') < correctOrder.indexOf('clipRadius'), 'Child before ClipRRect', '');
|
|
assert(correctOrder.indexOf('border') < correctOrder.indexOf('shadow'), 'Border before shadow', '');
|
|
|
|
assert(oldOrder.indexOf('clipRadius') > oldOrder.indexOf('shadow'), 'Old: ClipRRect was after shadow (bug)', '');
|
|
assert(oldOrder.indexOf('clipRadius') < oldOrder.indexOf('border'), 'Old: ClipRRect was before border (but after shadow, which was the bug)', `clipPos=${oldOrder.indexOf('clipRadius')} borderPos=${oldOrder.indexOf('border')}`);
|
|
|
|
console.log(' Correct order: ' + correctOrder.join(' → '));
|
|
console.log(' Old (buggy) order: ' + oldOrder.join(' → '));
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 4: Canvas Style Model Defaults
|
|
// ============================================================
|
|
function testCanvasStyleModelDefaults() {
|
|
console.log('\n=== Test 4: Canvas Style Model Defaults ===');
|
|
|
|
const defaults = {
|
|
borderRadius: 0,
|
|
borderWidth: 0,
|
|
borderColor: '',
|
|
borderStyle: 'solid',
|
|
shadowBlur: 0,
|
|
shadowOffsetX: 0,
|
|
shadowOffsetY: 0,
|
|
shadowColor: '',
|
|
stackCount: 0,
|
|
stackOffsetX: 0,
|
|
stackOffsetY: 0,
|
|
stackBorderColor: '',
|
|
outerMarginLeft: 0,
|
|
outerMarginRight: 0,
|
|
outerMarginTop: 0,
|
|
outerMarginBottom: 0,
|
|
};
|
|
|
|
assert(defaults.borderRadius === 0, 'Default borderRadius is 0', '');
|
|
assert(defaults.borderWidth === 0, 'Default borderWidth is 0', '');
|
|
assert(defaults.shadowBlur === 0, 'Default shadowBlur is 0', '');
|
|
assert(defaults.stackCount === 0, 'Default stackCount is 0', '');
|
|
assert(defaults.borderStyle === 'solid', 'Default borderStyle is solid', '');
|
|
|
|
function hasBorder(style) { return (style.borderWidth ?? 0) > 0; }
|
|
function hasShadow(style) { return (style.shadowBlur ?? 0) > 0; }
|
|
function hasStack(style) { return (style.stackCount ?? 0) > 0; }
|
|
function hasOuterMargin(style) {
|
|
return (style.outerMarginLeft ?? 0) > 0 || (style.outerMarginRight ?? 0) > 0 ||
|
|
(style.outerMarginTop ?? 0) > 0 || (style.outerMarginBottom ?? 0) > 0;
|
|
}
|
|
|
|
assert(!hasBorder(defaults), 'Defaults have no border', '');
|
|
assert(!hasShadow(defaults), 'Defaults have no shadow', '');
|
|
assert(!hasStack(defaults), 'Defaults have no stack layers', '');
|
|
assert(!hasOuterMargin(defaults), 'Defaults have no outer margin', '');
|
|
|
|
const customStyle = { ...defaults, borderRadius: 20, borderWidth: 2, shadowBlur: 10, stackCount: 3 };
|
|
assert(hasBorder(customStyle), 'Custom style has border', '');
|
|
assert(hasShadow(customStyle), 'Custom style has shadow', '');
|
|
assert(hasStack(customStyle), 'Custom style has stack layers', '');
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 5: Screen Share State Transitions
|
|
// ============================================================
|
|
function testScreenShareStateTransitions() {
|
|
console.log('\n=== Test 5: Screen Share State Transitions ===');
|
|
|
|
const validStates = [
|
|
{ isSharing: false, isViewing: false, isActive: false, label: 'idle' },
|
|
{ isSharing: true, isViewing: false, isActive: true, label: 'sharing' },
|
|
{ isSharing: false, isViewing: true, isActive: true, label: 'viewing' },
|
|
];
|
|
|
|
const invalidStates = [
|
|
{ isSharing: true, isViewing: true, label: 'both sharing and viewing' },
|
|
];
|
|
|
|
for (const s of validStates) {
|
|
assert(true, `Valid state: ${s.label}`, '');
|
|
}
|
|
|
|
for (const s of invalidStates) {
|
|
const isInvalid = s.isSharing && s.isViewing;
|
|
assert(isInvalid, `Invalid state detected: ${s.label}`, `sharing=${s.isSharing} viewing=${s.isViewing}`);
|
|
}
|
|
|
|
assert(validStates[0].isActive === false, 'Idle state is not active', '');
|
|
assert(validStates[1].isActive === true, 'Sharing state is active', '');
|
|
assert(validStates[2].isActive === true, 'Viewing state is active', '');
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 6: HotZone Hit Test Logic
|
|
// ============================================================
|
|
function testHotZoneHitTest() {
|
|
console.log('\n=== Test 6: HotZone Hit Test Logic ===');
|
|
|
|
function hitTest(point, zones) {
|
|
for (const zone of zones) {
|
|
const r = zone.rect;
|
|
if (point.dx >= r.left && point.dx <= r.right &&
|
|
point.dy >= r.top && point.dy <= r.bottom) {
|
|
return zone;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const zones = [
|
|
{ id: 'top', label: '顶部栏', rect: { left: 0, top: 0, right: 400, bottom: 50 } },
|
|
{ id: 'content', label: '内容区', rect: { left: 0, top: 50, right: 400, bottom: 700 } },
|
|
{ id: 'bottom', label: '底部栏', rect: { left: 0, top: 700, right: 400, bottom: 800 } },
|
|
];
|
|
|
|
const hit1 = hitTest({ dx: 200, dy: 25 }, zones);
|
|
assert(hit1 && hit1.id === 'top', 'Hit test: top zone', `hit=${hit1?.id}`);
|
|
|
|
const hit2 = hitTest({ dx: 200, dy: 400 }, zones);
|
|
assert(hit2 && hit2.id === 'content', 'Hit test: content zone', `hit=${hit2?.id}`);
|
|
|
|
const hit3 = hitTest({ dx: 200, dy: 750 }, zones);
|
|
assert(hit3 && hit3.id === 'bottom', 'Hit test: bottom zone', `hit=${hit3?.id}`);
|
|
|
|
const hit4 = hitTest({ dx: -10, dy: -10 }, zones);
|
|
assert(hit4 === null, 'Hit test: outside all zones', `hit=${hit4?.id}`);
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 7: CapturedFrame Signaling Payload
|
|
// ============================================================
|
|
function testCapturedFramePayload() {
|
|
console.log('\n=== Test 7: CapturedFrame Signaling Payload ===');
|
|
|
|
const frame = {
|
|
data: Buffer.from('fake_image_data').toString('base64'),
|
|
width: 1080,
|
|
height: 1920,
|
|
timestamp: Date.now(),
|
|
format: 'jpeg',
|
|
sizeInBytes: 1024
|
|
};
|
|
|
|
const payload = {
|
|
d: frame.data,
|
|
w: frame.width,
|
|
h: frame.height,
|
|
t: frame.timestamp,
|
|
f: frame.format,
|
|
};
|
|
|
|
assert(typeof payload.d === 'string', 'Frame data is base64 string', '');
|
|
assert(payload.w === 1080, 'Frame width preserved', '');
|
|
assert(payload.h === 1920, 'Frame height preserved', '');
|
|
assert(payload.f === 'jpeg', 'Frame format preserved', '');
|
|
assert(payload.t > 0, 'Frame timestamp is positive', '');
|
|
|
|
const decoded = Buffer.from(payload.d, 'base64');
|
|
assert(decoded.toString() === 'fake_image_data', 'Frame data round-trip correct', '');
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 8: Canvas Stroke Model
|
|
// ============================================================
|
|
function testCanvasStrokeModel() {
|
|
console.log('\n=== Test 8: Canvas Stroke Model ===');
|
|
|
|
const stroke = {
|
|
id: 'stroke_001',
|
|
points: [{ x: 0, y: 0 }, { x: 100, y: 100 }, { x: 200, y: 50 }],
|
|
color: '#FF0000',
|
|
width: 3,
|
|
tool: 'pen',
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
assert(stroke.points.length === 3, 'Stroke has 3 points', '');
|
|
assert(stroke.tool === 'pen', 'Stroke tool is pen', '');
|
|
assert(/^#[0-9A-Fa-f]{6}$/.test(stroke.color), 'Stroke color is valid hex', '');
|
|
|
|
const eraserStroke = { ...stroke, tool: 'eraser', color: '#00000000' };
|
|
assert(eraserStroke.tool === 'eraser', 'Eraser stroke tool correct', '');
|
|
|
|
const validTools = ['pen', 'eraser', 'line', 'rect', 'circle', 'arrow'];
|
|
assert(validTools.includes(stroke.tool), 'Pen is valid tool', '');
|
|
assert(validTools.includes(eraserStroke.tool), 'Eraser is valid tool', '');
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 9: Pairing Code Validation
|
|
// ============================================================
|
|
function testPairingCodeValidation() {
|
|
console.log('\n=== Test 9: Pairing Code Validation ===');
|
|
|
|
function validatePairingCode(code) {
|
|
if (!code) return { valid: false, error: 'empty' };
|
|
const trimmed = code.trim();
|
|
if (!/^\d{4}$/.test(trimmed)) return { valid: false, error: 'not 4-digit numeric' };
|
|
return { valid: true, error: null };
|
|
}
|
|
|
|
assert(validatePairingCode('1234').valid, '1234 is valid', '');
|
|
assert(validatePairingCode('0000').valid, '0000 is valid', '');
|
|
assert(validatePairingCode('9999').valid, '9999 is valid', '');
|
|
assert(!validatePairingCode('abcd').valid, 'abcd is invalid', '');
|
|
assert(!validatePairingCode('12345').valid, '12345 is invalid', '');
|
|
assert(!validatePairingCode('123').valid, '123 is invalid', '');
|
|
assert(!validatePairingCode('12a4').valid, '12a4 is invalid', '');
|
|
assert(!validatePairingCode('').valid, 'empty is invalid', '');
|
|
assert(validatePairingCode(' 1234 ').valid === true, 'spaced code auto-trimmed and valid', `result=${JSON.stringify(validatePairingCode(' 1234 '))}`);
|
|
assert(validatePairingCode(' 1234 '.trim()).valid, 'trimmed spaced code is valid', '');
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 10: Timer Duration Calculation
|
|
// ============================================================
|
|
function testTimerDuration() {
|
|
console.log('\n=== Test 10: Timer Duration Calculation ===');
|
|
|
|
function formatDuration(seconds) {
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = seconds % 60;
|
|
if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
assert(formatDuration(0) === '00:00', '0 seconds', `got=${formatDuration(0)}`);
|
|
assert(formatDuration(30) === '00:30', '30 seconds', `got=${formatDuration(30)}`);
|
|
assert(formatDuration(90) === '01:30', '90 seconds', `got=${formatDuration(90)}`);
|
|
assert(formatDuration(1800) === '30:00', '1800 seconds (30 min)', `got=${formatDuration(1800)}`);
|
|
assert(formatDuration(3600) === '1:00:00', '3600 seconds (1 hour)', `got=${formatDuration(3600)}`);
|
|
assert(formatDuration(3661) === '1:01:01', '3661 seconds', `got=${formatDuration(3661)}`);
|
|
|
|
const maxDuration = 30 * 60;
|
|
const progress = 1800 / maxDuration;
|
|
assert(progress === 1.0, '30 min is 100% progress', `progress=${progress}`);
|
|
assert((900 / maxDuration) === 0.5, '15 min is 50% progress', '');
|
|
}
|
|
|
|
// ============================================================
|
|
// Main
|
|
// ============================================================
|
|
function main() {
|
|
console.log('\n' + '='.repeat(70));
|
|
console.log(' 闲言APP 本地逻辑测试');
|
|
console.log(` Time: ${new Date().toISOString()}`);
|
|
console.log('='.repeat(70));
|
|
|
|
testPairingCodeGeneration();
|
|
testDeviceNameDisplay();
|
|
testCanvasStyleRenderingOrder();
|
|
testCanvasStyleModelDefaults();
|
|
testScreenShareStateTransitions();
|
|
testHotZoneHitTest();
|
|
testCapturedFramePayload();
|
|
testCanvasStrokeModel();
|
|
testPairingCodeValidation();
|
|
testTimerDuration();
|
|
|
|
console.log('\n' + '='.repeat(70));
|
|
console.log(' Test Results Summary');
|
|
console.log('='.repeat(70));
|
|
|
|
const failedTests = results.filter(r => r.status === 'FAIL');
|
|
for (const r of failedTests) {
|
|
console.log(` ❌ ${r.name} — ${r.detail}`);
|
|
}
|
|
|
|
console.log('-'.repeat(70));
|
|
console.log(` Total: ${passed + failed} | Passed: ${passed} | Failed: ${failed}`);
|
|
console.log('='.repeat(70) + '\n');
|
|
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
main();
|