maybe don't delete collectstatic static/tests/ dir

This commit is contained in:
Disco DeDisco
2026-03-29 23:39:03 -04:00
parent 224f5e2ad0
commit fb782cf5ef
14 changed files with 15894 additions and 206 deletions

View File

@@ -163,6 +163,9 @@ var RoleSelect = (function () {
var invSlot = document.getElementById("id_inv_role_card"); var invSlot = document.getElementById("id_inv_role_card");
if (invSlot) invSlot.innerHTML = ""; if (invSlot) invSlot.innerHTML = "";
// Force-close tray instantly so it never obscures the next player's card-stack.
if (typeof Tray !== "undefined") Tray.forceClose();
var stack = document.querySelector(".card-stack[data-user-slots]"); var stack = document.querySelector(".card-stack[data-user-slots]");
if (stack) { if (stack) {
// Sync starter-roles from server so the fan reflects actual DB state // Sync starter-roles from server so the fan reflects actual DB state

View File

@@ -8,6 +8,11 @@ class JasmineTest(FunctionalTest):
def check_results(): def check_results():
result = self.browser.find_element(By.CSS_SELECTOR, ".jasmine-overall-result") result = self.browser.find_element(By.CSS_SELECTOR, ".jasmine-overall-result")
self.assertIn("0 failures", result.text) if "0 failures" not in result.text:
failures = self.browser.find_elements(
By.CSS_SELECTOR, ".jasmine-failed .jasmine-description"
)
detail = "\n".join(f.text for f in failures) if failures else "(no detail)"
self.fail(f"{result.text}\nFailing specs:\n{detail}")
self.wait_for(check_results) self.wait_for(check_results)

View File

@@ -547,23 +547,14 @@ class RoleSelectTrayTest(FunctionalTest):
"""After confirming a role pick, the role card enters the tray grid and """After confirming a role pick, the role card enters the tray grid and
the tray opens to reveal it. the tray opens to reveal it.
Grid conventions: Portrait — card lands at the topmost grid square (first child, row 1 col 1).
Portrait — grid-auto-flow:column, 8 explicit rows. Position 0 = row 1, col 1 Landscape — card lands at the leftmost grid square (first child, 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 = [ EMAILS = [
"slot1@test.io", "slot2@test.io", "slot3@test.io", "slot1@test.io", "slot2@test.io", "slot3@test.io",
"slot4@test.io", "slot5@test.io", "slot6@test.io", "slot4@test.io", "slot5@test.io", "slot6@test.io",
] ]
ALL_ROLES = ["PC", "BC", "SC", "AC", "NC", "EC"]
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@@ -574,21 +565,17 @@ class RoleSelectTrayTest(FunctionalTest):
slug="my-games", defaults={"name": "My Games", "context": "gameboard"} slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
) )
def _make_room(self, active_slot=1): def _make_room(self):
"""Room in ROLE_SELECT with all 6 seats created. Seats 1..(active_slot-1) """Room in ROLE_SELECT with all 6 seats created, slot 1 eligible."""
already have roles assigned so the active_slot gamer is eligible."""
founder, _ = User.objects.get_or_create(email=self.EMAILS[0]) founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
room = Room.objects.create(name="Tray Card Test", owner=founder) room = Room.objects.create(name="Tray Card Test", owner=founder)
_fill_room_via_orm(room, self.EMAILS) _fill_room_via_orm(room, self.EMAILS)
room.table_status = Room.ROLE_SELECT room.table_status = Room.ROLE_SELECT
room.save() room.save()
for slot in room.gate_slots.order_by("slot_number"): for slot in room.gate_slots.order_by("slot_number"):
ts = TableSeat.objects.create( TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number 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 return room
def _select_role(self): def _select_role(self):
@@ -602,199 +589,76 @@ class RoleSelectTrayTest(FunctionalTest):
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click() self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard() 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 # # T1 — Portrait: role card at topmost grid square, tray opens #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_portrait_first_role_card_enters_grid_position_zero(self): def test_portrait_role_card_enters_topmost_grid_square(self):
"""Portrait, slot 1: after confirming a role, a .tray-role-card element """Portrait: after confirming a role, a .tray-role-card is the first child
appears as the first child of #id_tray_grid (topmost-leftmost cell), and of #id_tray_grid (topmost cell) and the tray is open."""
the tray wrap is at least partially open."""
self.browser.set_window_size(390, 844) self.browser.set_window_size(390, 844)
room = self._make_room(active_slot=1) room = self._make_room()
self.create_pre_authenticated_session("slot1@test.io") self.create_pre_authenticated_session("slot1@test.io")
self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/") 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")) 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() self._select_role()
# 1. A .tray-role-card is now in the grid. # Card appears in the grid.
self.wait_for( self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card" By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
) )
) )
# 2. It is the first child — topmost, leftmost in portrait. # It is the first child — topmost in portrait.
is_first = self.browser.execute_script(""" is_first = self.browser.execute_script("""
var card = document.querySelector('#id_tray_grid .tray-role-card'); var card = document.querySelector('#id_tray_grid .tray-role-card');
return card !== null && card === card.parentElement.firstElementChild; return card !== null && card === card.parentElement.firstElementChild;
""") """)
self.assertTrue(is_first, "Role card should be the first child of #id_tray_grid") self.assertTrue(is_first, "Role card should be the first child of #id_tray_grid")
# 3. Exactly one item was prepended. # Tray is open.
grid_after = self.browser.execute_script( self.assertTrue(
"return document.getElementById('id_tray_grid').children.length" self.browser.execute_script("return Tray.isOpen()"),
"Tray should be open after role selection"
) )
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 # # T2 — Landscape: role card at leftmost grid square, tray opens #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_portrait_second_card_prepended_pushes_eighth_item_to_col_2(self): @tag('two-browser')
"""Portrait, slot 2: col 1 already holds slot 1's role card (position 0) def test_landscape_role_card_enters_leftmost_grid_square(self):
plus 7 tray-cells (positions 1-7), filling the column. After slot 2 """Landscape: after confirming a role, a .tray-role-card is the first child
confirms, the new card takes position 0; the old position-7 item of #id_tray_grid (leftmost cell) and the tray is open."""
(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. Wait for grid to grow (fetch .then() is async).
self.wait_for(
lambda: self.assertEqual(
self.browser.execute_script(
"return document.getElementById('id_tray_grid').children.length"
),
grid_before + 1,
)
)
grid_after = grid_before + 1
# 2. New tray-role-card is the first child.
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, "Newest role card should be first child")
# 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 #
# ------------------------------------------------------------------ #
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) self.browser.set_window_size(844, 390)
room = self._make_room(active_slot=3) room = self._make_room()
self.create_pre_authenticated_session("slot3@test.io") self.create_pre_authenticated_session("slot1@test.io")
self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/") 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.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() self._select_role()
# 1. Wait for grid to grow (fetch .then() is async). # Card appears in the grid.
self.wait_for( self.wait_for(
lambda: self.assertEqual( lambda: self.browser.find_element(
self.browser.execute_script( By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
"return document.getElementById('id_tray_grid').children.length"
),
grid_before + 1,
) )
) )
grid_after = grid_before + 1
# 2. Newest tray-role-card is the first child — bottommost-leftmost in landscape. # It is the first child — leftmost in landscape.
is_first = self.browser.execute_script(""" is_first = self.browser.execute_script("""
var card = document.querySelector('#id_tray_grid .tray-role-card'); var card = document.querySelector('#id_tray_grid .tray-role-card');
return card !== null && card === card.parentElement.firstElementChild; return card !== null && card === card.parentElement.firstElementChild;
""") """)
self.assertTrue(is_first, "Newest role card should be first child") self.assertTrue(is_first, "Role card should be the first child of #id_tray_grid")
# 3. Item at position 8 (row 2, col 1) is a tray-cell — it was the # Tray is open.
# rightmost item in row 1 (position 7) and has been displaced upward. self.assertTrue(
displaced = self.browser.execute_script(""" self.browser.execute_script("return Tray.isOpen()"),
var grid = document.getElementById('id_tray_grid'); "Tray should be open after role selection"
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') @tag('channels')
@@ -877,11 +741,10 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
return b return b
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Test 5 — Turn passes to next gamer via WebSocket after selection # # Test 5 — Tray closes on turn advance (portrait) #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@unittest.skip("tray obscures card-stack after role selection — needs tray-close-on-turn-change + grid ordering fixes first") def _make_turn_test_room(self):
def test_turn_passes_after_selection(self):
founder, _ = User.objects.get_or_create(email="founder@test.io") founder, _ = User.objects.get_or_create(email="founder@test.io")
User.objects.get_or_create(email="friend@test.io") User.objects.get_or_create(email="friend@test.io")
room = Room.objects.create(name="Turn Test", owner=founder) room = Room.objects.create(name="Turn Test", owner=founder)
@@ -895,47 +758,80 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
TableSeat.objects.create( TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=slot.slot_number, room=room, gamer=slot.gamer, slot_number=slot.slot_number,
) )
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/" return f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
# 1. Founder (slot 1) — eligible def test_portrait_tray_closes_on_turn_advance(self):
"""Portrait: after selecting a role the tray opens and the role card lands
in the topmost grid square. When turn_changed arrives via WS, the tray
force-closes so the next player's card-stack is not obscured."""
self.browser.set_window_size(390, 844)
room_url = self._make_turn_test_room()
self.create_pre_authenticated_session("founder@test.io") self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url) self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element( self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']" By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)) ))
# 2. Friend (slot 2) — ineligible in second browser # Select a role — tray opens and card lands in topmost square.
self.browser2 = self._make_browser2("friend@test.io") self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
try: self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser2.get(room_url) self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.wait_for(lambda: self.browser2.find_element( self.confirm_guard()
By.CSS_SELECTOR, ".card-stack[data-state='ineligible']"
))
# 3. Founder picks a role self.wait_for(lambda: self.assertTrue(
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click() self.browser.execute_script("return Tray.isOpen()")
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() is_first = self.browser.execute_script("""
self.confirm_guard() 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 first child (topmost) of grid")
# 4. Friend's stack becomes eligible via WebSocket — no page refresh # Turn advances via WS — seat 2 becomes active.
self.wait_for(lambda: self.browser2.find_element( self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']" By.CSS_SELECTOR, ".table-seat.active[data-slot='2']"
)) ))
# 5. Founder's stack is STILL ineligible — WS must not re-enable it # Tray must be closed after turn_changed.
self.wait_for(lambda: self.assertEqual( self.assertFalse(
self.browser.find_element( self.browser.execute_script("return Tray.isOpen()"),
By.CSS_SELECTOR, ".card-stack" "Tray should be closed after turn advances"
).get_attribute("data-state"), )
"ineligible",
)) def test_landscape_tray_closes_on_turn_advance(self):
"""Landscape: same sequence — role card at leftmost grid square; tray
closes when turn_changed arrives."""
self.browser.set_window_size(844, 390)
room_url = self._make_turn_test_room()
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").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()
self.wait_for(lambda: self.assertTrue(
self.browser.execute_script("return Tray.isOpen()")
))
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 first child (leftmost) of grid")
# Turn advances via WS — seat 2 becomes active.
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-seat.active[data-slot='2']"
))
# Tray must be closed after turn_changed.
self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
"Tray should be closed after turn advances"
)
# 6. Clicking founder's stack does not reopen the fan
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
self.wait_for(lambda: self.assertEqual(
len(self.browser.find_elements(By.ID, "id_role_select")), 0
))
finally:
self.browser2.quit()

21
src/static/tests/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2026 The Jasmine developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,477 @@
describe("RoleSelect", () => {
let testDiv;
beforeEach(() => {
testDiv = document.createElement("div");
testDiv.innerHTML = `
<div class="room-page"
data-select-role-url="/epic/room/test-uuid/select-role">
</div>
<div id="id_inv_role_card"></div>
`;
document.body.appendChild(testDiv);
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true })
);
// Default stub: auto-confirm so existing card-click tests pass unchanged.
// The click-guard integration describe overrides this with a capturing spy.
window.showGuard = (_anchor, _msg, onConfirm) => onConfirm && onConfirm();
});
afterEach(() => {
RoleSelect.closeFan();
testDiv.remove();
delete window.showGuard;
});
// ------------------------------------------------------------------ //
// openFan() //
// ------------------------------------------------------------------ //
describe("openFan()", () => {
it("creates .role-select-backdrop in the DOM", () => {
RoleSelect.openFan();
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
});
it("creates #id_role_select inside the backdrop", () => {
RoleSelect.openFan();
expect(document.getElementById("id_role_select")).not.toBeNull();
});
it("renders exactly 6 .card elements", () => {
RoleSelect.openFan();
const cards = document.querySelectorAll("#id_role_select .card");
expect(cards.length).toBe(6);
});
it("does not open a second backdrop if already open", () => {
RoleSelect.openFan();
RoleSelect.openFan();
expect(document.querySelectorAll(".role-select-backdrop").length).toBe(1);
});
});
// ------------------------------------------------------------------ //
// closeFan() //
// ------------------------------------------------------------------ //
describe("closeFan()", () => {
it("removes .role-select-backdrop from the DOM", () => {
RoleSelect.openFan();
RoleSelect.closeFan();
expect(document.querySelector(".role-select-backdrop")).toBeNull();
});
it("removes #id_role_select from the DOM", () => {
RoleSelect.openFan();
RoleSelect.closeFan();
expect(document.getElementById("id_role_select")).toBeNull();
});
it("does not throw if no fan is open", () => {
expect(() => RoleSelect.closeFan()).not.toThrow();
});
});
// ------------------------------------------------------------------ //
// Card interactions //
// ------------------------------------------------------------------ //
describe("card interactions", () => {
beforeEach(() => {
RoleSelect.openFan();
});
it("mouseenter adds .flipped to the card", () => {
const card = document.querySelector("#id_role_select .card");
card.dispatchEvent(new MouseEvent("mouseenter"));
expect(card.classList.contains("flipped")).toBe(true);
});
it("mouseleave removes .flipped from the card", () => {
const card = document.querySelector("#id_role_select .card");
card.dispatchEvent(new MouseEvent("mouseenter"));
card.dispatchEvent(new MouseEvent("mouseleave"));
expect(card.classList.contains("flipped")).toBe(false);
});
it("clicking a card closes the fan", () => {
const card = document.querySelector("#id_role_select .card");
card.click();
expect(document.getElementById("id_role_select")).toBeNull();
});
it("clicking a card appends a .card to #id_inv_role_card", () => {
const card = document.querySelector("#id_role_select .card");
card.click();
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
});
it("clicking a card POSTs to the select_role URL", () => {
const card = document.querySelector("#id_role_select .card");
card.click();
expect(window.fetch).toHaveBeenCalledWith(
"/epic/room/test-uuid/select-role",
jasmine.objectContaining({ method: "POST" })
);
});
it("clicking a card results in exactly one card in inventory", () => {
const card = document.querySelector("#id_role_select .card");
card.click();
expect(document.querySelectorAll("#id_inv_role_card .card").length).toBe(1);
});
});
// ------------------------------------------------------------------ //
// Backdrop click //
// ------------------------------------------------------------------ //
describe("backdrop click", () => {
it("closes the fan", () => {
RoleSelect.openFan();
document.querySelector(".role-select-backdrop").click();
expect(document.getElementById("id_role_select")).toBeNull();
});
it("does not add a card to inventory", () => {
RoleSelect.openFan();
document.querySelector(".role-select-backdrop").click();
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
});
});
// ------------------------------------------------------------------ //
// room:roles_revealed event //
// ------------------------------------------------------------------ //
describe("room:roles_revealed event", () => {
let reloadCalled;
beforeEach(() => {
reloadCalled = false;
RoleSelect.setReload(() => { reloadCalled = true; });
});
afterEach(() => {
RoleSelect.setReload(() => { window.location.reload(); });
});
it("triggers a page reload", () => {
window.dispatchEvent(new CustomEvent("room:roles_revealed", { detail: {} }));
expect(reloadCalled).toBe(true);
});
});
// ------------------------------------------------------------------ //
// room:turn_changed event //
// ------------------------------------------------------------------ //
describe("room:turn_changed event", () => {
let stack;
beforeEach(() => {
// Six table seats, slot 1 starts active
for (let i = 1; i <= 6; i++) {
const seat = document.createElement("div");
seat.className = "table-seat" + (i === 1 ? " active" : "");
seat.dataset.slot = String(i);
seat.innerHTML = '<div class="seat-card-arc"></div>';
testDiv.appendChild(seat);
}
stack = document.createElement("div");
stack.className = "card-stack";
stack.dataset.state = "ineligible";
stack.dataset.userSlots = "1";
stack.dataset.starterRoles = "";
testDiv.appendChild(stack);
});
it("calls Tray.forceClose() on turn change", () => {
spyOn(Tray, "forceClose");
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
expect(Tray.forceClose).toHaveBeenCalled();
});
it("moves .active to the newly active seat", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
expect(
testDiv.querySelector(".table-seat.active").dataset.slot
).toBe("2");
});
it("removes .active from the previously active seat", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
expect(
testDiv.querySelector(".table-seat[data-slot='1']").classList.contains("active")
).toBe(false);
});
it("sets data-state to eligible when active_slot matches user slot", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 1 }
}));
expect(stack.dataset.state).toBe("eligible");
});
it("sets data-state to ineligible when active_slot does not match", () => {
stack.dataset.state = "eligible";
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
expect(stack.dataset.state).toBe("ineligible");
});
it("clicking stack opens fan when newly eligible", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 1 }
}));
stack.click();
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
});
it("clicking stack does not open fan when ineligible", () => {
// Make eligible first (adds listener), then flip back to ineligible
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 1 }
}));
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
stack.click();
expect(document.querySelector(".role-select-backdrop")).toBeNull();
});
});
// ------------------------------------------------------------------ //
// Tray card placement after successful role selection //
// ------------------------------------------------------------------ //
// The tray-role-card is created in the fetch .then() callback, so //
// these tests are async — await Promise.resolve() flushes the //
// microtask queue before asserting. //
// ------------------------------------------------------------------ //
describe("tray card after successful role selection", () => {
let grid, guardConfirm;
beforeEach(() => {
// Minimal tray grid matching room.html structure
grid = document.createElement("div");
grid.id = "id_tray_grid";
for (let i = 0; i < 8; i++) {
const cell = document.createElement("div");
cell.className = "tray-cell";
grid.appendChild(cell);
}
testDiv.appendChild(grid);
spyOn(Tray, "open");
// Capturing guard spy — holds onConfirm so we can fire it per-test
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
(anchor, message, onConfirm) => { guardConfirm = onConfirm; }
);
RoleSelect.openFan();
document.querySelector("#id_role_select .card").click();
});
it("prepends a .tray-role-card to #id_tray_grid on success", async () => {
guardConfirm();
await Promise.resolve();
expect(grid.querySelector(".tray-role-card")).not.toBeNull();
});
it("tray-role-card is the first child of #id_tray_grid", async () => {
guardConfirm();
await Promise.resolve();
expect(grid.firstElementChild.classList.contains("tray-role-card")).toBe(true);
});
it("tray-role-card carries the selected role as data-role", async () => {
guardConfirm();
await Promise.resolve();
const trayCard = grid.querySelector(".tray-role-card");
expect(trayCard.dataset.role).toBeTruthy();
});
it("calls Tray.open() on success", async () => {
guardConfirm();
await Promise.resolve();
expect(Tray.open).toHaveBeenCalled();
});
it("does not prepend a tray-role-card on server rejection", async () => {
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: false })
);
guardConfirm();
await Promise.resolve();
expect(grid.querySelector(".tray-role-card")).toBeNull();
});
it("does not call Tray.open() on server rejection", async () => {
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: false })
);
guardConfirm();
await Promise.resolve();
expect(Tray.open).not.toHaveBeenCalled();
});
it("grid grows by exactly 1 on success", async () => {
const before = grid.children.length;
guardConfirm();
await Promise.resolve();
expect(grid.children.length).toBe(before + 1);
});
});
// ------------------------------------------------------------------ //
// click-guard integration //
// ------------------------------------------------------------------ //
// NOTE: cascade prevention (outside-click on backdrop not closing the //
// fan while the guard is active) relies on the guard portal's capture- //
// phase stopPropagation, which lives in base.html and requires //
// integration testing. The callback contract is fully covered below. //
// ------------------------------------------------------------------ //
describe("click-guard integration", () => {
let guardAnchor, guardMessage, guardConfirm, guardDismiss;
beforeEach(() => {
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
(anchor, message, onConfirm, onDismiss) => {
guardAnchor = anchor;
guardMessage = message;
guardConfirm = onConfirm;
guardDismiss = onDismiss;
}
);
RoleSelect.openFan();
});
describe("clicking a card", () => {
let card;
beforeEach(() => {
card = document.querySelector("#id_role_select .card");
card.click();
});
it("calls window.showGuard", () => {
expect(window.showGuard).toHaveBeenCalled();
});
it("passes the card element as the anchor", () => {
expect(guardAnchor).toBe(card);
});
it("message contains the role name", () => {
const roleName = card.querySelector(".card-role-name").textContent.trim();
expect(guardMessage).toContain(roleName);
});
it("message contains the role code", () => {
expect(guardMessage).toContain(card.dataset.role);
});
it("message contains a <br>", () => {
expect(guardMessage).toContain("<br>");
});
it("does not immediately close the fan", () => {
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
});
it("does not immediately POST to the select_role URL", () => {
expect(window.fetch).not.toHaveBeenCalled();
});
it("adds .flipped to the card", () => {
expect(card.classList.contains("flipped")).toBe(true);
});
it("adds .guard-active to the card", () => {
expect(card.classList.contains("guard-active")).toBe(true);
});
it("mouseleave does not remove .flipped while guard is active", () => {
card.dispatchEvent(new MouseEvent("mouseleave"));
expect(card.classList.contains("flipped")).toBe(true);
});
});
describe("confirming the guard (OK)", () => {
let card;
beforeEach(() => {
card = document.querySelector("#id_role_select .card");
card.click();
guardConfirm();
});
it("removes .guard-active from the card", () => {
expect(card.classList.contains("guard-active")).toBe(false);
});
it("closes the fan", () => {
expect(document.querySelector(".role-select-backdrop")).toBeNull();
});
it("POSTs to the select_role URL", () => {
expect(window.fetch).toHaveBeenCalledWith(
"/epic/room/test-uuid/select-role",
jasmine.objectContaining({ method: "POST" })
);
});
it("appends a .card to #id_inv_role_card", () => {
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
});
});
describe("dismissing the guard (NVM or outside click)", () => {
let card;
beforeEach(() => {
card = document.querySelector("#id_role_select .card");
card.click();
guardDismiss();
});
it("removes .guard-active from the card", () => {
expect(card.classList.contains("guard-active")).toBe(false);
});
it("removes .flipped from the card", () => {
expect(card.classList.contains("flipped")).toBe(false);
});
it("leaves the fan open", () => {
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
});
it("does not POST to the select_role URL", () => {
expect(window.fetch).not.toHaveBeenCalled();
});
it("does not add a card to inventory", () => {
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
});
it("restores normal mouseleave behaviour on the card", () => {
card.dispatchEvent(new MouseEvent("mouseenter"));
card.dispatchEvent(new MouseEvent("mouseleave"));
expect(card.classList.contains("flipped")).toBe(false);
});
});
});
});

57
src/static/tests/Spec.js Normal file
View File

@@ -0,0 +1,57 @@
console.log("Spec.js is loading");
describe("GameArray JavaScript", () => {
const inputId= "id_text";
const errorClass = "invalid-feedback";
const inputSelector = `#${inputId}`;
const errorSelector = `.${errorClass}`;
let testDiv;
let textInput;
let errorMsg;
beforeEach(() => {
console.log("beforeEach");
testDiv = document.createElement("div");
testDiv.innerHTML = `
<form>
<input
id="${inputId}"
name="text"
class="form-control form-control-lg is-invalid"
placeholder="Enter a to-do item"
value="Value as submitted"
aria-describedby="id_text_feedback"
required
/>
<div id="id_text_feedback" class="${errorClass}">An error message</div>
</form>
`;
document.body.appendChild(testDiv);
textInput = document.querySelector(inputSelector);
errorMsg = document.querySelector(errorSelector);
});
afterEach(() => {
testDiv.remove();
});
it("should have a useful html fixture", () => {
console.log("in test 1");
expect(errorMsg.checkVisibility()).toBe(true);
});
it("should hide error message on input", () => {
console.log("in test 2");
initialize(inputSelector);
textInput.dispatchEvent(new InputEvent("input"));
expect(errorMsg.checkVisibility()).toBe(false);
});
it("should not hide error message before event is fired", () => {
console.log("in test 3");
initialize(inputSelector);
expect(errorMsg.checkVisibility()).toBe(true);
});
});

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Disco DeDisco">
<meta name="robots" content="noindex, nofollow">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/css/bootstrap.min.css"/>
<link rel="stylesheet" href="lib/jasmine-6.0.1/jasmine.css">
<title>Jasmine Spec Runner</title>
<link rel="stylesheet" href="lib/jasmine.css">
<!-- Jasmine -->
<script src="lib/jasmine-6.0.1/jasmine.js"></script>
<script src="lib/jasmine-6.0.1/jasmine-html.js"></script>
<script src="lib/jasmine-6.0.1/boot0.js"></script>
<!-- spec files -->
<script src="Spec.js"></script>
<script src="RoleSelectSpec.js"></script>
<script src="TraySpec.js"></script>
<!-- src files -->
<script src="/static/apps/dashboard/dashboard.js"></script>
<script src="/static/apps/epic/role-select.js"></script>
<script src="/static/apps/epic/tray.js"></script>
<!-- Jasmine env config (optional) -->
<script src="lib/jasmine-6.0.1/boot1.js"></script>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,425 @@
// ── TraySpec.js ───────────────────────────────────────────────────────────────
//
// Unit specs for tray.js — the per-seat, per-room slide-out panel anchored
// to the right edge of the viewport.
//
// DOM contract assumed by the module:
// #id_tray_wrap — outermost container; JS sets style.left for positioning
// #id_tray_btn — the drawer-handle button
// #id_tray — the tray panel (hidden by default)
//
// Public API under test:
// Tray.init() — compute bounds, apply vertical bounds, attach listeners
// Tray.open() — reveal tray, animate wrap to minLeft
// Tray.close() — hide tray, animate wrap to maxLeft
// Tray.isOpen() — state predicate
// Tray.reset() — restore initial state (for afterEach)
//
// Drag model: tray follows pointer in real-time; position persists on release.
// Any leftward drag opens the tray.
// Drag > 10px suppresses the subsequent click event.
//
// ─────────────────────────────────────────────────────────────────────────────
describe("Tray", () => {
let btn, tray, wrap;
beforeEach(() => {
wrap = document.createElement("div");
wrap.id = "id_tray_wrap";
btn = document.createElement("button");
btn.id = "id_tray_btn";
tray = document.createElement("div");
tray.id = "id_tray";
tray.style.display = "none";
wrap.appendChild(btn);
document.body.appendChild(wrap);
document.body.appendChild(tray);
Tray._testSetLandscape(false); // force portrait regardless of window size
Tray.init();
});
afterEach(() => {
Tray.reset();
wrap.remove();
tray.remove();
});
// ---------------------------------------------------------------------- //
// open() //
// ---------------------------------------------------------------------- //
describe("open()", () => {
it("makes #id_tray visible", () => {
Tray.open();
expect(tray.style.display).not.toBe("none");
});
it("adds .open to #id_tray_btn", () => {
Tray.open();
expect(btn.classList.contains("open")).toBe(true);
});
it("sets wrap left to minLeft (0)", () => {
Tray.open();
expect(wrap.style.left).toBe("0px");
});
it("calling open() twice does not duplicate .open", () => {
Tray.open();
Tray.open();
const openCount = btn.className.split(" ").filter(c => c === "open").length;
expect(openCount).toBe(1);
});
});
// ---------------------------------------------------------------------- //
// close() //
// ---------------------------------------------------------------------- //
describe("close()", () => {
beforeEach(() => Tray.open());
it("hides #id_tray after slide + snap both complete", () => {
Tray.close();
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
wrap.dispatchEvent(new Event("animationend"));
expect(tray.style.display).toBe("none");
});
it("adds .snap to wrap after slide transition completes", () => {
Tray.close();
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
expect(wrap.classList.contains("snap")).toBe(true);
});
it("removes .snap from wrap once animationend fires", () => {
Tray.close();
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
wrap.dispatchEvent(new Event("animationend"));
expect(wrap.classList.contains("snap")).toBe(false);
});
it("removes .open from #id_tray_btn", () => {
Tray.close();
expect(btn.classList.contains("open")).toBe(false);
});
it("sets wrap left to maxLeft", () => {
Tray.close();
expect(parseInt(wrap.style.left, 10)).toBeGreaterThan(0);
});
it("does not throw if already closed", () => {
Tray.close();
expect(() => Tray.close()).not.toThrow();
});
});
// ---------------------------------------------------------------------- //
// isOpen() //
// ---------------------------------------------------------------------- //
describe("isOpen()", () => {
it("returns false by default", () => {
expect(Tray.isOpen()).toBe(false);
});
it("returns true after open()", () => {
Tray.open();
expect(Tray.isOpen()).toBe(true);
});
it("returns false after close()", () => {
Tray.open();
Tray.close();
expect(Tray.isOpen()).toBe(false);
});
});
// ---------------------------------------------------------------------- //
// Click when closed — wobble wrap, do not open //
// ---------------------------------------------------------------------- //
describe("clicking btn when closed", () => {
it("adds .wobble to wrap", () => {
btn.click();
expect(wrap.classList.contains("wobble")).toBe(true);
});
it("does not open the tray", () => {
btn.click();
expect(Tray.isOpen()).toBe(false);
});
it("removes .wobble once animationend fires on wrap", () => {
btn.click();
wrap.dispatchEvent(new Event("animationend"));
expect(wrap.classList.contains("wobble")).toBe(false);
});
});
// ---------------------------------------------------------------------- //
// Click when open — close, no wobble //
// ---------------------------------------------------------------------- //
describe("clicking btn when open", () => {
beforeEach(() => Tray.open());
it("closes the tray", () => {
btn.click();
expect(Tray.isOpen()).toBe(false);
});
it("does not add .wobble", () => {
btn.click();
expect(wrap.classList.contains("wobble")).toBe(false);
});
});
// ---------------------------------------------------------------------- //
// Drag interaction — continuous positioning //
// ---------------------------------------------------------------------- //
describe("drag interaction", () => {
function simulateDrag(deltaX) {
const startX = 800;
btn.dispatchEvent(new PointerEvent("pointerdown", { clientX: startX, bubbles: true }));
btn.dispatchEvent(new PointerEvent("pointermove", { clientX: startX + deltaX, bubbles: true }));
btn.dispatchEvent(new PointerEvent("pointerup", { clientX: startX + deltaX, bubbles: true }));
}
it("dragging left opens the tray", () => {
simulateDrag(-60);
expect(Tray.isOpen()).toBe(true);
});
it("any leftward drag opens the tray", () => {
simulateDrag(-20);
expect(Tray.isOpen()).toBe(true);
});
it("dragging right does not open the tray", () => {
simulateDrag(100);
expect(Tray.isOpen()).toBe(false);
});
it("drag > 10px suppresses the subsequent click", () => {
simulateDrag(-60);
btn.click(); // should be swallowed — tray stays open
expect(Tray.isOpen()).toBe(true);
});
it("does not add .wobble during drag", () => {
simulateDrag(-60);
expect(wrap.classList.contains("wobble")).toBe(false);
});
});
// ---------------------------------------------------------------------- //
// Landscape mode — Y-axis drag, top-positioned wrap //
// ---------------------------------------------------------------------- //
describe("landscape mode", () => {
// Re-init in landscape after the portrait init from outer beforeEach.
beforeEach(() => {
Tray.reset();
Tray._testSetLandscape(true);
Tray.init();
});
function simulateDragY(deltaY) {
const startY = 50;
btn.dispatchEvent(new PointerEvent("pointerdown", { clientY: startY, clientX: 0, bubbles: true }));
btn.dispatchEvent(new PointerEvent("pointermove", { clientY: startY + deltaY, clientX: 0, bubbles: true }));
btn.dispatchEvent(new PointerEvent("pointerup", { clientY: startY + deltaY, clientX: 0, bubbles: true }));
}
// ── open() in landscape ─────────────────────────────────────────── //
describe("open()", () => {
it("makes #id_tray visible", () => {
Tray.open();
expect(tray.style.display).not.toBe("none");
});
it("adds .open to #id_tray_btn", () => {
Tray.open();
expect(btn.classList.contains("open")).toBe(true);
});
it("positions wrap via style.top, not style.left", () => {
Tray.open();
expect(wrap.style.top).not.toBe("");
expect(wrap.style.left).toBe("");
});
});
// ── close() in landscape ────────────────────────────────────────── //
describe("close()", () => {
beforeEach(() => Tray.open());
it("closes the tray (display not toggled in landscape)", () => {
Tray.close();
expect(Tray.isOpen()).toBe(false);
});
it("removes .open from #id_tray_btn", () => {
Tray.close();
expect(btn.classList.contains("open")).toBe(false);
});
it("closed top is less than open top (wrap slides up to close)", () => {
const openTop = parseInt(wrap.style.top, 10);
Tray.close();
const closedTop = parseInt(wrap.style.top, 10);
expect(closedTop).toBeLessThan(openTop);
});
it("adds .snap to wrap after top transition completes", () => {
Tray.close();
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "top" }));
expect(wrap.classList.contains("snap")).toBe(true);
});
it("removes .snap from wrap once animationend fires", () => {
Tray.close();
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "top" }));
wrap.dispatchEvent(new Event("animationend"));
expect(wrap.classList.contains("snap")).toBe(false);
});
});
// ── drag — Y axis ──────────────────────────────────────────────── //
describe("drag interaction", () => {
it("dragging down opens the tray", () => {
simulateDragY(100);
expect(Tray.isOpen()).toBe(true);
});
it("dragging up does not open the tray", () => {
simulateDragY(-100);
expect(Tray.isOpen()).toBe(false);
});
it("drag > 10px downward suppresses subsequent click", () => {
simulateDragY(100);
btn.click(); // should be swallowed — tray stays open
expect(Tray.isOpen()).toBe(true);
});
it("does not set style.left (Y axis only)", () => {
simulateDragY(100);
expect(wrap.style.left).toBe("");
});
it("does not add .wobble during drag", () => {
simulateDragY(100);
expect(wrap.classList.contains("wobble")).toBe(false);
});
});
// ── click when closed — wobble, no open ───────────────────────── //
describe("clicking btn when closed", () => {
it("adds .wobble to wrap", () => {
btn.click();
expect(wrap.classList.contains("wobble")).toBe(true);
});
it("does not open the tray", () => {
btn.click();
expect(Tray.isOpen()).toBe(false);
});
});
// ── click when open — close ────────────────────────────────────── //
describe("clicking btn when open", () => {
beforeEach(() => Tray.open());
it("closes the tray", () => {
btn.click();
expect(Tray.isOpen()).toBe(false);
});
});
// ── init positions wrap at closed (top) ────────────────────────── //
it("init sets wrap to closed position (top < 0 or = maxTop)", () => {
// After landscape init with no real elements, _maxTop = -(wrapH_fallback - handleH_fallback)
// which will be negative. Wrap starts off-screen above.
const top = parseInt(wrap.style.top, 10);
expect(top).toBeLessThan(0);
});
// ── resize closes landscape tray ─────────────────────────────── //
describe("resize closes the tray", () => {
it("closes when landscape tray is open", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
it("removes .open from btn on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(btn.classList.contains("open")).toBe(false);
});
it("resets wrap to closed top position on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(parseInt(wrap.style.top, 10)).toBeLessThan(0);
});
it("does not re-open a closed tray on resize", () => {
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
});
});
// ---------------------------------------------------------------------- //
// window resize — portrait //
// ---------------------------------------------------------------------- //
describe("window resize (portrait)", () => {
it("closes the tray when open", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
it("removes .open from btn on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(btn.classList.contains("open")).toBe(false);
});
it("hides the tray panel on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(tray.style.display).toBe("none");
});
it("resets wrap to closed left position on resize", () => {
Tray.open();
expect(wrap.style.left).toBe("0px");
window.dispatchEvent(new Event("resize"));
expect(parseInt(wrap.style.left, 10)).toBeGreaterThan(0);
});
it("does not re-open a closed tray on resize", () => {
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
});
});

View File

@@ -0,0 +1,68 @@
/*
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2026 The Jasmine developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
/**
This file starts the process of "booting" Jasmine. It initializes Jasmine,
makes its globals available, and creates the env. This file should be loaded
after `jasmine.js` and `jasmine_html.js`, but before `boot1.js` or any project
source files or spec files are loaded.
*/
(function() {
const jasmineRequire = window.jasmineRequire || require('./jasmine.js');
/**
* ## Require &amp; Instantiate
*
* Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
*/
const jasmine = jasmineRequire.core(jasmineRequire),
global = jasmine.getGlobal();
global.jasmine = jasmine;
/**
* Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
*/
jasmineRequire.html(jasmine);
/**
* Create the Jasmine environment. This is used to run all specs in a project.
*/
const env = jasmine.getEnv();
/**
* ## The Global Interface
*
* Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
*/
const jasmineInterface = jasmineRequire.interface(jasmine, env);
/**
* Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
*/
for (const property in jasmineInterface) {
global[property] = jasmineInterface[property];
}
})();

View File

@@ -0,0 +1,64 @@
/*
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2026 The Jasmine developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
/**
This file finishes 'booting' Jasmine, performing all of the necessary
initialization before executing the loaded environment and all of a project's
specs. This file should be loaded after `boot0.js` but before any project
source files or spec files are loaded. Thus this file can also be used to
customize Jasmine for a project.
If a project is using Jasmine via the standalone distribution, this file can
be customized directly. If you only wish to configure the Jasmine env, you
can load another file that calls `jasmine.getEnv().configure({...})`
after `boot0.js` is loaded and before this file is loaded.
*/
(function() {
const env = jasmine.getEnv();
const urls = new jasmine.HtmlReporterV2Urls();
/**
* Configures Jasmine based on the current set of query parameters. This
* supports all parameters set by the HTML reporter as well as
* spec=partialPath, which filters out specs whose paths don't contain the
* parameter.
*/
env.configure(urls.configFromCurrentUrl());
const currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
// The HTML reporter needs to be set up here so it can access the DOM. Other
// reporters can be added at any time before env.execute() is called.
const htmlReporter = new jasmine.HtmlReporterV2({ env, urls });
env.addReporter(htmlReporter);
env.execute();
};
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -188,6 +188,14 @@ describe("RoleSelect", () => {
testDiv.appendChild(stack); testDiv.appendChild(stack);
}); });
it("calls Tray.forceClose() on turn change", () => {
spyOn(Tray, "forceClose");
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
expect(Tray.forceClose).toHaveBeenCalled();
});
it("moves .active to the newly active seat", () => { it("moves .active to the newly active seat", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", { window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 } detail: { active_slot: 2 }