refactor(halkyon_app): the account section, About, the language and countries' names, shared

What Learn and Todo each held now comes from halkyon_app:
- AccountTiles: whose account on which instance, switching, signing
  out (asking first for an anonymous account, as it deletes it) and
  deleting it (asking first), the app's own words passed in.
- showAppAbout: the app's name, version, icon and legalese as it gives
  them, the source of what the instance runs (AppDependencies gains
  sourceCodeUrl), the app's own parts, the licence list.
- resolveAppLocale: the first preferred language the app speaks, never
  by country alone, British English otherwise.
- AssetCountryNames, the default of InstanceGate and InstanceScope:
  Unicode CLDR 48.2's territory names in English and German
  (tool/country_names.py, 7.6 KB), Hong Kong, Macao, Myanmar and
  Palestine by their short names as Learn's world map has them, so
  Todo says "Germany (EU)" where it said "DE (EU)".

Learn's texts read as before in every language; its About bloc and
gateway, its country names from the world map and the keys only the
moved code used are gone, as are Todo's copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 23:51:52 +02:00
commit 87500b7c6b
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 1001 additions and 834 deletions

View file

@ -0,0 +1,84 @@
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';
void main() {
late TestAppDependencies dependencies;
setUp(() => dependencies = TestAppDependencies());
/// Opens About from a button, with [version], [legalese] and [children]
/// as the app gives them.
Future<void> open(
WidgetTester tester, {
String? version,
String? legalese,
List<Widget> children = const [],
Locale locale = const Locale('en'),
}) async {
await tester.pumpWidget(
testApp(
Builder(
builder: (context) => TextButton(
onPressed: () => showAppAbout(
context,
version: version,
legalese: legalese,
children: children,
),
child: const Text('About'),
),
),
dependencies: dependencies,
locale: locale,
),
);
await tester.tap(find.text('About'));
await tester.pumpAndSettle();
}
testWidgets('names the app and where the source of what the instance runs '
'is, which both licences require offering', (tester) async {
await open(tester);
expect(find.text('Halkyon Test'), findsWidgets);
expect(find.text('The source of what this instance runs:'), findsOneWidget);
expect(find.text('https://code.example.org/halkyon'), findsOneWidget);
// Flutter's own button to every package's notice.
expect(find.text('View licenses'), findsOneWidget);
});
testWidgets("says so when the instance can't be asked", (tester) async {
dependencies.sourceCodeError = Exception('No connection');
await open(tester);
expect(
find.text("Couldn't reach this instance to ask where its source is."),
findsOneWidget,
);
});
testWidgets("shows the app's version, legalese and own parts where it "
'gives them', (tester) async {
await open(
tester,
version: '0.18.0',
legalese: 'Free software.',
children: const [Text('Data in the app')],
);
expect(find.text('0.18.0'), findsOneWidget);
expect(find.text('Free software.'), findsOneWidget);
expect(find.text('Data in the app'), findsOneWidget);
});
testWidgets('speaks German', (tester) async {
await open(tester, locale: const Locale('de'));
expect(
find.text('Der Quellcode dessen, was diese Instanz ausführt:'),
findsOneWidget,
);
});
}

View file

@ -0,0 +1,58 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:material_ui/material_ui.dart';
void main() {
/// The languages a Halkyon app speaks: British English (the template, as
/// `en` and `en-GB`), American English and German.
const supported = [
Locale('en'),
Locale('de'),
Locale('en', 'GB'),
Locale('en', 'US'),
];
Locale resolved(List<Locale>? preferred) =>
resolveAppLocale(preferred, supported);
test('the language asked for, where the app speaks it', () {
expect(resolved(const [Locale('en', 'US')]), const Locale('en', 'US'));
expect(resolved(const [Locale('en', 'GB')]), const Locale('en', 'GB'));
expect(resolved(const [Locale('de', 'DE')]), const Locale('de'));
expect(resolved(const [Locale('de')]), const Locale('de'));
});
test('British English for any other English', () {
expect(resolved(const [Locale('en')]), const Locale('en', 'GB'));
expect(resolved(const [Locale('en', 'AU')]), const Locale('en', 'GB'));
});
test('the next language the device prefers, where the first is none the '
'app speaks', () {
expect(
resolved(const [Locale('fr', 'FR'), Locale('de', 'AT')]),
const Locale('de'),
);
});
test('British English where the app speaks none of them', () {
expect(resolved(const [Locale('fr', 'FR')]), const Locale('en', 'GB'));
expect(resolved(const []), const Locale('en', 'GB'));
expect(resolved(null), const Locale('en', 'GB'));
});
test('never a language picked by the country alone', () {
// Spanish in the United States isn't American English.
expect(resolved(const [Locale('es', 'US')]), const Locale('en', 'GB'));
});
test('every language this package speaks is one it resolves to', () {
for (final locale in HalkyonAppLocalizations.supportedLocales) {
expect(
resolveAppLocale([locale], HalkyonAppLocalizations.supportedLocales),
isIn(HalkyonAppLocalizations.supportedLocales),
reason: '$locale',
);
}
});
}

View file

@ -0,0 +1,191 @@
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: whose it is, and the
/// account's tiles in the app's own words.
class const _PersonApp(final SignedIn person) extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
localizationsDelegates: testLocalizationsDelegates,
supportedLocales: HalkyonAppLocalizations.supportedLocales,
home: Scaffold(
body: ListView(
children: [
Text('App of ${person.authUserId}'),
const AccountTiles(
anonymousNote: 'Your notes stay on this device.',
signOutAnonymousMessage:
"This account can't be signed into again, so signing out "
'deletes it with its notes.',
deleteMessage: 'Your notes are deleted everywhere.',
),
],
),
),
);
}
void main() {
late FakeAuthGateway gateway;
late TestAppDependencies dependencies;
Future<void> start(WidgetTester tester, {InstanceChoice? choice}) async {
dependencies = TestAppDependencies(gateway: gateway);
final gate = AppScope(
dependencies: dependencies,
child: SessionGate(
look: (context) => (themeMode: ThemeMode.light, locale: null),
app: _PersonApp.new,
),
);
await tester.pumpWidget(
AppFrameScope(
frame: testFrame(),
child: switch (choice) {
final choice? => InstanceScope(
notifier: choice,
checker: FakeInstanceChecker(),
countryNames: MemoryCountryNames(const {}),
subdivisionNames: const MemorySubdivisionNames({}),
child: gate,
),
null => gate,
},
),
);
await tester.pumpAndSettle();
}
setUp(() => gateway = FakeAuthGateway());
testWidgets('says whose account it is, anonymous on which instance, with '
"the app's note", (tester) async {
await start(tester);
expect(find.text('Anonymous on test.example.org'), findsOneWidget);
expect(find.text('Your notes stay on this device.'), findsOneWidget);
expect(
find.text('Switch account'),
findsNothing,
reason: 'no instances to switch among here',
);
expect(find.text('Sign out'), findsOneWidget);
expect(find.text('Delete account'), findsOneWidget);
});
testWidgets('says the account is signed in, without the note', (
tester,
) async {
gateway = FakeAuthGateway(
status: const SignedIn(authUserId: 'github-person', isIdentified: true),
);
await start(tester);
expect(find.text('Signed in on test.example.org'), findsOneWidget);
expect(find.text('Your notes stay on this device.'), findsNothing);
});
testWidgets('switching account lists the accounts on the device, the '
'instance in use named', (tester) async {
final instance = Instance.parse('learn.example.org')!;
final choice = InstanceChoice(
MemoryInstanceStore((current: instance, known: [instance])),
builtIn: instance,
forget: (instance) async {},
);
await choice.load(usedBefore: false);
await start(tester, choice: choice);
expect(find.text('Switch account'), findsOneWidget);
expect(find.text('learn.example.org'), findsWidgets);
await tester.tap(find.text('Switch account'));
await tester.pumpAndSettle();
expect(find.text('Accounts'), findsOneWidget);
});
testWidgets('signing out of an anonymous account asks first, in the '
"app's words, as it deletes it; Cancel keeps it", (tester) async {
await start(tester);
await tester.tap(find.text('Sign out'));
await tester.pumpAndSettle();
expect(find.text('Sign out?'), findsOneWidget);
expect(
find.text(
"This account can't be signed into again, so signing out deletes "
'it with its notes.',
),
findsOneWidget,
);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
expect(find.text('App of anonymous-1'), findsOneWidget);
expect(gateway.signOuts, 0);
await tester.tap(find.text('Sign out'));
await tester.pumpAndSettle();
await tester.tap(find.text('Sign out').last);
await tester.pumpAndSettle();
expect(gateway.status, const SignedOut());
expect(find.text('Signed out'), findsOneWidget);
});
testWidgets('signing out of an identified account asks nothing', (
tester,
) async {
gateway = FakeAuthGateway(
status: const SignedIn(authUserId: 'github-person', isIdentified: true),
);
await start(tester);
await tester.tap(find.text('Sign out'));
await tester.pumpAndSettle();
expect(find.text('Sign out?'), findsNothing);
expect(gateway.status, const SignedOut());
});
testWidgets("deleting the account asks first, in the app's words; Cancel "
'keeps it', (tester) async {
await start(tester);
await tester.tap(find.text('Delete account'));
await tester.pumpAndSettle();
expect(find.text('Delete your account?'), findsOneWidget);
expect(find.text('Your notes are deleted everywhere.'), findsOneWidget);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
expect(gateway.accountDeletions, 0);
await tester.tap(find.text('Delete account'));
await tester.pumpAndSettle();
await tester.tap(find.text('Delete account').last);
await tester.pumpAndSettle();
expect(gateway.accountDeletions, 1);
expect(find.text('Signed out'), findsOneWidget);
});
testWidgets('speaks German', (tester) async {
await start(tester);
final german = lookupHalkyonAppLocalizations(const Locale('de'));
expect(
german.accountAnonymousOn('test.example.org'),
'Anonym auf test.example.org',
);
expect(german.accountSignedInOn('x'), 'Angemeldet auf x');
expect(german.accountSwitch, 'Konto wechseln');
expect(german.accountSignOut, 'Abmelden');
expect(german.accountSignOutTitle, 'Abmelden?');
expect(german.accountDelete, 'Konto löschen');
expect(german.accountDeleteTitle, 'Dein Konto löschen?');
});
}

View file

@ -13,4 +13,31 @@ void main() {
expect(await names.nameOf('CH', 'ja'), 'Switzerland');
expect(await names.nameOf('XX', 'en'), isNull);
});
testWidgets("Unicode CLDR's names come with the package, in English and "
'German', (tester) async {
final names = await tester.runAsync(
() async => (
germany: await const AssetCountryNames().nameOf('DE', 'en'),
deutschland: await const AssetCountryNames().nameOf('de', 'de'),
states: await const AssetCountryNames().nameOf('US', 'de'),
kingdom: await const AssetCountryNames().nameOf('GB', 'de'),
switzerland: await const AssetCountryNames().nameOf('CH', 'ja'),
kosovo: await const AssetCountryNames().nameOf('XK', 'en'),
// The union is the package's own words (instanceEuropeanUnion),
// and a region of the world no country.
union: await const AssetCountryNames().nameOf('EU', 'en'),
unknown: await const AssetCountryNames().nameOf('ZZ', 'en'),
),
);
expect(names?.germany, 'Germany');
expect(names?.deutschland, 'Deutschland');
expect(names?.states, 'Vereinigte Staaten');
expect(names?.kingdom, 'Vereinigtes Königreich');
expect(names?.switzerland, 'Switzerland', reason: 'English otherwise');
expect(names?.kosovo, 'Kosovo');
expect(names?.union, isNull);
expect(names?.unknown, isNull);
});
}