fix: mount StringsScope, keep list inline text, show FOB until scroll down

This commit is contained in:
randogoth 2026-10-11 10:28:54 +03:00
parent 9a11194f77
commit 65e693b9c6
6 changed files with 281 additions and 27 deletions

View file

@ -0,0 +1,190 @@
import 'dart:io' show File;
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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/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';
const pageSource = '''
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Signal Bars</title>
<meta name="mews-profile" content="0.1" />
</head>
<body class="mews-green">
<h1>Signal Bars</h1>
<p>Notes on old phones.</p>
<ul>
<li><a href="2026-10-08-retro.html">2026-10-08 Collecting retro phones</a></li>
<li><a href="2026-09-21-first.html">2026-09-21 First post</a></li>
</ul>
<form action="/sign" method="post">
<label for="name">Your name</label>
<input type="text" id="name" name="name" />
<input type="submit" value="Sign guestbook" />
</form>
</body>
</html>
''';
void main() {
late MewsDatabase db;
late AppDependencies deps;
setUp(() async {
db = MewsDatabase.test(NativeDatabase.memory());
deps = AppDependencies(database: db);
});
tearDown(() async {
await db.close();
});
Future<void> pumpApp(WidgetTester tester) async {
final strings = Strings();
strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync());
await tester.pumpWidget(MewsApp(deps: deps, strings: strings));
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('<p>Half a page &amp; 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('settings sheet changes presentation and toggles library',
(tester) async {
await pumpApp(tester);
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.
await tester.tap(find.text('Atkinson Hyperlegible'));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
await tester.tap(find.text('Dark'));
await tester.pumpAndSettle();
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.
final sheet = find.byType(Scrollable).last;
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'));
await tester.pumpAndSettle();
expect(deps.library.isSubscribed('https://example.com/index.html'), isTrue);
// Settings survive a reload of the controller.
final reloaded = ReaderSettingsController(db);
await reloaded.load();
expect(reloaded.themeMode.value, 'dark');
});
testWidgets('the FOB hides on scroll down and returns on scroll up',
(tester) async {
await pumpApp(tester);
// A page long enough to scroll.
final tall = pageSource.replaceFirst(
'</body>',
'<p>Padding paragraph to make the page scroll.</p>' * 80 + '</body>');
final page = parseSource(tall, 'https://example.com/index.html');
deps.browser.currentPage.value = page;
await tester.pumpAndSettle();
Fob fob() => tester.widget<Fob>(find.byType(Fob));
expect(fob().visible, isTrue);
// Scrolling down hides the address bar and the FOB.
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.
await tester.drag(find.byType(ListView), const Offset(0, 300));
await tester.pumpAndSettle();
expect(fob().visible, isTrue);
expect(find.byType(AddressBar), findsOneWidget);
});
testWidgets('reports new posts once per page', (tester) async {
await pumpApp(tester);
final page = parseSource(pageSource, 'https://example.com/index.html');
deps.library.subscribedUrls.value = {'https://example.com/index.html'};
deps.library.newEntries.value = {
'https://example.com/index.html': [
MewsEntry(
url: 'https://example.com/2026-10-08-retro.html',
date: DateTime(2026, 10, 8),
title: 'Collecting retro phones'),
],
};
deps.browser.currentPage.value = page;
await tester.pumpAndSettle();
expect(find.textContaining('1 new post'), findsOneWidget);
});
}
MewsPage parseSource(String source, String url) =>
parseMewsPage(source: source, url: url);

View file

@ -164,4 +164,39 @@ void main() {
final text = mewsPlainText(broken);
expect(text, contains('Visible & intact.'));
});
test('keeps bare inline content in list items and quotes', () {
final source = '''
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Bare</title><meta name="mews-profile" content="0.1" /></head>
<body>
<ul>
<li><a href="2026-10-08-post.html">2026-10-08 A dated link</a></li>
<li>Plain item</li>
<li>Text then a list:<ul><li>Nested</li></ul></li>
</ul>
<blockquote>Quoted words <em>verbatim</em>.</blockquote>
</body>
</html>
''';
final page = parseMewsPage(source: source, url: 'https://example.com/bare.html');
final list = page.body[0] as ListBlock;
final first = (list.items[0].blocks.single as ParagraphBlock).inline;
final link = first.single as MewsLink;
expect(link.href, 'https://example.com/2026-10-08-post.html');
expect((link.children.single as MewsText).text, '2026-10-08 A dated link');
expect((list.items[1].blocks.single as ParagraphBlock).inline.single,
isA<MewsText>());
// Text before a nested list stays with the item; the list becomes a block.
expect(list.items[2].blocks, hasLength(2));
expect(list.items[2].blocks[0], isA<ParagraphBlock>());
expect(list.items[2].blocks[1], isA<ListBlock>());
final quote = page.body[1] as QuoteBlock;
final quoteText = (quote.children.single as ParagraphBlock).inline;
expect(quoteText, hasLength(3));
expect((quoteText[0] as MewsText).text, 'Quoted words ');
});
}

View file

@ -20,16 +20,16 @@ Future<MewsApp> _app() async {
}
void main() {
testWidgets('starts with the address bar and no FOB', (tester) async {
testWidgets('starts with the address bar and the FOB on screen', (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 only appears once the reader scrolls a page; without a page it
// stays invisible and untouchable.
// 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, isFalse);
expect(fob.visible, isTrue);
});
testWidgets('renders a page and follows links', (tester) async {