feat: add video media attachment support (#574)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
|
||||
|
||||
class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
|
||||
final calls = <String>[];
|
||||
final dataSources = <DataSource>[];
|
||||
final streams = <int, StreamController<VideoEvent>>{};
|
||||
|
||||
bool forceInitError = false;
|
||||
int nextPlayerId = 0;
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
calls.add('init');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> createWithOptions(VideoCreationOptions options) async {
|
||||
calls.add('createWithOptions');
|
||||
final playerId = nextPlayerId++;
|
||||
final stream = StreamController<VideoEvent>();
|
||||
streams[playerId] = stream;
|
||||
dataSources.add(options.dataSource);
|
||||
|
||||
if (forceInitError) {
|
||||
stream.addError(
|
||||
PlatformException(
|
||||
code: 'VideoError',
|
||||
message: 'Video player failed to initialize',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
stream.add(
|
||||
VideoEvent(
|
||||
eventType: VideoEventType.initialized,
|
||||
size: const Size(640, 360),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return playerId;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<VideoEvent> videoEventsFor(int playerId) {
|
||||
return streams[playerId]!.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose(int playerId) async {
|
||||
calls.add('dispose');
|
||||
final controller = streams.remove(playerId);
|
||||
await controller?.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setLooping(int playerId, bool looping) async {
|
||||
calls.add('setLooping');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play(int playerId) async {
|
||||
calls.add('play');
|
||||
streams[playerId]?.add(
|
||||
VideoEvent(
|
||||
eventType: VideoEventType.isPlayingStateUpdate,
|
||||
isPlaying: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause(int playerId) async {
|
||||
calls.add('pause');
|
||||
streams[playerId]?.add(
|
||||
VideoEvent(
|
||||
eventType: VideoEventType.isPlayingStateUpdate,
|
||||
isPlaying: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setVolume(int playerId, double volume) async {
|
||||
calls.add('setVolume');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setPlaybackSpeed(int playerId, double speed) async {
|
||||
calls.add('setPlaybackSpeed');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Duration> getPosition(int playerId) async {
|
||||
calls.add('getPosition');
|
||||
return Duration.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seekTo(int playerId, Duration position) async {
|
||||
calls.add('seekTo');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildView(int playerId) {
|
||||
return Texture(textureId: playerId);
|
||||
}
|
||||
}
|
||||
|
||||
FakeVideoPlayerPlatform setUpFakeVideoPlayerPlatform() {
|
||||
final fake = FakeVideoPlayerPlatform();
|
||||
VideoPlayerPlatform.instance = fake;
|
||||
return fake;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class _MockImagePicker extends ImagePicker {
|
||||
int pickCallCount = 0;
|
||||
|
||||
@override
|
||||
Future<List<XFile>> pickMultiImage({
|
||||
Future<List<XFile>> pickMultipleMedia({
|
||||
double? maxWidth,
|
||||
double? maxHeight,
|
||||
int? imageQuality,
|
||||
@@ -100,12 +100,12 @@ void main() {
|
||||
expect(getResult().uploadedFiles, isEmpty);
|
||||
});
|
||||
|
||||
group('pickImages', () {
|
||||
testWidgets('calls image picker', (tester) async {
|
||||
group('pickMedia', () {
|
||||
testWidgets('calls media picker', (tester) async {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pump();
|
||||
|
||||
expect(mockPicker.pickCallCount, 1);
|
||||
@@ -115,7 +115,7 @@ void main() {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pump();
|
||||
|
||||
expect(getResult().items, isEmpty);
|
||||
@@ -125,7 +125,7 @@ void main() {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pump();
|
||||
|
||||
expect(getResult().items.length, 1);
|
||||
@@ -133,6 +133,18 @@ void main() {
|
||||
expect(getResult().items.first.filePath, '/path/to/image.jpg');
|
||||
});
|
||||
|
||||
testWidgets('adds item in uploading state when video is selected', (tester) async {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [XFile('/path/to/video.mp4')];
|
||||
|
||||
await getResult().pickMedia();
|
||||
await tester.pump();
|
||||
|
||||
expect(getResult().items.length, 1);
|
||||
expect(getResult().items.first.status, MediaUploadStatus.uploading);
|
||||
expect(getResult().items.first.filePath, '/path/to/video.mp4');
|
||||
});
|
||||
|
||||
testWidgets('adds multiple items when multiple images selected', (tester) async {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [
|
||||
@@ -140,7 +152,7 @@ void main() {
|
||||
XFile('/path/to/image2.jpg'),
|
||||
];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pump();
|
||||
|
||||
expect(getResult().items.length, 2);
|
||||
@@ -154,7 +166,7 @@ void main() {
|
||||
];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(uploadCallCount, 2);
|
||||
@@ -167,14 +179,14 @@ void main() {
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().items.length, 1);
|
||||
final uploadsAfterFirst = uploadCallCount;
|
||||
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().items.length, 1);
|
||||
@@ -186,11 +198,11 @@ void main() {
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image1.jpg')];
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image2.jpg')];
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().items.length, 2);
|
||||
@@ -203,7 +215,7 @@ void main() {
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().items.first.status, MediaUploadStatus.uploaded);
|
||||
@@ -214,7 +226,7 @@ void main() {
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().items.first.file, isNotNull);
|
||||
@@ -226,7 +238,7 @@ void main() {
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().canSend, isTrue);
|
||||
@@ -237,7 +249,7 @@ void main() {
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().uploadedFiles.length, 1);
|
||||
@@ -250,7 +262,7 @@ void main() {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().items.first.status, MediaUploadStatus.error);
|
||||
@@ -261,7 +273,7 @@ void main() {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().items.first.retry, isNotNull);
|
||||
@@ -272,7 +284,7 @@ void main() {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().canSend, isFalse);
|
||||
@@ -283,7 +295,7 @@ void main() {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
expect(getResult().items.first.status, MediaUploadStatus.error);
|
||||
|
||||
@@ -300,7 +312,7 @@ void main() {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final initialUploadCount = uploadCallCount;
|
||||
@@ -324,7 +336,7 @@ void main() {
|
||||
];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
expect(getResult().items.length, 2);
|
||||
|
||||
@@ -340,13 +352,13 @@ void main() {
|
||||
|
||||
mockPicker.filesToReturn = [XFile('/path/to/good.jpg')];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
shouldFailUpload = true;
|
||||
uploadCompleter = Completer<MediaFile>();
|
||||
mockPicker.filesToReturn = [XFile('/path/to/bad.jpg')];
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().canSend, isFalse);
|
||||
@@ -367,7 +379,7 @@ void main() {
|
||||
];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
expect(getResult().items.length, 2);
|
||||
|
||||
@@ -382,7 +394,7 @@ void main() {
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
expect(getResult().canSend, isTrue);
|
||||
|
||||
@@ -397,7 +409,7 @@ void main() {
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
expect(getResult().uploadedFiles, isNotEmpty);
|
||||
|
||||
@@ -413,7 +425,7 @@ void main() {
|
||||
final getResult = await pump(tester);
|
||||
mockPicker.filesToReturn = [XFile('/path/to/image.jpg')];
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pump();
|
||||
|
||||
expect(getResult().items.first.status, MediaUploadStatus.uploading);
|
||||
@@ -425,13 +437,13 @@ void main() {
|
||||
|
||||
mockPicker.filesToReturn = [XFile('/path/to/good.jpg')];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
shouldFailUpload = true;
|
||||
uploadCompleter = Completer<MediaFile>();
|
||||
mockPicker.filesToReturn = [XFile('/path/to/bad.jpg')];
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().canSend, isFalse);
|
||||
@@ -445,7 +457,7 @@ void main() {
|
||||
];
|
||||
uploadCompleter.complete(_mediaFile());
|
||||
|
||||
await getResult().pickImages();
|
||||
await getResult().pickMedia();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getResult().canSend, isTrue);
|
||||
|
||||
@@ -44,6 +44,11 @@ class _MockImagePickerPlatform extends ImagePickerPlatform with MockPlatformInte
|
||||
}) async {
|
||||
return filesToReturn;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XFile>> getMedia({required MediaOptions options}) async {
|
||||
return filesToReturn;
|
||||
}
|
||||
}
|
||||
|
||||
class _MockTag implements Tag {
|
||||
@@ -2033,7 +2038,7 @@ void main() {
|
||||
ImagePickerPlatform.instance = mockImagePicker;
|
||||
});
|
||||
|
||||
testWidgets('shows media upload preview when images are picked', (tester) async {
|
||||
testWidgets('shows media upload preview when media is picked', (tester) async {
|
||||
mockImagePicker.filesToReturn = [XFile('/tmp/test_image.jpg')];
|
||||
_api.initialMessages = [_message('m1', DateTime(2024))];
|
||||
await pumpChatScreen(tester);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:whitenoise/src/rust/api/media_files.dart';
|
||||
import 'package:whitenoise/utils/media_type.dart';
|
||||
|
||||
import '../test_helpers.dart';
|
||||
|
||||
MediaFile _mediaFile({
|
||||
String mimeType = 'image/jpeg',
|
||||
String mediaType = 'image',
|
||||
}) => MediaFile(
|
||||
id: 'media1',
|
||||
mlsGroupId: testGroupId,
|
||||
accountPubkey: testPubkeyA,
|
||||
filePath: '/path/to/file',
|
||||
originalFileHash: 'hash123',
|
||||
encryptedFileHash: 'encrypted123',
|
||||
mimeType: mimeType,
|
||||
mediaType: mediaType,
|
||||
blossomUrl: 'https://example.com/media',
|
||||
nostrKey: 'nostr123',
|
||||
createdAt: DateTime(2024),
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('media type helpers', () {
|
||||
test('detects video media type', () {
|
||||
expect(isVideoMediaFile(_mediaFile(mediaType: 'video')), isTrue);
|
||||
});
|
||||
|
||||
test('detects video mime type', () {
|
||||
expect(isVideoMediaFile(_mediaFile(mimeType: 'video/mp4')), isTrue);
|
||||
});
|
||||
|
||||
test('detects video mime type with parameters', () {
|
||||
expect(isVideoMediaFile(_mediaFile(mimeType: 'video/mp4; codecs=h264')), isTrue);
|
||||
});
|
||||
|
||||
test('does not treat images as video', () {
|
||||
expect(isVideoMediaFile(_mediaFile()), isFalse);
|
||||
});
|
||||
|
||||
test('does not treat malformed media values as video', () {
|
||||
expect(isVideoMediaFile(_mediaFile(mimeType: 'video/', mediaType: '')), isFalse);
|
||||
expect(isVideoMediaFile(_mediaFile(mimeType: 'notvideo/mp4', mediaType: '')), isFalse);
|
||||
expect(isVideoMediaFile(_mediaFile(mimeType: '', mediaType: '')), isFalse);
|
||||
});
|
||||
|
||||
test('detects common video file extensions', () {
|
||||
expect(isVideoFilePath('/path/to/clip.mp4'), isTrue);
|
||||
expect(isVideoFilePath('/path/to/clip.mov'), isTrue);
|
||||
});
|
||||
|
||||
test('does not treat image file paths as video', () {
|
||||
expect(isVideoFilePath('/path/to/photo.jpg'), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -193,7 +193,7 @@ void main() {
|
||||
expect(item.subtitle, 'Hello from pending chat');
|
||||
});
|
||||
|
||||
testWidgets('shows photo subtitle and icon for media-only message', (tester) async {
|
||||
testWidgets('shows media subtitle and icon for media-only message', (tester) async {
|
||||
await pumpTile(
|
||||
tester,
|
||||
_chatSummary(
|
||||
@@ -204,12 +204,12 @@ void main() {
|
||||
);
|
||||
final finder = find.byType(WnChatListItem);
|
||||
final item = tester.widget<WnChatListItem>(finder);
|
||||
expect(item.subtitle, 'Photo');
|
||||
expect(item.subtitle, 'Media item');
|
||||
expect(item.subtitleIcon, isNotNull);
|
||||
expect(find.byKey(const Key('media_subtitle_icon')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows photos subtitle for multiple media-only message', (tester) async {
|
||||
testWidgets('shows media subtitle for multiple media-only message', (tester) async {
|
||||
await pumpTile(
|
||||
tester,
|
||||
_chatSummary(
|
||||
@@ -220,7 +220,7 @@ void main() {
|
||||
);
|
||||
final finder = find.byType(WnChatListItem);
|
||||
final item = tester.widget<WnChatListItem>(finder);
|
||||
expect(item.subtitle, 'Photos');
|
||||
expect(item.subtitle, 'Media items');
|
||||
expect(item.subtitleIcon, isNotNull);
|
||||
});
|
||||
|
||||
@@ -328,7 +328,7 @@ void main() {
|
||||
expect(item.subtitle, 'Group message in pending chat');
|
||||
});
|
||||
|
||||
testWidgets('shows photo subtitle and icon for media-only message', (tester) async {
|
||||
testWidgets('shows media subtitle and icon for media-only message', (tester) async {
|
||||
await pumpTile(
|
||||
tester,
|
||||
_chatSummary(
|
||||
@@ -338,12 +338,12 @@ void main() {
|
||||
);
|
||||
final finder = find.byType(WnChatListItem);
|
||||
final item = tester.widget<WnChatListItem>(finder);
|
||||
expect(item.subtitle, 'Photo');
|
||||
expect(item.subtitle, 'Media item');
|
||||
expect(item.subtitleIcon, isNotNull);
|
||||
expect(find.byKey(const Key('media_subtitle_icon')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows photos subtitle for multiple media-only message', (tester) async {
|
||||
testWidgets('shows media subtitle for multiple media-only message', (tester) async {
|
||||
await pumpTile(
|
||||
tester,
|
||||
_chatSummary(
|
||||
@@ -353,7 +353,7 @@ void main() {
|
||||
);
|
||||
final finder = find.byType(WnChatListItem);
|
||||
final item = tester.widget<WnChatListItem>(finder);
|
||||
expect(item.subtitle, 'Photos');
|
||||
expect(item.subtitle, 'Media items');
|
||||
expect(item.subtitleIcon, isNotNull);
|
||||
});
|
||||
|
||||
@@ -501,7 +501,7 @@ void main() {
|
||||
expect(item.subtitle, '');
|
||||
});
|
||||
|
||||
testWidgets('shows "Photo" and image icon when single media-only message', (
|
||||
testWidgets('shows "Media item" and media icon when single media-only message', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpTile(
|
||||
@@ -510,12 +510,12 @@ void main() {
|
||||
);
|
||||
final finder = find.byType(WnChatListItem);
|
||||
final item = tester.widget<WnChatListItem>(finder);
|
||||
expect(item.subtitle, 'Photo');
|
||||
expect(item.subtitle, 'Media item');
|
||||
expect(item.subtitleIcon, isNotNull);
|
||||
expect(find.byKey(const Key('media_subtitle_icon')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows "Photos" and image icon when multiple media-only message', (
|
||||
testWidgets('shows "Media items" and media icon when multiple media-only message', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpTile(
|
||||
@@ -524,7 +524,7 @@ void main() {
|
||||
);
|
||||
final finder = find.byType(WnChatListItem);
|
||||
final item = tester.widget<WnChatListItem>(finder);
|
||||
expect(item.subtitle, 'Photos');
|
||||
expect(item.subtitle, 'Media items');
|
||||
expect(item.subtitleIcon, isNotNull);
|
||||
expect(find.byKey(const Key('media_subtitle_icon')), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:whitenoise/src/rust/frb_generated.dart';
|
||||
import 'package:whitenoise/widgets/chat_media_thumbnail.dart';
|
||||
import 'package:whitenoise/widgets/wn_media_thumbnail.dart';
|
||||
|
||||
import '../fakes/fake_video_player_platform.dart';
|
||||
import '../mocks/mock_wn_api.dart';
|
||||
import '../test_helpers.dart';
|
||||
|
||||
@@ -17,6 +18,8 @@ MediaFile _mediaFile({
|
||||
String? originalFileHash = 'hash123',
|
||||
String? blurhash,
|
||||
String? thumbhash,
|
||||
String mimeType = 'image/jpeg',
|
||||
String mediaType = 'image',
|
||||
}) => MediaFile(
|
||||
id: id,
|
||||
mlsGroupId: testGroupId,
|
||||
@@ -24,8 +27,8 @@ MediaFile _mediaFile({
|
||||
filePath: filePath,
|
||||
originalFileHash: originalFileHash,
|
||||
encryptedFileHash: 'encrypted123',
|
||||
mimeType: 'image/jpeg',
|
||||
mediaType: 'image',
|
||||
mimeType: mimeType,
|
||||
mediaType: mediaType,
|
||||
blossomUrl: 'https://example.com/media',
|
||||
nostrKey: 'nostr123',
|
||||
createdAt: DateTime(2024),
|
||||
@@ -104,6 +107,32 @@ void main() {
|
||||
expect(find.byKey(const Key('thumbnail_image')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows video thumbnail with play indicator when file exists locally', (
|
||||
tester,
|
||||
) async {
|
||||
setUpFakeVideoPlayerPlatform();
|
||||
final tempDir = Directory.systemTemp.createTempSync('thumbnail_video_test');
|
||||
final tempFile = File('${tempDir.path}/test.mp4');
|
||||
tempFile.writeAsBytesSync([0, 0, 0, 0]);
|
||||
addTearDown(() => tempDir.deleteSync(recursive: true));
|
||||
|
||||
await mountWidget(
|
||||
ChatMediaThumbnail(
|
||||
mediaFile: _mediaFile(
|
||||
filePath: tempFile.path,
|
||||
mimeType: 'video/mp4',
|
||||
mediaType: 'video',
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('thumbnail_video')), findsOneWidget);
|
||||
expect(find.byKey(const Key('thumbnail_video_indicator')), findsOneWidget);
|
||||
expect(find.byKey(const Key('thumbnail_image')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('fade transition animates from 0 to 1', (tester) async {
|
||||
_api.downloadCompleter = Completer<MediaFile>();
|
||||
final tempDir = Directory.systemTemp.createTempSync('thumbnail_fade_test');
|
||||
|
||||
@@ -7,12 +7,14 @@ import 'package:whitenoise/widgets/chat_media_upload_preview.dart';
|
||||
import 'package:whitenoise/widgets/wn_media_preview.dart';
|
||||
import 'package:whitenoise/widgets/wn_spinner.dart';
|
||||
|
||||
import '../fakes/fake_video_player_platform.dart';
|
||||
import '../test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
late File testImageFile;
|
||||
late File testImageFile2;
|
||||
late File testVideoFile;
|
||||
|
||||
setUpAll(() {
|
||||
tempDir = Directory.systemTemp.createTempSync('chat_media_preview_test');
|
||||
@@ -20,6 +22,8 @@ void main() {
|
||||
testImageFile.writeAsBytesSync([0xFF, 0xD8, 0xFF, 0xE0]);
|
||||
testImageFile2 = File('${tempDir.path}/test2.jpg');
|
||||
testImageFile2.writeAsBytesSync([0xFF, 0xD8, 0xFF, 0xE0]);
|
||||
testVideoFile = File('${tempDir.path}/test.mp4');
|
||||
testVideoFile.writeAsBytesSync([0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
@@ -62,6 +66,22 @@ void main() {
|
||||
expect(find.byKey(const Key('chat_media_upload_preview')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders video tile for video files', (tester) async {
|
||||
setUpFakeVideoPlayerPlatform();
|
||||
|
||||
await mountWidget(
|
||||
ChatMediaUploadPreview(
|
||||
items: [createItem(filePath: testVideoFile.path)],
|
||||
onRemove: (_) {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('video_tile_player')), findsOneWidget);
|
||||
expect(find.byKey(const Key('video_tile_indicator')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows uploading overlay when current item is uploading', (tester) async {
|
||||
await mountWidget(
|
||||
ChatMediaUploadPreview(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:whitenoise/src/rust/frb_generated.dart';
|
||||
import 'package:whitenoise/widgets/chat_message_media.dart';
|
||||
import 'package:whitenoise/widgets/wn_message_media.dart';
|
||||
|
||||
import '../fakes/fake_video_player_platform.dart';
|
||||
import '../mocks/mock_wn_api.dart';
|
||||
import '../test_helpers.dart';
|
||||
|
||||
@@ -17,6 +18,8 @@ MediaFile _mediaFile({
|
||||
String? originalFileHash = 'hash123',
|
||||
String? blurhash,
|
||||
String? thumbhash,
|
||||
String mimeType = 'image/jpeg',
|
||||
String mediaType = 'image',
|
||||
}) => MediaFile(
|
||||
id: id,
|
||||
mlsGroupId: testGroupId,
|
||||
@@ -24,8 +27,8 @@ MediaFile _mediaFile({
|
||||
filePath: filePath,
|
||||
originalFileHash: originalFileHash,
|
||||
encryptedFileHash: 'encrypted123',
|
||||
mimeType: 'image/jpeg',
|
||||
mediaType: 'image',
|
||||
mimeType: mimeType,
|
||||
mediaType: mediaType,
|
||||
blossomUrl: 'https://example.com/media',
|
||||
nostrKey: 'nostr123',
|
||||
createdAt: DateTime(2024),
|
||||
@@ -232,6 +235,32 @@ void main() {
|
||||
expect(find.byKey(const Key('media_image')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows video preview with play indicator on success', (tester) async {
|
||||
setUpFakeVideoPlayerPlatform();
|
||||
final tempDir = Directory.systemTemp.createTempSync('chat_media_video_test');
|
||||
final tempFile = File('${tempDir.path}/test.mp4');
|
||||
tempFile.writeAsBytesSync([0, 0, 0, 0]);
|
||||
addTearDown(() => tempDir.deleteSync(recursive: true));
|
||||
|
||||
await mountWidget(
|
||||
ChatMessageMedia(
|
||||
mediaFiles: [
|
||||
_mediaFile(
|
||||
filePath: tempFile.path,
|
||||
mimeType: 'video/mp4',
|
||||
mediaType: 'video',
|
||||
),
|
||||
],
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('media_video')), findsOneWidget);
|
||||
expect(find.byKey(const Key('media_video_indicator')), findsOneWidget);
|
||||
expect(find.byKey(const Key('media_image')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows blurhash placeholder when blurhash provided', (tester) async {
|
||||
_api.downloadCompleter = Completer<MediaFile>();
|
||||
await mountWidget(
|
||||
|
||||
@@ -86,6 +86,8 @@ MediaFile _mediaFile({
|
||||
String id = 'media1',
|
||||
String filePath = '',
|
||||
String? originalFileHash = 'hash123',
|
||||
String mimeType = 'image/jpeg',
|
||||
String mediaType = 'image',
|
||||
}) => MediaFile(
|
||||
id: id,
|
||||
mlsGroupId: testGroupId,
|
||||
@@ -93,8 +95,8 @@ MediaFile _mediaFile({
|
||||
filePath: filePath,
|
||||
originalFileHash: originalFileHash,
|
||||
encryptedFileHash: 'encrypted123',
|
||||
mimeType: 'image/jpeg',
|
||||
mediaType: 'image',
|
||||
mimeType: mimeType,
|
||||
mediaType: mediaType,
|
||||
blossomUrl: 'https://example.com/media',
|
||||
nostrKey: 'nostr123',
|
||||
createdAt: DateTime(2024),
|
||||
@@ -342,6 +344,24 @@ void main() {
|
||||
expect(find.byKey(const Key('quote_thumbnail')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows video thumbnail for video media', (tester) async {
|
||||
await mountWidget(
|
||||
ChatMessageQuote(
|
||||
data: _quoteData(
|
||||
mediaFile: _mediaFile(
|
||||
mimeType: 'video/mp4',
|
||||
mediaType: 'video',
|
||||
),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('quote_thumbnail')), findsOneWidget);
|
||||
expect(find.byKey(const Key('video_play_indicator')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('does not show thumbnail when no media file', (tester) async {
|
||||
await mountWidget(
|
||||
ChatMessageQuote(data: _quoteData()),
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:whitenoise/widgets/local_video_player.dart';
|
||||
|
||||
import '../fakes/fake_video_player_platform.dart';
|
||||
import '../test_helpers.dart';
|
||||
|
||||
Future<void> pumpUntil(
|
||||
WidgetTester tester,
|
||||
bool Function() condition,
|
||||
) async {
|
||||
await tester.runAsync(() async {
|
||||
for (var i = 0; i < 20; i++) {
|
||||
if (condition()) return;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
late FakeVideoPlayerPlatform fakeVideoPlatform;
|
||||
late Directory tempDir;
|
||||
late File videoFile;
|
||||
|
||||
setUp(() {
|
||||
fakeVideoPlatform = setUpFakeVideoPlayerPlatform();
|
||||
tempDir = Directory.systemTemp.createTempSync('local_video_player_test');
|
||||
videoFile = File('${tempDir.path}/clip.mp4')..writeAsBytesSync([0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
group('LocalVideoPlayer', () {
|
||||
testWidgets('initializes a file video player', (tester) async {
|
||||
await mountWidget(
|
||||
LocalVideoPlayer(filePath: videoFile.path),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('video_player')), findsOneWidget);
|
||||
expect(fakeVideoPlatform.calls, contains('createWithOptions'));
|
||||
expect(fakeVideoPlatform.dataSources.first.uri, startsWith('file://'));
|
||||
});
|
||||
|
||||
testWidgets('shows play indicator while paused', (tester) async {
|
||||
await mountWidget(
|
||||
LocalVideoPlayer(filePath: videoFile.path),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('video_play_indicator')), findsOneWidget);
|
||||
expect(find.byKey(const Key('video_play_icon')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('toggles playback when tapped', (tester) async {
|
||||
await mountWidget(
|
||||
LocalVideoPlayer(filePath: videoFile.path),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('local_video_tap_area')));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const Key('local_video_tap_area')));
|
||||
await tester.pump();
|
||||
|
||||
expect(fakeVideoPlatform.calls, contains('play'));
|
||||
expect(fakeVideoPlatform.calls, contains('pause'));
|
||||
});
|
||||
|
||||
testWidgets('shows error placeholder when initialization fails', (tester) async {
|
||||
fakeVideoPlatform.forceInitError = true;
|
||||
|
||||
await mountWidget(
|
||||
LocalVideoPlayer(filePath: videoFile.path),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('video_error_placeholder')), findsOneWidget);
|
||||
expect(find.byKey(const Key('video_player')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('hides controls when showControls is false', (tester) async {
|
||||
await mountWidget(
|
||||
LocalVideoPlayer(filePath: videoFile.path, showControls: false),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('video_play_indicator')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('disposes and reinitializes when filePath changes', (tester) async {
|
||||
final secondVideoFile = File('${tempDir.path}/clip2.mp4')..writeAsBytesSync([0, 0, 0, 0]);
|
||||
var currentPath = videoFile.path;
|
||||
late StateSetter setState;
|
||||
|
||||
await mountWidget(
|
||||
StatefulBuilder(
|
||||
builder: (context, setStateCallback) {
|
||||
setState = setStateCallback;
|
||||
return LocalVideoPlayer(filePath: currentPath);
|
||||
},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
setState(() => currentPath = secondVideoFile.path);
|
||||
await tester.pumpAndSettle();
|
||||
await pumpUntil(
|
||||
tester,
|
||||
() => fakeVideoPlatform.calls.where((call) => call == 'createWithOptions').length == 2,
|
||||
);
|
||||
|
||||
expect(fakeVideoPlatform.calls.where((call) => call == 'createWithOptions'), hasLength(2));
|
||||
final firstCreateIndex = fakeVideoPlatform.calls.indexOf('createWithOptions');
|
||||
final disposeIndex = fakeVideoPlatform.calls.indexOf('dispose');
|
||||
final secondCreateIndex = fakeVideoPlatform.calls.lastIndexOf('createWithOptions');
|
||||
|
||||
expect(disposeIndex, greaterThan(firstCreateIndex));
|
||||
expect(disposeIndex, lessThan(secondCreateIndex));
|
||||
expect(fakeVideoPlatform.dataSources.last.uri, contains('clip2.mp4'));
|
||||
});
|
||||
});
|
||||
|
||||
group('VideoPlayIndicator', () {
|
||||
testWidgets('honors visible and size parameters', (tester) async {
|
||||
await mountWidget(
|
||||
const VideoPlayIndicator(visible: false, size: 32),
|
||||
tester,
|
||||
);
|
||||
|
||||
final opacity = tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity));
|
||||
final container = tester.widget<Container>(find.byType(Container));
|
||||
|
||||
expect(opacity.opacity, 0);
|
||||
expect(container.constraints?.maxWidth, 32);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,21 +4,28 @@ 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_overlay.dart';
|
||||
|
||||
import '../mocks/mock_wn_api.dart';
|
||||
import '../test_helpers.dart';
|
||||
|
||||
MediaFile _mediaFile(String id, {String filePath = '', String? blurhash}) => MediaFile(
|
||||
MediaFile _mediaFile(
|
||||
String id, {
|
||||
String filePath = '',
|
||||
String? blurhash,
|
||||
String mimeType = 'image/jpeg',
|
||||
String mediaType = 'image',
|
||||
}) => MediaFile(
|
||||
id: id,
|
||||
mlsGroupId: testGroupId,
|
||||
accountPubkey: testPubkeyA,
|
||||
filePath: filePath,
|
||||
originalFileHash: 'hash$id',
|
||||
encryptedFileHash: 'encrypted$id',
|
||||
mimeType: 'image/jpeg',
|
||||
mediaType: 'image',
|
||||
mimeType: mimeType,
|
||||
mediaType: mediaType,
|
||||
blossomUrl: 'https://example.com/$id',
|
||||
nostrKey: 'nostr$id',
|
||||
createdAt: DateTime(2024),
|
||||
@@ -71,6 +78,33 @@ void main() {
|
||||
expect(find.byKey(const Key('media_thumbnail_strip')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('renders video media with video viewer', (tester) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () => MediaModal.show(
|
||||
context: context,
|
||||
mediaFiles: [
|
||||
_mediaFile(
|
||||
'1',
|
||||
mimeType: 'video/mp4',
|
||||
mediaType: 'video',
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MediaVideo), findsOneWidget);
|
||||
expect(find.byKey(const Key('media_image_0')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('renders thumbnail strip for multiple media', (tester) async {
|
||||
await mountWidget(
|
||||
Builder(
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:whitenoise/src/rust/api/media_files.dart';
|
||||
import 'package:whitenoise/src/rust/frb_generated.dart';
|
||||
import 'package:whitenoise/widgets/media_video.dart';
|
||||
|
||||
import '../fakes/fake_video_player_platform.dart';
|
||||
import '../mocks/mock_wn_api.dart';
|
||||
import '../test_helpers.dart';
|
||||
|
||||
MediaFile _mediaFile({
|
||||
String id = 'media1',
|
||||
String filePath = '',
|
||||
String? originalFileHash = 'hash123',
|
||||
String? blurhash,
|
||||
}) => MediaFile(
|
||||
id: id,
|
||||
mlsGroupId: testGroupId,
|
||||
accountPubkey: testPubkeyA,
|
||||
filePath: filePath,
|
||||
originalFileHash: originalFileHash,
|
||||
encryptedFileHash: 'encrypted123',
|
||||
mimeType: 'video/mp4',
|
||||
mediaType: 'video',
|
||||
blossomUrl: 'https://example.com/media',
|
||||
nostrKey: 'nostr123',
|
||||
createdAt: DateTime(2024),
|
||||
fileMetadata: blurhash != null ? FileMetadata(blurhash: blurhash) : null,
|
||||
);
|
||||
|
||||
class _MockApi extends MockWnApi {
|
||||
Completer<MediaFile>? downloadCompleter;
|
||||
bool shouldFail = false;
|
||||
|
||||
@override
|
||||
Future<MediaFile> crateApiMediaFilesDownloadChatMedia({
|
||||
required String accountPubkey,
|
||||
required String groupId,
|
||||
required String originalFileHash,
|
||||
}) async {
|
||||
if (shouldFail) throw Exception('Download failed');
|
||||
if (downloadCompleter != null) return downloadCompleter!.future;
|
||||
return _mediaFile(filePath: '/downloaded/path.mp4');
|
||||
}
|
||||
}
|
||||
|
||||
final _api = _MockApi();
|
||||
|
||||
void main() {
|
||||
setUpAll(() => RustLib.initMock(api: _api));
|
||||
|
||||
setUp(() {
|
||||
_api.downloadCompleter = null;
|
||||
_api.shouldFail = false;
|
||||
});
|
||||
|
||||
group('MediaVideo', () {
|
||||
testWidgets('shows loading placeholder while downloading', (tester) async {
|
||||
_api.downloadCompleter = Completer<MediaFile>();
|
||||
|
||||
await mountWidget(
|
||||
MediaVideo(
|
||||
mediaFile: _mediaFile(blurhash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('media_video_loading')), findsOneWidget);
|
||||
expect(find.byKey(const Key('blurhash_placeholder')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows error placeholder when download fails', (tester) async {
|
||||
_api.shouldFail = true;
|
||||
|
||||
await mountWidget(
|
||||
MediaVideo(mediaFile: _mediaFile()),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('media_video_error')), findsOneWidget);
|
||||
expect(find.byKey(const Key('media_video_player')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows local video player when file exists locally', (tester) async {
|
||||
setUpFakeVideoPlayerPlatform();
|
||||
final tempDir = Directory.systemTemp.createTempSync('media_video_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)),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('media_video_player')), findsOneWidget);
|
||||
expect(find.byKey(const Key('video_player')), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,16 +6,20 @@ import 'package:whitenoise/hooks/use_media_upload.dart' show MediaUploadItem, Me
|
||||
import 'package:whitenoise/widgets/wn_media_upload_preview.dart';
|
||||
import 'package:whitenoise/widgets/wn_spinner.dart';
|
||||
|
||||
import '../fakes/fake_video_player_platform.dart';
|
||||
import '../test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
late File testImageFile;
|
||||
late File testVideoFile;
|
||||
|
||||
setUpAll(() {
|
||||
tempDir = Directory.systemTemp.createTempSync('media_preview_test');
|
||||
testImageFile = File('${tempDir.path}/test.jpg');
|
||||
testImageFile.writeAsBytesSync([0xFF, 0xD8, 0xFF, 0xE0]);
|
||||
testVideoFile = File('${tempDir.path}/test.mp4');
|
||||
testVideoFile.writeAsBytesSync([0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
@@ -79,6 +83,25 @@ void main() {
|
||||
expect(find.byKey(Key('thumbnail_${testImageFile.path}')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays video thumbnail for video items', (tester) async {
|
||||
setUpFakeVideoPlayerPlatform();
|
||||
|
||||
await mountWidget(
|
||||
WnMediaUploadPreview(
|
||||
items: [
|
||||
createItem(filePath: testVideoFile.path, status: MediaUploadStatus.uploaded),
|
||||
],
|
||||
onRemove: (_) {},
|
||||
onAddMore: () {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('thumbnail_video_player')), findsOneWidget);
|
||||
expect(find.byKey(const Key('thumbnail_video_indicator')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays multiple thumbnails', (tester) async {
|
||||
final file1 = File('${tempDir.path}/test1.jpg')..writeAsBytesSync([0xFF, 0xD8, 0xFF, 0xE0]);
|
||||
final file2 = File('${tempDir.path}/test2.jpg')..writeAsBytesSync([0xFF, 0xD8, 0xFF, 0xE0]);
|
||||
|
||||
@@ -115,6 +115,19 @@ void main() {
|
||||
expect(find.byKey(const Key('quote_thumbnail')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows thumbnail when mediaThumbnail is provided', (tester) async {
|
||||
await mountWidget(
|
||||
const WnMessageQuote(
|
||||
author: 'Alice',
|
||||
text: 'Hello',
|
||||
mediaThumbnail: ColoredBox(color: Colors.black),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('quote_thumbnail')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hides thumbnail when image is null', (tester) async {
|
||||
await mountWidget(
|
||||
const WnMessageQuote(author: 'Alice', text: 'Hello'),
|
||||
|
||||
Reference in New Issue
Block a user