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 nobody signed in on each where [signedOut] /// (a device starting there). 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), sharesStorage: (a, b) => false, ); 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, gateway: FakeAuthGateway( status: signedOut ? const SignedOut() : FakeAuthGateway.person, ), ); 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, opens the ' "build's in one tap, and asks how to start there", (tester) async { await start(tester, signedOut: true); // 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('Start on learn.halkyon.eu:'), 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 instance over plain HTTP is refused before anything is ' 'sent there', (tester) async { await start(tester, answering: {other}); await tester.tap(find.text('Another instance')); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), 'http://learn.example.org'); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect( find.textContaining('reaches instances over HTTPS only'), findsOneWidget, ); expect(opened, isEmpty); }); testWidgets('a single word is no address, said at once rather than looked ' "for (the developer's review, 9 Nov 2026)", (tester) async { await start(tester, answering: {other}); await tester.tap(find.text('Another instance')); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), 'todo'); await tester.tap(find.text('Continue')); await tester.pump(); expect(find.text("This isn't an address."), findsOneWidget); expect(find.byType(LinearProgressIndicator), findsNothing); expect(opened, isEmpty); }); testWidgets('an instance kept from before over plain HTTP is never opened: ' 'another is used, or it is removed', (tester) async { final insecure = Instance.parse('http://learn.example.net')!; store = MemoryInstanceStore(( current: insecure, known: [builtIn, insecure], )); await start(tester, answering: {builtIn}); expect(opened, isEmpty, reason: 'nothing sent over plain HTTP'); expect(find.textContaining('learn.example.net is reached over'), findsOne); await tester.tap(find.text('Use another instance')); await tester.pumpAndSettle(); expect(opened, [builtIn]); expect(store.kept?.known, [builtIn, insecure], reason: 'kept, unused'); expect(forgotten, isEmpty); }); testWidgets('an instance kept from before over plain HTTP can be removed ' 'from the device', (tester) async { final insecure = Instance.parse('http://learn.example.net')!; store = MemoryInstanceStore((current: insecure, known: [insecure])); await start(tester, answering: {builtIn}); await tester.tap(find.text('Remove from this device')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Remove')); await tester.pumpAndSettle(); expect(forgotten, [insecure]); expect(opened, [builtIn]); expect(store.kept?.known, [builtIn]); }); testWidgets('an install from before goes on without asking', (tester) async { await start(tester, usedBefore: true); expect(opened, [builtIn]); expect(find.text('Home of person-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('Signed in'), 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 person-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 {}, sharesStorage: (a, b) => false, ); 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); }); }