import 'dart:io' show File; import 'package:flutter/foundation.dart' show TargetPlatform, debugDefaultTargetPlatformOverride; import 'package:flutter/services.dart' show LogicalKeyboardKey; import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderParagraph; import 'package:flutter_test/flutter_test.dart'; import 'package:mews/application/controllers/browser_controller.dart' show LoadFailed; 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/pages/mews_urls.dart'; import 'package:mews/presentation/app.dart'; import 'package:mews/presentation/reader/address_bar.dart'; import 'package:mews/presentation/reader/mews_page_view.dart' show searchTypingPause; import 'package:mews/presentation/reader/swipe_navigator.dart'; import 'fake_fetch.dart'; const pageSource = '''
Notes on old phones.
'''; String _page(String title, String body) => '''Never closed '''; final stubDeps = AppDependencies( database: MewsDatabase.test(NativeDatabase.memory()), fetch: FakeFetch(const {'https://example.com/journal': webPage}), strings: strings, ); await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings)); await tester.pumpAndSettle(); await stubDeps.browser.open('https://example.com/journal'); await tester.pumpAndSettle(); expect(find.textContaining('not a Mews page'), findsOneWidget); expect(find.textContaining('markup error'), findsNothing); expect(find.textContaining('Open in a web browser'), findsOneWidget); await tester.tap(find.textContaining('Show a simplified version')); await tester.pumpAndSettle(); // The structure survives, and what Mews cannot show is left out. expect(find.textContaining('A heading'), findsOneWidget); expect(find.textContaining('Loose text in a div'), findsOneWidget); expect(find.textContaining('Never closed'), findsOneWidget); expect(find.textContaining('Second'), findsWidgets); expect(find.textContaining('ignore.me'), findsNothing); expect(find.textContaining('simplified version'), findsWidgets); }); testWidgets('calls a page that claims Mews and will not parse broken', (tester) async { const claimed = '''
Never closed
''';
final stubDeps = AppDependencies(
database: MewsDatabase.test(NativeDatabase.memory()),
fetch: FakeFetch(const {'https://example.com/claimed': claimed}),
strings: strings,
);
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
await tester.pumpAndSettle();
await stubDeps.browser.open('https://example.com/claimed');
await tester.pumpAndSettle();
expect(find.textContaining('markup error'), findsOneWidget);
expect(find.textContaining('not a Mews page'), findsNothing);
});
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 first start gives the directory bookmark once', (tester) async {
final db = MewsDatabase.test(NativeDatabase.memory());
addTearDown(db.close);
// The first start of the app wires in the directory of sites, named in
// the reader's language.
var deps = AppDependencies(
database: db, fetch: FakeFetch(const {}), strings: strings);
await deps.start();
expect(await db.allBookmarks(), hasLength(1));
final bookmark = (await db.allBookmarks()).single;
expect(bookmark.url, 'https://mews.page/directory');
expect(bookmark.title, 'Mews directory');
expect(deps.bookmarks.isBookmarked(bookmark.url), isTrue);
// The reader removes it, as any bookmark can be removed.
await deps.bookmarks.toggle(bookmark.url, bookmark.title);
expect(deps.bookmarks.isBookmarked(bookmark.url), isFalse);
// A later start does not give it again: what the reader removed stays
// removed.
deps = AppDependencies(
database: db, fetch: FakeFetch(const {}), strings: strings);
await deps.start();
expect(await db.allBookmarks(), isEmpty);
});
testWidgets('a post a refresh finds arrives unread on the posts page',
(tester) async {
// The site's index, as gemtext with dated links (spec 6.1).
var index = '''
# A capsule
=> a.gmi 2026-10-08 Post a
=> b.gmi 2026-09-21 Post b
''';
final sources = {'gemini://capsule.example': index};
final stubDeps = AppDependencies(
database: MewsDatabase.test(NativeDatabase.memory()),
fetch: FakeFetch(sources),
strings: strings,
);
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
await tester.pumpAndSettle();
// Subscribing takes on what the site already published as read.
await stubDeps.browser.open('gemini://capsule.example');
await tester.pumpAndSettle();
await stubDeps.subscriptions.toggle(stubDeps.browser.currentPage.value!);
// The site publishes a new post, and a refresh finds it.
sources['gemini://capsule.example'] = '''
# A capsule
=> a.gmi 2026-10-08 Post a
=> b.gmi 2026-09-21 Post b
=> c.gmi 2026-10-11 New post
''';
await stubDeps.subscriptions.refreshAll();
// On the posts page the new post carries weight and the back catalogue
// does not, which is how the page says unread.
await stubDeps.browser.open('mews:posts');
await tester.pumpAndSettle();
// Whether a row's glyphs carry bold weight somewhere in their spans.
// TextSpan.visitChildren walks whole subtrees, so this walks the children
// itself, one level per step.
bool bold(String label) {
final text = tester.widget No entries here.Evil
'),
}),
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.
FuturePlain