import 'dart:convert' show base64; import 'dart:io' show File; import 'dart:typed_data'; import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; 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/mews_page_view.dart'; import 'package:mews/presentation/reader/palettes.dart'; Future _app() async { final strings = Strings(); await strings.load(); final deps = AppDependencies(database: MewsDatabase.test(NativeDatabase.memory())); return MewsApp(deps: deps, strings: strings); } void main() { 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 an address'), findsOneWidget); // The hub opens from the left icon; no floating button remains. expect(find.byIcon(Icons.menu), findsOneWidget); expect(find.byType(FloatingActionButton), findsNothing); }); testWidgets('renders a page and follows links', (tester) async { final page = MewsPage( url: 'https://example.com/index.html', title: 'Test page', lang: 'en', direction: TextDirection.ltr, variant: MewsVariant.plain, specVersion: '0.1', entries: const [], body: const [ HeadingBlock(1, [MewsText('A heading')]), ParagraphBlock([MewsText('Read on for more.')]), ParagraphBlock([ MewsLink('https://example.com/next.html', [MewsText('the next page')]), ]), ], ); String? opened; await tester.pumpWidget( MaterialApp( home: StringsScope( strings: await _strings(), child: MewsPageView( page: page, palette: paletteFor( pageVariant: MewsVariant.plain, readerVariant: 'site', dark: false), fontFamily: 'Atkinson Hyperlegible', fontScale: 1.0, visitedUrls: const {}, onLinkTap: (url) => opened = url, onFormSubmit: (_, _) {}, ), ), ), ); expect(find.text('A heading'), findsOneWidget); expect(find.textContaining('the next page'), findsOneWidget); // The paragraph Text stretches the full column width, so tap on the // glyphs of the link itself, not the widget's center. final rect = tester.getRect(find.textContaining('the next page')); await tester.tapAt(rect.topLeft + const Offset(30, 13)); await tester.pump(); expect(opened, 'https://example.com/next.html'); }); testWidgets('an image on a capsule arrives over the protocol', (tester) async { // A 1x1 PNG, small enough to spell out. final png = Uint8List.fromList(base64.decode( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==')); final dot = MewsPage( url: 'gemini://capsule.example/dot.png', lang: 'en', direction: TextDirection.ltr, variant: MewsVariant.plain, specVersion: 'gemtext', entries: const [], body: const [ ImageBlock(src: 'gemini://capsule.example/dot.png', alt: 'A dot', allowed: true), ], ); Future load(String url) async => url == 'gemini://capsule.example/dot.png' ? png : null; await tester.pumpWidget( MaterialApp( home: StringsScope( strings: await _strings(), child: MewsPageView( page: dot, palette: paletteFor( pageVariant: MewsVariant.plain, readerVariant: 'site', dark: false), fontFamily: 'Atkinson Hyperlegible', fontScale: 1.0, visitedUrls: const {}, onLinkTap: (_) {}, onFormSubmit: (_, _) {}, fetchImageBytes: load, ), ), ), ); await tester.pumpAndSettle(); // The image itself, decoded from the protocol's bytes. expect(find.byType(Image), findsOneWidget); // An image that never arrives falls back to its alt text, the way a // missing image on the web does. final missing = MewsPage( url: 'gemini://capsule.example/none.png', lang: 'en', direction: TextDirection.ltr, variant: MewsVariant.plain, specVersion: 'gemtext', entries: const [], body: const [ ImageBlock(src: 'gemini://capsule.example/none.png', alt: 'A dot', allowed: true), ], ); await tester.pumpWidget( MaterialApp( home: StringsScope( strings: await _strings(), child: MewsPageView( page: missing, palette: paletteFor( pageVariant: MewsVariant.plain, readerVariant: 'site', dark: false), fontFamily: 'Atkinson Hyperlegible', fontScale: 1.0, visitedUrls: const {}, onLinkTap: (_) {}, onFormSubmit: (_, _) {}, fetchImageBytes: load, ), ), ), ); await tester.pumpAndSettle(); expect(find.byType(Image), findsNothing); expect(find.textContaining('could not be loaded'), findsOneWidget); }); } Future _strings() async { final strings = Strings(); strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync()); return strings; }