feat: integrate platform room entry, UI migration and erqiwang documentation
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {readInviteClipboard,readClipboardText} from '../../YouleNexus/assets/framework/adapters/native/invite-clipboard.ts';
|
||||
test('invite paste keeps native signatures and strips non-digits without truncating',async()=>{
|
||||
const calls:unknown[]=[];
|
||||
assert.equal(await readInviteClipboard({kind:'settings',read:(...args:unknown[])=>{calls.push(args);return '码00 1234567';}}),'001234567');
|
||||
assert.deepEqual(calls,[[]]);
|
||||
assert.equal(await readInviteClipboard({kind:'bridge',call:(name,data,cb)=>{assert.equal(name,'gamepastetext');assert.equal(data,'');cb('00-12');}}),'0012');
|
||||
assert.equal(await readInviteClipboard({kind:'browser',read:async()=>'A123'}),'123');
|
||||
await assert.rejects(readInviteClipboard({kind:'settings',read:()=>null}),/clipboard/);
|
||||
});
|
||||
|
||||
test('redemption paste preserves letters, punctuation, whitespace and leading zeroes',async()=>{
|
||||
for(const text of ['00Ab-12',' aB ','']){
|
||||
assert.equal(await readClipboardText({kind:'settings',read:()=>text}),text);
|
||||
assert.equal(await readClipboardText({kind:'bridge',call:(name,data,cb)=>{assert.equal(name,'gamepastetext');assert.equal(data,'');cb(text);}}),text);
|
||||
assert.equal(await readClipboardText({kind:'browser',read:async()=>text}),text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {OwnedRoomHistory} from '../../YouleNexus/assets/framework/adapters/native/owned-room-history.ts';
|
||||
test('owned rooms use legacy cache, order, and server outcome lifecycle',()=>{
|
||||
const data=new Map<string,string>();const history=new OwnedRoomHistory({getItem:k=>data.get(k)??null,setItem:(k,v)=>{data.set(k,v);}},'g','a');
|
||||
assert.deepEqual(history.read(),[]);assert.equal(data.get('tsgame_roomlist_ga'),'{{}}'.replace('{{}}','{"data":[]}'));
|
||||
history.exited({isowner:1,roomcode:'123456'},0);history.exited({isowner:1,roomcode:'654321'},0);
|
||||
history.exited({isowner:1,roomcode:'123456'},0);assert.deepEqual(history.read(),[654321,123456]);
|
||||
history.exited({isowner:0,roomcode:333333},0);history.exited({isowner:1,roomcode:333333},1);assert.equal(history.read().length,2);
|
||||
history.joined({state:2,roomcode:123456});assert.equal(history.read().length,2);
|
||||
history.joined({state:99,roomcode:123456});assert.deepEqual(history.read(),[654321]);
|
||||
history.joined({state:0,isowner:1,roomcode:654321});assert.deepEqual(history.read(),[]);
|
||||
data.set('tsgame_roomlist_ga','bad');assert.throws(()=>history.read());
|
||||
});
|
||||
@@ -1,223 +1,283 @@
|
||||
import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||
// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。
|
||||
const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const});
|
||||
import { remoteFixture } from '../fixtures/remote-config.ts';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
|
||||
import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts';
|
||||
import { PlatformCommands } from '../../YouleNexus/assets/framework/platform/commands.ts';
|
||||
import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts';
|
||||
import {
|
||||
parseLoginResponse,
|
||||
} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts';
|
||||
|
||||
const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({
|
||||
startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release',
|
||||
isDebugger: false,
|
||||
identity: Object.freeze({
|
||||
agentid: 'agent-from-config',
|
||||
channelid: 'channel-from-config',
|
||||
gameid: 'game-from-config',
|
||||
marketid: 'market-from-config',
|
||||
version: 41,
|
||||
}),
|
||||
servers: Object.freeze(['ws://fixture']),
|
||||
source: 'remote',
|
||||
gameserver: 'https://fixture/config',
|
||||
...remoteFixture(),
|
||||
});
|
||||
|
||||
function fixture(name: string): { data: Record<string, unknown> } {
|
||||
return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8'));
|
||||
}
|
||||
|
||||
function makeStore(location: 'outside' | 'inside', stage = 0, infinite = 0): PlatformStore {
|
||||
const store = new PlatformStore();
|
||||
const source = fixture(location === 'inside' ? 'player-login-room.json' : 'player-login-success.json');
|
||||
if (location === 'inside') {
|
||||
source.data.isbattle = stage;
|
||||
source.data.infinite = infinite;
|
||||
}
|
||||
store.applyLoginSuccess(parseLoginResponse(source.data));
|
||||
return store;
|
||||
}
|
||||
|
||||
import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||
// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。
|
||||
const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const});
|
||||
import { remoteFixture } from '../fixtures/remote-config.ts';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
|
||||
import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts';
|
||||
import { PlatformCommands } from '../../YouleNexus/assets/framework/platform/commands.ts';
|
||||
import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts';
|
||||
import {
|
||||
parseLoginResponse,
|
||||
} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts';
|
||||
|
||||
const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({
|
||||
startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release',
|
||||
isDebugger: false,
|
||||
identity: Object.freeze({
|
||||
agentid: 'agent-from-config',
|
||||
channelid: 'channel-from-config',
|
||||
gameid: 'game-from-config',
|
||||
marketid: 'market-from-config',
|
||||
version: 41,
|
||||
}),
|
||||
servers: Object.freeze(['ws://fixture']),
|
||||
source: 'remote',
|
||||
gameserver: 'https://fixture/config',
|
||||
...remoteFixture(),
|
||||
});
|
||||
|
||||
function fixture(name: string): { data: Record<string, unknown> } {
|
||||
return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8'));
|
||||
}
|
||||
|
||||
function makeStore(location: 'outside' | 'inside', stage = 0, infinite = 0): PlatformStore {
|
||||
const store = new PlatformStore();
|
||||
const source = fixture(location === 'inside' ? 'player-login-room.json' : 'player-login-success.json');
|
||||
if (location === 'inside') {
|
||||
source.data.isbattle = stage;
|
||||
source.data.infinite = infinite;
|
||||
}
|
||||
store.applyLoginSuccess(parseLoginResponse(source.data));
|
||||
return store;
|
||||
}
|
||||
|
||||
function makeCommands(store: PlatformStore): {
|
||||
readonly commands: PlatformCommands;
|
||||
readonly sent: OutboundEnvelope[];
|
||||
} {
|
||||
const sent: OutboundEnvelope[] = [];
|
||||
return {
|
||||
commands: new PlatformCommands({
|
||||
runtimeConfig: RUNTIME_CONFIG,
|
||||
store,
|
||||
send: { send: (envelope) => { sent.push(envelope); } },
|
||||
}),
|
||||
sent,
|
||||
};
|
||||
readonly commands: PlatformCommands;
|
||||
readonly sent: OutboundEnvelope[];
|
||||
} {
|
||||
const sent: OutboundEnvelope[] = [];
|
||||
return {
|
||||
commands: new PlatformCommands({
|
||||
runtimeConfig: RUNTIME_CONFIG,
|
||||
store,
|
||||
send: { send: (envelope) => { sent.push(envelope); } },
|
||||
}),
|
||||
sent,
|
||||
};
|
||||
}
|
||||
|
||||
test('VIP query and both entry forms use the exact agent protocol',()=>{
|
||||
test('invite query and binding preserve exact legacy fields and leading zeros',()=>{
|
||||
const store=makeStore('outside'),{commands,sent}=makeCommands(store);
|
||||
commands.getVipRooms(['123456']);commands.joinVipRoom('123456',null);commands.joinVipRoom(900001,{city:'测试'});
|
||||
assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'getInfoByShortCode',data:{agentid:'agent-from-config',gameid:'game-from-config',shortcodeList:['123456']}});
|
||||
assert.deepEqual(sent[1],{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:'123456',joinmode:0,location:null,ip:store.getState().players.entities[430511].ip}});
|
||||
assert.equal((sent[2].data as {roomcode:number}).roomcode,900001);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getVipRooms([]),/logged-in/);
|
||||
});
|
||||
|
||||
test('task requests preserve legacy identity fields and reject unclaimable entries',()=>{
|
||||
const store=makeStore('outside');const {commands,sent}=makeCommands(store);
|
||||
commands.getPlayerTasks();
|
||||
store.applyTaskList({tasks:[{taskid:7,title:'任务',award:1,finish:1,total:1,state:1}]});
|
||||
commands.claimTask(7);
|
||||
commands.getPlayerInvitecode();commands.bindInvitecode('00123');
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'get_player_task',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config'}},
|
||||
{app:'youle',route:'agent',rpc:'get_task_award',data:{agentid:'agent-from-config',playerid:430511,taskid:7}},
|
||||
{app:'youle',route:'agent',rpc:'get_player_invitecode',data:{agentid:'agent-from-config',playerid:430511,unionid:'test_unionid',openid:'o1'}},
|
||||
{app:'youle',route:'agent',rpc:'binding_invitecode',data:{agentid:'agent-from-config',playerid:430511,invitecode:'00123'}},
|
||||
]);
|
||||
store.applyTaskAward({taskid:7,taskstate:0});
|
||||
assert.equal(store.getState().tasks?.[0].state,2);
|
||||
assert.equal(store.getState().players.entities[430511].taskstate,0);
|
||||
assert.throws(()=>commands.claimTask(7),/claimable/);
|
||||
const before=store.getState();
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{taskid:7}]}));
|
||||
assert.equal(store.getState(),before);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerTasks(),/logged-in/);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerInvitecode(),/logged-in/);
|
||||
assert.throws(()=>makeCommands(makeStore('inside')).commands.bindInvitecode('1'),/logged-in/);
|
||||
});
|
||||
|
||||
test('task numeric strings normalize at ingestion and invalid values never commit',()=>{
|
||||
const store=makeStore('outside');
|
||||
const task={taskid:'7',title:'任务',award:'3',finish:'2',total:'5',state:'0'};
|
||||
store.applyTaskList({tasks:[task]});
|
||||
assert.deepEqual(store.getState().tasks,[{...task,award:3,finish:2,total:5,state:0}]);
|
||||
assert.equal(task.award,'3');
|
||||
test('invite responses publish even unchanged codes, update only on success, and reject malformed data atomically',()=>{
|
||||
const store=makeStore('outside');let updates=0;store.subscribe(()=>updates++);
|
||||
store.applyInvitecode({invitecode:''});store.applyInvitecode({invitecode:''});
|
||||
assert.equal(updates,2);
|
||||
store.applyInviteBinding({state:0,invitecode:'00123',error:'绑定成功'});
|
||||
assert.equal(store.getState().players.entities[430511].invitecode,'00123');
|
||||
store.applyInviteBinding({state:1,error:'邀请码无效'});
|
||||
assert.equal(store.getState().players.entities[430511].invitecode,'00123');
|
||||
const before=store.getState();
|
||||
for(const field of ['award','finish','total','state']){
|
||||
for(const value of ['', ' ', 'abc', 'Infinity', null, true, [], {}, undefined]){
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{...task,[field]:value}]}),new RegExp(field));
|
||||
assert.equal(store.getState(),before);
|
||||
}
|
||||
assert.throws(()=>store.applyInvitecode({}),/invitecode/);
|
||||
assert.throws(()=>store.applyInviteBinding({state:0,invitecode:123,error:'bad'}),/invitecode/);
|
||||
assert.throws(()=>store.applyInviteBinding({state:0,invitecode:'wrong'}),/error/);
|
||||
for(const state of ['',null,true,'invalid','0.5']){
|
||||
assert.throws(()=>store.applyInviteBinding({state,invitecode:'wrong',error:'bad'}),/state/);
|
||||
}
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{...task,state:'1.5'}]}),/state/);
|
||||
assert.equal(store.getState(),before);
|
||||
});
|
||||
|
||||
test('VIP query and both entry forms use the exact agent protocol',()=>{
|
||||
const store=makeStore('outside'),{commands,sent}=makeCommands(store);
|
||||
commands.getVipRooms(['123456']);commands.joinVipRoom('123456',null);commands.joinVipRoom(900001,{city:'测试'});
|
||||
assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'getInfoByShortCode',data:{agentid:'agent-from-config',gameid:'game-from-config',shortcodeList:['123456']}});
|
||||
assert.deepEqual(sent[1],{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:'123456',joinmode:0,location:null,ip:store.getState().players.entities[430511].ip}});
|
||||
assert.equal((sent[2].data as {roomcode:number}).roomcode,900001);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getVipRooms([]),/logged-in/);
|
||||
});
|
||||
|
||||
test('prepare and exitRoom use the first-slice builders with authoritative config and room state', () => {
|
||||
const { commands, sent } = makeCommands(makeStore('inside', 0));
|
||||
|
||||
commands.prepare();
|
||||
commands.exitRoom();
|
||||
|
||||
assert.deepEqual(sent, [
|
||||
{
|
||||
app: 'youle',
|
||||
route: 'room',
|
||||
rpc: 'player_prepare',
|
||||
data: {
|
||||
agentid: 'agent-from-config',
|
||||
playerid: 430511,
|
||||
gameid: 'game-from-config',
|
||||
roomcode: '100001',
|
||||
},
|
||||
},
|
||||
{
|
||||
app: 'youle',
|
||||
route: 'room',
|
||||
rpc: 'self_exit_room',
|
||||
data: {
|
||||
agentid: 'agent-from-config',
|
||||
playerid: 430511,
|
||||
gameid: 'game-from-config',
|
||||
roomcode: '100001',
|
||||
},
|
||||
},
|
||||
test('ranking and redemption use exact legacy fields, preserving card text',()=>{
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
commands.getTreasureList();commands.redeemCard('00Ab-12');
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'get_treasurelist',data:{agentid:'agent-from-config',gameid:'game-from-config'}},
|
||||
{app:'youle',route:'agent',rpc:'topup_card',data:{agentid:'agent-from-config',playerid:430511,cardno:'00Ab-12'}},
|
||||
]);
|
||||
assert.equal(commands.prepare.length, 0);
|
||||
assert.equal(commands.exitRoom.length, 0);
|
||||
assert.throws(()=>commands.redeemCard(' '),/cardno/);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getTreasureList(),/logged-in/);
|
||||
});
|
||||
|
||||
test('task requests preserve legacy identity fields and reject unclaimable entries',()=>{
|
||||
const store=makeStore('outside');const {commands,sent}=makeCommands(store);
|
||||
commands.getPlayerTasks();
|
||||
store.applyTaskList({tasks:[{taskid:7,title:'任务',award:1,finish:1,total:1,state:1}]});
|
||||
commands.claimTask(7);
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'get_player_task',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config'}},
|
||||
{app:'youle',route:'agent',rpc:'get_task_award',data:{agentid:'agent-from-config',playerid:430511,taskid:7}},
|
||||
]);
|
||||
store.applyTaskAward({taskid:7,taskstate:0});
|
||||
assert.equal(store.getState().tasks?.[0].state,2);
|
||||
assert.equal(store.getState().players.entities[430511].taskstate,0);
|
||||
assert.throws(()=>commands.claimTask(7),/claimable/);
|
||||
const before=store.getState();
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{taskid:7}]}));
|
||||
assert.equal(store.getState(),before);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerTasks(),/logged-in/);
|
||||
});
|
||||
|
||||
test('task numeric strings normalize at ingestion and invalid values never commit',()=>{
|
||||
const store=makeStore('outside');
|
||||
const task={taskid:'7',title:'任务',award:'3',finish:'2',total:'5',state:'0'};
|
||||
store.applyTaskList({tasks:[task]});
|
||||
assert.deepEqual(store.getState().tasks,[{...task,award:3,finish:2,total:5,state:0}]);
|
||||
assert.equal(task.award,'3');
|
||||
const before=store.getState();
|
||||
for(const field of ['award','finish','total','state']){
|
||||
for(const value of ['', ' ', 'abc', 'Infinity', null, true, [], {}, undefined]){
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{...task,[field]:value}]}),new RegExp(field));
|
||||
assert.equal(store.getState(),before);
|
||||
}
|
||||
}
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{...task,state:'1.5'}]}),/state/);
|
||||
});
|
||||
|
||||
test('prepare and exitRoom use the first-slice builders with authoritative config and room state', () => {
|
||||
const { commands, sent } = makeCommands(makeStore('inside', 0));
|
||||
|
||||
commands.prepare();
|
||||
commands.exitRoom();
|
||||
|
||||
assert.deepEqual(sent, [
|
||||
{
|
||||
app: 'youle',
|
||||
route: 'room',
|
||||
rpc: 'player_prepare',
|
||||
data: {
|
||||
agentid: 'agent-from-config',
|
||||
playerid: 430511,
|
||||
gameid: 'game-from-config',
|
||||
roomcode: '100001',
|
||||
},
|
||||
},
|
||||
{
|
||||
app: 'youle',
|
||||
route: 'room',
|
||||
rpc: 'self_exit_room',
|
||||
data: {
|
||||
agentid: 'agent-from-config',
|
||||
playerid: 430511,
|
||||
gameid: 'game-from-config',
|
||||
roomcode: '100001',
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.equal(commands.prepare.length, 0);
|
||||
assert.equal(commands.exitRoom.length, 0);
|
||||
});
|
||||
|
||||
test('joinRoom uses authenticated Store identity and requires every non-optional environment field', () => {
|
||||
const { commands, sent } = makeCommands(makeStore('outside'));
|
||||
const location = { latitude: 28.7, longitude: 115.8 };
|
||||
|
||||
commands.joinRoom({
|
||||
roomcode: '654321',
|
||||
location,
|
||||
ip: '203.0.113.7',
|
||||
vipMatch: 1,
|
||||
match_id: 'match-9',
|
||||
});
|
||||
|
||||
assert.deepEqual(sent, [{
|
||||
app: 'youle',
|
||||
route: 'agent',
|
||||
rpc: 'self_join_room',
|
||||
data: {
|
||||
agentid: 'agent-from-config',
|
||||
playerid: 430511,
|
||||
gameid: 'game-from-config',
|
||||
roomcode: '654321',
|
||||
location,
|
||||
ip: '203.0.113.7',
|
||||
vipMatch: 1,
|
||||
match_id: 'match-9',
|
||||
},
|
||||
}]);
|
||||
assert.equal((sent[0]!.data as { location: unknown }).location, location);
|
||||
assert.equal(commands.joinRoom.length, 1);
|
||||
|
||||
assert.throws(
|
||||
() => commands.joinRoom({ roomcode: '654321', ip: '203.0.113.7' } as never),
|
||||
/location/i,
|
||||
);
|
||||
assert.throws(
|
||||
() => commands.joinRoom({ roomcode: '654321', location } as never),
|
||||
/ip/i,
|
||||
);
|
||||
assert.equal(sent.length, 1);
|
||||
});
|
||||
|
||||
test('exitRoom sends self_exit_room while an infinite room is in battle', () => {
|
||||
const { commands, sent } = makeCommands(makeStore('inside', 1, 1));
|
||||
|
||||
commands.exitRoom();
|
||||
|
||||
assert.deepEqual(sent, [{
|
||||
app: 'youle',
|
||||
route: 'room',
|
||||
rpc: 'self_exit_room',
|
||||
data: {
|
||||
agentid: 'agent-from-config',
|
||||
playerid: 430511,
|
||||
gameid: 'game-from-config',
|
||||
roomcode: '100001',
|
||||
},
|
||||
}]);
|
||||
});
|
||||
|
||||
test('command preconditions reject missing authentication, wrong room location, and battle exit without sending', () => {
|
||||
const unauthenticated = makeCommands(new PlatformStore());
|
||||
assert.throws(
|
||||
() => unauthenticated.commands.joinRoom({ roomcode: '654321', location: null, ip: '' }),
|
||||
/authenticated|player/i,
|
||||
);
|
||||
|
||||
const outside = makeCommands(makeStore('outside'));
|
||||
assert.throws(() => outside.commands.prepare(), /inside.*room|room.*inside/i);
|
||||
assert.throws(() => outside.commands.exitRoom(), /inside.*room|room.*inside/i);
|
||||
|
||||
const inside = makeCommands(makeStore('inside', 0));
|
||||
assert.throws(
|
||||
() => inside.commands.joinRoom({ roomcode: '654321', location: null, ip: '' }),
|
||||
/outside.*room|already.*room/i,
|
||||
);
|
||||
|
||||
const battle = makeCommands(makeStore('inside', 1));
|
||||
assert.throws(() => battle.commands.exitRoom(), /room\.exit.*stage=0/i);
|
||||
|
||||
assert.equal(unauthenticated.sent.length, 0);
|
||||
assert.equal(outside.sent.length, 0);
|
||||
assert.equal(inside.sent.length, 0);
|
||||
assert.equal(battle.sent.length, 0);
|
||||
});
|
||||
|
||||
test('VIP rank and owned room requests preserve legacy exact fields',()=>{
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
commands.getVipRankList(10);commands.joinOwnedRoom(123456,{city:'x'});
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'getVipRankList',data:{agentid:'agent-from-config',limit:10}},
|
||||
{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:123456,joinmode:1,location:{city:'x'},ip:'127.0.0.1'}}]);
|
||||
assert.throws(()=>commands.getVipRankList(-1));
|
||||
});
|
||||
|
||||
test('joinRoom uses authenticated Store identity and requires every non-optional environment field', () => {
|
||||
const { commands, sent } = makeCommands(makeStore('outside'));
|
||||
const location = { latitude: 28.7, longitude: 115.8 };
|
||||
|
||||
commands.joinRoom({
|
||||
roomcode: '654321',
|
||||
location,
|
||||
ip: '203.0.113.7',
|
||||
vipMatch: 1,
|
||||
match_id: 'match-9',
|
||||
});
|
||||
|
||||
assert.deepEqual(sent, [{
|
||||
app: 'youle',
|
||||
route: 'agent',
|
||||
rpc: 'self_join_room',
|
||||
data: {
|
||||
agentid: 'agent-from-config',
|
||||
playerid: 430511,
|
||||
gameid: 'game-from-config',
|
||||
roomcode: '654321',
|
||||
location,
|
||||
ip: '203.0.113.7',
|
||||
vipMatch: 1,
|
||||
match_id: 'match-9',
|
||||
},
|
||||
}]);
|
||||
assert.equal((sent[0]!.data as { location: unknown }).location, location);
|
||||
assert.equal(commands.joinRoom.length, 1);
|
||||
|
||||
assert.throws(
|
||||
() => commands.joinRoom({ roomcode: '654321', ip: '203.0.113.7' } as never),
|
||||
/location/i,
|
||||
);
|
||||
assert.throws(
|
||||
() => commands.joinRoom({ roomcode: '654321', location } as never),
|
||||
/ip/i,
|
||||
);
|
||||
assert.equal(sent.length, 1);
|
||||
});
|
||||
|
||||
test('exitRoom sends self_exit_room while an infinite room is in battle', () => {
|
||||
const { commands, sent } = makeCommands(makeStore('inside', 1, 1));
|
||||
|
||||
commands.exitRoom();
|
||||
|
||||
assert.deepEqual(sent, [{
|
||||
app: 'youle',
|
||||
route: 'room',
|
||||
rpc: 'self_exit_room',
|
||||
data: {
|
||||
agentid: 'agent-from-config',
|
||||
playerid: 430511,
|
||||
gameid: 'game-from-config',
|
||||
roomcode: '100001',
|
||||
},
|
||||
}]);
|
||||
});
|
||||
|
||||
test('command preconditions reject missing authentication, wrong room location, and battle exit without sending', () => {
|
||||
const unauthenticated = makeCommands(new PlatformStore());
|
||||
assert.throws(
|
||||
() => unauthenticated.commands.joinRoom({ roomcode: '654321', location: null, ip: '' }),
|
||||
/authenticated|player/i,
|
||||
);
|
||||
|
||||
const outside = makeCommands(makeStore('outside'));
|
||||
assert.throws(() => outside.commands.prepare(), /inside.*room|room.*inside/i);
|
||||
assert.throws(() => outside.commands.exitRoom(), /inside.*room|room.*inside/i);
|
||||
|
||||
const inside = makeCommands(makeStore('inside', 0));
|
||||
assert.throws(
|
||||
() => inside.commands.joinRoom({ roomcode: '654321', location: null, ip: '' }),
|
||||
/outside.*room|already.*room/i,
|
||||
);
|
||||
|
||||
const battle = makeCommands(makeStore('inside', 1));
|
||||
assert.throws(() => battle.commands.exitRoom(), /room\.exit.*stage=0/i);
|
||||
|
||||
assert.equal(unauthenticated.sent.length, 0);
|
||||
assert.equal(outside.sent.length, 0);
|
||||
assert.equal(inside.sent.length, 0);
|
||||
assert.equal(battle.sent.length, 0);
|
||||
});
|
||||
test('create command emits exact legacy packet and numeric keypad join stays numeric', () => {
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
const roomtype=[1,4,[7,8],{option:true}];
|
||||
commands.createRoom({roomtype,ip:'127.0.0.8',location:null});
|
||||
commands.joinRoom({roomcode:12345,ip:'127.0.0.8',location:null});
|
||||
assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'create_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomtype,ip:'127.0.0.8',location:null}});
|
||||
assert.deepEqual(sent[1].data,{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:12345,ip:'127.0.0.8',location:null});
|
||||
assert.throws(()=>commands.createRoom({roomtype:[undefined],ip:'x',location:null}),/JSON/);
|
||||
assert.throws(()=>makeCommands(makeStore('inside')).commands.createRoom({roomtype,ip:'x',location:null}),/outside|lobby/);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { announcementMotion } from '../../YouleNexus/assets/framework/presentation/lobby-model.ts';
|
||||
|
||||
test('announcement follows source byte width, speed, right entry and looping left exit',()=>{
|
||||
const initial=announcementMotion('公告ABC',645,0);
|
||||
assert.equal(initial.textWidth,105); // gblen=7, Game_Config.Notice.width=15
|
||||
assert.equal(initial.duration,7.5); // source speed=0.1 px/ms
|
||||
assert.equal(initial.left,645);
|
||||
assert.equal(announcementMotion('公告ABC',645,1).left,545);
|
||||
assert.ok(announcementMotion('公告ABC',645,7.49).left < -103);
|
||||
assert.equal(announcementMotion('公告ABC',645,7.5).left,645);
|
||||
assert.equal(announcementMotion('公告ABC',645,15).left,645);
|
||||
assert.equal(announcementMotion('',645,0).visible,false);
|
||||
assert.equal(announcementMotion(' ',645,0).visible,true);
|
||||
assert.throws(()=>announcementMotion('公告',0,0),/width/);
|
||||
});
|
||||
@@ -1,84 +1,98 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||||
import { resolveLobbyMenuConfig, lobbyMenuState, lobbyTrayState, LOBBY_MENU_ACTIONS, LOBBY_UNAVAILABLE } from '../../YouleNexus/assets/framework/presentation/lobby-menu.ts';
|
||||
|
||||
const identity={agentid:'a',gameid:'g',channelid:'c',marketid:'m',version:1};
|
||||
const remote=(data:Record<string,unknown>)=>createRemoteConfig(data,identity);
|
||||
const source=readFileSync(new URL('../../../projects/Game_Surface_3/js/00_Surface/11_GameUI.js',import.meta.url),'utf8');
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||||
import { resolveLobbyMenuConfig, lobbyMenuState, lobbyTrayState, LOBBY_MENU_ACTIONS, LOBBY_UNAVAILABLE, lobbyLockedTip } from '../../YouleNexus/assets/framework/presentation/lobby-menu.ts';
|
||||
|
||||
const identity={agentid:'a',gameid:'g',channelid:'c',marketid:'m',version:1};
|
||||
const remote=(data:Record<string,unknown>)=>createRemoteConfig(data,identity);
|
||||
const source=readFileSync(new URL('../../../projects/Game_Surface_3/js/00_Surface/11_GameUI.js',import.meta.url),'utf8');
|
||||
const ids=[11,12,13,14,17,7,234,483,568,286,287,10,508,496,509,3233,3232,3256,3264,76,3139,679,99,3120,3161,3162,3016,3018];
|
||||
function original(config:ReturnType<typeof resolveLobbyMenuConfig>,versionState:number,launchMode:number) {
|
||||
const visible:Record<number,boolean>=Object.fromEntries(ids.map(id=>[id,true]));
|
||||
const left:Record<number,number>={};
|
||||
const noop=()=>{};
|
||||
const ui=new Proxy({onstate:[],isprepare:[]} as Record<string,any>,{get:(o,k)=>k in o?o[k as string]:noop});
|
||||
const context=vm.createContext({GameUI:ui,GameData:{...config,Scene:1,versionState,LaunchMode:launchMode,
|
||||
hallConfig:config.hall,sysConfig:config.sys,isLocation:true,submitPhoneInfo:true,submitAddressBook:true},
|
||||
Game_Config:{sysConfig:config.template,Max:{},Share:{}},C_Player:{},ConstVal:{isGameHall:config.template.isGameHall,
|
||||
Launch_mode:{from_self:0,from_hall:1},sysNotice:{}},Logic:new Proxy({},{get:()=>noop}),Func:new Proxy({},{get:()=>noop}),
|
||||
Utl:new Proxy({},{get:()=>noop}),Game_Modify:{},gameHallImport:{jumpMenuScene:noop},
|
||||
set_self:(id:number,key:number,value:number)=>{if(key===37)visible[id]=Boolean(value);if(key===18)left[id]=value;},
|
||||
get_self:(id:number,key:number)=>key===37?Number(visible[id]):0,set_group:noop,shareLink:noop,isArray:Array.isArray});
|
||||
for(const name of ['JumpMenuScene','adaptSmartBar','adaptSmartBar2','updateActBtn','showStoreBtn']){
|
||||
const start=source.indexOf(`GameUI.${name} = function`);
|
||||
assert.ok(start>=0,name);
|
||||
const end=source.indexOf('\nGameUI.',start+1);
|
||||
vm.runInContext(source.slice(start,end),context);
|
||||
}
|
||||
ui.JumpMenuScene();
|
||||
return {visible,left};
|
||||
}
|
||||
|
||||
test('menu visibility and visible-only spacing match original JumpMenuScene across configuration combinations',()=>{
|
||||
for(const versionState of [0,1] as const)for(const isGameHall of [false,true])for(const agentmode of [1,2,3])
|
||||
for(const androidPay of [0,1])for(const flags of [0,1,2,3,4,7]){
|
||||
const config=resolveLobbyMenuConfig(remote({agentmode,androidPay,rankList:flags&1,hideTaskBtn:flags&1,
|
||||
hideWxShare:flags&2,miniPro:flags&4,hideRoomCard:flags&2?2:1,cdKey:flags&4?2:1,hall_config:{actData:flags&1?['',0,'','']:0}}),
|
||||
{isGameHall,mainMenuButton:isGameHall,hideNotice:isGameHall,vipInfinite:false});
|
||||
for(const launchMode of [0,1] as const){
|
||||
const expected=original(config,versionState,launchMode);
|
||||
const actual=lobbyMenuState(config,{versionState,launchMode});
|
||||
for(const id of ids){
|
||||
assert.equal(actual.visible[id]??true,expected.visible[id],JSON.stringify({id,versionState,isGameHall,agentmode,androidPay,flags}));
|
||||
assert.equal(actual.left[id],expected.left[id],`spacing ${id}`);
|
||||
}
|
||||
}
|
||||
test('three locked entries preserve source flags and exact click tips',()=>{
|
||||
const closed=resolveLobbyMenuConfig(remote({}));
|
||||
assert.deepEqual(closed.locked,{568:true,483:true,3139:true});
|
||||
assert.equal(lobbyLockedTip(closed,'world-rooms'),'暂未开放,敬请期待');
|
||||
assert.equal(lobbyLockedTip(closed,'match'),'暂未开放,敬请期待');
|
||||
assert.equal(lobbyLockedTip(closed,'official-rooms'),'暂未开放,敬请期待');
|
||||
for(const worldRoomList of [0,1,'0'])for(const vipConfig of [null,[['001234']]])for(const ofcShortCode of [null,'001234']){
|
||||
const config=resolveLobbyMenuConfig(remote({hall_config:{worldRoomList},vipConfig,ofcShortCode}));
|
||||
assert.deepEqual(config.locked,{568:!worldRoomList,483:!vipConfig,3139:!ofcShortCode});
|
||||
assert.equal(lobbyLockedTip(config,'world-rooms'),worldRoomList?null:'暂未开放,敬请期待');
|
||||
assert.equal(lobbyLockedTip(config,'match'),vipConfig?null:'暂未开放,敬请期待');
|
||||
assert.equal(lobbyLockedTip(config,'official-rooms'),ofcShortCode?null:'暂未开放,敬请期待');
|
||||
}
|
||||
});
|
||||
test('source hall merge preserves scoped top-level override and rejects invalid data',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({hall_config:{androidPay:1,rankList:2},rankList:1}));
|
||||
assert.equal(config.hall.androidPay,1);assert.equal(config.hall.rankList,1);
|
||||
assert.throws(()=>resolveLobbyMenuConfig(remote({hall_config:'bad'})),/hall_config/);
|
||||
assert.throws(()=>resolveLobbyMenuConfig(remote({agentmode:'bad'})),/agentmode/);
|
||||
});
|
||||
test('legacy hallConfigName actData colon typo leaves activity sourced only from hall_config',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({actData:['top'],hall_config:{actData:['hall']}}));
|
||||
assert.deepEqual(config.hall.actData,['hall']);
|
||||
assert.equal(resolveLobbyMenuConfig(remote({actData:['top']})).hall.actData,0);
|
||||
});
|
||||
test('source truthiness is preserved for string zero flags rather than coerced to numeric zero',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({hideTaskBtn:'0',hideWxShare:'0',rankList:'0'}));
|
||||
const actual=lobbyMenuState(config,{versionState:0,launchMode:0});
|
||||
const expected=original(config,0,0);
|
||||
for(const id of [17,14,3233])assert.equal(actual.visible[id],expected.visible[id]);
|
||||
assert.equal(actual.visible[17],false);assert.equal(actual.visible[3233],true);
|
||||
});
|
||||
test('main menu override hides all four entries and cdKey runs after review restriction',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({cdKey:2}),{isGameHall:false,mainMenuButton:true,hideNotice:false,vipInfinite:true});
|
||||
const normal=lobbyMenuState(config,{versionState:0,launchMode:0});
|
||||
for(const id of [11,12,483,568,3139,679])assert.equal(normal.visible[id],false);
|
||||
assert.equal(lobbyMenuState(config,{versionState:1,launchMode:0}).visible[3264],true);
|
||||
});
|
||||
test('tray excludes hidden entries from placement and keeps original feedback hidden',()=>{
|
||||
const state=lobbyTrayState(resolveLobbyMenuConfig(remote({agentmode:2,rankList:1,bagUrl:'bag',wareHouse:1,miniPro:1})),{versionState:0,launchMode:0});
|
||||
assert.equal(state.visible[235],false);assert.equal(state.visible[15],false);
|
||||
assert.deepEqual([state.left[604],state.left[3017],state.left[3057],state.left[5]],[216,371,526,681]);
|
||||
});
|
||||
test('every menu entry has an existing local action or an explicit unavailable business description',()=>{
|
||||
const local=['settings','profile','join-room','notice','tray','tasks','share','vip-rooms'];
|
||||
for(const action of Object.values(LOBBY_MENU_ACTIONS))assert.ok(local.includes(action)||LOBBY_UNAVAILABLE[action],action);
|
||||
for(const id of [10,286,287,3161] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-roomcard');
|
||||
for(const id of [508,509,496,3162] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-bean');
|
||||
});
|
||||
function original(config:ReturnType<typeof resolveLobbyMenuConfig>,versionState:number,launchMode:number) {
|
||||
const visible:Record<number,boolean>=Object.fromEntries(ids.map(id=>[id,true]));
|
||||
const left:Record<number,number>={};
|
||||
const noop=()=>{};
|
||||
const ui=new Proxy({onstate:[],isprepare:[]} as Record<string,any>,{get:(o,k)=>k in o?o[k as string]:noop});
|
||||
const context=vm.createContext({GameUI:ui,GameData:{...config,Scene:1,versionState,LaunchMode:launchMode,
|
||||
hallConfig:config.hall,sysConfig:config.sys,isLocation:true,submitPhoneInfo:true,submitAddressBook:true},
|
||||
Game_Config:{sysConfig:config.template,Max:{},Share:{}},C_Player:{},ConstVal:{isGameHall:config.template.isGameHall,
|
||||
Launch_mode:{from_self:0,from_hall:1},sysNotice:{}},Logic:new Proxy({},{get:()=>noop}),Func:new Proxy({},{get:()=>noop}),
|
||||
Utl:new Proxy({},{get:()=>noop}),Game_Modify:{},gameHallImport:{jumpMenuScene:noop},
|
||||
set_self:(id:number,key:number,value:number)=>{if(key===37)visible[id]=Boolean(value);if(key===18)left[id]=value;},
|
||||
get_self:(id:number,key:number)=>key===37?Number(visible[id]):0,set_group:noop,shareLink:noop,isArray:Array.isArray});
|
||||
for(const name of ['JumpMenuScene','adaptSmartBar','adaptSmartBar2','updateActBtn','showStoreBtn']){
|
||||
const start=source.indexOf(`GameUI.${name} = function`);
|
||||
assert.ok(start>=0,name);
|
||||
const end=source.indexOf('\nGameUI.',start+1);
|
||||
vm.runInContext(source.slice(start,end),context);
|
||||
}
|
||||
ui.JumpMenuScene();
|
||||
return {visible,left};
|
||||
}
|
||||
|
||||
test('menu visibility and visible-only spacing match original JumpMenuScene across configuration combinations',()=>{
|
||||
for(const versionState of [0,1] as const)for(const isGameHall of [false,true])for(const agentmode of [1,2,3])
|
||||
for(const androidPay of [0,1])for(const flags of [0,1,2,3,4,7]){
|
||||
const config=resolveLobbyMenuConfig(remote({agentmode,androidPay,rankList:flags&1,hideTaskBtn:flags&1,
|
||||
hideWxShare:flags&2,miniPro:flags&4,hideRoomCard:flags&2?2:1,cdKey:flags&4?2:1,hall_config:{actData:flags&1?['',0,'','']:0}}),
|
||||
{isGameHall,mainMenuButton:isGameHall,hideNotice:isGameHall,vipInfinite:false});
|
||||
for(const launchMode of [0,1] as const){
|
||||
const expected=original(config,versionState,launchMode);
|
||||
const actual=lobbyMenuState(config,{versionState,launchMode});
|
||||
for(const id of ids){
|
||||
assert.equal(actual.visible[id]??true,expected.visible[id],JSON.stringify({id,versionState,isGameHall,agentmode,androidPay,flags}));
|
||||
assert.equal(actual.left[id],expected.left[id],`spacing ${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
test('source hall merge preserves scoped top-level override and rejects invalid data',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({hall_config:{androidPay:1,rankList:2},rankList:1}));
|
||||
assert.equal(config.hall.androidPay,1);assert.equal(config.hall.rankList,1);
|
||||
assert.throws(()=>resolveLobbyMenuConfig(remote({hall_config:'bad'})),/hall_config/);
|
||||
assert.throws(()=>resolveLobbyMenuConfig(remote({agentmode:'bad'})),/agentmode/);
|
||||
});
|
||||
test('legacy hallConfigName actData colon typo leaves activity sourced only from hall_config',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({actData:['top'],hall_config:{actData:['hall']}}));
|
||||
assert.deepEqual(config.hall.actData,['hall']);
|
||||
assert.equal(resolveLobbyMenuConfig(remote({actData:['top']})).hall.actData,0);
|
||||
});
|
||||
test('source truthiness is preserved for string zero flags rather than coerced to numeric zero',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({hideTaskBtn:'0',hideWxShare:'0',rankList:'0'}));
|
||||
const actual=lobbyMenuState(config,{versionState:0,launchMode:0});
|
||||
const expected=original(config,0,0);
|
||||
for(const id of [17,14,3233])assert.equal(actual.visible[id],expected.visible[id]);
|
||||
assert.equal(actual.visible[17],false);assert.equal(actual.visible[3233],true);
|
||||
});
|
||||
test('main menu override hides all four entries and cdKey runs after review restriction',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({cdKey:2}),{isGameHall:false,mainMenuButton:true,hideNotice:false,vipInfinite:true});
|
||||
const normal=lobbyMenuState(config,{versionState:0,launchMode:0});
|
||||
for(const id of [11,12,483,568,3139,679])assert.equal(normal.visible[id],false);
|
||||
assert.equal(lobbyMenuState(config,{versionState:1,launchMode:0}).visible[3264],true);
|
||||
});
|
||||
test('tray excludes hidden entries from placement and keeps original feedback hidden',()=>{
|
||||
const state=lobbyTrayState(resolveLobbyMenuConfig(remote({agentmode:2,rankList:1,bagUrl:'bag',wareHouse:1,miniPro:1})),{versionState:0,launchMode:0});
|
||||
assert.equal(state.visible[235],false);assert.equal(state.visible[15],false);
|
||||
assert.deepEqual([state.left[604],state.left[3017],state.left[3057],state.left[5]],[216,371,526,681]);
|
||||
});
|
||||
test('every menu entry has an existing local action or an explicit unavailable business description',()=>{
|
||||
const local=['settings','profile','join-room','notice','tray','tasks','share','vip-rooms','qrcode','rank','cdkey','vip-rank','my-rooms','buy-roomcard','buy-bean'];
|
||||
for(const action of Object.values(LOBBY_MENU_ACTIONS))assert.ok(local.includes(action)||LOBBY_UNAVAILABLE[action],action);
|
||||
for(const id of [10,286,287,3161] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-roomcard');
|
||||
for(const id of [508,509,496,3162] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-bean');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {runInNewContext} from 'node:vm';
|
||||
import {validateProfileAuth} from '../../YouleNexus/assets/framework/presentation/profile-auth.ts';
|
||||
|
||||
const root=new URL('../../../projects/Game_Surface_3/js/00_Surface/',import.meta.url);
|
||||
const source=readFileSync(new URL('05_Func.js',root),'utf8');
|
||||
const districts=readFileSync(new URL('03_Banwords.js',root),'utf8');
|
||||
const context:any={};
|
||||
runInNewContext(districts.slice(districts.indexOf('var GB2260 ='),districts.indexOf(';',districts.indexOf('var GB2260 ='))+1)
|
||||
+source.slice(source.indexOf('var IDValidator ='),source.indexOf('Func.subString ='))+';validator=new IDValidator();',context);
|
||||
|
||||
test('auth follows source name check and known checksum outcomes',()=>{
|
||||
assert.equal(validateProfileAuth('张','11010519491231002X'),'name');
|
||||
assert.equal(validateProfileAuth('测试','11010519491231002X'),null);
|
||||
assert.equal(validateProfileAuth('测试','11010519491231002x'),null);
|
||||
assert.equal(validateProfileAuth('测试','110105194912310020'),'id');
|
||||
assert.equal(validateProfileAuth('测试',''),'id');
|
||||
assert.equal(validateProfileAuth('测试','110105491231002'),null);
|
||||
});
|
||||
|
||||
test('ID validation matches the original validator including legacy date and address semantics',()=>{
|
||||
const ids=['','11010519491231002X','110105194912310020','110105491231002','110105491332002','999999491231002',
|
||||
'110199491231002','110105490231002','110105490000002','11010549aa31abc',' 11010519491231002X','11010519491231002X '];
|
||||
for(const base of ['11010519491231002X','110105491231002'])for(let i=0;i<base.length;i++){
|
||||
for(const ch of ['0','9','A','x'])ids.push(base.slice(0,i)+ch+base.slice(i+1));
|
||||
}
|
||||
for(const id of ids)assert.equal(validateProfileAuth('测试',id)===null,context.validator.isValid(id),`parity for ${id}`);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {parseRankList,parseVipRankList} from '../../YouleNexus/assets/framework/protocol/contracts/rank-list.ts';
|
||||
test('config rank normalizes legacy names and signatures without modifying server entries',()=>{
|
||||
const row={name:'一二三四五六七八九',headimg:'',playerid:'001',star:100,signature:'a#b'};
|
||||
assert.deepEqual(parseRankList([row]),[row]);
|
||||
assert.deepEqual(parseRankList([row],true),[{...row,name:'一二三四五六七八...',signature:'a\nb'}]);
|
||||
assert.equal(parseRankList([{...row,name:'12345678901234567',signature:null}],true)[0].name,'1234567890123456...');
|
||||
assert.deepEqual(parseRankList([]),[]);assert.throws(()=>parseRankList([{}]));
|
||||
});
|
||||
|
||||
test('VIP ranks retain announcement fields and ignore absent optional text',()=>{
|
||||
const row={name:'VIP',headimg:'',playerid:1,star:8,signature:'签名',notice:'公告',destwo:'补充'};
|
||||
assert.deepEqual(parseVipRankList([row]),[row]);
|
||||
assert.deepEqual(parseVipRankList([{...row,notice:null,destwo:null}]),[{...row,notice:'',destwo:''}]);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {RequestLoading} from '../../YouleNexus/assets/framework/presentation/request-loading.ts';
|
||||
test('request loading closes on matching success or failure independent of payload',()=>{
|
||||
const shown:boolean[]=[];const loading=new RequestLoading(v=>shown.push(v));
|
||||
for(const rpc of ['get_player_task','get_task_award','getVipRankList'] as const){
|
||||
for(const data of [{state:0},{state:1,error:'失败'},null]){loading.begin(rpc);loading.received({route:'agent',rpc,data});assert.deepEqual(shown.splice(0),[true,false]);}
|
||||
}
|
||||
loading.begin('get_player_task');loading.received({route:'agent',rpc:'getVipRankList'});assert.deepEqual(shown,[true]);
|
||||
loading.received({route:'agent',rpc:'show_message'});assert.deepEqual(shown,[true,false]);
|
||||
});
|
||||
test('overlapping requests, cancellation, and duplicate replies do not flicker',()=>{
|
||||
const shown:boolean[]=[];const loading=new RequestLoading(v=>shown.push(v));
|
||||
loading.begin('get_player_task');loading.begin('getVipRankList');loading.received({route:'agent',rpc:'get_player_task'});
|
||||
assert.deepEqual(shown,[true]);loading.cancel('getVipRankList');loading.cancel('getVipRankList');
|
||||
assert.deepEqual(shown,[true,false]);loading.begin('get_task_award');loading.clear();assert.deepEqual(shown,[true,false,true,false]);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {JoinRoomInput} from '../../YouleNexus/assets/framework/presentation/join-room-input.ts';
|
||||
import {TemplateRoomRules} from '../../YouleNexus/assets/games/template/template-room-rules.ts';
|
||||
|
||||
test('keypad preserves display zeros and submits a number at six digits',()=>{
|
||||
const sent:number[]=[];const input=new JoinRoomInput(code=>sent.push(code));
|
||||
for(const digit of [0,1,2,3,4])input.digit(digit);
|
||||
assert.deepEqual(sent,[]);input.digit(5);
|
||||
assert.equal(input.code,'012345');assert.deepEqual(sent,[12345]);
|
||||
input.setBusy(true);input.digit(6);input.remove();input.clear();input.confirm();
|
||||
assert.equal(input.code,'012345');assert.deepEqual(sent,[12345]);
|
||||
input.setBusy(false);input.remove();assert.equal(input.code,'01234');
|
||||
input.clear();input.confirm();assert.deepEqual(sent,[12345]);
|
||||
input.digit(8);input.confirm();assert.deepEqual(sent,[12345,8]);
|
||||
});
|
||||
test('paste strips non-digits, truncates and submits; prefill does not submit',()=>{
|
||||
const sent:number[]=[];const input=new JoinRoomInput(code=>sent.push(code));
|
||||
input.prefill('123456');assert.deepEqual(sent,[]);
|
||||
input.paste('房号:987-654321');assert.equal(input.code,'987654');assert.deepEqual(sent,[987654]);
|
||||
input.paste('12a');assert.equal(input.code,'12');assert.equal(sent.length,1);
|
||||
assert.throws(()=>input.digit(10));assert.throws(()=>input.prefill('corrupt'));
|
||||
});
|
||||
test('template options do not rewrite source fixed sample; each submission is independent',()=>{
|
||||
const rules=new TemplateRoomRules();const expected=[1,4,1,2,2,[1,1,[1,2000,10],null,null,1],[1,0,5]];
|
||||
assert.deepEqual(rules.build(),expected);rules.select(0,1);rules.select(1,1);
|
||||
assert.deepEqual(rules.build(),expected);const packet=rules.build();packet[0]=999;
|
||||
assert.deepEqual(rules.build(),expected);assert.throws(()=>rules.select(0,2));
|
||||
// Original saved array's index 1 is 4: no corresponding Type_2 checkmark.
|
||||
rules.open(expected);assert.deepEqual(rules.selection,[0,3]);
|
||||
assert.equal(rules.seatCount(expected),10);assert.throws(()=>rules.open({}));
|
||||
});
|
||||
@@ -1,14 +1,23 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||||
import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
|
||||
import { resolveSharePolicy,sharePayload } from '../../YouleNexus/assets/framework/presentation/share-model.ts';
|
||||
test('share reads url_download and preserves original friend and timeline payloads',()=>{
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||||
import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
|
||||
import { resolveSharePolicy,sharePayload,resolveQrCodePolicy } from '../../YouleNexus/assets/framework/presentation/share-model.ts';
|
||||
|
||||
test('QR entry uses scoped WechatPublicNo_ewm and exact image share parameters',()=>{
|
||||
const identity={agentid:'a',channelid:'c',gameid:'g',marketid:4,version:1};
|
||||
const remoteConfig=createRemoteConfig({url_download:'https://example.test/app',sharename:'标题',hall_config:{shareDesc:'描述',shareType:1,shareImgUrl:'https://example.test/share.png'}},identity);
|
||||
const policy=resolveSharePolicy({identity,remoteConfig} as RuntimeConfig);
|
||||
assert.equal(policy.url,'https://example.test/app?agentid=a&channelid=c&gameid=g&marketid=4');
|
||||
assert.deepEqual(sharePayload(policy,1),{sharefriend:1,type:1,webpageUrl:policy.url,title:'标题',description:'描述',sharetype:0});
|
||||
assert.deepEqual(sharePayload(policy,2),{sharefriend:2,type:3,webpageUrl:'https://example.test/share.png',title:'标题描述',description:undefined,sharetype:0});
|
||||
assert.equal(sharePayload({...policy,imageTimeline:false},2),null);
|
||||
const config=(data:Record<string,unknown>)=>({identity,remoteConfig:createRemoteConfig(data,identity)} as RuntimeConfig);
|
||||
assert.equal(resolveQrCodePolicy(config({})),null);
|
||||
const result=resolveQrCodePolicy(config({WechatPublicNo_ewm:'root.png',agentlist:[{agentid:'a',WechatPublicNo_ewm:'https://example.test/qr.png'}],sharename:'标题',shareDesc:'说明'}));
|
||||
assert.deepEqual(result,{url:'https://example.test/qr.png',share:{sharefriend:1,type:3,webpageUrl:'https://example.test/qr.png',title:'标题说明',description:undefined,sharetype:0}});
|
||||
assert.throws(()=>resolveQrCodePolicy(config({WechatPublicNo_ewm:123})),/WechatPublicNo_ewm/);
|
||||
});
|
||||
test('share reads url_download and preserves original friend and timeline payloads',()=>{
|
||||
const identity={agentid:'a',channelid:'c',gameid:'g',marketid:4,version:1};
|
||||
const remoteConfig=createRemoteConfig({url_download:'https://example.test/app',sharename:'标题',hall_config:{shareDesc:'描述',shareType:1,shareImgUrl:'https://example.test/share.png'}},identity);
|
||||
const policy=resolveSharePolicy({identity,remoteConfig} as RuntimeConfig);
|
||||
assert.equal(policy.url,'https://example.test/app?agentid=a&channelid=c&gameid=g&marketid=4');
|
||||
assert.deepEqual(sharePayload(policy,1),{sharefriend:1,type:1,webpageUrl:policy.url,title:'标题',description:'描述',sharetype:0});
|
||||
assert.deepEqual(sharePayload(policy,2),{sharefriend:2,type:3,webpageUrl:'https://example.test/share.png',title:'标题描述',description:undefined,sharetype:0});
|
||||
assert.equal(sharePayload({...policy,imageTimeline:false},2),null);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createTemplateGame} from '../../YouleNexus/assets/games/template/template-game.ts';
|
||||
test('template module attaches only when opened and releases its view and subscription',()=>{
|
||||
let unsubscribed=0;const commands:unknown[]=[];const views:unknown[]=[];
|
||||
const snapshot={connection:{phase:'logged-in'},self:{playerId:1,seat:0},room:{roomcode:'123456',roomtype:[],stage:0,needprepare:0,infinite:0},seats:[]};
|
||||
const game=createTemplateGame('g');const probe=game.entry.createModule();probe.dispose();
|
||||
assert.throws(()=>game.mount({render:s=>views.push(s),close(){}}));
|
||||
const module=game.entry.createModule();
|
||||
module.attach({getSnapshot:()=>snapshot,subscribe:()=>()=>unsubscribed++,execute:(c:unknown)=>commands.push(c),seat:{toView:(n:number)=>n,toServer:(n:number)=>n},sendGameMessage(){}} as any);
|
||||
let actions:any;game.mount({render:(s,a)=>{views.push(s);actions=a;},close:()=>views.push('closed')});
|
||||
actions.exit();actions.prepare();assert.deepEqual(commands,[{type:'room.exit'},{type:'room.prepare'}]);
|
||||
const deskinfo={nested:[1,2]};module.restore(deskinfo);assert.equal(game.restoredSnapshot,deskinfo);
|
||||
module.dispose();assert.equal(unsubscribed,1);assert.equal(views.at(-1),'closed');assert.throws(()=>actions.exit());
|
||||
});
|
||||
@@ -1,38 +1,45 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
FIRST_SLICE_RPC_ROUTE,
|
||||
routeForFirstSlice,
|
||||
} from '../../YouleNexus/assets/framework/protocol/first-slice-routes.ts';
|
||||
|
||||
test('FIRST_SLICE_RPC_ROUTE 是第一纵向切片唯一且精确的 rpc route 表', () => {
|
||||
assert.deepEqual(FIRST_SLICE_RPC_ROUTE, {
|
||||
getInfoByShortCode: 'agent',
|
||||
get_player_task: 'agent',
|
||||
get_task_award: 'agent',
|
||||
update_roomcard: 'agent',
|
||||
player_finish_task: 'agent',
|
||||
player_login: 'agent',
|
||||
self_join_room: 'agent',
|
||||
player_prepare: 'room',
|
||||
self_exit_room: 'room',
|
||||
other_join_room: 'room',
|
||||
other_exit_room: 'room',
|
||||
other_offline: 'room',
|
||||
other_online: 'room',
|
||||
connect_roomserver: 'room',
|
||||
connect_agentserver: 'agent',
|
||||
kick_server: 'agent',
|
||||
kick_offline: 'agent',
|
||||
show_message: 'agent',
|
||||
});
|
||||
});
|
||||
|
||||
test('routeForFirstSlice 从单源映射取 route,未知 rpc 显式失败', () => {
|
||||
assert.equal(routeForFirstSlice('self_join_room'), 'agent');
|
||||
assert.equal(routeForFirstSlice('connect_roomserver'), 'room');
|
||||
assert.throws(
|
||||
() => routeForFirstSlice('unknown_rpc' as never),
|
||||
/unknown_rpc/,
|
||||
);
|
||||
});
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
FIRST_SLICE_RPC_ROUTE,
|
||||
routeForFirstSlice,
|
||||
} from '../../YouleNexus/assets/framework/protocol/first-slice-routes.ts';
|
||||
|
||||
test('FIRST_SLICE_RPC_ROUTE 是第一纵向切片唯一且精确的 rpc route 表', () => {
|
||||
assert.deepEqual(FIRST_SLICE_RPC_ROUTE, {
|
||||
getVipRankList: 'agent',
|
||||
get_treasurelist: 'agent',
|
||||
topup_card: 'agent',
|
||||
update_bean: 'agent',
|
||||
get_player_invitecode: 'agent',
|
||||
binding_invitecode: 'agent',
|
||||
getInfoByShortCode: 'agent',
|
||||
get_player_task: 'agent',
|
||||
get_task_award: 'agent',
|
||||
update_roomcard: 'agent',
|
||||
player_finish_task: 'agent',
|
||||
player_login: 'agent',
|
||||
create_room: 'agent',
|
||||
self_join_room: 'agent',
|
||||
player_prepare: 'room',
|
||||
self_exit_room: 'room',
|
||||
other_join_room: 'room',
|
||||
other_exit_room: 'room',
|
||||
other_offline: 'room',
|
||||
other_online: 'room',
|
||||
connect_roomserver: 'room',
|
||||
connect_agentserver: 'agent',
|
||||
kick_server: 'agent',
|
||||
kick_offline: 'agent',
|
||||
show_message: 'agent',
|
||||
});
|
||||
});
|
||||
|
||||
test('routeForFirstSlice 从单源映射取 route,未知 rpc 显式失败', () => {
|
||||
assert.equal(routeForFirstSlice('self_join_room'), 'agent');
|
||||
assert.equal(routeForFirstSlice('connect_roomserver'), 'room');
|
||||
assert.throws(
|
||||
() => routeForFirstSlice('unknown_rpc' as never),
|
||||
/unknown_rpc/,
|
||||
);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user