From e6e7d8cdfdfd68c0084f2de42faf93acce8892c5 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:34:12 +0200 Subject: [PATCH] test: remove redundant coverage and shorten timers --- .../exoplayer/DvBitstreamSanitizerTest.kt | 21 -- .../exoplayer/LatmMatroskaExtractorTest.kt | 11 - .../media/widget/SpecRenderEngineTest.kt | 3 +- ios/RunnerTests/RunnerTests.swift | 12 - macos/RunnerTests/RunnerTests.swift | 12 - server/main_test.go | 20 -- server/oauth_test.go | 9 - test/database/app_database_test.dart | 75 +------ test/focus/focusable_wrapper_test.dart | 6 - test/focus/key_event_utils_test.dart | 63 ------ test/media/media_sort_test.dart | 5 - test/mixins/context_menu_tap_mixin_test.dart | 42 +--- ...disposable_change_notifier_mixin_test.dart | 15 -- test/mixins/event_aware_test.dart | 33 --- test/mixins/item_updatable_test.dart | 120 ---------- test/mixins/library_tab_state_test.dart | 45 +--- test/mixins/mounted_set_state_mixin_test.dart | 18 +- test/mixins/paginated_item_loader_test.dart | 32 --- test/mixins/refreshable_test.dart | 209 ------------------ .../mixins/server_bound_media_mixin_test.dart | 24 -- test/mixins/tab_navigation_mixin_test.dart | 21 +- test/mpv/property_observation_test.dart | 6 - .../companion_remote_provider_test.dart | 16 -- test/providers/download_provider_test.dart | 13 -- .../providers/multi_server_provider_test.dart | 7 - .../providers/offline_mode_provider_test.dart | 23 -- test/screens/media_detail_screen_test.dart | 24 -- test/scripts/upload_symbols_test.dart | 3 +- test/services/api_cache_watch_state_test.dart | 8 +- .../download_storage_service_test.dart | 31 +-- .../episode_navigation_service_test.dart | 38 ---- test/services/file_info_parser_test.dart | 30 --- test/services/play_queue_launcher_test.dart | 49 +--- .../playback_progress_tracker_test.dart | 12 - test/services/playback_session_test.dart | 24 -- test/services/plex_api_cache_test.dart | 4 - test/services/seerr/seerr_client_test.dart | 52 +---- test/services/track_manager_test.dart | 57 +---- .../trackers/tracker_session_utils_test.dart | 28 --- test/test_helpers/media_items.dart | 68 ------ test/test_helpers/media_items_test.dart | 44 ---- test/test_helpers/watch_together_fakes.dart | 51 ----- test/utils/active_client_scope_test.dart | 4 - test/utils/base_notifier_test.dart | 15 -- test/utils/codec_utils_test.dart | 15 -- test/utils/external_ids_matching_test.dart | 12 - test/utils/formatters_test.dart | 6 - test/utils/global_key_utils_test.dart | 14 -- test/utils/grid_size_calculator_test.dart | 24 -- test/utils/layout_constants_test.dart | 12 - test/utils/media_image_helper_test.dart | 12 - .../media_server_http_exception_test.dart | 17 -- test/utils/player_utils_test.dart | 22 -- test/utils/rating_utils_test.dart | 5 - test/watch_together/playback_state_test.dart | 4 - test/watch_together/primitives_test.dart | 7 - .../watch_together_controller_test.dart | 10 +- .../watch_together_peer_service_test.dart | 27 ++- .../watch_together_provider_test.dart | 106 +-------- test/widgets/focusable_text_field_test.dart | 67 ------ test/widgets/tv_browse_rail_test.dart | 32 --- test/widgets/video_controls_test.dart | 40 ---- test/widgets/video_settings_sheet_test.dart | 15 -- 63 files changed, 90 insertions(+), 1760 deletions(-) delete mode 100644 test/mixins/item_updatable_test.dart delete mode 100644 test/mixins/refreshable_test.dart delete mode 100644 test/test_helpers/media_items_test.dart diff --git a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/DvBitstreamSanitizerTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/DvBitstreamSanitizerTest.kt index cd4bef74..988cbef5 100644 --- a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/DvBitstreamSanitizerTest.kt +++ b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/DvBitstreamSanitizerTest.kt @@ -26,17 +26,6 @@ class DvBitstreamSanitizerTest { assertEquals(0, buffer.position()) } - @Test - fun stripsSuffixSei() { - val vcl = annexBNal(1, byteArrayOf(0x01)) - val suffixSei = annexBNal(40, hdr10PlusSeiPayload()) - val buffer = bufferOf(vcl, suffixSei) - - sanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) - - assertArrayEquals(vcl, remainingBytes(buffer)) - } - @Test fun handles3ByteStartCodes() { val vcl1 = annexBNal(1, byteArrayOf(0x01, 0x02), startCodeLen = 3) @@ -141,16 +130,6 @@ class DvBitstreamSanitizerTest { assertEquals(0xBB.toByte(), buffer.get(1)) } - @Test - fun worksOnDirectBuffers() { - val vcl = annexBNal(1, byteArrayOf(0x01, 0x02)) - val buffer = directBufferOf(vcl, hdr10PlusSei()) - - sanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) - - assertArrayEquals(vcl, remainingBytes(buffer)) - } - @Test fun emptyBufferIsNoOp() { val buffer = ByteBuffer.allocate(0) diff --git a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/LatmMatroskaExtractorTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/LatmMatroskaExtractorTest.kt index 440595df..a2f55d6f 100644 --- a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/LatmMatroskaExtractorTest.kt +++ b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/LatmMatroskaExtractorTest.kt @@ -15,7 +15,6 @@ import androidx.media3.extractor.mkv.MatroskaExtractor import java.io.EOFException import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Test @@ -190,16 +189,6 @@ class LatmMatroskaExtractorTest { assertFalse(isLoasAcmTrack(null, loas)) } - @Test - fun formatIsEmittedBeforeFirstSample() { - val output = extractFixture() - val track = output.tracks.values.first() - assertNotNull(track.formats.firstOrNull()) - // LatmReader emits the format from the first StreamMuxConfig, which arrives - // with the first LOAS frame — before any sample metadata is committed. - assertTrue(track.samples.isNotEmpty()) - } - @Test fun rejectsUnexpectedEndOfInput() { val output = LatmTrackOutput(FakeTrackOutput(), 1) diff --git a/android/libass/src/test/java/com/edde746/plezy/libass/media/widget/SpecRenderEngineTest.kt b/android/libass/src/test/java/com/edde746/plezy/libass/media/widget/SpecRenderEngineTest.kt index 283cf902..e85a9b69 100644 --- a/android/libass/src/test/java/com/edde746/plezy/libass/media/widget/SpecRenderEngineTest.kt +++ b/android/libass/src/test/java/com/edde746/plezy/libass/media/widget/SpecRenderEngineTest.kt @@ -272,7 +272,7 @@ class SpecRenderEngineTest { // last-rendered slot so GL skips the upload entirely. val h = Harness() h.script.add(changed()) - val first = h.engine.service(0, pinned = true) as SpecRenderEngine.Outcome.Post + h.engine.service(0, pinned = true) var pts = 0L repeat(4) { // build cadence; renders return changed for simplicity @@ -287,7 +287,6 @@ class SpecRenderEngineTest { assertTrue(outcome.specHit) assertFalse(outcome.newContent) assertEquals(before, h.calls.size) - assertNotNull(first) // first slot existed; hit reposts whichever slot was last rendered } @Test diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift index 86a7c3b1..e69de29b 100644 --- a/ios/RunnerTests/RunnerTests.swift +++ b/ios/RunnerTests/RunnerTests.swift @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift index 61f3bd1f..e69de29b 100644 --- a/macos/RunnerTests/RunnerTests.swift +++ b/macos/RunnerTests/RunnerTests.swift @@ -1,12 +0,0 @@ -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/server/main_test.go b/server/main_test.go index c0b195fa..ae2dfdce 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -328,10 +328,6 @@ func newRelayHarnessAt(t *testing.T, logDir, stateFile string) *relayHarness { mux := http.NewServeMux() mux.HandleFunc("/relay", srv.handleWS) - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) - }) mux.HandleFunc("/logs", srv.handlePostLogs) mux.HandleFunc("/logs/", srv.handleGetLogs) mux.HandleFunc("/posters", srv.handlePostPosters) @@ -1639,22 +1635,6 @@ func TestPosterStoreCleanupExpiresOldPosters(t *testing.T) { } } -func TestHealthEndpointReturnsOK(t *testing.T) { - h := newRelayHarness(t) - resp, err := http.Get(h.baseURL + "/health") - if err != nil { - t.Fatalf("get: %v", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("status=%d", resp.StatusCode) - } - body, _ := io.ReadAll(resp.Body) - if string(body) != "ok" { - t.Fatalf("body=%q want ok", body) - } -} - // ====================================================================== // End-to-end: rooms survive a process restart // ====================================================================== diff --git a/server/oauth_test.go b/server/oauth_test.go index 6d19b207..23da5dad 100644 --- a/server/oauth_test.go +++ b/server/oauth_test.go @@ -433,15 +433,6 @@ func TestOAuthCallbackUnknownSessionIgnored(t *testing.T) { } } -func TestOAuthResultUnknownSession(t *testing.T) { - h := newOAuthHarness(t) - resp := httpGet(t, h.base+"/auth/result?session=nope") - defer resp.Body.Close() - if resp.StatusCode != http.StatusGone { - t.Fatalf("status=%d want 410", resp.StatusCode) - } -} - func TestOAuthResultConsumedSecondCallIsGone(t *testing.T) { h := newOAuthHarness(t) sess, _ := h.startSession(t, "mal", "3.3.3.1") diff --git a/test/database/app_database_test.dart b/test/database/app_database_test.dart index d9bccb4b..f07e2556 100644 --- a/test/database/app_database_test.dart +++ b/test/database/app_database_test.dart @@ -40,10 +40,6 @@ class _AppDatabaseTestSuite { // ============================================================ group('schema', () { - test('schemaVersion is 16', () { - expect(db.schemaVersion, 16); - }); - test('all tables are accessible and start empty', () async { expect(await db.select(db.downloadedMedia).get(), isEmpty); expect(await db.select(db.downloadOwners).get(), isEmpty); @@ -255,22 +251,10 @@ class _AppDatabaseTestSuite { void _registerApiCacheTests() { // ============================================================ - // ApiCache: insert / select / update / delete round-trip + // ApiCache schema defaults and constraints // ============================================================ group('ApiCache', () { - test('insert + select round-trip preserves fields', () async { - await db - .into(db.apiCache) - .insert(ApiCacheCompanion.insert(cacheKey: 'srv:/library/metadata/1', data: '{"hello":"world"}')); - - final rows = await db.select(db.apiCache).get(); - expect(rows, hasLength(1)); - expect(rows.first.cacheKey, 'srv:/library/metadata/1'); - expect(rows.first.data, '{"hello":"world"}'); - expect(rows.first.pinned, isFalse); // default - }); - test('default pinned=false, custom pinned=true is honored', () async { await db.into(db.apiCache).insert(ApiCacheCompanion.insert(cacheKey: 'k1', data: 'a')); await db @@ -288,44 +272,12 @@ class _AppDatabaseTestSuite { throwsA(isA()), ); }); - - test('insertOnConflictUpdate replaces the row', () async { - await db.into(db.apiCache).insert(ApiCacheCompanion.insert(cacheKey: 'dup', data: 'first')); - await db - .into(db.apiCache) - .insertOnConflictUpdate( - ApiCacheCompanion.insert(cacheKey: 'dup', data: 'second', pinned: const Value(true)), - ); - - final rows = await db.select(db.apiCache).get(); - expect(rows, hasLength(1)); - expect(rows.first.data, 'second'); - expect(rows.first.pinned, isTrue); - }); - - test('update modifies existing row', () async { - await db.into(db.apiCache).insert(ApiCacheCompanion.insert(cacheKey: 'k', data: 'orig')); - await (db.update( - db.apiCache, - )..where((t) => t.cacheKey.equals('k'))).write(const ApiCacheCompanion(data: Value('updated'))); - - final row = await (db.select(db.apiCache)..where((t) => t.cacheKey.equals('k'))).getSingle(); - expect(row.data, 'updated'); - }); - - test('delete removes the row', () async { - await db.into(db.apiCache).insert(ApiCacheCompanion.insert(cacheKey: 'k', data: 'v')); - expect(await db.select(db.apiCache).get(), hasLength(1)); - - await (db.delete(db.apiCache)..where((t) => t.cacheKey.equals('k'))).go(); - expect(await db.select(db.apiCache).get(), isEmpty); - }); }); } void _registerDownloadedMediaTests() { // ============================================================ - // DownloadedMedia: round-trip + helpers + update + delete + // DownloadedMedia: persistence, defaults, constraints, and helpers // ============================================================ group('DownloadedMedia', () { @@ -378,34 +330,11 @@ class _AppDatabaseTestSuite { expect(row.clientScopeId, 'jf-machine/user-a'); }); - test('updating progress field works', () async { - await insertMovie(); - await (db.update(db.downloadedMedia)..where((t) => t.globalKey.equals('srv1:100'))).write( - const DownloadedMediaCompanion(progress: Value(75), downloadedBytes: Value(1024)), - ); - - final row = await (db.select(db.downloadedMedia)..where((t) => t.globalKey.equals('srv1:100'))).getSingle(); - expect(row.progress, 75); - expect(row.downloadedBytes, 1024); - }); - test('globalKey unique constraint blocks duplicate insert', () async { await insertMovie(); expect(insertMovie(), throwsA(isA())); }); - test('delete removes only the matching row', () async { - await insertMovie(ratingKey: '1'); - await insertMovie(ratingKey: '2'); - expect(await db.select(db.downloadedMedia).get(), hasLength(2)); - - await (db.delete(db.downloadedMedia)..where((t) => t.globalKey.equals('srv1:1'))).go(); - - final rows = await db.select(db.downloadedMedia).get(); - expect(rows, hasLength(1)); - expect(rows.first.ratingKey, '2'); - }); - test('getAllDownloadedMetadata returns only completed items', () async { await insertMovie(ratingKey: '1', status: DownloadStatus.queued.index); await insertMovie(ratingKey: '2', status: DownloadStatus.completed.index); diff --git a/test/focus/focusable_wrapper_test.dart b/test/focus/focusable_wrapper_test.dart index 8dd35a34..10a22fd7 100644 --- a/test/focus/focusable_wrapper_test.dart +++ b/test/focus/focusable_wrapper_test.dart @@ -4,9 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/focus/focusable_wrapper.dart'; import 'package:plezy/focus/input_mode_tracker.dart'; -/// The wrapper's focus chrome (scale Transform + border AnimatedContainer) -/// must only exist in keyboard/d-pad mode: on touch it is pure dead weight -/// multiplied by every card in a grid (see library scroll jank). void main() { Finder chromeIn(Type type) => find.descendant(of: find.byType(FocusableWrapper), matching: find.byType(type)); @@ -19,15 +16,12 @@ void main() { expect(chromeIn(Transform), findsNothing); expect(chromeIn(AnimatedContainer), findsNothing); - // The Focus node stays mounted so d-pad traversal finds the card the - // moment keyboard mode activates. expect(chromeIn(Focus), findsWidgets); }); testWidgets('keyboard mode builds the scale/border chrome', (tester) async { await tester.pumpWidget(InputModeTracker(child: MaterialApp(home: buildWrapper()))); - // A navigation key press flips the tracker into keyboard mode. await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); diff --git a/test/focus/key_event_utils_test.dart b/test/focus/key_event_utils_test.dart index 28562fec..153bc531 100644 --- a/test/focus/key_event_utils_test.dart +++ b/test/focus/key_event_utils_test.dart @@ -252,68 +252,5 @@ void main() { expect(activations, 1); }); - - testWidgets('moves through detail actions when trailer is inserted before shuffle', (tester) async { - final play = FocusNode(debugLabel: 'detail_play'); - final outside = FocusNode(debugLabel: 'outside'); - addTearDown(play.dispose); - addTearDown(outside.dispose); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Row( - children: [ - FocusableActionBar( - actions: [ - FocusableAction( - debugLabel: 'unused_play_label', - focusNode: play, - icon: Icons.play_arrow, - onPressed: () {}, - ), - FocusableAction(debugLabel: 'detail_trailer', icon: Icons.theaters, onPressed: () {}), - FocusableAction(debugLabel: 'detail_shuffle', icon: Icons.shuffle, onPressed: () {}), - FocusableAction(debugLabel: 'detail_download', icon: Icons.download, onPressed: () {}), - FocusableAction(debugLabel: 'detail_watched', icon: Icons.check, onPressed: () {}), - FocusableAction(debugLabel: 'detail_more', icon: Icons.more_vert, onPressed: () {}), - ], - ), - Focus(focusNode: outside, child: const SizedBox(width: 50, height: 50)), - ], - ), - ), - ), - ); - await tester.pump(); - - play.requestFocus(); - await tester.pump(); - expect(FocusManager.instance.primaryFocus?.debugLabel, 'detail_play'); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - expect(FocusManager.instance.primaryFocus?.debugLabel, 'detail_trailer'); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - expect(FocusManager.instance.primaryFocus?.debugLabel, 'detail_shuffle'); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - expect(FocusManager.instance.primaryFocus?.debugLabel, 'detail_download'); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - expect(FocusManager.instance.primaryFocus?.debugLabel, 'detail_watched'); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - expect(FocusManager.instance.primaryFocus?.debugLabel, 'detail_more'); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - expect(FocusManager.instance.primaryFocus?.debugLabel, 'detail_more'); - }); }); } diff --git a/test/media/media_sort_test.dart b/test/media/media_sort_test.dart index cb1d9cb4..a835e1ca 100644 --- a/test/media/media_sort_test.dart +++ b/test/media/media_sort_test.dart @@ -77,10 +77,5 @@ void main() { final b = MediaSort(key: 'k2', title: 'A'); expect(a, isNot(equals(b))); }); - - test('identity short-circuit', () { - final a = MediaSort(key: 'k', title: 't'); - expect(a == a, isTrue); - }); }); } diff --git a/test/mixins/context_menu_tap_mixin_test.dart b/test/mixins/context_menu_tap_mixin_test.dart index 93f41d56..38a118f2 100644 --- a/test/mixins/context_menu_tap_mixin_test.dart +++ b/test/mixins/context_menu_tap_mixin_test.dart @@ -1,27 +1,7 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/mixins/context_menu_tap_mixin.dart'; -// NOTE on coverage scope: -// `ContextMenuTapMixin` is a thin glue layer: -// 1. caches the last-known global tap position so the menu can anchor -// itself at the press location, and -// 2. forwards show calls to the embedded MediaContextMenu's GlobalKey. -// -// The interesting branches for tests are the pure helpers: -// - storeTapPosition writes the global Offset. -// - showContextMenuFromTap / showContextMenu null-safe when no MediaContextMenu -// is attached (currentState is null). -// - isContextMenuOpen returns false when currentState is null. -// -// What's NOT covered (and intentionally skipped): -// - The branch where `contextMenuKey.currentState` is non-null and the menu -// actually opens — that requires mounting the production -// [MediaContextMenu] widget, which depends on a full provider stack -// (PlexClient, MultiServerProvider, etc.). The mixin's job is just to -// forward the call, so the value of widget-level coverage is low. - class _Probe extends StatefulWidget { const _Probe({required this.onState}); @@ -47,17 +27,14 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('ContextMenuTapMixin', () { - testWidgets('contextMenuKey is a stable GlobalKey instance', (tester) async { + testWidgets('contextMenuKey remains stable across rebuilds', (tester) async { late _ProbeState state; await tester.pumpWidget(_Probe(onState: (s) => state = s)); + final initialKey = state.contextMenuKey; - expect(state.contextMenuKey, isA()); - // GlobalKey identity is stable across rebuilds — important because the - // production widget passes this key to MediaContextMenu and reads - // currentState through it. - final keyA = state.contextMenuKey; - await tester.pump(); - expect(identical(state.contextMenuKey, keyA), isTrue); + await tester.pumpWidget(_Probe(onState: (s) => state = s)); + + expect(identical(state.contextMenuKey, initialKey), isTrue); }); testWidgets('isContextMenuOpen returns false when no menu is mounted', (tester) async { @@ -71,15 +48,10 @@ void main() { late _ProbeState state; await tester.pumpWidget(_Probe(onState: (s) => state = s)); - // Synthesise a TapDownDetails — the mixin only reads globalPosition. const offset = Offset(123.0, 456.0); - state.storeTapPosition(TapDownDetails(globalPosition: offset, kind: PointerDeviceKind.mouse)); + state.storeTapPosition(TapDownDetails(globalPosition: offset)); - // The field is private but both show methods consume it without throwing - // when the MediaContextMenu key has no currentState. Calling them after - // storeTapPosition is the closest observable assertion that the position - // got captured. - expect(state.showContextMenuFromTap, returnsNormally); + expect(state.lastTapPosition, offset); }); testWidgets('showContextMenuFromTap and showContextMenu are no-ops without a mounted menu', (tester) async { diff --git a/test/mixins/disposable_change_notifier_mixin_test.dart b/test/mixins/disposable_change_notifier_mixin_test.dart index d3b5e021..a5a6a4e9 100644 --- a/test/mixins/disposable_change_notifier_mixin_test.dart +++ b/test/mixins/disposable_change_notifier_mixin_test.dart @@ -6,12 +6,6 @@ class _Probe extends ChangeNotifier with DisposableChangeNotifierMixin {} void main() { group('DisposableChangeNotifierMixin', () { - test('isDisposed is false on a fresh notifier', () { - final n = _Probe(); - expect(n.isDisposed, isFalse); - n.dispose(); - }); - test('safeNotifyListeners returns true and notifies when not disposed', () { final n = _Probe(); var fired = 0; @@ -57,14 +51,5 @@ void main() { expect(fired, 3); n.dispose(); }); - - test('safeNotifyListeners after dispose does not throw', () { - final n = _Probe(); - n.dispose(); - - // Without the mixin's guard, ChangeNotifier.notifyListeners would throw - // a debug-only assertion. The whole point of the mixin is to no-op. - expect(n.safeNotifyListeners, returnsNormally); - }); }); } diff --git a/test/mixins/event_aware_test.dart b/test/mixins/event_aware_test.dart index 800e351d..d638043e 100644 --- a/test/mixins/event_aware_test.dart +++ b/test/mixins/event_aware_test.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'package:plezy/media/ids.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -40,24 +39,6 @@ void main() { tearDown(() => notifier.dispose()); - test('delivers events when no filters are set (mounted, no serverId/keys)', () async { - final sub = subscribeToHierarchicalEvents<_FakeEvent>( - notifier: notifier, - mounted: () => true, - serverId: () => null, - globalKeys: () => null, - itemIds: () => null, - onEvent: received.add, - ); - - final ev = _FakeEvent(serverId: ServerId('s1'), itemId: '42'); - notifier.notify(ev); - await _settle(); - - expect(received, [ev]); - await sub.cancel(); - }); - test('drops events when not mounted', () async { var mounted = false; final sub = subscribeToHierarchicalEvents<_FakeEvent>( @@ -275,19 +256,5 @@ void main() { await _settle(); expect(received, hasLength(1)); }); - - test('returns a typed StreamSubscription', () { - final sub = subscribeToHierarchicalEvents<_FakeEvent>( - notifier: notifier, - mounted: () => true, - serverId: () => null, - globalKeys: () => null, - itemIds: () => null, - onEvent: received.add, - ); - - expect(sub, isA>()); - sub.cancel(); - }); }); } diff --git a/test/mixins/item_updatable_test.dart b/test/mixins/item_updatable_test.dart deleted file mode 100644 index 9a5f6e68..00000000 --- a/test/mixins/item_updatable_test.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:plezy/media/media_backend.dart'; -import 'package:plezy/media/media_item.dart'; -import 'package:plezy/media/media_kind.dart'; -import 'package:plezy/mixins/item_updatable.dart'; -import '../test_helpers/media_items.dart'; - -/// Probe that mixes in [ItemUpdatable]. These tests exercise the -/// `updateItemInLists` contract directly — the override-point screens -/// implement and the only piece [ItemUpdatable] adds on top of a plain -/// `setState` call site. The network path (`updateItem`) keys off -/// `itemServerId`; left null here so it short-circuits. -class _Probe extends StatefulWidget { - const _Probe({this.onState}); - final void Function(_ProbeState)? onState; - - @override - State<_Probe> createState() => _ProbeState(); -} - -class _ProbeState extends State<_Probe> with ItemUpdatable { - /// In-memory list, mirroring the typical screen pattern: a list keyed by - /// `id` whose entries get swapped out by `updateItemInLists`. - final List items = []; - - /// Records every `updateItemInLists` invocation for assertions. - final List<({String itemId, MediaItem metadata})> updates = []; - - @override - void updateItemInLists(String itemId, MediaItem updatedItem) { - updates.add((itemId: itemId, metadata: updatedItem)); - final index = items.indexWhere((item) => item.id == itemId); - if (index != -1) { - items[index] = updatedItem; - } - } - - @override - void initState() { - super.initState(); - widget.onState?.call(this); - } - - @override - Widget build(BuildContext context) => const SizedBox.shrink(); -} - -MediaItem _meta(String id, {String? title}) => - testMediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.movie, title: title); - -void main() { - group('ItemUpdatable', () { - testWidgets('mixin satisfies its own type predicate', (tester) async { - late _ProbeState state; - await tester.pumpWidget(_Probe(onState: (s) => state = s)); - - expect(state, isA()); - }); - - testWidgets('updateItemInLists is called with the forwarded itemId/metadata', (tester) async { - late _ProbeState state; - await tester.pumpWidget(_Probe(onState: (s) => state = s)); - - final updated = _meta('42', title: 'Updated'); - state.updateItemInLists('42', updated); - - expect(state.updates, hasLength(1)); - expect(state.updates.first.itemId, '42'); - expect(identical(state.updates.first.metadata, updated), isTrue); - }); - - testWidgets('updateItemInLists swaps a matching entry by id', (tester) async { - late _ProbeState state; - await tester.pumpWidget(_Probe(onState: (s) => state = s)); - - state.items - ..add(_meta('1', title: 'One')) - ..add(_meta('2', title: 'Two')) - ..add(_meta('3', title: 'Three')); - - final replacement = _meta('2', title: 'Two (refreshed)'); - state.updateItemInLists('2', replacement); - - expect(state.items.map((i) => i.title).toList(), ['One', 'Two (refreshed)', 'Three']); - expect(identical(state.items[1], replacement), isTrue); - }); - - testWidgets('updateItemInLists is a no-op for an unknown id', (tester) async { - late _ProbeState state; - await tester.pumpWidget(_Probe(onState: (s) => state = s)); - - state.items - ..add(_meta('1')) - ..add(_meta('2')); - - state.updateItemInLists('999', _meta('999')); - - expect(state.items.map((i) => i.id).toList(), ['1', '2']); - // Still recorded — the contract is "we received this update", regardless - // of whether the screen's list contained the key. - expect(state.updates, hasLength(1)); - }); - - testWidgets('multiple updates accumulate in the screen-defined list', (tester) async { - late _ProbeState state; - await tester.pumpWidget(_Probe(onState: (s) => state = s)); - - state.items.addAll([_meta('1'), _meta('2')]); - - state.updateItemInLists('1', _meta('1', title: 'A')); - state.updateItemInLists('2', _meta('2', title: 'B')); - state.updateItemInLists('1', _meta('1', title: 'A2')); - - expect(state.updates.map((u) => u.itemId).toList(), ['1', '2', '1']); - expect(state.items[0].title, 'A2'); - expect(state.items[1].title, 'B'); - }); - }); -} diff --git a/test/mixins/library_tab_state_test.dart b/test/mixins/library_tab_state_test.dart index 64d9782b..43a87408 100644 --- a/test/mixins/library_tab_state_test.dart +++ b/test/mixins/library_tab_state_test.dart @@ -10,28 +10,11 @@ import 'package:plezy/providers/multi_server_provider.dart'; import 'package:plezy/services/data_aggregation_service.dart'; import 'package:plezy/services/multi_server_manager.dart'; -// NOTE on coverage scope: -// `LibraryTabStateMixin` is a 14-line forwarding mixin: -// - exposes `library` (abstract) and -// - resolves the per-library PlexClient via a BuildContext extension. -// -// Coverage: -// - The mixin returns the same library reference back to subclass code. -// - `getClientForLibrary` throws when there is no MultiServerProvider with a -// matching server — the documented "no client available" failure path. -// -// What's NOT covered (and intentionally skipped): -// - The success path of `getClientForLibrary` requires either a real -// [PlexClient] inside a [MultiServerManager] (which itself requires a -// server registry, network, and prefs) or a deep fake of the manager's -// client cache. Not worth it for a mixin whose only contribution is -// `context.getPlexClientForLibrary(library)`. - class _Probe extends StatefulWidget { const _Probe({required this.library, required this.onState}); final MediaLibrary library; - final void Function(_ProbeState state, BuildContext context) onState; + final void Function(_ProbeState state) onState; @override State<_Probe> createState() => _ProbeState(); @@ -43,10 +26,9 @@ class _ProbeState extends State<_Probe> with LibraryTabStateMixin<_Probe> { @override Widget build(BuildContext context) { - // Surface state+context after the first frame so callers can poke the - // mixin against a live BuildContext. + // Surface state after the first frame so tests receive a mounted probe. WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) widget.onState(this, context); + if (mounted) widget.onState(this); }); return const SizedBox.shrink(); } @@ -59,21 +41,8 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('LibraryTabStateMixin', () { - testWidgets('library getter returns the host state\'s library', (tester) async { - late _ProbeState state; - final library = _lib(serverId: ServerId('srv-A'), key: 'lib-1'); - - await tester.pumpWidget(_Probe(library: library, onState: (s, _) => state = s)); - await tester.pump(); - - expect(identical(state.library, library), isTrue); - expect(state.library.serverId, 'srv-A'); - expect(state.library.id, 'lib-1'); - }); - testWidgets('getClientForLibrary throws when no server matches and no fallback online', (tester) async { late _ProbeState state; - late BuildContext ctx; final manager = MultiServerManager(); final aggregation = DataAggregationService(manager); @@ -87,19 +56,13 @@ void main() { value: provider, child: _Probe( library: _lib(serverId: ServerId('srv-missing')), - onState: (s, c) { - state = s; - ctx = c; - }, + onState: (s) => state = s, ), ), ); await tester.pump(); - // No registered servers means no client and no fallback — the - // extension throws a localized "no client available" Exception. expect(() => state.getClientForLibrary(), throwsA(isA())); - expect(ctx.mounted, isTrue); // sanity: exception came from the lookup, not a torn-down context }); }); } diff --git a/test/mixins/mounted_set_state_mixin_test.dart b/test/mixins/mounted_set_state_mixin_test.dart index 8a2873a8..bac47d8c 100644 --- a/test/mixins/mounted_set_state_mixin_test.dart +++ b/test/mixins/mounted_set_state_mixin_test.dart @@ -34,9 +34,14 @@ void main() { await tester.pumpWidget(_Probe(onState: (s) => state = s)); final initialBuilds = state.builds; - state.setStateIfMounted(() => state.counter = 5); + var callbackCalls = 0; + state.setStateIfMounted(() { + callbackCalls++; + state.counter = 5; + }); await tester.pump(); + expect(callbackCalls, 1); expect(state.counter, 5); expect(state.builds, greaterThan(initialBuilds)); expect(find.text('count=5'), findsOneWidget); @@ -61,16 +66,5 @@ void main() { expect(state.counter, 0); expect(state.builds, buildsBefore); }); - - testWidgets('setStateIfMounted callback is invoked exactly once per call when mounted', (tester) async { - late _ProbeState state; - await tester.pumpWidget(_Probe(onState: (s) => state = s)); - - var calls = 0; - state.setStateIfMounted(() => calls++); - await tester.pump(); - - expect(calls, 1); - }); }); } diff --git a/test/mixins/paginated_item_loader_test.dart b/test/mixins/paginated_item_loader_test.dart index 1e5071a4..2e95ce49 100644 --- a/test/mixins/paginated_item_loader_test.dart +++ b/test/mixins/paginated_item_loader_test.dart @@ -343,38 +343,6 @@ void main() { expect(state.loadedItems, isEmpty); }); - testWidgets('disposePagination clears state and aborts in-flight fetches', (tester) async { - late _PaginatedProbeState state; - final futures = >>[]; - - await tester.pumpWidget( - _PaginatedProbe( - onState: (s) => state = s, - fetcher: (start, size, abort) { - final c = Completer>(); - futures.add(c); - return c.future; - }, - ), - ); - - // Trigger an in-flight fetch for the initial page. - unawaited(state.loadInitialPage(10)); - await tester.pump(); - - // Capture the abort controller's state via a side channel: the mixin's - // public surface tells us about totalSize/loadedItems but not the - // controller. Instead, we observe the side-effect: after - // disposePagination, completing the staged future does not mutate state. - state.disposePagination(); - // Completing the future after dispose should not touch loadedItems. - futures.first.complete(_result(start: 0, size: 10, totalSize: 50)); - await tester.pump(); - - expect(state.totalSize, 0); - expect(state.loadedItems, isEmpty); - }); - testWidgets('removeLoadedItemAndShift removes index and shifts higher entries down', (tester) async { late _PaginatedProbeState state; await tester.pumpWidget( diff --git a/test/mixins/refreshable_test.dart b/test/mixins/refreshable_test.dart deleted file mode 100644 index f4ed6f84..00000000 --- a/test/mixins/refreshable_test.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:plezy/mixins/refreshable.dart'; - -class _RefreshProbe extends StatefulWidget { - const _RefreshProbe({this.onState}); - final void Function(_RefreshProbeState)? onState; - - @override - State<_RefreshProbe> createState() => _RefreshProbeState(); -} - -class _RefreshProbeState extends State<_RefreshProbe> - with Refreshable, FullRefreshable, FocusableTab, SearchInputFocusable, LibraryLoadable { - int refreshCalls = 0; - int fullRefreshCalls = 0; - int focusActiveTabCalls = 0; - int focusSearchInputCalls = 0; - String? lastSubmittedQuery; - String? lastLibraryKey; - - @override - void initState() { - super.initState(); - widget.onState?.call(this); - } - - @override - void refresh() => refreshCalls++; - - @override - void fullRefresh() => fullRefreshCalls++; - - @override - void focusActiveTabIfReady() => focusActiveTabCalls++; - - @override - void focusSearchInput() => focusSearchInputCalls++; - - @override - void submitSearchQuery(String query) => lastSubmittedQuery = query; - - @override - void loadLibraryByKey(String libraryGlobalKey) => lastLibraryKey = libraryGlobalKey; - - @override - Widget build(BuildContext context) => const SizedBox.shrink(); -} - -class _RefreshableOnly extends StatefulWidget { - const _RefreshableOnly({this.onState}); - final void Function(_RefreshableOnlyState)? onState; - - @override - State<_RefreshableOnly> createState() => _RefreshableOnlyState(); -} - -class _RefreshableOnlyState extends State<_RefreshableOnly> with Refreshable { - int refreshCalls = 0; - - @override - void initState() { - super.initState(); - widget.onState?.call(this); - } - - @override - void refresh() => refreshCalls++; - - @override - Widget build(BuildContext context) => const SizedBox.shrink(); -} - -class _PlainProbe extends StatefulWidget { - const _PlainProbe({this.onState}); - final void Function(_PlainProbeState)? onState; - - @override - State<_PlainProbe> createState() => _PlainProbeState(); -} - -class _PlainProbeState extends State<_PlainProbe> { - @override - void initState() { - super.initState(); - widget.onState?.call(this); - } - - @override - Widget build(BuildContext context) => const SizedBox.shrink(); -} - -void main() { - group('Refreshable', () { - testWidgets('refresh() invokes the implementation', (tester) async { - late _RefreshProbeState state; - await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s)); - - state.refresh(); - state.refresh(); - - expect(state.refreshCalls, 2); - }); - - testWidgets('a State mixing in Refreshable matches an `is Refreshable` check', (tester) async { - late _RefreshableOnlyState state; - await tester.pumpWidget(_RefreshableOnly(onState: (s) => state = s)); - - // This is the production usage: `if (currentState case final Refreshable r) r.refresh()`. - expect(state, isA()); - - // Drive the refresh via the interface to mirror real callers. - if (state case final Refreshable r) { - r.refresh(); - } - expect(state.refreshCalls, 1); - }); - - testWidgets('a plain State without the mixin does not match Refreshable', (tester) async { - late _PlainProbeState state; - await tester.pumpWidget(_PlainProbe(onState: (s) => state = s)); - - expect(state, isNot(isA())); - expect(state, isNot(isA())); - expect(state, isNot(isA())); - expect(state, isNot(isA())); - expect(state, isNot(isA())); - }); - }); - - group('FullRefreshable', () { - testWidgets('fullRefresh() invokes the implementation', (tester) async { - late _RefreshProbeState state; - await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s)); - - if (state case final FullRefreshable f) { - f.fullRefresh(); - } - - expect(state.fullRefreshCalls, 1); - }); - - testWidgets('refresh() and fullRefresh() are independent counters', (tester) async { - late _RefreshProbeState state; - await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s)); - - state.refresh(); - state.refresh(); - state.fullRefresh(); - - expect(state.refreshCalls, 2); - expect(state.fullRefreshCalls, 1); - }); - }); - - group('FocusableTab', () { - testWidgets('focusActiveTabIfReady() invokes the implementation', (tester) async { - late _RefreshProbeState state; - await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s)); - - expect(state, isA()); - state.focusActiveTabIfReady(); - expect(state.focusActiveTabCalls, 1); - }); - }); - - group('SearchInputFocusable', () { - testWidgets('focusSearchInput() invokes the implementation', (tester) async { - late _RefreshProbeState state; - await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s)); - - state.focusSearchInput(); - expect(state.focusSearchInputCalls, 1); - }); - - testWidgets('submitSearchQuery() forwards the query argument', (tester) async { - late _RefreshProbeState state; - await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s)); - - if (state case final SearchInputFocusable s) { - s.submitSearchQuery('movie'); - } - expect(state.lastSubmittedQuery, 'movie'); - }); - }); - - group('LibraryLoadable', () { - testWidgets('loadLibraryByKey() forwards the key argument', (tester) async { - late _RefreshProbeState state; - await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s)); - - state.loadLibraryByKey('server1:42'); - expect(state.lastLibraryKey, 'server1:42'); - }); - }); - - group('combined mixins', () { - testWidgets('a State can mix in all five interface mixins simultaneously', (tester) async { - late _RefreshProbeState state; - await tester.pumpWidget(_RefreshProbe(onState: (s) => state = s)); - - expect(state, isA()); - expect(state, isA()); - expect(state, isA()); - expect(state, isA()); - expect(state, isA()); - }); - }); -} diff --git a/test/mixins/server_bound_media_mixin_test.dart b/test/mixins/server_bound_media_mixin_test.dart index 1c58ae55..6e565dad 100644 --- a/test/mixins/server_bound_media_mixin_test.dart +++ b/test/mixins/server_bound_media_mixin_test.dart @@ -65,30 +65,6 @@ void main() { expect(state.serverBoundServerId, isNull); }); - testWidgets('isServerBoundOffline reflects the host state override', (tester) async { - late _ProbeState onState; - late _ProbeState offState; - await tester.pumpWidget( - _Probe( - metadata: _meta(serverId: ServerId('s1')), - offline: false, - onState: (s, _) => offState = s, - ), - ); - await tester.pump(); - expect(offState.isServerBoundOffline, isFalse); - - await tester.pumpWidget( - _Probe( - metadata: _meta(serverId: ServerId('s1')), - offline: true, - onState: (s, _) => onState = s, - ), - ); - await tester.pump(); - expect(onState.isServerBoundOffline, isTrue); - }); - testWidgets('toServerBoundGlobalKey uses the metadata serverId by default', (tester) async { late _ProbeState state; await tester.pumpWidget( diff --git a/test/mixins/tab_navigation_mixin_test.dart b/test/mixins/tab_navigation_mixin_test.dart index 3b4d693e..ebdbdb51 100644 --- a/test/mixins/tab_navigation_mixin_test.dart +++ b/test/mixins/tab_navigation_mixin_test.dart @@ -52,8 +52,12 @@ class _ProbeState extends State<_Probe> with TickerProviderStateMixin<_Probe>, T } @override - Widget build(BuildContext context) => - const Directionality(textDirection: TextDirection.ltr, child: SizedBox.shrink()); + Widget build(BuildContext context) => Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [for (final node in _nodes) Focus(focusNode: node, child: const SizedBox.shrink())], + ), + ); } void main() { @@ -210,22 +214,17 @@ void main() { expect(state.onTabChangedCalls, greaterThan(before)); }); - - testWidgets('focusTabBar sets suppressAutoFocus and calls requestFocus on the active chip', (tester) async { + testWidgets('focusTabBar focuses the active chip and suppresses content auto-focus', (tester) async { late _ProbeState state; - await tester.pumpWidget(_Probe(tabCount: 3, onState: (s) => state = s)); + await tester.pumpWidget(_Probe(tabCount: 3, initialIndex: 1, onState: (s) => state = s)); + final activeNode = state.getTabChipFocusNode(1); - // Pre-condition: nothing is focused. - final activeNode = state.getTabChipFocusNode(state.tabController.index); expect(activeNode.hasFocus, isFalse); - state.focusTabBar(); await tester.pump(); - // The flag flip is the deterministic, mountable side-effect of - // focusTabBar; actual focus delivery requires a real Focus widget tree - // (the production usage attaches each node to a FocusableTabChip). expect(state.suppressAutoFocus, isTrue); + expect(activeNode.hasFocus, isTrue); }); testWidgets('onTabBarBack is null-safe outside MainScreenFocusScope (no throw)', (tester) async { diff --git a/test/mpv/property_observation_test.dart b/test/mpv/property_observation_test.dart index c01281fe..e663e192 100644 --- a/test/mpv/property_observation_test.dart +++ b/test/mpv/property_observation_test.dart @@ -3,7 +3,6 @@ import 'dart:io' show Platform; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/mpv/player/platform/player_android.dart'; -import 'package:plezy/mpv/player/player_base.dart'; import 'package:plezy/mpv/player/player_native.dart'; import 'package:plezy/services/settings_service.dart'; @@ -62,11 +61,6 @@ void main() { Set names(List calls) => calls.map((c) => (c.arguments as Map)['name'] as String).toSet(); - test('the shared core table covers every state-critical property', () { - final tableNames = PlayerBase.corePropertyObservations.map((e) => e.$1).toSet()..add('track-list'); - expect(tableNames, coreNames); - }); - test('ExoPlayer registers the core properties (plus its cache extra)', () async { final player = PlayerAndroid(); final observations = await capturedObservations( diff --git a/test/providers/companion_remote_provider_test.dart b/test/providers/companion_remote_provider_test.dart index d520ccba..dafb20e1 100644 --- a/test/providers/companion_remote_provider_test.dart +++ b/test/providers/companion_remote_provider_test.dart @@ -88,11 +88,6 @@ void main() { }); group('CompanionRemoteProvider — dispose hygiene', () { - test('dispose runs cleanly with no peer service or subscriptions', () { - final p = CompanionRemoteProvider(); - expect(p.dispose, returnsNormally); - }); - test('cancelReconnect on a fresh provider does not throw', () { final p = CompanionRemoteProvider(); // No timer, no session — copyWith on null _session is a no-op so @@ -137,17 +132,6 @@ void main() { ); p.dispose(); }); - - test('connectToManualHost rejects empty host strings via localized crypto guard', () async { - final p = CompanionRemoteProvider(); - await expectLater( - () => p.connectToManualHost(''), - throwsA( - isA().having((error) => error.message, 'message', t.companionRemote.pairing.cryptoInitFailed), - ), - ); - p.dispose(); - }); }); group('CompanionRemoteProvider — crypto identity', () { diff --git a/test/providers/download_provider_test.dart b/test/providers/download_provider_test.dart index 2b9e5566..61d8a346 100644 --- a/test/providers/download_provider_test.dart +++ b/test/providers/download_provider_test.dart @@ -1594,12 +1594,6 @@ void main() { }); group('DownloadProvider — dispose hygiene', () { - test('dispose cancels stream subscriptions and is safe to call once', () async { - final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); - await p.ensureInitialized(); - expect(p.dispose, returnsNormally); - }); - test('isDisposed flips from false to true on dispose', () async { final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); await p.ensureInitialized(); @@ -1608,11 +1602,4 @@ void main() { expect(p.isDisposed, isTrue); }); }); - - group('DownloadProvider — DownloadFilter enum', () { - test('DownloadFilter has all/unwatched values', () { - expect(DownloadFilter.values, contains(DownloadFilter.all)); - expect(DownloadFilter.values, contains(DownloadFilter.unwatched)); - }); - }); } diff --git a/test/providers/multi_server_provider_test.dart b/test/providers/multi_server_provider_test.dart index 16823623..5fdb2bf4 100644 --- a/test/providers/multi_server_provider_test.dart +++ b/test/providers/multi_server_provider_test.dart @@ -30,13 +30,6 @@ void main() { p.dispose(); }); - test('exposes the injected manager and aggregation service', () { - final p = MultiServerProvider(manager, aggregation); - expect(identical(p.serverManager, manager), isTrue); - expect(identical(p.aggregationService, aggregation), isTrue); - p.dispose(); - }); - test('isServerOnline / getClientForServer return defaults for unknown ids', () { final p = MultiServerProvider(manager, aggregation); expect(p.isServerOnline(ServerId('nope')), isFalse); diff --git a/test/providers/offline_mode_provider_test.dart b/test/providers/offline_mode_provider_test.dart index 85535d6e..46c7985e 100644 --- a/test/providers/offline_mode_provider_test.dart +++ b/test/providers/offline_mode_provider_test.dart @@ -70,16 +70,6 @@ void main() { manager.dispose(); }); - test('dispose without initialize is safe (no subscriptions to cancel)', () { - final manager = MultiServerManager(); - final p = OfflineModeProvider(manager); - - // Both subscriptions are null since initialize() was never called. - // dispose must tolerate this without throwing. - expect(p.dispose, returnsNormally); - manager.dispose(); - }); - test('dispose marks provider as disposed; later notifies are no-ops', () { final manager = MultiServerManager(); final p = OfflineModeProvider(manager); @@ -92,19 +82,6 @@ void main() { manager.dispose(); }); - test('OfflineModeSource interface contract: isOffline is exposed', () { - final manager = MultiServerManager(); - manager.updateServerStatus(ServerId('srv'), true); - final p = OfflineModeProvider(manager); - - // The provider implements OfflineModeSource — its isOffline getter is the - // sole observable surface for downstream consumers. - expect(p.isOffline, isFalse); - - p.dispose(); - manager.dispose(); - }); - test('warmup skipped when manager already has an online server at construction', () { // If the manager already has an online server when the provider is // built, we have ground truth — no need for the warmup window. diff --git a/test/screens/media_detail_screen_test.dart b/test/screens/media_detail_screen_test.dart index c774b12e..ed9cb593 100644 --- a/test/screens/media_detail_screen_test.dart +++ b/test/screens/media_detail_screen_test.dart @@ -865,30 +865,6 @@ void main() { expect(FocusManager.instance.primaryFocus?.debugLabel, 'first_episode'); }); - testWidgets('marking the show watched flips every visible episode row', (tester) async { - final show = buildShow(); - final season1 = buildSeason(show, 1); - final season2 = buildSeason(show, 2); - final episodes = [buildEpisode(show, season1, 1), buildEpisode(show, season1, 2)]; - final client = _FakeMediaServerClient( - show: show, - childrenByParent: { - show.id: [season1, season2], - season1.id: episodes, - season2.id: [buildEpisode(show, season2, 1), buildEpisode(show, season2, 2)], - }, - ); - - await pumpPhoneDetail(tester, client, show); - expect(episodeRowWatched(tester, 'Episode S1E1'), isFalse); - expect(episodeRowWatched(tester, 'Episode S1E2'), isFalse); - - await emit(tester, () => WatchStateNotifier().notifyWatched(item: show, isNowWatched: true)); - - expect(episodeRowWatched(tester, 'Episode S1E1'), isTrue); - expect(episodeRowWatched(tester, 'Episode S1E2'), isTrue); - }); - testWidgets('container mark overrides an older per-episode patch', (tester) async { final show = buildShow(); final season1 = buildSeason(show, 1); diff --git a/test/scripts/upload_symbols_test.dart b/test/scripts/upload_symbols_test.dart index 4c0235db..a39564dc 100644 --- a/test/scripts/upload_symbols_test.dart +++ b/test/scripts/upload_symbols_test.dart @@ -18,7 +18,7 @@ void main() { test('discovers symbols and passes the complete upload request to the plugin', () async { final symbolRoot = Directory(path.join(repository.path, 'debug-info', 'linux-x64'))..createSync(recursive: true); - final archive = File(path.join(symbolRoot.path, 'symbols.zip'))..writeAsStringSync('symbols'); + File(path.join(symbolRoot.path, 'symbols.zip')).writeAsStringSync('symbols'); final symbolMap = File(path.join(symbolRoot.path, 'obfuscation.map.json'))..writeAsStringSync('{}'); late List uploadArguments; late Map uploadEnvironment; @@ -42,7 +42,6 @@ void main() { ); expect(result, 0); - expect(archive.existsSync(), isTrue); expect(uploadWorkingDirectory, path.normalize(path.absolute(repository.path))); expect(uploadEnvironment['SENTRY_AUTH_TOKEN'], 'admin-token'); expect(uploadEnvironment['SENTRY_LOG_LEVEL'], 'info'); diff --git a/test/services/api_cache_watch_state_test.dart b/test/services/api_cache_watch_state_test.dart index 1a1b151d..d2155b28 100644 --- a/test/services/api_cache_watch_state_test.dart +++ b/test/services/api_cache_watch_state_test.dart @@ -114,16 +114,14 @@ void main() { return cached!['UserData'] as Map; } - 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 { diff --git a/test/services/download_storage_service_test.dart b/test/services/download_storage_service_test.dart index 7f5c1aac..b7149044 100644 --- a/test/services/download_storage_service_test.dart +++ b/test/services/download_storage_service_test.dart @@ -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; diff --git a/test/services/episode_navigation_service_test.dart b/test/services/episode_navigation_service_test.dart index f481bcf5..37ac6b82 100644 --- a/test/services/episode_navigation_service_test.dart +++ b/test/services/episode_navigation_service_test.dart @@ -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. diff --git a/test/services/file_info_parser_test.dart b/test/services/file_info_parser_test.dart index b9299501..97f3462c 100644 --- a/test/services/file_info_parser_test.dart +++ b/test/services/file_info_parser_test.dart @@ -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); - }); - }); } diff --git a/test/services/play_queue_launcher_test.dart b/test/services/play_queue_launcher_test.dart index 309b28b0..4eddad6a 100644 --- a/test/services/play_queue_launcher_test.dart +++ b/test/services/play_queue_launcher_test.dart @@ -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()); - }); - - test('PlayQueueEmpty is a const, identity-comparable singleton', () { - const a = PlayQueueEmpty(); - const b = PlayQueueEmpty(); - expect(identical(a, b), isTrue); - expect(a, isA()); - }); - 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'); - }); - }); } diff --git a/test/services/playback_progress_tracker_test.dart b/test/services/playback_progress_tracker_test.dart index 157b3640..2d2723b6 100644 --- a/test/services/playback_progress_tracker_test.dart +++ b/test/services/playback_progress_tracker_test.dart @@ -322,18 +322,6 @@ void main() { throwsA(isA()), ); }); - - 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); - }); }); // ============================================================ diff --git a/test/services/playback_session_test.dart b/test/services/playback_session_test.dart index 6b26d983..f5ec4ae2 100644 --- a/test/services/playback_session_test.dart +++ b/test/services/playback_session_test.dart @@ -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'); - }); } diff --git a/test/services/plex_api_cache_test.dart b/test/services/plex_api_cache_test.dart index d9966720..e55159d1 100644 --- a/test/services/plex_api_cache_test.dart +++ b/test/services/plex_api_cache_test.dart @@ -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}); diff --git a/test/services/seerr/seerr_client_test.dart b/test/services/seerr/seerr_client_test.dart index 5c95ca96..c08ff757 100644 --- a/test/services/seerr/seerr_client_test.dart +++ b/test/services/seerr/seerr_client_test.dart @@ -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.fromJson({ - 'page': 1, - 'totalPages': 3, - 'results': [ - {'id': 1}, - ], - }, (item) => item['id'] as int); - expect(tmdbShape.hasMore, isTrue); - - final pageInfoShape = SeerrPage.fromJson({ + test('parses the pageInfo pagination shape', () { + final page = SeerrPage.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]); }); }); diff --git a/test/services/track_manager_test.dart b/test/services/track_manager_test.dart index 527888d3..07226414 100644 --- a/test/services/track_manager_test.dart +++ b/test/services/track_manager_test.dart @@ -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) // ============================================================ diff --git a/test/services/trackers/tracker_session_utils_test.dart b/test/services/trackers/tracker_session_utils_test.dart index 421cc5cd..17437461 100644 --- a/test/services/trackers/tracker_session_utils_test.dart +++ b/test/services/trackers/tracker_session_utils_test.dart @@ -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', diff --git a/test/test_helpers/media_items.dart b/test/test_helpers/media_items.dart index 2dc9ad10..ce99b46c 100644 --- a/test/test_helpers/media_items.dart +++ b/test/test_helpers/media_items.dart @@ -130,71 +130,3 @@ MediaItem testMediaItem({ raw: raw, ); } - -/// Season fixture with canonical show linkage. -MediaItem testSeason({ - String id = 'season-1', - MediaItem? show, - int index = 1, - String? title, - MediaBackend? backend, - String? serverId, - String? libraryId, - int? leafCount, - int? viewedLeafCount, -}) { - return testMediaItem( - id: id, - backend: backend ?? show?.backend ?? MediaBackend.plex, - kind: MediaKind.season, - title: title, - parentId: show?.id, - parentTitle: show?.title, - index: index, - serverId: serverId ?? show?.serverId, - serverName: show?.serverName, - libraryId: libraryId ?? show?.libraryId, - libraryTitle: show?.libraryTitle, - leafCount: leafCount, - viewedLeafCount: viewedLeafCount, - ); -} - -/// Episode fixture with canonical show and season linkage. -MediaItem testEpisode({ - String id = 'episode-1', - MediaItem? show, - MediaItem? season, - int index = 1, - String? title, - MediaBackend? backend, - String? serverId, - String? libraryId, - int? durationMs, - int? viewOffsetMs, - int? viewCount, - String? originallyAvailableAt, - List? mediaVersions, -}) { - return testMediaItem( - id: id, - backend: backend ?? season?.backend ?? show?.backend ?? MediaBackend.plex, - kind: MediaKind.episode, - title: title, - parentId: season?.id, - parentTitle: season?.title, - parentIndex: season?.index, - index: index, - grandparentId: show?.id, - grandparentTitle: show?.title, - serverId: serverId ?? season?.serverId ?? show?.serverId, - serverName: season?.serverName ?? show?.serverName, - libraryId: libraryId ?? season?.libraryId ?? show?.libraryId, - libraryTitle: season?.libraryTitle ?? show?.libraryTitle, - durationMs: durationMs, - viewOffsetMs: viewOffsetMs, - viewCount: viewCount, - originallyAvailableAt: originallyAvailableAt, - mediaVersions: mediaVersions, - ); -} diff --git a/test/test_helpers/media_items_test.dart b/test/test_helpers/media_items_test.dart deleted file mode 100644 index dd5c865d..00000000 --- a/test/test_helpers/media_items_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:plezy/media/media_backend.dart'; -import 'package:plezy/media/media_kind.dart'; - -import 'media_items.dart'; - -void main() { - test('default fixture is a minimal Plex movie', () { - final item = testMediaItem(); - - expect(item.id, 'item-1'); - expect(item.backend, MediaBackend.plex); - expect(item.kind, MediaKind.movie); - expect(item.serverId, isNull); - expect(item.parentId, isNull); - expect(item.viewCount, isNull); - }); - - test('season and episode fixtures preserve canonical hierarchy and scope', () { - final show = testMediaItem( - id: 'show-1', - kind: MediaKind.show, - backend: MediaBackend.jellyfin, - title: 'Show', - serverId: 'server-1', - serverName: 'Server', - libraryId: 'library-1', - libraryTitle: 'Library', - ); - final season = testSeason(id: 'season-2', show: show, index: 2, title: 'Season 2'); - final episode = testEpisode(id: 'episode-3', show: show, season: season, index: 3, title: 'Episode 3'); - - expect(season.backend, show.backend); - expect(season.parentId, show.id); - expect(season.parentTitle, show.title); - expect(episode.parentId, season.id); - expect(episode.parentTitle, season.title); - expect(episode.parentIndex, season.index); - expect(episode.grandparentId, show.id); - expect(episode.grandparentTitle, show.title); - expect(episode.serverId, show.serverId); - expect(episode.libraryId, show.libraryId); - }); -} diff --git a/test/test_helpers/watch_together_fakes.dart b/test/test_helpers/watch_together_fakes.dart index 4e8bcb12..7d889b11 100644 --- a/test/test_helpers/watch_together_fakes.dart +++ b/test/test_helpers/watch_together_fakes.dart @@ -186,57 +186,6 @@ class FakeSyncPlayer implements Player { dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } -/// Standalone recording fake peer service (no relay behind it). -class FakeWatchTogetherPeerService extends WatchTogetherPeerService { - FakeWatchTogetherPeerService({required this.peerId}) : super(customBaseUrl: 'http://localhost'); - - final String peerId; - final _messages = StreamController.broadcast(); - final _peerConnected = StreamController.broadcast(); - final _peerDisconnected = StreamController.broadcast(); - - final List broadcasts = []; - final Map> sent = {}; - - @override - String? get myPeerId => peerId; - - @override - Stream get onMessageReceived => _messages.stream; - - @override - Stream get onPeerConnected => _peerConnected.stream; - - @override - Stream get onPeerDisconnected => _peerDisconnected.stream; - - @override - void broadcast(SyncMessage message) { - broadcasts.add(message); - } - - @override - void sendTo(String peerId, SyncMessage message) { - sent.putIfAbsent(peerId, () => []).add(message); - } - - /// All recorded outgoing messages of [type], broadcast and targeted. - Iterable outgoing(SyncMessageType type) => - [...broadcasts, ...sent.values.expand((m) => m)].where((m) => m.type == type); - - void emit(SyncMessage message) => _messages.add(message); - - void emitPeerConnected(String peerId) => _peerConnected.add(peerId); - - void emitPeerDisconnected(String peerId) => _peerDisconnected.add(peerId); - - Future close() async { - await _messages.close(); - await _peerConnected.close(); - await _peerDisconnected.close(); - } -} - /// In-memory relay linking [HubPeerService]s for duplex end-to-end tests. /// /// Mirrors the real relay's contract: broadcasts fan out to every other diff --git a/test/utils/active_client_scope_test.dart b/test/utils/active_client_scope_test.dart index 076a9cb0..58f2a139 100644 --- a/test/utils/active_client_scope_test.dart +++ b/test/utils/active_client_scope_test.dart @@ -23,10 +23,6 @@ void main() { expect(resolveActiveClientScopeId(serverId: serverId, cacheServerId: 'other-machine/user-a'), isNull); }); - test('resolves a compound user scope', () { - expect(resolveActiveClientScopeId(serverId: serverId, cacheServerId: 'jf-machine/user-a'), 'jf-machine/user-a'); - }); - test('keeps users on the same server in distinct active scopes', () { expect(resolveActiveClientScopeId(serverId: serverId, cacheServerId: 'jf-machine/user-a'), 'jf-machine/user-a'); expect(resolveActiveClientScopeId(serverId: serverId, cacheServerId: 'jf-machine/user-b'), 'jf-machine/user-b'); diff --git a/test/utils/base_notifier_test.dart b/test/utils/base_notifier_test.dart index 12dffaf1..63211d15 100644 --- a/test/utils/base_notifier_test.dart +++ b/test/utils/base_notifier_test.dart @@ -7,21 +7,6 @@ class _IntNotifier extends BaseNotifier {} void main() { group('BaseNotifier', () { - test('single listener receives events', () async { - final n = _IntNotifier(); - final received = []; - final sub = n.stream.listen(received.add); - - n.notify(1); - n.notify(2); - n.notify(3); - await Future.delayed(Duration.zero); - - expect(received, [1, 2, 3]); - await sub.cancel(); - n.dispose(); - }); - test('broadcasts to multiple listeners', () async { final n = _IntNotifier(); final a = []; diff --git a/test/utils/codec_utils_test.dart b/test/utils/codec_utils_test.dart index 221a7b65..9b9a8eb1 100644 --- a/test/utils/codec_utils_test.dart +++ b/test/utils/codec_utils_test.dart @@ -40,21 +40,6 @@ void main() { expect(CodecUtils.getSubtitleExtension('dvb_subtitle'), 'sub'); }); - test('every image subtitle codec maps to a non-srt extension', () { - for (final codec in [ - 'pgs', - 'pgssub', - 'hdmv_pgs_subtitle', - 'dvd_subtitle', - 'dvdsub', - 'vobsub', - 'dvb_sub', - 'dvb_subtitle', - ]) { - expect(CodecUtils.getSubtitleExtension(codec), isNot('srt'), reason: codec); - } - }); - test('defaults to srt for unknown codec', () { expect(CodecUtils.getSubtitleExtension('weirdcodec'), 'srt'); expect(CodecUtils.getSubtitleExtension(''), 'srt'); diff --git a/test/utils/external_ids_matching_test.dart b/test/utils/external_ids_matching_test.dart index 9bccc293..e36a6087 100644 --- a/test/utils/external_ids_matching_test.dart +++ b/test/utils/external_ids_matching_test.dart @@ -2,18 +2,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/utils/external_ids.dart'; void main() { - group('ExternalIds.intersects with Plex Guid arrays', () { - test('verifies a raw Plex Guid array against target ids', () { - final candidate = ExternalIds.fromGuids(const [ - {'id': 'imdb://tt15398776'}, - {'id': 'tmdb://872585'}, - {'id': 'tvdb://287533'}, - ]); - expect(const ExternalIds(tmdb: 872585).intersects(candidate), isTrue); - expect(const ExternalIds(imdb: 'tt0000001').intersects(candidate), isFalse); - }); - }); - group('ExternalIds.intersects', () { test('matches when any shared id form is equal', () { const trakt = ExternalIds(imdb: 'tt0133093', tmdb: 603); diff --git a/test/utils/formatters_test.dart b/test/utils/formatters_test.dart index b3aa19f2..3e2292c4 100644 --- a/test/utils/formatters_test.dart +++ b/test/utils/formatters_test.dart @@ -175,11 +175,5 @@ void main() { expect(formatFullDate('not-a-date'), 'not-a-date'); expect(formatFullDate(''), ''); }); - - test('does not throw for a valid ISO date', () { - // DateFormat may fall back to raw input if intl date symbols aren't - // initialised in the test runner — just verify no crash and string output. - expect(formatFullDate('2024-01-15'), isA()); - }); }); } diff --git a/test/utils/global_key_utils_test.dart b/test/utils/global_key_utils_test.dart index aca5761a..88b6ecb2 100644 --- a/test/utils/global_key_utils_test.dart +++ b/test/utils/global_key_utils_test.dart @@ -11,10 +11,6 @@ void main() { test('allows empty ratingKey', () { expect(buildGlobalKey(ServerId('server'), ''), 'server:'); }); - - test('rejects empty serverId', () { - expect(() => ServerId(''), throwsArgumentError); - }); }); group('parseGlobalKey', () { @@ -48,14 +44,4 @@ void main() { expect(result.ratingKey, ''); }); }); - - test('round-trip build → parse returns original components', () { - for (final pair in const [('s1', '42'), ('serverXYZ', '/library/metadata/123'), ('s', '')]) { - final built = buildGlobalKey(ServerId(pair.$1), pair.$2); - final parsed = parseGlobalKey(built); - expect(parsed, isNotNull); - expect(parsed!.serverId, pair.$1); - expect(parsed.ratingKey, pair.$2); - } - }); } diff --git a/test/utils/grid_size_calculator_test.dart b/test/utils/grid_size_calculator_test.dart index c7247c4f..8e744f69 100644 --- a/test/utils/grid_size_calculator_test.dart +++ b/test/utils/grid_size_calculator_test.dart @@ -3,7 +3,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/services/settings_service.dart' show LibraryDensity; import 'package:plezy/utils/grid_size_calculator.dart'; -import 'package:plezy/utils/layout_constants.dart'; /// Column count the stock [SliverGridDelegateWithMaxCrossAxisExtent] renders for /// [crossAxisExtent]. This is the source of truth that the navigation column @@ -39,13 +38,6 @@ void main() { group('GridSizeCalculator.getColumnCount', () { // crossAxisSpacing is 0 in the current layout constants, so the formula // reduces to ceil(crossAxisExtent / maxCrossAxisExtent). - test('returns 1 when extent equals maxCrossAxisExtent', () { - expect(GridSizeCalculator.getColumnCount(200, 200), 1); - }); - - test('returns 2 when extent slightly exceeds maxCrossAxisExtent', () { - expect(GridSizeCalculator.getColumnCount(201, 200), 2); - }); test('rounds up partial columns', () { // 600 / 200 = 3 exactly @@ -63,15 +55,6 @@ void main() { // 100000 / 100 = 1000 -> clamped to 100 expect(GridSizeCalculator.getColumnCount(100000, 100), 100); }); - - test('uses GridLayoutConstants.crossAxisSpacing in the formula', () { - // The formula adds crossAxisSpacing to the denominator only (matching the - // stock grid delegate), and that constant is currently 0. If it ever - // becomes non-zero, this test forces a rethink. - expect(GridLayoutConstants.crossAxisSpacing, 0); - // Identity-ish: extent = max -> 1 column. - expect(GridSizeCalculator.getColumnCount(200, 200), 1); - }); }); group('GridSizeCalculator.getColumnCount matches the rendered grid', () { @@ -93,13 +76,6 @@ void main() { } }); } - - test('regression: #1288 diagonal case (200px cells, 8px spacing, 1040px wide)', () { - // The old formula gave ceil((1040 + 8) / 208) = 6, but the grid renders - // ceil(1040 / 208) = 5, so "down" jumped a column right. Must be 5. - expect(GridSizeCalculator.getColumnCount(1040, 200, crossAxisSpacing: 8), 5); - expect(_renderedColumnCount(1040, 200, 8), 5); - }); }); group('GridSizeCalculator.isFirstRow / isFirstColumn', () { diff --git a/test/utils/layout_constants_test.dart b/test/utils/layout_constants_test.dart index a8e5e418..0bdfde51 100644 --- a/test/utils/layout_constants_test.dart +++ b/test/utils/layout_constants_test.dart @@ -57,17 +57,5 @@ void main() { expect(ScreenBreakpoints.desktop, 1200); expect(ScreenBreakpoints.largeDesktop, 1600); }); - - test('partitioning: every width matches exactly one of mobile/tablet/desktop/largeDesktop', () { - for (final w in const [0.0, 300, 599.9, 600, 899.9, 1199.9, 1200, 1599.9, 1600, 2500]) { - final matches = [ - ScreenBreakpoints.isMobile(w.toDouble()), - ScreenBreakpoints.isTablet(w.toDouble()) && !ScreenBreakpoints.isDesktopOrLarger(w.toDouble()), - ScreenBreakpoints.isDesktop(w.toDouble()), - ScreenBreakpoints.isLargeDesktop(w.toDouble()), - ].where((b) => b).length; - expect(matches, 1, reason: 'width $w should match exactly one tier'); - } - }); }); } diff --git a/test/utils/media_image_helper_test.dart b/test/utils/media_image_helper_test.dart index 6526af42..85e431cc 100644 --- a/test/utils/media_image_helper_test.dart +++ b/test/utils/media_image_helper_test.dart @@ -93,18 +93,6 @@ void main() { expect(url, contains('h=240')); }); - test('near-minimum slots request a sized transcode', () { - final url = MediaImageHelper.getOptimizedImageUrl( - client: client, - thumbPath: '/library/metadata/1/thumb/2', - maxWidth: 96, - maxHeight: 144, - devicePixelRatio: 1, - ); - - expect(url, startsWith('sized:')); - }); - test('regular slots request DPR-scaled dimensions', () { final url = MediaImageHelper.getOptimizedImageUrl( client: client, diff --git a/test/utils/media_server_http_exception_test.dart b/test/utils/media_server_http_exception_test.dart index d26f079b..b63046be 100644 --- a/test/utils/media_server_http_exception_test.dart +++ b/test/utils/media_server_http_exception_test.dart @@ -143,23 +143,6 @@ void main() { ); }); - test('preserves 500 status and raw body when JSON decoding fails', () async { - final client = MediaServerHttpClient( - baseUrl: 'https://example.test', - client: MockClient((_) async => http.Response('{bad json', 500, headers: {'content-type': 'application/json'})), - ); - addTearDown(client.close); - - await expectLater( - client.get('/System/Info'), - throwsA( - isA() - .having((e) => e.statusCode, 'statusCode', 500) - .having((e) => e.responseData, 'responseData', '{bad json'), - ), - ); - }); - test('preserves 200 status when successful JSON response is malformed', () async { final client = MediaServerHttpClient( baseUrl: 'https://example.test', diff --git a/test/utils/player_utils_test.dart b/test/utils/player_utils_test.dart index 96bfae27..b0ac2c52 100644 --- a/test/utils/player_utils_test.dart +++ b/test/utils/player_utils_test.dart @@ -91,17 +91,6 @@ void main() { ); }); - test('uses native seek near the start of a buffer range', () { - expect( - resolvePlexTranscodeSeekAction( - currentPosition: const Duration(seconds: 30), - target: const Duration(milliseconds: 29500), - bufferRanges: const [BufferRange(start: Duration(seconds: 30), end: Duration(seconds: 50))], - ), - PlexTranscodeSeekAction.nativeSeek, - ); - }); - test('restarts near the tail of a buffer range to avoid optimistic cache edges', () { expect( resolvePlexTranscodeSeekAction( @@ -152,17 +141,6 @@ void main() { ); }); - test('does not treat a flat buffer end as a local seekable range', () { - expect( - resolvePlexTranscodeSeekAction( - currentPosition: const Duration(seconds: 30), - target: const Duration(seconds: 45), - bufferRanges: const [], - ), - PlexTranscodeSeekAction.restartTranscode, - ); - }); - test('restarts large seeks when no buffer information exists', () { expect( resolvePlexTranscodeSeekAction( diff --git a/test/utils/rating_utils_test.dart b/test/utils/rating_utils_test.dart index 26bc9c68..8420e6fc 100644 --- a/test/utils/rating_utils_test.dart +++ b/test/utils/rating_utils_test.dart @@ -59,11 +59,6 @@ void main() { expect(info!.assetPath, 'assets/rating_icons/imdb.svg'); expect(info.formattedValue, '7.5'); }); - - test('formats to one decimal (truncation follows toStringAsFixed semantics)', () { - final info = parseRatingImage('imdb://title', 7.25); - expect(info!.formattedValue, anyOf('7.2', '7.3')); - }); }); group('parseRatingImage - TMDB', () { diff --git a/test/watch_together/playback_state_test.dart b/test/watch_together/playback_state_test.dart index b92539a4..1bd5e9d7 100644 --- a/test/watch_together/playback_state_test.dart +++ b/test/watch_together/playback_state_test.dart @@ -77,10 +77,6 @@ void main() { expect(paused.targetPositionMs(fullState.anchorHostTimeMs + 60000), 90000); }); }); - - test('mediaKey matches mediaKeyFor', () { - expect(fullState.mediaKey, PlaybackState.mediaKeyFor(ratingKey: '12345', serverId: 'srv-1')); - }); }); group('PeerStatus', () { diff --git a/test/watch_together/primitives_test.dart b/test/watch_together/primitives_test.dart index 21002826..100012b2 100644 --- a/test/watch_together/primitives_test.dart +++ b/test/watch_together/primitives_test.dart @@ -2,13 +2,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/watch_together/primitives.dart'; void main() { - test('host and guest derive the same host peer ID', () { - final hostPeerId = watchTogetherHostPeerId('ROOM1'); - final guestExpectedHostPeerId = watchTogetherHostPeerId('room1'); - - expect(guestExpectedHostPeerId, hostPeerId); - }); - test('stored room codes preserve the established host peer wire format', () { const persistedSessionId = 'Ab12z'; diff --git a/test/watch_together/watch_together_controller_test.dart b/test/watch_together/watch_together_controller_test.dart index 0acff94f..70de40c6 100644 --- a/test/watch_together/watch_together_controller_test.dart +++ b/test/watch_together/watch_together_controller_test.dart @@ -209,14 +209,14 @@ void main() { }); }); - test('clock sync runs over the relay and converges', () { + test('guest controller starts clock-sync pings automatically', () { fakeAsync((async) { final room = _Room(async); - // The guest's clock-sync burst pings the host; pongs come back with the - // shared fake clock → offset 0. + // The guest's clock-sync burst starts immediately and sends pings + // through its relay-backed peer service. async.elapse(const Duration(seconds: 2)); - final pongs = room.guestService.outgoingLog.where((m) => m.type == SyncMessageType.ping); - expect(pongs, isNotEmpty); + final pings = room.guestService.outgoingLog.where((m) => m.type == SyncMessageType.ping); + expect(pings, isNotEmpty); room.dispose(); }); }); diff --git a/test/watch_together/watch_together_peer_service_test.dart b/test/watch_together/watch_together_peer_service_test.dart index 65ae3d54..9812fe45 100644 --- a/test/watch_together/watch_together_peer_service_test.dart +++ b/test/watch_together/watch_together_peer_service_test.dart @@ -8,6 +8,21 @@ import 'package:plezy/watch_together/models/sync_message.dart'; typedef _MessageHandler = FutureOr Function(int connection, WebSocket socket, Map message); +Future _withShortenedTimer({ + required Duration original, + required Duration replacement, + required Future Function() body, +}) { + return runZoned( + body, + zoneSpecification: ZoneSpecification( + createTimer: (self, parent, zone, duration, callback) { + return parent.createTimer(zone, duration == original ? replacement : duration, callback); + }, + ), + ); +} + class _RelayServer { _RelayServer._(this._server, this._handler); @@ -147,7 +162,11 @@ void main() { reconnected.complete(); }; - await service.createSession(sessionId: 'room2'); + await _withShortenedTimer( + original: const Duration(seconds: 2), + replacement: const Duration(milliseconds: 10), + body: () => service.createSession(sessionId: 'room2'), + ); await relay.sockets.single.close(); await reconnected.future.timeout(const Duration(seconds: 6)); @@ -167,7 +186,11 @@ void main() { final timeoutService = serviceFor(timeoutRelay); await expectLater( - timeoutService.createSession(sessionId: 'slow1'), + _withShortenedTimer( + original: const Duration(seconds: 10), + replacement: const Duration(milliseconds: 10), + body: () => timeoutService.createSession(sessionId: 'slow1'), + ), throwsA( isA() .having((error) => error.type, 'type', PeerErrorType.timeout) diff --git a/test/watch_together/watch_together_provider_test.dart b/test/watch_together/watch_together_provider_test.dart index 925fd4ac..8c285b95 100644 --- a/test/watch_together/watch_together_provider_test.dart +++ b/test/watch_together/watch_together_provider_test.dart @@ -61,19 +61,10 @@ void main() { expect(p.canControl(), isTrue); p.dispose(); }); - - test('participantEvents is a broadcast stream that listeners can attach to', () async { - final p = WatchTogetherProvider(); - // Attach a listener so the stream is observed; on a fresh provider no - // events will fire, but the stream must already be live. - final sub = p.participantEvents.listen((_) {}); - await sub.cancel(); - p.dispose(); - }); }); - group('WatchTogetherProvider — listener firing via public API', () { - test('setCurrentMedia notifies listeners as host', () { + group('WatchTogetherProvider — session guards', () { + test('setCurrentMedia is rejected outside a session', () { final p = WatchTogetherProvider(); var notified = 0; p.addListener(() => notified++); @@ -84,64 +75,12 @@ void main() { p.dispose(); }); - test('setDisplayName mutates internal state without notifying', () { - final p = WatchTogetherProvider(); - var notified = 0; - p.addListener(() => notified++); - // setDisplayName is a plain assignment with no notify; verify it doesn't - // accidentally fire one. - p.setDisplayName('Tester'); - expect(notified, 0); - p.dispose(); - }); - - test('markCurrentPlaybackHandled does not throw on a fresh provider', () { - final p = WatchTogetherProvider(); - expect(() => p.markCurrentPlaybackHandled(ratingKey: 'rk1', serverId: ServerId('s1')), returnsNormally); - p.dispose(); - }); - - test('requestCurrentPlaybackSnapshot is a no-op when not in session', () { - final p = WatchTogetherProvider(); - var notified = 0; - p.addListener(() => notified++); - // Guard fires before any peer service work, so no listener notification. - p.requestCurrentPlaybackSnapshot(); - expect(notified, 0); - p.dispose(); - }); - - test('attachPlayer is a no-op without a sync controller (logs warning)', () { - final p = WatchTogetherProvider(); - // The mpv Player object is platform-tied; skipping it would reach the - // null-controller guard first and bail. Calling with a null check via - // the same path used by the production code: just verify the early - // return path on detachPlayer (which is also null-safe). - expect(p.detachPlayer, returnsNormally); - p.dispose(); - }); - - test('setBackgrounded forwards to the sync controller but is null-safe', () { + test('setBackgrounded is null-safe without a sync controller', () { final p = WatchTogetherProvider(); expect(() => p.setBackgrounded(true), returnsNormally); expect(() => p.setBackgrounded(false), returnsNormally); p.dispose(); }); - - test('onLocalSeek is null-safe without a sync controller', () { - final p = WatchTogetherProvider(); - expect(() => p.onLocalSeek(const Duration(seconds: 5)), returnsNormally); - p.dispose(); - }); - - test('notifyHostExitedPlayer is a no-op when not host or not in session', () { - final p = WatchTogetherProvider(); - var notified = 0; - p.addListener(() => notified++); - p.notifyHostExitedPlayer(); - expect(notified, 0); - p.dispose(); - }); }); group('WatchTogetherProvider — media switch dispatch', () { @@ -271,28 +210,7 @@ void main() { }); }); - group('WatchTogetherProvider — leaveSession safety', () { - test('leaveSession on a fresh provider is a no-op (no notify)', () async { - final p = WatchTogetherProvider(); - var notified = 0; - p.addListener(() => notified++); - await p.leaveSession(); - // Early-return path: no session ever existed, no listener fires. - expect(notified, 0); - expect(p.session, isNull); - p.dispose(); - }); - }); - group('WatchTogetherProvider — dispose hygiene', () { - test('dispose runs cleanly with no peer service or subscriptions', () { - final p = WatchTogetherProvider(); - // Fresh provider: 4 stream subscriptions are all null, 1 stream - // controller is open, _hostReconnectTimer is null. dispose() must - // close the controller and tear down without throwing. - expect(p.dispose, returnsNormally); - }); - test('participantEvents stream is closed after dispose', () async { final p = WatchTogetherProvider(); // Attach a listener; capture done via the stream's done future. @@ -305,23 +223,5 @@ void main() { await sub.cancel(); expect(streamDone, isTrue); }); - - test('notifyListeners after dispose does not throw (coalescing guard)', () async { - // The provider overrides notifyListeners to coalesce into a microtask. - // After dispose, the _disposed flag must short-circuit any pending or - // late notifications. - final p = WatchTogetherProvider(); - p.dispose(); - // Even if some pathway tried to notify (it won't from outside, but the - // microtask path in the override is the relevant guard), it must not - // throw and not call super.notifyListeners() on a disposed instance. - await Future.delayed(Duration.zero); - }); - - test('dispose is safe to call after a leaveSession on a fresh provider', () async { - final p = WatchTogetherProvider(); - await p.leaveSession(); - expect(p.dispose, returnsNormally); - }); }); } diff --git a/test/widgets/focusable_text_field_test.dart b/test/widgets/focusable_text_field_test.dart index 9dd43641..b42dd9bb 100644 --- a/test/widgets/focusable_text_field_test.dart +++ b/test/widgets/focusable_text_field_test.dart @@ -69,51 +69,6 @@ void main() { expect(selects, 1); }); - testWidgets('d-pad direction handlers are installed on the text field focus node', (tester) async { - final controller = TextEditingController(); - final fieldFocusNode = FocusNode(debugLabel: 'name_field'); - final nextFocusNode = FocusNode(debugLabel: 'next_button'); - addTearDown(controller.dispose); - addTearDown(fieldFocusNode.dispose); - addTearDown(nextFocusNode.dispose); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Column( - children: [ - FocusableTextField( - controller: controller, - focusNode: fieldFocusNode, - onNavigateDown: nextFocusNode.requestFocus, - ), - FilledButton(focusNode: nextFocusNode, onPressed: () {}, child: const Text('Next')), - ], - ), - ), - ), - ); - - fieldFocusNode.requestFocus(); - await tester.pump(); - final handler = fieldFocusNode.onKeyEvent; - - expect(handler, isNotNull); - final result = handler!( - fieldFocusNode, - const KeyDownEvent( - physicalKey: PhysicalKeyboardKey.arrowDown, - logicalKey: LogicalKeyboardKey.arrowDown, - timeStamp: Duration.zero, - deviceType: ui.KeyEventDeviceType.directionalPad, - ), - ); - await tester.pump(); - - expect(result, KeyEventResult.handled); - expect(nextFocusNode.hasPrimaryFocus, isTrue); - }); - testWidgets('existing focus node key handler is preserved before text field navigation', (tester) async { final controller = TextEditingController(); final handledKeys = []; @@ -179,28 +134,6 @@ void main() { expect(nextFocusNode.hasPrimaryFocus, isTrue); }); - testWidgets('tvOS focus opens virtual keyboard', (tester) async { - TvDetectionService.debugSetAppleTVOverride(true); - await _setTvSurfaceSize(tester); - final controller = TextEditingController(); - final fieldFocusNode = FocusNode(debugLabel: 'search_field'); - addTearDown(controller.dispose); - addTearDown(fieldFocusNode.dispose); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: FocusableTextField(controller: controller, focusNode: fieldFocusNode), - ), - ), - ); - - fieldFocusNode.requestFocus(); - await tester.pumpAndSettle(); - - expect(find.byType(Dialog), findsOneWidget); - }); - testWidgets('hidden TV text field does not auto-open virtual keyboard', (tester) async { TvDetectionService.debugSetAppleTVOverride(true); await _setTvSurfaceSize(tester); diff --git a/test/widgets/tv_browse_rail_test.dart b/test/widgets/tv_browse_rail_test.dart index 9c4befe7..d52009dc 100644 --- a/test/widgets/tv_browse_rail_test.dart +++ b/test/widgets/tv_browse_rail_test.dart @@ -665,38 +665,6 @@ void main() { expect(find.byType(CompositedTransformFollower), findsOneWidget); }); - testWidgets('detailed card layout can still show media text', (tester) async { - await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, false); - - final serverManager = MultiServerManager(); - final movie = testMediaItem( - id: 'movie_1', - backend: MediaBackend.plex, - kind: MediaKind.movie, - title: 'Visible Movie', - ); - final hub = MediaHub(id: 'movies', title: 'Movies', type: 'movie', items: [movie], size: 1); - - await tester.pumpWidget( - ChangeNotifierProvider( - create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)), - child: MaterialApp( - theme: monoTheme(dark: true), - home: Scaffold( - body: SizedBox( - width: 1280, - height: 720, - child: TvBrowseRail(hubs: [hub], iconForHub: (_, _) => Icons.movie_rounded), - ), - ), - ), - ), - ); - await tester.pump(); - - expect(find.text('Visible Movie'), findsOneWidget); - }); - testWidgets('detailed card focus border hugs the poster, captions outside', (tester) async { await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, false); TvDetectionService.debugSetAppleTVOverride(true); diff --git a/test/widgets/video_controls_test.dart b/test/widgets/video_controls_test.dart index eda90a0b..ff31bbb3 100644 --- a/test/widgets/video_controls_test.dart +++ b/test/widgets/video_controls_test.dart @@ -703,46 +703,6 @@ void main() { PlayerBackDisposition.exitPlayer, ); }); - - test('macOS physical Escape uses the same staged disposition as semantic Back', () { - expect( - resolvePlayerBackDisposition( - navigationKey: PlayerNavigationKey.physicalEscape, - contentStripVisible: false, - controlsVisible: true, - physicalEscapeExitsFullscreen: false, - ), - PlayerBackDisposition.hideControls, - ); - expect( - resolvePlayerBackDisposition( - navigationKey: PlayerNavigationKey.physicalEscape, - contentStripVisible: false, - controlsVisible: false, - physicalEscapeExitsFullscreen: false, - ), - PlayerBackDisposition.exitPlayer, - ); - }); - - test('semantic Back hides visible controls then exits when hidden', () { - expect( - resolvePlayerBackDisposition( - navigationKey: PlayerNavigationKey.back, - contentStripVisible: false, - controlsVisible: true, - ), - PlayerBackDisposition.hideControls, - ); - expect( - resolvePlayerBackDisposition( - navigationKey: PlayerNavigationKey.back, - contentStripVisible: false, - controlsVisible: false, - ), - PlayerBackDisposition.exitPlayer, - ); - }); }); group('SkipMarkerButton', () { diff --git a/test/widgets/video_settings_sheet_test.dart b/test/widgets/video_settings_sheet_test.dart index ffce695a..6a89c81a 100644 --- a/test/widgets/video_settings_sheet_test.dart +++ b/test/widgets/video_settings_sheet_test.dart @@ -6,7 +6,6 @@ import 'package:plezy/mpv/player/player_state.dart'; import 'package:plezy/mpv/player/player_streams.dart'; import 'package:plezy/services/settings_service.dart'; import 'package:plezy/theme/mono_tokens.dart'; -import 'package:plezy/utils/platform_detector.dart'; import 'package:plezy/widgets/video_controls/sheets/video_settings_sheet.dart'; import '../test_helpers/prefs.dart'; @@ -35,11 +34,6 @@ void main() { resetSharedPreferencesForTest(); SettingsService.resetForTesting(); await SettingsService.getInstance(); - TvDetectionService.debugSetAppleTVOverride(null); - }); - - tearDown(() { - TvDetectionService.debugSetAppleTVOverride(null); }); testWidgets('shows audio passthrough on supported TV-style surfaces', (tester) async { @@ -49,15 +43,6 @@ void main() { expect(find.text('Audio Passthrough'), findsOneWidget); }); - - testWidgets('shows audio passthrough on Apple TV', (tester) async { - TvDetectionService.debugSetAppleTVOverride(true); - - await _pumpSheet(tester); - await tester.scrollUntilVisible(find.text('Audio Passthrough'), 500, scrollable: find.byType(Scrollable).first); - - expect(find.text('Audio Passthrough'), findsOneWidget); - }); } Future _pumpSheet(WidgetTester tester) async {