Initial commit: ssessesse ki battle
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { GameApi } from '../src/client/api.js';
|
||||
import { createLobbyFeatures } from '../src/client/lobby-features.js';
|
||||
import { normalizeAppearance, type FighterAppearance } from '../src/shared/cosmetics.js';
|
||||
import type { PlayerProfile } from '../src/shared/ranked-types.js';
|
||||
|
||||
// A selector/event surface, not an HTML parser or rendering implementation.
|
||||
// The test invokes the production selection/equip handlers and observes the
|
||||
// appearance callback used by main.ts to display the equipped character.
|
||||
function documentSurface() {
|
||||
const ids = new Map<string, Element>();
|
||||
class Element {
|
||||
nodes = new Map<string, Element>();
|
||||
listeners = new Map<string, (event: any) => void>();
|
||||
classList = { remove() {} };
|
||||
disabled = false; textContent = ''; innerHTML = ''; value = '';
|
||||
onclick?: () => void | Promise<void>;
|
||||
set id(id: string) { ids.set(id, this); }
|
||||
querySelector(selector: string): Element {
|
||||
if (!this.nodes.has(selector)) this.nodes.set(selector, new Element());
|
||||
return this.nodes.get(selector)!;
|
||||
}
|
||||
addEventListener(name: string, listener: (event: any) => void) { this.listeners.set(name, listener); }
|
||||
prepend() {} after() {} append() {} showModal() {} close() {}
|
||||
}
|
||||
const document = {
|
||||
body: new Element(), createElement: () => new Element(), querySelector: () => new Element(),
|
||||
getElementById(id: string) {
|
||||
if (!ids.has(id)) ids.set(id, new Element());
|
||||
return ids.get(id)!;
|
||||
},
|
||||
};
|
||||
return document;
|
||||
}
|
||||
|
||||
test('a profile read started before equip cannot undo the saved appearance', async t => {
|
||||
const document = documentSurface(), original = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: document });
|
||||
t.after(() => {
|
||||
if (original) Object.defineProperty(globalThis, 'document', original);
|
||||
else Reflect.deleteProperty(globalThis, 'document');
|
||||
});
|
||||
const profile = (skinTone: 'light' | 'tan'): PlayerProfile => ({
|
||||
playerId: 'player-a', displayName: 'Alpha', appearance: normalizeAppearance({ characterId: 0, skinTone, colorId: 'white' }),
|
||||
stats: { wins: 0, losses: 0, draws: 0, current: 0, best: 0 }, unlockedColors: ['white'], unlockedEquipment: [],
|
||||
});
|
||||
let finishOldRead!: (value: PlayerProfile) => void;
|
||||
const oldRead = new Promise<PlayerProfile>(resolve => { finishOldRead = resolve; });
|
||||
let reads = 0, savedAppearance: FighterAppearance | undefined;
|
||||
const appearances: FighterAppearance[] = [];
|
||||
let savedDisplayed!: () => void;
|
||||
const displayed = new Promise<void>(resolve => { savedDisplayed = resolve; });
|
||||
const api = { async request(path: string, method?: string, body?: { appearance: FighterAppearance }) {
|
||||
assert.equal(path, '/api/profile');
|
||||
if (method === 'POST') {
|
||||
savedAppearance = body!.appearance;
|
||||
return { ...profile('light'), appearance: normalizeAppearance(savedAppearance) };
|
||||
}
|
||||
return ++reads === 1 ? profile('light') : oldRead;
|
||||
} } as unknown as GameApi;
|
||||
const lobby = createLobbyFeatures({ api, onQueue() {}, onMatch() {}, onHome() {}, onAvailabilityChange() {},
|
||||
onError(error) { throw error; },
|
||||
onAppearance(appearance) {
|
||||
appearances.push(appearance);
|
||||
if (appearance.skinTone === 'tan') savedDisplayed();
|
||||
},
|
||||
});
|
||||
await lobby.initialize(false);
|
||||
const opening = document.getElementById('appearance-open').onclick!();
|
||||
const dialog = document.getElementById('appearance-dialog');
|
||||
dialog.querySelector('.skin-options').listeners.get('click')!({
|
||||
target: { closest: () => ({ dataset: { skin: 'tan' } }) },
|
||||
});
|
||||
const equip = dialog.querySelector('.equip');
|
||||
assert.equal(equip.disabled, false);
|
||||
equip.onclick!();
|
||||
await displayed;
|
||||
assert.deepEqual(savedAppearance, normalizeAppearance({ characterId: 0, skinTone: 'tan', colorId: 'white' }));
|
||||
finishOldRead(profile('light'));
|
||||
await opening;
|
||||
assert.deepEqual(appearances.at(-1), savedAppearance, 'the late GET must not revert the displayed equipment');
|
||||
assert.equal(equip.disabled, true, 'the saved preview must still be equipped');
|
||||
});
|
||||
|
||||
for (const editedDuringLoad of [false, true]) {
|
||||
test(editedDuringLoad
|
||||
? 'refresh preserves hair edits made while the profile is loading'
|
||||
: 'refresh replaces an untouched preview with equipment saved in another tab', async t => {
|
||||
const document = documentSurface(), original = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: document });
|
||||
t.after(() => {
|
||||
if (original) Object.defineProperty(globalThis, 'document', original);
|
||||
else Reflect.deleteProperty(globalThis, 'document');
|
||||
});
|
||||
const cached = normalizeAppearance({ characterId: 0, colorId: 'white' });
|
||||
const remote = normalizeAppearance({ characterId: 1, colorId: 'white', equipment: ['shoes'] });
|
||||
const profile = (appearance: FighterAppearance): PlayerProfile => ({
|
||||
playerId: 'player-a', displayName: 'Alpha', appearance,
|
||||
stats: { wins: 5, losses: 0, draws: 0, current: 5, best: 5 },
|
||||
unlockedColors: ['white'], unlockedEquipment: ['shoes', 'wristbands'],
|
||||
});
|
||||
let finishRead!: (profile: PlayerProfile) => void;
|
||||
const pendingRead = new Promise<PlayerProfile>(resolve => { finishRead = resolve; });
|
||||
let reads = 0, savedAppearance: FighterAppearance | undefined;
|
||||
let savedDisplayed!: () => void;
|
||||
const displayed = new Promise<void>(resolve => { savedDisplayed = resolve; });
|
||||
const api = { async request(path: string, method?: string, body?: { appearance: FighterAppearance }) {
|
||||
assert.equal(path, '/api/profile');
|
||||
if (method === 'POST') { savedAppearance = body!.appearance; return profile(savedAppearance); }
|
||||
return ++reads === 1 ? profile(cached) : pendingRead;
|
||||
} } as unknown as GameApi;
|
||||
const lobby = createLobbyFeatures({ api, onQueue() {}, onMatch() {}, onHome() {}, onAvailabilityChange() {},
|
||||
onError(error) { throw error; }, onAppearance() { if (savedAppearance) savedDisplayed(); },
|
||||
});
|
||||
await lobby.initialize(false);
|
||||
const opening = document.getElementById('appearance-open').onclick!();
|
||||
const dialog = document.getElementById('appearance-dialog');
|
||||
const chooseBald = () => dialog.querySelector('.hair-options').listeners.get('click')!({
|
||||
target: { closest: () => ({ dataset: { hair: 'bald' } }) },
|
||||
});
|
||||
if (editedDuringLoad) chooseBald();
|
||||
finishRead(profile(remote));
|
||||
await opening;
|
||||
const equip = dialog.querySelector('.equip');
|
||||
assert.equal(equip.disabled, !editedDuringLoad, 'an untouched refreshed preview is already equipped');
|
||||
if (!editedDuringLoad) chooseBald();
|
||||
assert.equal(equip.disabled, false);
|
||||
equip.onclick!();
|
||||
await displayed;
|
||||
assert.deepEqual(savedAppearance, editedDuringLoad
|
||||
? normalizeAppearance({ characterId: 0, colorId: 'white', hairStyle: 'bald', equipment: [] })
|
||||
: normalizeAppearance({ characterId: 1, colorId: 'white', hairStyle: 'bald', equipment: ['shoes'] }));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user