import 'package:flag/flag.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:halkyon_app/halkyon_app.dart'; import 'package:halkyon_app/testing.dart'; import 'package:material_ui/material_ui.dart'; /// A signed-in person's app as a test makes one: who it is for, and the /// way to Switch account. class const _PersonApp(final SignedIn person) extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( localizationsDelegates: testLocalizationsDelegates, supportedLocales: HalkyonAppLocalizations.supportedLocales, home: Scaffold( body: Builder( builder: (context) => Column( children: [ Text('Home of ${person.authUserId}'), TextButton( onPressed: () => showAccountSwitcher(context), child: const Text('Switch account'), ), ], ), ), ), ); } void main() { final builtIn = Instance.parse('learn.halkyon.eu')!; final other = Instance.parse('learn.example.org')!; late MemoryInstanceStore store; late List opened; late List dependencies; late List forgotten; setUp(() { store = MemoryInstanceStore(); opened = []; dependencies = []; forgotten = []; }); /// Starts the app on a device that [usedBefore] the choice existed, with /// [answering] instances answering a check, saying where they keep data /// as [locations] has it, and the account on each [signedOut]. Future start( WidgetTester tester, { bool usedBefore = false, bool signedOut = false, Set answering = const {}, Map locations = const {}, SubdivisionNames subdivisionNames = const MemorySubdivisionNames({}), }) async { tester.view.physicalSize = const Size(412, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final choice = InstanceChoice( store, builtIn: builtIn, forget: (instance) async => forgotten.add(instance), ); await choice.load(usedBefore: usedBefore); await tester.pumpWidget( InstanceGate( frame: testFrame(), choice: choice, checker: FakeInstanceChecker(answering, locations), countryNames: MemoryCountryNames(const { 'CH': {'en': 'Switzerland'}, 'DE': {'en': 'Germany'}, 'US': {'en': 'United States'}, }), subdivisionNames: subdivisionNames, readLook: () async => (themeMode: ThemeMode.light, locale: null), open: (instance) async { opened.add(instance); final opening = TestAppDependencies(serverName: instance.name); await opening.signedOut.write(signedOut: signedOut); dependencies.add(opening); return opening; }, app: SessionGate( look: (context) => (themeMode: ThemeMode.light, locale: null), app: _PersonApp.new, ), ), ); await tester.pumpAndSettle(); return choice; } Future openSwitcher(WidgetTester tester) async { await tester.tap(find.text('Switch account')); await tester.pumpAndSettle(); } testWidgets('a new install asks for the instance first, and opens the ' "build's in one tap", (tester) async { await start(tester); // The wordmark says it to screen readers, naming the app. expect(find.bySemanticsLabel('Welcome to Halkyon Test'), findsOneWidget); expect( find.text('Choose the instance to keep your notes on.'), findsOneWidget, reason: "the app's own words", ); expect(opened, isEmpty, reason: 'nothing made anywhere yet'); expect(find.byType(TextField), findsNothing, reason: 'no address asked'); await tester.tap(find.text('learn.halkyon.eu')); await tester.pumpAndSettle(); expect(opened, [builtIn]); expect(find.text('Home of anonymous-1'), findsOneWidget); }); testWidgets("the build's instance shows where it keeps people's data, " 'once it says', (tester) async { await start( tester, answering: {builtIn}, locations: { builtIn: const InstanceLocations(data: 'EU', backups: 'CH'), }, ); expect(find.text('Data: European Union\nBackups: Switzerland'), findsOne); expect(find.byType(Flag), findsOne); }); testWidgets('an instance may say where more precisely: a state, a region', ( tester, ) async { await start( tester, answering: {builtIn}, locations: { builtIn: const InstanceLocations(data: 'US-CA', backups: 'DE-BY'), }, subdivisionNames: const MemorySubdivisionNames({ 'DE-BY': {'en': 'Bavaria'}, 'US-CA': {'en': 'California'}, }), ); expect( find.text( 'Data: California, United States\nBackups: Bavaria, Germany (EU)', ), findsOne, ); expect(find.byType(Flag), findsOne); }); testWidgets('an instance saying nothing, or not answering, comes with the ' 'app, without a flag', (tester) async { await start(tester); expect(find.text('Comes with the app'), findsOne); expect(find.byType(Flag), findsNothing); }); testWidgets('another instance is checked before the app moves there', ( tester, ) async { await start(tester, answering: {other}); await tester.tap(find.text('Another instance')); await tester.pumpAndSettle(); expect( FocusManager.instance.primaryFocus?.context ?.findAncestorWidgetOfExactType(), isNotNull, reason: 'the address is typed at once', ); expect( find.widgetWithText(TextField, 'test.example.org'), findsOneWidget, reason: "the app's example address as the hint", ); await tester.enterText(find.byType(TextField), 'nowhere.example.org'); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect( find.text('No instance of Halkyon Test answers at nowhere.example.org.'), findsOneWidget, ); expect(opened, isEmpty); await tester.enterText(find.byType(TextField), 'learn.example.org'); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect(opened, [other]); }); testWidgets('an install from before goes on without asking', (tester) async { await start(tester, usedBefore: true); expect(opened, [builtIn]); expect(find.text('Home of anonymous-1'), findsOneWidget); }); testWidgets('Switch account moves between the accounts on the device, ' 'closing the app on the one before, and removes one not in ' 'use', (tester) async { store = MemoryInstanceStore((current: builtIn, known: [builtIn, other])); await start( tester, answering: {builtIn, other}, locations: { other: const InstanceLocations( data: 'CH', backups: InstanceLocations.noBackups, ), }, ); await openSwitcher(tester); expect(find.text('learn.halkyon.eu'), findsWidgets); // Each account with where its instance says it keeps people's data. expect(find.text('Data: Switzerland\nNo backups'), findsOne); expect(find.byType(Flag), findsOne); expect(find.text('Anonymous'), findsOne, reason: 'the account in use'); await tester.tap(find.text('learn.example.org')); await tester.pumpAndSettle(); expect(opened, [builtIn, other]); expect(dependencies.first.disposed, isTrue); expect(find.text('Home of anonymous-1'), findsOneWidget); await openSwitcher(tester); await tester.tap(find.byTooltip('Remove account')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Remove')); await tester.pumpAndSettle(); expect(forgotten, [builtIn]); expect(store.kept?.known, [other]); }); testWidgets('signed out, the start screen names the instance and offers ' 'another, through Switch account', (tester) async { store = MemoryInstanceStore((current: builtIn, known: [builtIn])); await start(tester, answering: {builtIn, other}, signedOut: true); expect(find.text('Start on learn.halkyon.eu:'), findsOneWidget); await tester.tap(find.text('Use another instance')); await tester.pumpAndSettle(); await tester.tap(find.text('Add account')); await tester.pumpAndSettle(); expect( find.text('Choose the instance for the account to add.'), findsOneWidget, ); await tester.enterText(find.byType(TextField), 'learn.example.org'); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect(opened, [builtIn, other]); expect(find.text('Start on learn.example.org:'), findsOneWidget); }); testWidgets("an instance whose app doesn't open says so, and tries " 'again', (tester) async { store = MemoryInstanceStore((current: builtIn, known: [builtIn])); final choice = InstanceChoice( store, builtIn: builtIn, forget: (instance) async {}, ); await choice.load(usedBefore: false); var attempts = 0; await tester.pumpWidget( InstanceGate( frame: testFrame(), choice: choice, checker: FakeInstanceChecker(), countryNames: MemoryCountryNames(const {}), readLook: () async => (themeMode: ThemeMode.light, locale: null), open: (instance) async { if (++attempts == 1) { throw StateError('disk unavailable'); } return TestAppDependencies(); }, app: const Directionality( textDirection: TextDirection.ltr, child: Text('the app'), ), ), ); await tester.pumpAndSettle(); expect(find.text("Couldn't open the app on learn.halkyon.eu."), findsOne); await tester.tap(find.text('Try again')); await tester.pumpAndSettle(); expect(find.text('the app'), findsOne); }); }