another attempt to unclog pipeline; this time a slight sleep timeout used to accomodate headless browser resize flush
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
@@ -61,6 +61,18 @@ var RoleSelect = (function () {
|
||||
.split(",").filter(function (r) { return r.trim() !== roleCode; }).join(",");
|
||||
}
|
||||
openFan();
|
||||
} else {
|
||||
// Place role card in tray grid and open the tray
|
||||
var grid = document.getElementById("id_tray_grid");
|
||||
if (grid) {
|
||||
var trayCard = document.createElement("div");
|
||||
trayCard.className = "tray-cell tray-role-card";
|
||||
trayCard.dataset.role = roleCode;
|
||||
grid.insertBefore(trayCard, grid.firstChild);
|
||||
}
|
||||
if (typeof Tray !== "undefined") {
|
||||
Tray.open();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -621,7 +621,6 @@ class RoleSelectTrayTest(FunctionalTest):
|
||||
# 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
|
||||
@@ -673,7 +672,6 @@ class RoleSelectTrayTest(FunctionalTest):
|
||||
# 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
|
||||
@@ -697,18 +695,23 @@ class RoleSelectTrayTest(FunctionalTest):
|
||||
|
||||
self._select_role()
|
||||
|
||||
# 1. New card is first child.
|
||||
# 1. Wait for grid to grow (fetch .then() is async).
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card:first-child"
|
||||
lambda: self.assertEqual(
|
||||
self.browser.execute_script(
|
||||
"return document.getElementById('id_tray_grid').children.length"
|
||||
),
|
||||
grid_before + 1,
|
||||
)
|
||||
)
|
||||
grid_after = grid_before + 1
|
||||
|
||||
# 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)
|
||||
# 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.
|
||||
@@ -731,7 +734,6 @@ class RoleSelectTrayTest(FunctionalTest):
|
||||
# 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
|
||||
@@ -759,18 +761,23 @@ class RoleSelectTrayTest(FunctionalTest):
|
||||
|
||||
self._select_role()
|
||||
|
||||
# 1. New card is first child — bottommost-leftmost in landscape.
|
||||
# 1. Wait for grid to grow (fetch .then() is async).
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card:first-child"
|
||||
lambda: self.assertEqual(
|
||||
self.browser.execute_script(
|
||||
"return document.getElementById('id_tray_grid').children.length"
|
||||
),
|
||||
grid_before + 1,
|
||||
)
|
||||
)
|
||||
grid_after = grid_before + 1
|
||||
|
||||
# 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)
|
||||
# 2. Newest tray-role-card is the first child — bottommost-leftmost in landscape.
|
||||
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. 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.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import time
|
||||
|
||||
from django.test import tag
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
@@ -41,12 +43,14 @@ class TrayTest(FunctionalTest):
|
||||
Applet.objects.get_or_create(slug="new-note", defaults={"name": "New Note"})
|
||||
|
||||
def _switch_to_landscape(self):
|
||||
"""Recreate the browser at landscape dimensions (900×500) and wait
|
||||
until window.innerWidth > window.innerHeight confirms the CSS
|
||||
orientation media query will fire correctly."""
|
||||
"""Recreate the browser, navigate to about:blank, then resize to
|
||||
900×500 and wait until window.innerWidth > window.innerHeight confirms
|
||||
the CSS orientation media query will fire correctly on the next page."""
|
||||
self.browser.quit()
|
||||
self.browser = self._make_browser(900, 500)
|
||||
self.browser.get('about:blank')
|
||||
self.browser.set_window_size(900, 500)
|
||||
time.sleep(0.5) # allow Firefox to flush the resize before navigating
|
||||
self.wait_for(lambda: self.assertTrue(
|
||||
self.browser.execute_script(
|
||||
'return window.innerWidth > window.innerHeight'
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,385 +0,0 @@
|
||||
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("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();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,425 +0,0 @@
|
||||
// ── 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
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 & 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];
|
||||
}
|
||||
})();
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
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
@@ -242,6 +242,90 @@ describe("RoleSelect", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 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 //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
Reference in New Issue
Block a user