halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
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<Instance> opened;
|
|
|
|
|
late List<TestAppDependencies> dependencies;
|
|
|
|
|
late List<Instance> 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<InstanceChoice> start(
|
|
|
|
|
WidgetTester tester, {
|
|
|
|
|
bool usedBefore = false,
|
|
|
|
|
bool signedOut = false,
|
|
|
|
|
Set<Instance> answering = const {},
|
|
|
|
|
Map<Instance, InstanceLocations> locations = const {},
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
SubdivisionNames subdivisionNames = const MemorySubdivisionNames({}),
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
}) 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'},
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
'DE': {'en': 'Germany'},
|
|
|
|
|
'US': {'en': 'United States'},
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
}),
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
subdivisionNames: subdivisionNames,
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
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<void> 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);
|
|
|
|
|
});
|
|
|
|
|
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
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<TextField>(),
|
|
|
|
|
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]);
|
|
|
|
|
});
|
|
|
|
|
|
fix(app)!: instances over plain HTTP are refused, but on this machine
What the app sends an instance (the proofs that sign in, the keys'
wrappings, every request a session carries) must not be read or changed on
the way, so an instance is reached over HTTPS, or over plain HTTP only on
this machine (localhost, 127.0.0.0/8, ::1), as Jellyfin servers are. An
address over plain HTTP elsewhere is refused before anything is sent there.
One kept from before is never opened: the app says why, and offers another
instance or removing it from the device, never dropping it silently.
Development builds still use their own instance over HTTP (an emulator
reaches the developer's machine as 10.0.2.2); releases never.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 14:35:21 +02:00
|
|
|
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('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]);
|
|
|
|
|
});
|
|
|
|
|
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|