Wrap existing physical hand cards with local name search, printed-category filters and saved/name/category display order. Preserve card IDs, actions and inspectors, keep required decisions outside filtering, and recompute results from current owned cards. Provide count, clear filters, keyboard scrolling, separate empty/no-match states and internal guidance. Filters reset on leaving the hand tab, changing seats or refreshing; saved order is never modified. Correct the stale Harass availability note to reflect connected allocation. Six focused tests cover normalized literal search, duplicate physical IDs, canonical expansion categories, stable immutable sorting, current holdings, JSON restoration, full-table per-seat privacy and empty setup messaging. Independent review found no material issue. Types, lint, all 4996 offline tests and production build pass on stable source. No HTTP, auth, schema or persistence implementation changed, so HTTP tests were not repeated. Browser QA used a new two-player Basic Harkonnen/Easy Atreides game created through genuine setup. Search/category/reset, named inspector and all three sort orders worked. An active no-match filter updated from 0 of 2 to 0 of 4 when a real auction purchase and Harkonnen bonus arrived. Leaving the hand tab and refreshing restored all four cards in their original saved order while the second auction remained at a human decision. Enter in search performed no game action. The internal guide worked by keyboard. At 390px, controls were 44px high with 16px text and 14px labels, form width/scroll width 339px; the focused overflowing card row responded to ArrowRight. Normal viewport restored. Screenshot capture timed out; full visual/screen-reader acceptance and complete expansion/card-effect combinations remain open. All 1538 opening rooms, 3258 seats and auxiliary rows remain unchanged. SQLite integrity and foreign keys pass. Only one isolated QA game, its human seat and entry receipt were added; the AI remains in game state. Healthy server reused without restart, migration, reset or automation. No AI strategy or mode/publication gate changed. Private evidence: 20260921-hand-browsing. Verified source tree: 39dce28efa52dd66baf019baaf8610e6795601b3b569ac4ac0b6f405bffc1d96
56 lines
3.4 KiB
TypeScript
56 lines
3.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { baseDeck, treacheryDeck, type Card } from '../game/cards';
|
|
import { richeseCards } from '../game/richese-cards';
|
|
import { ecazTreacheryCards } from '../game/ecaz-cards';
|
|
import { browseHand } from '../game/hand-browsing';
|
|
|
|
const all = [...treacheryDeck(['ix', 'choam', 'ecaz']), ...richeseCards(), ...ecazTreacheryCards()];
|
|
const named = (name: string) => {
|
|
const card = all.find(c => c.name === name);
|
|
assert.ok(card, name);
|
|
return card;
|
|
};
|
|
|
|
void test('name search keeps each matching physical card and treats input as literal text', () => {
|
|
const shields = all.filter(c => c.name === 'Shield');
|
|
assert.ok(shields.length > 1);
|
|
assert.deepEqual(browseHand(shields, ' SHiELD ', 'all', 'hand'), shields);
|
|
assert.equal(new Set(browseHand(shields, 'shield', 'all', 'name').map(c => c.id)).size, shields.length);
|
|
assert.deepEqual(browseHand(all, '.*', 'all', 'hand'), []);
|
|
const accented: Card[] = [{ id: 'visible-accented', name: 'Épreuve', kind: 'special' }];
|
|
assert.deepEqual(browseHand(accented, 'epreuve', 'all', 'hand'), accented);
|
|
});
|
|
|
|
void test('printed categories include canonical expansion identities without redefining battle roles', () => {
|
|
const hand = ['Weirding Way', 'Chemistry', 'Stone Burner', 'Portable Snooper', 'Harass & Withdraw', 'Juice of Sapho'].map(named);
|
|
assert.deepEqual(browseHand(hand, '', 'weapon', 'hand').map(c => c.name), ['Weirding Way', 'Stone Burner']);
|
|
assert.deepEqual(browseHand(hand, '', 'defense', 'hand').map(c => c.name), ['Chemistry', 'Portable Snooper']);
|
|
assert.deepEqual(browseHand(hand, '', 'special', 'hand').map(c => c.name), ['Harass & Withdraw', 'Juice of Sapho']);
|
|
assert.deepEqual(browseHand(hand, 'snooper', 'weapon', 'name'), []);
|
|
assert.equal(browseHand(hand, 'snooper', 'defense', 'name')[0].id, named('Portable Snooper').id);
|
|
});
|
|
|
|
void test('sorting changes a copied display list, preserves duplicate order, and can restore saved hand order', () => {
|
|
const shields = baseDeck().filter(c => c.name === 'Shield');
|
|
assert.ok(shields.length >= 2);
|
|
const hand = Object.freeze([named('Harass & Withdraw'), shields[1], named('Lasgun'), shields[0]].map(c => Object.freeze({ ...c })));
|
|
const before = JSON.stringify(hand);
|
|
assert.deepEqual(browseHand(hand, '', 'all', 'name').map(c => c.name), ['Harass & Withdraw', 'Lasgun', 'Shield', 'Shield']);
|
|
assert.deepEqual(browseHand(hand, '', 'all', 'category').map(c => c.name), ['Lasgun', 'Shield', 'Shield', 'Harass & Withdraw']);
|
|
assert.deepEqual(browseHand(hand, '', 'defense', 'name').map(c => c.id), [shields[1].id, shields[0].id]);
|
|
assert.deepEqual(browseHand(hand, '', 'all', 'hand'), hand);
|
|
assert.equal(JSON.stringify(hand), before);
|
|
});
|
|
|
|
void test('current owned cards drive results after a draw, removal and JSON restore, with no cached private holdings', () => {
|
|
const first = [named('Shield')];
|
|
assert.deepEqual(browseHand(first, 'sapho', 'all', 'hand'), []);
|
|
const acquired = [...first, named('Juice of Sapho')];
|
|
assert.equal(browseHand(acquired, 'sapho', 'all', 'hand')[0].id, named('Juice of Sapho').id);
|
|
const removed = acquired.filter(c => c.name !== 'Juice of Sapho');
|
|
assert.deepEqual(browseHand(removed, 'sapho', 'all', 'hand'), []);
|
|
assert.deepEqual(browseHand(JSON.parse(JSON.stringify(acquired)), 'sapho', 'all', 'name'), [named('Juice of Sapho')]);
|
|
assert.deepEqual(browseHand([], '', 'all', 'category'), []);
|
|
});
|