import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:worldhopper/models/opds_entry.dart'; import 'package:worldhopper/models/opds_feed.dart'; import 'package:worldhopper/providers/opds_provider.dart'; import 'package:worldhopper/providers/series_navigation_provider.dart'; import 'package:worldhopper/repositories/opds_repository.dart'; /// Fake repository that returns a canned feed for any request. class FakeOPDSRepository extends OPDSRepository { final OPDSFeed fakeFeed; FakeOPDSRepository(this.fakeFeed); @override Future fetchFeed( String serverId, String url, { bool forceRefresh = false, CancelToken? cancelToken, }) async { return fakeFeed; } } OPDSEntry _entry(String id, String title) => OPDSEntry(id: id, title: title); OPDSFeed _feedWith(List entries) => OPDSFeed(id: 'feed-1', title: 'Series', entries: entries); void main() { group('nextInSeriesProvider', () { test('returns next entry when current is in the middle', () async { final entries = [ _entry('ch1', 'Chapter 1'), _entry('ch2', 'Chapter 2'), _entry('ch3', 'Chapter 3'), ]; final container = ProviderContainer( overrides: [ opdsRepositoryProvider .overrideWithValue(FakeOPDSRepository(_feedWith(entries))), ], ); addTearDown(container.dispose); final result = await container.read( nextInSeriesProvider(const NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch1', )).future, ); expect(result, isNotNull); expect(result!.id, 'ch2'); expect(result.title, 'Chapter 2'); }); test('returns next entry when current is second-to-last', () async { final entries = [ _entry('ch1', 'Chapter 1'), _entry('ch2', 'Chapter 2'), _entry('ch3', 'Chapter 3'), ]; final container = ProviderContainer( overrides: [ opdsRepositoryProvider .overrideWithValue(FakeOPDSRepository(_feedWith(entries))), ], ); addTearDown(container.dispose); final result = await container.read( nextInSeriesProvider(const NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch2', )).future, ); expect(result, isNotNull); expect(result!.id, 'ch3'); }); test('returns null when current entry is the last', () async { final entries = [ _entry('ch1', 'Chapter 1'), _entry('ch2', 'Chapter 2'), _entry('ch3', 'Chapter 3'), ]; final container = ProviderContainer( overrides: [ opdsRepositoryProvider .overrideWithValue(FakeOPDSRepository(_feedWith(entries))), ], ); addTearDown(container.dispose); final result = await container.read( nextInSeriesProvider(const NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch3', )).future, ); expect(result, isNull); }); test('returns null when current entry is not found in feed', () async { final entries = [ _entry('ch1', 'Chapter 1'), _entry('ch2', 'Chapter 2'), ]; final container = ProviderContainer( overrides: [ opdsRepositoryProvider .overrideWithValue(FakeOPDSRepository(_feedWith(entries))), ], ); addTearDown(container.dispose); final result = await container.read( nextInSeriesProvider(const NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'unknown-id', )).future, ); expect(result, isNull); }); test('returns null for an empty feed', () async { final container = ProviderContainer( overrides: [ opdsRepositoryProvider .overrideWithValue(FakeOPDSRepository(_feedWith([]))), ], ); addTearDown(container.dispose); final result = await container.read( nextInSeriesProvider(const NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch1', )).future, ); expect(result, isNull); }); test('returns null for single-entry feed', () async { final entries = [_entry('ch1', 'Only Chapter')]; final container = ProviderContainer( overrides: [ opdsRepositoryProvider .overrideWithValue(FakeOPDSRepository(_feedWith(entries))), ], ); addTearDown(container.dispose); final result = await container.read( nextInSeriesProvider(const NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch1', )).future, ); expect(result, isNull); }); }); group('NextInSeriesRequest equality', () { test('equal requests have same hashCode', () { const a = NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch1', ); const b = NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch1', ); expect(a, equals(b)); expect(a.hashCode, equals(b.hashCode)); }); test('different requests are not equal', () { const a = NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch1', ); const b = NextInSeriesRequest( serverId: 's1', feedUrl: '/feed', currentEntryId: 'ch2', ); expect(a, isNot(equals(b))); }); }); }