mirror of
https://github.com/immich-app/immich
synced 2026-08-22 13:13:05 +00:00
Added controller and widget tests
This commit is contained in:
parent
d5f9141b15
commit
16e9aad0a4
4 changed files with 612 additions and 2 deletions
388
mobile/test/presentation/pages/drift_slideshow_page_test.dart
Normal file
388
mobile/test/presentation/pages/drift_slideshow_page_test.dart
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_slideshow.page.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/slideshow/slideshow_controller.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/slideshow/slideshow_slide.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
|
||||
import '../../unit/factories/remote_asset_factory.dart';
|
||||
import '../../unit/presentation/presentation_context.dart';
|
||||
|
||||
/// A VideoPlayerNotifier that records play/pause events
|
||||
class FakeVideoPlayer extends VideoPlayerNotifier {
|
||||
/// Every call made against this video, in order
|
||||
final calls = <String>[];
|
||||
|
||||
void emit(VideoPlayerState next) => state = next;
|
||||
|
||||
@override
|
||||
Future<void> play() async => calls.add('play');
|
||||
|
||||
@override
|
||||
Future<void> pause() async => calls.add('pause');
|
||||
}
|
||||
|
||||
const slideDuration = Duration(seconds: 3);
|
||||
|
||||
/// A small step to precisely test event timing
|
||||
const tick = Duration(milliseconds: 1);
|
||||
|
||||
List<BaseAsset> images(int count) => List.generate(count, (_) => RemoteAssetFactory.create());
|
||||
|
||||
TimelineService timelineOf(List<BaseAsset> assets, {TimelineAssetSource? source}) => TimelineService((
|
||||
assetSource: source ?? (index, count) async => assets.sublist(index, math.min(index + count, assets.length)),
|
||||
bucketSource: () => Stream.value([Bucket(assetCount: assets.length)]),
|
||||
origin: TimelineOrigin.main,
|
||||
));
|
||||
|
||||
/// A timeline whose last asset is still loading
|
||||
TimelineService partlyLoaded(List<BaseAsset> assets, {Completer<void>? stall}) {
|
||||
var loadCount = 0;
|
||||
|
||||
return timelineOf(
|
||||
assets,
|
||||
source: (index, count) async {
|
||||
loadCount += 1;
|
||||
|
||||
if (loadCount != 1) {
|
||||
await stall?.future;
|
||||
}
|
||||
|
||||
return assets.sublist(index, math.min(index + count, loadCount == 1 ? assets.length - 1 : assets.length));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late PresentationContext ctx;
|
||||
|
||||
setUp(() async => ctx = await PresentationContext.create());
|
||||
|
||||
tearDown(() async => ctx.dispose());
|
||||
|
||||
void slideshowTest(
|
||||
String description,
|
||||
Future<void> Function(WidgetTester tester) body, {
|
||||
TimelineService Function()? timeline,
|
||||
SlideshowDirection direction = SlideshowDirection.forward,
|
||||
bool repeat = true,
|
||||
bool disableAnimations = false,
|
||||
List<Override> overrides = const [],
|
||||
}) {
|
||||
testWidgets(description, (tester) async {
|
||||
if (disableAnimations) {
|
||||
tester.binding.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures(
|
||||
disableAnimations: true,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpTestWidget(
|
||||
ctx,
|
||||
DriftSlideshowPage(timeline: (timeline ?? () => timelineOf(images(3)))()),
|
||||
expectSettle: false,
|
||||
overrides: [
|
||||
appConfigProvider.overrideWithValue(
|
||||
AppConfig(
|
||||
slideshow: SlideshowConfig(duration: 3, look: .contain, direction: direction, repeat: repeat),
|
||||
),
|
||||
),
|
||||
...overrides,
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
await body(tester);
|
||||
});
|
||||
}
|
||||
|
||||
/// The delegate for this page
|
||||
SlideshowDelegate delegateOf(WidgetTester tester) =>
|
||||
tester.state<ConsumerState<DriftSlideshowPage>>(find.byType(DriftSlideshowPage)) as SlideshowDelegate;
|
||||
|
||||
/// The currently visible slide index
|
||||
int visibleIndex(WidgetTester tester) =>
|
||||
tester.widgetList<SlideshowSlide>(find.byType(SlideshowSlide)).firstWhere((slide) => slide.isCurrent).index;
|
||||
|
||||
/// The current value of the Ken Burns zoom for a particular slide
|
||||
double currentZoomOf(WidgetTester tester, int index) => tester
|
||||
.widget<SlideshowSlide>(find.byWidgetPredicate((w) => w is SlideshowSlide && w.index == index && !w.frozen))
|
||||
.zoom
|
||||
.value;
|
||||
|
||||
double? progressIndicatorProgress(WidgetTester tester) =>
|
||||
tester.widget<LinearProgressIndicator>(find.byType(LinearProgressIndicator)).value;
|
||||
|
||||
final fadingSlide = find.byWidgetPredicate((w) => w is SlideshowSlide && w.frozen);
|
||||
|
||||
Future<void> tapScreen(WidgetTester tester) async {
|
||||
await tester.tap(find.byType(PageView));
|
||||
|
||||
// The app bar does stuff in post frame, so we need to run twice
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
group('basic rendering', () {
|
||||
slideshowTest('should show the first slide only', (tester) async {
|
||||
expect(visibleIndex(tester), 0);
|
||||
expect(fadingSlide, findsNothing);
|
||||
});
|
||||
|
||||
slideshowTest('should zoom the slide for its duration', (tester) async {
|
||||
expect(currentZoomOf(tester, 0), 0.0);
|
||||
|
||||
await tester.pump(slideDuration * 0.5);
|
||||
// 50% through duration we should see 50% of the zoom completed
|
||||
expect(currentZoomOf(tester, 0), closeTo(0.5, 0.01));
|
||||
});
|
||||
|
||||
slideshowTest('should reverse the zoom direction when moving to the next slide', (tester) async {
|
||||
await tester.pump(slideDuration + tick);
|
||||
await tester.pump();
|
||||
|
||||
// The previous slide zoomed in, so this one starts zoomed in and will zoom out
|
||||
expect(currentZoomOf(tester, 1), closeTo(1.0, 0.01));
|
||||
|
||||
await tester.pump(slideDuration * 0.3);
|
||||
expect(currentZoomOf(tester, 1), closeTo(0.7, 0.01));
|
||||
});
|
||||
|
||||
slideshowTest('should show the next slide once the duration passes', (tester) async {
|
||||
await tester.pump(slideDuration - tick);
|
||||
expect(visibleIndex(tester), 0);
|
||||
|
||||
await tester.pump(tick * 2);
|
||||
expect(visibleIndex(tester), 1);
|
||||
});
|
||||
|
||||
slideshowTest('should fade the outgoing slide over the incoming one', (tester) async {
|
||||
await tester.pump(slideDuration + tick);
|
||||
await tester.pump();
|
||||
|
||||
// The old slide covers the new one, then is removed
|
||||
expect(tester.widget<SlideshowSlide>(fadingSlide).index, 0);
|
||||
expect(visibleIndex(tester), 1);
|
||||
|
||||
final fade = find.ancestor(of: fadingSlide, matching: find.byType(FadeTransition)).first;
|
||||
expect(tester.widget<FadeTransition>(fade).opacity.value, 1.0);
|
||||
|
||||
await tester.pump(Durations.extralong2 * 0.5);
|
||||
expect(tester.widget<FadeTransition>(fade).opacity.value, lessThan(1.0));
|
||||
|
||||
await tester.pump(Durations.extralong2);
|
||||
expect(fadingSlide, findsNothing);
|
||||
});
|
||||
|
||||
slideshowTest('should not fade when animations are disabled', (tester) async {
|
||||
await tester.pump(slideDuration + tick);
|
||||
await tester.pump();
|
||||
|
||||
expect(visibleIndex(tester), 1);
|
||||
expect(fadingSlide, findsNothing);
|
||||
|
||||
expect(currentZoomOf(tester, 1), 0.0);
|
||||
|
||||
await tester.pump(slideDuration * 0.5);
|
||||
expect(currentZoomOf(tester, 1), 0.0);
|
||||
}, disableAnimations: true);
|
||||
|
||||
slideshowTest(
|
||||
'should show a loading indicator for a slide that isnt ready',
|
||||
(tester) async {
|
||||
await tester.drag(find.byType(PageView), const Offset(-800, 0));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(ImmichLoadingIndicator), findsOneWidget);
|
||||
},
|
||||
timeline: () => partlyLoaded(images(2), stall: Completer<void>()),
|
||||
);
|
||||
});
|
||||
|
||||
group('interactions', () {
|
||||
slideshowTest('should advance the progress bar', (tester) async {
|
||||
expect(progressIndicatorProgress(tester), 0.0);
|
||||
|
||||
await tester.pump(slideDuration * 0.45);
|
||||
expect(progressIndicatorProgress(tester), closeTo(0.45, 0.01));
|
||||
});
|
||||
|
||||
slideshowTest('should show/hide the AppBar', (tester) async {
|
||||
final appBar = find.byType(AnimatedOpacity).first;
|
||||
|
||||
expect(tester.widget<AnimatedOpacity>(appBar).opacity, 0.0);
|
||||
|
||||
await tapScreen(tester);
|
||||
expect(tester.widget<AnimatedOpacity>(appBar).opacity, 1.0);
|
||||
|
||||
await tapScreen(tester);
|
||||
expect(tester.widget<AnimatedOpacity>(appBar).opacity, 0.0);
|
||||
});
|
||||
|
||||
slideshowTest('should stop slide movement and progress bar when paused', (tester) async {
|
||||
await tester.pump(slideDuration * 0.5);
|
||||
|
||||
// Show AppBar and pause the app
|
||||
await tapScreen(tester);
|
||||
await tester.tap(find.byIcon(Icons.pause));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byIcon(Icons.play_arrow), findsOneWidget);
|
||||
|
||||
final held = progressIndicatorProgress(tester);
|
||||
await tester.pump(slideDuration);
|
||||
expect(progressIndicatorProgress(tester), held);
|
||||
expect(visibleIndex(tester), 0);
|
||||
});
|
||||
|
||||
slideshowTest('should resume slide from its paused position', (tester) async {
|
||||
await tester.pump(slideDuration * 0.64);
|
||||
|
||||
await tapScreen(tester);
|
||||
await tester.tap(find.byIcon(Icons.pause));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byIcon(Icons.play_arrow));
|
||||
|
||||
// Resuming restarts the ticker, so this frame is its new baseline and elapses nothing
|
||||
await tester.pump();
|
||||
|
||||
// We should have precisely 36% left before transition
|
||||
await tester.pump(slideDuration * 0.36 - tick);
|
||||
expect(visibleIndex(tester), 0);
|
||||
|
||||
await tester.pump(tick * 2);
|
||||
expect(visibleIndex(tester), 1);
|
||||
});
|
||||
|
||||
late Completer<void> arrived;
|
||||
|
||||
slideshowTest(
|
||||
'should properly handle an async load the user navigated away from',
|
||||
(tester) async {
|
||||
await tester.pump(slideDuration + tick);
|
||||
expect(visibleIndex(tester), 1);
|
||||
|
||||
// This slide will get stuck
|
||||
await tester.pump(slideDuration + tick);
|
||||
|
||||
// Since the slide is stuck, the user swipes back to the first asset
|
||||
await tester.drag(find.byType(PageView), const Offset(800, 0));
|
||||
await tester.pump();
|
||||
expect(visibleIndex(tester), 0);
|
||||
|
||||
// The stuck slide loads, but it doesn't matter
|
||||
arrived.complete();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(visibleIndex(tester), 0);
|
||||
},
|
||||
timeline: () => partlyLoaded(images(3), stall: arrived = Completer<void>()),
|
||||
);
|
||||
|
||||
slideshowTest('should start the slide timer when navigating to a new slide', (tester) async {
|
||||
await tester.pump(slideDuration * 0.75);
|
||||
|
||||
await tester.drag(find.byType(PageView), const Offset(-800, 0));
|
||||
await tester.pump();
|
||||
|
||||
expect(visibleIndex(tester), 1);
|
||||
expect(progressIndicatorProgress(tester), 0.0);
|
||||
|
||||
await tester.pump(slideDuration * 0.35);
|
||||
expect(progressIndicatorProgress(tester), closeTo(0.35, 0.01));
|
||||
});
|
||||
});
|
||||
|
||||
group('delegate API', () {
|
||||
final video = RemoteAssetFactory.create(type: .video);
|
||||
|
||||
late FakeVideoPlayer player;
|
||||
|
||||
final playing = [videoPlayerProvider(video.id).overrideWith((ref) => player)];
|
||||
|
||||
TimelineService withVideo() => timelineOf([video, ...images(1)]);
|
||||
|
||||
setUp(() => player = FakeVideoPlayer());
|
||||
|
||||
slideshowTest('should wrap past end', (tester) async {
|
||||
expect(delegateOf(tester).nextIndexAfter(0), 1);
|
||||
expect(delegateOf(tester).nextIndexAfter(2), 0);
|
||||
});
|
||||
|
||||
slideshowTest('should wrap past start', (tester) async {
|
||||
expect(delegateOf(tester).nextIndexAfter(2), 1);
|
||||
expect(delegateOf(tester).nextIndexAfter(0), 2);
|
||||
}, direction: .backward);
|
||||
|
||||
slideshowTest('should end the slideshow at the last slide when repeat is disabled', (tester) async {
|
||||
expect(delegateOf(tester).nextIndexAfter(1), 2);
|
||||
expect(delegateOf(tester).nextIndexAfter(2), null);
|
||||
}, repeat: false);
|
||||
|
||||
slideshowTest(
|
||||
'should end the slideshow at the first slide when running backwards',
|
||||
(tester) async {
|
||||
expect(delegateOf(tester).nextIndexAfter(1), 0);
|
||||
expect(delegateOf(tester).nextIndexAfter(0), null);
|
||||
},
|
||||
repeat: false,
|
||||
direction: .backward,
|
||||
);
|
||||
|
||||
slideshowTest('should end the slideshow when there is nothing to show', (tester) async {
|
||||
expect(delegateOf(tester).nextIndexAfter(0), null);
|
||||
}, timeline: () => timelineOf(const []));
|
||||
|
||||
slideshowTest(
|
||||
'should provide video playback position, but not image positions',
|
||||
(tester) async {
|
||||
player.emit(const VideoPlayerState(position: Duration(seconds: 2), duration: .zero, status: .playing));
|
||||
|
||||
expect(delegateOf(tester).videoProgressOf(0), const Duration(seconds: 2));
|
||||
expect(delegateOf(tester).videoProgressOf(1), null);
|
||||
},
|
||||
timeline: withVideo,
|
||||
overrides: playing,
|
||||
);
|
||||
|
||||
slideshowTest(
|
||||
'should report when videos complete',
|
||||
(tester) async {
|
||||
expect(delegateOf(tester).isVideoCompleted(0), false);
|
||||
|
||||
player.emit(const VideoPlayerState(position: Duration(seconds: 2), duration: .zero, status: .completed));
|
||||
expect(delegateOf(tester).isVideoCompleted(0), true);
|
||||
expect(delegateOf(tester).isVideoCompleted(1), false);
|
||||
},
|
||||
timeline: withVideo,
|
||||
overrides: playing,
|
||||
);
|
||||
|
||||
slideshowTest(
|
||||
'should pause and play the video along with the slideshow',
|
||||
(tester) async {
|
||||
delegateOf(tester).onPlaybackChanged(0, false);
|
||||
delegateOf(tester).onPlaybackChanged(0, true);
|
||||
expect(player.calls, ['pause', 'play']);
|
||||
|
||||
// An image does nothing here
|
||||
delegateOf(tester).onPlaybackChanged(1, true);
|
||||
expect(player.calls, ['pause', 'play']);
|
||||
},
|
||||
timeline: withVideo,
|
||||
overrides: playing,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/slideshow/slideshow_controller.dart';
|
||||
|
||||
const slideDuration = Duration(seconds: 5);
|
||||
const tick = Duration(milliseconds: 1);
|
||||
|
||||
class FakeSlideshow implements SlideshowDelegate {
|
||||
FakeSlideshow({this.nextIndex = _nextInOrder}) {
|
||||
controller = SlideshowController(
|
||||
vsync: const TestVSync(),
|
||||
slideDuration: slideDuration,
|
||||
initialIndex: 0,
|
||||
delegate: this,
|
||||
);
|
||||
}
|
||||
|
||||
static int? _nextInOrder(int index) => index + 1;
|
||||
|
||||
int? Function(int index) nextIndex;
|
||||
|
||||
/// Video slide index -> playback position. Image slides do not appear in this map at all
|
||||
final videos = <int, Duration>{};
|
||||
final completedVideos = <int>{};
|
||||
|
||||
/// Test action log
|
||||
final events = <String>[];
|
||||
|
||||
late final SlideshowController controller;
|
||||
|
||||
@override
|
||||
int? nextIndexAfter(int index) => nextIndex(index);
|
||||
|
||||
@override
|
||||
Duration? videoProgressOf(int index) => videos[index];
|
||||
|
||||
@override
|
||||
bool isVideoCompleted(int index) => completedVideos.contains(index);
|
||||
|
||||
@override
|
||||
void onPlaybackChanged(int index, bool playing) => events.add(playing ? 'play' : 'pause');
|
||||
|
||||
@override
|
||||
void onShowSlide(int index, int prevIndex) {
|
||||
events.add('show $index');
|
||||
|
||||
controller.didCompleteShowSlide(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs [body] against a freshly started slideshow
|
||||
void slideshowTest(
|
||||
String description,
|
||||
Future<void> Function(WidgetTester tester, FakeSlideshow show) body, {
|
||||
int? Function(int index)? nextIndex,
|
||||
}) {
|
||||
testWidgets(description, (tester) async {
|
||||
final show = FakeSlideshow(nextIndex: nextIndex ?? FakeSlideshow._nextInOrder);
|
||||
|
||||
// The slideshow's ticker takes its baseline on this frame, so a test can jump the clock straight away
|
||||
await tester.pump();
|
||||
|
||||
try {
|
||||
await body(tester, show);
|
||||
} finally {
|
||||
// The slideshow ticks forever. All tests must stop it, otherwise it will leak into subsequent tests
|
||||
// This cannot appear in tearDown due to invariants being called outside of "render"
|
||||
show.controller.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void main() {
|
||||
slideshowTest('should advance once the slide duration passes', (tester, show) async {
|
||||
await tester.pump(slideDuration - tick);
|
||||
expect(show.events, isEmpty);
|
||||
|
||||
await tester.pump(tick * 2);
|
||||
expect(show.events, ['show 1']);
|
||||
});
|
||||
|
||||
slideshowTest('should show the slide the delegate provides next', (tester, show) async {
|
||||
await tester.pump(slideDuration + tick);
|
||||
|
||||
expect(show.events, ['show 7']);
|
||||
}, nextIndex: (_) => 7);
|
||||
|
||||
slideshowTest('should pause when there is no next slide', (tester, show) async {
|
||||
await tester.pump(slideDuration + tick);
|
||||
|
||||
expect(show.events, isEmpty);
|
||||
expect(show.controller.paused, isTrue);
|
||||
}, nextIndex: (_) => null);
|
||||
|
||||
slideshowTest('should stop the clock when paused', (tester, show) async {
|
||||
show.controller.pause();
|
||||
|
||||
expect(show.events, ['pause']);
|
||||
|
||||
await tester.pump(slideDuration * 2);
|
||||
|
||||
expect(show.events, ['pause']);
|
||||
});
|
||||
|
||||
slideshowTest('should continue the current slide when resumed', (tester, show) async {
|
||||
await tester.pump(slideDuration * 0.75);
|
||||
|
||||
show.controller.pause();
|
||||
show.controller.resume();
|
||||
|
||||
// Resuming restarts the ticker, so this frame is its new baseline and elapses nothing
|
||||
await tester.pump();
|
||||
|
||||
// A quarter of the slide is left
|
||||
await tester.pump(slideDuration * 0.25 - tick);
|
||||
expect(show.events, ['pause', 'play']);
|
||||
|
||||
await tester.pump(tick * 2);
|
||||
expect(show.events, ['pause', 'play', 'show 1']);
|
||||
});
|
||||
|
||||
slideshowTest('should not watchdog modify a video that is successfully playing back', (tester, show) async {
|
||||
show.videos[0] = Duration.zero;
|
||||
|
||||
for (var i = 1; i <= 3; i++) {
|
||||
// Simulate progress
|
||||
show.videos[0] = slideDuration * i;
|
||||
|
||||
await tester.pump(slideDuration + tick);
|
||||
}
|
||||
|
||||
expect(show.events, isEmpty);
|
||||
});
|
||||
|
||||
slideshowTest('should advance past a stalled video once the slide duration passes', (tester, show) async {
|
||||
show.videos[0] = Duration.zero;
|
||||
|
||||
await tester.pump(slideDuration - tick);
|
||||
expect(show.events, isEmpty);
|
||||
|
||||
await tester.pump(tick * 2);
|
||||
expect(show.events, ['show 1']);
|
||||
});
|
||||
|
||||
slideshowTest('should advance immediately when a video ends', (tester, show) async {
|
||||
show.videos[0] = Duration.zero;
|
||||
|
||||
show.controller.didCompleteVideo();
|
||||
|
||||
expect(show.events, ['show 1']);
|
||||
});
|
||||
|
||||
slideshowTest('should not move when a video ends while paused', (tester, show) async {
|
||||
show.videos[0] = Duration.zero;
|
||||
show.controller.pause();
|
||||
|
||||
show.controller.didCompleteVideo();
|
||||
|
||||
expect(show.events, ['pause']);
|
||||
});
|
||||
|
||||
slideshowTest('should move to next slide when a finished video is resumed', (tester, show) async {
|
||||
show.videos[0] = Duration.zero;
|
||||
show.controller.pause();
|
||||
show.completedVideos.add(0);
|
||||
|
||||
expect(show.events, ['pause']);
|
||||
|
||||
show.controller.resume();
|
||||
|
||||
expect(show.events, ['pause', 'show 1']);
|
||||
});
|
||||
|
||||
slideshowTest('should restart the timer on manual navigation to another slide', (tester, show) async {
|
||||
await tester.pump(slideDuration * 0.60);
|
||||
|
||||
// Widget informs us of a slide change
|
||||
show.controller.didCompleteShowSlide(2);
|
||||
|
||||
// A new slide restarts the ticker, so this frame is its new baseline and elapses nothing
|
||||
await tester.pump();
|
||||
|
||||
// The new slide gets a full slide duration
|
||||
await tester.pump(slideDuration - tick);
|
||||
expect(show.events, isEmpty);
|
||||
|
||||
await tester.pump(tick * 2);
|
||||
expect(show.events, ['show 3']);
|
||||
});
|
||||
|
||||
slideshowTest('should pick up a new next slide when recalculating', (tester, show) async {
|
||||
expect(show.controller.nextIndex, 1);
|
||||
|
||||
show.nextIndex = (_) => 7;
|
||||
show.controller.recalculateNextIndex();
|
||||
|
||||
expect(show.controller.nextIndex, 7);
|
||||
});
|
||||
}
|
||||
|
|
@ -170,6 +170,7 @@ class ServiceMocks {
|
|||
}
|
||||
|
||||
void _stubAssetService() {
|
||||
when(asset.getAsset).thenAnswer((_) async => null);
|
||||
when(asset.update).thenAnswer((_) async {});
|
||||
when(asset.stack).thenAnswer((_) async {});
|
||||
when(asset.unstack).thenAnswer((_) async {});
|
||||
|
|
@ -329,6 +330,9 @@ extension type const UserServiceStub(MockUserService service) implements Stub<Mo
|
|||
}
|
||||
|
||||
extension type const AssetServiceStub(MockAssetService service) implements Stub<MockAssetService> {
|
||||
Future<BaseAsset?> Function() get getAsset =>
|
||||
() => service.getAsset(any());
|
||||
|
||||
Future<void> Function() get update =>
|
||||
() => service.update(
|
||||
any(),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
|
|||
import 'package:drift/native.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
|
|
@ -89,6 +90,12 @@ class PresentationContext {
|
|||
|
||||
void setup() {
|
||||
when(service.user.tryGetMyUser).thenReturn(currentUser);
|
||||
|
||||
// Handle system chrome messages
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(call) async => null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
|
|
@ -98,7 +105,15 @@ class PresentationContext {
|
|||
}
|
||||
|
||||
extension PumpPresentationWidget on WidgetTester {
|
||||
Future<void> pumpTestWidget(PresentationContext context, Widget widget, {List<Override> overrides = const []}) async {
|
||||
/// Renders the UI from the given [widget]
|
||||
///
|
||||
/// Provide [expectSettle] `false` for a component that infinitely animates
|
||||
Future<void> pumpTestWidget(
|
||||
PresentationContext context,
|
||||
Widget widget, {
|
||||
List<Override> overrides = const [],
|
||||
bool expectSettle = true,
|
||||
}) async {
|
||||
await pumpWidget(
|
||||
EasyLocalization(
|
||||
supportedLocales: locales.values.toList(),
|
||||
|
|
@ -126,7 +141,12 @@ extension PumpPresentationWidget on WidgetTester {
|
|||
),
|
||||
),
|
||||
);
|
||||
await pumpAndSettle();
|
||||
|
||||
if (expectSettle) {
|
||||
await pumpAndSettle();
|
||||
} else {
|
||||
await pump();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pumpTestAction(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue