import { describe, expect, it } from 'vitest'; import { JSDOM } from 'jsdom'; import { buildSpeakerNotesPresenterHtml, extractSpeakerNotesFromHtml, PRESENTER_WINDOW_MIN_HEIGHT, PRESENTER_WINDOW_MIN_WIDTH, removeSpeakerNotesFromHtml, upsertSpeakerNotesInHtml, } from '../../src/runtime/speaker-notes'; describe('speaker notes HTML helpers', () => { it('reads the shared #speaker-notes JSON array format', () => { const html = [ '', '
One
', '', '', ].join(''); expect(extractSpeakerNotesFromHtml(html, 3)).toEqual(['Intro', 'Details']); }); it('falls back to per-slide .notes blocks', () => { const html = [ '

One

', '

Two

Close & transition.

', ].join(''); expect(extractSpeakerNotesFromHtml(html, 2)).toEqual([ 'Open\nstrong.', 'Close & transition.', ]); }); it('upserts notes without mutating visible slide content', () => { const source = '
Visible
'; const next = upsertSpeakerNotesInHtml(source, ['Private note']); expect(next).toContain('
Visible
'); expect(next).toContain('id="speaker-notes"'); expect(extractSpeakerNotesFromHtml(next)).toEqual(['Private note']); }); it('replaces an existing speaker notes script', () => { const source = ''; const next = upsertSpeakerNotesInHtml(source, ['New']); expect(next).not.toContain('Old'); expect(extractSpeakerNotesFromHtml(next)).toEqual(['New']); }); it('ignores invalid speaker notes JSON instead of showing script garbage', () => { const source = [ '
One
', '', ].join('\n'); expect(extractSpeakerNotesFromHtml(source, 1)).toEqual([]); }); it('removes only the real speaker notes script block', () => { const source = [ '', '', ].join('\n'); const next = removeSpeakerNotesFromHtml(source); expect(next).toContain('window.RUNTIME = true;'); expect(next).toContain('mentions ` as the block's close, clobbering the entire // runtime on upsert and reading garbage on extract. The result is a deck // that renders as a black screen (issue: deck black screen after notes save). const deckWithRuntimeMention = [ '', '
One
', '', '', '', ].join('\n'); it('reads the real notes tag, not the deck-stage JSDoc mention', () => { expect(extractSpeakerNotesFromHtml(deckWithRuntimeMention, 2)).toEqual(['Real one', 'Real two']); }); it('upserts without clobbering a deck-stage runtime that mentions the tag', () => { const next = upsertSpeakerNotesInHtml(deckWithRuntimeMention, ['Edited']); // The runtime and its JSDoc must survive untouched. expect(next).toContain('const DECK_RUNTIME_MARKER = 1'); expect(next).toContain("customElements.define('deck-stage'"); expect(next).toContain('(b) keyboard navigation'); // And the edit lands in the real notes block. expect(extractSpeakerNotesFromHtml(next)).toEqual(['Edited']); // Exactly one real speaker-notes element still exists. expect(next.match(/id="speaker-notes"/g)?.length).toBe(2); // one mention + one real tag }); it('escapes script-closing text inside presenter data', () => { const html = buildSpeakerNotesPresenterHtml({ previewHtml: '")', title: 'Deck', projectId: 'project-1', fileName: 'deck.html', notes: ['Do not close '], initialSlideIndex: 0, slideCount: 1, labels: { title: 'Speaker notes', edit: 'Edit', save: 'Save notes', pause: 'Pause', resume: 'Resume', reset: 'Reset', previous: 'Previous', next: 'Next', empty: 'Empty', slide: 'Slide {current} / {total}', }, }); expect(html).toContain('id="od-presenter-data"'); expect(html).not.toContain('Do not close '); expect(html).toContain('\\u003c/script>'); }); it('hides deck chrome inside presenter slide frames', () => { const html = buildSpeakerNotesPresenterHtml({ previewHtml: '', title: 'Deck', projectId: 'project-1', fileName: 'deck.html', notes: ['Intro'], initialSlideIndex: 0, slideCount: 1, labels: { title: 'Speaker notes', edit: 'Edit', save: 'Save notes', pause: 'Pause', resume: 'Resume', reset: 'Reset', previous: 'Previous', next: 'Next', empty: 'Empty', slide: 'Slide {current} / {total}', }, }); const match = /