Ocideck/test/marp_html_service_test.dart
Brenno de Winter 67408c213c Improve chart rendering and resolve theme logo paths
Charts:
- Shrink axis label fonts and thin/space x-axis labels by actual pixel
  spacing so dense or long labels no longer overlap.
- Line tooltip shows only the point nearest the cursor instead of every
  series stacked vertically.
- Hovering a legend entry highlights its element: bar/line series fade the
  others (pie expands the matching slice), in app and presentation mode.
- Add optional min/max threshold lines per bar/line chart (ignored for pie),
  editable in the chart editor and drawn in both the live preview and the
  exported SVG.

Theme:
- Resolve relative logo paths in a ThemeProfile against the project path and
  home directory so deck logos load regardless of working directory.

Tests cover bound round-trip, editor fields, SVG bounds, legend-hover fading,
and bound-line rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:18:35 +02:00

198 lines
5.2 KiB
Dart

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<String> _diskLoader(String asset) => File(asset).readAsString();
Future<Uint8List> _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('<!doctype html>'));
// 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 <script src=...> tags.
expect(html, isNot(contains('<script src')));
});
test('build() neutralises a closing-script breakout in content', () async {
final service = MarpHtmlService(loadAsset: _diskLoader);
final html = await service.build('# X\n\nfoo </script> bar');
// The literal breakout must be escaped so it cannot terminate the payload.
expect(html, isNot(contains('foo </script> 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('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')));
});
}