the-mews/app/test/quotations_page_test.dart

232 lines
8.8 KiB
Dart
Raw Normal View History

import 'dart:io' show File;
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/pages/mews_urls.dart';
import 'package:mews/database/database.dart';
import 'package:mews/l10n/strings.dart';
import 'package:mews/mews/document.dart' show MewsEntry;
import 'package:mews/presentation/app.dart';
import 'fake_fetch.dart';
void main() {
late MewsDatabase db;
late Strings strings;
final shared = <String>[];
setUp(() async {
db = MewsDatabase.test(NativeDatabase.memory());
strings = Strings();
strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync());
});
tearDown(() async {
await db.close();
shared.clear();
});
Future<AppDependencies> pumpApp(WidgetTester tester) async {
final deps = AppDependencies(
database: db,
fetch: FakeFetch(const {}),
strings: strings,
share: (text) async => shared.add(text),
);
await tester.pumpWidget(MewsApp(deps: deps, strings: strings));
await tester.pumpAndSettle();
return deps;
}
/// Taps the run reading [label] inside a rendered paragraph: several links
/// share one paragraph, so the widget's center can miss the glyphs. Pass
/// [within] to pick the paragraph when several carry the label.
Future<void> tapRun(WidgetTester tester, String label, {String? within}) async {
final text = within ?? label;
final paragraphFinder = find.byWidgetPredicate((widget) =>
widget is Text &&
(widget.textSpan?.toPlainText() ?? widget.data ?? '').contains(text));
// The row may sit below the fold; its glyphs must be on screen to tap.
await tester.ensureVisible(paragraphFinder.first);
await tester.pumpAndSettle();
final paragraph = tester.renderObject<RenderParagraph>(
find.descendant(of: paragraphFinder, matching: find.byType(RichText)).first,
);
final plain = paragraph.text.toPlainText();
final start = plain.indexOf(label);
final box = paragraph
.getBoxesForSelection(
TextSelection(baseOffset: start, extentOffset: start + label.length))
.first;
await tester.tapAt(paragraph.localToGlobal(
Offset(box.left + 2, (box.top + box.bottom) / 2)));
await tester.pumpAndSettle();
}
testWidgets('the hub counts the kept quotations', (tester) async {
final deps = await pumpApp(tester);
await deps.browser.restoreLastSession();
await tester.pumpAndSettle();
expect(find.textContaining('No quotations yet'), findsOneWidget);
await db.addQuotation(
quote: 'A line worth keeping.',
pageUrl: 'https://example.com/a.html');
await deps.browser.reload();
await tester.pumpAndSettle();
expect(find.textContaining('1 quotation'), findsOneWidget);
await db.addQuotation(
quote: 'Another line.',
pageUrl: 'https://example.com/b.html');
await deps.browser.reload();
await tester.pumpAndSettle();
expect(find.textContaining('2 quotations'), findsOneWidget);
});
testWidgets('the page lists kept text with its citation and actions',
(tester) async {
await db.addQuotation(
quote: 'A line worth keeping.',
pageUrl: 'https://example.com/a.html',
pageTitle: 'Alpha',
author: 'A. Author');
await db.addQuotation(
quote: 'Another, plainer line.',
pageUrl: 'https://example.com/b.html');
final deps = await pumpApp(tester);
await deps.browser.open(MewsUrls.quotations);
await tester.pumpAndSettle();
expect(find.textContaining('A line worth keeping.'), findsOneWidget);
// The author the page named stands behind its words; a page that gave
// neither author nor title stands behind its address.
expect(find.textContaining('A. Author'), findsOneWidget);
expect(find.textContaining('example.com'), findsWidgets);
expect(find.textContaining('2 quotations'), findsOneWidget);
// The strip and the title both carry the page.
expect(find.text('Quotations'), findsWidgets);
// Each row carries its actions, in the paragraph of its row.
expect(find.textContaining('Share'), findsNWidgets(2));
expect(find.textContaining('Remove quotation'), findsNWidgets(2));
});
testWidgets('a search narrows the page and says so when nothing matches',
(tester) async {
await db.addQuotation(
quote: 'A line worth keeping.',
pageUrl: 'https://example.com/a.html',
author: 'A. Author');
await db.addQuotation(
quote: 'Another, plainer line.',
pageUrl: 'https://example.com/b.html');
final deps = await pumpApp(tester);
await deps.browser.open('mews:quotations?q=plain');
await tester.pumpAndSettle();
expect(find.textContaining('plainer line'), findsOneWidget);
expect(find.textContaining('A line worth keeping.'), findsNothing);
// The search matches the author and the address as well.
await deps.browser.open('mews:quotations?q=Author');
await tester.pumpAndSettle();
expect(find.textContaining('A line worth keeping.'), findsOneWidget);
await deps.browser.open('mews:quotations?q=nowhere');
await tester.pumpAndSettle();
expect(find.textContaining('No quotations match your search'),
findsOneWidget);
});
testWidgets('sorting by page reorders the rows', (tester) async {
await db.addQuotation(
quote: 'A line worth keeping.',
pageUrl: 'https://example.com/zebra.html',
pageTitle: 'Zebra');
await db.addQuotation(
quote: 'Another, plainer line.',
pageUrl: 'https://example.com/ant.html',
pageTitle: 'Ant');
final deps = await pumpApp(tester);
await deps.browser.open('mews:quotations?sort=page');
await tester.pumpAndSettle();
// Ordered by their page, Ant comes before Zebra.
final rows = [
for (final text in ['Ant', 'Zebra'])
tester.getTopLeft(find.textContaining(text).first).dy,
];
expect(rows.first, lessThan(rows.last));
});
testWidgets('a quotation can be removed from its row', (tester) async {
await db.addQuotation(
quote: 'A line worth keeping.',
pageUrl: 'https://example.com/a.html');
final deps = await pumpApp(tester);
await deps.browser.open(MewsUrls.quotations);
await tester.pumpAndSettle();
expect(find.textContaining('A line worth keeping.'), findsOneWidget);
await tapRun(tester, 'Remove quotation');
expect(find.textContaining('A line worth keeping.'), findsNothing);
expect(find.textContaining('No quotations yet'), findsOneWidget);
});
testWidgets('sharing carries the quotation and its citation', (tester) async {
await db.addQuotation(
quote: 'A line worth keeping.',
pageUrl: 'https://example.com/a.html',
pageTitle: 'Alpha',
author: 'A. Author');
final deps = await pumpApp(tester);
await deps.browser.open(MewsUrls.quotations);
await tester.pumpAndSettle();
await tapRun(tester, 'Share');
expect(shared, hasLength(1));
expect(shared.single, contains('A line worth keeping.'));
// APA-shaped: the author, the no-date marker, the title, the site and
// the address.
expect(shared.single,
contains('A. Author (n.d.). Alpha. example.com. https://example.com/a.html.'));
// A page the reader's subscriptions know by name: the citation names
// the site's own title after the article, instead of the host.
await db.addSubscription(
'https://example.com/index.html', 'Signal Bars', null);
await db.addEntries('https://example.com/index.html', [
MewsEntry(
url: 'https://example.com/retro.html',
date: DateTime(2026, 10, 8),
title: 'Collecting retro phones'),
]);
await db.addQuotation(
quote: 'A retro line.',
pageUrl: 'https://example.com/retro.html',
pageTitle: 'Collecting retro phones');
await deps.browser.open(MewsUrls.hub);
await deps.browser.open(MewsUrls.quotations);
await tester.pumpAndSettle();
// The row names the site the text came from, not just the article.
expect(find.textContaining('Signal Bars'), findsOneWidget);
await tapRun(tester, 'Share', within: 'A retro line.');
expect(shared.last, contains('Collecting retro phones (n.d.). Signal Bars. https://example.com/retro.html.'));
// A page without an author leads with its title.
await db.addQuotation(
quote: 'A line without an author.',
pageUrl: 'https://example.com/b.html',
pageTitle: 'Bravo');
await deps.browser.open(MewsUrls.hub);
await deps.browser.open(MewsUrls.quotations);
await tester.pumpAndSettle();
await tapRun(tester, 'Share', within: 'A line without an author.');
expect(shared.last,
contains('Bravo (n.d.). example.com. https://example.com/b.html.'));
});
}