import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:ocideck/models/settings.dart'; import 'package:ocideck/services/marp_html_service.dart'; /// Reads the vendored libraries straight from the repo (tests run at the root). Future _diskLoader(String asset) => File(asset).readAsString(); Future _diskBytes(String asset) => File(asset).readAsBytes(); void main() { group('marpSlides', () { test('drops the YAML front-matter and splits on --- separators', () { const md = ''' --- marp: true theme: ocideck --- # Slide one --- ## Slide two '''; final slides = MarpHtmlService.marpSlides(md); expect(slides, hasLength(2)); expect(slides[0], contains('# Slide one')); expect(slides[0], isNot(contains('marp: true'))); expect(slides[1], contains('## Slide two')); }); test('a deck without front-matter keeps every slide', () { final slides = MarpHtmlService.marpSlides( '# A\n\n---\n\n# B\n\n---\n\n# C', ); expect(slides, hasLength(3)); }); }); test('build() inlines the libraries and the slide content', () async { final service = MarpHtmlService(loadAsset: _diskLoader); const md = ''' --- marp: true --- # Titel \$\$E=mc^2\$\$ ```dart void main() {} ``` '''; final html = await service.build(md); expect(html, startsWith('')); // Slide payload is embedded for the in-browser renderer. expect(html, contains('# Titel')); expect(html, contains(r'E=mc^2')); // Each engine is inlined (offline): marked, highlight.js, MathJax, mermaid. expect(html, contains('marked')); expect(html, contains('hljs')); expect(html, contains('MathJax')); expect(html, contains('mermaid')); // Everything is inlined: there must be no external bar'); // The literal breakout must be escaped so it cannot terminate the payload. expect(html, isNot(contains('foo bar'))); expect(html, contains(r'<\/script')); }); test('a theme colours the slides with the profile palette', () async { final service = MarpHtmlService( loadAsset: _diskLoader, loadBytes: _diskBytes, ); const theme = ThemeProfile( slideBackgroundColor: '#102030', textColor: '#EEF1F4', accentColor: '#33CC99', fontFamily: 'Arial', ); final html = await service.build('# Titel', theme: theme); expect(html, contains('background:#102030')); expect(html, contains('color:#EEF1F4')); expect(html, contains('#33CC99')); expect(html, contains("'Arial'")); // A system font is not embedded as base64. expect(html, isNot(contains('data:font/ttf;base64,'))); }); test('code blocks use the themed code colours in the export CSS', () async { final service = MarpHtmlService( loadAsset: _diskLoader, loadBytes: _diskBytes, ); const theme = ThemeProfile( codeBackgroundColor: '#000000', codeTextColor: '#33FF33', ); final html = await service.build('```dart\nvoid main() {}\n```', theme: theme); expect(html, contains('.slide pre{background:#000000;color:#33FF33')); expect(html, contains('.slide pre code{color:#33FF33')); }); test('EB Garamond theme embeds the font for offline rendering', () async { final service = MarpHtmlService( loadAsset: _diskLoader, loadBytes: _diskBytes, ); const theme = ThemeProfile(fontFamily: 'EB Garamond'); final html = await service.build('# Titel', theme: theme); expect(html, contains('@font-face')); expect(html, contains('data:font/ttf;base64,')); expect(html, contains("'EB Garamond'")); }); test('pie chart SVG renders every series and label', () { const slide = ''' ```chart { "type": "pie", "x": ["Team A", "Team B"], "series": [ {"name": "Gereed", "color": "#10B981", "data": [70, 40]}, {"name": "Open", "color": "#EF4444", "data": [30, 60]} ] } ``` '''; final html = MarpHtmlService.renderChartBlocks(slide); expect(html, contains('Team A')); expect(html, contains('Team B')); expect(html, contains('Gereed')); expect(html, contains('Open')); expect(html, contains('#003399')); expect(html, contains('#FFCC00')); }); test('pie chart SVG renders at most two series', () { const slide = ''' ```chart { "type": "pie", "x": ["A", "B"], "series": [ {"name": "Een", "data": [1, 2]}, {"name": "Twee", "data": [2, 3]}, {"name": "Drie", "data": [3, 4]} ] } ``` '''; final html = MarpHtmlService.renderChartBlocks(slide); expect(html, contains('Een')); expect(html, contains('Twee')); expect(html, isNot(contains('Drie'))); }); test('bar chart SVG draws optional min/max bound lines with labels', () { const slide = ''' ```chart { "type": "bar", "x": ["Q1", "Q2"], "series": [{"name": "Omzet", "data": [10, 14]}], "minBound": 5, "maxBound": 20 } ``` '''; final html = MarpHtmlService.renderChartBlocks(slide); expect(html, contains('stroke-dasharray')); expect(html, contains('min 5')); expect(html, contains('max 20')); }); test('pie chart SVG never draws bound lines', () { const slide = ''' ```chart { "type": "pie", "x": ["A", "B"], "series": [{"name": "Een", "data": [1, 2]}], "minBound": 5, "maxBound": 20 } ``` '''; final html = MarpHtmlService.renderChartBlocks(slide); expect(html, isNot(contains('stroke-dasharray'))); expect(html, isNot(contains('min 5'))); }); test('radar chart SVG draws a polygon per series with axis labels', () { const slide = ''' ```chart { "type": "radar", "x": ["Snelheid", "Kracht", "Uithouding"], "series": [ {"name": "A", "color": "#2563EB", "data": [3, 4, 5]}, {"name": "B", "color": "#EF4444", "data": [5, 2, 3]} ] } ``` '''; final html = MarpHtmlService.renderChartBlocks(slide); expect(html, contains('