diff --git a/src/functional_tests/test_room_role_select.py b/src/functional_tests/test_room_role_select.py index f304325..d4c9cf5 100644 --- a/src/functional_tests/test_room_role_select.py +++ b/src/functional_tests/test_room_role_select.py @@ -1,4 +1,5 @@ import os +import unittest from django.conf import settings as django_settings from django.test import tag @@ -542,6 +543,253 @@ class RoleSelectTest(FunctionalTest): ) +class RoleSelectTrayTest(FunctionalTest): + """After confirming a role pick, the role card enters the tray grid and + the tray opens to reveal it. + + Grid conventions: + Portrait — grid-auto-flow:column, 8 explicit rows. Position 0 = row 1, col 1 + (topmost-leftmost). New items prepended → grid grows rightward. + Landscape — grid-auto-flow:row, 8 explicit columns, anchored to bottom. + Position 0 = row 1 (bottom), col 1. New items prepended → grid + grows upward. + + "Dummy objects" in T2/T3 are prior gamers' role cards already placed in the + tray. They are injected via JS because no backend mechanism exists yet to + populate the tray for a specific gamer's view. + """ + + EMAILS = [ + "slot1@test.io", "slot2@test.io", "slot3@test.io", + "slot4@test.io", "slot5@test.io", "slot6@test.io", + ] + ALL_ROLES = ["PC", "BC", "SC", "AC", "NC", "EC"] + + def setUp(self): + super().setUp() + Applet.objects.get_or_create( + slug="new-game", defaults={"name": "New Game", "context": "gameboard"} + ) + Applet.objects.get_or_create( + slug="my-games", defaults={"name": "My Games", "context": "gameboard"} + ) + + def _make_room(self, active_slot=1): + """Room in ROLE_SELECT with all 6 seats created. Seats 1..(active_slot-1) + already have roles assigned so the active_slot gamer is eligible.""" + founder, _ = User.objects.get_or_create(email=self.EMAILS[0]) + room = Room.objects.create(name="Tray Card Test", owner=founder) + _fill_room_via_orm(room, self.EMAILS) + room.table_status = Room.ROLE_SELECT + room.save() + for slot in room.gate_slots.order_by("slot_number"): + ts = TableSeat.objects.create( + room=room, gamer=slot.gamer, slot_number=slot.slot_number + ) + if slot.slot_number < active_slot: + ts.role = self.ALL_ROLES[slot.slot_number - 1] + ts.save() + return room + + def _select_role(self): + """Open the fan, pick the first card, confirm the guard dialog.""" + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".card-stack[data-state='eligible']" + ) + ).click() + self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select")) + self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click() + self.confirm_guard() + + def _inject_prior_role_cards(self, roles): + """Prepend tray-role-card divs into #id_tray_grid to simulate cards + placed by earlier gamers. roles is oldest-first; the final state has + the most-recent card at position 0 (front of grid).""" + self.browser.execute_script(""" + var grid = document.getElementById('id_tray_grid'); + var roles = arguments[0]; + roles.forEach(function(role) { + var card = document.createElement('div'); + card.className = 'tray-cell tray-role-card'; + card.dataset.role = role; + grid.insertBefore(card, grid.firstChild); + }); + """, roles) + + # ------------------------------------------------------------------ # + # T1 — Portrait, position 1: empty tray, card at row 1 col 1 # + # ------------------------------------------------------------------ # + + @unittest.skip("tray-open-on-role-select not yet implemented") + def test_portrait_first_role_card_enters_grid_position_zero(self): + """Portrait, slot 1: after confirming a role, a .tray-role-card element + appears as the first child of #id_tray_grid (topmost-leftmost cell), and + the tray wrap is at least partially open.""" + self.browser.set_window_size(390, 844) + room = self._make_room(active_slot=1) + self.create_pre_authenticated_session("slot1@test.io") + self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/") + + wrap = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap")) + # Record closed position before selection. + initial_left = self.browser.execute_script( + "return parseInt(arguments[0].style.left, 10) || window.innerWidth", wrap + ) + grid_before = self.browser.execute_script( + "return document.getElementById('id_tray_grid').children.length" + ) + + self._select_role() + + # 1. A .tray-role-card is now in the grid. + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, "#id_tray_grid .tray-role-card" + ) + ) + + # 2. It is the first child — topmost, leftmost in portrait. + is_first = self.browser.execute_script(""" + var card = document.querySelector('#id_tray_grid .tray-role-card'); + return card !== null && card === card.parentElement.firstElementChild; + """) + self.assertTrue(is_first, "Role card should be the first child of #id_tray_grid") + + # 3. Exactly one item was prepended. + grid_after = self.browser.execute_script( + "return document.getElementById('id_tray_grid').children.length" + ) + self.assertEqual(grid_after, grid_before + 1) + + # 4. Tray moved from closed position toward open. + current_left = self.browser.execute_script( + "return parseInt(arguments[0].style.left, 10)", wrap + ) + self.assertLess(current_left, initial_left, + "Tray should have moved left (toward open) after role selection") + + # ------------------------------------------------------------------ # + # T2 — Portrait, position 2: col 1 full, 8th item overflows to col 2 # + # ------------------------------------------------------------------ # + + @unittest.skip("tray-open-on-role-select not yet implemented") + def test_portrait_second_card_prepended_pushes_eighth_item_to_col_2(self): + """Portrait, slot 2: col 1 already holds slot 1's role card (position 0) + plus 7 tray-cells (positions 1-7), filling the column. After slot 2 + confirms, the new card takes position 0; the old position-7 item + (tray-cell 6) moves to col 2, row 1 (position 8).""" + self.browser.set_window_size(390, 844) + room = self._make_room(active_slot=2) + self.create_pre_authenticated_session("slot2@test.io") + self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/") + + # Simulate slot 1's card already placed in the tray. + # Grid starts with 8 tray-cells; injecting 1 role card → 9 items total. + # Col 1: [PC-card, tray-0..tray-6] = 8 (full). Col 2: [tray-7]. + self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap")) + self._inject_prior_role_cards(["PC"]) + + grid_before = self.browser.execute_script( + "return document.getElementById('id_tray_grid').children.length" + ) + self.assertEqual(grid_before, 9, "9 items before: 1 prior card + 8 tray-cells") + + self._select_role() + + # 1. New card is first child. + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, "#id_tray_grid .tray-role-card:first-child" + ) + ) + + # 2. Grid now has 10 items (one more than before). + grid_after = self.browser.execute_script( + "return document.getElementById('id_tray_grid').children.length" + ) + self.assertEqual(grid_after, grid_before + 1) + + # 3. The item now at position 8 (col 2, row 1) is a tray-cell — + # it was the 8th item in col 1 and has been displaced. + displaced = self.browser.execute_script(""" + var grid = document.getElementById('id_tray_grid'); + var el = grid.children[8]; + return el ? el.className : null; + """) + self.assertIsNotNone(displaced) + self.assertIn("tray-cell", displaced) + + # 4. Tray open enough to reveal at least col 1 (left < initial closed pos). + wrap = self.browser.find_element(By.ID, "id_tray_wrap") + left = self.browser.execute_script("return parseInt(arguments[0].style.left, 10)", wrap) + viewport_w = self.browser.execute_script("return window.innerWidth") + self.assertLess(left, viewport_w, + "Tray should be at least partially open after role selection") + + # ------------------------------------------------------------------ # + # T3 — Landscape, position 3: row 1 full, rightmost item enters row 2 # + # ------------------------------------------------------------------ # + + @unittest.skip("tray-open-on-role-select not yet implemented") + def test_landscape_third_card_at_bottom_left_rightmost_overflows_to_row_2(self): + """Landscape, slot 3: row 1 (bottom, 8 cols) already holds 2 prior role + cards + 6 tray-cells. After slot 3 confirms, new card at position 0 + (bottommost-leftmost); old position-7 item enters row 2, col 1 (pos 8).""" + self.browser.set_window_size(844, 390) + room = self._make_room(active_slot=3) + self.create_pre_authenticated_session("slot3@test.io") + self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/") + + # Inject 2 prior role cards (oldest first → newest at grid front). + # Grid: [BC-card(0), PC-card(1), tray-0(2)..tray-7(9)] = 10 items. + # Row 1 (bottom): positions 0-7 = full. Row 2: positions 8-9. + self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap")) + self._inject_prior_role_cards(["PC", "BC"]) + + grid_before = self.browser.execute_script( + "return document.getElementById('id_tray_grid').children.length" + ) + self.assertEqual(grid_before, 10, "10 items before: 2 prior cards + 8 tray-cells") + + wrap = self.browser.find_element(By.ID, "id_tray_wrap") + initial_top = self.browser.execute_script( + "return parseInt(arguments[0].style.top, 10)", wrap + ) + + self._select_role() + + # 1. New card is first child — bottommost-leftmost in landscape. + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, "#id_tray_grid .tray-role-card:first-child" + ) + ) + + # 2. Grid grew by exactly one item. + grid_after = self.browser.execute_script( + "return document.getElementById('id_tray_grid').children.length" + ) + self.assertEqual(grid_after, grid_before + 1) + + # 3. Item at position 8 (row 2, col 1) is a tray-cell — it was the + # rightmost item in row 1 (position 7) and has been displaced upward. + displaced = self.browser.execute_script(""" + var grid = document.getElementById('id_tray_grid'); + var el = grid.children[8]; + return el ? el.className : null; + """) + self.assertIsNotNone(displaced) + self.assertIn("tray-cell", displaced) + + # 4. Tray opened downward — top is less negative (closer to 0) than before. + current_top = self.browser.execute_script( + "return parseInt(arguments[0].style.top, 10)", wrap + ) + self.assertGreater(current_top, initial_top, + "Tray should have moved down (toward open) after role selection") + + @tag('channels') class RoleSelectChannelsTest(ChannelsFunctionalTest): diff --git a/src/static_src/scss/rootvars.scss b/src/static_src/scss/rootvars.scss index 1938ca6..17fdc2d 100644 --- a/src/static_src/scss/rootvars.scss +++ b/src/static_src/scss/rootvars.scss @@ -237,7 +237,7 @@ --priSwp: 221, 206, 149; --secSwp: 148, 150, 103; --terSwp: 102, 92, 67; - --quaSwp: 43, 46, 37; + --quaSwp: 43, 76, 37; // blood (Tyche's Phlegethon) --priBld: 200, 79, 50; --secBld: 177, 63, 52; @@ -418,12 +418,12 @@ --priUser: var(--priCu); /* 46,24,5 — very dark warm brown bg */ --secUser: var(--quiCu); /* 207,173,143 — warm beige text/border */ --terUser: var(--priBpk); /* 214,186,84 — amber gold accent */ - --quaUser: var(--quaAg); /* 195,176,145 — warm tan interactive */ + --quaUser: var(--quiAg); /* 195,176,145 — warm tan interactive */ --quiUser: var(--quaSwp); /* 95,76,45 — deep khaki */ --sixUser: var(--quaCu); /* 171,112,60 — copper mid */ --sepUser: var(--terCu); /* 133,81,36 — deep copper */ --octUser: var(--quaAu); /* 181,154,54 — golden links */ - --ninUser: var(--sixCu); /* 242,216,191 — warm cream highlight */ + --ninUser: var(--sixAg); /* 242,216,191 — warm cream highlight */ --decUser: var(--secKhk); /* 145,126,95 — warm mid-tone */ }