added type='button' to both guard portal btns so firefox won't normalize to type='submit'; fixed several FTs for new click-guard functionality on Role card select & room gear menu DEL & BYE btns; several restorations to landscape breakpoint incl. logged-ion display_name, copyright info; provided title to room_scroll.html; a slurry of other minor fixes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
@@ -104,11 +104,26 @@ var RoleSelect = (function () {
|
|||||||
card.classList.add("flipped");
|
card.classList.add("flipped");
|
||||||
});
|
});
|
||||||
card.addEventListener("mouseleave", function () {
|
card.addEventListener("mouseleave", function () {
|
||||||
card.classList.remove("flipped");
|
if (!card.classList.contains("guard-active")) {
|
||||||
|
card.classList.remove("flipped");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
card.addEventListener("click", function (e) {
|
card.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
selectRole(role.code, card);
|
card.classList.add("flipped");
|
||||||
|
card.classList.add("guard-active");
|
||||||
|
window.showGuard(
|
||||||
|
card,
|
||||||
|
"Start round 1 as<br>" + role.name + " (" + role.code + ") …?",
|
||||||
|
function () { // confirm
|
||||||
|
card.classList.remove("guard-active");
|
||||||
|
selectRole(role.code, card);
|
||||||
|
},
|
||||||
|
function () { // dismiss (NVM / outside click)
|
||||||
|
card.classList.remove("guard-active");
|
||||||
|
card.classList.remove("flipped");
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
modal.appendChild(card);
|
modal.appendChild(card);
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ class FunctionalTest(StaticLiveServerTestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def confirm_guard(self, browser=None):
|
||||||
|
b = browser or self.browser
|
||||||
|
def _click():
|
||||||
|
btn = b.find_element(By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes")
|
||||||
|
b.execute_script("arguments[0].click()", btn)
|
||||||
|
self.wait_for(_click)
|
||||||
|
|
||||||
@wait
|
@wait
|
||||||
def wait_to_be_logged_in(self, email):
|
def wait_to_be_logged_in(self, email):
|
||||||
self.browser.find_element(By.CSS_SELECTOR, "#id_logout"),
|
self.browser.find_element(By.CSS_SELECTOR, "#id_logout"),
|
||||||
@@ -199,6 +206,13 @@ class ChannelsFunctionalTest(ChannelsLiveServerTestCase):
|
|||||||
raise e
|
raise e
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
def confirm_guard(self, browser=None):
|
||||||
|
b = browser or self.browser
|
||||||
|
def _click():
|
||||||
|
btn = b.find_element(By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes")
|
||||||
|
b.execute_script("arguments[0].click()", btn)
|
||||||
|
self.wait_for(_click)
|
||||||
|
|
||||||
def create_pre_authenticated_session(self, email):
|
def create_pre_authenticated_session(self, email):
|
||||||
if self.test_server:
|
if self.test_server:
|
||||||
session_key = create_session_on_server(self.test_server, email)
|
session_key = create_session_on_server(self.test_server, email)
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ class GatekeeperTest(FunctionalTest):
|
|||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-danger")
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-danger")
|
||||||
).click()
|
).click()
|
||||||
|
self.confirm_guard()
|
||||||
|
|
||||||
self.wait_for(lambda: self.assertEqual(
|
self.wait_for(lambda: self.assertEqual(
|
||||||
self.browser.current_url, self.live_server_url + "/gameboard/"
|
self.browser.current_url, self.live_server_url + "/gameboard/"
|
||||||
@@ -248,6 +249,7 @@ class GatekeeperTest(FunctionalTest):
|
|||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon")
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon")
|
||||||
).click()
|
).click()
|
||||||
|
self.confirm_guard()
|
||||||
|
|
||||||
self.wait_for(lambda: self.assertEqual(
|
self.wait_for(lambda: self.assertEqual(
|
||||||
self.browser.current_url, self.live_server_url + "/gameboard/"
|
self.browser.current_url, self.live_server_url + "/gameboard/"
|
||||||
|
|||||||
@@ -57,5 +57,6 @@ class LoginTest(FunctionalTest):
|
|||||||
self.wait_to_be_logged_in(email=TEST_EMAIL)
|
self.wait_to_be_logged_in(email=TEST_EMAIL)
|
||||||
|
|
||||||
self.browser.find_element(By.CSS_SELECTOR, "#id_logout").click()
|
self.browser.find_element(By.CSS_SELECTOR, "#id_logout").click()
|
||||||
|
self.confirm_guard()
|
||||||
|
|
||||||
self.wait_to_be_logged_out(email=TEST_EMAIL)
|
self.wait_to_be_logged_out(email=TEST_EMAIL)
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ class RoleSelectTest(FunctionalTest):
|
|||||||
|
|
||||||
# 5. Click first card to select it
|
# 5. Click first card to select it
|
||||||
cards[0].click()
|
cards[0].click()
|
||||||
|
self.confirm_guard()
|
||||||
|
|
||||||
# 6. Modal closes
|
# 6. Modal closes
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
@@ -436,6 +437,7 @@ class RoleSelectTest(FunctionalTest):
|
|||||||
).click()
|
).click()
|
||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
|
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.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||||
|
self.confirm_guard()
|
||||||
|
|
||||||
# No WS — only the JS fix can make this transition happen
|
# No WS — only the JS fix can make this transition happen
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
@@ -475,6 +477,7 @@ class RoleSelectTest(FunctionalTest):
|
|||||||
).click()
|
).click()
|
||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
|
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.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||||
|
self.confirm_guard()
|
||||||
|
|
||||||
# Wait for fan to close (selectRole closes it synchronously)
|
# Wait for fan to close (selectRole closes it synchronously)
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
@@ -588,6 +591,7 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
|||||||
self.browser2.find_element(By.CSS_SELECTOR, ".card-stack").click()
|
self.browser2.find_element(By.CSS_SELECTOR, ".card-stack").click()
|
||||||
self.wait_for(lambda: self.browser2.find_element(By.ID, "id_role_select"))
|
self.wait_for(lambda: self.browser2.find_element(By.ID, "id_role_select"))
|
||||||
self.browser2.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
self.browser2.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||||
|
self.confirm_guard(browser=self.browser2)
|
||||||
|
|
||||||
# 3. Watcher's seat arc moves to slot 2 — no page refresh
|
# 3. Watcher's seat arc moves to slot 2 — no page refresh
|
||||||
self.wait_for(lambda: self.browser.find_element(
|
self.wait_for(lambda: self.browser.find_element(
|
||||||
@@ -656,6 +660,7 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
|||||||
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
|
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
|
||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
|
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.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||||
|
self.confirm_guard()
|
||||||
|
|
||||||
# 4. Friend's stack becomes eligible via WebSocket — no page refresh
|
# 4. Friend's stack becomes eligible via WebSocket — no page refresh
|
||||||
self.wait_for(lambda: self.browser2.find_element(
|
self.wait_for(lambda: self.browser2.find_element(
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// In landscape: shift gear btn and applet menus left of the footer right sidebar
|
// In landscape: shift gear btn and applet menus left of the footer right sidebar
|
||||||
@media (orientation: landscape) and (max-width: 1023px) {
|
@media (orientation: landscape) and (max-width: 1440px) {
|
||||||
$sidebar-w: 4rem;
|
$sidebar-w: 4rem;
|
||||||
|
|
||||||
.gameboard-page,
|
.gameboard-page,
|
||||||
|
|||||||
@@ -173,8 +173,8 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-width: 1023px) {
|
@media (orientation: landscape) and (max-width: 1440px) {
|
||||||
$sidebar-w: 4rem;
|
$sidebar-w: 5rem;
|
||||||
|
|
||||||
// ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
|
// ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
|
||||||
body {
|
body {
|
||||||
@@ -198,34 +198,49 @@ body {
|
|||||||
.container-fluid {
|
.container-fluid {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
max-height: 700px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.5rem;
|
gap: 1rem;
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
|
|
||||||
> form { margin-left: 0; flex-shrink: 0; order: -1; } // logout at top
|
> form { flex-shrink: 0; order: -1; } // logout above brand
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand { order: 1; } // brand at bottom
|
|
||||||
|
|
||||||
.navbar-brand h1 {
|
.navbar-brand h1 {
|
||||||
writing-mode: vertical-rl;
|
writing-mode: vertical-rl;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
font-size: 1rem;
|
font-size: 1.2rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-right: 2.75rem;
|
// margin-right: 3.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
order: 1; // brand at bottom
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-text,
|
|
||||||
.navbar-link { display: none; }
|
.navbar-link { display: none; }
|
||||||
|
|
||||||
|
.navbar-text {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: auto 0;
|
||||||
|
|
||||||
|
.navbar-label { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
border-width: 0.125rem;
|
border-width: 0.125rem;
|
||||||
margin-left: 0.75rem;
|
// margin-left: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login form: email input can't fit in narrow sidebar
|
// Login form: email input can't fit in narrow sidebar
|
||||||
@@ -275,7 +290,7 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
gap: 1.5rem;
|
gap: 3rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
@@ -285,7 +300,16 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-container { display: none; }
|
.footer-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: rgba(var(--secUser), 0.5);
|
||||||
|
|
||||||
|
br { display: block; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,15 +317,16 @@ body {
|
|||||||
body .container {
|
body .container {
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0 0 0.25rem 0;
|
padding: 0 0 0.25rem 0;
|
||||||
|
|
||||||
.navbar-brand h1 {
|
.navbar-brand h1 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
border-width: 0.125rem;
|
border-width: 0.125rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,25 +344,25 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) and (max-height: 700px) {
|
// @media (min-width: 1024px) and (max-height: 700px) {
|
||||||
body .container .navbar {
|
// body .container .navbar {
|
||||||
padding: 0.5rem 0;
|
// padding: 0.5rem 0;
|
||||||
|
|
||||||
.navbar-brand h1 {
|
// .navbar-brand h1 {
|
||||||
font-size: 1.4rem;
|
// font-size: 1.4rem;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
#id_footer {
|
// #id_footer {
|
||||||
height: 3.5rem;
|
// height: 3.5rem;
|
||||||
padding: 0.7rem 1rem;
|
// padding: 0.7rem 1rem;
|
||||||
gap: 0.35rem;
|
// gap: 0.35rem;
|
||||||
|
|
||||||
#id_footer_nav a {
|
// #id_footer_nav a {
|
||||||
font-size: 1.2rem;
|
// font-size: 1.2rem;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
#id_footer {
|
#id_footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -386,9 +411,41 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-container {
|
.footer-container {
|
||||||
|
br { display: none; }
|
||||||
|
|
||||||
small {
|
small {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#id_guard_portal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: rgba(var(--tooltip-bg), 0.5);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||||
|
box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-message {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(var(--secUser), 0.9);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-width: 1023px) {
|
@media (orientation: landscape) and (max-width: 1440px) {
|
||||||
right: calc(4rem + 0.5rem);
|
right: calc(4rem + 0.5rem);
|
||||||
bottom: 0.75rem;
|
bottom: 0.75rem;
|
||||||
top: auto;
|
top: auto;
|
||||||
|
|||||||
@@ -13,11 +13,15 @@ describe("RoleSelect", () => {
|
|||||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
Promise.resolve({ ok: true })
|
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(() => {
|
afterEach(() => {
|
||||||
RoleSelect.closeFan();
|
RoleSelect.closeFan();
|
||||||
testDiv.remove();
|
testDiv.remove();
|
||||||
|
delete window.showGuard;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
@@ -237,4 +241,145 @@ describe("RoleSelect", () => {
|
|||||||
expect(document.querySelector(".role-select-backdrop")).toBeNull();
|
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,6 +1,7 @@
|
|||||||
{% extends "core/base.html" %}
|
{% extends "core/base.html" %}
|
||||||
|
|
||||||
{% block title_text %}{{ room.name }} — Drama Log{% endblock %}
|
{% block title_text %}{{ room.name }} — Billscroll{% endblock %}
|
||||||
|
{% block header_text %}<span>Bill</span>scroll{% endblock header_text %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="billboard-page">
|
<div class="billboard-page">
|
||||||
|
|||||||
@@ -1,27 +1,4 @@
|
|||||||
|
{% include "apps/gameboard/_partials/_applet_menu.html" %}
|
||||||
<div id="id_game_applets_container">
|
<div id="id_game_applets_container">
|
||||||
<div id="id_game_applet_menu" style="display:none;">
|
|
||||||
<form
|
|
||||||
hx-post="{% url "toggle_game_applets" %}"
|
|
||||||
hx-target="#id_game_applets_container"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
>
|
|
||||||
{% csrf_token %}
|
|
||||||
{% for entry in applets %}
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="applets"
|
|
||||||
value="{{ entry.applet.slug }}"
|
|
||||||
{% if entry.visible %}checked{% endif %}
|
|
||||||
>
|
|
||||||
{{ entry.applet.name }}
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="menu-btns">
|
|
||||||
<button type="submit" class="btn btn-confirm">OK</button>
|
|
||||||
<button type="button" id="id_game_applet_menu_cancel" class="btn btn-cancel applet-menu-cancel">NVM</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% include "apps/applets/_partials/_applets.html" %}
|
{% include "apps/applets/_partials/_applets.html" %}
|
||||||
</div>
|
</div>
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
{% if request.user == room.owner %}
|
{% if request.user == room.owner %}
|
||||||
<form method="POST" action="{% url 'epic:delete_room' room.id %}">
|
<form method="POST" action="{% url 'epic:delete_room' room.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn btn-danger">DEL</button>
|
<button type="submit" class="btn btn-danger" data-confirm="Delete this room?">DEL</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="POST" action="{% url 'epic:abandon_room' room.id %}">
|
<form method="POST" action="{% url 'epic:abandon_room' room.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn btn-abandon">BYE</button>
|
<button type="submit" class="btn btn-abandon" data-confirm="Leave this room?">BYE</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="footer-container">
|
<div class="footer-container">
|
||||||
<small>©{% now "Y" %} Dis Co.</small>
|
<small>©{% now "Y" %} <br>Dis Co.</small>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<form method="POST" action="{% url "logout" %}">
|
<form method="POST" action="{% url "logout" %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button id="id_logout" class="btn btn-primary btn-xl" type="submit">
|
<button id="id_logout" class="btn btn-primary btn-xl" type="submit" data-confirm="Log out?">
|
||||||
Log Out
|
Log Out
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -57,8 +57,91 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<dialog id="id_kit_bag_dialog"></dialog>
|
<dialog id="id_kit_bag_dialog"></dialog>
|
||||||
|
|
||||||
|
<div id="id_guard_portal">
|
||||||
|
<span class="guard-message"></span>
|
||||||
|
<div class="guard-actions">
|
||||||
|
<button class="btn btn-confirm guard-yes" type="button">OK</button>
|
||||||
|
<button class="btn btn-cancel guard-no" type="button">NVM</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var portal = null;
|
||||||
|
var _cb = null;
|
||||||
|
var _onDismiss = null;
|
||||||
|
|
||||||
|
function show(anchor, message, callback, onDismiss) {
|
||||||
|
if (!portal) return;
|
||||||
|
_cb = callback;
|
||||||
|
_onDismiss = onDismiss || null;
|
||||||
|
portal.querySelector('.guard-message').innerHTML = message;
|
||||||
|
portal.classList.add('active');
|
||||||
|
var rect = anchor.getBoundingClientRect();
|
||||||
|
var pw = portal.offsetWidth;
|
||||||
|
var rawLeft = rect.left + rect.width / 2;
|
||||||
|
var cleft = Math.max(pw / 2 + 8, Math.min(rawLeft, window.innerWidth - pw / 2 - 8));
|
||||||
|
portal.style.left = Math.round(cleft) + 'px';
|
||||||
|
if (rect.top > 120) {
|
||||||
|
portal.style.top = Math.round(rect.top) + 'px';
|
||||||
|
portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))';
|
||||||
|
} else {
|
||||||
|
portal.style.top = Math.round(rect.bottom) + 'px';
|
||||||
|
portal.style.transform = 'translate(-50%, 0.5rem)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
if (!portal) return;
|
||||||
|
var od = _onDismiss;
|
||||||
|
portal.classList.remove('active');
|
||||||
|
_cb = null;
|
||||||
|
_onDismiss = null;
|
||||||
|
if (od) od();
|
||||||
|
}
|
||||||
|
|
||||||
|
function doConfirm() {
|
||||||
|
var cb = _cb;
|
||||||
|
portal.classList.remove('active');
|
||||||
|
_cb = null;
|
||||||
|
_onDismiss = null;
|
||||||
|
if (cb) cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
portal = document.getElementById('id_guard_portal');
|
||||||
|
if (!portal) return;
|
||||||
|
portal.querySelector('.guard-yes').addEventListener('click', doConfirm);
|
||||||
|
portal.querySelector('.guard-no').addEventListener('click', dismiss);
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Escape') dismiss();
|
||||||
|
});
|
||||||
|
// Outside-click to dismiss — capture phase + stopPropagation
|
||||||
|
// prevents the click from cascading to backdrop listeners (e.g. closeFan)
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (!portal.classList.contains('active')) return;
|
||||||
|
if (portal.contains(e.target)) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
dismiss();
|
||||||
|
}, true);
|
||||||
|
// Intercept [data-confirm] buttons (capture phase, before form submits)
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var btn = e.target.closest('[data-confirm]');
|
||||||
|
if (!btn) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
var form = btn.closest('form');
|
||||||
|
show(btn, btn.dataset.confirm, function () {
|
||||||
|
if (form) form.submit();
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.showGuard = show;
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
||||||
<script src="{% static "apps/applets/applets.js" %}"></script>
|
<script src="{% static "apps/applets/applets.js" %}"></script>
|
||||||
<script src="{% static "apps/dashboard/game-kit.js" %}"></script>
|
<script src="{% static "apps/dashboard/game-kit.js" %}"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user