diff --git a/src/apps/billboard/static/apps/billboard/note-page.js b/src/apps/billboard/static/apps/billboard/note-page.js index 0ae9cd3..bfb4d24 100644 --- a/src/apps/billboard/static/apps/billboard/note-page.js +++ b/src/apps/billboard/static/apps/billboard/note-page.js @@ -5,6 +5,8 @@ var _activeItem = null; var _originalPalette = null; var _dismissTimer = null; + var _lockedItem = null; // click-locked note (glow + DON/DOFF pinned) + var _donnedItem = null; // currently DONned note (persistent glow) // ── helpers ────────────────────────────────────────────────────────────── @@ -47,6 +49,23 @@ if (el) el.style.display = 'none'; } + // ── lock helpers ────────────────────────────────────────────────────────── + + function _clearLock() { + if (_lockedItem) { + _lockedItem.classList.remove('note-item--locked'); + _lockedItem = null; + } + document.body.classList.remove('notes-locked'); + } + + function _setGreeting(greeting, name) { + var prefix = document.getElementById('id_greeting_prefix'); + var nameEl = document.getElementById('id_greeting_name'); + if (prefix) prefix.innerHTML = greeting; + if (nameEl) nameEl.textContent = name; + } + // ── modal lifecycle ─────────────────────────────────────────────────────── function _openModal() { @@ -88,47 +107,31 @@ _originalPalette = null; } - // Wire event listeners onto the freshly-cloned modal DOM. function _wireModal() { var modal = _activeModal(); if (!modal) return; - // Swatch body click → preview palette sitewide + show confirm modal.querySelectorAll('.note-swatch-body').forEach(function (body) { body.addEventListener('click', function (e) { e.stopPropagation(); if (_selectedPalette) _revertPreview(); - _selectedPalette = _paletteClass(body.parentElement); _originalPalette = _currentBodyPalette(); - body.classList.add('previewing'); _swapBodyPalette(_selectedPalette); _showConfirm(modal); - - _dismissTimer = setTimeout(function () { - _revertPreview(); - }, 10000); + _dismissTimer = setTimeout(function () { _revertPreview(); }, 10000); }); }); - // Confirm OK → commit palette sitewide modal.querySelectorAll('.note-palette-confirm .btn.btn-confirm').forEach(function (btn) { - btn.addEventListener('click', function (e) { - e.stopPropagation(); - _doSetPalette(); - }); + btn.addEventListener('click', function (e) { e.stopPropagation(); _doSetPalette(); }); }); - // Confirm NVM → revert preview only; main swatch modal stays open modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) { - btn.addEventListener('click', function (e) { - e.stopPropagation(); - _revertPreview(); - }); + btn.addEventListener('click', function (e) { e.stopPropagation(); _revertPreview(); }); }); - // Stop modal clicks from reaching the body dismiss handler. modal.addEventListener('click', function (e) { e.stopPropagation(); }); } @@ -138,18 +141,13 @@ var url = _activeItem.dataset.setPaletteUrl; var palette = _selectedPalette; var item = _activeItem; - // Read label from swatch row while modal is still in DOM var swatchRow = _activeModal() && _activeModal().querySelector('.' + palette + '[data-palette-label]'); var paletteLabel = swatchRow ? swatchRow.dataset.paletteLabel : palette.slice(8).replace(/^\w/, function (c) { return c.toUpperCase(); }); fetch(url, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': _getCsrf(), - }, + method: 'POST', credentials: 'same-origin', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': _getCsrf() }, body: JSON.stringify({ palette: palette }), }) .then(function (r) { return r.json(); }) @@ -171,12 +169,7 @@ }); } - // ── init ────────────────────────────────────────────────────────────────── - - function _setGreeting(name) { - var el = document.getElementById('id_greeting_name'); - if (el) el.textContent = name; - } + // ── DON/DOFF ────────────────────────────────────────────────────────────── function _bindDonDoff(item) { var donBtn = item.querySelector('.note-don-btn'); @@ -192,11 +185,21 @@ }) .then(function (r) { return r.json(); }) .then(function (data) { - _setGreeting(data.title); - donBtn.classList.add('btn-disabled'); - donBtn.textContent = '×'; - doffBtn.classList.remove('btn-disabled'); - doffBtn.textContent = 'DOFF'; + // Auto-DOFF any previously DONned note (UI only — backend replaces active_title) + if (_donnedItem && _donnedItem !== item) { + _donnedItem.classList.remove('note-item--donned'); + var prevDon = _donnedItem.querySelector('.note-don-btn'); + var prevDoff = _donnedItem.querySelector('.note-doff-btn'); + if (prevDon) { prevDon.classList.remove('btn-disabled'); prevDon.textContent = 'DON'; } + if (prevDoff) { prevDoff.classList.add('btn-disabled'); prevDoff.textContent = '×'; } + } + _donnedItem = item; + item.classList.add('note-item--donned'); + // Clear lock so hover is restored for other notes + _clearLock(); + donBtn.classList.add('btn-disabled'); donBtn.textContent = '×'; + doffBtn.classList.remove('btn-disabled'); doffBtn.textContent = 'DOFF'; + _setGreeting(data.greeting || 'Welcome,', data.title || 'Earthman'); }); }); @@ -207,33 +210,60 @@ method: 'POST', credentials: 'same-origin', headers: { 'X-CSRFToken': _getCsrf() }, }) - .then(function () { - _setGreeting('Earthman'); - doffBtn.classList.add('btn-disabled'); - doffBtn.textContent = '×'; - donBtn.classList.remove('btn-disabled'); - donBtn.textContent = 'DON'; + .then(function (r) { return r.json(); }) + .then(function (data) { + _donnedItem = null; + item.classList.remove('note-item--donned'); + _clearLock(); + doffBtn.classList.add('btn-disabled'); doffBtn.textContent = '×'; + donBtn.classList.remove('btn-disabled'); donBtn.textContent = 'DON'; + _setGreeting(data.greeting || 'Welcome,', data.title || 'Earthman'); }); }); } + // ── init ────────────────────────────────────────────────────────────────── + function _init() { - document.querySelectorAll('.note-item__image-box').forEach(function (box) { - box.addEventListener('click', function (e) { + document.querySelectorAll('.note-item').forEach(function (item) { + // Detect already-DONned note on load (DON btn is disabled = currently equipped) + var don = item.querySelector('.note-don-btn'); + if (don && don.classList.contains('btn-disabled')) { + item.classList.add('note-item--donned'); + _donnedItem = item; + } + + _bindDonDoff(item); + + // Image box click → palette modal (for notes that have one) + var box = item.querySelector('.note-item__image-box:not(.note-item__image-box--label)'); + if (box) { + box.addEventListener('click', function (e) { + e.stopPropagation(); + _activeItem = item; + _openModal(); + }); + } + + // Note click → toggle lock + item.addEventListener('click', function (e) { e.stopPropagation(); - _activeItem = box.closest('.note-item'); - _openModal(); + if (_lockedItem === item) { + _clearLock(); + } else { + _clearLock(); + _lockedItem = item; + item.classList.add('note-item--locked'); + document.body.classList.add('notes-locked'); + } }); }); - document.querySelectorAll('.note-item').forEach(function (item) { - _bindDonDoff(item); - }); - - // Body click → dismiss modal and revert any preview + // Body click → dismiss modal and clear lock document.body.addEventListener('click', function () { if (_selectedPalette) _revertPreview(); _closeModal(); + _clearLock(); }); } @@ -242,4 +272,20 @@ } else { _init(); } + + // Expose test API + window.NotePage = { + _init: _init, + _testReset: function () { + _selectedPalette = null; + _activeItem = null; + _originalPalette = null; + _dismissTimer = null; + _lockedItem = null; + _donnedItem = null; + document.body.classList.remove('notes-locked'); + }, + get _donnedItem() { return _donnedItem; }, + set _donnedItem(v) { _donnedItem = v; }, + }; }()); diff --git a/src/apps/billboard/views.py b/src/apps/billboard/views.py index 9606088..5bab098 100644 --- a/src/apps/billboard/views.py +++ b/src/apps/billboard/views.py @@ -116,13 +116,13 @@ _NOTE_META = { "swatch_label": None, }, "super-schizo": { - "title": "Schizoid Man", + "title": "Super-Schizo", "description": mark_safe('Admin access granted to I. The Schizo as Significator'), "palette_options": [], "swatch_label": "I", }, "super-nomad": { - "title": "Stranger", + "title": "Super-Nomad", "description": mark_safe('Admin access granted to 0. The Nomad as Significator'), "palette_options": [], "swatch_label": "0", @@ -156,13 +156,14 @@ def my_notes(request): active_title = request.user.active_title note_items = [ { - "obj": n, - "title": _NOTE_META.get(n.slug, {}).get("title", n.slug), - "description": _NOTE_META.get(n.slug, {}).get("description", ""), - "palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []), - "swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"), - "palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "", - "is_equipped": active_title is not None and active_title.pk == n.pk, + "obj": n, + "title": _NOTE_META.get(n.slug, {}).get("title", n.slug), + "recognition_title": n.display_title, + "description": _NOTE_META.get(n.slug, {}).get("description", ""), + "palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []), + "swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"), + "palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "", + "is_equipped": active_title is not None and active_title.pk == n.pk, } for n in qs ] @@ -183,8 +184,7 @@ def don_title(request, slug): if request.method == "POST": request.user.active_title = note request.user.save(update_fields=["active_title"]) - title = _NOTE_META.get(slug, {}).get("title", slug.capitalize()) - return JsonResponse({"title": title}) + return JsonResponse({"title": note.display_title, "greeting": note.display_greeting}) @login_required(login_url="/") @@ -192,7 +192,7 @@ def doff_title(request, slug): if request.method == "POST": request.user.active_title = None request.user.save(update_fields=["active_title"]) - return JsonResponse({"ok": True}) + return JsonResponse({"ok": True, "greeting": "Welcome,", "title": "Earthman"}) @login_required(login_url="/") diff --git a/src/static/tests/NotePageSpec.js b/src/static/tests/NotePageSpec.js new file mode 100644 index 0000000..ec686c1 --- /dev/null +++ b/src/static/tests/NotePageSpec.js @@ -0,0 +1,204 @@ +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 = ` +
+ + +
+

${slug}

+ `; + return li; + } + + beforeEach(() => { + NotePage._testReset(); + testDiv = document.createElement("div"); + testDiv.innerHTML = `Welcome,Earthman`; + 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); + }); + }); +}); diff --git a/src/static/tests/SpecRunner.html b/src/static/tests/SpecRunner.html index e8bfc7d..63366ce 100644 --- a/src/static/tests/SpecRunner.html +++ b/src/static/tests/SpecRunner.html @@ -24,9 +24,11 @@ + + diff --git a/src/static_src/scss/_base.scss b/src/static_src/scss/_base.scss index 61d0f53..7315e6c 100644 --- a/src/static_src/scss/_base.scss +++ b/src/static_src/scss/_base.scss @@ -596,3 +596,8 @@ body { gap: 0.5rem; } } + +.card-ref { + color: rgba(var(--terUser), 1) !important; + font-weight: 600 !important; +} diff --git a/src/static_src/scss/_note.scss b/src/static_src/scss/_note.scss index 0f1f027..e44ba9e 100644 --- a/src/static_src/scss/_note.scss +++ b/src/static_src/scss/_note.scss @@ -70,6 +70,8 @@ flex-direction: column; gap: 0.4rem; z-index: 1; + opacity: 0; + transition: opacity 0.15s; .btn { margin: 0; } } @@ -85,19 +87,33 @@ backdrop-filter: blur(6px); border: 0.1rem solid rgba(var(--secUser), 0.4); border-radius: 0.5rem; - cursor: help; + cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s; - &:hover, - &:active, + // Hover glow — only when no note is click-locked on the page + body:not(.notes-locked) &:hover { + border-color: rgba(var(--terUser), 1); + box-shadow: 0 0 10px rgba(var(--ninUser), 0.35); + .note-item__title { color: rgba(var(--terUser), 1); } + .note-don-doff { opacity: 1; } + } + + // Palette modal open (existing) &.note-item--active { border-color: rgba(var(--terUser), 1); - opacity: 1; box-shadow: 0 0 10px rgba(var(--ninUser), 0.35); - .note-item__title { color: rgba(var(--terUser), 1); } } + // Click-locked: glow + DON/DOFF always visible + &.note-item--locked, + &.note-item--donned { + border-color: rgba(var(--terUser), 1); + box-shadow: 0 0 10px rgba(var(--ninUser), 0.35); + .note-item__title { color: rgba(var(--terUser), 1); } + .note-don-doff { opacity: 1; } + } + .note-item__body { flex: 1; min-width: 0; @@ -168,11 +184,11 @@ &--label { font-size: 1.1rem; font-weight: bold; - font-style: italic; - color: rgba(var(--terUser), 1); + color: rgba(var(--secUser), 0.6); opacity: 1; cursor: default; border-style: solid; + border-color: rgba(var(--secUser), 0.6); &:hover { opacity: 1; } } } diff --git a/src/static_src/tests/NotePageSpec.js b/src/static_src/tests/NotePageSpec.js new file mode 100644 index 0000000..ec686c1 --- /dev/null +++ b/src/static_src/tests/NotePageSpec.js @@ -0,0 +1,204 @@ +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 = ` +
+ + +
+

${slug}

+ `; + return li; + } + + beforeEach(() => { + NotePage._testReset(); + testDiv = document.createElement("div"); + testDiv.innerHTML = `Welcome,Earthman`; + 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); + }); + }); +}); diff --git a/src/static_src/tests/SpecRunner.html b/src/static_src/tests/SpecRunner.html index e8bfc7d..63366ce 100644 --- a/src/static_src/tests/SpecRunner.html +++ b/src/static_src/tests/SpecRunner.html @@ -24,9 +24,11 @@ + + diff --git a/src/templates/apps/billboard/my_notes.html b/src/templates/apps/billboard/my_notes.html index 93bd7ae..d54422a 100644 --- a/src/templates/apps/billboard/my_notes.html +++ b/src/templates/apps/billboard/my_notes.html @@ -25,11 +25,11 @@

{{ item.title }}

-

{{ item.description }}

+

{{ item.description|safe }}

Recognitions