Save to gallery (#619)
* feat: add gal depency to save images in gallery * feat: add save to gallery translations * feat: add save to gallery hook * refactor: extract aspect ratio from dim to utils * refactor: use wn icon button for deleting media in previews * feat: add overlay to video to then have download button * feat: save to gallery in media modal * docs: update changelog * test: improve coverage of chat message media
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:gal/src/gal_platform_interface.dart';
|
||||
import 'package:whitenoise/hooks/use_save_to_gallery.dart';
|
||||
|
||||
import '../test_helpers.dart';
|
||||
|
||||
base class _SuccessGalPlatform extends GalPlatform {
|
||||
@override
|
||||
Future<void> putImage(String path, {String? album}) async {}
|
||||
|
||||
@override
|
||||
Future<void> putVideo(String path, {String? album}) async {}
|
||||
}
|
||||
|
||||
base class _ErrorGalPlatform extends GalPlatform {
|
||||
final GalExceptionType type;
|
||||
_ErrorGalPlatform(this.type);
|
||||
|
||||
@override
|
||||
Future<void> putImage(String path, {String? album}) async {
|
||||
throw GalException(
|
||||
type: type,
|
||||
platformException: PlatformException(code: type.code),
|
||||
stackTrace: StackTrace.current,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> putVideo(String path, {String? album}) async {
|
||||
throw GalException(
|
||||
type: type,
|
||||
platformException: PlatformException(code: type.code),
|
||||
stackTrace: StackTrace.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
base class _GenericErrorGalPlatform extends GalPlatform {
|
||||
@override
|
||||
Future<void> putImage(String path, {String? album}) async {
|
||||
throw Exception('Generic error');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> putVideo(String path, {String? album}) async {
|
||||
throw Exception('Generic error');
|
||||
}
|
||||
}
|
||||
|
||||
base class _SlowGalPlatform extends GalPlatform {
|
||||
final Completer<void> completer;
|
||||
_SlowGalPlatform(this.completer);
|
||||
|
||||
@override
|
||||
Future<void> putImage(String path, {String? album}) => completer.future;
|
||||
|
||||
@override
|
||||
Future<void> putVideo(String path, {String? album}) => completer.future;
|
||||
}
|
||||
|
||||
Widget _buildHookWidget(String path, void Function(SaveToGalleryResult) onResult) {
|
||||
return MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
home: HookBuilder(
|
||||
builder: (context) {
|
||||
final result = useSaveToGallery(localPath: path);
|
||||
onResult(result);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('useSaveToGallery', () {
|
||||
late GalPlatform galPlatform;
|
||||
|
||||
setUp(() {
|
||||
galPlatform = GalPlatform.instance;
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
GalPlatform.instance = galPlatform;
|
||||
});
|
||||
|
||||
test('has correct initial state', () {
|
||||
expect(SaveToGalleryStatus.idle.name, 'idle');
|
||||
expect(SaveToGalleryStatus.saving.name, 'saving');
|
||||
expect(SaveToGalleryStatus.success.name, 'success');
|
||||
expect(SaveToGalleryStatus.error.name, 'error');
|
||||
});
|
||||
|
||||
test('SaveToGalleryResult has correct structure', () {
|
||||
final SaveToGalleryResult result = (
|
||||
status: SaveToGalleryStatus.idle,
|
||||
save: () {},
|
||||
error: null,
|
||||
savedRecently: false,
|
||||
);
|
||||
|
||||
expect(result.status, SaveToGalleryStatus.idle);
|
||||
expect(result.save, isA<void Function()>());
|
||||
expect(result.error, isNull);
|
||||
expect(result.savedRecently, isFalse);
|
||||
});
|
||||
|
||||
test('SaveToGalleryResult has correct structure with error field', () {
|
||||
final SaveToGalleryResult result = (
|
||||
status: SaveToGalleryStatus.error,
|
||||
save: null,
|
||||
error: SaveToGalleryError.unexpected,
|
||||
savedRecently: false,
|
||||
);
|
||||
|
||||
expect(result.status, SaveToGalleryStatus.error);
|
||||
expect(result.save, isNull);
|
||||
expect(result.error, isA<SaveToGalleryError>());
|
||||
expect(result.error, SaveToGalleryError.unexpected);
|
||||
expect(result.savedRecently, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('save() sets error status when localPath is empty', (tester) async {
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: ''),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().status, SaveToGalleryStatus.error);
|
||||
expect(hook().error, SaveToGalleryError.unexpected);
|
||||
});
|
||||
|
||||
testWidgets('savedRecently is false initially', (tester) async {
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: '/some/path'),
|
||||
);
|
||||
|
||||
expect(hook().savedRecently, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('savedRecently becomes true after successful save', (tester) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: '/some/path'),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().savedRecently, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('savedRecently reverts to false after 2 seconds', (tester) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: '/some/path'),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().savedRecently, isTrue);
|
||||
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
|
||||
expect(hook().savedRecently, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('status resets to idle after savedRecently timer fires', (tester) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: '/some/path'),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
|
||||
expect(hook().status, SaveToGalleryStatus.idle);
|
||||
});
|
||||
|
||||
testWidgets('savedRecently resets when localPath changes', (tester) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
|
||||
late SaveToGalleryResult result;
|
||||
await tester.pumpWidget(_buildHookWidget('/path/a', (r) => result = r));
|
||||
await tester.pump();
|
||||
|
||||
result.save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(result.savedRecently, isTrue);
|
||||
|
||||
await tester.pumpWidget(_buildHookWidget('/path/b', (r) => result = r));
|
||||
await tester.pump();
|
||||
|
||||
expect(result.savedRecently, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('error resets when localPath changes', (tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.accessDenied);
|
||||
|
||||
late SaveToGalleryResult result;
|
||||
await tester.pumpWidget(_buildHookWidget('/path/a', (r) => result = r));
|
||||
await tester.pump();
|
||||
|
||||
result.save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(result.error, SaveToGalleryError.accessDenied);
|
||||
|
||||
await tester.pumpWidget(_buildHookWidget('/path/b', (r) => result = r));
|
||||
await tester.pump();
|
||||
|
||||
expect(result.error, isNull);
|
||||
});
|
||||
|
||||
testWidgets('onError called with accessDenied', (tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.accessDenied);
|
||||
|
||||
SaveToGalleryError? receivedError;
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(
|
||||
localPath: '/some/path',
|
||||
onError: (err) => receivedError = err,
|
||||
),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(receivedError, SaveToGalleryError.accessDenied);
|
||||
});
|
||||
|
||||
testWidgets('onError called with notEnoughSpace', (tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.notEnoughSpace);
|
||||
|
||||
SaveToGalleryError? receivedError;
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(
|
||||
localPath: '/some/path',
|
||||
onError: (err) => receivedError = err,
|
||||
),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(receivedError, SaveToGalleryError.notEnoughSpace);
|
||||
});
|
||||
|
||||
testWidgets('onError called with notSupportedFormat', (tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.notSupportedFormat);
|
||||
|
||||
SaveToGalleryError? receivedError;
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(
|
||||
localPath: '/some/path',
|
||||
onError: (err) => receivedError = err,
|
||||
),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(receivedError, SaveToGalleryError.notSupportedFormat);
|
||||
});
|
||||
|
||||
testWidgets('onError called with unexpected GalException', (tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.unexpected);
|
||||
|
||||
SaveToGalleryError? receivedError;
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(
|
||||
localPath: '/some/path',
|
||||
onError: (err) => receivedError = err,
|
||||
),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(receivedError, SaveToGalleryError.unexpected);
|
||||
});
|
||||
|
||||
testWidgets('onError called with unexpected on generic exception', (tester) async {
|
||||
GalPlatform.instance = _GenericErrorGalPlatform();
|
||||
|
||||
SaveToGalleryError? receivedError;
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(
|
||||
localPath: '/some/path',
|
||||
onError: (err) => receivedError = err,
|
||||
),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(receivedError, SaveToGalleryError.unexpected);
|
||||
});
|
||||
|
||||
testWidgets('onError called with unexpected when localPath is empty', (tester) async {
|
||||
SaveToGalleryError? receivedError;
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(
|
||||
localPath: '',
|
||||
onError: (err) => receivedError = err,
|
||||
),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
|
||||
expect(receivedError, SaveToGalleryError.unexpected);
|
||||
});
|
||||
|
||||
testWidgets('save is null while saving', (tester) async {
|
||||
final completer = Completer<void>();
|
||||
GalPlatform.instance = _SlowGalPlatform(completer);
|
||||
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: '/some/path'),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().save, isNull);
|
||||
|
||||
completer.complete();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().save, isNotNull);
|
||||
});
|
||||
|
||||
testWidgets('save() calls putVideo when isVideo is true and succeeds', (tester) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: '/some/video.mp4', isVideo: true),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().savedRecently, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('save() handles GalException when saving video', (tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.accessDenied);
|
||||
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: '/some/video.mp4', isVideo: true),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().error, SaveToGalleryError.accessDenied);
|
||||
});
|
||||
|
||||
testWidgets('save is null while saving video', (tester) async {
|
||||
final completer = Completer<void>();
|
||||
GalPlatform.instance = _SlowGalPlatform(completer);
|
||||
|
||||
final hook = await mountHook(
|
||||
tester,
|
||||
() => useSaveToGallery(localPath: '/some/video.mp4', isVideo: true),
|
||||
);
|
||||
|
||||
hook().save!();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().save, isNull);
|
||||
|
||||
completer.complete();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(hook().save, isNotNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:whitenoise/utils/aspect_ratio.dart';
|
||||
|
||||
void main() {
|
||||
group('getAspectRatioFromDimensions', () {
|
||||
group('returns null', () {
|
||||
test('with null input', () {
|
||||
expect(getAspectRatioFromDimensions(null), isNull);
|
||||
});
|
||||
|
||||
test('with empty string', () {
|
||||
expect(getAspectRatioFromDimensions(''), isNull);
|
||||
});
|
||||
|
||||
test('with missing separator', () {
|
||||
expect(getAspectRatioFromDimensions('1920'), isNull);
|
||||
});
|
||||
|
||||
test('with more than two parts', () {
|
||||
expect(getAspectRatioFromDimensions('1920x1080x10'), isNull);
|
||||
});
|
||||
|
||||
test('with non-numeric width', () {
|
||||
expect(getAspectRatioFromDimensions('foox1080'), isNull);
|
||||
});
|
||||
|
||||
test('with non-numeric height', () {
|
||||
expect(getAspectRatioFromDimensions('1920xbar'), isNull);
|
||||
});
|
||||
|
||||
test('with zero width', () {
|
||||
expect(getAspectRatioFromDimensions('0x1080'), isNull);
|
||||
});
|
||||
|
||||
test('with zero height', () {
|
||||
expect(getAspectRatioFromDimensions('1920x0'), isNull);
|
||||
});
|
||||
|
||||
test('with negative width', () {
|
||||
expect(getAspectRatioFromDimensions('-1920x1080'), isNull);
|
||||
});
|
||||
|
||||
test('with negative height', () {
|
||||
expect(getAspectRatioFromDimensions('1920x-1080'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('returns aspect ratio', () {
|
||||
test('for landscape dimensions', () {
|
||||
expect(getAspectRatioFromDimensions('1920x1080'), 1920 / 1080);
|
||||
});
|
||||
|
||||
test('for portrait dimensions', () {
|
||||
expect(getAspectRatioFromDimensions('1080x1920'), 1080 / 1920);
|
||||
});
|
||||
|
||||
test('for square dimensions', () {
|
||||
expect(getAspectRatioFromDimensions('500x500'), 1.0);
|
||||
});
|
||||
|
||||
test('for decimal dimensions', () {
|
||||
expect(getAspectRatioFromDimensions('100.5x50.25'), 100.5 / 50.25);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -235,6 +235,18 @@ void main() {
|
||||
expect(find.byKey(const Key('media_image')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows video loading indicator while video is downloading', (tester) async {
|
||||
_api.downloadCompleter = Completer<MediaFile>();
|
||||
await mountWidget(
|
||||
ChatMessageMedia(
|
||||
mediaFiles: [_mediaFile(mimeType: 'video/mp4', mediaType: 'video')],
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('media_video_loading_indicator')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows video preview with play indicator on success', (tester) async {
|
||||
setUpFakeVideoPlayerPlatform();
|
||||
final tempDir = Directory.systemTemp.createTempSync('chat_media_video_test');
|
||||
|
||||
@@ -130,6 +130,58 @@ void main() {
|
||||
expect(disposeIndex, lessThan(secondCreateIndex));
|
||||
expect(fakeVideoPlatform.dataSources.last.uri, contains('clip2.mp4'));
|
||||
});
|
||||
|
||||
testWidgets('renders overlay at fallback position before initialization', (tester) async {
|
||||
fakeVideoPlatform.forceInitError = true;
|
||||
|
||||
await mountWidget(
|
||||
LocalVideoPlayer(
|
||||
filePath: videoFile.path,
|
||||
overlay: const SizedBox.square(key: Key('test_overlay'), dimension: 40),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('test_overlay')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders overlay inside video bounds after initialization', (tester) async {
|
||||
await mountWidget(
|
||||
LocalVideoPlayer(
|
||||
filePath: videoFile.path,
|
||||
overlay: const SizedBox.square(key: Key('test_overlay'), dimension: 40),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('video_player')), findsOneWidget);
|
||||
expect(find.byKey(const Key('test_overlay')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hides overlay while video is playing, restores when paused', (tester) async {
|
||||
await mountWidget(
|
||||
LocalVideoPlayer(
|
||||
filePath: videoFile.path,
|
||||
overlay: const SizedBox.square(key: Key('test_overlay'), dimension: 40),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('test_overlay')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('local_video_tap_area')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('test_overlay')), findsNothing);
|
||||
|
||||
await tester.tap(find.byKey(const Key('local_video_tap_area')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('test_overlay')), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('VideoPlayIndicator', () {
|
||||
|
||||
@@ -1,20 +1,74 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:gal/src/gal_platform_interface.dart';
|
||||
import 'package:whitenoise/src/rust/api/media_files.dart';
|
||||
import 'package:whitenoise/src/rust/frb_generated.dart';
|
||||
import 'package:whitenoise/widgets/media_image.dart';
|
||||
import 'package:whitenoise/widgets/media_modal.dart';
|
||||
import 'package:whitenoise/widgets/media_video.dart';
|
||||
import 'package:whitenoise/widgets/wn_avatar.dart';
|
||||
import 'package:whitenoise/widgets/wn_icon.dart';
|
||||
import 'package:whitenoise/widgets/wn_icon_button.dart';
|
||||
import 'package:whitenoise/widgets/wn_overlay.dart';
|
||||
import 'package:whitenoise/widgets/wn_system_notice.dart';
|
||||
|
||||
import '../mocks/mock_wn_api.dart';
|
||||
import '../test_helpers.dart';
|
||||
|
||||
base class _SuccessGalPlatform extends GalPlatform {
|
||||
@override
|
||||
Future<void> putImage(String path, {String? album}) async {}
|
||||
}
|
||||
|
||||
base class _ErrorGalPlatform extends GalPlatform {
|
||||
final GalExceptionType type;
|
||||
_ErrorGalPlatform(this.type);
|
||||
|
||||
@override
|
||||
Future<void> putImage(String path, {String? album}) async {
|
||||
throw GalException(
|
||||
type: type,
|
||||
platformException: PlatformException(code: type.code),
|
||||
stackTrace: StackTrace.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
base class _GenericErrorGalPlatform extends GalPlatform {
|
||||
@override
|
||||
Future<void> putImage(String path, {String? album}) async {
|
||||
throw Exception('Generic error');
|
||||
}
|
||||
}
|
||||
|
||||
base class _VideoSuccessGalPlatform extends GalPlatform {
|
||||
@override
|
||||
Future<void> putVideo(String path, {String? album}) async {}
|
||||
}
|
||||
|
||||
base class _VideoErrorGalPlatform extends GalPlatform {
|
||||
final GalExceptionType type;
|
||||
_VideoErrorGalPlatform(this.type);
|
||||
|
||||
@override
|
||||
Future<void> putVideo(String path, {String? album}) async {
|
||||
throw GalException(
|
||||
type: type,
|
||||
platformException: PlatformException(code: type.code),
|
||||
stackTrace: StackTrace.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MediaFile _mediaFile(
|
||||
String id, {
|
||||
String filePath = '',
|
||||
String? blurhash,
|
||||
String? dimensions,
|
||||
String mimeType = 'image/jpeg',
|
||||
String mediaType = 'image',
|
||||
}) => MediaFile(
|
||||
@@ -29,13 +83,215 @@ MediaFile _mediaFile(
|
||||
blossomUrl: 'https://example.com/$id',
|
||||
nostrKey: 'nostr$id',
|
||||
createdAt: DateTime(2024),
|
||||
fileMetadata: blurhash != null ? FileMetadata(blurhash: blurhash) : null,
|
||||
fileMetadata: (blurhash != null || dimensions != null)
|
||||
? FileMetadata(blurhash: blurhash, dimensions: dimensions)
|
||||
: null,
|
||||
);
|
||||
|
||||
// Minimal valid 1x1 PNG. Required because Image.file with invalid bytes
|
||||
// causes the native image codec to hang the test pump loop.
|
||||
const _minimalPng = <int>[
|
||||
0x89,
|
||||
0x50,
|
||||
0x4E,
|
||||
0x47,
|
||||
0x0D,
|
||||
0x0A,
|
||||
0x1A,
|
||||
0x0A,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0D,
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x08,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x90,
|
||||
0x77,
|
||||
0x53,
|
||||
0xDE,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0C,
|
||||
0x49,
|
||||
0x44,
|
||||
0x41,
|
||||
0x54,
|
||||
0x08,
|
||||
0xD7,
|
||||
0x63,
|
||||
0xF8,
|
||||
0xCF,
|
||||
0xC0,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x01,
|
||||
0xE2,
|
||||
0x21,
|
||||
0xBC,
|
||||
0x33,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x49,
|
||||
0x45,
|
||||
0x4E,
|
||||
0x44,
|
||||
0xAE,
|
||||
0x42,
|
||||
0x60,
|
||||
0x82,
|
||||
];
|
||||
|
||||
Future<void> _openAndTapDownload(
|
||||
WidgetTester tester,
|
||||
String filePath,
|
||||
String id,
|
||||
) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [_mediaFile(id, filePath: filePath)],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.tap(find.byKey(const Key('media_modal_download_button_0')));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
Future<void> _openAndLongPress(
|
||||
WidgetTester tester,
|
||||
String filePath,
|
||||
String id,
|
||||
) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [_mediaFile(id, filePath: filePath)],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.longPress(find.byKey(const Key('media_content_tap_area')));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
Future<void> _openAndTapDownloadVideo(
|
||||
WidgetTester tester,
|
||||
String filePath,
|
||||
String id,
|
||||
) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [
|
||||
_mediaFile(id, filePath: filePath, mimeType: 'video/mp4', mediaType: 'video'),
|
||||
],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.tap(find.byKey(const Key('media_modal_download_button_0')));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
Future<void> _openAndLongPressVideo(
|
||||
WidgetTester tester,
|
||||
String filePath,
|
||||
String id,
|
||||
) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [
|
||||
_mediaFile(id, filePath: filePath, mimeType: 'video/mp4', mediaType: 'video'),
|
||||
],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.longPress(find.byKey(const Key('media_content_tap_area')));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUpAll(() => RustLib.initMock(api: MockWnApi()));
|
||||
|
||||
group('MediaModal', () {
|
||||
late GalPlatform galPlatform;
|
||||
|
||||
setUp(() {
|
||||
galPlatform = GalPlatform.instance;
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
GalPlatform.instance = galPlatform;
|
||||
});
|
||||
|
||||
testWidgets('renders overlay background', (tester) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
@@ -356,6 +612,62 @@ void main() {
|
||||
expect(find.byKey(const Key('media_modal_sender_name')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('wraps download button in AspectRatio when image has dimensions', (tester) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [_mediaFile('1', dimensions: '1600x900')],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final aspectRatio = tester.widget<AspectRatio>(
|
||||
find
|
||||
.ancestor(
|
||||
of: find.byKey(const Key('media_modal_download_button_0')),
|
||||
matching: find.byType(AspectRatio),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
expect(aspectRatio.aspectRatio, 1600 / 900);
|
||||
});
|
||||
|
||||
testWidgets('does not wrap download button in AspectRatio when image has no dimensions', (
|
||||
tester,
|
||||
) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [_mediaFile('1')],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.ancestor(
|
||||
of: find.byKey(const Key('media_modal_download_button_0')),
|
||||
matching: find.byType(AspectRatio),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('page view uses NeverScrollableScrollPhysics when zoomed', (tester) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
@@ -380,5 +692,291 @@ void main() {
|
||||
final pageView = tester.widget<PageView>(find.byKey(const Key('media_page_view')));
|
||||
expect(pageView.physics, isA<NeverScrollableScrollPhysics>());
|
||||
});
|
||||
|
||||
testWidgets('shows error system notice when gallery save fails with permission denied', (
|
||||
tester,
|
||||
) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.accessDenied);
|
||||
final dir = Directory.systemTemp.createTempSync('mm_access');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'save_e1');
|
||||
|
||||
expect(find.byType(WnSystemNotice), findsOneWidget);
|
||||
expect(find.text('Permission denied to save image'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows error system notice when gallery save fails with not enough space', (
|
||||
tester,
|
||||
) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.notEnoughSpace);
|
||||
final dir = Directory.systemTemp.createTempSync('mm_space');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'save_e2');
|
||||
|
||||
expect(find.byType(WnSystemNotice), findsOneWidget);
|
||||
expect(find.text('Not enough storage space'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows error system notice when gallery save fails with unsupported format', (
|
||||
tester,
|
||||
) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.notSupportedFormat);
|
||||
final dir = Directory.systemTemp.createTempSync('mm_format');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'save_e3');
|
||||
|
||||
expect(find.byType(WnSystemNotice), findsOneWidget);
|
||||
expect(find.text('Image format not supported'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'shows error system notice when gallery save fails with unexpected GalException',
|
||||
(tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.unexpected);
|
||||
final dir = Directory.systemTemp.createTempSync('mm_unexpected');
|
||||
final file = File('${dir.path}/test.png')
|
||||
..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'save_e4');
|
||||
|
||||
expect(find.byType(WnSystemNotice), findsOneWidget);
|
||||
expect(find.text('Failed to save image to gallery'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'shows error system notice when gallery save fails with generic exception',
|
||||
(tester) async {
|
||||
GalPlatform.instance = _GenericErrorGalPlatform();
|
||||
final dir = Directory.systemTemp.createTempSync('mm_generic');
|
||||
final file = File('${dir.path}/test.png')
|
||||
..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'save_e5');
|
||||
|
||||
expect(find.byType(WnSystemNotice), findsOneWidget);
|
||||
expect(find.text('Failed to save image to gallery'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('shows checkmark on download button after saving to gallery', (tester) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
final dir = Directory.systemTemp.createTempSync('mm_checkmark');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'save_c1');
|
||||
|
||||
final button = tester.widget<WnIconButton>(
|
||||
find.byKey(const Key('media_modal_download_button_0')),
|
||||
);
|
||||
expect(button.icon, WnIcons.checkmark);
|
||||
});
|
||||
|
||||
testWidgets('checkmark reverts to download icon after timeout', (tester) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
final dir = Directory.systemTemp.createTempSync('mm_revert');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'save_c2');
|
||||
|
||||
var button = tester.widget<WnIconButton>(
|
||||
find.byKey(const Key('media_modal_download_button_0')),
|
||||
);
|
||||
expect(button.icon, WnIcons.checkmark);
|
||||
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
|
||||
button = tester.widget<WnIconButton>(
|
||||
find.byKey(const Key('media_modal_download_button_0')),
|
||||
);
|
||||
expect(button.icon, WnIcons.download);
|
||||
});
|
||||
|
||||
testWidgets('download button re-enables after save so image can be saved multiple times', (
|
||||
tester,
|
||||
) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
final dir = Directory.systemTemp.createTempSync('mm_reenable');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'save_r1');
|
||||
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
|
||||
final button = tester.widget<FilledButton>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('media_modal_download_button_0')),
|
||||
matching: find.byType(FilledButton),
|
||||
),
|
||||
);
|
||||
expect(button.onPressed, isNotNull);
|
||||
});
|
||||
|
||||
testWidgets('long press saves to gallery successfully', (tester) async {
|
||||
GalPlatform.instance = _SuccessGalPlatform();
|
||||
final dir = Directory.systemTemp.createTempSync('mm_lp_success');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndLongPress(tester, file.path, 'lp_s1');
|
||||
|
||||
final button = tester.widget<WnIconButton>(
|
||||
find.byKey(const Key('media_modal_download_button_0')),
|
||||
);
|
||||
expect(button.icon, WnIcons.checkmark);
|
||||
});
|
||||
|
||||
testWidgets('long press shows error notice when save fails', (tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.accessDenied);
|
||||
final dir = Directory.systemTemp.createTempSync('mm_lp_error');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndLongPress(tester, file.path, 'lp_e1');
|
||||
|
||||
expect(find.byType(WnSystemNotice), findsOneWidget);
|
||||
expect(find.text('Permission denied to save image'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('long press is disabled when media is not downloaded', (tester) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [_mediaFile('lp_d1')],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final gestureDetector = tester.widget<GestureDetector>(
|
||||
find.byKey(const Key('media_content_tap_area')),
|
||||
);
|
||||
expect(gestureDetector.onLongPress, isNull);
|
||||
});
|
||||
|
||||
testWidgets('shows download button for video media', (tester) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [
|
||||
_mediaFile('vid_btn_1', mimeType: 'video/mp4', mediaType: 'video'),
|
||||
],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('media_modal_download_button_0')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('video download button saves to gallery successfully', (tester) async {
|
||||
GalPlatform.instance = _VideoSuccessGalPlatform();
|
||||
final dir = Directory.systemTemp.createTempSync('mm_vid_success');
|
||||
final file = File('${dir.path}/test.mp4')..writeAsBytesSync([0]);
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownloadVideo(tester, file.path, 'vid_dl_s1');
|
||||
|
||||
final button = tester.widget<WnIconButton>(
|
||||
find.byKey(const Key('media_modal_download_button_0')),
|
||||
);
|
||||
expect(button.icon, WnIcons.checkmark);
|
||||
});
|
||||
|
||||
testWidgets('video long press saves to gallery successfully', (tester) async {
|
||||
GalPlatform.instance = _VideoSuccessGalPlatform();
|
||||
final dir = Directory.systemTemp.createTempSync('mm_vid_lp_success');
|
||||
final file = File('${dir.path}/test.mp4')..writeAsBytesSync([0]);
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndLongPressVideo(tester, file.path, 'vid_lp_s1');
|
||||
|
||||
final button = tester.widget<WnIconButton>(
|
||||
find.byKey(const Key('media_modal_download_button_0')),
|
||||
);
|
||||
expect(button.icon, WnIcons.checkmark);
|
||||
});
|
||||
|
||||
testWidgets('video long press shows error notice when save fails', (tester) async {
|
||||
GalPlatform.instance = _VideoErrorGalPlatform(GalExceptionType.accessDenied);
|
||||
final dir = Directory.systemTemp.createTempSync('mm_vid_lp_err');
|
||||
final file = File('${dir.path}/test.mp4')..writeAsBytesSync([0]);
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndLongPressVideo(tester, file.path, 'vid_lp_e1');
|
||||
|
||||
expect(find.byType(WnSystemNotice), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('system notice for errors is rendered inside the slate', (tester) async {
|
||||
GalPlatform.instance = _ErrorGalPlatform(GalExceptionType.accessDenied);
|
||||
final dir = Directory.systemTemp.createTempSync('mm_notice_slate');
|
||||
final file = File('${dir.path}/test.png')..writeAsBytesSync(Uint8List.fromList(_minimalPng));
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
await _openAndTapDownload(tester, file.path, 'notice_slate_1');
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('media_modal_slate')),
|
||||
matching: find.byType(WnSystemNotice),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('video download button is overlaid on the video player', (tester) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [
|
||||
_mediaFile('vid_ar_1', mimeType: 'video/mp4', mediaType: 'video'),
|
||||
],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('media_modal_download_button_0')), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(MediaVideo),
|
||||
matching: find.byKey(const Key('media_modal_download_button_0')),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,5 +101,56 @@ void main() {
|
||||
expect(find.byKey(const Key('media_video_player')), findsOneWidget);
|
||||
expect(find.byKey(const Key('video_player')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders overlay on loading placeholder', (tester) async {
|
||||
_api.downloadCompleter = Completer<MediaFile>();
|
||||
|
||||
await mountWidget(
|
||||
MediaVideo(
|
||||
mediaFile: _mediaFile(),
|
||||
overlay: const SizedBox.square(key: Key('test_overlay'), dimension: 40),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('media_video_loading')), findsOneWidget);
|
||||
expect(find.byKey(const Key('test_overlay')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders overlay on error placeholder', (tester) async {
|
||||
_api.shouldFail = true;
|
||||
|
||||
await mountWidget(
|
||||
MediaVideo(
|
||||
mediaFile: _mediaFile(),
|
||||
overlay: const SizedBox.square(key: Key('test_overlay'), dimension: 40),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('media_video_error')), findsOneWidget);
|
||||
expect(find.byKey(const Key('test_overlay')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('passes overlay to local video player when downloaded', (tester) async {
|
||||
setUpFakeVideoPlayerPlatform();
|
||||
final tempDir = Directory.systemTemp.createTempSync('media_video_overlay_test');
|
||||
final tempFile = File('${tempDir.path}/test.mp4');
|
||||
tempFile.writeAsBytesSync([0, 0, 0, 0]);
|
||||
addTearDown(() => tempDir.deleteSync(recursive: true));
|
||||
|
||||
await mountWidget(
|
||||
MediaVideo(
|
||||
mediaFile: _mediaFile(filePath: tempFile.path),
|
||||
overlay: const SizedBox.square(key: Key('test_overlay'), dimension: 40),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('video_player')), findsOneWidget);
|
||||
expect(find.byKey(const Key('test_overlay')), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user