"""US-02 (join by code) e2e: a second browser tab joins the kitchen. The join form normalises the typed code (uppercase, five characters — the confusable mapping `I L O U` is the server's, not the client's), remembers it in `sessionStorage` (a reload rejoins fast) and opens `/ws/{code}`; the seat/spectate decision is the server's (the `docs/PROTOCOL.md` close-code table). This suite covers: two contexts seeing each other's seat (the phase's "two tabs in one lobby see each other's seat" criterion), a bad code (unknown code → `room_gone` → the error screen without a same-code retry), a full room (a fresh token on a `max_players=2` room → `room_full` → "try another room"), and the last-code prefill after a reload. All of it keyboard-first. """ from __future__ import annotations from typing import TYPE_CHECKING import pytest from playwright.sync_api import expect from conftest import VIEWPORT, app_screen, load_app, open_two_pages if TYPE_CHECKING: from playwright.sync_api import Browser, Page pytestmark = pytest.mark.e2e def _join_ui(page: Page, code: str) -> None: """The keyboard-first join: focus the code field, type, Enter (submit).""" page.locator("#join-code").focus() page.keyboard.type(code) page.keyboard.press("Enter") def test_join_by_code_two_contexts_see_each_others_seats( browser: Browser, round_server ) -> None: """A creates (name "Ada"), B joins by the typed code; *both* pages show both seats — A with the crown, B with the server's fallback name — and B's page shows A's colour chip (the 4-colour palette).""" url = round_server.url context_a, page_a, context_b, page_b = open_two_pages(browser, url) try: load_app(page_a, url) load_app(page_b, url) page_a.evaluate("() => window.__undercooked.createLobby({ name: 'Ada' })") expect(page_a.locator("#room-code")).to_be_visible(timeout=20000) code = page_a.locator("#room-code").inner_text() _join_ui(page_b, code) expect(page_b.locator("#room-code")).to_have_text(code, timeout=20000) assert app_screen(page_b) == "lobby" # B's page: two seats — Ada (crown) and B's own row (the fallback # name, the colour chip, marked data-you). expect(page_b.locator("#seat-list")).to_contain_text("Ada") you_b = page_b.locator("#seat-list li[data-you]") expect(you_b).to_contain_text("Cook #2") expect(you_b.locator(".chip")).to_be_visible() expect(page_b.locator("#seat-list li:has(.crown)")).to_contain_text("Ada") # A's page: B's join is a lobby re-broadcast — the second seat # row is there too. expect(page_a.locator("#seat-list")).to_contain_text("Cook #2") assert page_a.locator("#seat-list li").count() == 2 finally: context_a.close() context_b.close() def test_a_bad_code_is_a_friendly_error_without_retry( browser: Browser, round_server ) -> None: """An unminted code (a Crockford-valid `ZZZZZ`): the server answers `err room_gone` + close `4401` — the error screen's copy names the situation, offers *no* same-code retry (the room is gone) and the *Back to landing* button works.""" url = round_server.url context = browser.new_context(viewport=VIEWPORT) page = context.new_page() try: load_app(page, url) _join_ui(page, "ZZZZZ") expect(page.locator("#error-title")).to_have_text("That kitchen is gone", timeout=20000) assert app_screen(page) == "error" # No same-code retry affordance. assert page.locator("#error-retry").count() == 0 # …but the code is shown for support, and back-to-landing works. expect(page.locator("#error-code")).to_contain_text("ZZZZZ") page.get_by_role("button", name="Back to landing").click() assert app_screen(page) == "landing" finally: context.close() def test_joining_a_full_room_is_a_friendly_error( browser: Browser, small_server ) -> None: """A `max_players=2` room with two seats: a third fresh token is a `room_full` refusal (close `4402`) — the error screen's copy points at another room, and the two seated tabs are untouched.""" url = small_server.url context_a, page_a, context_b, page_b = open_two_pages(browser, url) context_c = browser.new_context(viewport=VIEWPORT) page_c = context_c.new_page() try: load_app(page_a, url) load_app(page_b, url) load_app(page_c, url) page_a.evaluate("() => window.__undercooked.createLobby()") expect(page_a.locator("#room-code")).to_be_visible(timeout=20000) code = page_a.locator("#room-code").inner_text() _join_ui(page_b, code) expect(page_b.locator("#room-code")).to_have_text(code, timeout=20000) _join_ui(page_c, code) expect(page_c.locator("#error-title")).to_have_text("That kitchen is full", timeout=20000) assert page_c.locator("#error-retry").count() == 0 expect(page_c.locator("#error-detail")).to_contain_text("another") # The two seated tabs are still in the lobby, both seats intact. assert app_screen(page_a) == "lobby" assert app_screen(page_b) == "lobby" assert page_a.locator("#seat-list li").count() == 2 finally: context_a.close() context_b.close() context_c.close() def test_the_last_code_prefills_after_reload_and_normalises( browser: Browser, round_server ) -> None: """Typed lowercase, normalised to uppercase on submit, remembered in `sessionStorage` — a reload pre-fills the join form (the task's last-code prefill).""" url = round_server.url context = browser.new_context(viewport=VIEWPORT) page = context.new_page() try: load_app(page, url) _join_ui(page, "k7xyz") # → normalised to K7XYZ (an unminted code) # K7XYZ is not a real room here: the error screen is the proof # the code left the field. expect(page.locator("#error-code")).to_contain_text("K7XYZ", timeout=20000) page.get_by_role("button", name="Back to landing").click() load_app(page, url) # the reload assert page.locator("#join-code").input_value() == "K7XYZ" assert page.evaluate("sessionStorage.getItem('uc.lastCode')") == "K7XYZ" finally: context.close() def test_the_join_form_is_keyboard_operable(browser: Browser, round_server) -> None: """Tab order into the join form, type with the keyboard, submit with Enter (no pointer anywhere). `abcde` is an unminted Crockford code, so the Enter submit ends on the error screen — the proof the frame left the field through the keyboard alone.""" url = round_server.url context = browser.new_context(viewport=VIEWPORT) page = context.new_page() try: load_app(page, url) page.keyboard.press("Tab") # → the Create button assert page.evaluate("document.activeElement.id") == "create-lobby" page.keyboard.press("Tab") # → the code field assert page.evaluate("document.activeElement.id") == "join-code" page.keyboard.type("abcde") page.keyboard.press("Enter") expect(page.locator("#error-code")).to_contain_text("ABCDE", timeout=20000) finally: context.close()