feat: theme chrome by palette, selectable text, wap fallback, and library pages

This commit is contained in:
randogoth 2026-10-11 11:48:24 +03:00
parent c104c2026b
commit 56f3d959fa
12 changed files with 534 additions and 51 deletions

View file

@ -91,19 +91,21 @@ String _page(String title, String body) => '''
void main() {
late MewsDatabase db;
late AppDependencies deps;
late Strings strings;
setUp(() async {
db = MewsDatabase.test(NativeDatabase.memory());
deps = AppDependencies(database: db);
strings = Strings();
strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync());
deps = AppDependencies(database: db, strings: strings);
});
tearDown(() async {
await db.close();
});
/// Pumps the full app for [deps]; library pages get real strings.
Future<void> pumpApp(WidgetTester tester) async {
final strings = Strings();
strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync());
await tester.pumpWidget(MewsApp(deps: deps, strings: strings));
await tester.pumpAndSettle();
}
@ -152,13 +154,16 @@ void main() {
showReaderSettingsSheet(tester.element(find.byType(ReaderScreen)));
await tester.pumpAndSettle();
// Typeface, size, theme, colors.
// Typeface, size, theme, colors; the lower sections need scrolling.
final sheet = find.byType(Scrollable).last;
await tester.tap(find.text('Atkinson Hyperlegible'));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(find.text('Dark'), 200, scrollable: sheet);
await tester.tap(find.text('Dark'));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(find.text('Warm'), 200, scrollable: sheet);
await tester.tap(find.text('Warm'));
await tester.pumpAndSettle();
@ -168,7 +173,6 @@ void main() {
// Bookmark and subscribe the current page; both tiles sit at the bottom
// of the sheet, so scroll the sheet down to the end first.
final sheet = find.byType(Scrollable).last;
await tester.drag(sheet, const Offset(0, -1000));
await tester.pumpAndSettle();
await tester.tap(find.text('Add bookmark'));
@ -224,9 +228,8 @@ void main() {
final stubDeps = AppDependencies(
database: MewsDatabase.test(NativeDatabase.memory()),
fetch: _FakeFetch(sources),
strings: strings,
);
final strings = Strings();
strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync());
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
await tester.pumpAndSettle();
@ -235,10 +238,14 @@ void main() {
// The swipe neighbor is parsed in the background.
await _pumpUntil(tester, () => stubDeps.browser.backPage.value != null);
// Swipes begin at the screen edge; mid-page drags select text. Small
// steps keep the drag recognizer's update pacing in the test environment.
final center = tester.getCenter(find.byType(SwipeNavigator));
final gesture = await tester.startGesture(center);
await gesture.moveBy(const Offset(400, 0));
await tester.pump();
final gesture = await tester.startGesture(Offset(10, center.dy));
for (var i = 0; i < 6; i++) {
await gesture.moveBy(const Offset(70, 0));
await tester.pump();
}
// The real previous page is on screen mid-swipe, not a placeholder.
expect(find.text('Page A'), findsOneWidget);
@ -253,6 +260,132 @@ void main() {
expect(find.text('https://example.com/a.html'), findsOneWidget);
});
testWidgets('the chrome follows the selected colors', (tester) async {
await pumpApp(tester);
await deps.settings.setThemeMode('light');
await deps.settings.setVariant('warm');
await tester.pumpAndSettle();
MaterialApp app() => tester.widget<MaterialApp>(find.byType(MaterialApp));
// Warm light palette on the light theme, FOB included.
expect(app().theme!.colorScheme.surface, const Color(0xfffbf4e8));
expect(app().theme!.colorScheme.primary, const Color(0xff8a3d14));
expect(app().theme!.floatingActionButtonTheme.backgroundColor,
const Color(0xff8a3d14));
expect(app().theme!.textSelectionTheme.selectionColor,
const Color(0xff8a3d14).withValues(alpha: 0.3));
await deps.settings.setThemeMode('dark');
await deps.settings.setVariant('green');
await tester.pumpAndSettle();
// Green dark palette on the dark theme.
expect(app().darkTheme!.colorScheme.surface, const Color(0xff161b16));
expect(app().darkTheme!.colorScheme.primary, const Color(0xff9fd39a));
expect(app().darkTheme!.floatingActionButtonTheme.backgroundColor,
const Color(0xff9fd39a));
});
testWidgets('page text is selectable', (tester) async {
await pumpApp(tester);
final page = parseSource(pageSource, 'https://example.com/index.html');
deps.browser.currentPage.value = page;
await tester.pumpAndSettle();
expect(find.byType(SelectionArea), findsOneWidget);
});
testWidgets('falls back to the wap subdomain before showing an error',
(tester) async {
final stubDeps = AppDependencies(
database: MewsDatabase.test(NativeDatabase.memory()),
fetch: _FakeFetch({
'https://wap.example.com/':
_page('Wap site', '<h1>Wap site</h1><p>Content lives here.</p>'),
}),
strings: strings,
);
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
await tester.pumpAndSettle();
// The address as typed has no page; the wap. equivalent does.
await stubDeps.browser.open('https://example.com/');
expect(stubDeps.browser.currentPage.value?.url, 'https://wap.example.com/');
await tester.pumpAndSettle();
expect(find.textContaining('Content lives here'), findsOneWidget);
// Addresses that work nowhere keep showing the error.
await stubDeps.browser.open('https://missing.example.org/');
expect(stubDeps.browser.currentPage.value, isNull);
expect(stubDeps.browser.error.value, isA<LoadFailed>());
});
testWidgets('library pages render as Mews pages and join the walk history',
(tester) async {
final stubDeps = AppDependencies(
database: MewsDatabase.test(NativeDatabase.memory()),
fetch: _FakeFetch({
'https://example.com/a.html':
_page('Page A', '<h1>Page A</h1><p>The first page.</p>'),
'https://example.com/b.html':
_page('Page B', '<h1>Page B</h1><p>The second page.</p>'),
}),
strings: strings,
);
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
await tester.pumpAndSettle();
await stubDeps.browser.open('https://example.com/a.html');
await stubDeps.browser.open('https://example.com/b.html');
await stubDeps.browser.open('mews:history');
await tester.pumpAndSettle();
// Rendered like any Mews page, with dated links to both visits.
expect(stubDeps.browser.currentPage.value?.url, 'mews:history');
expect(find.text('History'), findsOneWidget);
expect(find.textContaining('Page A'), findsOneWidget);
expect(find.textContaining('Page B'), findsOneWidget);
// Part of the walk: back returns to page B, forward to history again.
await stubDeps.browser.back();
await tester.pumpAndSettle();
expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/b.html');
await stubDeps.browser.forward();
await tester.pumpAndSettle();
expect(stubDeps.browser.currentPage.value?.url, 'mews:history');
// A history link navigates like any other link.
await tester.tap(find.textContaining('Page A'));
await tester.pumpAndSettle();
expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/a.html');
// Bookmarks and subscriptions get the same treatment.
await stubDeps.library.load();
await stubDeps.library.toggleBookmark('https://example.com/b.html', 'Page B');
await stubDeps.library.toggleSubscription(
'https://example.com/a.html', 'Page A', const []);
await stubDeps.browser.open('mews:bookmarks');
await tester.pumpAndSettle();
expect(find.text('Bookmarks'), findsOneWidget);
expect(find.textContaining('Page B'), findsOneWidget);
await stubDeps.browser.open('mews:subscriptions');
await tester.pumpAndSettle();
expect(find.text('Subscriptions'), findsOneWidget);
expect(find.textContaining('Page A'), findsOneWidget);
});
testWidgets('the settings sheet opens the library pages', (tester) async {
await pumpApp(tester);
showReaderSettingsSheet(tester.element(find.byType(ReaderScreen)));
await tester.pumpAndSettle();
await tester.tap(find.text('History'));
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, 'mews:history');
expect(find.text('History'), findsOneWidget);
});
testWidgets('reports new posts once per page', (tester) async {
await pumpApp(tester);