test: extract shared fixtures and scaffolds
Collapse duplicated setup across the suite into six shared helpers under test/test_helpers/ and rewrite the 28 suites that were open-coding it: http_fixtures.dart jsonResponse() for http.Response JSON stubs library_tab_scaffold.dart pumps library tabs under their required ancestors multi_server_fixtures.dart MultiServerProvider wiring for widget tests playback_report_fakes.dart PlaybackReportCall + fake report sinks profile_stack.dart production-shaped profile dependency graph theme.dart testMonoTokens for fast-settling widget tests Net -1245 lines with no change in coverage or assertions.
This commit is contained in:
@@ -18,13 +18,13 @@ import 'package:plezy/profiles/profile_connection.dart';
|
||||
import 'package:plezy/profiles/profile_connection_registry.dart';
|
||||
import 'package:plezy/profiles/profile_registry.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_auth_service.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
import 'package:plezy/utils/media_server_http_client.dart';
|
||||
import 'package:plezy/utils/media_server_timeouts.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
/// Poll [condition] until it holds, failing after [timeout]. Used to observe
|
||||
@@ -74,7 +74,7 @@ void main() {
|
||||
storage: storage,
|
||||
);
|
||||
manager = MultiServerManager();
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
shouldDeferInitialBind = false;
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
@@ -193,7 +193,7 @@ void main() {
|
||||
|
||||
final failingManager = _FailingPlexMultiServerManager();
|
||||
manager = failingManager;
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
@@ -247,7 +247,7 @@ void main() {
|
||||
|
||||
final mixedManager = _BlockingMixedMultiServerManager();
|
||||
manager = mixedManager;
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
@@ -349,7 +349,7 @@ void main() {
|
||||
|
||||
final capturingManager = _CapturingMultiServerManager();
|
||||
manager = capturingManager;
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
@@ -591,7 +591,7 @@ void main() {
|
||||
|
||||
final recoveringManager = _RecordingPlexManager();
|
||||
manager = recoveringManager;
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
@@ -644,7 +644,7 @@ void main() {
|
||||
multiServerProvider.dispose();
|
||||
|
||||
manager = testManager ?? _CapturingMultiServerManager();
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
@@ -809,7 +809,7 @@ void main() {
|
||||
|
||||
final gated = _GatedJellyfinManager();
|
||||
manager = gated;
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
@@ -849,7 +849,7 @@ void main() {
|
||||
|
||||
final gated = _GatedJellyfinManager();
|
||||
manager = gated;
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
@@ -890,7 +890,7 @@ void main() {
|
||||
|
||||
final failing = _CountingFailingJellyfinManager();
|
||||
manager = failing;
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
@@ -929,7 +929,7 @@ void main() {
|
||||
|
||||
var pinPrompts = 0;
|
||||
manager = _CapturingMultiServerManager();
|
||||
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServerProvider = testMultiServerProvider(manager);
|
||||
binder = ActiveProfileBinder(
|
||||
activeProfile: activeProfile,
|
||||
connections: connections,
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/connection/connection.dart';
|
||||
import 'package:plezy/connection/connection_registry.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/i18n/strings.g.dart';
|
||||
import 'package:plezy/models/plex/plex_home.dart';
|
||||
import 'package:plezy/models/plex/plex_home_user.dart';
|
||||
import 'package:plezy/models/companion_remote/remote_command.dart';
|
||||
import 'package:plezy/models/companion_remote/remote_session.dart';
|
||||
import 'package:plezy/profiles/active_profile_provider.dart';
|
||||
import 'package:plezy/profiles/plex_home_service.dart';
|
||||
import 'package:plezy/profiles/profile.dart';
|
||||
import 'package:plezy/profiles/profile_connection.dart';
|
||||
import 'package:plezy/profiles/profile_connection_registry.dart';
|
||||
import 'package:plezy/profiles/profile_registry.dart';
|
||||
import 'package:plezy/providers/companion_remote_provider.dart';
|
||||
import 'package:plezy/services/companion_remote/companion_remote_peer_service.dart';
|
||||
import 'package:plezy/services/companion_remote/lan_discovery_service.dart';
|
||||
import 'package:plezy/services/companion_remote/remote_auth_context.dart';
|
||||
import 'package:plezy/services/companion_remote/remote_auth_service.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/profile_stack.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -473,69 +466,48 @@ void main() {
|
||||
});
|
||||
|
||||
test('ensureCryptoReady rebuilds when the active profile/account changes', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
});
|
||||
final stack = await ProfileStack.create();
|
||||
addTearDown(stack.dispose);
|
||||
|
||||
final accountA = _plexAccount('plex-a', 'client-a');
|
||||
final accountB = _plexAccount('plex-b', 'client-b');
|
||||
final profileA = _localProfile('profile-a');
|
||||
final profileB = _localProfile('profile-b');
|
||||
await connections.upsert(accountA);
|
||||
await connections.upsert(accountB);
|
||||
await profiles.upsert(profileA);
|
||||
await profiles.upsert(profileB);
|
||||
await profileConnections.upsert(
|
||||
await stack.connections.upsert(accountA);
|
||||
await stack.connections.upsert(accountB);
|
||||
await stack.profiles.upsert(profileA);
|
||||
await stack.profiles.upsert(profileB);
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profileA.id, connectionId: accountA.id, userIdentifier: 'admin-a'),
|
||||
makeDefault: true,
|
||||
);
|
||||
await profileConnections.upsert(
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profileB.id, connectionId: accountB.id, userIdentifier: 'admin-b'),
|
||||
makeDefault: true,
|
||||
);
|
||||
await storage.setActiveProfileId(profileA.id);
|
||||
await active.initialize();
|
||||
await stack.storage.setActiveProfileId(profileA.id);
|
||||
await stack.active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider();
|
||||
addTearDown(provider.dispose);
|
||||
final okA = await provider.ensureCryptoReady(
|
||||
_home('admin-a'),
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
account: accountA,
|
||||
);
|
||||
expect(okA, isTrue);
|
||||
expect(provider.debugCryptoConnectionId, accountA.id);
|
||||
expect(provider.debugCryptoProfileId, profileA.id);
|
||||
|
||||
await active.activate(profileB);
|
||||
await stack.active.activate(profileB);
|
||||
|
||||
final ok = await provider.ensureCryptoReady(
|
||||
_home('admin-b'),
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
account: accountB,
|
||||
);
|
||||
|
||||
@@ -545,50 +517,29 @@ void main() {
|
||||
});
|
||||
|
||||
test('ensureCryptoReady uses the active local profile Plex row', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
});
|
||||
final stack = await ProfileStack.create();
|
||||
addTearDown(stack.dispose);
|
||||
|
||||
final accountA = _plexAccount('plex-a', 'client-a');
|
||||
final accountB = _plexAccount('plex-b', 'client-b');
|
||||
final profile = _localProfile('profile-local');
|
||||
await connections.upsert(accountA);
|
||||
await connections.upsert(accountB);
|
||||
await profiles.upsert(profile);
|
||||
await profileConnections.upsert(
|
||||
await stack.connections.upsert(accountA);
|
||||
await stack.connections.upsert(accountB);
|
||||
await stack.profiles.upsert(profile);
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profile.id, connectionId: accountB.id, userIdentifier: 'child-b', isDefault: true),
|
||||
makeDefault: true,
|
||||
);
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await active.initialize();
|
||||
await stack.storage.setActiveProfileId(profile.id);
|
||||
await stack.active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider();
|
||||
addTearDown(provider.dispose);
|
||||
final ok = await provider.ensureCryptoReady(
|
||||
_homeWithUsers('admin-b', ['child-b']),
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
);
|
||||
|
||||
expect(ok, isTrue);
|
||||
@@ -598,48 +549,27 @@ void main() {
|
||||
});
|
||||
|
||||
test('ensureCryptoReady uses the active local profile Jellyfin row', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
});
|
||||
final stack = await ProfileStack.create();
|
||||
addTearDown(stack.dispose);
|
||||
|
||||
final jellyfin = _jellyfinConnection('jf-a');
|
||||
final profile = _localProfile('profile-jf');
|
||||
await connections.upsert(jellyfin);
|
||||
await profiles.upsert(profile);
|
||||
await profileConnections.upsert(
|
||||
await stack.connections.upsert(jellyfin);
|
||||
await stack.profiles.upsert(profile);
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profile.id, connectionId: jellyfin.id, userIdentifier: jellyfin.userId),
|
||||
makeDefault: true,
|
||||
);
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await active.initialize();
|
||||
await stack.storage.setActiveProfileId(profile.id);
|
||||
await stack.active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider();
|
||||
addTearDown(provider.dispose);
|
||||
final ok = await provider.ensureCryptoReady(
|
||||
null,
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
);
|
||||
|
||||
expect(ok, isTrue);
|
||||
@@ -649,54 +579,33 @@ void main() {
|
||||
});
|
||||
|
||||
test('ensureCryptoReady includes every active local profile remote identity', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
});
|
||||
final stack = await ProfileStack.create();
|
||||
addTearDown(stack.dispose);
|
||||
|
||||
final account = _plexAccount('plex-a', 'client-a');
|
||||
final jellyfin = _jellyfinConnection('jf-a');
|
||||
final profile = _localProfile('profile-mixed');
|
||||
final home = _homeWithUsers('admin-a', ['child-a']);
|
||||
await connections.upsert(account);
|
||||
await connections.upsert(jellyfin);
|
||||
await profiles.upsert(profile);
|
||||
await profileConnections.upsert(
|
||||
await stack.connections.upsert(account);
|
||||
await stack.connections.upsert(jellyfin);
|
||||
await stack.profiles.upsert(profile);
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profile.id, connectionId: jellyfin.id, userIdentifier: jellyfin.userId),
|
||||
makeDefault: true,
|
||||
);
|
||||
await profileConnections.upsert(
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profile.id, connectionId: account.id, userIdentifier: 'child-a'),
|
||||
);
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await active.initialize();
|
||||
await stack.storage.setActiveProfileId(profile.id);
|
||||
await stack.active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider();
|
||||
addTearDown(provider.dispose);
|
||||
final ok = await provider.ensureCryptoReady(
|
||||
home,
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
plexHomeForConnection: (_) async => home,
|
||||
);
|
||||
|
||||
@@ -706,41 +615,20 @@ void main() {
|
||||
});
|
||||
|
||||
test('ensureCryptoReady does not fall back to an account without an active profile', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
});
|
||||
final stack = await ProfileStack.create();
|
||||
addTearDown(stack.dispose);
|
||||
|
||||
await connections.upsert(_plexAccount('plex-a', 'client-a'));
|
||||
await profiles.upsert(_localProfile('profile-a'));
|
||||
await active.initialize();
|
||||
await stack.connections.upsert(_plexAccount('plex-a', 'client-a'));
|
||||
await stack.profiles.upsert(_localProfile('profile-a'));
|
||||
await stack.active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider();
|
||||
addTearDown(provider.dispose);
|
||||
final ok = await provider.ensureCryptoReady(
|
||||
_home('admin-a'),
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
);
|
||||
|
||||
expect(ok, isFalse);
|
||||
@@ -748,48 +636,27 @@ void main() {
|
||||
});
|
||||
|
||||
test('resetForLogout clears crypto context', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
});
|
||||
final stack = await ProfileStack.create();
|
||||
addTearDown(stack.dispose);
|
||||
|
||||
final account = _plexAccount('plex-a', 'client-a');
|
||||
final profile = _localProfile('profile-a');
|
||||
await connections.upsert(account);
|
||||
await profiles.upsert(profile);
|
||||
await profileConnections.upsert(
|
||||
await stack.connections.upsert(account);
|
||||
await stack.profiles.upsert(profile);
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profile.id, connectionId: account.id, userIdentifier: 'admin-a'),
|
||||
makeDefault: true,
|
||||
);
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await active.initialize();
|
||||
await stack.storage.setActiveProfileId(profile.id);
|
||||
await stack.active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider();
|
||||
addTearDown(provider.dispose);
|
||||
await provider.ensureCryptoReady(
|
||||
_home('admin-a'),
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
account: account,
|
||||
);
|
||||
expect(provider.isCryptoReady, isTrue);
|
||||
@@ -1061,46 +928,28 @@ class _FakeLanDiscoveryService extends LanDiscoveryService {
|
||||
}
|
||||
|
||||
class _RemoteHarness {
|
||||
_RemoteHarness({required this.provider, required this.database, required this.activeProfile, required this.plexHome});
|
||||
_RemoteHarness({required this.provider, required this.stack});
|
||||
|
||||
final CompanionRemoteProvider provider;
|
||||
final AppDatabase database;
|
||||
final ActiveProfileProvider activeProfile;
|
||||
final PlexHomeService plexHome;
|
||||
final ProfileStack stack;
|
||||
bool _closed = false;
|
||||
|
||||
static Future<_RemoteHarness> create(
|
||||
CompanionRemotePeerServiceFactory peerServiceFactory, {
|
||||
LanDiscoveryServiceFactory discoveryServiceFactory = LanDiscoveryService.new,
|
||||
}) async {
|
||||
final database = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(database);
|
||||
final profileConnections = ProfileConnectionRegistry(database);
|
||||
final profiles = ProfileRegistry(database);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final activeProfile = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
final stack = await ProfileStack.create();
|
||||
|
||||
final account = _plexAccount('remote-account', 'remote-client');
|
||||
final profile = _localProfile('remote-profile');
|
||||
await connections.upsert(account);
|
||||
await profiles.upsert(profile);
|
||||
await profileConnections.upsert(
|
||||
await stack.connections.upsert(account);
|
||||
await stack.profiles.upsert(profile);
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profile.id, connectionId: account.id, userIdentifier: 'remote-admin'),
|
||||
makeDefault: true,
|
||||
);
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await activeProfile.initialize();
|
||||
await stack.storage.setActiveProfileId(profile.id);
|
||||
await stack.active.initialize();
|
||||
|
||||
final provider = CompanionRemoteProvider.forTesting(
|
||||
peerServiceFactory: peerServiceFactory,
|
||||
@@ -1108,25 +957,22 @@ class _RemoteHarness {
|
||||
);
|
||||
final ready = await provider.ensureCryptoReady(
|
||||
_home('remote-admin'),
|
||||
connections: connections,
|
||||
activeProfile: activeProfile,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
account: account,
|
||||
);
|
||||
if (!ready) {
|
||||
throw StateError('Remote test harness failed to initialize crypto');
|
||||
}
|
||||
|
||||
return _RemoteHarness(provider: provider, database: database, activeProfile: activeProfile, plexHome: plexHome);
|
||||
return _RemoteHarness(provider: provider, stack: stack);
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
if (_closed) return;
|
||||
_closed = true;
|
||||
if (!provider.isDisposed) provider.dispose();
|
||||
await activeProfile.resetForTesting();
|
||||
activeProfile.dispose();
|
||||
await plexHome.dispose();
|
||||
await database.close();
|
||||
await stack.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/connection/connection.dart';
|
||||
import 'package:plezy/connection/connection_registry.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/models/plex/plex_home_user.dart';
|
||||
import 'package:plezy/profiles/active_profile_provider.dart';
|
||||
import 'package:plezy/profiles/plex_home_service.dart';
|
||||
import 'package:plezy/profiles/profile.dart';
|
||||
import 'package:plezy/profiles/profile_connection.dart';
|
||||
import 'package:plezy/profiles/profile_connection_registry.dart';
|
||||
import 'package:plezy/profiles/profile_registry.dart';
|
||||
import 'package:plezy/providers/user_profile_provider.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_auth_service.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
import 'package:plezy/utils/media_server_http_client.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/profile_stack.dart';
|
||||
|
||||
void main() {
|
||||
setUp(resetSharedPreferencesForTest);
|
||||
@@ -57,30 +50,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('settings connection follows the profile default row', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
final stack = await ProfileStack.create();
|
||||
final manager = MultiServerManager();
|
||||
addTearDown(() async {
|
||||
manager.dispose();
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
await stack.dispose();
|
||||
});
|
||||
|
||||
final profile = Profile.local(id: 'local-owner', displayName: 'Owner', createdAt: DateTime(2026, 1, 1));
|
||||
@@ -102,10 +76,10 @@ void main() {
|
||||
deviceId: 'device-a',
|
||||
createdAt: DateTime(2026, 1, 1),
|
||||
);
|
||||
await profiles.upsert(profile);
|
||||
await connections.upsert(plex);
|
||||
await connections.upsert(jellyfin);
|
||||
await profileConnections.upsert(
|
||||
await stack.profiles.upsert(profile);
|
||||
await stack.connections.upsert(plex);
|
||||
await stack.connections.upsert(jellyfin);
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(
|
||||
profileId: profile.id,
|
||||
connectionId: plex.id,
|
||||
@@ -115,53 +89,36 @@ void main() {
|
||||
),
|
||||
makeDefault: true,
|
||||
);
|
||||
await profileConnections.upsert(
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(profileId: profile.id, connectionId: jellyfin.id, userIdentifier: jellyfin.userId),
|
||||
);
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await active.initialize();
|
||||
await stack.storage.setActiveProfileId(profile.id);
|
||||
await stack.active.initialize();
|
||||
|
||||
final p = UserProfileProvider()
|
||||
..attach(
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
serverManager: manager,
|
||||
);
|
||||
addTearDown(p.dispose);
|
||||
|
||||
expect(await p.debugResolveActiveSettingsConnectionForTesting(), isA<PlexAccountConnection>());
|
||||
|
||||
await profileConnections.setDefault(profile.id, jellyfin.id);
|
||||
await stack.profileConnections.setDefault(profile.id, jellyfin.id);
|
||||
|
||||
expect(await p.debugResolveActiveSettingsConnectionForTesting(), isA<JellyfinConnection>());
|
||||
});
|
||||
|
||||
test('watches Plex Home profile connection rows', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => [_homeUser(uuid: 'home-user-a', title: 'Home User')],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
final stack = await ProfileStack.create(
|
||||
homeUsers: [_homeUser(uuid: 'home-user-a', title: 'Home User')],
|
||||
);
|
||||
final manager = MultiServerManager();
|
||||
addTearDown(() async {
|
||||
manager.dispose();
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
await stack.dispose();
|
||||
});
|
||||
|
||||
final account = PlexAccountConnection(
|
||||
@@ -171,21 +128,23 @@ void main() {
|
||||
accountLabel: 'Plex A',
|
||||
createdAt: DateTime(2026, 1, 1),
|
||||
);
|
||||
await connections.upsert(account);
|
||||
await plexHome.refresh(account);
|
||||
await storage.setActiveProfileId(plexHomeProfileId(accountConnectionId: account.id, homeUserUuid: 'home-user-a'));
|
||||
await active.initialize();
|
||||
await stack.connections.upsert(account);
|
||||
await stack.plexHome.refresh(account);
|
||||
await stack.storage.setActiveProfileId(
|
||||
plexHomeProfileId(accountConnectionId: account.id, homeUserUuid: 'home-user-a'),
|
||||
);
|
||||
await stack.active.initialize();
|
||||
|
||||
final p = UserProfileProvider()
|
||||
..attach(
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
serverManager: manager,
|
||||
);
|
||||
addTearDown(p.dispose);
|
||||
|
||||
expect(p.debugWatchedProfileConnectionProfileId, active.activeId);
|
||||
expect(p.debugWatchedProfileConnectionProfileId, stack.active.activeId);
|
||||
});
|
||||
|
||||
test('Plex Home profile without a switched token makes no user request', () async {
|
||||
@@ -231,30 +190,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('Plex token fallback uses the selected local profile account', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
);
|
||||
final stack = await ProfileStack.create();
|
||||
final manager = MultiServerManager();
|
||||
addTearDown(() async {
|
||||
manager.dispose();
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
await stack.dispose();
|
||||
});
|
||||
|
||||
final profile = Profile.local(id: 'local-owner', displayName: 'Owner', createdAt: DateTime(2026, 1, 1));
|
||||
@@ -272,10 +212,10 @@ void main() {
|
||||
accountLabel: 'Plex B',
|
||||
createdAt: DateTime(2026, 1, 1),
|
||||
);
|
||||
await profiles.upsert(profile);
|
||||
await connections.upsert(accountA);
|
||||
await connections.upsert(accountB);
|
||||
await profileConnections.upsert(
|
||||
await stack.profiles.upsert(profile);
|
||||
await stack.connections.upsert(accountA);
|
||||
await stack.connections.upsert(accountB);
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(
|
||||
profileId: profile.id,
|
||||
connectionId: accountB.id,
|
||||
@@ -284,8 +224,8 @@ void main() {
|
||||
),
|
||||
makeDefault: true,
|
||||
);
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await active.initialize();
|
||||
await stack.storage.setActiveProfileId(profile.id);
|
||||
await stack.active.initialize();
|
||||
|
||||
final requests = <http.Request>[];
|
||||
final auth = _recordingAuth(requests, audioLanguage: 'fra');
|
||||
@@ -293,9 +233,9 @@ void main() {
|
||||
|
||||
final p = UserProfileProvider(authService: auth)
|
||||
..attach(
|
||||
connections: connections,
|
||||
activeProfile: active,
|
||||
profileConnections: profileConnections,
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
serverManager: manager,
|
||||
);
|
||||
addTearDown(p.dispose);
|
||||
@@ -356,39 +296,16 @@ PlexAuthService _recordingAuth(List<http.Request> requests, {required String aud
|
||||
}
|
||||
|
||||
class _HomeProfileFixture {
|
||||
_HomeProfileFixture({
|
||||
required this.db,
|
||||
required this.active,
|
||||
required this.plexHome,
|
||||
required this.auth,
|
||||
required this.provider,
|
||||
required this.requests,
|
||||
});
|
||||
_HomeProfileFixture({required this.stack, required this.auth, required this.provider, required this.requests});
|
||||
|
||||
final AppDatabase db;
|
||||
final ActiveProfileProvider active;
|
||||
final PlexHomeService plexHome;
|
||||
final ProfileStack stack;
|
||||
final PlexAuthService auth;
|
||||
final UserProfileProvider provider;
|
||||
final List<http.Request> requests;
|
||||
|
||||
static Future<_HomeProfileFixture> create({String? switchedToken}) async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final profiles = ProfileRegistry(db);
|
||||
final storage = await StorageService.getInstance();
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => [_homeUser(uuid: 'home-user-a', title: 'Home User')],
|
||||
);
|
||||
final active = ActiveProfileProvider(
|
||||
registry: profiles,
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
storage: storage,
|
||||
final stack = await ProfileStack.create(
|
||||
homeUsers: [_homeUser(uuid: 'home-user-a', title: 'Home User')],
|
||||
);
|
||||
final account = PlexAccountConnection(
|
||||
id: 'plex-parent',
|
||||
@@ -397,12 +314,12 @@ class _HomeProfileFixture {
|
||||
accountLabel: 'Plex Parent',
|
||||
createdAt: DateTime(2026, 1, 1),
|
||||
);
|
||||
await connections.upsert(account);
|
||||
await plexHome.refresh(account);
|
||||
await stack.connections.upsert(account);
|
||||
await stack.plexHome.refresh(account);
|
||||
|
||||
final activeId = plexHomeProfileId(accountConnectionId: account.id, homeUserUuid: 'home-user-a');
|
||||
if (switchedToken != null) {
|
||||
await profileConnections.upsert(
|
||||
await stack.profileConnections.upsert(
|
||||
ProfileConnection(
|
||||
profileId: activeId,
|
||||
connectionId: account.id,
|
||||
@@ -413,29 +330,23 @@ class _HomeProfileFixture {
|
||||
makeDefault: true,
|
||||
);
|
||||
}
|
||||
await storage.setActiveProfileId(activeId);
|
||||
await active.initialize();
|
||||
await stack.storage.setActiveProfileId(activeId);
|
||||
await stack.active.initialize();
|
||||
|
||||
final requests = <http.Request>[];
|
||||
final auth = _recordingAuth(requests, audioLanguage: 'jpn');
|
||||
final provider = UserProfileProvider(authService: auth)
|
||||
..attach(connections: connections, activeProfile: active, profileConnections: profileConnections);
|
||||
return _HomeProfileFixture(
|
||||
db: db,
|
||||
active: active,
|
||||
plexHome: plexHome,
|
||||
auth: auth,
|
||||
provider: provider,
|
||||
requests: requests,
|
||||
);
|
||||
..attach(
|
||||
connections: stack.connections,
|
||||
activeProfile: stack.active,
|
||||
profileConnections: stack.profileConnections,
|
||||
);
|
||||
return _HomeProfileFixture(stack: stack, auth: auth, provider: provider, requests: requests);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
provider.dispose();
|
||||
auth.dispose();
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
await stack.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/focus/focusable_button.dart';
|
||||
import 'package:plezy/focus/input_mode_tracker.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/navigation/main_screen_scope.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/state_messages.dart';
|
||||
import 'package:plezy/screens/libraries/tabs/library_browse_tab.dart';
|
||||
@@ -20,13 +18,12 @@ import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:plezy/widgets/focusable_filter_chip.dart';
|
||||
import 'package:plezy/widgets/media_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/backend_client_fixtures.dart';
|
||||
import '../../test_helpers/library_tab_scaffold.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
final _musicLibrary = MediaLibrary(
|
||||
@@ -88,7 +85,7 @@ void main() {
|
||||
expect(retry.focusNode!.hasFocus, isTrue);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await _pumpRequestFrames(tester);
|
||||
await pumpRequestFrames(tester);
|
||||
|
||||
expect(harness.browseRequestCount, 2);
|
||||
expect(find.byType(ErrorStateWidget), findsNothing);
|
||||
@@ -115,52 +112,17 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> _pumpBrowseTab(WidgetTester tester, _MusicBrowseHarness harness, {MediaLibrary? library}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(1280, 720);
|
||||
addTearDown(() {
|
||||
tester.view.resetDevicePixelRatio();
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
value: harness.provider,
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: 0,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, _) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: const SliverToBoxAdapter(child: SizedBox(height: 1)),
|
||||
),
|
||||
],
|
||||
body: LibraryBrowseTab(
|
||||
library: library ?? _musicLibrary,
|
||||
canGroupByFolders: true,
|
||||
suppressAutoFocus: true,
|
||||
onBack: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
await pumpLibraryTab(
|
||||
tester,
|
||||
provider: harness.provider,
|
||||
tab: LibraryBrowseTab(
|
||||
library: library ?? _musicLibrary,
|
||||
canGroupByFolders: true,
|
||||
suppressAutoFocus: true,
|
||||
onBack: () {},
|
||||
),
|
||||
);
|
||||
await _pumpRequestFrames(tester);
|
||||
}
|
||||
|
||||
Future<void> _pumpRequestFrames(WidgetTester tester) async {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await pumpRequestFrames(tester);
|
||||
}
|
||||
|
||||
class _MusicBrowseHarness {
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/focus/input_mode_tracker.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/library_filter_result.dart';
|
||||
import 'package:plezy/media/library_query.dart';
|
||||
@@ -15,7 +14,6 @@ import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/media_sort.dart';
|
||||
import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/navigation/main_screen_scope.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/state_messages.dart';
|
||||
import 'package:plezy/screens/libraries/tabs/library_browse_tab.dart';
|
||||
@@ -23,12 +21,11 @@ import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/media_server_http_client.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:plezy/widgets/focusable_filter_chip.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/library_tab_scaffold.dart';
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
@@ -52,12 +49,12 @@ void main() {
|
||||
expect(harness.clientA.pageRequestCount, 0);
|
||||
|
||||
harness.selectedLibrary.value = harness.libraryB;
|
||||
await _pumpRequestFrames(tester);
|
||||
await pumpRequestFrames(tester);
|
||||
expect(find.text('Library B'), findsOneWidget);
|
||||
expect(harness.loadedLibraries, [harness.libraryB.globalKey]);
|
||||
|
||||
sortA.complete(const []);
|
||||
await _pumpRequestFrames(tester);
|
||||
await pumpRequestFrames(tester);
|
||||
|
||||
expect(find.text('Library A'), findsNothing);
|
||||
expect(find.text('Library B'), findsOneWidget);
|
||||
@@ -79,7 +76,7 @@ void main() {
|
||||
expect(harness.loadedLibraries, isEmpty);
|
||||
|
||||
harness.selectedLibrary.value = harness.libraryB;
|
||||
await _pumpRequestFrames(tester);
|
||||
await pumpRequestFrames(tester);
|
||||
|
||||
expect(find.text('Library A'), findsNothing);
|
||||
expect(find.text('Library B'), findsOneWidget);
|
||||
@@ -125,7 +122,7 @@ void main() {
|
||||
await _pumpUntil(tester, () => clientA.pageRequestCount == 2);
|
||||
|
||||
emptyPage.complete(const LibraryPage<MediaItem>(items: [], totalCount: 0));
|
||||
await _pumpRequestFrames(tester);
|
||||
await pumpRequestFrames(tester);
|
||||
|
||||
expect(find.byType(ErrorStateWidget), findsNothing);
|
||||
expect(find.byType(EmptyStateWidget), findsOneWidget);
|
||||
@@ -135,56 +132,21 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> _pumpHarness(WidgetTester tester, _BrowseHarness harness, {bool settle = true}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(1280, 720);
|
||||
addTearDown(() {
|
||||
tester.view.resetDevicePixelRatio();
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
value: harness.provider,
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: 0,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, _) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: const SliverToBoxAdapter(child: SizedBox(height: 1)),
|
||||
),
|
||||
],
|
||||
body: ValueListenableBuilder<MediaLibrary>(
|
||||
valueListenable: harness.selectedLibrary,
|
||||
builder: (context, library, _) => LibraryBrowseTab(
|
||||
library: library,
|
||||
canGroupByFolders: true,
|
||||
isActive: true,
|
||||
onDataLoaded: () => harness.loadedLibraries.add(library.globalKey),
|
||||
onBack: () => harness.chromeFocusRequests++,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
await pumpLibraryTab(
|
||||
tester,
|
||||
provider: harness.provider,
|
||||
tab: ValueListenableBuilder<MediaLibrary>(
|
||||
valueListenable: harness.selectedLibrary,
|
||||
builder: (context, library, _) => LibraryBrowseTab(
|
||||
library: library,
|
||||
canGroupByFolders: true,
|
||||
isActive: true,
|
||||
onDataLoaded: () => harness.loadedLibraries.add(library.globalKey),
|
||||
onBack: () => harness.chromeFocusRequests++,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (settle) await _pumpRequestFrames(tester);
|
||||
}
|
||||
|
||||
Future<void> _pumpRequestFrames(WidgetTester tester) async {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
if (settle) await pumpRequestFrames(tester);
|
||||
}
|
||||
|
||||
Future<void> _pumpUntil(WidgetTester tester, bool Function() condition) async {
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/focus/input_mode_tracker.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
@@ -14,23 +13,21 @@ import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/models/plex/plex_config.dart';
|
||||
import 'package:plezy/navigation/main_screen_scope.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/tabs/library_collections_tab.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/jellyfin_api_cache.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:plezy/widgets/card_inflation_budget.dart';
|
||||
import 'package:plezy/widgets/focusable_media_card.dart';
|
||||
import 'package:plezy/widgets/media_card_sliver_layout.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/backend_client_fixtures.dart';
|
||||
import '../../test_helpers/library_tab_scaffold.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
final _serverId = ServerId('collection-server');
|
||||
@@ -93,39 +90,11 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> _pumpTab(WidgetTester tester, {required _CollectionHarness harness, required MediaLibrary library}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(800, 600);
|
||||
addTearDown(() {
|
||||
tester.view.resetDevicePixelRatio();
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
value: harness.provider,
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: 0,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, _) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: const SliverToBoxAdapter(child: SizedBox(height: 1)),
|
||||
),
|
||||
],
|
||||
body: LibraryCollectionsTab(library: library, suppressAutoFocus: true, onBack: () {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
await pumpLibraryTab(
|
||||
tester,
|
||||
provider: harness.provider,
|
||||
tab: LibraryCollectionsTab(library: library, suppressAutoFocus: true, onBack: () {}),
|
||||
size: const Size(800, 600),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
@@ -137,7 +106,7 @@ class _CollectionHarness {
|
||||
|
||||
_CollectionHarness._({required this.database, required MediaServerClient client}) {
|
||||
manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
provider = testMultiServerProvider(manager);
|
||||
}
|
||||
|
||||
factory _CollectionHarness.plex() {
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/focus/input_mode_tracker.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
@@ -15,7 +14,6 @@ import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/media/media_playlist.dart';
|
||||
import 'package:plezy/models/plex/plex_config.dart';
|
||||
import 'package:plezy/navigation/main_screen_scope.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/tabs/library_playlists_tab.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
@@ -23,14 +21,13 @@ import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_client.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:plezy/widgets/card_inflation_budget.dart';
|
||||
import 'package:plezy/widgets/focusable_media_card.dart';
|
||||
import 'package:plezy/widgets/media_card_sliver_layout.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/backend_client_fixtures.dart';
|
||||
import '../../test_helpers/library_tab_scaffold.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
final _serverId = ServerId('playlist-server');
|
||||
@@ -187,43 +184,15 @@ Future<void> _pumpTab(
|
||||
required VoidCallback onBack,
|
||||
required VoidCallback onSidebar,
|
||||
}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(800, 600);
|
||||
addTearDown(() {
|
||||
tester.view.resetDevicePixelRatio();
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
value: harness.provider,
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: onSidebar,
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: 0,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, _) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: const SliverToBoxAdapter(child: SizedBox(height: 1)),
|
||||
),
|
||||
],
|
||||
body: ValueListenableBuilder<int>(
|
||||
valueListenable: harness.rebuild,
|
||||
builder: (context, _, _) =>
|
||||
LibraryPlaylistsTab(library: library, suppressAutoFocus: true, onBack: onBack),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
await pumpLibraryTab(
|
||||
tester,
|
||||
provider: harness.provider,
|
||||
tab: ValueListenableBuilder<int>(
|
||||
valueListenable: harness.rebuild,
|
||||
builder: (context, _, _) => LibraryPlaylistsTab(library: library, suppressAutoFocus: true, onBack: onBack),
|
||||
),
|
||||
size: const Size(800, 600),
|
||||
focusSidebar: onSidebar,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
@@ -31,6 +30,7 @@ import 'package:plezy/widgets/loading_indicator_box.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/http_fixtures.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
|
||||
void main() {
|
||||
@@ -448,7 +448,7 @@ class _PlexMetadataRequests {
|
||||
path.startsWith('/library/metadata/') &&
|
||||
request.url.queryParameters['includePreferences'] == '1') {
|
||||
final id = path.split('/').last;
|
||||
return _jsonResponse({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{
|
||||
@@ -644,10 +644,6 @@ class _FakeFilePicker implements FilePickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
http.Response _jsonResponse(Object body) {
|
||||
return http.Response(jsonEncode(body), 200, headers: const {'content-type': 'application/json'});
|
||||
}
|
||||
|
||||
http.Response _ok() => _response(200);
|
||||
|
||||
http.Response _response(int statusCode) {
|
||||
|
||||
@@ -17,7 +17,6 @@ import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/mixins/refreshable.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/search_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
@@ -28,6 +27,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -321,7 +321,7 @@ Future<(_FakeMediaServerClient, GlobalKey<State<SearchScreen>>)> _pumpTvSearchSc
|
||||
for (final additionalClient in additionalClients) {
|
||||
manager.debugRegisterClientForTesting(additionalClient);
|
||||
}
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
final key = GlobalKey<State<SearchScreen>>();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
@@ -12,10 +10,9 @@ import 'package:plezy/services/catalog/plex_catalog_source.dart';
|
||||
import 'package:plezy/services/plex_discover_client.dart';
|
||||
import 'package:plezy/utils/external_ids.dart';
|
||||
|
||||
const _session = PlexDiscoverSession(accessToken: 'profile-token', clientIdentifier: 'client-id');
|
||||
import '../../test_helpers/http_fixtures.dart';
|
||||
|
||||
http.Response _json(Object body, [int status = 200]) =>
|
||||
http.Response(jsonEncode(body), status, headers: const {'content-type': 'application/json'});
|
||||
const _session = PlexDiscoverSession(accessToken: 'profile-token', clientIdentifier: 'client-id');
|
||||
|
||||
Map<String, Object?> _metadata({
|
||||
String ratingKey = 'plex-movie-1',
|
||||
@@ -51,7 +48,7 @@ void main() {
|
||||
_session,
|
||||
httpClient: MockClient((request) async {
|
||||
captured = request;
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'offset': 25,
|
||||
'size': 1,
|
||||
@@ -94,7 +91,7 @@ void main() {
|
||||
httpClient: MockClient((request) async {
|
||||
requests.add(request);
|
||||
if (request.url.path == '/hubs/sections/watchlist') {
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'Hub': [
|
||||
{
|
||||
@@ -122,7 +119,7 @@ void main() {
|
||||
});
|
||||
}
|
||||
if (request.url.path == '/hubs/sections/watchlist/because-watchlisted') {
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'offset': 2,
|
||||
'totalSize': 3,
|
||||
@@ -130,7 +127,7 @@ void main() {
|
||||
},
|
||||
});
|
||||
}
|
||||
return _json({'error': 'unexpected'}, 500);
|
||||
return jsonResponse({'error': 'unexpected'}, status: 500);
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -162,7 +159,7 @@ void main() {
|
||||
_session,
|
||||
httpClient: MockClient((request) async {
|
||||
requests.add(request);
|
||||
return _json({'error': 'unexpected'}, 500);
|
||||
return jsonResponse({'error': 'unexpected'}, status: 500);
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -181,7 +178,7 @@ void main() {
|
||||
_session,
|
||||
httpClient: MockClient((request) async {
|
||||
captured = request;
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'SearchResults': [
|
||||
{
|
||||
@@ -221,7 +218,7 @@ void main() {
|
||||
httpClient: MockClient((request) async {
|
||||
requests.add(request);
|
||||
if (request.url.path == '/library/sections/watchlist/all') {
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'totalSize': watchlisted ? 1 : 0,
|
||||
'Metadata': watchlisted ? [_metadata()] : <Object>[],
|
||||
@@ -232,7 +229,7 @@ void main() {
|
||||
expect(request.url.path, '/actions/removeFromWatchlist');
|
||||
expect(request.url.queryParameters['ratingKey'], 'plex-movie-1');
|
||||
watchlisted = false;
|
||||
return _json(const <String, Object?>{});
|
||||
return jsonResponse(const <String, Object?>{});
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -256,7 +253,7 @@ void main() {
|
||||
requests.add(request);
|
||||
if (request.url.path == '/library/metadata/matches') {
|
||||
expect(request.url.queryParameters['guid'], 'imdb://tt1375666');
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'Metadata': [_metadata()],
|
||||
},
|
||||
@@ -265,7 +262,7 @@ void main() {
|
||||
expect(request.method, 'PUT');
|
||||
expect(request.url.path, '/actions/addToWatchlist');
|
||||
expect(request.url.queryParameters['ratingKey'], 'plex-movie-1');
|
||||
return _json(const <String, Object?>{});
|
||||
return jsonResponse(const <String, Object?>{});
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -283,13 +280,13 @@ void main() {
|
||||
switch (request.url.path) {
|
||||
case '/library/metadata/matches':
|
||||
expect(request.url.queryParameters['guid'], 'imdb://tt1375666');
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'Metadata': [_metadata(type: 'show')],
|
||||
},
|
||||
});
|
||||
case '/library/metadata/plex-movie-1':
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{
|
||||
@@ -302,7 +299,7 @@ void main() {
|
||||
},
|
||||
});
|
||||
case '/library/metadata/plex-movie-1/related':
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'MediaContainer': {
|
||||
'Hub': [
|
||||
{
|
||||
@@ -312,7 +309,7 @@ void main() {
|
||||
},
|
||||
});
|
||||
}
|
||||
return _json({'error': 'unexpected'}, 500);
|
||||
return jsonResponse({'error': 'unexpected'}, status: 500);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/models/catalog/catalog_item.dart';
|
||||
@@ -11,6 +8,8 @@ import 'package:plezy/services/catalog/seerr_catalog_source.dart';
|
||||
import 'package:plezy/services/seerr/seerr_client.dart';
|
||||
import 'package:plezy/utils/external_ids.dart';
|
||||
|
||||
import '../../test_helpers/http_fixtures.dart';
|
||||
|
||||
SeerrCatalogSource _source(MockClient mock) {
|
||||
final client = SeerrClient(
|
||||
const SeerrSession(
|
||||
@@ -36,15 +35,13 @@ SeerrCatalogSource _source(MockClient mock) {
|
||||
return source;
|
||||
}
|
||||
|
||||
http.Response _json(Object body) => http.Response(jsonEncode(body), 200, headers: {'content-type': 'application/json'});
|
||||
|
||||
void main() {
|
||||
group('SeerrCatalogSource', () {
|
||||
test('trending row keeps movies and shows, drops people, maps TMDB images', () async {
|
||||
final source = _source(
|
||||
MockClient((request) async {
|
||||
expect(request.url.path, '/api/v1/discover/trending');
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'page': 1,
|
||||
'totalPages': 3,
|
||||
'results': [
|
||||
@@ -89,7 +86,7 @@ void main() {
|
||||
final source = _source(
|
||||
MockClient((request) async {
|
||||
paths.add('${request.url.path}?${request.url.query}');
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'page': 2,
|
||||
'totalPages': 2,
|
||||
'results': [
|
||||
@@ -106,13 +103,13 @@ void main() {
|
||||
});
|
||||
|
||||
test('rows Seerr does not serve throw', () {
|
||||
final source = _source(MockClient((request) async => _json({})));
|
||||
final source = _source(MockClient((request) async => jsonResponse({})));
|
||||
expect(() => source.fetchRow(CatalogRowId.watchlist), throwsArgumentError);
|
||||
expect(() => source.fetchRow(CatalogRowId.suggestedAnime), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('resolveItemIds needs a tmdb id', () async {
|
||||
final source = _source(MockClient((request) async => _json({})));
|
||||
final source = _source(MockClient((request) async => jsonResponse({})));
|
||||
final resolved = await source.resolveItemIds(MediaKind.movie, const ExternalIds(tmdb: 603, imdb: 'tt0133093'));
|
||||
expect(resolved?.tmdb, 603);
|
||||
expect(resolved?.imdb, 'tt0133093');
|
||||
@@ -123,7 +120,7 @@ void main() {
|
||||
final source = _source(
|
||||
MockClient((request) async {
|
||||
expect(request.url.path, '/api/v1/tv/1396');
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'id': 1396,
|
||||
'name': 'Breaking Bad',
|
||||
'credits': {
|
||||
@@ -156,7 +153,7 @@ void main() {
|
||||
MockClient((request) async {
|
||||
expect(request.url.path, '/api/v1/search');
|
||||
expect(request.url.queryParameters['query'], 'the matrix');
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'page': 1,
|
||||
'totalPages': 1,
|
||||
'results': [
|
||||
@@ -175,7 +172,7 @@ void main() {
|
||||
final source = _source(
|
||||
MockClient((request) async {
|
||||
expect(request.url.path, '/api/v1/movie/603/recommendations');
|
||||
return _json({
|
||||
return jsonResponse({
|
||||
'page': 1,
|
||||
'totalPages': 1,
|
||||
'results': [
|
||||
@@ -197,13 +194,13 @@ void main() {
|
||||
|
||||
test('canRequest honors the per-kind permission split', () {
|
||||
// permissions: 2 = ADMIN in the fixture session → everything allowed.
|
||||
final source = _source(MockClient((request) async => _json({})));
|
||||
final source = _source(MockClient((request) async => jsonResponse({})));
|
||||
expect(source.canRequest(MediaKind.movie), isTrue);
|
||||
expect(source.canRequest(MediaKind.show), isTrue);
|
||||
});
|
||||
|
||||
test('has no watchlist: membership unknown, mutations unsupported', () async {
|
||||
final source = _source(MockClient((request) async => _json({})));
|
||||
final source = _source(MockClient((request) async => jsonResponse({})));
|
||||
expect(source.supportsWatchlist, isFalse);
|
||||
expect(source.isOnWatchlist(MediaKind.movie, const CatalogItemIds(tmdb: 603)), isNull);
|
||||
expect(() => source.addToWatchlist(MediaKind.movie, const CatalogItemIds(tmdb: 603)), throwsUnsupportedError);
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/playback_report_metadata.dart';
|
||||
import 'package:plezy/models/external_player_models.dart';
|
||||
import 'package:plezy/services/external_player_service.dart';
|
||||
import 'package:plezy/services/jellyfin_api_cache.dart';
|
||||
@@ -15,8 +14,9 @@ import 'package:plezy/services/offline_watch_sync_service.dart';
|
||||
import 'package:plezy/utils/active_client_scope.dart';
|
||||
import 'package:plezy/utils/watch_state_notifier.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/playback_report_fakes.dart';
|
||||
|
||||
class _RecordingClient implements MediaServerClient, ScopedMediaServerClient {
|
||||
class _RecordingClient with PlaybackReportRecorder implements MediaServerClient, ScopedMediaServerClient {
|
||||
_RecordingClient({this.backend = MediaBackend.plex, String? scopedServerId})
|
||||
: scopedServerId =
|
||||
scopedServerId ??
|
||||
@@ -48,33 +48,18 @@ class _RecordingClient implements MediaServerClient, ScopedMediaServerClient {
|
||||
bool get marksWatchedOnPlaybackStopped => backend == MediaBackend.jellyfin;
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStarted({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {
|
||||
started.add((positionMs: position.inMilliseconds, durationMs: duration?.inMilliseconds));
|
||||
if (failStart) throw StateError('start failed');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStopped({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
||||
}) async {
|
||||
stopped.add((positionMs: position.inMilliseconds, durationMs: duration?.inMilliseconds));
|
||||
if (failStop) throw StateError('stop failed');
|
||||
Future<void> onPlaybackReport(PlaybackReportCall call) async {
|
||||
final entry = (positionMs: call.position.inMilliseconds, durationMs: call.duration?.inMilliseconds);
|
||||
switch (call.kind) {
|
||||
case PlaybackReportKind.started:
|
||||
started.add(entry);
|
||||
if (failStart) throw StateError('start failed');
|
||||
case PlaybackReportKind.progress:
|
||||
throw UnimplementedError();
|
||||
case PlaybackReportKind.stopped:
|
||||
stopped.add(entry);
|
||||
if (failStop) throw StateError('stop failed');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,57 +1,27 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/playback_report_metadata.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/services/live_session_tracker.dart';
|
||||
|
||||
class _FakeJellyfinClient implements JellyfinClient {
|
||||
import '../test_helpers/playback_report_fakes.dart';
|
||||
|
||||
class _FakeJellyfinClient with PlaybackReportRecorder implements JellyfinClient {
|
||||
final calls = <String>[];
|
||||
final startGate = Completer<void>();
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStarted({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {
|
||||
await startGate.future;
|
||||
calls.add('started:$itemId:$playSessionId:$mediaSourceId:$liveStreamId:$playMethod');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackProgress({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
bool isPaused = false,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {
|
||||
calls.add('${isPaused ? 'paused' : 'playing'}:$itemId:$playSessionId:$mediaSourceId:$liveStreamId');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStopped({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
||||
}) async {
|
||||
calls.add('stopped:$itemId:$playSessionId:$mediaSourceId:$liveStreamId');
|
||||
Future<void> onPlaybackReport(PlaybackReportCall call) async {
|
||||
final identity = '${call.itemId}:${call.playSessionId}:${call.mediaSourceId}:${call.liveStreamId}';
|
||||
switch (call.kind) {
|
||||
case PlaybackReportKind.started:
|
||||
await startGate.future;
|
||||
calls.add('started:$identity:${call.playMethod}');
|
||||
case PlaybackReportKind.progress:
|
||||
calls.add('${call.isPaused ? 'paused' : 'playing'}:$identity');
|
||||
case PlaybackReportKind.stopped:
|
||||
calls.add('stopped:$identity');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:plezy/media/media_display_criteria.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/playback_report_metadata.dart';
|
||||
import 'package:plezy/mpv/models.dart';
|
||||
import 'package:plezy/mpv/player/player.dart';
|
||||
import 'package:plezy/mpv/player/player_state.dart';
|
||||
@@ -20,6 +19,7 @@ import 'package:plezy/services/music/music_playback_service_impl.dart';
|
||||
import 'package:plezy/services/music/music_source_resolver.dart';
|
||||
import 'package:plezy/services/playback_coordinator.dart';
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/playback_report_fakes.dart';
|
||||
|
||||
const _trackDuration = Duration(minutes: 3);
|
||||
|
||||
@@ -351,7 +351,7 @@ class RecordedReport {
|
||||
|
||||
/// Records the progress-report surface; everything else is unimplemented
|
||||
/// (the engine and tracker never touch it in these tests).
|
||||
class FakeMediaServerClient extends Fake implements MediaServerClient {
|
||||
class FakeMediaServerClient extends Fake with PlaybackReportRecorder implements MediaServerClient {
|
||||
final List<RecordedReport> reports = [];
|
||||
final List<String> markedWatched = [];
|
||||
Completer<List<MediaItem>>? instantMixGate;
|
||||
@@ -381,47 +381,13 @@ class FakeMediaServerClient extends Fake implements MediaServerClient {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStarted({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {
|
||||
reports.add(RecordedReport('started', itemId, position));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackProgress({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
bool isPaused = false,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {
|
||||
reports.add(RecordedReport(isPaused ? 'paused' : 'progress', itemId, position));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStopped({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
||||
}) async {
|
||||
reports.add(RecordedReport('stopped', itemId, position));
|
||||
Future<void> onPlaybackReport(PlaybackReportCall call) async {
|
||||
final state = switch (call.kind) {
|
||||
PlaybackReportKind.started => 'started',
|
||||
PlaybackReportKind.progress => call.isPaused ? 'paused' : 'progress',
|
||||
PlaybackReportKind.stopped => 'stopped',
|
||||
};
|
||||
reports.add(RecordedReport(state, call.itemId, call.position));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'package:plezy/utils/active_client_scope.dart';
|
||||
import 'package:plezy/utils/watch_state_notifier.dart';
|
||||
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/playback_report_fakes.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
|
||||
@@ -55,7 +56,7 @@ class _FakeOfflineModeSource extends ChangeNotifier implements OfflineModeSource
|
||||
bool get hasListeners => super.hasListeners;
|
||||
}
|
||||
|
||||
class _RecordingMediaClient implements MediaServerClient {
|
||||
class _RecordingMediaClient with PlaybackReportRecorder implements MediaServerClient {
|
||||
_RecordingMediaClient({required this.serverId, required this.backend});
|
||||
|
||||
@override
|
||||
@@ -85,36 +86,24 @@ class _RecordingMediaClient implements MediaServerClient {
|
||||
testMediaItem(id: id, backend: backend, kind: MediaKind.movie, serverId: serverId);
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStarted({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {
|
||||
started.add((itemId: itemId, positionMs: position.inMilliseconds, durationMs: duration?.inMilliseconds));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStopped({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
||||
}) async {
|
||||
stopped.add((
|
||||
itemId: itemId,
|
||||
positionMs: position.inMilliseconds,
|
||||
durationMs: duration?.inMilliseconds,
|
||||
report: report,
|
||||
));
|
||||
Future<void> onPlaybackReport(PlaybackReportCall call) async {
|
||||
switch (call.kind) {
|
||||
case PlaybackReportKind.started:
|
||||
started.add((
|
||||
itemId: call.itemId,
|
||||
positionMs: call.position.inMilliseconds,
|
||||
durationMs: call.duration?.inMilliseconds,
|
||||
));
|
||||
case PlaybackReportKind.progress:
|
||||
throw UnimplementedError();
|
||||
case PlaybackReportKind.stopped:
|
||||
stopped.add((
|
||||
itemId: call.itemId,
|
||||
positionMs: call.position.inMilliseconds,
|
||||
durationMs: call.duration?.inMilliseconds,
|
||||
report: call.report,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:plezy/utils/active_client_scope.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/playback_report_fakes.dart';
|
||||
|
||||
// Periodic behavior is virtualized with fake_async and the tracker's existing
|
||||
// updateInterval seam. Routing, threshold, scrobble, cadence, coalescing,
|
||||
@@ -85,7 +86,7 @@ class _FakePlayer implements Player {
|
||||
|
||||
/// Recording fake [PlexClient] that captures every progress / scrobble call
|
||||
/// without touching the network.
|
||||
class _FakePlexClient implements PlexClient {
|
||||
class _FakePlexClient with PlaybackReportRecorder implements PlexClient {
|
||||
_FakePlexClient({this.thresholdPercent = 90});
|
||||
|
||||
/// Watched-threshold percentage to report. Defaults to 90 (matches
|
||||
@@ -149,68 +150,25 @@ class _FakePlexClient implements PlexClient {
|
||||
// The interface report* methods delegate to updateProgress so existing
|
||||
// assertions on `updateProgressCalls` keep working.
|
||||
@override
|
||||
Future<void> reportPlaybackStarted({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) {
|
||||
playbackSessionIds.add(playSessionId);
|
||||
Future<void> onPlaybackReport(PlaybackReportCall call) {
|
||||
playbackSessionIds.add(call.playSessionId);
|
||||
playbackStreamSelections.add((
|
||||
mediaSourceId: mediaSourceId,
|
||||
audioStreamIndex: audioStreamIndex,
|
||||
subtitleStreamIndex: subtitleStreamIndex,
|
||||
));
|
||||
return updateProgress(itemId, time: position.inMilliseconds, state: 'playing', duration: duration?.inMilliseconds);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackProgress({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
bool isPaused = false,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) {
|
||||
playbackSessionIds.add(playSessionId);
|
||||
playbackStreamSelections.add((
|
||||
mediaSourceId: mediaSourceId,
|
||||
audioStreamIndex: audioStreamIndex,
|
||||
subtitleStreamIndex: subtitleStreamIndex,
|
||||
mediaSourceId: call.mediaSourceId,
|
||||
audioStreamIndex: call.audioStreamIndex,
|
||||
subtitleStreamIndex: call.subtitleStreamIndex,
|
||||
));
|
||||
return updateProgress(
|
||||
itemId,
|
||||
time: position.inMilliseconds,
|
||||
state: isPaused ? 'paused' : 'playing',
|
||||
duration: duration.inMilliseconds,
|
||||
call.itemId,
|
||||
time: call.position.inMilliseconds,
|
||||
state: switch (call.kind) {
|
||||
PlaybackReportKind.started => 'playing',
|
||||
PlaybackReportKind.progress => call.isPaused ? 'paused' : 'playing',
|
||||
PlaybackReportKind.stopped => 'stopped',
|
||||
},
|
||||
duration: call.duration?.inMilliseconds,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStopped({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
||||
}) {
|
||||
playbackSessionIds.add(playSessionId);
|
||||
playbackStreamSelections.add((mediaSourceId: mediaSourceId, audioStreamIndex: null, subtitleStreamIndex: null));
|
||||
return updateProgress(itemId, time: position.inMilliseconds, state: 'stopped', duration: duration?.inMilliseconds);
|
||||
}
|
||||
|
||||
// Transport-only, like production: the single watch event for the stop
|
||||
// flow is emitted by markWatchedFromPlaybackStop after this returns.
|
||||
@override
|
||||
@@ -1431,7 +1389,7 @@ void main() {
|
||||
|
||||
/// A more precise fake than [_FakePlexClient]: lets the test independently
|
||||
/// fail the scrobble (markWatched) without touching the progress signals.
|
||||
class _ScrobblePreciseClient implements PlexClient {
|
||||
class _ScrobblePreciseClient with PlaybackReportRecorder implements PlexClient {
|
||||
_ScrobblePreciseClient({this.thresholdPercent = 90, this.failScrobbleFirstTime = false});
|
||||
|
||||
final int thresholdPercent;
|
||||
@@ -1472,42 +1430,7 @@ class _ScrobblePreciseClient implements PlexClient {
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStarted({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackProgress({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
bool isPaused = false,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStopped({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
||||
}) async {}
|
||||
Future<void> onPlaybackReport(PlaybackReportCall call) async {}
|
||||
|
||||
@override
|
||||
Future<void> markWatched(MediaItem item) async {
|
||||
|
||||
@@ -2,66 +2,36 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/playback_report_metadata.dart';
|
||||
import 'package:plezy/services/playback_report_session.dart';
|
||||
|
||||
class _RecordingClient implements MediaServerClient {
|
||||
import '../test_helpers/playback_report_fakes.dart';
|
||||
|
||||
class _RecordingClient with PlaybackReportRecorder implements MediaServerClient {
|
||||
final calls = <String>[];
|
||||
Completer<void>? startGate;
|
||||
Completer<void>? stopGate;
|
||||
bool failNextStop = false;
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStarted({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {
|
||||
final gate = startGate;
|
||||
if (gate != null) await gate.future;
|
||||
calls.add('started:${position.inMilliseconds}:$mediaSourceId:$audioStreamIndex:$subtitleStreamIndex');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackProgress({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
bool isPaused = false,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) async {
|
||||
calls.add('${isPaused ? 'paused' : 'playing'}:${position.inMilliseconds}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reportPlaybackStopped({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
||||
}) async {
|
||||
calls.add('stopped-attempt:${position.inMilliseconds}:$mediaSourceId');
|
||||
final gate = stopGate;
|
||||
if (gate != null) await gate.future;
|
||||
if (failNextStop) {
|
||||
failNextStop = false;
|
||||
throw StateError('stop failed');
|
||||
Future<void> onPlaybackReport(PlaybackReportCall call) async {
|
||||
final positionMs = call.position.inMilliseconds;
|
||||
switch (call.kind) {
|
||||
case PlaybackReportKind.started:
|
||||
final start = startGate;
|
||||
if (start != null) await start.future;
|
||||
calls.add('started:$positionMs:${call.mediaSourceId}:${call.audioStreamIndex}:${call.subtitleStreamIndex}');
|
||||
case PlaybackReportKind.progress:
|
||||
calls.add('${call.isPaused ? 'paused' : 'playing'}:$positionMs');
|
||||
case PlaybackReportKind.stopped:
|
||||
calls.add('stopped-attempt:$positionMs:${call.mediaSourceId}');
|
||||
final stop = stopGate;
|
||||
if (stop != null) await stop.future;
|
||||
if (failNextStop) {
|
||||
failNextStop = false;
|
||||
throw StateError('stop failed');
|
||||
}
|
||||
calls.add('stopped:$positionMs:${call.mediaSourceId}');
|
||||
}
|
||||
calls.add('stopped:${position.inMilliseconds}:$mediaSourceId');
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// JSON-encodes [body] into an [http.Response] carrying a JSON content type.
|
||||
http.Response jsonResponse(Object body, {int status = 200}) =>
|
||||
http.Response(jsonEncode(body), status, headers: const {'content-type': 'application/json'});
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/focus/input_mode_tracker.dart';
|
||||
import 'package:plezy/navigation/main_screen_scope.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// Pumps [tab] under the ancestors every library tab requires: [provider], an
|
||||
/// [InputModeTracker], a [MainScreenFocusScope] and a [NestedScrollView] whose
|
||||
/// overlap absorber handle the tabs look up. Sizes the view to [size] and
|
||||
/// restores it when the test ends. Settling is left to the caller.
|
||||
Future<void> pumpLibraryTab(
|
||||
WidgetTester tester, {
|
||||
required MultiServerProvider provider,
|
||||
required Widget tab,
|
||||
Size size = const Size(1280, 720),
|
||||
VoidCallback? focusSidebar,
|
||||
}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = size;
|
||||
addTearDown(() {
|
||||
tester.view.resetDevicePixelRatio();
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
value: provider,
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: focusSidebar ?? () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: 0,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, _) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: const SliverToBoxAdapter(child: SizedBox(height: 1)),
|
||||
),
|
||||
],
|
||||
body: tab,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Frames a library tab needs to issue its debounced request and apply the
|
||||
/// response, for tabs whose loading never quiesces enough for `pumpAndSettle`.
|
||||
Future<void> pumpRequestFrames(WidgetTester tester) async {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
|
||||
/// Wires [manager] into the provider widget tests read servers from. The caller
|
||||
/// owns disposal of the returned provider.
|
||||
MultiServerProvider testMultiServerProvider(MultiServerManager manager) {
|
||||
return MultiServerProvider(manager, DataAggregationService(manager));
|
||||
}
|
||||
|
||||
/// Registers [clients] on a fresh manager and returns it with its provider,
|
||||
/// disposing both when the test ends. Clients also named in [offline] are
|
||||
/// registered as unreachable; [clients] fixes the registration order.
|
||||
({MultiServerManager manager, MultiServerProvider provider}) testMultiServer({
|
||||
List<MediaServerClient> clients = const [],
|
||||
List<MediaServerClient> offline = const [],
|
||||
}) {
|
||||
final manager = MultiServerManager();
|
||||
for (final client in clients) {
|
||||
manager.debugRegisterClientForTesting(client, online: !offline.contains(client));
|
||||
}
|
||||
final provider = testMultiServerProvider(manager);
|
||||
addTearDown(() {
|
||||
provider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
return (manager: manager, provider: provider);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:plezy/media/playback_report_metadata.dart';
|
||||
|
||||
enum PlaybackReportKind { started, progress, stopped }
|
||||
|
||||
/// One `reportPlayback*` invocation flattened into a single value.
|
||||
class PlaybackReportCall {
|
||||
const PlaybackReportCall({
|
||||
required this.kind,
|
||||
required this.itemId,
|
||||
required this.position,
|
||||
this.duration,
|
||||
this.isPaused = false,
|
||||
this.playSessionId,
|
||||
this.playMethod,
|
||||
this.liveStreamId,
|
||||
this.mediaSourceId,
|
||||
this.audioStreamIndex,
|
||||
this.subtitleStreamIndex,
|
||||
this.report = const PlaybackReportMetadata.live(),
|
||||
});
|
||||
|
||||
final PlaybackReportKind kind;
|
||||
final String itemId;
|
||||
final Duration position;
|
||||
final Duration? duration;
|
||||
final bool isPaused;
|
||||
final String? playSessionId;
|
||||
final String? playMethod;
|
||||
final String? liveStreamId;
|
||||
final String? mediaSourceId;
|
||||
final int? audioStreamIndex;
|
||||
final int? subtitleStreamIndex;
|
||||
final PlaybackReportMetadata report;
|
||||
}
|
||||
|
||||
/// Carries the three playback-reporting signatures so fakes implement the
|
||||
/// surface once in [onPlaybackReport]. Forwarding is synchronous, so
|
||||
/// [onPlaybackReport] runs with the same timing the overridden method had.
|
||||
mixin PlaybackReportRecorder {
|
||||
Future<void> onPlaybackReport(PlaybackReportCall call);
|
||||
|
||||
Future<void> reportPlaybackStarted({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) {
|
||||
return onPlaybackReport(
|
||||
PlaybackReportCall(
|
||||
kind: PlaybackReportKind.started,
|
||||
itemId: itemId,
|
||||
position: position,
|
||||
duration: duration,
|
||||
playSessionId: playSessionId,
|
||||
playMethod: playMethod,
|
||||
liveStreamId: liveStreamId,
|
||||
mediaSourceId: mediaSourceId,
|
||||
audioStreamIndex: audioStreamIndex,
|
||||
subtitleStreamIndex: subtitleStreamIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reportPlaybackProgress({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
bool isPaused = false,
|
||||
String? playSessionId,
|
||||
String? playMethod,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
}) {
|
||||
return onPlaybackReport(
|
||||
PlaybackReportCall(
|
||||
kind: PlaybackReportKind.progress,
|
||||
itemId: itemId,
|
||||
position: position,
|
||||
duration: duration,
|
||||
isPaused: isPaused,
|
||||
playSessionId: playSessionId,
|
||||
playMethod: playMethod,
|
||||
liveStreamId: liveStreamId,
|
||||
mediaSourceId: mediaSourceId,
|
||||
audioStreamIndex: audioStreamIndex,
|
||||
subtitleStreamIndex: subtitleStreamIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reportPlaybackStopped({
|
||||
required String itemId,
|
||||
required Duration position,
|
||||
Duration? duration,
|
||||
String? playSessionId,
|
||||
String? liveStreamId,
|
||||
String? mediaSourceId,
|
||||
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
||||
}) {
|
||||
return onPlaybackReport(
|
||||
PlaybackReportCall(
|
||||
kind: PlaybackReportKind.stopped,
|
||||
itemId: itemId,
|
||||
position: position,
|
||||
duration: duration,
|
||||
playSessionId: playSessionId,
|
||||
liveStreamId: liveStreamId,
|
||||
mediaSourceId: mediaSourceId,
|
||||
report: report,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/connection/connection_registry.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/models/plex/plex_home_user.dart';
|
||||
import 'package:plezy/profiles/active_profile_provider.dart';
|
||||
import 'package:plezy/profiles/plex_home_service.dart';
|
||||
import 'package:plezy/profiles/profile_connection_registry.dart';
|
||||
import 'package:plezy/profiles/profile_registry.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
|
||||
/// The profile dependency graph wired the way production wires it: database →
|
||||
/// registries → [PlexHomeService] → [ActiveProfileProvider].
|
||||
class ProfileStack {
|
||||
ProfileStack._({
|
||||
required this.db,
|
||||
required this.connections,
|
||||
required this.profileConnections,
|
||||
required this.profiles,
|
||||
required this.plexHome,
|
||||
required this.active,
|
||||
required this._storage,
|
||||
required this._ownsDatabase,
|
||||
});
|
||||
|
||||
final AppDatabase db;
|
||||
final ConnectionRegistry connections;
|
||||
final ProfileConnectionRegistry profileConnections;
|
||||
final ProfileRegistry profiles;
|
||||
final PlexHomeService plexHome;
|
||||
final ActiveProfileProvider active;
|
||||
|
||||
final StorageService? _storage;
|
||||
final bool _ownsDatabase;
|
||||
|
||||
/// Only wired when the stack was created with `withStorage: true`.
|
||||
StorageService get storage => _storage!;
|
||||
|
||||
/// Pass [db] when the test also needs the database for caches or downloads;
|
||||
/// the caller then owns closing it.
|
||||
static Future<ProfileStack> create({
|
||||
AppDatabase? db,
|
||||
List<PlexHomeUser> homeUsers = const [],
|
||||
bool withStorage = true,
|
||||
}) async {
|
||||
final database = db ?? AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(database);
|
||||
final profileConnections = ProfileConnectionRegistry(database);
|
||||
final profiles = ProfileRegistry(database);
|
||||
final storage = withStorage ? await StorageService.getInstance() : null;
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
storage: storage,
|
||||
plexHomeUserFetcher: (_) async => homeUsers,
|
||||
);
|
||||
return ProfileStack._(
|
||||
db: database,
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
profiles: profiles,
|
||||
plexHome: plexHome,
|
||||
active: ActiveProfileProvider(registry: profiles, plexHome: plexHome, connections: connections, storage: storage),
|
||||
storage: storage,
|
||||
ownsDatabase: db == null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await active.resetForTesting();
|
||||
active.dispose();
|
||||
await plexHome.dispose();
|
||||
if (_ownsDatabase) await db.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/theme/mono_tokens.dart';
|
||||
|
||||
/// Default [MonoTokens] for widget tests: production-shaped metrics with
|
||||
/// 1ms animations and no ink splashes, so a single `pump` settles the tree.
|
||||
///
|
||||
/// Use as `ThemeData(extensions: const [testMonoTokens])`.
|
||||
const testMonoTokens = MonoTokens(
|
||||
radiusSm: 8,
|
||||
radiusMd: 12,
|
||||
radiusLg: 20,
|
||||
radiusXs: 5,
|
||||
groupGap: 2,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 1),
|
||||
normal: Duration(milliseconds: 1),
|
||||
slow: Duration(milliseconds: 1),
|
||||
expressive: Duration(milliseconds: 1),
|
||||
bg: Colors.black,
|
||||
surface: Colors.black,
|
||||
outline: Colors.white24,
|
||||
text: Colors.white,
|
||||
textMuted: Colors.white70,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
|
||||
/// [testMonoTokens] with realistic animation durations, for tests that step
|
||||
/// through intermediate frames instead of settling straight to the end state.
|
||||
const testMonoTokensAnimated = MonoTokens(
|
||||
radiusSm: 4,
|
||||
radiusMd: 8,
|
||||
radiusLg: 20,
|
||||
radiusXs: 5,
|
||||
groupGap: 2,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 100),
|
||||
normal: Duration(milliseconds: 200),
|
||||
slow: Duration(milliseconds: 300),
|
||||
expressive: Duration(milliseconds: 300),
|
||||
bg: Colors.black,
|
||||
surface: Color(0xFF111111),
|
||||
outline: Color(0xFF333333),
|
||||
text: Colors.white,
|
||||
textMuted: Color(0xFFAAAAAA),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
@@ -8,13 +8,12 @@ import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
import 'package:plezy/utils/provider_extensions.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
const _missingOwnerLibrary = MediaLibrary(
|
||||
id: '1',
|
||||
@@ -59,13 +58,7 @@ void main() {
|
||||
tester,
|
||||
) async {
|
||||
final replacement = testPlexClient(serverId: ServerId('server-b'));
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(replacement);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
addTearDown(() {
|
||||
provider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
final context = await _pumpContext(tester, provider);
|
||||
final context = await _pumpContext(tester, testMultiServer(clients: [replacement]).provider);
|
||||
|
||||
expect(() => context.getPlexClientForLibrary(_missingOwnerLibrary), _throwsNoClientAvailable);
|
||||
expect(() => context.getMediaClientForLibrary(_missingOwnerLibrary), _throwsNoClientAvailable);
|
||||
@@ -75,13 +68,7 @@ void main() {
|
||||
tester,
|
||||
) async {
|
||||
final replacement = testPlexClient(serverId: ServerId('server-b'));
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(replacement);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
addTearDown(() {
|
||||
provider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
final context = await _pumpContext(tester, provider);
|
||||
final context = await _pumpContext(tester, testMultiServer(clients: [replacement]).provider);
|
||||
|
||||
for (final serverId in <String?>[null, ' ']) {
|
||||
final library = MediaLibrary(
|
||||
@@ -103,15 +90,10 @@ void main() {
|
||||
testWidgets('library-qualified helpers return their registered owner even when it is marked offline', (tester) async {
|
||||
final owner = testPlexClient(serverId: ServerId('server-a'));
|
||||
final replacement = testPlexClient(serverId: ServerId('server-b'));
|
||||
final manager = MultiServerManager()
|
||||
..debugRegisterClientForTesting(owner, online: false)
|
||||
..debugRegisterClientForTesting(replacement);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
addTearDown(() {
|
||||
provider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
final context = await _pumpContext(tester, provider);
|
||||
final context = await _pumpContext(
|
||||
tester,
|
||||
testMultiServer(clients: [owner, replacement], offline: [owner]).provider,
|
||||
);
|
||||
|
||||
expect(context.getPlexClientForLibrary(_missingOwnerLibrary), same(owner));
|
||||
expect(context.getMediaClientForLibrary(_missingOwnerLibrary), same(owner));
|
||||
|
||||
@@ -4,28 +4,10 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/i18n/strings.g.dart';
|
||||
import 'package:plezy/media/media_source_info.dart';
|
||||
import 'package:plezy/mpv/mpv.dart';
|
||||
import 'package:plezy/theme/mono_tokens.dart';
|
||||
import 'package:plezy/widgets/overlay_sheet.dart';
|
||||
import 'package:plezy/widgets/video_controls/sheets/chapter_sheet.dart';
|
||||
|
||||
const _tokens = MonoTokens(
|
||||
radiusSm: 8,
|
||||
radiusMd: 12,
|
||||
radiusLg: 20,
|
||||
radiusXs: 5,
|
||||
groupGap: 2,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 1),
|
||||
normal: Duration(milliseconds: 1),
|
||||
slow: Duration(milliseconds: 1),
|
||||
expressive: Duration(milliseconds: 1),
|
||||
bg: Colors.black,
|
||||
surface: Colors.black,
|
||||
outline: Colors.white24,
|
||||
text: Colors.white,
|
||||
textMuted: Colors.white70,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
import '../test_helpers/theme.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() => LocaleSettings.setLocaleSync(AppLocale.en));
|
||||
@@ -64,7 +46,7 @@ Future<void> _pumpSheet(
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_tokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: OverlaySheetHost(
|
||||
child: Scaffold(
|
||||
body: Builder(
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/connection/connection.dart';
|
||||
import 'package:plezy/connection/connection_registry.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/i18n/strings.g.dart';
|
||||
import 'package:plezy/navigation/profile_navigation_scope.dart';
|
||||
@@ -26,9 +25,6 @@ import 'package:plezy/models/plex/plex_home_user.dart';
|
||||
import 'package:plezy/models/plex/plex_config.dart';
|
||||
import 'package:plezy/profiles/profile.dart';
|
||||
import 'package:plezy/profiles/active_profile_provider.dart';
|
||||
import 'package:plezy/profiles/plex_home_service.dart';
|
||||
import 'package:plezy/profiles/profile_connection_registry.dart';
|
||||
import 'package:plezy/profiles/profile_registry.dart';
|
||||
import 'package:plezy/providers/download_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
@@ -51,6 +47,7 @@ import 'package:provider/provider.dart';
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/profile_stack.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -126,28 +123,14 @@ void main() {
|
||||
];
|
||||
final client = _AudioPlaylistClient(tracks);
|
||||
final music = _RecordingMusicPlaybackService();
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final activeProfileProvider = ActiveProfileProvider(
|
||||
registry: ProfileRegistry(db),
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
);
|
||||
final stack = await ProfileStack.create(withStorage: false);
|
||||
addTearDown(() async {
|
||||
activeProfileProvider.dispose();
|
||||
await plexHome.dispose();
|
||||
await stack.dispose();
|
||||
music.dispose();
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final menuKey = GlobalKey<MediaContextMenuState>();
|
||||
@@ -164,7 +147,7 @@ void main() {
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfileProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: stack.active),
|
||||
ChangeNotifierProvider<MusicPlaybackService>.value(value: music),
|
||||
],
|
||||
child: MaterialApp(
|
||||
@@ -244,28 +227,14 @@ void main() {
|
||||
),
|
||||
])..blockWithAbort = true;
|
||||
final playback = PlaybackStateProvider();
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final activeProfileProvider = ActiveProfileProvider(
|
||||
registry: ProfileRegistry(db),
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
);
|
||||
final stack = await ProfileStack.create(withStorage: false);
|
||||
addTearDown(() async {
|
||||
playback.dispose();
|
||||
activeProfileProvider.dispose();
|
||||
await plexHome.dispose();
|
||||
await stack.dispose();
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final menuKey = GlobalKey<MediaContextMenuState>();
|
||||
@@ -281,7 +250,7 @@ void main() {
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfileProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: stack.active),
|
||||
ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
|
||||
],
|
||||
child: MaterialApp(
|
||||
@@ -323,27 +292,13 @@ void main() {
|
||||
TvDetectionService.debugSetAppleTVOverride(true);
|
||||
addTearDown(() => TvDetectionService.debugSetAppleTVOverride(null));
|
||||
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final manager = MultiServerManager();
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final activeProfileProvider = ActiveProfileProvider(
|
||||
registry: ProfileRegistry(db),
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
);
|
||||
final stack = await ProfileStack.create(withStorage: false);
|
||||
addTearDown(() async {
|
||||
activeProfileProvider.dispose();
|
||||
await plexHome.dispose();
|
||||
await stack.dispose();
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final menuKey = GlobalKey<MediaContextMenuState>();
|
||||
@@ -360,7 +315,7 @@ void main() {
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfileProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: stack.active),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
@@ -615,21 +570,9 @@ Future<GlobalKey<MediaContextMenuState>> _pumpPlexMovieMenu(
|
||||
);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final activeProfileProvider = ActiveProfileProvider(
|
||||
registry: ProfileRegistry(db),
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
);
|
||||
final stack = await ProfileStack.create(db: db, withStorage: false);
|
||||
addTearDown(() async {
|
||||
activeProfileProvider.dispose();
|
||||
await plexHome.dispose();
|
||||
await stack.dispose();
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
await db.close();
|
||||
@@ -648,7 +591,7 @@ Future<GlobalKey<MediaContextMenuState>> _pumpPlexMovieMenu(
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfileProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: stack.active),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
@@ -829,18 +772,7 @@ Future<_SiblingMusicMenuHarness> _pumpSiblingMusicMenu(
|
||||
final client = _RelatedMusicClient(relatedItems);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final activeProfileProvider = ActiveProfileProvider(
|
||||
registry: ProfileRegistry(db),
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
);
|
||||
final stack = await ProfileStack.create(db: db, withStorage: false);
|
||||
final music = _RecordingMusicPlaybackService();
|
||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final profileNavigatorKey = GlobalKey<NavigatorState>();
|
||||
@@ -849,8 +781,7 @@ Future<_SiblingMusicMenuHarness> _pumpSiblingMusicMenu(
|
||||
addTearDown(() async {
|
||||
downloadProvider.dispose();
|
||||
downloadManager.dispose();
|
||||
activeProfileProvider.dispose();
|
||||
await plexHome.dispose();
|
||||
await stack.dispose();
|
||||
music.dispose();
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
@@ -866,7 +797,7 @@ Future<_SiblingMusicMenuHarness> _pumpSiblingMusicMenu(
|
||||
providers: [
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
ChangeNotifierProvider<DownloadProvider>.value(value: downloadProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfileProvider),
|
||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: stack.active),
|
||||
ChangeNotifierProvider<MusicPlaybackService>.value(value: music),
|
||||
],
|
||||
child: ProfileNavigationScope(
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:plezy/connection/connection_registry.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/focus/focusable_action_bar.dart';
|
||||
import 'package:plezy/focus/focusable_wrapper.dart';
|
||||
import 'package:plezy/i18n/strings.g.dart';
|
||||
@@ -15,9 +12,6 @@ import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/models/download_models.dart';
|
||||
import 'package:plezy/profiles/active_profile_provider.dart';
|
||||
import 'package:plezy/profiles/plex_home_service.dart';
|
||||
import 'package:plezy/profiles/profile_connection_registry.dart';
|
||||
import 'package:plezy/profiles/profile_registry.dart';
|
||||
import 'package:plezy/providers/download_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
@@ -33,6 +27,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
import '../../test_helpers/profile_stack.dart';
|
||||
|
||||
final _track = testMediaItem(
|
||||
id: 'track_1',
|
||||
@@ -303,25 +298,11 @@ void main() {
|
||||
testWidgets('keyboard long-press anchors the context menu to the focused card instead of a stale pointer', (
|
||||
tester,
|
||||
) async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
final connections = ConnectionRegistry(db);
|
||||
final profileConnections = ProfileConnectionRegistry(db);
|
||||
final plexHome = PlexHomeService(
|
||||
connections: connections,
|
||||
profileConnections: profileConnections,
|
||||
plexHomeUserFetcher: (_) async => const [],
|
||||
);
|
||||
final activeProfileProvider = ActiveProfileProvider(
|
||||
registry: ProfileRegistry(db),
|
||||
plexHome: plexHome,
|
||||
connections: connections,
|
||||
);
|
||||
final stack = await ProfileStack.create(withStorage: false);
|
||||
final downloadProvider = _FakeDownloadProvider();
|
||||
addTearDown(() async {
|
||||
activeProfileProvider.dispose();
|
||||
await stack.dispose();
|
||||
downloadProvider.dispose();
|
||||
await plexHome.dispose();
|
||||
await db.close();
|
||||
});
|
||||
final service = _FakeMusicService(track: _track);
|
||||
final observer = MusicUiRouteObserver();
|
||||
@@ -330,7 +311,7 @@ void main() {
|
||||
wrap(
|
||||
service: service,
|
||||
observer: observer,
|
||||
activeProfileProvider: activeProfileProvider,
|
||||
activeProfileProvider: stack.active,
|
||||
downloadProvider: downloadProvider,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:plezy/mpv/mpv.dart';
|
||||
import 'package:plezy/media/media_source_info.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_tokens.dart';
|
||||
import 'package:plezy/widgets/video_controls/sheets/queue_sheet.dart';
|
||||
import 'package:plezy/widgets/video_controls/widgets/content_strip.dart';
|
||||
import 'package:plezy/widgets/video_controls/widgets/media_selector_thumbnail.dart';
|
||||
@@ -20,25 +19,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
|
||||
const _testTokens = MonoTokens(
|
||||
radiusSm: 8,
|
||||
radiusMd: 12,
|
||||
radiusLg: 20,
|
||||
radiusXs: 5,
|
||||
groupGap: 2,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 1),
|
||||
normal: Duration(milliseconds: 1),
|
||||
slow: Duration(milliseconds: 1),
|
||||
expressive: Duration(milliseconds: 1),
|
||||
bg: Colors.black,
|
||||
surface: Colors.black,
|
||||
outline: Colors.white24,
|
||||
text: Colors.white,
|
||||
textMuted: Colors.white70,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
import '../test_helpers/theme.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -334,7 +315,7 @@ Widget _queueHarness({required PlaybackStateProvider playback, required Widget c
|
||||
value: playback,
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(body: SizedBox(width: 600, height: 400, child: child)),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,14 +9,13 @@ import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/i18n/strings.g.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/widgets/server_activities_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -31,15 +30,9 @@ void main() {
|
||||
tearDown(() => database.close());
|
||||
|
||||
testWidgets('togglePanel opens and closes the server activities overlay', (tester) async {
|
||||
final manager = MultiServerManager();
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServer().provider;
|
||||
final buttonKey = GlobalKey<ServerActivitiesButtonState>();
|
||||
|
||||
addTearDown(() {
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
@@ -178,15 +171,9 @@ class _ActivitiesHarness {
|
||||
Future<_ActivitiesHarness> _pumpActivitiesHarness(WidgetTester tester, _ControlledActivitiesClient transport) async {
|
||||
final serverId = ServerId('plex-server');
|
||||
final client = testPlexClient(serverId: serverId, serverName: 'Test server', httpClient: transport);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServer(clients: [client]).provider;
|
||||
final buttonKey = GlobalKey<ServerActivitiesButtonState>();
|
||||
|
||||
addTearDown(() {
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
|
||||
@@ -17,32 +17,13 @@ import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.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/app_icon.dart';
|
||||
import 'package:plezy/widgets/side_navigation_rail.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
const _testTokens = MonoTokens(
|
||||
radiusSm: 8,
|
||||
radiusMd: 12,
|
||||
radiusLg: 20,
|
||||
radiusXs: 5,
|
||||
groupGap: 2,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 1),
|
||||
normal: Duration(milliseconds: 1),
|
||||
slow: Duration(milliseconds: 1),
|
||||
expressive: Duration(milliseconds: 1),
|
||||
bg: Colors.black,
|
||||
surface: Colors.black,
|
||||
outline: Colors.white24,
|
||||
text: Colors.white,
|
||||
textMuted: Colors.white70,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
import '../test_helpers/theme.dart';
|
||||
|
||||
MediaLibrary _library({
|
||||
required String id,
|
||||
@@ -124,7 +105,7 @@ Future<void> _pumpBasicRail(
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: height == null ? rail : SizedBox(height: height, child: rail),
|
||||
),
|
||||
@@ -172,7 +153,7 @@ void main() {
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SideNavigationRail(
|
||||
selectedTab: NavigationTabId.discover,
|
||||
@@ -237,7 +218,7 @@ void main() {
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SideNavigationRail(
|
||||
selectedTab: NavigationTabId.discover,
|
||||
@@ -263,7 +244,7 @@ void main() {
|
||||
await _pumpBasicRail(tester, alwaysExpanded: true);
|
||||
|
||||
final selectedItem = find.byType(NavigationRailItem).first;
|
||||
expect(_railItemDecoration(tester, selectedItem)?.color, _testTokens.text.withValues(alpha: 0.1));
|
||||
expect(_railItemDecoration(tester, selectedItem)?.color, testMonoTokens.text.withValues(alpha: 0.1));
|
||||
});
|
||||
|
||||
testWidgets('D-pad sidebar focus hides selected item background after focus moves', (tester) async {
|
||||
@@ -343,7 +324,7 @@ void main() {
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SideNavigationRail(
|
||||
selectedTab: NavigationTabId.discover,
|
||||
@@ -408,7 +389,7 @@ void main() {
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SideNavigationRail(
|
||||
key: sideNavKey,
|
||||
@@ -485,7 +466,7 @@ void main() {
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SideNavigationRail(
|
||||
key: sideNavKey,
|
||||
@@ -527,7 +508,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
|
||||
@@ -8,29 +8,11 @@ import 'package:plezy/i18n/strings.g.dart';
|
||||
import 'package:plezy/media/media_source_info.dart';
|
||||
import 'package:plezy/mpv/mpv.dart';
|
||||
import 'package:plezy/services/playback_subtitle_resolver.dart';
|
||||
import 'package:plezy/theme/mono_tokens.dart';
|
||||
import 'package:plezy/widgets/overlay_sheet.dart';
|
||||
import 'package:plezy/widgets/video_controls/models/track_controls_state.dart';
|
||||
import 'package:plezy/widgets/video_controls/sheets/track_sheet.dart';
|
||||
|
||||
const _testTokens = MonoTokens(
|
||||
radiusSm: 8,
|
||||
radiusMd: 12,
|
||||
radiusLg: 20,
|
||||
radiusXs: 5,
|
||||
groupGap: 2,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 1),
|
||||
normal: Duration(milliseconds: 1),
|
||||
slow: Duration(milliseconds: 1),
|
||||
expressive: Duration(milliseconds: 1),
|
||||
bg: Colors.black,
|
||||
surface: Colors.black,
|
||||
outline: Colors.white24,
|
||||
text: Colors.white,
|
||||
textMuted: Colors.white70,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
import '../test_helpers/theme.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -175,7 +157,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: OverlaySheetHost(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
@@ -399,7 +381,7 @@ Future<void> _pumpTrackSheet(
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: OverlaySheetHost(
|
||||
child: Scaffold(
|
||||
body: SizedBox(
|
||||
|
||||
@@ -17,7 +17,6 @@ import 'package:plezy/services/playback_subtitle_resolver.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/services/video_volume_controller.dart';
|
||||
import 'package:plezy/theme/mono_tokens.dart';
|
||||
import 'package:plezy/widgets/video_controls/widgets/player_toast_indicator.dart';
|
||||
import 'package:plezy/widgets/video_controls/desktop_video_controls.dart';
|
||||
import 'package:plezy/widgets/video_controls/mobile_video_controls.dart';
|
||||
@@ -36,25 +35,7 @@ import 'package:plezy/widgets/video_controls/widgets/video_timeline_bar.dart';
|
||||
import '../test_helpers/watch_together_fakes.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
const _testTokens = MonoTokens(
|
||||
radiusSm: 8,
|
||||
radiusMd: 12,
|
||||
radiusLg: 20,
|
||||
radiusXs: 5,
|
||||
groupGap: 2,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 1),
|
||||
normal: Duration(milliseconds: 1),
|
||||
slow: Duration(milliseconds: 1),
|
||||
expressive: Duration(milliseconds: 1),
|
||||
bg: Colors.black,
|
||||
surface: Colors.black,
|
||||
outline: Colors.white24,
|
||||
text: Colors.white,
|
||||
textMuted: Colors.white70,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
import '../test_helpers/theme.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -968,7 +949,7 @@ void main() {
|
||||
ChangeNotifierProvider<WatchTogetherProvider>.value(
|
||||
value: watchTogether,
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 1000,
|
||||
@@ -1023,7 +1004,7 @@ void main() {
|
||||
ChangeNotifierProvider<WatchTogetherProvider>.value(
|
||||
value: watchTogether,
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 500,
|
||||
@@ -1514,7 +1495,7 @@ void main() {
|
||||
ChangeNotifierProvider<WatchTogetherProvider>.value(value: watchTogether),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(platform: TargetPlatform.macOS, extensions: const [_testTokens]),
|
||||
theme: ThemeData(platform: TargetPlatform.macOS, extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 1200,
|
||||
@@ -1568,7 +1549,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 700,
|
||||
@@ -1604,7 +1585,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 700,
|
||||
@@ -1650,7 +1631,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 700,
|
||||
@@ -1703,7 +1684,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 700,
|
||||
@@ -1755,7 +1736,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 700,
|
||||
@@ -1865,7 +1846,7 @@ Future<void> _pumpSkipMarkerButton(
|
||||
}) {
|
||||
return tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: SkipMarkerButton(
|
||||
|
||||
@@ -10,29 +10,10 @@ import 'package:plezy/mpv/player/player_streams.dart';
|
||||
import 'package:plezy/screens/settings/subtitle_styling_screen.dart';
|
||||
import 'package:plezy/services/sleep_timer_service.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_tokens.dart';
|
||||
import 'package:plezy/widgets/video_controls/sheets/video_settings_sheet.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
const _testTokens = MonoTokens(
|
||||
radiusSm: 4,
|
||||
radiusMd: 8,
|
||||
radiusLg: 20,
|
||||
radiusXs: 5,
|
||||
groupGap: 2,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 100),
|
||||
normal: Duration(milliseconds: 200),
|
||||
slow: Duration(milliseconds: 300),
|
||||
expressive: Duration(milliseconds: 300),
|
||||
bg: Colors.black,
|
||||
surface: Color(0xFF111111),
|
||||
outline: Color(0xFF333333),
|
||||
text: Colors.white,
|
||||
textMuted: Color(0xFFAAAAAA),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
import '../test_helpers/theme.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
@@ -82,7 +63,7 @@ void main() {
|
||||
LocaleSettings.setLocaleSync(AppLocale.ru);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokensAnimated]),
|
||||
home: const SubtitleStylingScreen(),
|
||||
),
|
||||
);
|
||||
@@ -159,7 +140,7 @@ Future<void> _pumpSheet(
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
theme: ThemeData(extensions: const [testMonoTokensAnimated]),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 900,
|
||||
|
||||
Reference in New Issue
Block a user