Files
xianyan/scripts/test_local_logic.js
Developer f9c19463f9 chore: 批量更新v6.5.21版本,整合多项功能修复与优化
主要变更:
1. 新增多风格音效资源与管理文档
2. 修复翻译服务空响应处理与Dio日志异常捕获
3. 完善Web端平台适配与路径获取Stub
4. 优化设备配对与文件传输功能
5. 新增角色命名常量与摇一摇检测器
6. 修复Riverpod dispose与鸿蒙导航路由
7. 新增每日通知服务与流体着色器
8. 优化备份服务与数据管理页面
9. 新增隐私设置附近设备发现选项
10. 重构诗词提供者支持历史记录
11. 完善桌面端构建配置与开发脚本
12. 清理旧版工具部署脚本
2026-05-21 00:19:14 +08:00

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();