Skip to content

Commit

Permalink
compose: Support images from keyboard for Android
Browse files Browse the repository at this point in the history
Fixes: zulip#419
Fixes: zulip#1173

Signed-off-by: Zixuan James Li <zixuan@zulip.com>
  • Loading branch information
PIG208 committed Feb 26, 2025
1 parent de2b4f6 commit e809a6f
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 0 deletions.
8 changes: 8 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,14 @@
"@topicValidationErrorMandatoryButEmpty": {
"description": "Topic validation error when topic is required but was empty."
},
"errorContentNotInsertedTitle": "Content not inserted",
"@errorContentNotInsertedTitle": {
"description": "Title for error dialog when an attempt to insert rich content failed."
},
"errorContentToInsertIsEmpty": "The file to be inserted is empty or cannot be accessed.",
"@errorContentToInsertIsEmpty": {
"description": "Error message when the rich content to be inserted is empty or cannot be accessed."
},
"errorInvalidApiKeyMessage": "Your account at {url} could not be authenticated. Please try logging in again or use another account.",
"@errorInvalidApiKeyMessage": {
"description": "Error message in the dialog for invalid API key.",
Expand Down
101 changes: 101 additions & 0 deletions test/widgets/compose_box_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io';

import 'package:checks/checks.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:flutter_checks/flutter_checks.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -831,6 +832,106 @@ void main() {
// target platform the test is simulating.
// TODO(upstream): unskip after fix to https://github.com/flutter/flutter/issues/161073
skip: Platform.isWindows);

group('attach from keyboard', () {
// This is adapted from:
// https://github.com/flutter/flutter/blob/0ffc4ce00/packages/flutter/test/widgets/editable_text_test.dart#L724-L740
Future<void> insertContentFromKeyboard(WidgetTester tester, {
required List<int>? data,
required String attachedFileUrl,
required String mimeType,
}) async {
await tester.showKeyboard(contentInputFinder);
// This invokes [EditableText.performAction] on the content [TextField],
// which did not expose an API for testing.
// TODO(upstream): support a better API for testing this
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
MethodCall('TextInputClient.performAction', <dynamic>[
-1,
'TextInputAction.commitContent',
// This fakes data originally provided by the Flutter engine:
// https://github.com/flutter/flutter/blob/0ffc4ce00/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java#L497-L548
{
"mimeType": mimeType,
"data": data,
"uri": attachedFileUrl,
},
])),
(ByteData? data) {});
}

testWidgets('success', (tester) async {
const fileContent = [1, 0, 1, 0, 0];
await prepare(tester);
const uploadUrl = '/user_uploads/1/4e/m2A3MSqFnWRLUf9SaPzQ0Up_/test.gif';
connection.prepare(json: UploadFileResult(uri: uploadUrl).toJson());
await insertContentFromKeyboard(tester,
data: fileContent,
attachedFileUrl:
'content://com.zulip.android.zulipboard.provider'
'/root/com.zulip.android.zulipboard/candidate_temp/test.gif',
mimeType: 'image/gif');

await tester.pump();
check(controller!.content.text)
.equals('see image: [Uploading test.gif…]()\n\n');
// (the request is checked more thoroughly in API tests)
check(connection.lastRequest!).isA<http.MultipartRequest>()
..method.equals('POST')
..files.single.which((it) => it
..field.equals('file')
..length.equals(fileContent.length)
..filename.equals('test.gif')
..contentType.asString.equals('image/gif')
..has<Future<List<int>>>((f) => f.finalize().toBytes(), 'contents')
.completes((it) => it.deepEquals(fileContent))
);
checkAppearsLoading(tester, true);

await tester.pump(Duration.zero);
check(controller!.content.text)
.equals('see image: [test.gif]($uploadUrl)\n\n');
checkAppearsLoading(tester, false);
});

testWidgets('data is null', (tester) async {
await prepare(tester);
await insertContentFromKeyboard(tester,
data: null,
attachedFileUrl:
'content://com.zulip.android.zulipboard.provider'
'/root/com.zulip.android.zulipboard/candidate_temp/test.gif',
mimeType: 'image/jpeg');

await tester.pump();
check(controller!.content.text).equals('see image: ');
check(connection.takeRequests()).isEmpty();
checkErrorDialog(tester,
expectedTitle: 'Content not inserted',
expectedMessage: 'The file to be inserted is empty or cannot be accessed.');
checkAppearsLoading(tester, false);
});

testWidgets('data is empty', (tester) async {
await prepare(tester);
await insertContentFromKeyboard(tester,
data: [],
attachedFileUrl:
'content://com.zulip.android.zulipboard.provider'
'/root/com.zulip.android.zulipboard/candidate_temp/test.gif',
mimeType: 'image/jpeg');

await tester.pump();
check(controller!.content.text).equals('see image: ');
check(connection.takeRequests()).isEmpty();
checkErrorDialog(tester,
expectedTitle: 'Content not inserted',
expectedMessage: 'The file to be inserted is empty or cannot be accessed.');
checkAppearsLoading(tester, false);
});
});
});

group('error banner', () {
Expand Down

0 comments on commit e809a6f

Please sign in to comment.