Files
python-tdd/src/static/tests/NoteSpec.js
Disco DeDisco ba5f6556c0 buddy btn sprint: banner-anchor + window.Brief fix lands the last red FT — 16/16 buddy + 12 share/jasmine/my_notes + 818 IT regression — TDD
Two small fixes close out the OK→banner gap:

1. Anchor over h2: base.html drops <div id="id_brief_banner_anchor"></div> right before {% block content %} (after the messages block). note.js's showBanner now prefers the explicit anchor over the first <h2> — keeps the banner in the visible content flow on pages where the first h2 is position:absolute (post.html's rotated navbar header was the immediate motivator; sky.html's rotated h2 is the same shape, so this catches that pre-emptively too).
2. window.Brief explicit assignment: const Brief = (...) at script-tag scope is reachable as a bare name but does NOT auto-attach to window. The buddy panel's OK handler gates banner reveal on `if (window.Brief && data.brief)` — that gate was always false, so Brief.showBanner never fired on share-OK even though the chip + Line append in DOM proved the fetch.then() was running. Explicit window.Brief = Brief; window.Note = Note; in note.js (post-IIFE) closes the gap.

Also picks up the deferred page-object update — functional_tests.post_page.PostPage.share_post_with() now drives the buddy-btn flow (click #id_buddy_btn → type → click #id_buddy_panel .btn.btn-confirm → wait for recipient chip), so legacy SharingTest exercises the new pipeline end-to-end.

NoteSpec.js T10 split into T10a/T10b: a covers the anchor-preferred path, b covers the <h2> fallback.

16/16 buddy FTs green (previously 15/16). 12/12 sharing + Jasmine + my_notes FTs green. 818-test IT sweep green.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 19:14:50 -04:00

160 lines
7.3 KiB
JavaScript

// ── NoteSpec.js ───────────────────────────────────────────────────────────────
//
// Unit specs for note.js (the slide-down Brief banner). The banner module is
// exposed as `Brief` (with `Note` kept as an alias during the C3 sprint).
//
// DOM contract assumed by showBanner():
// Any <h2> in the document — banner is inserted immediately after it.
// If absent, banner is prepended to <body>.
//
// API under test:
// Brief.showBanner(brief)
// brief = { id, kind, title, line_text, post_url, square_url, created_at }
// → inject .note-banner
// brief = null → no-op
//
// Brief.handleSaveResponse(data)
// data = { saved: true, brief: {...} } → delegates to showBanner
// data = { saved: true, brief: null } → no-op
//
// ─────────────────────────────────────────────────────────────────────────────
const SAMPLE_BRIEF = {
id: '00000000-0000-0000-0000-000000000001',
kind: 'note_unlock',
title: 'Stargazer',
line_text: 'Stargazer, 02:00:00 AM',
post_url: '/billboard/post/abc/',
square_url: '/billboard/my-notes/',
created_at: '2026-04-22T02:00:00+00:00',
};
describe('Brief.showBanner', () => {
let fixture;
beforeEach(() => {
fixture = document.createElement('div');
fixture.id = 'note-fixture';
fixture.innerHTML = '<h2>Dash</h2>';
document.body.appendChild(fixture);
});
afterEach(() => {
document.querySelectorAll('.note-banner').forEach(b => b.remove());
fixture.remove();
});
// ── T1 ── null → no banner ────────────────────────────────────────────────
it('T1: showBanner(null) does not inject a banner', () => {
Brief.showBanner(null);
expect(document.querySelector('.note-banner')).toBeNull();
});
// ── T2 ── brief present → banner in DOM ──────────────────────────────────
it('T2: showBanner(brief) injects .note-banner into the document', () => {
Brief.showBanner(SAMPLE_BRIEF);
expect(document.querySelector('.note-banner')).not.toBeNull();
});
// ── T3 ── title ───────────────────────────────────────────────────────────
it('T3: banner .note-banner__title contains brief.title', () => {
Brief.showBanner(SAMPLE_BRIEF);
const el = document.querySelector('.note-banner__title');
expect(el).not.toBeNull();
expect(el.textContent).toContain('Stargazer');
});
// ── T4 ── description (line_text) ─────────────────────────────────────────
it('T4: banner .note-banner__description carries brief.line_text', () => {
Brief.showBanner(SAMPLE_BRIEF);
const el = document.querySelector('.note-banner__description');
expect(el).not.toBeNull();
expect(el.textContent).toContain('Stargazer, 02:00:00 AM');
});
// ── T5 ── timestamp ───────────────────────────────────────────────────────
it('T5: banner has a .note-banner__timestamp element', () => {
Brief.showBanner(SAMPLE_BRIEF);
expect(document.querySelector('.note-banner__timestamp')).not.toBeNull();
});
// ── T6 ── image area; for note_unlock kind it's a clickable square ──────
it('T6: banner .note-banner__image is a clickable <a> when brief.square_url is set', () => {
Brief.showBanner(SAMPLE_BRIEF);
const sq = document.querySelector('.note-banner__image');
expect(sq).not.toBeNull();
expect(sq.tagName.toLowerCase()).toBe('a');
expect(sq.getAttribute('href')).toBe('/billboard/my-notes/');
});
// ── T7 ── NVM button ──────────────────────────────────────────────────────
it('T7: banner has a .btn.btn-cancel NVM button', () => {
Brief.showBanner(SAMPLE_BRIEF);
expect(document.querySelector('.note-banner .btn.btn-cancel')).not.toBeNull();
});
// ── T8 ── FYI link points to brief.post_url (not hardcoded my-notes) ─────
it('T8: banner FYI .btn.btn-info link points to brief.post_url', () => {
Brief.showBanner(SAMPLE_BRIEF);
const fyi = document.querySelector('.note-banner .btn.btn-info');
expect(fyi).not.toBeNull();
expect(fyi.getAttribute('href')).toBe('/billboard/post/abc/');
});
// ── T9 ── NVM dismissal ───────────────────────────────────────────────────
it('T9: clicking the NVM button removes the banner from the DOM', () => {
Brief.showBanner(SAMPLE_BRIEF);
document.querySelector('.note-banner .btn.btn-cancel').click();
expect(document.querySelector('.note-banner')).toBeNull();
});
// ── T10 ── placement: anchor preferred over h2 ───────────────────────────
it('T10a: banner is inserted as nextSibling of #id_brief_banner_anchor when present', () => {
const anchor = document.createElement('div');
anchor.id = 'id_brief_banner_anchor';
fixture.appendChild(anchor);
Brief.showBanner(SAMPLE_BRIEF);
expect(anchor.nextElementSibling.classList.contains('note-banner')).toBeTrue();
});
it('T10b: falls back to inserting after the first h2 when anchor is absent', () => {
Brief.showBanner(SAMPLE_BRIEF);
const h2 = fixture.querySelector('h2');
expect(h2.nextElementSibling.classList.contains('note-banner')).toBeTrue();
});
});
describe('Brief.handleSaveResponse', () => {
afterEach(() => {
document.querySelectorAll('.note-banner').forEach(b => b.remove());
});
// ── T11 ── delegates when brief present ──────────────────────────────────
it('T11: handleSaveResponse shows banner when data.brief is present', () => {
Brief.handleSaveResponse({ saved: true, brief: SAMPLE_BRIEF });
expect(document.querySelector('.note-banner')).not.toBeNull();
});
// ── T12 ── no banner when brief null ─────────────────────────────────────
it('T12: handleSaveResponse does not show banner when data.brief is null', () => {
Brief.handleSaveResponse({ saved: true, brief: null });
expect(document.querySelector('.note-banner')).toBeNull();
});
});