feat: retire FOB and sheet; settings as a Mews page and actions on the address bar

This commit is contained in:
randogoth 2026-10-11 12:14:32 +03:00
parent 6f9c0e587a
commit 28ff7ca327
10 changed files with 324 additions and 361 deletions

View file

@ -8,17 +8,13 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mews/application/actors/fetch_actor.dart';
import 'package:mews/application/controllers/browser_controller.dart'
show BrokenPage, LoadFailed;
import 'package:mews/application/controllers/reader_settings_controller.dart'
show ReaderSettingsController;
import 'package:mews/database/database.dart';
import 'package:mews/l10n/strings.dart';
import 'package:mews/mews/document.dart';
import 'package:mews/mews/parser.dart';
import 'package:mews/application/internal_pages.dart';
import 'package:mews/presentation/app.dart';
import 'package:mews/presentation/reader/address_bar.dart';
import 'package:mews/presentation/reader/fob.dart';
import 'package:mews/presentation/reader/reader_screen.dart';
import 'package:mews/presentation/reader/settings_sheet.dart';
import 'package:mews/presentation/reader/swipe_navigator.dart';
const pageSource = '''
@ -143,53 +139,102 @@ void main() {
expect(find.textContaining('Try again'), findsOneWidget);
});
testWidgets('settings sheet changes presentation and toggles library',
testWidgets('the settings page changes presentation and keeps its place',
(tester) async {
await pumpApp(tester);
// Settings opens from the left address bar icon.
await tester.tap(find.byIcon(Icons.tune));
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, InternalPages.settingsUrl);
// Rendered like a Mews page with the option groups.
expect(find.text('Settings'), findsOneWidget);
expect(find.text('Typeface'), findsOneWidget);
expect(find.text('Theme'), findsOneWidget);
expect(find.text('Colors'), findsOneWidget);
expect(find.text('Library'), findsOneWidget);
// Tapping a theme link applies it and stays on the settings page; the
// lower sections scroll into view first.
Future<void> scrollTo(Finder target) async {
// Search down the page, then back up for targets above.
for (var i = 0; i < 20 && target.evaluate().isEmpty; i++) {
await tester.drag(find.byType(ListView), const Offset(0, -200));
await tester.pumpAndSettle();
}
for (var i = 0; i < 40 && target.evaluate().isEmpty; i++) {
await tester.drag(find.byType(ListView), const Offset(0, 200));
await tester.pumpAndSettle();
}
await tester.ensureVisible(target);
await tester.pumpAndSettle();
}
// The option rows stretch the full column width, so tap on the glyphs
// themselves rather than the widget's center.
Future<void> tapLink(String label) async {
final rect = tester.getRect(find.text(label));
await tester.tapAt(rect.topLeft + const Offset(30, 13));
await tester.pumpAndSettle();
}
await scrollTo(find.text('Dark'));
await tapLink('Dark');
expect(deps.settings.themeMode.value, 'dark');
expect(deps.browser.currentPage.value?.url, InternalPages.settingsUrl);
expect(deps.browser.canGoBack, isFalse);
await scrollTo(find.text('Warm'));
await tapLink('Warm');
expect(deps.settings.variant.value, 'warm');
await scrollTo(find.text('Larger text'));
await tapLink('Larger text');
expect(deps.settings.fontScale.value, 1.125);
// The library links navigate and join the walk history.
await scrollTo(find.text('History'));
await tapLink('History');
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, InternalPages.historyUrl);
expect(deps.browser.canGoBack, isTrue);
await deps.browser.back();
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, InternalPages.settingsUrl);
});
testWidgets('bookmark and subscribe live on the address bar', (tester) async {
await pumpApp(tester);
// A page with dated entries can be subscribed to.
final page = parseSource(pageSource, 'https://example.com/index.html');
deps.browser.currentPage.value = page;
await tester.pumpAndSettle();
showReaderSettingsSheet(tester.element(find.byType(ReaderScreen)));
await tester.pumpAndSettle();
// 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();
expect(deps.settings.fontScale.value, 1.125);
expect(deps.settings.themeMode.value, 'dark');
expect(deps.settings.variant.value, 'warm');
// Bookmark and subscribe the current page; both tiles sit at the bottom
// of the sheet, so scroll the sheet down to the end first.
await tester.drag(sheet, const Offset(0, -1000));
await tester.pumpAndSettle();
await tester.tap(find.text('Add bookmark'));
await tester.pumpAndSettle();
expect(deps.library.isBookmarked('https://example.com/index.html'), isTrue);
await tester.tap(find.text('Subscribe to this site'));
expect(find.byIcon(Icons.notifications_outlined), findsOneWidget);
await tester.tap(find.byIcon(Icons.notifications_outlined));
await tester.pumpAndSettle();
expect(deps.library.isSubscribed('https://example.com/index.html'), isTrue);
expect(find.byIcon(Icons.notifications_active), findsOneWidget);
// Settings survive a reload of the controller.
final reloaded = ReaderSettingsController(db);
await reloaded.load();
expect(reloaded.themeMode.value, 'dark');
expect(find.byIcon(Icons.star_border), findsOneWidget);
await tester.tap(find.byIcon(Icons.star_border));
await tester.pumpAndSettle();
expect(deps.library.isBookmarked('https://example.com/index.html'), isTrue);
expect(find.byIcon(Icons.star), findsOneWidget);
// Pages with neither Atom feed nor dated links offer no subscription.
final plain = parseSource(
_page('Plain', '<h1>Plain</h1><p>No entries here.</p>'),
'https://example.com/plain.html');
deps.browser.currentPage.value = plain;
await tester.pumpAndSettle();
expect(find.byIcon(Icons.notifications_outlined), findsNothing);
});
testWidgets('the FOB hides on scroll down and returns on scroll up',
testWidgets('the address bar hides on scroll down and returns on scroll up',
(tester) async {
await pumpApp(tester);
@ -201,19 +246,16 @@ void main() {
deps.browser.currentPage.value = page;
await tester.pumpAndSettle();
Fob fob() => tester.widget<Fob>(find.byType(Fob));
expect(fob().visible, isTrue);
expect(find.byType(AddressBar), findsOneWidget);
// Scrolling down hides the address bar and the FOB.
// Scrolling down hides the address bar.
await tester.drag(find.byType(ListView), const Offset(0, -300));
await tester.pumpAndSettle();
expect(fob().visible, isFalse);
expect(find.byType(AddressBar), findsNothing);
// Scrolling up brings both back.
// Scrolling up brings it back.
await tester.drag(find.byType(ListView), const Offset(0, 300));
await tester.pumpAndSettle();
expect(fob().visible, isTrue);
expect(find.byType(AddressBar), findsOneWidget);
});
@ -260,40 +302,6 @@ 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(
@ -375,17 +383,6 @@ void main() {
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);

View file

@ -8,7 +8,6 @@ import 'package:mews/database/database.dart';
import 'package:mews/l10n/strings.dart';
import 'package:mews/mews/document.dart';
import 'package:mews/presentation/app.dart';
import 'package:mews/presentation/reader/fob.dart';
import 'package:mews/presentation/reader/mews_page_view.dart';
import 'package:mews/presentation/reader/palettes.dart';
@ -20,16 +19,15 @@ Future<MewsApp> _app() async {
}
void main() {
testWidgets('starts with the address bar and the FOB on screen', (tester) async {
testWidgets('starts with the address bar and its icons', (tester) async {
await tester.pumpWidget(await _app());
await tester.pumpAndSettle();
expect(find.byType(TextField), findsOneWidget);
expect(find.text('Enter a web address'), findsOneWidget);
// The FOB is on screen from the beginning and hides only while the
// reader scrolls down.
final fob = tester.widget<Fob>(find.byType(Fob));
expect(fob.visible, isTrue);
// Settings opens from the left icon; no floating button remains.
expect(find.byIcon(Icons.tune), findsOneWidget);
expect(find.byType(FloatingActionButton), findsNothing);
});
testWidgets('renders a page and follows links', (tester) async {