feat(todo): the app on halkyon_app, with its own icon
Todo's app is halkyon_app's instance and session gates with Todo's
frame ("Todo" under "Halkyon", its icon, its own sentences) and an
instance check accepting only `todo`; TodoDependencies on an instance,
an empty TodoPerson per person, nothing kept or synced yet. A person
sees an empty home and their account: whose it is on which instance,
switching account, signing out and deleting it (each asking first where
it deletes), and About with the source of what the instance runs and
the licence list. Texts in British English and German. App ID
app.halkyon.todo on every platform, GitHub's callback scheme registered
on Android, no web build.
Its icon is Lucide's list-checks (lucide-static 1.50.0) in the brand's
blues on white: tool/app_icons.py draws its strokes with Pillow and
writes every platform's launcher icon; Lucide's ISC notice joins the
licence list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
28dd3ea65a
commit
6871e3763b
4 changed files with 1718 additions and 693 deletions
179
apps/todo/halkyon_todo_flutter/test/app/todo_app_test.dart
Normal file
179
apps/todo/halkyon_todo_flutter/test/app/todo_app_test.dart
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_app.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
final builtIn = Instance.parse('todo.example.org')!;
|
||||
late MemoryInstanceStore store;
|
||||
late FakeAuthGateway gateway;
|
||||
late MemorySignedOutMark signedOut;
|
||||
late List<SignedIn> opened;
|
||||
|
||||
setUp(() {
|
||||
store = MemoryInstanceStore();
|
||||
gateway = FakeAuthGateway();
|
||||
signedOut = MemorySignedOutMark();
|
||||
opened = [];
|
||||
});
|
||||
|
||||
/// Starts Todo on a new install ([usedBefore] false) or on one that chose
|
||||
/// its instance before, signed out as [wasSignedOut] says.
|
||||
Future<void> start(
|
||||
WidgetTester tester, {
|
||||
bool usedBefore = false,
|
||||
bool wasSignedOut = false,
|
||||
}) 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 {},
|
||||
);
|
||||
await choice.load(usedBefore: usedBefore);
|
||||
await signedOut.write(signedOut: wasSignedOut);
|
||||
await tester.pumpWidget(
|
||||
TodoApp(
|
||||
choice: choice,
|
||||
checker: FakeInstanceChecker({builtIn}),
|
||||
countryNames: MemoryCountryNames(const {}),
|
||||
open: (instance) async => TodoDependencies(
|
||||
serverName: instance.name,
|
||||
auth: AuthRepository(gateway, MemorySignInAddresses()),
|
||||
signedOut: signedOut,
|
||||
sourceCode: () async =>
|
||||
Uri.parse('https://code.halkyon.app/project-halkyon/halkyon'),
|
||||
openPerson: (person) async {
|
||||
opened.add(person);
|
||||
return TodoPerson(person);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Opens the account sheet from home.
|
||||
Future<void> openAccount(WidgetTester tester) async {
|
||||
await tester.tap(find.byTooltip('Account'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets("a new install chooses its instance first, in Todo's words and "
|
||||
'look', (tester) async {
|
||||
await start(tester);
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(widget) => widget is BrandWordmark && widget.product == 'Todo',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.text(
|
||||
'Choose the instance to keep your tasks on. Someone runs each one; '
|
||||
'you can add others later.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('todo.example.org'), findsOneWidget);
|
||||
expect(find.text('Another instance'), findsOneWidget);
|
||||
expect(opened, isEmpty, reason: 'nothing made anywhere yet');
|
||||
});
|
||||
|
||||
testWidgets("the build's instance in one tap: an account without a name, "
|
||||
'and an empty home', (tester) async {
|
||||
await start(tester);
|
||||
|
||||
await tester.tap(find.text('todo.example.org'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(opened.single.isIdentified, isFalse);
|
||||
expect(find.text('Nothing here yet'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Tasks come with a later version of Halkyon Todo.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('signed out, it asks how to start; a GitHub account nobody here '
|
||||
"signs in with is said so, with Todo's ways in", (tester) async {
|
||||
await start(tester, usedBefore: true, wasSignedOut: true);
|
||||
|
||||
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
||||
gateway.gitHubError = const GitHubSignInFailed(
|
||||
GitHubSignInProblem.unknownAccount,
|
||||
);
|
||||
await tester.tap(find.text('Sign in with GitHub'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
'No account here signs in with this GitHub account yet. Start '
|
||||
'without an account, or continue with your email address.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the account says whose it is, on which instance, and signing '
|
||||
'out of an anonymous one asks first, as it deletes it', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await openAccount(tester);
|
||||
|
||||
expect(find.text('Anonymous on todo.example.org'), findsOneWidget);
|
||||
expect(find.text('Switch account'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Sign out'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Sign out?'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Sign out').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.accountDeletions, 1);
|
||||
expect(gateway.status, const SignedOut());
|
||||
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('deleting the account asks first; Cancel keeps it, Delete '
|
||||
'deletes it and asks how to start', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await openAccount(tester);
|
||||
|
||||
await tester.tap(find.text('Delete account'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Delete your account?'), 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('Sign in with GitHub'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('About names the source of what the instance runs, which both '
|
||||
'licences ask to offer', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await openAccount(tester);
|
||||
|
||||
await tester.tap(find.text('About'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('The source of what this instance runs:'), findsOneWidget);
|
||||
expect(
|
||||
find.text('https://code.halkyon.app/project-halkyon/halkyon'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
78
apps/todo/halkyon_todo_flutter/test/l10n/arb_files_test.dart
Normal file
78
apps/todo/halkyon_todo_flutter/test/l10n/arb_files_test.dart
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// The messages of the `.arb` file [name] in `lib/l10n/`, without metadata.
|
||||
Map<String, String> _messages(String name) => {
|
||||
for (final MapEntry(:key, :value) in (jsonDecode(
|
||||
File('lib/l10n/$name').readAsStringSync(),
|
||||
) as Map<String, Object?>).entries)
|
||||
if (!key.startsWith('@')) key: value! as String,
|
||||
};
|
||||
|
||||
/// The names of the placeholders in [message], with plural and select
|
||||
/// arguments; the words of their branches don't count. (gen-l10n reads
|
||||
/// quotes as text here: `use-escaping` is off.)
|
||||
Set<String> _placeholders(String message) {
|
||||
final names = <String>{};
|
||||
// Even depths hold text and placeholders, odd ones a plural's branches.
|
||||
var depth = 0;
|
||||
for (var i = 0; i < message.length; i++) {
|
||||
switch (message[i]) {
|
||||
case '{':
|
||||
final name = RegExp(r'\{\s*(\w+)\s*[,}]').matchAsPrefix(message, i);
|
||||
if (name != null && depth.isEven) {
|
||||
names.add(name.group(1)!);
|
||||
}
|
||||
depth++;
|
||||
case '}':
|
||||
depth--;
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
void main() {
|
||||
final template = _messages('app_en.arb');
|
||||
|
||||
test('German has every message, with the same placeholders', () {
|
||||
final german = _messages('app_de.arb');
|
||||
|
||||
expect(
|
||||
template.keys.where((key) => !german.containsKey(key)),
|
||||
isEmpty,
|
||||
reason: 'untranslated',
|
||||
);
|
||||
expect(
|
||||
german.keys.where((key) => !template.containsKey(key)),
|
||||
isEmpty,
|
||||
reason: 'not in the template',
|
||||
);
|
||||
for (final MapEntry(:key, :value) in german.entries) {
|
||||
expect(_placeholders(value), _placeholders(template[key]!), reason: key);
|
||||
}
|
||||
});
|
||||
|
||||
test("the English variants override the template's messages only, with "
|
||||
'the same placeholders', () {
|
||||
for (final variant in ['app_en_GB.arb', 'app_en_US.arb']) {
|
||||
for (final MapEntry(:key, :value) in _messages(variant).entries) {
|
||||
expect(template, contains(key), reason: '$variant: $key');
|
||||
expect(
|
||||
_placeholders(value),
|
||||
_placeholders(template[key]!),
|
||||
reason: '$variant: $key',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a placeholder is found also inside a plural', () {
|
||||
expect(
|
||||
_placeholders('{count, plural, =1{1 card} other{{count} cards in {t}}}'),
|
||||
{'count', 't'},
|
||||
);
|
||||
expect(_placeholders("Couldn't open {name}"), {'name'});
|
||||
});
|
||||
}
|
||||
23
apps/todo/halkyon_todo_flutter/test/licences_test.dart
Normal file
23
apps/todo/halkyon_todo_flutter/test/licences_test.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/licences.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets("Lucide's notice, whose icon is Todo's, joins the licence list", (
|
||||
tester,
|
||||
) async {
|
||||
registerLucideLicense();
|
||||
|
||||
final entries = await tester.runAsync(
|
||||
() => LicenseRegistry.licenses.toList(),
|
||||
);
|
||||
final lucide = entries!.where(
|
||||
(entry) => entry.packages.contains('Lucide (the app icon)'),
|
||||
);
|
||||
expect(lucide, isNotEmpty);
|
||||
expect(
|
||||
lucide.first.paragraphs.map((paragraph) => paragraph.text).join('\n'),
|
||||
contains('ISC License'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility that Flutter provides. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_test/flutter_test.dart';
|
||||
//
|
||||
// import 'package:PROJECTNAME_flutter/main.dart';
|
||||
|
||||
void main() {
|
||||
// Add your app tests here
|
||||
}
|
||||
Loading…
Reference in a new issue