2026-10-11 22:07:25 +03:00
|
|
|
import 'dart:convert' show base64;
|
2026-10-11 09:33:25 +03:00
|
|
|
import 'dart:io' show File;
|
2026-10-11 22:07:25 +03:00
|
|
|
import 'dart:typed_data';
|
2026-10-11 09:33:25 +03:00
|
|
|
|
|
|
|
|
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<MewsApp> _app() async {
|
|
|
|
|
final strings = Strings();
|
|
|
|
|
await strings.load();
|
|
|
|
|
final deps = AppDependencies(database: MewsDatabase.test(NativeDatabase.memory()));
|
|
|
|
|
return MewsApp(deps: deps, strings: strings);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
2026-10-11 12:14:32 +03:00
|
|
|
testWidgets('starts with the address bar and its icons', (tester) async {
|
2026-10-11 09:33:25 +03:00
|
|
|
await tester.pumpWidget(await _app());
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(find.byType(TextField), findsOneWidget);
|
2026-10-11 22:07:25 +03:00
|
|
|
expect(find.text('Enter an address'), findsOneWidget);
|
2026-10-11 13:05:07 +03:00
|
|
|
// The hub opens from the left icon; no floating button remains.
|
|
|
|
|
expect(find.byIcon(Icons.menu), findsOneWidget);
|
2026-10-11 12:14:32 +03:00
|
|
|
expect(find.byType(FloatingActionButton), findsNothing);
|
2026-10-11 09:33:25 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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(
|
2026-10-11 22:07:25 +03:00
|
|
|
strings: await _strings(),
|
2026-10-11 09:33:25 +03:00
|
|
|
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');
|
|
|
|
|
});
|
2026-10-11 22:07:25 +03:00
|
|
|
|
|
|
|
|
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<Uint8List?> 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);
|
|
|
|
|
});
|
2026-10-11 09:33:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<Strings> _strings() async {
|
|
|
|
|
final strings = Strings();
|
|
|
|
|
strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync());
|
|
|
|
|
return strings;
|
|
|
|
|
}
|