// ── 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

in the document — banner is inserted immediately after it. // If absent, banner is prepended to . // // 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 = '

Dash

'; 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 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(); }); }); // ───────────────────────────────────────────────────────────────────────────── // // Brief.showDuplicateBanner — the "already added" error variant. Distinct // from showBanner because: the server doesn't persist a Brief row for these // (transient, client-only), title is rendered as `@ is already // present`, there's no date/square/post_url, and the FYI button toggles // `.bud-duplicate-flash` onto a caller-supplied target element instead of // navigating. // // API under test: // Brief.showDuplicateBanner({ display_name, target_selector? }) // null / missing display_name → no banner // target_selector matches an element → FYI click adds .bud-duplicate-flash // target_selector missing or matches nothing → FYI just dismisses // // ───────────────────────────────────────────────────────────────────────────── describe('Brief.showDuplicateBanner', () => { let fixture; beforeEach(() => { fixture = document.createElement('div'); fixture.id = 'dup-fixture'; fixture.innerHTML = '

Dash

' + ''; document.body.appendChild(fixture); }); afterEach(() => { document.querySelectorAll('.note-banner').forEach(b => b.remove()); fixture.remove(); }); it('D1: missing display_name → no banner', () => { Brief.showDuplicateBanner({}); expect(document.querySelector('.note-banner')).toBeNull(); }); it('D2: title reads "@ is already present"', () => { Brief.showDuplicateBanner({ display_name: 'alice' }); const title = document.querySelector('.note-banner__title'); expect(title).not.toBeNull(); expect(title.textContent).toBe('@alice is already present'); }); it('D3: banner carries the .note-banner--duplicate modifier class', () => { Brief.showDuplicateBanner({ display_name: 'alice' }); const b = document.querySelector('.note-banner'); expect(b).not.toBeNull(); expect(b.classList.contains('note-banner--duplicate')).toBeTrue(); }); it('D4: duplicate banner has no .note-banner__description or .note-banner__timestamp', () => { Brief.showDuplicateBanner({ display_name: 'alice' }); expect(document.querySelector('.note-banner__description')).toBeNull(); expect(document.querySelector('.note-banner__timestamp')).toBeNull(); }); it('D5: FYI button is a