主要变更: 1. 新增多风格音效资源与管理文档 2. 修复翻译服务空响应处理与Dio日志异常捕获 3. 完善Web端平台适配与路径获取Stub 4. 优化设备配对与文件传输功能 5. 新增角色命名常量与摇一摇检测器 6. 修复Riverpod dispose与鸿蒙导航路由 7. 新增每日通知服务与流体着色器 8. 优化备份服务与数据管理页面 9. 新增隐私设置附近设备发现选项 10. 重构诗词提供者支持历史记录 11. 完善桌面端构建配置与开发脚本 12. 清理旧版工具部署脚本
699 lines
24 KiB
JavaScript
699 lines
24 KiB
JavaScript
// ============================================================
|
|
// 闲言APP — 信令服务器综合测试脚本
|
|
// 创建时间: 2026-05-20
|
|
// 更新时间: 2026-05-20
|
|
// 作用: 测试信令服务器的文件传输/屏幕共享/画布/配对码/设备发现等功能
|
|
// 上次更新: 初始创建
|
|
// ============================================================
|
|
|
|
const WebSocket = require('ws');
|
|
|
|
const SERVER_URL = 'wss://tools.wktyl.com:9443';
|
|
const HEALTH_URL = 'https://tools.wktyl.com:9443/health';
|
|
const TEST_TIMEOUT = 30000;
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const results = [];
|
|
|
|
function log(tag, msg) {
|
|
console.log(`[${new Date().toISOString().slice(11, 19)}] [${tag}] ${msg}`);
|
|
}
|
|
|
|
function assert(condition, testName, detail) {
|
|
if (condition) {
|
|
passed++;
|
|
results.push({ name: testName, status: 'PASS', detail: '' });
|
|
log('PASS', testName);
|
|
} else {
|
|
failed++;
|
|
results.push({ name: testName, status: 'FAIL', detail: detail || 'Assertion failed' });
|
|
log('FAIL', `${testName} — ${detail || 'Assertion failed'}`);
|
|
}
|
|
}
|
|
|
|
function createClient(id, extraPayload) {
|
|
return new Promise((resolve, reject) => {
|
|
const ws = new WebSocket(SERVER_URL);
|
|
const messages = [];
|
|
let registered = false;
|
|
let peerId = '';
|
|
|
|
const timeout = setTimeout(() => {
|
|
reject(new Error(`Client ${id} connection timeout`));
|
|
}, TEST_TIMEOUT);
|
|
|
|
ws.on('open', () => {
|
|
log(id, 'WebSocket connected');
|
|
const registerMsg = {
|
|
type: 'register',
|
|
payload: {
|
|
userId: `test_user_${id}`,
|
|
fingerprint: `fp_${id}_${Date.now()}`,
|
|
alias: `测试设备${id}`,
|
|
deviceModel: `TestModel-${id}`,
|
|
deviceType: 'mobile',
|
|
platform: 'test',
|
|
...extraPayload
|
|
}
|
|
};
|
|
ws.send(JSON.stringify(registerMsg));
|
|
});
|
|
|
|
ws.on('message', (data) => {
|
|
const msg = JSON.parse(data.toString());
|
|
messages.push(msg);
|
|
if (!registered && msg.type === 'registered') {
|
|
registered = true;
|
|
peerId = msg.id;
|
|
log(id, `Registered, peerId=${peerId}`);
|
|
clearTimeout(timeout);
|
|
resolve({ ws, messages, id, peerId, registered });
|
|
} else if (registered) {
|
|
log(id, `Received: ${msg.type}`);
|
|
}
|
|
});
|
|
|
|
ws.on('error', (err) => {
|
|
clearTimeout(timeout);
|
|
log(id, `Error: ${err.message}`);
|
|
});
|
|
|
|
ws.on('close', () => {
|
|
log(id, 'Disconnected');
|
|
});
|
|
});
|
|
}
|
|
|
|
function waitForMessage(client, type, timeoutMs = 8000) {
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
client.ws.removeListener('message', handler);
|
|
reject(new Error(`Timeout waiting for message type: ${type}`));
|
|
}, timeoutMs);
|
|
|
|
const handler = (data) => {
|
|
try {
|
|
const msg = JSON.parse(data.toString());
|
|
if (msg.type === type) {
|
|
clearTimeout(timeout);
|
|
client.ws.removeListener('message', handler);
|
|
resolve(msg);
|
|
}
|
|
} catch (e) {}
|
|
};
|
|
client.ws.on('message', handler);
|
|
});
|
|
}
|
|
|
|
function sendAndWait(client, msg, responseType, timeoutMs = 8000) {
|
|
const promise = waitForMessage(client, responseType, timeoutMs);
|
|
send(client, msg);
|
|
return promise;
|
|
}
|
|
|
|
function sendAndWaitBoth(clientA, msgA, typeA, clientB, msgB, typeB, timeoutMs = 8000) {
|
|
const promiseA = waitForMessage(clientA, typeA, timeoutMs);
|
|
const promiseB = waitForMessage(clientB, typeB, timeoutMs);
|
|
if (msgA) send(clientA, msgA);
|
|
if (msgB) send(clientB, msgB);
|
|
return Promise.all([promiseA, promiseB]);
|
|
}
|
|
|
|
function send(client, msg) {
|
|
client.ws.send(JSON.stringify(msg));
|
|
log(client.id, `Sent: ${msg.type}`);
|
|
}
|
|
|
|
function closeClient(client) {
|
|
if (client.ws.readyState === WebSocket.OPEN) {
|
|
client.ws.close();
|
|
}
|
|
}
|
|
|
|
async function sleep(ms) {
|
|
return new Promise(r => setTimeout(r, ms));
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 1: Health Check
|
|
// ============================================================
|
|
async function testHealthCheck() {
|
|
log('TEST', '=== Test 1: Health Check ===');
|
|
try {
|
|
const http = require('https');
|
|
const url = new URL('https://tools.wktyl.com/api/file_transfer/health');
|
|
const health = await new Promise((resolve, reject) => {
|
|
http.get(url, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
|
|
});
|
|
}).on('error', reject);
|
|
});
|
|
assert(health.code === 1 || health.status === 'healthy' || health.data, 'Health check: server healthy', `code=${health.code}`);
|
|
log('TEST', `Health response: ${JSON.stringify(health).slice(0, 200)}`);
|
|
} catch (e) {
|
|
assert(false, 'Health check: server reachable', e.message);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 2: Device Registration & Discovery
|
|
// ============================================================
|
|
async function testRegistrationAndDiscovery() {
|
|
log('TEST', '=== Test 2: Registration & Discovery ===');
|
|
let clientA, clientB;
|
|
try {
|
|
clientA = await createClient('A');
|
|
clientB = await createClient('B');
|
|
|
|
assert(clientA.registered, 'Client A registered', '');
|
|
assert(clientB.registered, 'Client B registered', '');
|
|
assert(clientA.peerId !== clientB.peerId, 'Different peer IDs', `A=${clientA.peerId} B=${clientB.peerId}`);
|
|
|
|
const discoverResp = await sendAndWait(clientA, { type: 'discover' }, 'discover_response');
|
|
const devices = discoverResp.devices || [];
|
|
const foundB = devices.find(d => d.id === clientB.peerId);
|
|
assert(foundB, 'Discovery: Client A finds Client B', `devices count=${devices.length}`);
|
|
if (foundB) {
|
|
const alias = foundB.alias || foundB.name?.displayName || '';
|
|
assert(alias.includes('测试设备B'), 'Discovery: alias correct', `alias=${alias}`);
|
|
assert(foundB.deviceModel === 'TestModel-B', 'Discovery: deviceModel correct', `deviceModel=${foundB.deviceModel}`);
|
|
}
|
|
|
|
const myDevicesResp = await sendAndWait(clientA, {
|
|
type: 'discoverMyDevices',
|
|
payload: { userId: 'test_user_A' }
|
|
}, 'myDevicesResponse');
|
|
assert(Array.isArray(myDevicesResp.devices), 'My devices: response is array', '');
|
|
} catch (e) {
|
|
assert(false, 'Registration & Discovery', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
closeClient(clientB);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 3: Pairing Code (4-digit numeric)
|
|
// ============================================================
|
|
async function testPairingCode() {
|
|
log('TEST', '=== Test 3: Pairing Code (4-digit numeric) ===');
|
|
let clientA, clientB;
|
|
try {
|
|
clientA = await createClient('A-pair');
|
|
clientB = await createClient('B-pair');
|
|
|
|
const codeCreated = await sendAndWait(clientA, {
|
|
type: 'pairing-code-create',
|
|
payload: { alias: '设备A', deviceModel: 'Model-A', deviceType: 'mobile' }
|
|
}, 'pairing-code-created');
|
|
const code = codeCreated.pairingCode;
|
|
log('TEST', `Pairing code created: ${code}`);
|
|
|
|
assert(!!code, 'Pairing code: code exists', '');
|
|
assert(/^\d{4}$/.test(code), 'Pairing code: 4-digit numeric only', `code=${code}`);
|
|
assert(codeCreated.expiresAt > Date.now(), 'Pairing code: not expired', `expiresAt=${codeCreated.expiresAt}`);
|
|
|
|
const [matchedA, matchedB] = await sendAndWaitBoth(
|
|
clientA, null, 'pairing-matched',
|
|
clientB, {
|
|
type: 'pairing-code-join',
|
|
payload: { pairingCode: code }
|
|
}, 'pairing-matched'
|
|
);
|
|
|
|
assert(matchedA.success === true, 'Pairing: A matched successfully', `success=${matchedA.success}`);
|
|
assert(matchedB.success === true, 'Pairing: B matched successfully', `success=${matchedB.success}`);
|
|
assert(matchedB.peer.alias === '设备A', 'Pairing: B sees A alias', `alias=${matchedB.peer.alias}`);
|
|
assert(matchedB.peer.deviceModel === 'Model-A', 'Pairing: B sees A deviceModel', `deviceModel=${matchedB.peer.deviceModel}`);
|
|
|
|
// Test invalid code
|
|
const clientC = await createClient('C-pair');
|
|
const invalidMatch = await sendAndWait(clientC, {
|
|
type: 'pairing-code-join',
|
|
payload: { pairingCode: 'abcd' }
|
|
}, 'pairing-matched');
|
|
assert(invalidMatch.success === false, 'Pairing: non-numeric code rejected', `success=${invalidMatch.success}, error=${invalidMatch.error}`);
|
|
|
|
const invalidLen = await sendAndWait(clientC, {
|
|
type: 'pairing-code-join',
|
|
payload: { pairingCode: '12345' }
|
|
}, 'pairing-matched');
|
|
assert(invalidLen.success === false, 'Pairing: 5-digit code rejected', `success=${invalidLen.success}, error=${invalidLen.error}`);
|
|
|
|
closeClient(clientC);
|
|
} catch (e) {
|
|
assert(false, 'Pairing Code', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
closeClient(clientB);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 4: Text Message & File Meta
|
|
// ============================================================
|
|
async function testTextMessageAndFileMeta() {
|
|
log('TEST', '=== Test 4: Text Message & File Meta ===');
|
|
let clientA, clientB;
|
|
try {
|
|
clientA = await createClient('A-msg');
|
|
clientB = await createClient('B-msg');
|
|
await sleep(500);
|
|
|
|
const textMsgPromise = waitForMessage(clientB, 'textMessage');
|
|
send(clientA, {
|
|
type: 'textMessage',
|
|
to: clientB.peerId,
|
|
message: { text: 'Hello from A!', type: 'text' },
|
|
timestamp: Date.now()
|
|
});
|
|
const textMsg = await textMsgPromise;
|
|
assert(textMsg.message.text === 'Hello from A!', 'Text message: content correct', `text=${textMsg.message?.text}`);
|
|
assert(textMsg.from === clientA.peerId || textMsg.fromPeerId === clientA.peerId, 'Text message: from correct', `from=${textMsg.from}`);
|
|
|
|
const fileMeta = {
|
|
name: 'test_image.png',
|
|
size: 102400,
|
|
mimeType: 'image/png',
|
|
hash: 'abc123def456'
|
|
};
|
|
const fileMsgPromise = waitForMessage(clientB, 'fileMeta');
|
|
send(clientA, {
|
|
type: 'fileMeta',
|
|
to: clientB.peerId,
|
|
file: fileMeta,
|
|
timestamp: Date.now()
|
|
});
|
|
const fileMsg = await fileMsgPromise;
|
|
assert(fileMsg.file.name === 'test_image.png', 'File meta: name correct', `name=${fileMsg.file?.name}`);
|
|
assert(fileMsg.file.size === 102400, 'File meta: size correct', `size=${fileMsg.file?.size}`);
|
|
assert(fileMsg.file.mimeType === 'image/png', 'File meta: mimeType correct', `mimeType=${fileMsg.file?.mimeType}`);
|
|
} catch (e) {
|
|
assert(false, 'Text Message & File Meta', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
closeClient(clientB);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 5: Screen Share
|
|
// ============================================================
|
|
async function testScreenShare() {
|
|
log('TEST', '=== Test 5: Screen Share ===');
|
|
let clientA, clientB;
|
|
try {
|
|
clientA = await createClient('A-ss');
|
|
clientB = await createClient('B-ss');
|
|
await sleep(500);
|
|
|
|
const ssRequestPromise = waitForMessage(clientB, 'screen-share-request');
|
|
send(clientA, {
|
|
type: 'screen-share-request',
|
|
to: clientB.peerId,
|
|
payload: { direction: 'share' }
|
|
});
|
|
const ssRequest = await ssRequestPromise;
|
|
assert(ssRequest.direction === 'share', 'Screen share: request direction correct', `direction=${ssRequest.direction}`);
|
|
assert(ssRequest.fromPeerId === clientA.peerId || ssRequest.from === clientA.peerId, 'Screen share: from correct', '');
|
|
|
|
const ssAcceptPromise = waitForMessage(clientA, 'screen-share-accept');
|
|
send(clientB, {
|
|
type: 'screen-share-accept',
|
|
to: clientA.peerId,
|
|
payload: { direction: 'share' }
|
|
});
|
|
const ssAccept = await ssAcceptPromise;
|
|
assert(!!ssAccept, 'Screen share: accept received', '');
|
|
|
|
const offerMsgPromise = waitForMessage(clientB, 'screen-share-offer');
|
|
send(clientA, {
|
|
type: 'screen-share-offer',
|
|
to: clientB.peerId,
|
|
payload: { sdp: 'fake_sdp_offer', hotZones: [{ id: 'zone1', label: 'top' }] }
|
|
});
|
|
const offerMsg = await offerMsgPromise;
|
|
assert(offerMsg.payload?.sdp === 'fake_sdp_offer', 'Screen share: SDP offer relayed', `sdp=${offerMsg.payload?.sdp}`);
|
|
|
|
const answerMsgPromise = waitForMessage(clientA, 'screen-share-answer');
|
|
send(clientB, {
|
|
type: 'screen-share-answer',
|
|
to: clientA.peerId,
|
|
payload: { sdp: 'fake_sdp_answer' }
|
|
});
|
|
const answerMsg = await answerMsgPromise;
|
|
assert(answerMsg.payload?.sdp === 'fake_sdp_answer', 'Screen share: SDP answer relayed', '');
|
|
|
|
const iceMsgPromise = waitForMessage(clientB, 'screen-share-ice-candidate');
|
|
send(clientA, {
|
|
type: 'screen-share-ice-candidate',
|
|
to: clientB.peerId,
|
|
payload: { candidate: 'fake_ice_candidate_a' }
|
|
});
|
|
const iceMsg = await iceMsgPromise;
|
|
assert(iceMsg.payload?.candidate === 'fake_ice_candidate_a', 'Screen share: ICE candidate relayed', '');
|
|
|
|
const stopMsgPromise = waitForMessage(clientB, 'screen-share-stop');
|
|
send(clientA, {
|
|
type: 'screen-share-stop',
|
|
to: clientB.peerId
|
|
});
|
|
const stopMsg = await stopMsgPromise;
|
|
assert(!!stopMsg, 'Screen share: stop received', '');
|
|
|
|
const rejectReqPromise = waitForMessage(clientB, 'screen-share-request');
|
|
const rejectRespPromise = waitForMessage(clientA, 'screen-share-reject');
|
|
send(clientA, {
|
|
type: 'screen-share-request',
|
|
to: clientB.peerId,
|
|
payload: { direction: 'view' }
|
|
});
|
|
await rejectReqPromise;
|
|
send(clientB, {
|
|
type: 'screen-share-reject',
|
|
to: clientA.peerId
|
|
});
|
|
const rejectMsg = await rejectRespPromise;
|
|
assert(!!rejectMsg, 'Screen share: reject received', '');
|
|
} catch (e) {
|
|
assert(false, 'Screen Share', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
closeClient(clientB);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 6: Canvas Collaboration
|
|
// ============================================================
|
|
async function testCanvas() {
|
|
log('TEST', '=== Test 6: Canvas Collaboration ===');
|
|
let clientA, clientB;
|
|
try {
|
|
clientA = await createClient('A-canvas');
|
|
clientB = await createClient('B-canvas');
|
|
await sleep(500);
|
|
|
|
const canvasId = `test_canvas_${Date.now()}`;
|
|
|
|
const joinA = await sendAndWait(clientA, {
|
|
type: 'canvas-join',
|
|
payload: { canvasId }
|
|
}, 'canvas-join');
|
|
assert(joinA.payload.canvasId === canvasId, 'Canvas: A joined', `canvasId=${joinA.payload.canvasId}`);
|
|
assert(joinA.payload.members.includes(clientA.peerId), 'Canvas: A in members list', '');
|
|
|
|
const [joinB1, joinB2] = await sendAndWaitBoth(
|
|
clientB, { type: 'canvas-join', payload: { canvasId } }, 'canvas-join',
|
|
clientA, null, 'canvas-join'
|
|
);
|
|
|
|
assert(joinB1.payload.canvasId === canvasId, 'Canvas: B joined', '');
|
|
assert(joinB2.payload.members.length === 2, 'Canvas: A notified of B joining', `members=${joinB2.payload.members.length}`);
|
|
|
|
const strokeData = {
|
|
canvasId,
|
|
strokeId: 'stroke_001',
|
|
points: [{ x: 10, y: 20 }, { x: 30, y: 40 }],
|
|
color: '#FF0000',
|
|
width: 3,
|
|
tool: 'pen'
|
|
};
|
|
const strokeMsgPromise = waitForMessage(clientB, 'canvas-stroke');
|
|
send(clientA, {
|
|
type: 'canvas-stroke',
|
|
payload: strokeData
|
|
});
|
|
const strokeMsg = await strokeMsgPromise;
|
|
assert(strokeMsg.payload.strokeId === 'stroke_001', 'Canvas: stroke relayed', `strokeId=${strokeMsg.payload.strokeId}`);
|
|
assert(strokeMsg.payload.color === '#FF0000', 'Canvas: stroke color correct', `color=${strokeMsg.payload.color}`);
|
|
|
|
const cursorMsgPromise = waitForMessage(clientB, 'canvas-cursor');
|
|
send(clientA, {
|
|
type: 'canvas-cursor',
|
|
payload: { canvasId, x: 50, y: 60 }
|
|
});
|
|
const cursorMsg = await cursorMsgPromise;
|
|
assert(cursorMsg.payload.x === 50, 'Canvas: cursor relayed', `x=${cursorMsg.payload.x}`);
|
|
|
|
const snapshotReqPromise = waitForMessage(clientB, 'canvas-snapshot');
|
|
send(clientA, {
|
|
type: 'canvas-snapshot',
|
|
payload: { canvasId, action: 'request' }
|
|
});
|
|
const snapshotReq = await snapshotReqPromise;
|
|
assert(snapshotReq.payload.action === 'request', 'Canvas: snapshot request relayed', '');
|
|
|
|
const snapshotRespPromise = waitForMessage(clientA, 'canvas-snapshot');
|
|
send(clientB, {
|
|
type: 'canvas-snapshot',
|
|
to: clientA.peerId,
|
|
payload: { canvasId, action: 'response', data: 'base64_fake_snapshot_data' }
|
|
});
|
|
const snapshotResp = await snapshotRespPromise;
|
|
assert(snapshotResp.payload.data === 'base64_fake_snapshot_data', 'Canvas: snapshot response relayed', '');
|
|
|
|
const leaveMsgPromise = waitForMessage(clientA, 'canvas-leave');
|
|
send(clientB, {
|
|
type: 'canvas-leave',
|
|
payload: { canvasId }
|
|
});
|
|
const leaveMsg = await leaveMsgPromise;
|
|
assert(leaveMsg.payload.canvasId === canvasId, 'Canvas: B leave notified to A', '');
|
|
} catch (e) {
|
|
assert(false, 'Canvas Collaboration', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
closeClient(clientB);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 7: Pair Request (direct pairing)
|
|
// ============================================================
|
|
async function testDirectPairing() {
|
|
log('TEST', '=== Test 7: Direct Pairing ===');
|
|
let clientA, clientB;
|
|
try {
|
|
clientA = await createClient('A-dpair');
|
|
clientB = await createClient('B-dpair');
|
|
await sleep(500);
|
|
|
|
const pairReqPromise = waitForMessage(clientB, 'pair-request');
|
|
send(clientA, {
|
|
type: 'pair-request',
|
|
to: clientB.peerId,
|
|
fingerprint: `fp_A-dpair_${Date.now()}`,
|
|
payload: { fingerprint: `fp_A-dpair_${Date.now()}` }
|
|
});
|
|
const pairReq = await pairReqPromise;
|
|
assert(!!pairReq.requestId, 'Direct pair: request has requestId', `requestId=${pairReq.requestId}`);
|
|
|
|
const [pairRespA, pairRespB] = await sendAndWaitBoth(
|
|
clientA, null, 'pair-response',
|
|
clientB, { type: 'pair-accept', requestId: pairReq.requestId }, 'pair-response'
|
|
);
|
|
|
|
assert(pairRespA.success === true, 'Direct pair: A success', `success=${pairRespA.success}`);
|
|
assert(pairRespB.success === true, 'Direct pair: B success', `success=${pairRespB.success}`);
|
|
} catch (e) {
|
|
assert(false, 'Direct Pairing', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
closeClient(clientB);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 8: Radar Broadcast & Scan
|
|
// ============================================================
|
|
async function testRadar() {
|
|
log('TEST', '=== Test 8: Radar Broadcast & Scan ===');
|
|
let clientA, clientB;
|
|
try {
|
|
clientA = await createClient('A-radar');
|
|
clientB = await createClient('B-radar');
|
|
await sleep(500);
|
|
|
|
const broadcastResp = await sendAndWait(clientA, {
|
|
type: 'radar-broadcast',
|
|
payload: {
|
|
alias: '雷达设备A',
|
|
deviceModel: 'RadarModel-A',
|
|
deviceType: 'mobile',
|
|
ipRange: '192.168.1',
|
|
ipCity: '深圳'
|
|
}
|
|
}, 'radar-broadcast');
|
|
assert(broadcastResp.success === true, 'Radar: A broadcast success', '');
|
|
|
|
await sendAndWait(clientB, {
|
|
type: 'radar-broadcast',
|
|
payload: {
|
|
alias: '雷达设备B',
|
|
deviceModel: 'RadarModel-B',
|
|
deviceType: 'desktop',
|
|
ipRange: '192.168.1',
|
|
ipCity: '深圳'
|
|
}
|
|
}, 'radar-broadcast');
|
|
|
|
const scanResp = await sendAndWait(clientA, {
|
|
type: 'radar-scan',
|
|
payload: { ipRange: '192.168.1', ipCity: '深圳' }
|
|
}, 'radar-devices');
|
|
assert(Array.isArray(scanResp.devices), 'Radar: scan returns array', '');
|
|
const foundB = scanResp.devices.find(d => d.alias === '雷达设备B');
|
|
assert(!!foundB, 'Radar: A finds B', `devices count=${scanResp.devices.length}`);
|
|
if (foundB) {
|
|
assert(foundB.deviceModel === 'RadarModel-B', 'Radar: B deviceModel correct', `deviceModel=${foundB.deviceModel}`);
|
|
assert(foundB.matchType === 'same-network' || foundB.matchType === 'my-device', 'Radar: matchType correct', `matchType=${foundB.matchType}`);
|
|
}
|
|
} catch (e) {
|
|
assert(false, 'Radar Broadcast & Scan', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
closeClient(clientB);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 9: WsRelay
|
|
// ============================================================
|
|
async function testWsRelay() {
|
|
log('TEST', '=== Test 9: WebSocket Relay ===');
|
|
let clientA, clientB;
|
|
try {
|
|
clientA = await createClient('A-relay');
|
|
clientB = await createClient('B-relay');
|
|
await sleep(500);
|
|
|
|
const relayMsgPromise = waitForMessage(clientB, 'wsRelay');
|
|
send(clientA, {
|
|
type: 'wsRelay',
|
|
to: clientB.peerId,
|
|
relayType: 'webrtc-signal',
|
|
payload: {
|
|
relayType: 'webrtc-signal',
|
|
signalType: 'offer',
|
|
sdp: 'fake_relay_sdp'
|
|
}
|
|
});
|
|
const relayMsg = await relayMsgPromise;
|
|
assert(relayMsg.payload.relayType === 'webrtc-signal', 'WsRelay: relayType correct', `relayType=${relayMsg.payload?.relayType}`);
|
|
assert(relayMsg.payload.signalType === 'offer', 'WsRelay: signalType correct', `signalType=${relayMsg.payload?.signalType}`);
|
|
|
|
const errorRelay = await sendAndWait(clientA, {
|
|
type: 'wsRelay',
|
|
to: 'non_existent_peer_id',
|
|
relayType: 'data',
|
|
payload: { relayType: 'data', test: true }
|
|
}, 'wsRelay');
|
|
assert(errorRelay.payload?.relayType === 'error', 'WsRelay: error for non-existent target', `relayType=${errorRelay.payload?.relayType}`);
|
|
} catch (e) {
|
|
assert(false, 'WebSocket Relay', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
closeClient(clientB);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 10: Heartbeat & Ping
|
|
// ============================================================
|
|
async function testHeartbeat() {
|
|
log('TEST', '=== Test 10: Heartbeat & Ping ===');
|
|
let clientA;
|
|
try {
|
|
clientA = await createClient('A-hb');
|
|
|
|
const pong = await sendAndWait(clientA, { type: 'ping' }, 'pong');
|
|
assert(!!pong.timestamp, 'Ping: pong has timestamp', `timestamp=${pong.timestamp}`);
|
|
|
|
const hbAck = await sendAndWait(clientA, { type: 'heartbeat' }, 'heartbeat_ack');
|
|
assert(!!hbAck.timestamp, 'Heartbeat: ack has timestamp', `timestamp=${hbAck.timestamp}`);
|
|
} catch (e) {
|
|
assert(false, 'Heartbeat & Ping', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 11: DeviceModel not localhost
|
|
// ============================================================
|
|
async function testDeviceModelNotLocalhost() {
|
|
log('TEST', '=== Test 11: DeviceModel not localhost ===');
|
|
let clientA;
|
|
try {
|
|
clientA = await createClient('A-model', { deviceModel: 'Samsung Galaxy S24' });
|
|
|
|
send(clientA, { type: 'discover' });
|
|
// We check the registered data via discover response from another client
|
|
const clientB = await createClient('B-model');
|
|
await sleep(500);
|
|
|
|
const discoverResp = await sendAndWait(clientB, { type: 'discover' }, 'discover_response');
|
|
const foundA = discoverResp.devices.find(d => d.id === clientA.peerId);
|
|
assert(!!foundA, 'DeviceModel: A found in discovery', '');
|
|
if (foundA) {
|
|
assert(foundA.deviceModel === 'Samsung Galaxy S24', 'DeviceModel: custom model preserved', `deviceModel=${foundA.deviceModel}`);
|
|
assert(foundA.deviceModel !== 'localhost', 'DeviceModel: not localhost', `deviceModel=${foundA.deviceModel}`);
|
|
}
|
|
|
|
closeClient(clientB);
|
|
} catch (e) {
|
|
assert(false, 'DeviceModel not localhost', e.message);
|
|
} finally {
|
|
closeClient(clientA);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Main
|
|
// ============================================================
|
|
async function main() {
|
|
console.log('\n' + '='.repeat(70));
|
|
console.log(' 闲言APP 信令服务器综合测试');
|
|
console.log(` Server: ${SERVER_URL}`);
|
|
console.log(` Time: ${new Date().toISOString()}`);
|
|
console.log('='.repeat(70) + '\n');
|
|
|
|
await testHealthCheck();
|
|
await testRegistrationAndDiscovery();
|
|
await testPairingCode();
|
|
await testTextMessageAndFileMeta();
|
|
await testScreenShare();
|
|
await testCanvas();
|
|
await testDirectPairing();
|
|
await testRadar();
|
|
await testWsRelay();
|
|
await testHeartbeat();
|
|
await testDeviceModelNotLocalhost();
|
|
|
|
console.log('\n' + '='.repeat(70));
|
|
console.log(' Test Results Summary');
|
|
console.log('='.repeat(70));
|
|
|
|
for (const r of results) {
|
|
const icon = r.status === 'PASS' ? '✅' : '❌';
|
|
const detail = r.detail ? ` (${r.detail})` : '';
|
|
console.log(` ${icon} ${r.name}${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().catch(err => {
|
|
console.error('Test runner error:', err);
|
|
process.exit(2);
|
|
});
|