Files
python-tdd/src/static/tests/NoteSpec.js
Disco DeDisco 473e6bc45a rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests
- new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts
- drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette
- recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-*
- _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes)
- NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py
- 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:32:34 -04:00

144 lines
6.8 KiB
JavaScript

// ── NoteSpec.js ───────────────────────────────────────────────────────────────
//
// Unit specs for note.js — banner injection from sky/save response.
//
// 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:
// Note.showBanner(note)
// note = { slug, title, description, earned_at } → inject .note-banner
// note = null → no-op
//
// Note.handleSaveResponse(data)
// data = { saved: true, note: {...} } → delegates to showBanner
// data = { saved: true, note: null } → no-op
//
// ─────────────────────────────────────────────────────────────────────────────
const SAMPLE_NOTE = {
slug: 'stargazer',
title: 'Stargazer',
description: 'You saved your first personal sky chart.',
earned_at: '2026-04-22T02:00:00+00:00',
};
describe('Note.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', () => {
Note.showBanner(null);
expect(document.querySelector('.note-banner')).toBeNull();
});
// ── T2 ── note present → banner in DOM ───────────────────────────────────
it('T2: showBanner(note) injects .note-banner into the document', () => {
Note.showBanner(SAMPLE_NOTE);
expect(document.querySelector('.note-banner')).not.toBeNull();
});
// ── T3 ── title ───────────────────────────────────────────────────────────
it('T3: banner .note-banner__title contains note.title', () => {
Note.showBanner(SAMPLE_NOTE);
const el = document.querySelector('.note-banner__title');
expect(el).not.toBeNull();
expect(el.textContent).toContain('Stargazer');
});
// ── T4 ── description ─────────────────────────────────────────────────────
it('T4: banner .note-banner__description contains note.description', () => {
Note.showBanner(SAMPLE_NOTE);
const el = document.querySelector('.note-banner__description');
expect(el).not.toBeNull();
expect(el.textContent).toContain('You saved your first personal sky chart.');
});
// ── T5 ── timestamp ───────────────────────────────────────────────────────
it('T5: banner has a .note-banner__timestamp element', () => {
Note.showBanner(SAMPLE_NOTE);
expect(document.querySelector('.note-banner__timestamp')).not.toBeNull();
});
// ── T6 ── image area ──────────────────────────────────────────────────────
it('T6: banner has a .note-banner__image element', () => {
Note.showBanner(SAMPLE_NOTE);
expect(document.querySelector('.note-banner__image')).not.toBeNull();
});
// ── T7 ── NVM button ──────────────────────────────────────────────────────
it('T7: banner has a .btn.btn-danger NVM button', () => {
Note.showBanner(SAMPLE_NOTE);
expect(document.querySelector('.note-banner .btn.btn-danger')).not.toBeNull();
});
// ── T8 ── FYI link ────────────────────────────────────────────────────────
it('T8: banner has a .btn.btn-caution FYI link pointing to /billboard/my-notes/', () => {
Note.showBanner(SAMPLE_NOTE);
const fyi = document.querySelector('.note-banner .btn.btn-caution');
expect(fyi).not.toBeNull();
expect(fyi.getAttribute('href')).toBe('/billboard/my-notes/');
});
// ── T9 ── NVM dismissal ───────────────────────────────────────────────────
it('T9: clicking the NVM button removes the banner from the DOM', () => {
Note.showBanner(SAMPLE_NOTE);
document.querySelector('.note-banner .btn.btn-danger').click();
expect(document.querySelector('.note-banner')).toBeNull();
});
// ── T10 ── placement after h2 ─────────────────────────────────────────────
it('T10: banner is inserted immediately after the first h2 in the document', () => {
Note.showBanner(SAMPLE_NOTE);
const h2 = fixture.querySelector('h2');
expect(h2.nextElementSibling.classList.contains('note-banner')).toBeTrue();
});
});
describe('Note.handleSaveResponse', () => {
afterEach(() => {
document.querySelectorAll('.note-banner').forEach(b => b.remove());
});
// ── T11 ── delegates when note present ────────────────────────────────────
it('T11: handleSaveResponse shows banner when data.note is present', () => {
Note.handleSaveResponse({ saved: true, note: SAMPLE_NOTE });
expect(document.querySelector('.note-banner')).not.toBeNull();
});
// ── T12 ── no banner when note null ───────────────────────────────────────
it('T12: handleSaveResponse does not show banner when data.note is null', () => {
Note.handleSaveResponse({ saved: true, note: null });
expect(document.querySelector('.note-banner')).toBeNull();
});
});