Presentation fixes: - Mirror the in-progress pen/highlighter stroke to the audience window live (new 'inkLive' channel) so highlights appear as they are drawn, not only after the pen lifts. - Cover the macOS menu bar on the beamer: raise the audience window above .mainMenu level so the Apple/Wi-Fi strip no longer shows during a presentation. Styling no longer lives in the file: - generateDeck no longer embeds the ThemeProfile; a saved .md holds only content. The profile is inlined only for the transient audience-window payload (inlineStyleProfile: true), never to disk. - On open, the app applies the active style profile (FileService.openDeck / activeProfileFor, DeckNotifier.loadDeck); applyMarkdown preserves the current profile. Quality pass / tests green: - Complete the consent-screen translations (English plus 7 missing strings per other language). - Pass the consent gate in widget/ui-scale tests by seeding the consent key, so the app shell renders. - Update markdown round-trip tests for the new default and add coverage for live stroke streaming and styling-free saves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
185 lines
6 KiB
Dart
185 lines
6 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:ocideck/models/deck.dart';
|
|
import 'package:ocideck/models/settings.dart';
|
|
import 'package:ocideck/models/slide.dart';
|
|
import 'package:ocideck/services/markdown_service.dart';
|
|
|
|
void main() {
|
|
test('round-trips image slide with title as image slide', () {
|
|
final service = MarkdownService();
|
|
final markdown = service.generateDeck(
|
|
Deck(
|
|
title: 'Demo',
|
|
slides: [
|
|
Slide.create(
|
|
SlideType.image,
|
|
).copyWith(title: 'Overlay title', imagePath: 'images/photo.png'),
|
|
],
|
|
),
|
|
);
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
expect(deck, isNotNull);
|
|
expect(deck!.slides.single.type, SlideType.image);
|
|
expect(deck.slides.single.title, 'Overlay title');
|
|
expect(deck.slides.single.imagePath, 'images/photo.png');
|
|
});
|
|
|
|
test('round-trips bulletsImage slide with image and size', () {
|
|
final service = MarkdownService();
|
|
final markdown = service.generateDeck(
|
|
Deck(
|
|
title: 'Demo',
|
|
slides: [
|
|
Slide.create(SlideType.bulletsImage).copyWith(
|
|
title: 'Profiel',
|
|
bullets: ['Eerste punt', '\tGenest punt'],
|
|
imagePath: 'images/portret.png',
|
|
imageCaption: 'Een onderschrift',
|
|
imageSize: 45,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
expect(deck, isNotNull);
|
|
final slide = deck!.slides.single;
|
|
expect(slide.type, SlideType.bulletsImage);
|
|
expect(slide.title, 'Profiel');
|
|
expect(slide.imagePath, 'images/portret.png');
|
|
expect(slide.imageCaption, 'Een onderschrift');
|
|
expect(slide.imageSize, 45);
|
|
expect(slide.bullets, ['Eerste punt', '\tGenest punt']);
|
|
});
|
|
|
|
test('keeps a plain image inside free markdown as free markdown', () {
|
|
final service = MarkdownService();
|
|
final deck = service.parseDeck(
|
|
'---\nmarp: true\ntheme: vigilis\n---\n\n'
|
|
'\n\nWat losse tekst.\n',
|
|
);
|
|
|
|
expect(deck, isNotNull);
|
|
final slide = deck!.slides.single;
|
|
expect(slide.type, SlideType.freeMarkdown);
|
|
expect(slide.imagePath, isEmpty);
|
|
});
|
|
|
|
test('round-trips deck style profile', () {
|
|
final service = MarkdownService();
|
|
final profile = const ThemeProfile(
|
|
name: 'Klant A',
|
|
slideBackgroundColor: '#111827',
|
|
textColor: '#F8FAFC',
|
|
accentColor: '#F59E0B',
|
|
tableTextColor: '#111111',
|
|
tableHeaderTextColor: '#EEEEEE',
|
|
logoPosition: 'top-left',
|
|
logoSize: 120,
|
|
fontFamily: 'Georgia',
|
|
footerText: 'Vertrouwelijk · {page}/{total}',
|
|
footerShowPageNumbers: true,
|
|
footerPosition: 'center',
|
|
closingSlideEnabled: true,
|
|
closingSlideMarkdown: '# Einde\n\nDank voor jullie aandacht.',
|
|
);
|
|
|
|
// The style profile only travels inside the markdown when explicitly
|
|
// inlined (transient beamer payloads); a plain save keeps the file clean.
|
|
final markdown = service.generateDeck(
|
|
Deck(
|
|
title: 'Demo',
|
|
themeProfile: profile,
|
|
slides: [Slide.create(SlideType.title).copyWith(title: 'Demo')],
|
|
),
|
|
inlineStyleProfile: true,
|
|
);
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
expect(deck, isNotNull);
|
|
expect(deck!.themeProfile.name, 'Klant A');
|
|
expect(deck.themeProfile.slideBackgroundColor, '#111827');
|
|
expect(deck.themeProfile.tableTextColor, '#111111');
|
|
expect(deck.themeProfile.tableHeaderTextColor, '#EEEEEE');
|
|
expect(deck.themeProfile.logoPosition, 'top-left');
|
|
expect(deck.themeProfile.logoSize, 120);
|
|
expect(deck.themeProfile.fontFamily, 'Georgia');
|
|
expect(deck.themeProfile.footerText, 'Vertrouwelijk · {page}/{total}');
|
|
expect(deck.themeProfile.footerShowPageNumbers, isTrue);
|
|
expect(deck.themeProfile.footerPosition, 'center');
|
|
expect(deck.themeProfile.closingSlideEnabled, isTrue);
|
|
expect(
|
|
deck.themeProfile.closingSlideMarkdown,
|
|
'# Einde\n\nDank voor jullie aandacht.',
|
|
);
|
|
});
|
|
|
|
test('a saved deck does not embed the style profile', () {
|
|
final service = MarkdownService();
|
|
final markdown = service.generateDeck(
|
|
Deck(
|
|
title: 'Demo',
|
|
themeProfile: const ThemeProfile(
|
|
name: 'Klant A',
|
|
slideBackgroundColor: '#111827',
|
|
),
|
|
slides: [Slide.create(SlideType.title).copyWith(title: 'Demo')],
|
|
),
|
|
);
|
|
|
|
// The file is the base content only; styling stays out of it. Parsing it
|
|
// back yields the default profile, not the one that was saved.
|
|
expect(markdown.contains('ocideck_style_profile'), isFalse);
|
|
final deck = service.parseDeck(markdown);
|
|
expect(deck!.themeProfile.name, 'Standaard');
|
|
expect(deck.themeProfile.slideBackgroundColor, isNot('#111827'));
|
|
});
|
|
|
|
test('adds logo-safe class when deck profile has logo', () {
|
|
final service = MarkdownService();
|
|
final markdown = service.generateDeck(
|
|
Deck(
|
|
title: 'Demo',
|
|
themeProfile: const ThemeProfile(logoPath: '/tmp/logo.png'),
|
|
slides: [
|
|
Slide.create(
|
|
SlideType.bullets,
|
|
).copyWith(title: 'Demo', bullets: ['Een lange bullet']),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(markdown, contains('<!-- _class: logo-safe -->'));
|
|
});
|
|
|
|
test('round-trips video slide with audio attachment', () {
|
|
final service = MarkdownService();
|
|
final markdown = service.generateDeck(
|
|
Deck(
|
|
title: 'Media',
|
|
slides: [
|
|
Slide.create(SlideType.video).copyWith(
|
|
title: 'Film',
|
|
videoPath: 'media/movie.mp4',
|
|
videoAutoplay: true,
|
|
audioPath: 'media/narration.mp3',
|
|
audioAutoplay: true,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
expect(deck, isNotNull);
|
|
expect(deck!.slides.single.type, SlideType.video);
|
|
expect(deck.slides.single.videoPath, 'media/movie.mp4');
|
|
expect(deck.slides.single.videoAutoplay, isTrue);
|
|
expect(deck.slides.single.audioPath, 'media/narration.mp3');
|
|
expect(deck.slides.single.audioAutoplay, isTrue);
|
|
});
|
|
}
|