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>
160 lines
7.3 KiB
JavaScript
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();
|
|
});
|
|
|
|
});
|