mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-11 03:13:45 +00:00
WordInput: the turn seed fires once per turn and is gated on the connection (regression test for C6), a reconnect blip mid-turn does not reseed over what the player typed, a rejection's suggestion fills the field on click, and the player's own in-turn composition is left alone. GameBoard: the chat pill renders only when onchatopen is passed, which is the actual contract between the bot and online routes — a bot game simply never passes it.
104 lines
2.9 KiB
JavaScript
104 lines
2.9 KiB
JavaScript
// @vitest-environment jsdom
|
|
|
|
// The chat pill in the board's top row only exists for the online screen —
|
|
// a bot game has no chat, so GameBoard simply never receives the handler
|
|
// that draws it. This pins that down as a prop contract rather than
|
|
// something only the online route happens to exercise.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { mount, unmount, flushSync } from 'svelte';
|
|
import { create } from '@bufbuild/protobuf';
|
|
import GameBoard from '../src/lib/components/GameBoard.svelte';
|
|
import { ServerMessageSchema } from '../src/lib/proto/noitu/v1/game_pb.js';
|
|
import { game } from '../src/lib/stores/game.svelte.js';
|
|
import { Status, connection } from '../src/lib/ws/connection.svelte.js';
|
|
|
|
function startGame() {
|
|
game.apply(
|
|
create(ServerMessageSchema, {
|
|
payload: {
|
|
case: 'gameStarted',
|
|
value: {
|
|
openingWord: 'bình yên',
|
|
currentSyllable: 'yên',
|
|
myTurn: true,
|
|
deadlineUnixMs: 1_700_000_020_000n,
|
|
turnSeq: 1,
|
|
turnLimitMs: 20_000,
|
|
players: [{ playerId: 'p1', name: 'Minh', isMe: true, connected: true }],
|
|
turnPlayerId: 'p1'
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
/** @param {Partial<ConstructorParameters<typeof GameBoard>[0]['props']>} [extra] */
|
|
function renderGameBoard(extra = {}) {
|
|
const target = document.createElement('div');
|
|
document.body.appendChild(target);
|
|
const component = mount(GameBoard, {
|
|
target,
|
|
props: {
|
|
onsubmit: () => true,
|
|
onresign: () => {},
|
|
onclaimdeadend: () => {},
|
|
onreportword: () => {},
|
|
...extra
|
|
}
|
|
});
|
|
flushSync();
|
|
return { target, component };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
game.leave();
|
|
connection.status = Status.OPEN;
|
|
// jsdom implements neither; ChainHistory (rendered inside the board) uses
|
|
// both to keep the newest word in view, which is not what this file
|
|
// tests.
|
|
Element.prototype.scrollTo = () => {};
|
|
globalThis.ResizeObserver ??= class {
|
|
observe() {}
|
|
disconnect() {}
|
|
};
|
|
startGame();
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
connection.status = Status.CLOSED;
|
|
});
|
|
|
|
describe('the chat pill', () => {
|
|
it('is absent when the caller passes no onchatopen, as a bot game does', () => {
|
|
const { target, component } = renderGameBoard();
|
|
|
|
expect(target.querySelector('[data-testid="chat-pill"]')).toBeNull();
|
|
unmount(component);
|
|
});
|
|
|
|
it('appears once a handler is passed, as the online room does', () => {
|
|
const { target, component } = renderGameBoard({ onchatopen: () => {} });
|
|
|
|
expect(target.querySelector('[data-testid="chat-pill"]')).not.toBeNull();
|
|
unmount(component);
|
|
});
|
|
|
|
it('calls the handler on click and shows the unread count', () => {
|
|
let opened = 0;
|
|
const { target, component } = renderGameBoard({
|
|
onchatopen: () => opened++,
|
|
chatUnread: 3
|
|
});
|
|
|
|
/** @type {HTMLButtonElement | null} */
|
|
const pill = target.querySelector('[data-testid="chat-pill"]');
|
|
expect(pill?.textContent).toContain('3');
|
|
pill?.click();
|
|
|
|
expect(opened).toBe(1);
|
|
unmount(component);
|
|
});
|
|
});
|