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 = [
'
',
'',
'',
'',
].join('');
expect(extractSpeakerNotesFromHtml(html, 3)).toEqual(['Intro', 'Details']);
});
it('falls back to per-slide .notes blocks', () => {
const html = [
'',
'',
].join('');
expect(extractSpeakerNotesFromHtml(html, 2)).toEqual([
'Open\nstrong.',
'Close & transition.',
]);
});
it('upserts notes without mutating visible slide content', () => {
const source = '';
const next = upsertSpeakerNotesInHtml(source, ['Private note']);
expect(next).toContain('');
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 = [
'',
'',
].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 = [
'',
'',
'',
'',
'',
].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 = /