test(app): nothing of one person reaches the next

Two guarantees of step 7 had no test. An open studyset closes when the person changes (here: the server revokes the session, and the app starts anew with another anonymous person), and the next person sees none of it. PersonScope shows the person's app once their data is open, says so when it can't be opened and then leaves no unhandled error behind (checked by removing the handler: the test fails), and closes the person's data when it goes away.

testDependencies takes an optional openPerson for this, and the fake auth gateway can revoke the session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-09-30 12:34:13 +02:00
commit 3fc51057f1
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 112 additions and 12 deletions

View file

@ -0,0 +1,71 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/test_dependencies.dart';
void main() {
const person = SignedIn(authUserId: 'anonymous-1', isIdentified: false);
Future<void> showScope(
WidgetTester tester,
Future<PersonDependencies> Function() open,
) async {
await tester.pumpWidget(
DependenciesScope(
dependencies: testDependencies(openPerson: (_) => open()),
child: const MaterialApp(
home: PersonScope(person: person, child: Text('Home')),
),
),
);
await tester.pumpAndSettle();
}
testWidgets("shows the person's app once their data is open", (
tester,
) async {
await showScope(
tester,
() async => PersonDependencies(
studysets: StudysetRepository(FakeStudysetGateway()),
),
);
expect(find.text('Home'), findsOneWidget);
});
testWidgets("says so when the person's data can't be opened", (
tester,
) async {
await showScope(tester, () async => throw Exception('Disk full'));
expect(find.text("Couldn't open this device's data."), findsOneWidget);
expect(find.text('Home'), findsNothing);
// Nothing was opened, so going away closes nothing and fails nothing.
await tester.pumpWidget(const SizedBox());
await tester.pump();
});
testWidgets("closes the person's data when it goes away", (tester) async {
var closed = false;
await showScope(
tester,
() async => PersonDependencies(
studysets: StudysetRepository(FakeStudysetGateway()),
onDispose: () async => closed = true,
),
);
await tester.pumpWidget(const SizedBox());
await tester.pump();
expect(closed, isTrue);
});
}

View file

@ -2,22 +2,28 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart'; import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/widget/session_gate.dart'; import 'package:halkyon_learn_flutter/src/feature/auth/widget/session_gate.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_list_screen.dart'; import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_list_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_screen.dart';
import 'package:material_ui/material_ui.dart'; import 'package:material_ui/material_ui.dart';
import '../../../support/fake_auth_gateway.dart';
import '../../../support/fake_studyset_gateway.dart'; import '../../../support/fake_studyset_gateway.dart';
import '../../../support/test_dependencies.dart'; import '../../../support/test_dependencies.dart';
void main() { void main() {
late FakeStudysetGateway gateway; late FakeStudysetGateway gateway;
Future<void> showApp(WidgetTester tester) async { /// Shows the app; every person shares [gateway] unless [auth] is given, in
/// which case each person gets data of their own.
Future<void> showApp(WidgetTester tester, {FakeAuthGateway? auth}) async {
tester.view.physicalSize = const Size(1200, 1600); tester.view.physicalSize = const Size(1200, 1600);
tester.view.devicePixelRatio = 1; tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset); addTearDown(tester.view.reset);
await tester.pumpWidget( await tester.pumpWidget(
DependenciesScope( DependenciesScope(
dependencies: testDependencies(studysetGateway: gateway), dependencies: auth == null
? testDependencies(studysetGateway: gateway)
: testDependencies(authGateway: auth),
child: const MaterialApp( child: const MaterialApp(
home: SessionGate(child: StudysetListScreen()), home: SessionGate(child: StudysetListScreen()),
), ),
@ -99,6 +105,22 @@ void main() {
expect(gateway.deleted, isEmpty); expect(gateway.deleted, isEmpty);
}); });
testWidgets('an open studyset closes when the person changes, and the next '
'person sees none of it', (tester) async {
final auth = FakeAuthGateway();
await showApp(tester, auth: auth);
await create(tester, 'Japanese');
await open(tester, 'Japanese');
expect(find.byType(StudysetScreen), findsOneWidget);
auth.revokeSession();
await tester.pumpAndSettle();
expect(find.byType(StudysetScreen), findsNothing);
expect(find.text('Japanese'), findsNothing);
expect(find.text('No studysets yet.'), findsOneWidget);
});
testWidgets('a confirmed delete removes the studyset and closes it', ( testWidgets('a confirmed delete removes the studyset and closes it', (
tester, tester,
) async { ) async {

View file

@ -39,6 +39,9 @@ class FakeAuthGateway({var AuthStatus _status = const SignedOut()})
_set(const SignedOut()); _set(const SignedOut());
} }
/// Ends the session as when the server revokes this device's session.
void revokeSession() => _set(const SignedOut());
void _set(AuthStatus status) { void _set(AuthStatus status) {
_status = status; _status = status;
_changes.add(status); _changes.add(status);

View file

@ -11,19 +11,23 @@ import 'fake_studyset_gateway.dart';
/// The app's dependencies with fakes instead of the server and the device's /// The app's dependencies with fakes instead of the server and the device's
/// database; each test passes in only the fakes it looks at. /// database; each test passes in only the fakes it looks at.
/// ///
/// Every person gets [studysetGateway] (or a fresh fake of their own). /// Every person gets [studysetGateway] (or a fresh fake of their own), unless
/// [openPerson] replaces how a person's dependencies are opened.
Dependencies testDependencies({ Dependencies testDependencies({
AuthGateway? authGateway, AuthGateway? authGateway,
StudysetGateway? studysetGateway, StudysetGateway? studysetGateway,
OpenPerson? openPerson,
}) => Dependencies( }) => Dependencies(
auth: AuthRepository(authGateway ?? FakeAuthGateway()), auth: AuthRepository(authGateway ?? FakeAuthGateway()),
openPerson: (person) async { openPerson:
final studysets = StudysetRepository( openPerson ??
studysetGateway ?? FakeStudysetGateway(), (person) async {
); final studysets = StudysetRepository(
return PersonDependencies( studysetGateway ?? FakeStudysetGateway(),
studysets: studysets, );
onDispose: studysets.dispose, return PersonDependencies(
); studysets: studysets,
}, onDispose: studysets.dispose,
);
},
); );