feat(j0.3): socle transverse (Result/Failure, theme, router, DI)

- core/error : Result<S> maison (Ok/Err) + Failure scellee avec egalite de valeur
- core/theme : AppTheme Material 3 (palette coucher, cibles tactiles enfant)
- core/router : routes nommees child/parentGate/parent (Navigator 1, placeholders)
- core/di : conventions providers
- CLAUDE.md §7 : Result maison & Navigator 1 actes (YAGNI)
- ROADMAP : 0.3 cochee, Jalon 0 termine
- corrections code review : egalite Failure, assertions tests, Map.unmodifiable

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vincent Bourdon
2026-06-19 17:30:45 +02:00
parent f9cdabfe98
commit 02a121703f
10 changed files with 325 additions and 15 deletions
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:storytime/core/error/failure.dart';
import 'package:storytime/core/error/result.dart';
void main() {
group('Result', () {
const failure = UnexpectedFailure('boom');
group('Ok', () {
test('map transforme la valeur', () {
const result = Ok<int>(1);
expect(result.map((v) => v + 1), equals(const Ok<int>(2)));
});
test('map préserve le type Ok et produit la bonne valeur', () {
const result = Ok<String>('hello');
final mapped = result.map((v) => v.length);
expect(mapped, isA<Ok<int>>());
expect((mapped as Ok<int>).value, equals(5));
});
test('fold appelle la branche ok', () {
const result = Ok<int>(42);
final out = result.fold(onOk: (v) => 'ok:$v', onErr: (_) => 'err');
expect(out, equals('ok:42'));
});
test('when appelle la branche ok et pas la branche err', () {
const result = Ok<int>(7);
var okCalled = false;
var errCalled = false;
result.when(ok: (_) => okCalled = true, err: (_) => errCalled = true);
expect(okCalled, isTrue);
expect(errCalled, isFalse);
});
});
group('Err', () {
test('map ne transforme pas — reste Err avec le même Failure', () {
const result = Err<int>(failure);
final mapped = result.map((v) => v + 1);
expect(mapped, isA<Err<int>>());
expect((mapped as Err<int>).failure, same(failure));
});
test('fold appelle la branche err', () {
const result = Err<int>(failure);
final out = result.fold(
onOk: (_) => 'ok',
onErr: (f) => 'err:${f.message}',
);
expect(out, equals('err:boom'));
});
test('when appelle la branche err et pas la branche ok', () {
const result = Err<int>(failure);
var okCalled = false;
var errCalled = false;
result.when(ok: (_) => okCalled = true, err: (_) => errCalled = true);
expect(errCalled, isTrue);
expect(okCalled, isFalse);
});
});
group('Failure sous-types', () {
test('NetworkFailure expose le message', () {
const f = NetworkFailure('timeout');
expect(f.message, equals('timeout'));
});
test('NotFoundFailure expose le message', () {
const f = NotFoundFailure('episode 42');
expect(f.message, equals('episode 42'));
});
test('UnexpectedFailure expose le message', () {
const f = UnexpectedFailure('oops');
expect(f.message, equals('oops'));
});
});
});
}
+40
View File
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:storytime/core/theme/app_theme.dart';
void main() {
group('AppTheme', () {
testWidgets('useMaterial3 est activé', (tester) async {
await tester.pumpWidget(
MaterialApp(theme: AppTheme.light(), home: const SizedBox.shrink()),
);
final theme = Theme.of(tester.element(find.byType(SizedBox)));
expect(theme.useMaterial3, isTrue);
});
testWidgets('la couleur primaire correspond à la palette coucher', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(theme: AppTheme.light(), home: const SizedBox.shrink()),
);
final theme = Theme.of(tester.element(find.byType(SizedBox)));
// Palette « coucher » : violet/indigo doux.
// On vérifie que la teinte de seed est bien dans le spectre attendu.
final primary = theme.colorScheme.primary;
// HSL : le bleu/violet doit dominer sur le rouge et le vert.
final hsl = HSLColor.fromColor(primary);
expect(
hsl.hue,
inInclusiveRange(200.0, 320.0),
reason: 'La couleur primaire doit être dans le spectre bleu-violet',
);
});
test('AppTheme.light() retourne un ThemeData non nul', () {
expect(AppTheme.light(), isA<ThemeData>());
});
});
}