test: remove redundant coverage and shorten timers
This commit is contained in:
@@ -114,16 +114,14 @@ void main() {
|
||||
return cached!['UserData'] as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
test('mutates every per-user row for the same item', () async {
|
||||
// Jellyfin caches one row per userId — both must flip, otherwise a
|
||||
// profile switch surfaces the other user's stale row (audit D-cluster).
|
||||
test('skips ambiguous bare scope when multiple users cache the same item', () async {
|
||||
await JellyfinApiCache.instance.put(serverId, '/Users/user-a/Items/item-1', dto());
|
||||
await JellyfinApiCache.instance.put(serverId, '/Users/user-b/Items/item-1', dto());
|
||||
|
||||
await JellyfinApiCache.instance.applyWatchState(serverId: serverId, itemId: 'item-1', isWatched: true);
|
||||
|
||||
expect((await readBack('user-a'))['Played'], isTrue);
|
||||
expect((await readBack('user-b'))['Played'], isTrue);
|
||||
expect((await readBack('user-a'))['Played'], isFalse);
|
||||
expect((await readBack('user-b'))['Played'], isFalse);
|
||||
});
|
||||
|
||||
test('converts viewOffsetMs to 100-ns ticks', () async {
|
||||
|
||||
@@ -33,22 +33,16 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Singleton + reset
|
||||
// ============================================================
|
||||
|
||||
group('singleton', () {
|
||||
test('instance returns same object across calls', () {
|
||||
final a = DownloadStorageService.instance;
|
||||
final b = DownloadStorageService.instance;
|
||||
expect(identical(a, b), isTrue);
|
||||
});
|
||||
|
||||
test('resetForTesting yields a fresh instance', () {
|
||||
group('singleton lifecycle', () {
|
||||
test('reacquiring the instance preserves initialized state', () async {
|
||||
final settings = await SettingsService.getInstance();
|
||||
final first = DownloadStorageService.instance;
|
||||
DownloadStorageService.resetForTesting();
|
||||
await first.initialize(settings);
|
||||
|
||||
final second = DownloadStorageService.instance;
|
||||
expect(identical(first, second), isFalse);
|
||||
expect(identical(first, second), isTrue);
|
||||
expect(second.artworkDirectoryPath, isNotNull);
|
||||
expect(second.artworkDirectoryPath, first.artworkDirectoryPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,15 +269,6 @@ void main() {
|
||||
expect(await dss.toRelativePath(uri), uri);
|
||||
});
|
||||
|
||||
test('returns the input unchanged when not under the base dir', () async {
|
||||
final settings = await SettingsService.getInstance();
|
||||
final dss = DownloadStorageService.instance;
|
||||
await dss.initialize(settings);
|
||||
|
||||
const foreign = '/some/other/place/file.mkv';
|
||||
expect(await dss.toRelativePath(foreign), foreign);
|
||||
});
|
||||
|
||||
test('toAbsolutePath joins relative paths against the base dir', () async {
|
||||
final settings = await SettingsService.getInstance();
|
||||
final dss = DownloadStorageService.instance;
|
||||
|
||||
@@ -104,44 +104,6 @@ class _ProbeWidgetState extends State<_ProbeWidget> {
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// ===========================================================
|
||||
// AdjacentEpisodes data class
|
||||
// ===========================================================
|
||||
|
||||
group('AdjacentEpisodes', () {
|
||||
test('default constructor reports no neighbours', () {
|
||||
final ae = AdjacentEpisodes();
|
||||
expect(ae.next, isNull);
|
||||
expect(ae.previous, isNull);
|
||||
expect(ae.hasNext, isFalse);
|
||||
expect(ae.hasPrevious, isFalse);
|
||||
});
|
||||
|
||||
test('next/previous flags reflect non-null fields', () {
|
||||
final ae = AdjacentEpisodes(next: _meta('n'), previous: _meta('p'));
|
||||
expect(ae.hasNext, isTrue);
|
||||
expect(ae.hasPrevious, isTrue);
|
||||
expect(ae.next!.id, 'n');
|
||||
expect(ae.previous!.id, 'p');
|
||||
});
|
||||
|
||||
test('only-next variant', () {
|
||||
final ae = AdjacentEpisodes(next: _meta('n'));
|
||||
expect(ae.hasNext, isTrue);
|
||||
expect(ae.hasPrevious, isFalse);
|
||||
});
|
||||
|
||||
test('only-previous variant', () {
|
||||
final ae = AdjacentEpisodes(previous: _meta('p'));
|
||||
expect(ae.hasNext, isFalse);
|
||||
expect(ae.hasPrevious, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================
|
||||
// loadAdjacentEpisodes: short-circuit without an active queue
|
||||
// ===========================================================
|
||||
|
||||
group('loadAdjacentEpisodes', () {
|
||||
testWidgets('returns empty AdjacentEpisodes when no play queue is active', (tester) async {
|
||||
// Bare provider — no setPlaybackFromPlayQueue() call → isQueueActive = false.
|
||||
|
||||
@@ -147,34 +147,4 @@ void main() {
|
||||
expect(out.audioTracks, hasLength(1));
|
||||
});
|
||||
});
|
||||
|
||||
group('cross-backend equivalence', () {
|
||||
test('both readers produce parallel track structures from analogous JSON', () {
|
||||
const plexReader = PlexFileInfoStreamReader();
|
||||
const jfReader = JellyfinFileInfoStreamReader();
|
||||
|
||||
final plexStreams = [
|
||||
{'streamType': 1, 'id': 1, 'frameRate': 24.0},
|
||||
{'streamType': 2, 'id': 2, 'codec': 'aac', 'language': 'English', 'channels': 2, 'selected': true},
|
||||
{'streamType': 3, 'id': 3, 'codec': 'srt', 'language': 'English', 'selected': false, 'forced': false},
|
||||
];
|
||||
final jfStreams = [
|
||||
{'Type': 'Video', 'Index': 0, 'RealFrameRate': 24.0},
|
||||
{'Type': 'Audio', 'Index': 1, 'Codec': 'aac', 'Language': 'eng', 'Channels': 2, 'IsDefault': true},
|
||||
{'Type': 'Subtitle', 'Index': 2, 'Codec': 'srt', 'Language': 'eng', 'IsDefault': false, 'IsForced': false},
|
||||
];
|
||||
|
||||
final plex = walkStreams(plexStreams, plexReader);
|
||||
final jf = walkStreams(jfStreams, jfReader);
|
||||
|
||||
expect(plex.audioTracks, hasLength(1));
|
||||
expect(jf.audioTracks, hasLength(1));
|
||||
expect(plex.subtitleTracks, hasLength(1));
|
||||
expect(jf.subtitleTracks, hasLength(1));
|
||||
expect(plex.videoStream?['frameRate'], jf.videoStream?['RealFrameRate']);
|
||||
expect(plex.audioTracks.first.codec, jf.audioTracks.first.codec);
|
||||
expect(plex.audioTracks.first.channels, jf.audioTracks.first.channels);
|
||||
expect(plex.audioTracks.first.selected, jf.audioTracks.first.selected);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import '../test_helpers/media_items.dart';
|
||||
// - then calls [navigateToVideoPlayer] (Navigator + DownloadProvider +
|
||||
// SettingsService singleton + Provider).
|
||||
//
|
||||
// Without re-implementing that entire dependency tree, the only meaningful
|
||||
// Without re-implementing that entire dependency tree, the meaningful
|
||||
// unit-testable surface is:
|
||||
// - The `PlayQueueResult` sealed hierarchy (constructor + identity).
|
||||
// - `PlayQueueError` preserves the underlying failure.
|
||||
// - `launchShuffledShow` short-circuits BEFORE any network call when the
|
||||
// metadata is not a show or season — that's a pure pre-flight branch.
|
||||
// - `launchFromCollectionOrPlaylist` short-circuits when the input is
|
||||
@@ -39,20 +39,6 @@ void main() {
|
||||
// ============================================================
|
||||
|
||||
group('PlayQueueResult', () {
|
||||
test('PlayQueueSuccess is a const, identity-comparable singleton', () {
|
||||
const a = PlayQueueSuccess();
|
||||
const b = PlayQueueSuccess();
|
||||
expect(identical(a, b), isTrue);
|
||||
expect(a, isA<PlayQueueResult>());
|
||||
});
|
||||
|
||||
test('PlayQueueEmpty is a const, identity-comparable singleton', () {
|
||||
const a = PlayQueueEmpty();
|
||||
const b = PlayQueueEmpty();
|
||||
expect(identical(a, b), isTrue);
|
||||
expect(a, isA<PlayQueueResult>());
|
||||
});
|
||||
|
||||
test('PlayQueueError carries the wrapped error', () {
|
||||
final error = StateError('boom');
|
||||
final result = PlayQueueError(error);
|
||||
@@ -114,35 +100,4 @@ void main() {
|
||||
expect(error.toString(), contains('collection or playlist'));
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Constructor
|
||||
// ============================================================
|
||||
|
||||
group('constructor', () {
|
||||
testWidgets('stores all wired arguments', (tester) async {
|
||||
late BuildContext capturedContext;
|
||||
await tester.pumpWidget(
|
||||
Builder(
|
||||
builder: (context) {
|
||||
capturedContext = context;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final client = _StubPlexClient();
|
||||
final launcher = PlexPlayQueueLauncher(
|
||||
context: capturedContext,
|
||||
client: client,
|
||||
serverId: 'srv-A',
|
||||
serverName: 'Plex',
|
||||
);
|
||||
|
||||
expect(launcher.context, capturedContext);
|
||||
expect(identical(launcher.client, client), isTrue);
|
||||
expect(launcher.serverId, 'srv-A');
|
||||
expect(launcher.serverName, 'Plex');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -322,18 +322,6 @@ void main() {
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('valid online construction succeeds', () {
|
||||
final tracker = PlaybackProgressTracker(
|
||||
client: _FakePlexClient(),
|
||||
metadata: _meta(),
|
||||
player: _FakePlayer(),
|
||||
isOffline: false,
|
||||
);
|
||||
addTearDown(tracker.dispose);
|
||||
// No assertion — the constructor returned cleanly.
|
||||
expect(tracker, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -97,28 +97,4 @@ void main() {
|
||||
expect(session.mediaSourceId, 'downloaded');
|
||||
});
|
||||
});
|
||||
|
||||
test('forwarding getters mirror the resolver output', () {
|
||||
final result = PlaybackInitializationResult(
|
||||
availableVersions: [MediaVersion(id: 'v0')],
|
||||
videoUrl: 'u',
|
||||
isTranscoding: true,
|
||||
playSessionId: 'psid',
|
||||
playMethod: 'Transcode',
|
||||
activeAudioStreamId: 7,
|
||||
);
|
||||
final session = PlaybackSession.fromContext(
|
||||
_context(result),
|
||||
requestedQualityPreset: TranscodeQualityPreset.original,
|
||||
);
|
||||
|
||||
expect(session.isTranscoding, isTrue);
|
||||
expect(session.isOffline, isFalse);
|
||||
expect(session.playSessionId, 'psid');
|
||||
expect(session.playMethod, 'Transcode');
|
||||
expect(session.audioStreamId, 7);
|
||||
expect(session.availableVersions, hasLength(1));
|
||||
expect(session.streamHeaders, containsPair('X-Test', 'token'));
|
||||
expect(session.metadata.id, 'item-1');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,10 +55,6 @@ void main() {
|
||||
await newDb.close();
|
||||
});
|
||||
|
||||
test('database getter exposes the underlying AppDatabase', () {
|
||||
expect(identical(cache.database, db), isTrue);
|
||||
});
|
||||
|
||||
test('registered cleanup ignores backend initialization order and preserves pinned rows', () async {
|
||||
await cache.put(ServerId('srv'), '/volatile', {'value': 1});
|
||||
await cache.put(ServerId('srv'), '/pinned', {'value': 2});
|
||||
|
||||
@@ -274,40 +274,20 @@ void main() {
|
||||
return client;
|
||||
}
|
||||
|
||||
test('trending drops person results and keeps native mediaType', () async {
|
||||
test('popular movies coerces missing mediaType to movie', () async {
|
||||
final client = clientWith(
|
||||
MockClient(
|
||||
(request) async => _json({
|
||||
'page': 1,
|
||||
'totalPages': 2,
|
||||
'results': [
|
||||
{'id': 1, 'mediaType': 'movie', 'title': 'Blade Runner', 'releaseDate': '1982-06-25'},
|
||||
{'id': 2, 'mediaType': 'person', 'name': 'Harrison Ford'},
|
||||
{'id': 3, 'mediaType': 'tv', 'name': 'Severance', 'firstAirDate': '2022-02-18'},
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
final page = await client.getTrending();
|
||||
expect(page.items.map((m) => m.displayTitle), ['Blade Runner', 'Severance']);
|
||||
expect(page.items.first.isMovie, isTrue);
|
||||
expect(page.items.last.isMovie, isFalse);
|
||||
expect(page.items.first.year, 1982);
|
||||
expect(page.hasMore, isTrue);
|
||||
});
|
||||
|
||||
test('single-type discover endpoints coerce the missing mediaType', () async {
|
||||
final client = clientWith(
|
||||
MockClient(
|
||||
(request) async => _json({
|
||||
MockClient((request) async {
|
||||
expect(request.url.path, '/api/v1/discover/movies');
|
||||
return _json({
|
||||
'page': 1,
|
||||
'totalPages': 1,
|
||||
'results': [
|
||||
{'id': 4, 'title': 'Dune', 'releaseDate': '2021-09-15'},
|
||||
],
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
final page = await client.getPopularMovies();
|
||||
expect(page.items.single.isMovie, isTrue);
|
||||
expect(page.hasMore, isFalse);
|
||||
@@ -359,24 +339,16 @@ void main() {
|
||||
});
|
||||
|
||||
group('SeerrPage', () {
|
||||
test('parses both the TMDB and the pageInfo pagination shapes', () {
|
||||
final tmdbShape = SeerrPage<int>.fromJson({
|
||||
'page': 1,
|
||||
'totalPages': 3,
|
||||
'results': [
|
||||
{'id': 1},
|
||||
],
|
||||
}, (item) => item['id'] as int);
|
||||
expect(tmdbShape.hasMore, isTrue);
|
||||
|
||||
final pageInfoShape = SeerrPage<int>.fromJson({
|
||||
test('parses the pageInfo pagination shape', () {
|
||||
final page = SeerrPage<int>.fromJson({
|
||||
'pageInfo': {'page': 2, 'pages': 2},
|
||||
'results': [
|
||||
{'id': 1},
|
||||
],
|
||||
}, (item) => item['id'] as int);
|
||||
expect(pageInfoShape.hasMore, isFalse);
|
||||
expect(pageInfoShape.items, [1]);
|
||||
|
||||
expect(page.hasMore, isFalse);
|
||||
expect(page.items, [1]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ import '../test_helpers/media_items.dart';
|
||||
// initialized SettingsService.
|
||||
//
|
||||
// Coverage:
|
||||
// - Constructor wiring (mutable fields are settable, default values).
|
||||
// - `cacheExternalSubtitles` / `lastExternalSubtitles` round-trip.
|
||||
// - `cacheExternalSubtitles` / `lastExternalSubtitles` replacement behavior.
|
||||
// - `addExternalSubtitles` invokes the player's addSubtitleTrack for each
|
||||
// entry with a non-null URI, preserves order, and silently swallows errors
|
||||
// thrown by the player.
|
||||
@@ -28,8 +27,6 @@ import '../test_helpers/media_items.dart';
|
||||
// fewer than 2 real tracks (early-return paths).
|
||||
// - `applyTrackSelectionWhenReady` waits for subtitle tracks when server
|
||||
// metadata says they exist.
|
||||
// - `onPlaybackRestart` is a no-op when not waiting for external subs.
|
||||
// - `onSecondarySubtitleTrackChanged` is a documented no-op.
|
||||
// - `dispose` is idempotent (timers/subscriptions cleared).
|
||||
//
|
||||
// What's NOT covered:
|
||||
@@ -150,49 +147,6 @@ void main() {
|
||||
// could leak across tests — reset to be safe.
|
||||
setUp(resetSharedPreferencesForTest);
|
||||
|
||||
// ============================================================
|
||||
// Construction
|
||||
// ============================================================
|
||||
|
||||
group('constructor', () {
|
||||
test('initialises mutable fields with the provided values', () {
|
||||
final player = _FakePlayer();
|
||||
final mgr = TrackManager(
|
||||
player: player,
|
||||
isActive: () => true,
|
||||
persistTrackPreference: _noopPersister,
|
||||
getProfileSettings: () => null,
|
||||
waitForProfileSettings: () async {},
|
||||
metadata: _meta(),
|
||||
preferredAudioTrack: const AudioTrack(id: 'a-1', language: 'eng'),
|
||||
preferredSubtitleTrack: const SubtitleTrack(id: 's-1', language: 'eng'),
|
||||
preferredSecondarySubtitleTrack: const SubtitleTrack(id: 's-2', language: 'fre'),
|
||||
);
|
||||
addTearDown(mgr.dispose);
|
||||
|
||||
expect(mgr.preferredAudioTrack?.id, 'a-1');
|
||||
expect(mgr.preferredSubtitleTrack?.id, 's-1');
|
||||
expect(mgr.preferredSecondarySubtitleTrack?.id, 's-2');
|
||||
expect(mgr.metadata.id, 'rk1');
|
||||
expect(mgr.waitingForExternalSubsTrackSelection, isFalse);
|
||||
expect(mgr.lastExternalSubtitles, isEmpty);
|
||||
expect(mgr.mediaInfo, isNull);
|
||||
});
|
||||
|
||||
test('mutable fields can be reassigned (episode-navigation pattern)', () {
|
||||
final mgr = _make(player: _FakePlayer());
|
||||
addTearDown(mgr.dispose);
|
||||
|
||||
mgr.metadata = _meta(id: 'next');
|
||||
mgr.preferredAudioTrack = const AudioTrack(id: 'a2', language: 'fre');
|
||||
mgr.waitingForExternalSubsTrackSelection = true;
|
||||
|
||||
expect(mgr.metadata.id, 'next');
|
||||
expect(mgr.preferredAudioTrack?.id, 'a2');
|
||||
expect(mgr.waitingForExternalSubsTrackSelection, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// External subtitle cache
|
||||
// ============================================================
|
||||
@@ -506,15 +460,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('onSecondarySubtitleTrackChanged', () {
|
||||
test('is a documented no-op', () {
|
||||
final mgr = _make(player: _FakePlayer());
|
||||
addTearDown(mgr.dispose);
|
||||
// Just verify it returns normally; nothing else to assert.
|
||||
expect(() => mgr.onSecondarySubtitleTrackChanged(const SubtitleTrack(id: '1')), returnsNormally);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// onSubtitleTrackChanged — same-language stream mapping (#1443)
|
||||
// ============================================================
|
||||
|
||||
@@ -19,13 +19,6 @@ void main() {
|
||||
});
|
||||
|
||||
group('tracker session json codec', () {
|
||||
test('round-trips through provided factory', () {
|
||||
final encoded = encodeTrackerSessionJson({'access_token': 'abc', 'created_at': 123});
|
||||
final decoded = decodeTrackerSessionJson(encoded, (json) => json);
|
||||
|
||||
expect(decoded, {'access_token': 'abc', 'created_at': 123});
|
||||
});
|
||||
|
||||
test('round-trips Trakt sessions with snake-case keys and default scope', () {
|
||||
const session = TrackerSession(
|
||||
accessToken: 'trakt-at',
|
||||
@@ -59,17 +52,6 @@ void main() {
|
||||
expect(decoded.createdAt, 1000);
|
||||
});
|
||||
|
||||
test('round-trips AniList sessions through shared encode mixin', () {
|
||||
const session = TrackerSession(accessToken: 'anilist-at', expiresAt: 2000, username: 'alice', createdAt: 1000);
|
||||
|
||||
final decoded = TrackerSession.decode(session.encode());
|
||||
|
||||
expect(decoded.accessToken, 'anilist-at');
|
||||
expect(decoded.expiresAt, 2000);
|
||||
expect(decoded.username, 'alice');
|
||||
expect(decoded.createdAt, 1000);
|
||||
});
|
||||
|
||||
test('round-trips MAL sessions through shared encode mixin', () {
|
||||
const session = TrackerSession(
|
||||
accessToken: 'mal-at',
|
||||
@@ -88,16 +70,6 @@ void main() {
|
||||
expect(decoded.createdAt, 1000);
|
||||
});
|
||||
|
||||
test('round-trips Simkl sessions through shared encode mixin', () {
|
||||
const session = TrackerSession(accessToken: 'simkl-at', username: 'carol', createdAt: 1000);
|
||||
|
||||
final decoded = TrackerSession.decode(session.encode());
|
||||
|
||||
expect(decoded.accessToken, 'simkl-at');
|
||||
expect(decoded.username, 'carol');
|
||||
expect(decoded.createdAt, 1000);
|
||||
});
|
||||
|
||||
test('builds Trakt token sessions with default scope', () {
|
||||
final session = TrackerSession.fromTokenResponse(TrackerService.trakt, {
|
||||
'access_token': 'trakt-at',
|
||||
|
||||
Reference in New Issue
Block a user