// ── 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 after h2 ───────────────────────────────────────────── it('T10: banner is inserted immediately after the first h2 in the document', () => { 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(); }); });