// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import { parseDeckThumbnails } from '../../src/runtime/deck-thumbnail-parser'; // Canonical OD framework deck: `.deck-shell > .deck-stage#deck-stage > // section.slide`, styles in a
${sections}
`; } describe('parseDeckThumbnails', () => { it('extracts slides, styles, ancestors and design size from a framework deck', () => { const parsed = parseDeckThumbnails(frameworkDeck(3), '/api/projects/p1/raw/'); expect(parsed.renderable).toBe(true); expect(parsed.slides).toHaveLength(3); expect(parsed.slides[0]).toMatch(/^
a.tag)).toEqual(['div', 'div']); // outer→inner: deck-shell then deck-stage expect(parsed.ancestors[0]!.attributes).toContainEqual(['class', 'deck-shell']); expect(parsed.ancestors[1]!.attributes).toContainEqual(['id', 'deck-stage']); }); it('rewrites :root / html / body selectors to :host', () => { const parsed = parseDeckThumbnails(frameworkDeck(1)); expect(parsed.styleText).toContain(':host { --bg: #fff'); expect(parsed.styleText).toContain(':host { background: var(--shell)'); expect(parsed.styleText).not.toMatch(/:root\s*\{/); // Compound selectors are left alone. expect(parsed.styleText).toContain('.deck-stage {'); }); it('rewrites :root to :host even when a CSS comment precedes it', () => { // Real decks lead their `
A
`; const parsed = parseDeckThumbnails(html); expect(parsed.renderable).toBe(true); // The custom properties must land on :host so they inherit into the slide. expect(parsed.styleText).toContain(':host { --stage-bg: #0a0a0a; --slide-bg: #ffffff; }'); expect(parsed.styleText).not.toMatch(/:root\s*\{/); }); it('absolutizes relative asset URLs against the base href', () => { const parsed = parseDeckThumbnails(frameworkDeck(1), '/api/projects/p1/raw/sub'); expect(parsed.slides[0]).toContain('src="/api/projects/p1/raw/sub/assets/pic-0.png"'); expect(parsed.styleText).toContain('url(/api/projects/p1/raw/sub/bg/hero.png)'); }); it('lifts @font-face out of the shadow styles into fontFaces', () => { const html = frameworkDeck(1).replace( '
A
B
`; const parsed = parseDeckThumbnails(html); expect(parsed.renderable).toBe(true); expect(parsed.slides).toHaveLength(2); expect(parsed.designWidth).toBe(1280); expect(parsed.designHeight).toBe(720); expect(parsed.ancestors.map((a) => a.tag)).toEqual(['deck-stage']); }); it('rewrites viewport units in CSS to canvas px (renderable, faithful)', () => { // No explicit px canvas → defaults to 1920×1080; 100vw→1920px, 100vh→1080px. const html = `
A
`; const parsed = parseDeckThumbnails(html); expect(parsed.renderable).toBe(true); expect(parsed.styleText).toContain('width: calc(100 * 19.2px)'); expect(parsed.styleText).toContain('height: calc(100 * 10.8px)'); expect(parsed.styleText).toContain('clamp(24px, calc(4 * 10.8px), 48px)'); expect(parsed.styleText).toContain('padding: calc(6 * 19.2px)'); expect(parsed.styleText).not.toMatch(/\d(?:vw|vh)\b/); }); it('rewrites viewport units in slide inline styles', () => { const html = `
bar
`; const parsed = parseDeckThumbnails(html); expect(parsed.renderable).toBe(true); expect(parsed.slides[0]).toContain('calc(12 * 10.8px)'); expect(parsed.slides[0]).not.toContain('12vh'); }); it('stays renderable for a fixed px canvas with percent-sized slides', () => { const html = `
A
`; const parsed = parseDeckThumbnails(html); expect(parsed.renderable).toBe(true); expect(parsed.designWidth).toBe(1920); // Percent sizing is left untouched — it already resolves to the canvas. expect(parsed.styleText).toContain('width: 100%'); }); it('falls back when the deck depends on an external layout stylesheet', () => { const html = frameworkDeck(1).replace( '', '', ); const parsed = parseDeckThumbnails(html); expect(parsed.renderable).toBe(false); expect(parsed.reason).toBe('external-stylesheet'); }); it('falls back for documents with no slides or no styles', () => { expect(parseDeckThumbnails('
not a deck
').reason).toBe('no-slides'); expect(parseDeckThumbnails('').reason).toBe('no-slides'); const styleless = '
A
'; expect(parseDeckThumbnails(styleless).reason).toBe('no-styles'); }); });