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 = []; 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 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 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( 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.')); }); }