Stage 7 (wip): tests + UI CI

- Vitest units: board replay, placement machine, premium parity, i18n key parity, FlatBuffers codec round-trips (19 tests)
- Playwright smoke (mock transport): guest -> lobby -> board -> place tile -> preview
- ui-test.yaml workflow: check/unit/build + bundle-size budget (67.5KB gzip < 100KB) + chromium e2e
- gateway transcode tests for games.list (seat display_name), pass, hint
- backend integration test for game.ListForAccount
This commit is contained in:
Ilia Denisov
2026-06-03 00:55:38 +02:00
parent 65689b903f
commit 0284c9b83a
10 changed files with 512 additions and 0 deletions
@@ -139,3 +139,96 @@ func TestDomainErrorSurfacesBackendCode(t *testing.T) {
t.Fatalf("DomainCode = (%q, %v), want (not_your_turn, true)", code, ok)
}
}
// gameActionPayload builds a GameActionRequest payload (pass / resign / hint / etc.).
func gameActionPayload(gameID string) []byte {
b := flatbuffers.NewBuilder(32)
gid := b.CreateString(gameID)
fb.GameActionRequestStart(b)
fb.GameActionRequestAddGameId(b, gid)
b.Finish(fb.GameActionRequestEnd(b))
return b.FinishedBytes()
}
func TestGamesListRoundTripDecodesSeatNames(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-User-ID"); got != "u-9" {
t.Errorf("X-User-ID = %q, want u-9", got)
}
if r.URL.Path != "/api/v1/user/games" {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"games":[{"id":"g-1","variant":"english","status":"active","players":2,"to_move":0,"seats":[{"seat":0,"account_id":"u-9","display_name":"You","score":10},{"seat":1,"account_id":"a-1","display_name":"Ann","score":7}]}]}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
op, _ := reg.Lookup(transcode.MsgGamesList)
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-9"})
if err != nil {
t.Fatalf("handler: %v", err)
}
gl := fb.GetRootAsGameList(payload, 0)
if gl.GamesLength() != 1 {
t.Fatalf("games length = %d, want 1", gl.GamesLength())
}
var g fb.GameView
gl.Games(&g, 0)
if string(g.Id()) != "g-1" {
t.Errorf("game id = %q, want g-1", g.Id())
}
var seat fb.SeatView
g.Seats(&seat, 1)
if string(seat.DisplayName()) != "Ann" {
t.Errorf("seat display name = %q, want Ann", seat.DisplayName())
}
}
func TestPassRoundTrip(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/user/games/g-2/pass" {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"move":{"player":0,"action":"pass"},"game":{"id":"g-2","status":"active","seats":[{"seat":0,"account_id":"u-1","display_name":"You"}]}}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
op, _ := reg.Lookup(transcode.MsgGamePass)
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1", Payload: gameActionPayload("g-2")})
if err != nil {
t.Fatalf("handler: %v", err)
}
mr := fb.GetRootAsMoveResult(payload, 0)
var move fb.MoveRecord
mr.Move(&move)
if string(move.Action()) != "pass" {
t.Errorf("action = %q, want pass", move.Action())
}
}
func TestHintRoundTrip(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/user/games/g-3/hint" {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"move":{"player":0,"action":"play","words":["CAT"],"score":9},"hints_remaining":2}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
op, _ := reg.Lookup(transcode.MsgGameHint)
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1", Payload: gameActionPayload("g-3")})
if err != nil {
t.Fatalf("handler: %v", err)
}
hr := fb.GetRootAsHintResult(payload, 0)
if hr.HintsRemaining() != 2 {
t.Errorf("hints remaining = %d, want 2", hr.HintsRemaining())
}
var move fb.MoveRecord
hr.Move(&move)
if move.Score() != 9 {
t.Errorf("hint move score = %d, want 9", move.Score())
}
}