(
find
.descendant(
of: find.byWidgetPredicate((widget) =>
widget is Text &&
(widget.textSpan?.toPlainText() ?? widget.data ?? '')
.contains(label)),
matching: find.byType(RichText),
)
.first,
);
final plain = paragraph.text.toPlainText();
final start = plain.indexOf(label);
final box = paragraph
.getBoxesForSelection(TextSelection(
baseOffset: start, extentOffset: start + label.length))
.first;
await tester.tapAt(paragraph.localToGlobal(
Offset(box.left + 2, (box.top + box.bottom) / 2)));
await tester.pumpAndSettle();
}
testWidgets('renders a page through the full app', (tester) async {
await pumpApp(tester);
final page = parseSource(pageSource, 'https://example.com/index.html');
deps.browser.currentPage.value = page;
await tester.pumpAndSettle();
expect(find.text('Signal Bars'), findsWidgets);
expect(find.textContaining('Collecting retro phones'), findsOneWidget);
expect(find.textContaining('Sign guestbook'), findsOneWidget);
});
testWidgets('shows the broken-page error and its text view', (tester) async {
await pumpApp(tester);
deps.browser.currentPage.value = null;
deps.browser.error.value = const BrokenPage('Half a page & more');
await tester.pumpAndSettle();
expect(find.textContaining('markup error'), findsOneWidget);
await tester.tap(find.textContaining('Show page text'));
await tester.pumpAndSettle();
expect(find.textContaining('Half a page & more'), findsOneWidget);
});
testWidgets('shows the load-failed error page', (tester) async {
await pumpApp(tester);
deps.browser.error.value = const LoadFailed();
await tester.pumpAndSettle();
expect(find.textContaining('could not be loaded'), findsOneWidget);
expect(find.textContaining('Try again'), findsOneWidget);
});
testWidgets('a web page that will not load can open in a web browser',
(tester) async {
final stubDeps = AppDependencies(
database: MewsDatabase.test(NativeDatabase.memory()),
fetch: FakeFetch(const {}),
strings: strings,
);
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
await tester.pumpAndSettle();
// A web address that answers nothing: the error offers the way out,
// since the web can be shown outside this reader.
await stubDeps.browser.open('https://missing.example.org/');
await tester.pumpAndSettle();
expect(stubDeps.browser.error.value, isA());
expect(find.text('Open in a web browser'), findsOneWidget);
// A protocol page has nowhere else to go, so only trying again is
// offered there.
await stubDeps.browser.open('gemini://missing.example.org/');
await tester.pumpAndSettle();
expect(stubDeps.browser.error.value, isA());
expect(find.text('Open in a web browser'), findsNothing);
});
testWidgets('the hub opens the settings page, which applies a choice in place',
(tester) async {
await pumpApp(tester);
// The hub opens from the left address bar icon.
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, MewsUrls.hub);
expect(find.text('Mews'), findsOneWidget);
expect(find.textContaining('No bookmarks yet'), findsOneWidget);
// Its menu reaches settings, which carries the strip of our own pages.
await tapRun(tester, 'Settings');
expect(deps.browser.currentPage.value?.url, MewsUrls.settings);
expect(find.text('Settings'), findsOneWidget);
expect(find.text('Typeface'), findsOneWidget);
expect(find.text('Theme'), findsOneWidget);
expect(find.text('Colors'), findsOneWidget);
// The buried Library section is gone; the strip replaced it.
expect(find.text('Library'), findsNothing);
Future 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();
}
// Choosing a value applies it and stays put: no new walk history entry.
await scrollTo(find.text('Dark'));
await tapRun(tester, 'Dark');
expect(deps.settings.themeMode.value, 'dark');
expect(deps.browser.currentPage.value?.url, MewsUrls.settings);
expect(deps.browser.canGoBack, isTrue);
final beforeSize = deps.browser.currentPage.value;
await scrollTo(find.text('Warm'));
await tapRun(tester, 'Warm');
expect(deps.settings.variant.value, 'warm');
await scrollTo(find.text('Larger text'));
await tapRun(tester, 'Larger text');
expect(deps.settings.fontScale.value, 1.125);
expect(deps.browser.currentPage.value, isNot(same(beforeSize)));
// The strip navigates and joins the walk history.
await scrollTo(find.textContaining('Bookmarks'));
await tapRun(tester, 'History');
expect(deps.browser.currentPage.value?.url, MewsUrls.history);
await deps.browser.back();
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, MewsUrls.settings);
});
testWidgets('a fetched page cannot run an action link', (tester) async {
final stubDeps = AppDependencies(
database: MewsDatabase.test(NativeDatabase.memory()),
fetch: FakeFetch({
'https://example.com/evil.html': _page('Evil',
'Evil
Go
'),
}),
strings: strings,
);
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
await tester.pumpAndSettle();
await stubDeps.browser.open('https://example.com/evil.html');
await tester.pumpAndSettle();
expect(stubDeps.settings.themeMode.value, 'system');
// Following the link from a fetched page changes nothing and goes nowhere
// (spec 8.1: page content must not act for the reader).
await tapRun(tester, 'Go');
expect(stubDeps.settings.themeMode.value, 'system');
expect(stubDeps.browser.currentPage.value?.url,
'https://example.com/evil.html');
// The same link works from one of our own pages.
await stubDeps.browser.open(MewsUrls.settings);
await stubDeps.browser.open('mews:do/set-theme?value=dark');
expect(stubDeps.settings.themeMode.value, 'dark');
expect(stubDeps.browser.currentPage.value?.url, MewsUrls.settings);
});
testWidgets('a form on one of our pages navigates instead of fetching',
(tester) async {
await pumpApp(tester);
// The fetch client cannot speak the mews scheme, so a form on an internal
// page must be routed before it ever reaches the actor.
await deps.browser.open(MewsUrls.bookmarks);
await deps.browser.submitForm(MewsUrls.bookmarks, 'get', {'q': 'retro'});
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, 'mews:bookmarks?q=retro');
expect(deps.browser.error.value, isNull);
// A form whose action is an action link applies it in place.
await deps.browser.open(MewsUrls.settings);
await deps.browser.submitForm('mews:do/set-theme', 'get', {'value': 'dark'});
await tester.pumpAndSettle();
expect(deps.settings.themeMode.value, 'dark');
expect(deps.browser.currentPage.value?.url, MewsUrls.settings);
expect(deps.browser.error.value, isNull);
});
testWidgets('choosing a language changes the interface in place',
(tester) async {
await pumpApp(tester);
await deps.browser.open(MewsUrls.settings);
await tester.pumpAndSettle();
expect(find.text('Settings'), findsOneWidget);
// The language group sits below the fold.
Future scrollTo(String label) async {
final target = find.textContaining(label);
for (var i = 0; i < 30 && target.evaluate().isEmpty; i++) {
await tester.drag(find.byType(ListView), const Offset(0, -200));
await tester.pumpAndSettle();
}
await tester.ensureVisible(target.first);
await tester.pumpAndSettle();
}
// Each language is named in its own words, so it can be found from an
// interface the reader cannot read.
await scrollTo('Deutsch');
final stepsBack = deps.browser.canGoBack;
await tapRun(tester, 'Deutsch');
expect(deps.settings.language.value, 'de');
// Applied in place, like the other reader choices.
expect(deps.browser.currentPage.value?.url, MewsUrls.settings);
expect(deps.browser.canGoBack, stepsBack);
expect(find.text('Einstellungen'), findsOneWidget);
expect(find.text('Settings'), findsNothing);
// Hebrew turns the whole page around.
await scrollTo('עברית');
await tapRun(tester, 'עברית');
expect(deps.settings.language.value, 'he');
expect(deps.browser.currentPage.value?.direction, TextDirection.rtl);
// Back to English.
await scrollTo('English');
await tapRun(tester, 'English');
expect(deps.settings.language.value, 'en-US');
expect(deps.browser.currentPage.value?.direction, TextDirection.ltr);
expect(find.text('Settings'), findsOneWidget);
});
testWidgets('bare mews: and an unknown name resolve predictably',
(tester) async {
await pumpApp(tester);
await deps.browser.open('mews:');
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, MewsUrls.hub);
await deps.browser.open('mews:nonsense');
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value, isNull);
expect(deps.browser.error.value, isA());
});
testWidgets('a session with no history starts at the hub', (tester) async {
await pumpApp(tester);
await deps.browser.restoreLastSession();
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, MewsUrls.hub);
});
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();
expect(find.byIcon(Icons.notifications_outlined), findsOneWidget);
await tester.tap(find.byIcon(Icons.notifications_outlined));
await tester.pumpAndSettle();
expect(deps.subscriptions.isSubscribed('https://example.com/index.html'), isTrue);
expect(find.byIcon(Icons.notifications_active), findsOneWidget);
expect(find.byIcon(Icons.star_border), findsOneWidget);
await tester.tap(find.byIcon(Icons.star_border));
await tester.pumpAndSettle();
expect(deps.bookmarks.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', 'Plain
No entries here.
'),
'https://example.com/plain.html');
deps.browser.currentPage.value = plain;
await tester.pumpAndSettle();
expect(find.byIcon(Icons.notifications_outlined), findsNothing);
});
testWidgets('the address bar hides on scroll down and returns on scroll up',
(tester) async {
await pumpApp(tester);
// A page long enough to scroll.
final tall = pageSource.replaceFirst(
'