import assert from 'node:assert/strict'; import test from 'node:test'; import { selectPageModel } from '../../YouleNexus/assets/framework/presentation/page-model.ts'; import type { PlatformState } from '../../YouleNexus/assets/framework/platform/stores/platform-types.ts'; test('loading is an immutable page without business data', () => { const model = selectPageModel('loading', { app: { phase: 'idle' }, players: { selfPlayerId: null, entities: {} }, room: { kind: 'outside' }, }, false); assert.deepEqual(model, { page: 'loading' }); assert.ok(Object.isFrozen(model)); }); test('empty seats keep authoritative positions and count', () => { for (const count of [2, 4, 10]) { const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0, isprepare: 0, onstate: 0 }, } }, room: { kind: 'inside', roomcode: '000123', selfSeat: 0, seatPlayerIds: [7, ...Array(count - 1).fill(null)], readySeats: [], offlineSeats: [], roomtype: [], stage: 0, needprepare: 1, infinite: 0 }, }; const model = selectPageModel('room', state, true); assert.equal(model.page, 'room'); if (model.page !== 'room') throw new Error('expected room'); assert.equal(model.roomcode, '000123'); assert.equal(model.seats.length, count); assert.deepEqual(model.seats[1], { kind: 'empty', seat: 1 }); assert.equal(model.canPrepare, true); assert.equal(model.canExit, true); assert.ok(Object.isFrozen(model)); assert.ok(Object.isFrozen(model.seats)); assert.ok(Object.isFrozen(model.seats[1])); } }); test('login permits commands when the runtime is ready', () => { const state: PlatformState = { app: { phase: 'connected' }, players: { selfPlayerId: null, entities: {} }, room: { kind: 'outside' }, }; const readyModel = selectPageModel('login', state, true); assert.deepEqual(readyModel, { page: 'login', canLogin: true, }); assert.ok(Object.isFrozen(readyModel)); assert.deepEqual(selectPageModel('login', state, false), { page: 'login', canLogin: false, }); assert.deepEqual(selectPageModel('login', { ...state, app: { phase: 'reconnecting' } }, true), { page: 'login', canLogin: true, }); }); test('lobby projects only the public self fields without normalizing their values', () => { const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 1, ip: '127.0.0.1' }, } }, room: { kind: 'outside' }, }; const model = selectPageModel('lobby', state, true); assert.deepEqual(model, { page: 'lobby', self: { playerid: 7, nickname: 'self', avatar: '', bean: 0 }, }); assert.ok(Object.isFrozen(model)); if (model.page !== 'lobby') throw new Error('expected lobby'); assert.ok(Object.isFrozen(model.self)); assert.equal('sex' in model.self, false); assert.equal('ip' in model.self, false); }); test('lobby rejects a missing authenticated player', () => { const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: null, entities: {} }, room: { kind: 'outside' }, }; assert.throws( () => selectPageModel('lobby', state, true), /players\.selfPlayerId: expected authenticated player/, ); }); test('room rejects an occupied seat whose player entity is absent', () => { const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0, isprepare: 0, onstate: 0 }, } }, room: { kind: 'inside', roomcode: '000123', selfSeat: 0, seatPlayerIds: [7, 8], readySeats: [], offlineSeats: [], roomtype: [], stage: 0, needprepare: 1, infinite: 0 }, }; assert.throws( () => selectPageModel('room', state, true), /players\.entities: missing player for occupied seat 1/, ); }); test('room requires selfSeat to identify the authenticated player', () => { const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0, isprepare: 0, onstate: 0 }, } }, room: { kind: 'inside', roomcode: '000123', selfSeat: 1, seatPlayerIds: [7, null], readySeats: [], offlineSeats: [], roomtype: [], stage: 0, needprepare: 1, infinite: 0 }, }; assert.throws( () => selectPageModel('room', state, true), /room\.selfSeat: expected authenticated player seat/, ); }); test('room rejects missing protocol status for an occupied seat', () => { const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0, isprepare: 0, onstate: 0 }, 8: { playerid: 8, nickname: 'guest', avatar: '', bean: 5, sex: 1, isprepare: 0 }, } }, room: { kind: 'inside', roomcode: '000123', selfSeat: 0, seatPlayerIds: [7, 8], readySeats: [], offlineSeats: [], roomtype: [], stage: 0, needprepare: 1, infinite: 0 }, }; assert.throws( () => selectPageModel('room', state, true), /room\.seatPlayerIds\[1\].*players\.entities\[8\]\.onstate/, ); const missingPrepare: PlatformState = { ...state, players: { ...state.players, entities: { ...state.players.entities, 8: { playerid: 8, nickname: 'guest', avatar: '', bean: 5, sex: 1, onstate: 0 }, } }, }; assert.throws( () => selectPageModel('room', missingPrepare, true), /room\.seatPlayerIds\[1\].*players\.entities\[8\]\.isprepare/, ); }); test('room exposes ready and offline status from the occupied player', () => { const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0, isprepare: 0, onstate: 0 }, 8: { playerid: 8, nickname: 'guest', avatar: 'avatar.png', bean: 5, sex: 1, isprepare: 1, onstate: 1 }, } }, room: { kind: 'inside', roomcode: '000123', selfSeat: 0, seatPlayerIds: [7, 8], readySeats: [], offlineSeats: [], roomtype: [], stage: 0, needprepare: 1, infinite: 0 }, }; const model = selectPageModel('room', state, true); if (model.page !== 'room') throw new Error('expected room'); assert.deepEqual(model.seats[1], { kind: 'occupied', seat: 1, player: { playerid: 8, nickname: 'guest', avatar: 'avatar.png', bean: 5 }, ready: true, offline: true, }); assert.ok(Object.isFrozen(model.seats[1])); if (model.seats[1]?.kind !== 'occupied') throw new Error('expected occupied seat'); assert.ok(Object.isFrozen(model.seats[1].player)); }); test('room command availability follows the authoritative room and connection state', () => { const room = { kind: 'inside' as const, roomcode: '000123', selfSeat: 0, seatPlayerIds: [7], readySeats: [], offlineSeats: [], roomtype: [], stage: 1, needprepare: 1, infinite: 0 }; const players = { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0, isprepare: 0, onstate: 0 }, } }; const battle = selectPageModel('room', { app: { phase: 'logged-in' }, players, room }, true); assert.equal(battle.page, 'room'); if (battle.page !== 'room') throw new Error('expected room'); assert.equal(battle.canPrepare, false); assert.equal(battle.canExit, false); const infinite = selectPageModel('room', { app: { phase: 'logged-in' }, players, room: { ...room, infinite: 1 }, }, true); assert.equal(infinite.page, 'room'); if (infinite.page !== 'room') throw new Error('expected room'); assert.equal(infinite.canExit, true); const reconnecting = selectPageModel('room', { app: { phase: 'reconnecting' }, players, room: { ...room, stage: 0 }, }, true); assert.equal(reconnecting.page, 'room'); if (reconnecting.page !== 'room') throw new Error('expected room'); assert.equal(reconnecting.canPrepare, false); assert.equal(reconnecting.canExit, false); const prepared = selectPageModel('room', { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { ...players.entities[7]!, isprepare: 1 }, } }, room: { ...room, stage: 0 }, }, true); assert.equal(prepared.page, 'room'); if (prepared.page !== 'room') throw new Error('expected room'); assert.equal(prepared.canPrepare, false); const optionalPrepare = selectPageModel('room', { app: { phase: 'logged-in' }, players, room: { ...room, stage: 0, needprepare: 0 }, }, true); assert.equal(optionalPrepare.page, 'room'); if (optionalPrepare.page !== 'room') throw new Error('expected room'); assert.equal(optionalPrepare.canPrepare, false); }); test('room does not inspect roomtype while projecting seats', () => { const roomtype = new Proxy([], { get: () => { throw new Error('roomtype must not be parsed by page projection'); }, }); const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0, isprepare: 0, onstate: 0 }, } }, room: { kind: 'inside', roomcode: '000123', selfSeat: 0, seatPlayerIds: [7, null], readySeats: [], offlineSeats: [], roomtype, stage: 0, needprepare: 1, infinite: 0 }, }; assert.equal(selectPageModel('room', state, true).page, 'room'); });