feat: show real neighboring pages during back and forward swipes
This commit is contained in:
parent
4e07c9a305
commit
c104c2026b
7 changed files with 283 additions and 71 deletions
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:io' show File;
|
||||
|
||||
import 'package:actors/actors.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:mews/application/actors/fetch_actor.dart';
|
||||
import 'package:mews/application/controllers/browser_controller.dart'
|
||||
show BrokenPage, LoadFailed;
|
||||
import 'package:mews/application/controllers/reader_settings_controller.dart'
|
||||
|
|
@ -17,6 +19,7 @@ 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';
|
||||
import 'package:mews/presentation/reader/swipe_navigator.dart';
|
||||
|
||||
const pageSource = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
|
@ -41,6 +44,50 @@ const pageSource = '''
|
|||
</html>
|
||||
''';
|
||||
|
||||
/// Serves pages from an in-memory map without network or isolates, so
|
||||
/// navigation tests are deterministic. Cached pages parse back from their
|
||||
/// cached source, like the real actor does for revalidation.
|
||||
class _FakeFetch implements Actor<FetchCommand, FetchResult> {
|
||||
_FakeFetch(this.sources);
|
||||
|
||||
final Map<String, String> sources;
|
||||
|
||||
@override
|
||||
Future<FetchResult> send(FetchCommand message) async {
|
||||
return switch (message) {
|
||||
ParseSource(:final url, :final source) => _parse(url, source),
|
||||
FetchPage(:final url, :final cachedSource) => cachedSource != null
|
||||
? _parse(url, cachedSource)
|
||||
: sources[url] != null
|
||||
? _parse(url, sources[url]!)
|
||||
: const FetchNetworkError(),
|
||||
_ => const FetchNetworkError(),
|
||||
};
|
||||
}
|
||||
|
||||
FetchResult _parse(String url, String source) {
|
||||
try {
|
||||
return FetchPageOk(page: parseMewsPage(source: source, url: url), source: source);
|
||||
} on MewsParseException catch (e) {
|
||||
return FetchParseError(e.message, source);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
String _page(String title, String body) => '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>$title</title><meta name="mews-profile" content="0.1" /></head>
|
||||
<body>$body</body>
|
||||
</html>
|
||||
''';
|
||||
|
||||
void main() {
|
||||
late MewsDatabase db;
|
||||
late AppDependencies deps;
|
||||
|
|
@ -166,6 +213,46 @@ void main() {
|
|||
expect(find.byType(AddressBar), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('swiping back shows the previous page during the drag',
|
||||
(tester) async {
|
||||
final sources = {
|
||||
'https://example.com/a.html':
|
||||
_page('Page A', '<h1>Page A</h1><p>The first pages content.</p>'),
|
||||
'https://example.com/b.html':
|
||||
_page('Page B', '<h1>Page B</h1><p>The second pages content.</p>'),
|
||||
};
|
||||
final stubDeps = AppDependencies(
|
||||
database: MewsDatabase.test(NativeDatabase.memory()),
|
||||
fetch: _FakeFetch(sources),
|
||||
);
|
||||
final strings = Strings();
|
||||
strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync());
|
||||
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await stubDeps.browser.open('https://example.com/a.html');
|
||||
await stubDeps.browser.open('https://example.com/b.html');
|
||||
// The swipe neighbor is parsed in the background.
|
||||
await _pumpUntil(tester, () => stubDeps.browser.backPage.value != null);
|
||||
|
||||
final center = tester.getCenter(find.byType(SwipeNavigator));
|
||||
final gesture = await tester.startGesture(center);
|
||||
await gesture.moveBy(const Offset(400, 0));
|
||||
await tester.pump();
|
||||
|
||||
// The real previous page is on screen mid-swipe, not a placeholder.
|
||||
expect(find.text('Page A'), findsOneWidget);
|
||||
expect(find.textContaining('The first pages content'), findsOneWidget);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/a.html');
|
||||
// Settled on the page itself; the address bar follows.
|
||||
expect(find.textContaining('The first pages content'), findsOneWidget);
|
||||
expect(find.text('https://example.com/a.html'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('reports new posts once per page', (tester) async {
|
||||
await pumpApp(tester);
|
||||
|
||||
|
|
@ -188,3 +275,16 @@ void main() {
|
|||
|
||||
MewsPage parseSource(String source, String url) =>
|
||||
parseMewsPage(source: source, url: url);
|
||||
|
||||
/// Pumps in short bursts until [condition] holds; fails the test on timeout.
|
||||
Future<void> _pumpUntil(WidgetTester tester, bool Function() condition,
|
||||
{Duration timeout = const Duration(seconds: 5)}) async {
|
||||
final end = DateTime.now().add(timeout);
|
||||
while (!condition()) {
|
||||
if (DateTime.now().isAfter(end)) {
|
||||
fail('condition not met within timeout');
|
||||
}
|
||||
await tester.pump(const Duration(milliseconds: 20));
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
|
|
|||
39
app/test/fetch_actor_test.dart
Normal file
39
app/test/fetch_actor_test.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:actors/actors.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:mews/application/actors/fetch_actor.dart';
|
||||
|
||||
const page = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Parsed</title><meta name="mews-profile" content="0.1" /></head>
|
||||
<body><p>Some content.</p></body>
|
||||
</html>
|
||||
''';
|
||||
|
||||
void main() {
|
||||
test('ParseSource parses without touching the network', () async {
|
||||
final actor = Actor.create(FetchHandler.new);
|
||||
addTearDown(actor.close);
|
||||
|
||||
final result = await actor
|
||||
.send(const ParseSource(url: 'https://example.com/parsed.html', source: page));
|
||||
|
||||
expect(result, isA<FetchPageOk>());
|
||||
final ok = result as FetchPageOk;
|
||||
expect(ok.page.title, 'Parsed');
|
||||
expect(ok.page.isMews, isTrue);
|
||||
expect(ok.notModified, isFalse);
|
||||
});
|
||||
|
||||
test('ParseSource reports broken markup with its source', () async {
|
||||
final actor = Actor.create(FetchHandler.new);
|
||||
addTearDown(actor.close);
|
||||
|
||||
final result = await actor.send(
|
||||
const ParseSource(url: 'https://example.com/broken.html', source: '<p>nope'));
|
||||
|
||||
expect(result, isA<FetchParseError>());
|
||||
expect((result as FetchParseError).source, '<p>nope');
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue