2026-08-02 02:43:34 +05:30
|
|
|
import 'package:flutter/material.dart';
|
2026-08-01 08:06:41 +05:30
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
|
|
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
|
|
|
|
import 'package:immich_mobile/presentation/actions/lock.action.dart';
|
|
|
|
|
import 'package:immich_ui/immich_ui.dart';
|
|
|
|
|
import 'package:mocktail/mocktail.dart';
|
|
|
|
|
|
|
|
|
|
import '../../../service.mocks.dart';
|
|
|
|
|
import '../../factories/remote_asset_factory.dart';
|
|
|
|
|
import '../presentation_context.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late PresentationContext context;
|
|
|
|
|
late MockAssetService assetService;
|
|
|
|
|
|
|
|
|
|
setUp(() async {
|
|
|
|
|
context = await PresentationContext.create();
|
|
|
|
|
assetService = context.service.asset.service;
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-01 08:06:44 +05:30
|
|
|
tearDown(() async {
|
|
|
|
|
await context.dispose();
|
2026-08-01 08:06:41 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
RemoteAsset owned({AssetVisibility visibility = .timeline}) =>
|
|
|
|
|
RemoteAssetFactory.create(ownerId: context.currentUser.id, visibility: visibility);
|
|
|
|
|
|
|
|
|
|
Future<void> pumpLock(WidgetTester tester, Set<BaseAsset> selection) =>
|
|
|
|
|
tester.pumpTestAction(context, const LockAction(source: .timeline), overrides: context.selected(selection));
|
|
|
|
|
|
|
|
|
|
group('LockAction', () {
|
|
|
|
|
testWidgets('locks the eligible owned assets', (tester) async {
|
|
|
|
|
final asset = owned();
|
|
|
|
|
|
|
|
|
|
await pumpLock(tester, {asset});
|
|
|
|
|
|
|
|
|
|
verify(() => assetService.update([asset.id], visibility: const .some(.locked))).called(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('unlocks the eligible owned assets', (tester) async {
|
|
|
|
|
final asset = owned(visibility: .locked);
|
|
|
|
|
|
|
|
|
|
await pumpLock(tester, {asset});
|
|
|
|
|
|
|
|
|
|
verify(() => assetService.update([asset.id], visibility: const .some(.timeline))).called(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('prioritizes lock when mixed state', (tester) async {
|
|
|
|
|
final unlocked = owned();
|
|
|
|
|
final locked = owned(visibility: .locked);
|
|
|
|
|
|
|
|
|
|
await pumpLock(tester, {unlocked, locked});
|
|
|
|
|
|
|
|
|
|
verify(() => assetService.update([unlocked.id], visibility: const .some(.locked))).called(1);
|
|
|
|
|
verifyNever(() => assetService.update(any(), visibility: const .some(.timeline)));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('ignores assets owned by someone else', (tester) async {
|
|
|
|
|
final mine = owned();
|
|
|
|
|
final theirs = RemoteAssetFactory.create();
|
|
|
|
|
|
|
|
|
|
await pumpLock(tester, {mine, theirs});
|
|
|
|
|
|
|
|
|
|
verify(() => assetService.update([mine.id], visibility: const .some(.locked))).called(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('locks only the owned assets not already locked', (tester) async {
|
|
|
|
|
final stale = owned();
|
|
|
|
|
final alreadyLocked = owned(visibility: .locked);
|
|
|
|
|
|
|
|
|
|
await pumpLock(tester, {stale, alreadyLocked});
|
|
|
|
|
|
|
|
|
|
verify(() => assetService.update([stale.id], visibility: const .some(.locked))).called(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('removes the local copies of the assets it locks', (tester) async {
|
|
|
|
|
final merged = RemoteAssetFactory.create(ownerId: context.currentUser.id, localId: 'local-1');
|
|
|
|
|
final remoteOnly = owned();
|
|
|
|
|
|
|
|
|
|
await pumpLock(tester, {merged, remoteOnly});
|
|
|
|
|
|
|
|
|
|
verify(() => assetService.deleteLocal(['local-1'])).called(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('leaves the local copies alone when unlocking', (tester) async {
|
|
|
|
|
final merged = RemoteAssetFactory.create(
|
|
|
|
|
ownerId: context.currentUser.id,
|
|
|
|
|
localId: 'local-1',
|
|
|
|
|
visibility: .locked,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await pumpLock(tester, {merged});
|
|
|
|
|
|
|
|
|
|
verifyNever(() => assetService.deleteLocal(any()));
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-02 02:43:34 +05:30
|
|
|
testWidgets('offers an undo that locks the unlocked assets again', (tester) async {
|
|
|
|
|
final asset = owned(visibility: .locked);
|
|
|
|
|
|
|
|
|
|
await pumpLock(tester, {asset});
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await tester.tap(find.text('Undo'));
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
|
|
|
|
verify(() => assetService.update([asset.id], visibility: const .some(.locked))).called(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('offers no undo for locking', (tester) async {
|
|
|
|
|
await pumpLock(tester, {owned()});
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(find.byType(SnackBar), findsOneWidget);
|
|
|
|
|
expect(find.text('Undo'), findsNothing);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-01 08:06:41 +05:30
|
|
|
testWidgets('clears the selection once the update succeeds', (tester) async {
|
|
|
|
|
await pumpLock(tester, {owned()});
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('is hidden when none of the selected assets are owned', (tester) async {
|
|
|
|
|
await tester.pumpTestWidget(
|
|
|
|
|
context,
|
|
|
|
|
const ActionIconButton(action: LockAction(source: .timeline)),
|
|
|
|
|
overrides: context.selected({RemoteAssetFactory.create()}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(find.byType(ImmichIconButton), findsNothing);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|