test: add pumpApp helpers, fakes and coverage matrix template
Refs #31 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
936ca7bd4b
commit
adf61b3ba2
8 changed files with 369 additions and 46 deletions
44
app/test/README.md
Normal file
44
app/test/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Tests der App
|
||||||
|
|
||||||
|
Unit- und Widget-Tests spiegeln die Struktur von `lib/`. Integration (`integration_test/`)
|
||||||
|
und Web-E2E (Playwright) kommen mit den jeweiligen Feature-Tasks.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter test --coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helfer (`test/helpers/`)
|
||||||
|
|
||||||
|
| Datei | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| `pump_app.dart` | `pumpApp(tester, home: …)` bzw. `router: …`: MaterialApp mit AppTheme, l10n-Delegates, ProviderScope, wählbare `locale` (Standard `de`), `textScale`, `themeMode`. `pumpShuffleApp(tester, deviceLocale: …)` startet die echte `ShuffleApp`. Beide geben den `ProviderContainer` zurück. |
|
||||||
|
| `fakes.dart` | `MockSupabaseClient`, `MockGoTrueClient`, `SupabaseMocks` (Client mit verdrahtetem `auth`), `testAppConfig(…)`, `testOverrides(…)` (feste AppConfig + Supabase-Mock, weitere Overrides anhängbar). |
|
||||||
|
| `router_helpers.dart` | `buildTestRouter`, `FakeRedirectSource`, `currentPath`, `currentUri`. |
|
||||||
|
|
||||||
|
Regeln: keine echten Netzwerkaufrufe, feste Zeit (`clock`/`fakeAsync`), feste Zufallswerte.
|
||||||
|
Schlägt ein Test wegen eines Produktivcode-Fehlers fehl, wird der Test nicht angepasst,
|
||||||
|
sondern der Fehler an den Entwickler-Agent zurückgegeben.
|
||||||
|
|
||||||
|
## Benennung mit Akzeptanzkriterien
|
||||||
|
|
||||||
|
Tests, die ein Akzeptanzkriterium belegen, tragen die Referenz `KTO-01 Kriterium 3`
|
||||||
|
(Story-ID, Kriterium-Nummer wie im Issue) am Anfang des Namens:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
group('KTO-01 Kriterium 3: Passwort-Richtlinie', () {
|
||||||
|
test('lehnt Passwörter unter 10 Zeichen ab', () { … });
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('KTO-01 Kriterium 7: zeigt bei falschen Zugangsdaten eine allgemeine Meldung', …);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Eine Referenz pro `group`/Test; deckt ein Test mehrere Kriterien ab, alle nennen
|
||||||
|
(`KTO-01 Kriterium 1, 2`).
|
||||||
|
- Konfigurationsabhängige Tests nennen zusätzlich die Konfig-ID (`AUTH-CFG-10`).
|
||||||
|
- Sprache der Testnamen einheitlich Englisch (wie der Code); die Referenz bleibt unverändert.
|
||||||
|
- Suche nach Abdeckung: `grep -rn "KTO-01 Kriterium 3" test integration_test ../supabase/tests ../e2e`.
|
||||||
|
|
||||||
|
## Abdeckungsmatrix
|
||||||
|
|
||||||
|
Jeder PR enthält eine Matrix Akzeptanzkriterium → Test(s). Vorlage:
|
||||||
|
`docs/templates/abdeckungsmatrix.md`.
|
||||||
|
|
@ -1,32 +1,20 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shuffle/app.dart';
|
|
||||||
|
|
||||||
Future<void> _pumpApp(WidgetTester tester) async {
|
import 'helpers/pump_app.dart';
|
||||||
await tester.pumpWidget(const ProviderScope(child: ShuffleApp()));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
tearDown(() {
|
|
||||||
TestWidgetsFlutterBinding.instance.platformDispatcher
|
|
||||||
.clearLocalesTestValue();
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('app starts on the placeholder home page (English fallback)', (
|
testWidgets('app starts on the placeholder home page (English fallback)', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
tester.platformDispatcher.localesTestValue = [const Locale('fr')];
|
await pumpShuffleApp(tester, deviceLocale: const Locale('fr'));
|
||||||
await _pumpApp(tester);
|
|
||||||
|
|
||||||
expect(find.byKey(const Key('home_screen')), findsOneWidget);
|
expect(find.byKey(const Key('home_screen')), findsOneWidget);
|
||||||
expect(find.textContaining('Welcome to Shuffle'), findsOneWidget);
|
expect(find.textContaining('Welcome to Shuffle'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('app shows German texts on a German device', (tester) async {
|
testWidgets('app shows German texts on a German device', (tester) async {
|
||||||
tester.platformDispatcher.localesTestValue = [const Locale('de', 'DE')];
|
await pumpShuffleApp(tester, deviceLocale: const Locale('de', 'DE'));
|
||||||
await _pumpApp(tester);
|
|
||||||
|
|
||||||
expect(find.textContaining('Willkommen bei Shuffle'), findsOneWidget);
|
expect(find.textContaining('Willkommen bei Shuffle'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,17 @@
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:shuffle/app.dart';
|
|
||||||
import 'package:shuffle/core/routing/app_router.dart';
|
import 'package:shuffle/core/routing/app_router.dart';
|
||||||
import 'package:shuffle/core/routing/redirect_source.dart';
|
import 'package:shuffle/core/routing/redirect_source.dart';
|
||||||
|
|
||||||
class _FakeRedirectSource implements RedirectSource {
|
import '../../helpers/fakes.dart';
|
||||||
final notifier = ValueNotifier<int>(0);
|
import '../../helpers/pump_app.dart';
|
||||||
String? target;
|
import '../../helpers/router_helpers.dart';
|
||||||
|
|
||||||
@override
|
|
||||||
Listenable get changes => notifier;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String? redirect(GoRouterState state) =>
|
|
||||||
state.uri.path == '/blocked' ? target : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('unknown path shows the 404 page and leads back home', (
|
testWidgets('unknown path shows the 404 page and leads back home', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final container = ProviderContainer.test();
|
final container = await pumpShuffleApp(tester);
|
||||||
await tester.pumpWidget(
|
|
||||||
UncontrolledProviderScope(
|
|
||||||
container: container,
|
|
||||||
child: const ShuffleApp(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
container.read(appRouterProvider).go('/does-not-exist');
|
container.read(appRouterProvider).go('/does-not-exist');
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|
@ -42,20 +24,18 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('redirect source decides where a route leads', (tester) async {
|
testWidgets('redirect source decides where a route leads', (tester) async {
|
||||||
final source = _FakeRedirectSource()..target = '/';
|
final source = FakeRedirectSource({'/blocked': '/'});
|
||||||
final container = ProviderContainer.test(
|
final container = await pumpShuffleApp(
|
||||||
|
tester,
|
||||||
|
overrides: testOverrides(
|
||||||
overrides: [redirectSourceProvider.overrideWithValue(source)],
|
overrides: [redirectSourceProvider.overrideWithValue(source)],
|
||||||
);
|
|
||||||
await tester.pumpWidget(
|
|
||||||
UncontrolledProviderScope(
|
|
||||||
container: container,
|
|
||||||
child: const ShuffleApp(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
container.read(appRouterProvider).go('/blocked');
|
final router = container.read(appRouterProvider)..go('/blocked');
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byKey(const Key('home_screen')), findsOneWidget);
|
expect(find.byKey(const Key('home_screen')), findsOneWidget);
|
||||||
|
expect(currentPath(router), '/');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
app/test/helpers/fakes.dart
Normal file
55
app/test/helpers/fakes.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'package:flutter_riverpod/misc.dart' show Override;
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:shuffle/core/config/app_config.dart';
|
||||||
|
import 'package:shuffle/core/config/app_config_provider.dart';
|
||||||
|
import 'package:shuffle/core/supabase/supabase_client_provider.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
/// Mock des Supabase-Clients. Kein Netzwerk: nicht gestubbte Aufrufe schlagen
|
||||||
|
/// mit einem mocktail-Fehler fehl, statt still zu laufen.
|
||||||
|
class MockSupabaseClient extends Mock implements SupabaseClient {}
|
||||||
|
|
||||||
|
/// Mock der Auth-Schnittstelle (`client.auth`).
|
||||||
|
class MockGoTrueClient extends Mock implements GoTrueClient {}
|
||||||
|
|
||||||
|
/// Feste Test-Konfiguration ohne echte Zugangsdaten.
|
||||||
|
AppConfig testAppConfig({
|
||||||
|
String environment = 'test',
|
||||||
|
String supabaseUrl = 'http://localhost:54321',
|
||||||
|
String supabaseAnonKey = 'test-anon-key',
|
||||||
|
String siteUrl = 'http://localhost:3000',
|
||||||
|
String legalPrivacyUrl = 'https://example.test/privacy',
|
||||||
|
String legalTermsUrl = 'https://example.test/terms',
|
||||||
|
String termsVersion = 'test-1',
|
||||||
|
}) => AppConfig(
|
||||||
|
environment: environment,
|
||||||
|
supabaseUrl: supabaseUrl,
|
||||||
|
supabaseAnonKey: supabaseAnonKey,
|
||||||
|
siteUrl: siteUrl,
|
||||||
|
legalPrivacyUrl: legalPrivacyUrl,
|
||||||
|
legalTermsUrl: legalTermsUrl,
|
||||||
|
termsVersion: termsVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Ein Mock-Client samt Auth-Mock: `client.auth` liefert [auth].
|
||||||
|
class SupabaseMocks {
|
||||||
|
SupabaseMocks() {
|
||||||
|
when(() => client.auth).thenReturn(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
final client = MockSupabaseClient();
|
||||||
|
final auth = MockGoTrueClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard-Overrides für Tests: feste [AppConfig] und ein Supabase-Mock.
|
||||||
|
///
|
||||||
|
/// Zusätzliche [overrides] kommen hinten dran und haben Vorrang.
|
||||||
|
List<Override> testOverrides({
|
||||||
|
AppConfig? config,
|
||||||
|
SupabaseClient? supabase,
|
||||||
|
List<Override> overrides = const [],
|
||||||
|
}) => [
|
||||||
|
appConfigProvider.overrideWithValue(config ?? testAppConfig()),
|
||||||
|
supabaseClientProvider.overrideWithValue(supabase ?? MockSupabaseClient()),
|
||||||
|
...overrides,
|
||||||
|
];
|
||||||
88
app/test/helpers/helpers_test.dart
Normal file
88
app/test/helpers/helpers_test.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:shuffle/core/config/app_config_provider.dart';
|
||||||
|
import 'package:shuffle/core/l10n/l10n.dart';
|
||||||
|
import 'package:shuffle/core/supabase/supabase_client_provider.dart';
|
||||||
|
|
||||||
|
import 'fakes.dart';
|
||||||
|
import 'pump_app.dart';
|
||||||
|
import 'router_helpers.dart';
|
||||||
|
|
||||||
|
class _L10nProbe extends StatelessWidget {
|
||||||
|
const _L10nProbe();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) =>
|
||||||
|
Scaffold(body: Text(context.l10n.homePlaceholderMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('pumpApp', () {
|
||||||
|
testWidgets('renders German texts by default', (tester) async {
|
||||||
|
await pumpApp(tester, home: const _L10nProbe());
|
||||||
|
expect(find.textContaining('Willkommen bei Shuffle'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('renders English texts for Locale en', (tester) async {
|
||||||
|
await pumpApp(
|
||||||
|
tester,
|
||||||
|
home: const _L10nProbe(),
|
||||||
|
locale: const Locale('en'),
|
||||||
|
);
|
||||||
|
expect(find.textContaining('Welcome to Shuffle'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('falls back to English for unsupported locales', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await pumpApp(
|
||||||
|
tester,
|
||||||
|
home: const _L10nProbe(),
|
||||||
|
locale: const Locale('fr'),
|
||||||
|
);
|
||||||
|
expect(find.textContaining('Welcome to Shuffle'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('applies the text scale factor', (tester) async {
|
||||||
|
await pumpApp(tester, home: const _L10nProbe(), textScale: 2);
|
||||||
|
final context = tester.element(find.byType(_L10nProbe));
|
||||||
|
expect(MediaQuery.textScalerOf(context).scale(10), 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('provides test AppConfig and Supabase mock by default', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final container = await pumpApp(tester, home: const _L10nProbe());
|
||||||
|
expect(container.read(appConfigProvider).termsVersion, 'test-1');
|
||||||
|
expect(container.read(supabaseClientProvider), isA<MockSupabaseClient>());
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('accepts a custom GoRouter', (tester) async {
|
||||||
|
final router = buildTestRouter(
|
||||||
|
routes: [
|
||||||
|
GoRoute(path: '/', builder: (_, _) => const _L10nProbe()),
|
||||||
|
GoRoute(path: '/other', builder: (_, _) => const Text('other')),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
addTearDown(router.dispose);
|
||||||
|
await pumpApp(tester, router: router);
|
||||||
|
|
||||||
|
router.go('/other');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(currentPath(router), '/other');
|
||||||
|
expect(find.text('other'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('SupabaseMocks', () {
|
||||||
|
test('client.auth returns the auth mock', () {
|
||||||
|
final mocks = SupabaseMocks();
|
||||||
|
when(() => mocks.auth.currentSession).thenReturn(null);
|
||||||
|
|
||||||
|
expect(mocks.client.auth.currentSession, isNull);
|
||||||
|
verify(() => mocks.auth.currentSession).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
103
app/test/helpers/pump_app.dart
Normal file
103
app/test/helpers/pump_app.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_riverpod/misc.dart' show Override;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:shuffle/app.dart';
|
||||||
|
import 'package:shuffle/core/l10n/app_locale.dart';
|
||||||
|
import 'package:shuffle/core/theme/app_theme.dart';
|
||||||
|
import 'package:shuffle/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
import 'fakes.dart';
|
||||||
|
|
||||||
|
/// Pumpt ein einzelnes Widget ([home]) oder einen eigenen [router] in eine
|
||||||
|
/// Test-App mit AppTheme, l10n-Delegates, ProviderScope und fester [locale].
|
||||||
|
///
|
||||||
|
/// Genau eines von [home] und [router] ist anzugeben. Ohne [overrides]
|
||||||
|
/// gelten [testOverrides] (feste AppConfig, Supabase-Mock). Gibt den
|
||||||
|
/// [ProviderContainer] zurück, um Provider im Test zu lesen.
|
||||||
|
Future<ProviderContainer> pumpApp(
|
||||||
|
WidgetTester tester, {
|
||||||
|
Widget? home,
|
||||||
|
GoRouter? router,
|
||||||
|
Locale locale = const Locale('de'),
|
||||||
|
List<Override>? overrides,
|
||||||
|
ThemeMode themeMode = ThemeMode.light,
|
||||||
|
double? textScale,
|
||||||
|
bool settle = true,
|
||||||
|
}) async {
|
||||||
|
assert(
|
||||||
|
(home == null) != (router == null),
|
||||||
|
'Entweder home oder router angeben.',
|
||||||
|
);
|
||||||
|
final container = ProviderContainer.test(
|
||||||
|
overrides: overrides ?? testOverrides(),
|
||||||
|
);
|
||||||
|
Widget builder(BuildContext context, Widget? child) => textScale == null
|
||||||
|
? child!
|
||||||
|
: MediaQuery(
|
||||||
|
data: MediaQuery.of(context)
|
||||||
|
.copyWith(textScaler: TextScaler.linear(textScale)),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
|
||||||
|
const delegates = <LocalizationsDelegate<dynamic>>[
|
||||||
|
AppLocalizations.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
];
|
||||||
|
final theme = AppTheme.light();
|
||||||
|
final darkTheme = AppTheme.dark();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: router != null
|
||||||
|
? MaterialApp.router(
|
||||||
|
routerConfig: router,
|
||||||
|
theme: theme,
|
||||||
|
darkTheme: darkTheme,
|
||||||
|
themeMode: themeMode,
|
||||||
|
locale: locale,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
localizationsDelegates: delegates,
|
||||||
|
localeResolutionCallback: (l, _) => resolveAppLocale(l),
|
||||||
|
builder: builder,
|
||||||
|
)
|
||||||
|
: MaterialApp(
|
||||||
|
home: home,
|
||||||
|
theme: theme,
|
||||||
|
darkTheme: darkTheme,
|
||||||
|
themeMode: themeMode,
|
||||||
|
locale: locale,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
localizationsDelegates: delegates,
|
||||||
|
localeResolutionCallback: (l, _) => resolveAppLocale(l),
|
||||||
|
builder: builder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (settle) await tester.pumpAndSettle();
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pumpt die echte [ShuffleApp] (inkl. Router) mit der Gerätesprache
|
||||||
|
/// [deviceLocale]. Die Sprache wird nach dem Test automatisch zurückgesetzt.
|
||||||
|
Future<ProviderContainer> pumpShuffleApp(
|
||||||
|
WidgetTester tester, {
|
||||||
|
Locale deviceLocale = const Locale('en'),
|
||||||
|
List<Override>? overrides,
|
||||||
|
}) async {
|
||||||
|
tester.platformDispatcher.localesTestValue = [deviceLocale];
|
||||||
|
addTearDown(tester.platformDispatcher.clearLocalesTestValue);
|
||||||
|
final container = ProviderContainer.test(
|
||||||
|
overrides: overrides ?? testOverrides(),
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(container: container, child: const ShuffleApp()),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
return container;
|
||||||
|
}
|
||||||
45
app/test/helpers/router_helpers.dart
Normal file
45
app/test/helpers/router_helpers.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:shuffle/core/routing/redirect_source.dart';
|
||||||
|
|
||||||
|
/// Steuerbare [RedirectSource]: `targets` bildet Pfad -> Weiterleitungsziel ab.
|
||||||
|
/// [notifyChanged] löst eine Neubewertung der Redirects aus.
|
||||||
|
class FakeRedirectSource implements RedirectSource {
|
||||||
|
FakeRedirectSource([Map<String, String>? targets])
|
||||||
|
: targets = targets ?? <String, String>{};
|
||||||
|
|
||||||
|
final Map<String, String> targets;
|
||||||
|
final _notifier = ValueNotifier<int>(0);
|
||||||
|
|
||||||
|
void notifyChanged() => _notifier.value++;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Listenable get changes => _notifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? redirect(GoRouterState state) => targets[state.uri.path];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kleiner Test-Router mit eigenen [routes]. Für Screens, die ohne die
|
||||||
|
/// App-Routen geprüft werden. Der Aufrufer ruft am Ende `dispose` (z. B. per
|
||||||
|
/// `addTearDown(router.dispose)`).
|
||||||
|
GoRouter buildTestRouter({
|
||||||
|
required List<RouteBase> routes,
|
||||||
|
String initialLocation = '/',
|
||||||
|
RedirectSource? redirectSource,
|
||||||
|
}) => GoRouter(
|
||||||
|
initialLocation: initialLocation,
|
||||||
|
routes: routes,
|
||||||
|
refreshListenable: redirectSource?.changes,
|
||||||
|
redirect: redirectSource == null
|
||||||
|
? null
|
||||||
|
: (context, state) => redirectSource.redirect(state),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Aktueller Pfad des Routers (ohne Query).
|
||||||
|
String currentPath(GoRouter router) =>
|
||||||
|
router.routerDelegate.currentConfiguration.uri.path;
|
||||||
|
|
||||||
|
/// Aktuelle URI inklusive Query, z. B. für `?from=`-Prüfungen.
|
||||||
|
Uri currentUri(GoRouter router) =>
|
||||||
|
router.routerDelegate.currentConfiguration.uri;
|
||||||
20
docs/templates/abdeckungsmatrix.md
vendored
Normal file
20
docs/templates/abdeckungsmatrix.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Abdeckungsmatrix – #<Issue-Nr> <Titel>
|
||||||
|
|
||||||
|
Pflicht in der PR-Beschreibung. Pro Akzeptanzkriterium mindestens eine Ebene belegen;
|
||||||
|
nicht zutreffende Ebenen mit „–“ markieren, nicht automatisierbare Punkte unten begründen.
|
||||||
|
Testnamen tragen die Referenz („KTO-01 Kriterium n“), siehe `app/test/README.md`.
|
||||||
|
|
||||||
|
| AK | Unit | Widget | Integration/E2E (Playwright) | RLS/pgTAP | Status |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| KTO-01 Kriterium 1 | `test/features/…/x_test.dart` › Name | `…` | `e2e/…` | `supabase/tests/database/…` | grün |
|
||||||
|
| KTO-01 Kriterium 2 | – | – | – | – | manuell (Begründung) |
|
||||||
|
|
||||||
|
## Nicht automatisiert testbar
|
||||||
|
|
||||||
|
- <Punkt, Begründung, wer prüft wann>
|
||||||
|
|
||||||
|
## Ausführung
|
||||||
|
|
||||||
|
- [ ] `cd app && dart format --set-exit-if-changed . && flutter analyze && flutter test --coverage`
|
||||||
|
- [ ] `supabase test db` (bei Backend-Änderungen)
|
||||||
|
- [ ] Abdeckung neue Logik ≥ 80 %, Domänenschicht ≥ 90 %: <Werte>
|
||||||
Loading…
Reference in a new issue