feat: search, filter and act on items from the library pages

This commit is contained in:
randogoth 2026-10-11 13:25:57 +03:00
parent 1b426ccd8b
commit 5cd053c8a9
11 changed files with 401 additions and 43 deletions

View file

@ -422,9 +422,9 @@ void main() {
await tester.pumpAndSettle();
expect(stubDeps.browser.currentPage.value?.url, 'mews:history');
// A history link navigates like any other link.
await tester.tap(find.textContaining('Page A'));
await tester.pumpAndSettle();
// A history link navigates like any other link. Rows carry their own
// actions now, so tap the title itself rather than the row's middle.
await tapRun(tester, 'Page A');
expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/a.html');
// Bookmarks and subscriptions get the same treatment.
@ -484,6 +484,102 @@ void main() {
debugDefaultTargetPlatformOverride = null;
});
testWidgets('bookmarks can be searched and removed from their own page',
(tester) async {
await pumpApp(tester);
await deps.bookmarks.load();
await deps.bookmarks.toggle('https://a.example/retro.html', 'Retro phones');
await deps.bookmarks.toggle('https://b.example/signal.html', 'Signal bars');
await deps.browser.open(MewsUrls.bookmarks);
await tester.pumpAndSettle();
expect(find.textContaining('Retro phones'), findsOneWidget);
expect(find.textContaining('Signal bars'), findsOneWidget);
// Searching narrows the list and the choice lands in the address.
await tester.enterText(find.byType(TextFormField), 'retro');
await tester.tap(find.widgetWithText(FilledButton, 'Search'));
await tester.pumpAndSettle();
expect(deps.browser.currentPage.value?.url, 'mews:bookmarks?q=retro');
expect(find.textContaining('Retro phones'), findsOneWidget);
expect(find.textContaining('Signal bars'), findsNothing);
// A search with no matches says so rather than looking empty.
await deps.browser.open('mews:bookmarks?q=nothinghere');
await tester.pumpAndSettle();
expect(find.textContaining('No bookmarks match'), findsOneWidget);
// Clearing the search brings the whole list back.
await deps.browser.open('mews:bookmarks?q=retro');
await tester.pumpAndSettle();
await tapRun(tester, 'Clear search');
expect(deps.browser.currentPage.value?.url, MewsUrls.bookmarks);
expect(find.textContaining('Signal bars'), findsOneWidget);
// Removing one stays on the page and adds no walk history entry. What the
// reader has typed survives, because the renderer matches form state by
// position and the search box keeps its place in the page.
await tester.enterText(find.byType(TextFormField), 'half typed');
final stepsBack = deps.browser.canGoBack;
await tapRun(tester, 'Remove bookmark');
expect(find.widgetWithText(TextFormField, 'half typed'), findsOneWidget);
expect(deps.browser.currentPage.value?.url, MewsUrls.bookmarks);
expect(deps.browser.canGoBack, stepsBack);
expect(find.textContaining('Retro phones'), findsNothing);
expect(find.textContaining('Signal bars'), findsOneWidget);
expect(deps.bookmarks.isBookmarked('https://a.example/retro.html'), isFalse);
});
testWidgets('clearing history takes a second step', (tester) async {
await pumpApp(tester);
await deps.db.recordVisit('https://a.example/one.html', 'Page one');
await deps.db.recordVisit('https://a.example/one.html', 'Page one');
await deps.db.recordVisit('https://b.example/two.html', 'Page two');
await deps.browser.open(MewsUrls.history);
await tester.pumpAndSettle();
// A page visited twice is listed once.
expect(find.textContaining('Page one'), findsOneWidget);
expect(find.textContaining('Page two'), findsOneWidget);
// Backing out of the confirmation leaves history alone.
await tapRun(tester, 'Clear history');
expect(deps.browser.currentPage.value?.url, 'mews:history?confirm=clear');
expect(find.textContaining("can't undo"), findsOneWidget);
await tapRun(tester, 'Keep my history');
expect(deps.browser.currentPage.value?.url, MewsUrls.history);
expect(find.textContaining('Page one'), findsOneWidget);
// Removing one page leaves the other. The most recent visit heads the
// list, so the first row is page two.
await tapRun(tester, 'Remove from history');
await tester.pumpAndSettle();
expect(find.textContaining('Page two'), findsNothing);
expect(find.textContaining('Page one'), findsOneWidget);
// Confirming clears the rest.
await tapRun(tester, 'Clear history');
await tapRun(tester, 'Clear history now');
expect(find.textContaining('Pages you visit appear here'), findsOneWidget);
});
testWidgets('a site can be left from the subscriptions page', (tester) async {
await pumpApp(tester);
await deps.subscriptions.load();
await deps.subscriptions
.toggle(parseSource(pageSource, 'https://example.com/index.html'));
await deps.browser.open(MewsUrls.subscriptions);
await tester.pumpAndSettle();
expect(find.textContaining('Signal Bars'), findsOneWidget);
await tapRun(tester, 'Stop subscribing');
expect(deps.browser.currentPage.value?.url, MewsUrls.subscriptions);
expect(deps.subscriptions.isSubscribed('https://example.com/index.html'),
isFalse);
expect(find.textContaining('Sites you subscribe to'), findsOneWidget);
});
testWidgets('the posts page lists, orders and filters posts',
(tester) async {
const siteA = 'https://a.example/';

View file

@ -93,6 +93,45 @@ void main() {
expect(await db.countHistory(), 2);
});
test('a search matches titles and addresses literally', () async {
await db.addBookmark('https://a.example/retro.html', 'Collecting retro phones');
await db.addBookmark('https://b.example/signal.html', 'Signal bars');
await db.addBookmark('https://c.example/100.html', '100% signal');
expect((await db.bookmarksByDate(query: 'retro')).map((b) => b.title),
['Collecting retro phones']);
// The address is searched too.
expect((await db.bookmarksByDate(query: 'b.example')).map((b) => b.title),
['Signal bars']);
// A blank search is not a filter.
expect(await db.bookmarksByDate(query: ' '), hasLength(3));
// The reader types text, not a pattern: LIKE's wildcards match literally.
expect(await db.bookmarksByDate(query: '%'), hasLength(1));
expect((await db.bookmarksByDate(query: '100%')).map((b) => b.title),
['100% signal']);
expect(await db.bookmarksByDate(query: '_'), isEmpty);
});
test('history lists one row per address, newest visit first', () async {
await db.recordVisit('https://a.example/one.html', 'First title');
await db.recordVisit('https://b.example/two.html', 'Page two');
await db.recordVisit('https://a.example/one.html', 'Newer title');
final pages = await db.recentPages();
expect(pages.map((p) => p.url),
['https://a.example/one.html', 'https://b.example/two.html']);
// The row carries the title of the latest visit.
expect(pages.first.title, 'Newer title');
await db.forgetPage('https://a.example/one.html');
expect((await db.recentPages()).map((p) => p.url),
['https://b.example/two.html']);
await db.clearHistory();
expect(await db.recentPages(), isEmpty);
});
test('an older database is recreated rather than migrated', () async {
final dir = Directory.systemTemp.createTempSync('mews-schema');
addTearDown(() => dir.deleteSync(recursive: true));