Ocideck/test/slide_quality_analyzer_test.dart

260 lines
6.8 KiB
Dart
Raw Normal View History

import 'package:flutter_test/flutter_test.dart';
import 'package:ocideck/models/chart.dart';
import 'package:ocideck/models/deck.dart';
import 'package:ocideck/models/markdown_validation.dart';
import 'package:ocideck/models/settings.dart';
import 'package:ocideck/models/slide.dart';
import 'package:ocideck/models/slide_quality.dart';
import 'package:ocideck/services/slide_layout_metrics.dart';
import 'package:ocideck/services/slide_quality_analyzer.dart';
import 'package:ocideck/utils/color_contrast.dart';
void main() {
const analyzer = SlideQualityAnalyzer();
group('color_contrast', () {
test('black on white meets AA for normal text', () {
expect(meetsWcagAa('#000000', '#FFFFFF'), isTrue);
expect(hexContrastRatio('#000000', '#FFFFFF'), closeTo(21.0, 0.5));
});
test('light gray on white fails AA for normal text', () {
expect(meetsWcagAa('#CCCCCC', '#FFFFFF'), isFalse);
});
test('invalid hex returns null ratio', () {
expect(hexContrastRatio('not-a-color', '#FFFFFF'), isNull);
});
});
group('SlideQualityAnalyzer', () {
test('clean deck has no issues', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.bullets).copyWith(
title: 'Kort',
bullets: ['Eerste punt'],
),
],
);
final result = analyzer.analyze(deck);
expect(result.hasIssues, isFalse);
});
test('detects missing image caption', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.image).copyWith(
imagePath: 'images/photo.jpg',
),
],
);
final result = analyzer.analyze(deck);
expect(
result.issues.any(
(i) =>
i.kind == SlideQualityIssueKind.missingAltCaption &&
i.field == 'imageCaption',
),
isTrue,
);
});
test('does not warn when image caption is present', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.image).copyWith(
imagePath: 'images/photo.jpg',
imageCaption: 'Teamfoto 2024',
),
],
);
final result = analyzer.analyze(deck);
expect(
result.issues.where((i) => i.category == SlideQualityCategory.altText),
isEmpty,
);
});
test('detects low theme body contrast as deck-wide issue', () {
final deck = Deck(
title: 'Demo',
themeProfile: const ThemeProfile(
textColor: '#CCCCCC',
slideBackgroundColor: '#FFFFFF',
),
slides: [Slide.create(SlideType.bullets)],
);
final result = analyzer.analyze(deck);
expect(
result.issues.any(
(i) => i.isDeckWide && i.kind == SlideQualityIssueKind.themeContrast,
),
isTrue,
);
});
test('detects dense bullet slide text', () {
final longBullet = 'Lorem ipsum dolor sit amet, ' * 8;
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.bullets).copyWith(
title: 'Overvol',
bullets: List.generate(14, (_) => longBullet),
),
],
);
final result = analyzer.analyze(deck);
expect(
result.issues.any(
(i) => i.category == SlideQualityCategory.textDensity,
),
isTrue,
);
});
test('critical text density is reported as error', () {
final longBullet = 'Woord ' * 60;
final slide = Slide.create(SlideType.bullets).copyWith(
title: 'Extreem ' * 20,
subtitle: 'Ondertitel ' * 20,
bullets: List.generate(30, (_) => longBullet),
);
expect(
bulletsSlideFitScale(slide: slide, font: 'Arial'),
lessThanOrEqualTo(kTextDensityCriticalScale + 0.001),
);
final result = analyzer.analyzeSlides(
slides: [slide],
theme: const ThemeProfile(),
font: 'Arial',
);
expect(
result.issues.any(
(i) =>
i.kind == SlideQualityIssueKind.textDensityCritical &&
i.severity == MarkdownValidationSeverity.error,
),
isTrue,
);
});
test('detects chart without title or descriptive data', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.chart).copyWith(
customMarkdown: const ChartSpec().toBlock(),
),
],
);
final result = analyzer.analyze(deck);
expect(
result.issues.any(
(i) => i.kind == SlideQualityIssueKind.chartMissingDescription,
),
isTrue,
);
});
test('accepts chart with title', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.chart).copyWith(
customMarkdown: const ChartSpec(title: 'Omzet').toBlock(),
),
],
);
final result = analyzer.analyze(deck);
expect(
result.issues.where((i) => i.category == SlideQualityCategory.altText),
isEmpty,
);
});
test('detects video without descriptive text', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.video).copyWith(
videoPath: 'media/demo.mp4',
),
],
);
final result = analyzer.analyze(deck);
expect(
result.issues.any(
(i) => i.kind == SlideQualityIssueKind.mediaMissingDescription,
),
isTrue,
);
});
test('warns about contrast on title slide with background image', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.title).copyWith(
title: 'Welkom',
imagePath: 'images/bg.jpg',
imageCaption: 'Achtergrond',
),
],
);
final result = analyzer.analyze(deck);
expect(
result.issues.any(
(i) => i.kind == SlideQualityIssueKind.imageContrastUnverified,
),
isTrue,
);
});
test('skips skipped slides', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.image).copyWith(
imagePath: 'images/hidden.jpg',
skipped: true,
),
],
);
final result = analyzer.analyze(deck);
expect(result.hasIssues, isFalse);
});
test('forSlide returns only matching slide issues', () {
final deck = Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.bullets),
Slide.create(SlideType.image).copyWith(
imagePath: 'images/photo.jpg',
),
],
);
final result = analyzer.analyze(deck);
expect(result.forSlide(0), isEmpty);
expect(result.forSlide(1), isNotEmpty);
});
});
}