refactor(theme): the look comes from halkyon_ui
All checks were successful
CI / check (push) Successful in 17m10s
CI / deploy (push) Has been skipped

The theme, Noto Sans, the window sizes and the widgets every screen is
built of move to halkyon_ui (halkyon_shared), pinned to a signed commit
as the sync fork is, so Halkyon's other apps share them. Learn keeps its
own colours (the FSRS ratings, a starred star) as LearnColors beside the
theme, the CJK fonts with their licences, and its launch screen's word
and icon. Nothing on screen changes.

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-18 22:04:10 +02:00
commit f43b92e4db
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
44 changed files with 451 additions and 4755 deletions

View file

@ -2,7 +2,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/profile/public_person.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/core/widget/text_limits.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/data/sign_in_addresses.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.dart';
@ -17,6 +16,7 @@ import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_reposit
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
import '../support/fake_auth_gateway.dart';

View file

@ -1,9 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
@ -132,7 +132,7 @@ void main() {
DependenciesScope(
dependencies: testDependencies(openPerson: (_) => open()),
child: const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: PersonScope(person: person, child: Text('Home')),
),
),

View file

@ -3,11 +3,11 @@ import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
@ -246,7 +246,7 @@ void main() {
]).node;
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(
body: RichContentView(
content: Document(
@ -286,7 +286,7 @@ void main() {
Future<void> pump(WidgetTester tester, Widget child) => tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(body: child),
),
);

View file

@ -1,12 +1,12 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/editor/format_bar.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
import 'package:halkyon_learn_flutter/src/core/editor/scripts.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.dart';
import 'package:material_ui/material_ui.dart';
@ -23,7 +23,7 @@ void main() {
latest = content ?? RichContent.plain(text);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: ContentLanguages(
languages: (front: language, back: 'en'),
child: Scaffold(
@ -226,7 +226,7 @@ void main() {
'smaller', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(
body: ListView(
children: [

View file

@ -1,12 +1,12 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/editor/editor_clipboard.dart';
import 'package:halkyon_learn_flutter/src/core/editor/link.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:material_ui/material_ui.dart';
/// Content of one line of [runs], each a text and its marks.
@ -80,7 +80,7 @@ void main() {
final latest = [...contents];
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(
body: Column(
children: [

View file

@ -1,14 +1,14 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_math_fork/flutter_math.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/editor/math_view.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
import 'package:halkyon_learn_flutter/src/core/theme/app_theme.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
import 'package:material_ui/material_ui.dart';
void main() {
@ -47,8 +47,8 @@ void main() {
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: appTheme(Brightness.light),
localizationsDelegates: AppLocalizations.localizationsDelegates,
theme: learnTheme(Brightness.light),
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(
body: SizedBox(
height: 600,
@ -208,8 +208,8 @@ void main() {
Future<void> show(WidgetTester tester, RichContent content) async {
await tester.pumpWidget(
MaterialApp(
theme: appTheme(Brightness.light),
localizationsDelegates: AppLocalizations.localizationsDelegates,
theme: learnTheme(Brightness.light),
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(body: RichContentView(content: content)),
),
);

View file

@ -1,8 +1,8 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:material_ui/material_ui.dart';
void main() {
@ -10,7 +10,7 @@ void main() {
'and bringing the selection into view comes to an end', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(
body: ListView(
children: [

View file

@ -1,11 +1,11 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
import 'package:halkyon_learn_flutter/src/core/editor/ruby.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:material_ui/material_ui.dart';
void main() {
@ -28,7 +28,7 @@ void main() {
Future<void> show(WidgetTester tester, RichContent content) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(
body: ListView(children: [RichContentView(content: content)]),
),
@ -152,7 +152,7 @@ void main() {
var tapped = 0;
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(
body: ListView(
children: [
@ -198,7 +198,7 @@ void main() {
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(
body: ListView(
children: [

View file

@ -1,12 +1,12 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
import 'package:halkyon_learn_flutter/src/core/theme/app_theme.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
import 'package:material_ui/material_ui.dart';
void main() {
@ -18,8 +18,8 @@ void main() {
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: appTheme(Brightness.light),
localizationsDelegates: AppLocalizations.localizationsDelegates,
theme: learnTheme(Brightness.light),
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(body: child),
),
);

View file

@ -1,13 +1,13 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
import 'package:halkyon_learn_flutter/src/core/theme/app_theme.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_youtube_videos.dart';
@ -24,8 +24,8 @@ void main() {
DependenciesScope(
dependencies: testDependencies(videos: videos),
child: MaterialApp(
theme: appTheme(Brightness.light),
localizationsDelegates: AppLocalizations.localizationsDelegates,
theme: learnTheme(Brightness.light),
localizationsDelegates: appLocalizationsDelegates,
home: Scaffold(body: child),
),
),

View file

@ -1,24 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/layout/window_size.dart';
void main() {
test("Material 3's classes, by the window's width", () {
expect(WindowSize.fromWidth(0), WindowSize.compact);
expect(WindowSize.fromWidth(599.9), WindowSize.compact);
expect(WindowSize.fromWidth(600), WindowSize.medium);
expect(WindowSize.fromWidth(839.9), WindowSize.medium);
expect(WindowSize.fromWidth(840), WindowSize.expanded);
expect(WindowSize.fromWidth(1199.9), WindowSize.expanded);
expect(WindowSize.fromWidth(1200), WindowSize.large);
expect(WindowSize.fromWidth(1599.9), WindowSize.large);
expect(WindowSize.fromWidth(1600), WindowSize.extraLarge);
});
test('a rail from medium on, desktop layouts from expanded on', () {
expect(WindowSize.compact.isAtLeastMedium, isFalse);
expect(WindowSize.medium.isAtLeastMedium, isTrue);
expect(WindowSize.medium.isAtLeastExpanded, isFalse);
expect(WindowSize.expanded.isAtLeastExpanded, isTrue);
expect(WindowSize.extraLarge.isAtLeastExpanded, isTrue);
});
}

View file

@ -2,11 +2,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/core/theme/brand.dart';
import 'package:halkyon_learn_flutter/src/core/theme/palette_color.dart';
import 'package:halkyon_learn_flutter/src/core/widget/floating_navigation_bar.dart';
import 'package:halkyon_learn_flutter/src/core/widget/screen_tint.dart';
import 'package:halkyon_learn_flutter/src/core/widget/top_gradient.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/home/widget/activity_heatmap.dart';
@ -15,6 +10,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/library_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_screen.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_card_gateway.dart';

View file

@ -0,0 +1,122 @@
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/theme/cjk_fonts.dart';
import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
/// Loads the bundled font [family] from [files], as the app does at start.
Future<void> load(String family, List<String> files) async {
final loader = FontLoader(family);
for (final file in files) {
loader.addFont(rootBundle.load(file));
}
await loader.load();
}
/// How many pixels [text] inks in [family] at [weight]: heavier strokes ink
/// more.
Future<int> ink(String text, String family, FontWeight weight) async {
final painter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
fontFamily: family,
fontSize: 40,
fontWeight: weight,
color: const Color(0xFF000000),
),
),
textDirection: TextDirection.ltr,
)..layout();
final recorder = ui.PictureRecorder();
painter.paint(Canvas(recorder), Offset.zero);
final image = await recorder.endRecording().toImage(
painter.width.ceil(),
painter.height.ceil(),
);
final pixels = (await image.toByteData())!;
var inked = 0;
for (var alpha = 3; alpha < pixels.lengthInBytes; alpha += 4) {
if (pixels.getUint8(alpha) > 128) {
inked++;
}
}
return inked;
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test("the theme sets text in halkyon_ui's Noto Sans, with Noto Sans JP "
'for what it lacks', () {
for (final brightness in Brightness.values) {
final style = learnTheme(brightness).textTheme.bodyLarge!;
expect(style.fontFamily, 'packages/halkyon_ui/Noto Sans');
expect(style.fontFamilyFallback, [
'Noto Sans JP',
'Noto Sans SC',
'Noto Sans TC',
'Noto Sans KR',
]);
}
});
test('the app bundles every font the theme names', () async {
final manifest = jsonDecode(
await rootBundle.loadString('FontManifest.json'),
) as List<dynamic>;
final families = {
for (final font in manifest.cast<Map<String, dynamic>>())
font['family'] as String,
};
expect(families, containsAll([appFontFamily, ...appFontFallback]));
});
test('each bundled font draws every weight from its one file', () async {
await load(appFontFamily, [
'packages/halkyon_ui/assets/fonts/NotoSans-Variable.ttf',
]);
for (final region in ['JP', 'SC', 'TC', 'KR']) {
await load('Noto Sans $region', [
'assets/fonts/NotoSans$region-Variable.otf',
]);
}
for (final (family, text) in [
(appFontFamily, 'Ἀθῆναι Halkyon'),
('Noto Sans JP', '日本語のかな'),
('Noto Sans SC', '我们说中文'),
('Noto Sans TC', '我們說中文'),
('Noto Sans KR', '한국어 공부'),
]) {
final regular = await ink(text, family, FontWeight.w400);
final bold = await ink(text, family, FontWeight.w700);
expect(bold, greaterThan(regular * 1.2), reason: family);
}
});
test("the fonts' licences join the licence list", () async {
registerFontLicenses();
registerCjkFontLicenses();
final packages = {
await for (final entry in LicenseRegistry.licenses) ...entry.packages,
};
expect(
packages,
containsAll([
'Noto Sans',
'Noto Sans JP',
'Noto Sans SC',
'Noto Sans TC',
'Noto Sans KR',
]),
);
});
}

View file

@ -0,0 +1,47 @@
import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/theme/learn_colors.dart';
import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
/// WCAG's contrast ratio of two colours, from 1 (none) to 21.
double _contrast(Color a, Color b) {
final (lighter, darker) = (
max(a.computeLuminance(), b.computeLuminance()),
min(a.computeLuminance(), b.computeLuminance()),
);
return (lighter + 0.05) / (darker + 0.05);
}
void main() {
for (final brightness in Brightness.values) {
test("$brightness: halkyon_ui's theme, with Learn's rating and starred "
'colours beside its status colours', () {
final theme = learnTheme(brightness);
expect(theme.colorScheme, appTheme(brightness).colorScheme);
expect(theme.extras, isA<ThemeExtras>());
expect(
theme.learnColors,
brightness == Brightness.light ? LearnColors.light : LearnColors.dark,
);
});
test('$brightness: a starred star stands out from the cards, as a '
'graphic must (3 to 1)', () {
final theme = learnTheme(brightness);
for (final surface in [
theme.colorScheme.surface,
theme.colorScheme.surfaceContainerLow,
]) {
expect(
_contrast(theme.learnColors.starred, surface),
greaterThanOrEqualTo(3),
);
}
});
}
}

View file

@ -1,95 +0,0 @@
import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/theme/app_theme.dart';
import 'package:halkyon_learn_flutter/src/core/theme/brand.dart';
import 'package:halkyon_learn_flutter/src/core/theme/palette_color.dart';
import 'package:halkyon_learn_flutter/src/core/theme/theme_extras.dart';
import 'package:material_ui/material_ui.dart';
/// WCAG's contrast ratio of two colours, from 1 (none) to 21.
double _contrast(Color a, Color b) {
final (lighter, darker) = (
max(a.computeLuminance(), b.computeLuminance()),
min(a.computeLuminance(), b.computeLuminance()),
);
return (lighter + 0.05) / (darker + 0.05);
}
void main() {
group('the picker palette', () {
test('has the 20 colours content can be given', () {
expect(PaletteColor.values, hasLength(20));
});
for (final color in PaletteColor.values) {
test(
'${color.name}: its text colour is readable on its fill (WCAG AA)',
() {
expect(
_contrast(color.foreground, color.background),
greaterThanOrEqualTo(4.5),
);
},
);
}
test(
'finds a colour by its stored name, and nothing for an unknown one',
() {
expect(PaletteColor.byName('cobalt'), PaletteColor.cobalt);
expect(PaletteColor.byName('ultraviolet'), isNull);
expect(PaletteColor.byName(null), isNull);
},
);
});
group('the theme', () {
for (final brightness in Brightness.values) {
test('$brightness: Material 3, with the status and rating colours', () {
final theme = appTheme(brightness);
expect(theme.useMaterial3, isTrue);
expect(theme.colorScheme.brightness, brightness);
expect(theme.extras, isA<ThemeExtras>());
});
}
test('light: cobalt is the primary colour, copper the accent', () {
final scheme = appTheme(Brightness.light).colorScheme;
expect(scheme.primary, Brand.primary);
expect(scheme.tertiary, Brand.accent);
});
test("dark: the primary colour's text stays readable", () {
final scheme = appTheme(Brightness.dark).colorScheme;
expect(
_contrast(scheme.onPrimary, scheme.primary),
greaterThanOrEqualTo(4.5),
);
expect(
_contrast(scheme.onSurface, scheme.surface),
greaterThanOrEqualTo(4.5),
);
});
for (final brightness in Brightness.values) {
test('$brightness: a starred star stands out from the cards, as a '
'graphic must (3 to 1)', () {
final theme = appTheme(brightness);
for (final surface in [
theme.colorScheme.surface,
theme.colorScheme.surfaceContainerLow,
]) {
expect(
_contrast(theme.extras.starred, surface),
greaterThanOrEqualTo(3),
);
}
});
}
});
}

View file

@ -1,217 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
import 'package:material_ui/material_ui.dart';
void main() {
/// Opens a sheet titled "New studyset" with [body] and a Create button, in
/// a window [width] wide, with a status bar [statusBar] high on top.
Future<void> open(
WidgetTester tester, {
required double width,
Widget body = const Text('Body'),
double statusBar = 0,
bool busy = false,
}) async {
tester.view.physicalSize = Size(width, 900);
tester.view.devicePixelRatio = 1;
tester.view.padding = FakeViewPadding(top: statusBar);
tester.view.viewPadding = FakeViewPadding(top: statusBar);
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: Builder(
builder: (context) => TextButton(
onPressed: () => showAdaptiveSheet<void>(
context,
swipeable: !busy,
builder: (context) => SheetScaffold(
title: 'New studyset',
busy: busy,
body: body,
footer: FilledButton(
onPressed: () {},
child: const Text('Create'),
),
),
),
child: const Text('Open'),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
}
testWidgets('is a bottom sheet below expanded windows', (tester) async {
await open(tester, width: 839);
expect(find.byType(BottomSheet), findsOneWidget);
expect(find.byType(Dialog), findsNothing);
});
testWidgets('is a dialog from expanded windows on', (tester) async {
await open(tester, width: 840);
expect(find.byType(Dialog), findsOneWidget);
expect(find.byType(BottomSheet), findsNothing);
});
for (final width in [400.0, 1200.0]) {
testWidgets('keeps its close button and footer in view above a long body '
'(${width.round()} wide)', (tester) async {
await open(
tester,
width: width,
body: Column(
children: [for (var i = 0; i < 80; i++) Text('Line $i')],
),
);
expect(find.byTooltip('Close').hitTestable(), findsOneWidget);
expect(find.text('Create').hitTestable(), findsOneWidget);
});
}
testWidgets('stays below the status bar with the keyboard open', (
tester,
) async {
await open(
tester,
width: 400,
statusBar: 48,
body: Column(
children: [for (var i = 0; i < 80; i++) Text('Line $i')],
),
);
tester.view.viewInsets = const FakeViewPadding(bottom: 400);
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, 48);
expect(find.byTooltip('Close').hitTestable(), findsOneWidget);
expect(find.text('Create').hitTestable(), findsOneWidget);
});
testWidgets('closes with its close button', (tester) async {
await open(tester, width: 400);
await tester.tap(find.byTooltip('Close'));
await tester.pumpAndSettle();
expect(find.text('New studyset'), findsNothing);
});
testWidgets('while busy, its close button is off and back keeps it open', (
tester,
) async {
await open(tester, width: 400, busy: true);
expect(
tester
.widget<IconButton>(
find.ancestor(
of: find.byTooltip('Close'),
matching: find.byType(IconButton),
),
)
.onPressed,
isNull,
);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();
expect(find.text('New studyset'), findsOneWidget);
});
group('a body longer than the sheet', () {
final tall = Column(
children: [for (var i = 0; i < 40; i++) const Text('Line')],
);
Finder below() => find.byKey(SheetScaffold.moreBelowKey);
Finder above() => find.byKey(SheetScaffold.moreAboveKey);
testWidgets('shows a line above the buttons while more lies below, and '
'one under the title while more lies above', (tester) async {
// As a dialog: a modal bottom sheet takes a test's drag itself.
await open(tester, width: 1000, body: tall);
expect(below(), findsOneWidget);
expect(above(), findsNothing);
await tester.drag(
find.byType(SingleChildScrollView),
const Offset(0, -300),
);
await tester.pumpAndSettle();
expect(below(), findsOneWidget);
expect(above(), findsOneWidget);
expect(
tester
.state<ScrollableState>(
find.descendant(
of: find.byType(SingleChildScrollView),
matching: find.byType(Scrollable),
),
)
.position
.pixels,
greaterThan(0),
reason: 'the line coming keeps where the body was scrolled to',
);
await tester.drag(
find.byType(SingleChildScrollView),
const Offset(0, -5000),
);
await tester.pumpAndSettle();
expect(below(), findsNothing);
expect(above(), findsOneWidget);
});
testWidgets('has a scrollbar, always shown on the desktop', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
await open(tester, width: 1000, body: tall);
final scrollbar = tester.widget<Scrollbar>(find.byType(Scrollbar));
expect(scrollbar.thumbVisibility, isTrue);
debugDefaultTargetPlatformOverride = null;
});
});
testWidgets('a body that fits shows no lines', (tester) async {
await open(tester, width: 400);
expect(find.byKey(SheetScaffold.moreBelowKey), findsNothing);
expect(find.byKey(SheetScaffold.moreAboveKey), findsNothing);
});
testWidgets("the body keeps clear of the scroll view's edges, where it is "
'cut and the scrollbar runs', (tester) async {
for (final width in [400.0, 1000.0]) {
await open(
tester,
width: width,
body: const TextField(
decoration: InputDecoration(border: OutlineInputBorder()),
),
);
final scrolling = tester.getRect(find.byType(SingleChildScrollView));
final field = tester.getRect(find.byType(TextField));
expect(field.left - scrolling.left, greaterThanOrEqualTo(16));
expect(scrolling.right - field.right, greaterThanOrEqualTo(16));
// The title and the button stay in line with the body.
expect(tester.getTopLeft(find.text('New studyset')).dx, field.left);
expect(tester.getTopRight(find.byType(FilledButton)).dx, field.right);
await tester.tap(find.byTooltip('Close'));
await tester.pumpAndSettle();
}
});
}

View file

@ -0,0 +1,25 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/widget/app_launch_screen.dart';
import 'package:material_ui/material_ui.dart';
void main() {
testWidgets('shows the kingfisher, and "Halkyon" over "Learn", read out '
"as the app's name", (tester) async {
await tester.pumpWidget(
const MaterialApp(
localizationsDelegates: appLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: AppLaunchScreen(),
),
);
expect(find.byType(Image), findsOneWidget);
final brand = tester.getRect(find.text('Halkyon'));
final product = tester.getRect(find.text('Learn'));
expect(product.top, greaterThanOrEqualTo(brand.bottom - 1));
expect(product.center.dx, moreOrLessEquals(brand.center.dx));
expect(find.bySemanticsLabel('Halkyon Learn'), findsOneWidget);
});
}

View file

@ -1,44 +0,0 @@
import 'dart:async';
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/theme/app_theme.dart';
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
import 'package:material_ui/material_ui.dart';
void main() {
testWidgets("pulled down, it refreshes with the app's spinner, Material 3 "
"Expressive's, not Flutter's older one", (tester) async {
final refreshing = Completer<void>();
var refreshes = 0;
await tester.pumpWidget(
MaterialApp(
theme: appTheme(Brightness.light),
home: Scaffold(
body: AppRefreshIndicator(
onRefresh: () {
refreshes++;
return refreshing.future;
},
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: const [Text('Japanese')],
),
),
),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsNothing);
await tester.fling(find.text('Japanese'), const Offset(0, 400), 1000);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(refreshes, 1);
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsNothing);
refreshing.complete();
await tester.pumpAndSettle();
expect(find.byType(ExpressiveLoadingIndicator), findsNothing);
});
}

View file

@ -1,17 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/widget/count_stepper.dart';
void main() {
test('steps go to the next multiple of the step, never below the first', () {
expect(stepUp(12, 5, max: 100), 15);
expect(stepUp(15, 5, max: 100), 20);
expect(stepUp(98, 5, max: 100), 100);
expect(stepDown(12, 5), 10);
expect(stepDown(15, 5), 10);
expect(stepDown(7, 5), 5);
expect(canStepDown(5, 5), isFalse);
expect(canStepDown(6, 5), isTrue);
expect(stepUp(1, 25, max: 1000), 25);
expect(stepDown(230, 25), 225);
});
}

View file

@ -1,101 +0,0 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
import 'package:halkyon_learn_flutter/src/core/widget/found_by_id.dart';
import 'package:material_ui/material_ui.dart';
void main() {
late StreamController<List<String>> items;
setUp(() => items = StreamController());
tearDown(() => items.close());
/// Opens the screen of the item [id] over a first screen.
Future<void> open(WidgetTester tester, String id) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) => TextButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => FoundById<String>(
id: id,
items: () => items.stream,
idOf: (item) => item,
builder: (item, items) =>
Scaffold(body: Text('Item $item of ${items.length}')),
),
),
),
child: const Text('Open'),
),
),
),
);
await tester.tap(find.text('Open'));
// Through the opening transition; the spinner never settles.
await tester.pump();
await tester.pump(const Duration(seconds: 1));
}
testWidgets('shows a spinner until the list arrives, then the item', (
tester,
) async {
await open(tester, 'a');
expect(find.byType(AppLoadingIndicator), findsOneWidget);
items.add(['a', 'b']);
await tester.pump();
expect(find.text('Item a of 2'), findsOneWidget);
expect(find.byType(AppLoadingIndicator), findsNothing);
});
testWidgets('says that an item missing from the list no longer exists', (
tester,
) async {
await open(tester, 'c');
items.add(['a', 'b']);
await tester.pump();
expect(find.textContaining('no longer exists'), findsOneWidget);
});
testWidgets('says so once the item goes while its screen is open', (
tester,
) async {
await open(tester, 'a');
items.add(['a']);
await tester.pump();
// Deleted on another device, say.
items.add([]);
await tester.pump();
expect(find.text('Item a of 1'), findsNothing);
expect(find.textContaining('no longer exists'), findsOneWidget);
});
testWidgets('a screen that closes as its item goes keeps showing the item '
'on its way out', (tester) async {
await open(tester, 'a');
items.add(['a']);
await tester.pump();
// As when the screen deleted its item and closes.
Navigator.of(tester.element(find.text('Item a of 1'))).pop();
items.add([]);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Item a of 1'), findsOneWidget);
expect(find.textContaining('no longer exists'), findsNothing);
await tester.pumpAndSettle();
expect(find.text('Open'), findsOneWidget);
});
}

Some files were not shown because too many files have changed in this diff Show more