feat(ui): tile-crossword loading splash for cold lobby open
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
On a cold app open the lobby's game list arrives over the network; on a slow link the empty "no games yet" line flashed before the games loaded. Add a full-screen tile splash that lays a Scrabble crossword of ЭРУДИТ / ЗАГРУЗКА / ОЖИДАНИЕ (Эрудит point values, hardcoded since the alphabet table is not cached at boot) until the lobby's first load settles, then removes itself to reveal the populated list. - lib/splash.ts: pure layout + reveal schedule (unit-tested). - components/Splash.svelte: App-level overlay; per-tile drop-in; loops ЗАГРУЗКА → ОЖИДАНИЕ until ready, dismisses on a word boundary. Static ЭРУДИТ under reduced motion / the mock build. - app state: lobbyReady (set by Lobby on first settle) + splashDone, reset on logout. - App.svelte: overlay while routeIsLobby && !splashDone; the plain text splash now only covers non-lobby deep-links during bootstrap. - docs: UI_DESIGN + FUNCTIONAL (+ _ru).
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ERUDIT_KEYS,
|
||||
ERUDIT_VALUES,
|
||||
PAUSE_MS,
|
||||
SPLASH_TILES,
|
||||
WORD_MS,
|
||||
splashSchedule,
|
||||
type SplashStep,
|
||||
} from './splash';
|
||||
|
||||
const at = (word: 'erudit' | 'zagruzka' | 'ozhidanie') =>
|
||||
SPLASH_TILES.filter((t) => t.word === word);
|
||||
const text = (word: 'erudit' | 'zagruzka' | 'ozhidanie') => at(word).map((t) => t.letter).join('');
|
||||
const reveals = (steps: SplashStep[]) => steps.filter((s) => s.kind === 'reveal');
|
||||
|
||||
describe('splash layout', () => {
|
||||
it('uses the Эрудит point values for every splash letter', () => {
|
||||
// Spot-check the distinctive weights against scrabble-solver rules.Erudit.
|
||||
expect(ERUDIT_VALUES['Э']).toBe(10);
|
||||
expect(ERUDIT_VALUES['Ж']).toBe(5);
|
||||
expect(ERUDIT_VALUES['З']).toBe(5);
|
||||
expect(ERUDIT_VALUES['У']).toBe(3);
|
||||
expect(ERUDIT_VALUES['Г']).toBe(3);
|
||||
expect(ERUDIT_VALUES['Р']).toBe(2);
|
||||
expect(ERUDIT_VALUES['И']).toBe(1);
|
||||
});
|
||||
|
||||
it('places ЭРУДИТ horizontally on row 3, left to right, with its values', () => {
|
||||
const e = at('erudit');
|
||||
expect(text('erudit')).toBe('ЭРУДИТ');
|
||||
expect(e.map((t) => t.col)).toEqual([0, 1, 2, 3, 4, 5]);
|
||||
expect(e.every((t) => t.row === 3)).toBe(true);
|
||||
expect(e.map((t) => t.value)).toEqual([10, 2, 3, 2, 1, 2]);
|
||||
});
|
||||
|
||||
it('drops the shared crossing tiles (Р, Д) from the vertical words', () => {
|
||||
// ЗАГРУЗКА in column 1 minus the shared Р at row 3; ОЖИДАНИЕ in column 3 minus Д.
|
||||
expect(at('zagruzka').map((t) => t.row)).toEqual([0, 1, 2, 4, 5, 6, 7]);
|
||||
expect(text('zagruzka')).toBe('ЗАГУЗКА'); // ЗАГ(Р)УЗКА without the shared Р
|
||||
expect(at('zagruzka').every((t) => t.col === 1)).toBe(true);
|
||||
expect(at('ozhidanie').map((t) => t.row)).toEqual([0, 1, 2, 4, 5, 6, 7]);
|
||||
expect(text('ozhidanie')).toBe('ОЖИАНИЕ'); // ОЖИ(_)АНИЕ without Д
|
||||
expect(at('ozhidanie').every((t) => t.col === 3)).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps only the ЭРУДИТ tiles as the persistent set', () => {
|
||||
expect(ERUDIT_KEYS.size).toBe(6);
|
||||
expect(ERUDIT_KEYS.has('3-1')).toBe(true); // Р crossing
|
||||
expect(ERUDIT_KEYS.has('3-3')).toBe(true); // Д crossing
|
||||
expect(SPLASH_TILES).toHaveLength(20); // 6 + 7 + 7 unique cells
|
||||
});
|
||||
});
|
||||
|
||||
describe('splash schedule', () => {
|
||||
const s = splashSchedule();
|
||||
|
||||
it('lays ЭРУДИТ first, completing by WORD_MS, then checks', () => {
|
||||
const r = reveals(s.prefix);
|
||||
expect(r).toHaveLength(6);
|
||||
expect(r.map((x) => x.atMs)).toEqual([...r.map((x) => x.atMs)].sort((a, b) => a - b));
|
||||
expect(r[0].atMs).toBe(0);
|
||||
expect(r[r.length - 1].atMs).toBeLessThan(WORD_MS);
|
||||
expect(s.prefix.at(-1)).toEqual({ atMs: WORD_MS, kind: 'check' });
|
||||
expect(s.prefixMs).toBe(WORD_MS);
|
||||
});
|
||||
|
||||
it('reveals both vertical words per cycle and never re-lays a shared tile', () => {
|
||||
const r = reveals(s.cycle);
|
||||
expect(r).toHaveLength(14); // 7 + 7
|
||||
expect(r.some((x) => x.key === '3-1' || x.key === '3-3')).toBe(false);
|
||||
// The first vertical reveal waits out the leading pause.
|
||||
expect(r[0].atMs).toBe(PAUSE_MS);
|
||||
});
|
||||
|
||||
it('checks at each word boundary and clears at the end of the cycle', () => {
|
||||
const checks = s.cycle.filter((x) => x.kind === 'check').map((x) => x.atMs);
|
||||
expect(checks).toEqual([PAUSE_MS + WORD_MS, PAUSE_MS + 2 * WORD_MS + PAUSE_MS]);
|
||||
const last = s.cycle.at(-1)!;
|
||||
expect(last.kind).toBe('clear');
|
||||
expect(last.atMs).toBe(s.cycleMs);
|
||||
expect(s.cycleMs).toBe(2 * WORD_MS + 3 * PAUSE_MS);
|
||||
});
|
||||
|
||||
it('honours overridden timings', () => {
|
||||
const fast = splashSchedule({ wordMs: 700, pauseMs: 100 });
|
||||
expect(fast.prefixMs).toBe(700);
|
||||
expect(fast.cycleMs).toBe(2 * 700 + 3 * 100);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user