Ocideck/test/slide_quality_analyzer_test.dart
Brenno de Winter 1fc4d25dcf
Some checks are pending
CI / Format · Analyze · Test (push) Waiting to run
Add slide quality checks for accessibility with export warnings.
Help authors spot missing alt text, low contrast, and dense slides in the editor, with an optional confirmation step before export when issues remain.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 08:57:18 +02:00

259 lines
6.8 KiB
Dart

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);
});
});
}