import test from 'node:test'; import assert from 'node:assert/strict'; import {RoomVoiceAdapter,createRoomVoiceSubscriptions} from '../../YouleNexus/assets/framework/adapters/native/room-voice.ts'; import type {PlatformNativeHost} from '../../YouleNexus/assets/framework/adapters/native/platform-native.ts'; import type {NativeHandler} from '../../YouleNexus/assets/framework/adapters/native/native-contracts.ts'; function setup(host:PlatformNativeHost) { const handlers=new Map();const sent:unknown[]=[];const played:unknown[]=[]; const adapter=new RoomVoiceAdapter({host,subscribe:(name,handler)=>{handlers.set(name,handler);return()=>{handlers.delete(name);};}, sendVoice:(url,time)=>sent.push([url,time]),onPlayback:(user,playing)=>played.push([user,playing])}); return {adapter,handlers,sent,played}; } test('settings recording and history playback retain exact positional arguments',()=>{ const calls:unknown[]=[]; const {adapter,handlers,sent}=setup({kind:'settings',settings:{prepareaudio(...args:unknown[]){calls.push(args);},mediaTypeAudio(...args:unknown[]){calls.push(args);}}}); adapter.startRecording();adapter.stopRecording(); handlers.get('getaudiourl')!('https://audio/real.aac','3'); adapter.playVoice('https://audio/real.aac',1,10003); assert.deepEqual(calls,[[],['https://audio/real.aac',1,10003]]); assert.deepEqual(sent,[['https://audio/real.aac','3']]); }); test('WVJB empty start payload, structured playback and JSON upload are unchanged',()=>{ const calls:unknown[]=[]; const {adapter,handlers,sent,played}=setup({kind:'bridge',call:(name,data)=>{calls.push([name,data]);}}); adapter.startRecording();adapter.stopRecording(); handlers.get('getaudiourl')!(JSON.stringify({audiourl:'actual.aac',time:2})); adapter.playVoice('actual.aac',0,10001); handlers.get('gameui_play_voice')!('10001');handlers.get('gameui_stop_voice')!(10001); assert.deepEqual(calls,[['prepareaudio',''],['mediaTypeAudio',{audiourl:'actual.aac',type:0,user:10001}]]); assert.deepEqual(sent,[['actual.aac',2]]);assert.deepEqual(played,[[10001,true],[10001,false]]); }); test('browser reports unsupported without creating uploads or playback',()=>{ const {adapter,sent,played}=setup({kind:'browser'}); assert.throws(()=>adapter.startRecording(),/不支持/);assert.throws(()=>adapter.playVoice('x',0,1),/不支持/); assert.deepEqual(sent,[]);assert.deepEqual(played,[]); }); test('cancelled, zero duration and duplicate uploads never send; dispose removes callbacks',()=>{ const {adapter,handlers,sent}=setup({kind:'bridge',call(){}}); adapter.startRecording();adapter.cancelRecording(); assert.throws(()=>adapter.startRecording(),/pending/); handlers.get('getaudiourl')!({audiourl:'cancelled',time:3}); adapter.startRecording();handlers.get('getaudiourl')!({audiourl:'',time:0}); adapter.startRecording();handlers.get('getaudiourl')!({audiourl:'once',time:1}); handlers.get('getaudiourl')!({audiourl:'duplicate',time:1}); assert.deepEqual(sent,[['once',1]]); adapter.startRecording();const late=handlers.get('getaudiourl')!;adapter.dispose();late({audiourl:'late',time:3}); assert.equal(handlers.size,0);assert.equal(sent.length,1); }); test('invalid upload source and missing native method are exposed',()=>{ const {adapter,handlers}=setup({kind:'bridge',call(){}}); adapter.startRecording();assert.throws(()=>handlers.get('getaudiourl')!({audiourl:'x'}),/time/); assert.throws(()=>setup({kind:'settings',settings:{}}).adapter.startRecording(),/prepareaudio/); }); test('settings subscriptions own globals and restore previous callbacks on dispose',()=>{ const original=()=>{};const globals:Record={getaudiourl:original};let count=0; const subscribe=createRoomVoiceSubscriptions({kind:'settings',settings:{}},globals); const off=subscribe('getaudiourl',()=>{count++;});const callback=globals.getaudiourl as NativeHandler; callback('actual',1);off();callback('late',1); assert.equal(count,1);assert.equal(globals.getaudiourl,original); const offNew=subscribe('gameui_play_voice',()=>{});offNew();assert.equal(Object.prototype.hasOwnProperty.call(globals,'gameui_play_voice'),false); }); test('bridge subscriptions use legacy names, neutralize retained callbacks and preserve newer owners',()=>{ const handlers=new Map();let count=0; const subscribe=createRoomVoiceSubscriptions({kind:'bridge',call(){}},{},{register:(name,handler)=>{handlers.set(name,handler);}}); const off=subscribe('gameui_play_voice',()=>{count++;});const retained=handlers.get('gameui_play_voice')!; retained(1);off();retained(1);assert.equal(count,1); const globals:Record={};const settingsSubscribe=createRoomVoiceSubscriptions({kind:'settings',settings:{}},globals); const release=settingsSubscribe('getaudiourl',()=>{});const newer=()=>{};globals.getaudiourl=newer;release();assert.equal(globals.getaudiourl,newer); });