Files
python-tdd/src/static_src/tests/NotePageSpec.js
Disco DeDisco e78bbb873b Billnotes note-page interaction: hover glow, click-lock, DON/DOFF; note titles + card-ref styling — TDD
- note-page.js: click-lock (_lockedItem + notes-locked body class); DON auto-DOFFs prev donned; _setGreeting updates navbar; _donnedItem exposed for test API
- NotePageSpec.js: 18 Jasmine specs covering lock/unlock, DON/DOFF state, auto-DOFF, greeting update, initial load; flushPromises helper for chained fetch .then()
- _note.scss: DON/DOFF opacity:0 by default; hover + locked + donned states show them; body:not(.notes-locked) hover suppression
- views.py: Super-Schizo/Super-Nomad card titles; recognition_title field (display_title) separate from card title; mark_safe descriptions w. card-ref spans
- my_notes.html: |safe on description; recognition_title for Recognitions block
- _navbar.html: id_greeting_prefix/id_greeting_name spans for JS greeting update
- _base.scss: global .card-ref rule (--terUser, font-weight 600, !important)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 01:54:57 -04:00

205 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function flushPromises() {
return new Promise(resolve => setTimeout(resolve, 0));
}
describe("NotePage", () => {
let testDiv, item1, item2, don1, doff1, don2, doff2;
function makeItem(slug, isEquipped) {
const li = document.createElement("li");
li.className = "note-item";
li.dataset.slug = slug;
li.dataset.donUrl = `/billboard/note/${slug}/don`;
li.dataset.doffUrl = `/billboard/note/${slug}/doff`;
li.innerHTML = `
<div class="note-don-doff">
<button class="btn btn-equip note-don-btn${isEquipped ? " btn-disabled" : ""}">${isEquipped ? "×" : "DON"}</button>
<button class="btn btn-unequip note-doff-btn${isEquipped ? "" : " btn-disabled"}">${isEquipped ? "DOFF" : "×"}</button>
</div>
<div class="note-item__body"><p class="note-item__title">${slug}</p></div>
`;
return li;
}
beforeEach(() => {
NotePage._testReset();
testDiv = document.createElement("div");
testDiv.innerHTML = `<span id="id_greeting_prefix">Welcome,</span><span id="id_greeting_name">Earthman</span>`;
item1 = makeItem("super-schizo", false);
item2 = makeItem("super-nomad", false);
testDiv.appendChild(item1);
testDiv.appendChild(item2);
document.body.appendChild(testDiv);
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true, json: () => Promise.resolve({ title: "Schizoid Man", greeting: "21st Century" }) })
);
don1 = item1.querySelector(".note-don-btn");
doff1 = item1.querySelector(".note-doff-btn");
don2 = item2.querySelector(".note-don-btn");
doff2 = item2.querySelector(".note-doff-btn");
NotePage._init();
});
afterEach(() => {
testDiv.remove();
document.body.classList.remove("notes-locked");
delete window.fetch;
});
// ── Lock / unlock ─────────────────────────────────────────────────────────
describe("click-lock behaviour", () => {
it("clicking a note adds note-item--locked", () => {
item1.click();
expect(item1.classList.contains("note-item--locked")).toBe(true);
});
it("clicking a note adds notes-locked to body", () => {
item1.click();
expect(document.body.classList.contains("notes-locked")).toBe(true);
});
it("clicking the same note again removes lock", () => {
item1.click();
item1.click();
expect(item1.classList.contains("note-item--locked")).toBe(false);
expect(document.body.classList.contains("notes-locked")).toBe(false);
});
it("clicking a different note moves lock to that note", () => {
item1.click();
item2.click();
expect(item1.classList.contains("note-item--locked")).toBe(false);
expect(item2.classList.contains("note-item--locked")).toBe(true);
});
it("body click clears all locks", () => {
item1.click();
document.body.click();
expect(item1.classList.contains("note-item--locked")).toBe(false);
expect(document.body.classList.contains("notes-locked")).toBe(false);
});
});
// ── DON/DOFF state ────────────────────────────────────────────────────────
describe("DON button", () => {
it("clicking DON sends POST to don URL", async () => {
don1.click();
await flushPromises();
expect(window.fetch).toHaveBeenCalledWith(
"/billboard/note/super-schizo/don",
jasmine.objectContaining({ method: "POST" })
);
});
it("after DON, note gains note-item--donned", async () => {
don1.click();
await flushPromises();
expect(item1.classList.contains("note-item--donned")).toBe(true);
});
it("after DON, lock is cleared", async () => {
item1.click(); // lock first
don1.click();
await flushPromises();
expect(item1.classList.contains("note-item--locked")).toBe(false);
expect(document.body.classList.contains("notes-locked")).toBe(false);
});
it("after DON, greeting prefix and name are updated", async () => {
don1.click();
await flushPromises();
expect(document.getElementById("id_greeting_prefix").innerHTML).toContain("21st Century");
expect(document.getElementById("id_greeting_name").textContent).toBe("Schizoid Man");
});
it("DONning item1 auto-removes donned state from previously DONned item2", async () => {
item2.classList.add("note-item--donned");
don2.classList.add("btn-disabled"); don2.textContent = "×";
doff2.classList.remove("btn-disabled"); doff2.textContent = "DOFF";
NotePage._donnedItem = item2;
don1.click();
await flushPromises();
expect(item2.classList.contains("note-item--donned")).toBe(false);
expect(don2.classList.contains("btn-disabled")).toBe(false);
expect(don2.textContent).toBe("DON");
expect(doff2.classList.contains("btn-disabled")).toBe(true);
});
it("DONning one does not POST a doff for the previous", async () => {
item2.classList.add("note-item--donned");
NotePage._donnedItem = item2;
don1.click();
await flushPromises();
// Only one fetch call (the DON), no DOFF call for item2
expect(window.fetch.calls.count()).toBe(1);
});
});
describe("DOFF button", () => {
it("clicking DOFF sends POST to doff URL", async () => {
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
);
item1.classList.add("note-item--donned");
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
don1.classList.add("btn-disabled"); don1.textContent = "×";
NotePage._donnedItem = item1;
doff1.click();
await flushPromises();
expect(window.fetch).toHaveBeenCalledWith(
"/billboard/note/super-schizo/doff",
jasmine.objectContaining({ method: "POST" })
);
});
it("after DOFF, note loses note-item--donned", async () => {
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
);
item1.classList.add("note-item--donned");
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
NotePage._donnedItem = item1;
doff1.click();
await flushPromises();
expect(item1.classList.contains("note-item--donned")).toBe(false);
});
it("after DOFF, greeting reverts to Welcome, / Earthman", async () => {
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
);
document.getElementById("id_greeting_prefix").textContent = "21st Century";
document.getElementById("id_greeting_name").textContent = "Schizoid Man";
item1.classList.add("note-item--donned");
doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
NotePage._donnedItem = item1;
doff1.click();
await flushPromises();
expect(document.getElementById("id_greeting_prefix").textContent).toBe("Welcome,");
expect(document.getElementById("id_greeting_name").textContent).toBe("Earthman");
});
});
// ── Initial donned state ──────────────────────────────────────────────────
describe("initial load with an already-donned note", () => {
it("note whose DON is btn-disabled gets note-item--donned on init", () => {
const equippedItem = makeItem("stargazer", true);
testDiv.appendChild(equippedItem);
NotePage._testReset();
NotePage._init();
expect(equippedItem.classList.contains("note-item--donned")).toBe(true);
});
});
});