feat: комета в 'поделится

'
This commit is contained in:
Jganenokk
2026-08-22 23:18:09 +07:00
parent 0091b5f272
commit 5d115e0f9e
15 changed files with 1972 additions and 9 deletions
+131
View File
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/backend/modules/share_sender.dart';
import 'package:komet/frontend/screens/chats/share_composer_bar.dart';
import 'package:komet/frontend/widgets/rich_message_controller.dart';
import 'package:komet/models/shared_payload.dart';
PreparedShareFile _file(String name, String mime) => PreparedShareFile(
source: SharedFile(path: '/synthetic/$name', name: name, mime: mime, size: 8),
);
Future<RichMessageController> _pump(
WidgetTester tester, {
required PreparedShare share,
required List<String> recipients,
bool sending = false,
Future<void> Function(String)? onSend,
}) async {
final controller = RichMessageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.bottomCenter,
child: ShareComposerBar(
share: share,
controller: controller,
recipientNames: recipients,
sending: sending,
onSend: onSend ?? (_) async {},
),
),
),
),
);
await tester.pump();
return controller;
}
void main() {
testWidgets('a single photo names itself and lists the recipient', (
tester,
) async {
await _pump(
tester,
share: PreparedShare(files: [_file('a.jpg', 'image/jpeg')]),
recipients: const ['ЛУКА'],
);
expect(find.text('Отправить фотографию'), findsOneWidget);
expect(find.text('В чат ЛУКА'), findsOneWidget);
expect(find.text('Добавить подпись...'), findsOneWidget);
});
testWidgets('three chats collapse into a count and a badge', (tester) async {
await _pump(
tester,
share: PreparedShare(
files: [
_file('a.jpg', 'image/jpeg'),
_file('b.jpg', 'image/jpeg'),
_file('c.jpg', 'image/jpeg'),
],
),
recipients: const ['a', 'b', 'c'],
);
expect(find.text('Отправить 3 фотографии'), findsOneWidget);
expect(find.text('В 3 чата'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
});
testWidgets('a text share drops the preview row', (tester) async {
final controller = await _pump(
tester,
share: const PreparedShare(files: [], text: 'https://komet.pw'),
recipients: const ['ЛУКА'],
);
expect(find.text('Отправить сообщение'), findsNothing);
expect(find.text('Сообщение'), findsOneWidget);
expect(controller.text, '');
});
testWidgets('a file share still shows a title but no photo wording', (
tester,
) async {
await _pump(
tester,
share: PreparedShare(files: [_file('doc.pdf', 'application/pdf')]),
recipients: const ['ЛУКА', 'Zarub'],
);
expect(find.text('Отправить файл'), findsOneWidget);
expect(find.text('В чат ЛУКА, Zarub'), findsOneWidget);
});
testWidgets('the caption reaches onSend', (tester) async {
String? captured;
final controller = await _pump(
tester,
share: PreparedShare(files: [_file('a.jpg', 'image/jpeg')]),
recipients: const ['ЛУКА'],
onSend: (caption) async => captured = caption,
);
controller.text = ' привет ';
await tester.pump();
await tester.tap(find.byIcon(Symbols.send));
await tester.pump();
expect(captured, 'привет');
});
testWidgets('with no recipients the send button is inert', (tester) async {
var sent = false;
await _pump(
tester,
share: PreparedShare(files: [_file('a.jpg', 'image/jpeg')]),
recipients: const [],
onSend: (_) async => sent = true,
);
expect(find.text('Выберите чат'), findsOneWidget);
await tester.tap(find.byIcon(Symbols.send));
await tester.pump();
expect(sent, isFalse);
});
}
+164
View File
@@ -0,0 +1,164 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/core/share/share_labels.dart';
import 'package:komet/models/shared_payload.dart';
late Directory _dir;
String _makeFile(String name, {int bytes = 8}) {
final file = File('${_dir.path}${Platform.pathSeparator}$name')
..writeAsBytesSync(List<int>.filled(bytes, 0x41));
return file.path;
}
Map<String, Object?> _entry(String path, String mime, {int size = 8}) => {
'path': path,
'name': path.split(Platform.pathSeparator).last,
'mime': mime,
'size': size,
};
void main() {
setUp(() {
_dir = Directory.systemTemp.createTempSync('synthetic_share_test');
addTearDown(() {
if (_dir.existsSync()) _dir.deleteSync(recursive: true);
});
});
group('SharedPayload.fromMap', () {
test('classifies photos, videos and documents by mime', () {
final payload = SharedPayload.fromMap({
'files': [
_entry(_makeFile('a.jpg'), 'image/jpeg'),
_entry(_makeFile('b.mp4'), 'video/mp4'),
_entry(_makeFile('c.pdf'), 'application/pdf'),
],
'text': null,
});
expect(payload, isNotNull);
expect(payload!.photos.map((f) => f.name), ['a.jpg']);
expect(payload.videos.map((f) => f.name), ['b.mp4']);
expect(payload.documents.map((f) => f.name), ['c.pdf']);
expect(payload.dominantKind, SharedFileKind.file);
});
test('an svg is a document, not a photo', () {
final payload = SharedPayload.fromMap({
'files': [_entry(_makeFile('d.svg'), 'image/svg+xml')],
});
expect(payload!.files.single.kind, SharedFileKind.file);
});
test('drops entries whose file is gone', () {
final payload = SharedPayload.fromMap({
'files': [
_entry(_makeFile('present.jpg'), 'image/jpeg'),
_entry('${_dir.path}/missing.jpg', 'image/jpeg'),
],
});
expect(payload!.files.map((f) => f.name), ['present.jpg']);
});
test('a text-only share survives with no files', () {
final payload = SharedPayload.fromMap({
'files': const [],
'text': ' https://komet.pw ',
});
expect(payload!.isTextOnly, isTrue);
expect(payload.text, 'https://komet.pw');
});
test('an empty share is rejected', () {
expect(SharedPayload.fromMap({'files': const [], 'text': ' '}), isNull);
expect(SharedPayload.fromMap(null), isNull);
expect(SharedPayload.fromMap('nonsense'), isNull);
});
test('a missing mime falls back to a document', () {
final payload = SharedPayload.fromMap({
'files': [
{'path': _makeFile('e.bin'), 'name': 'e.bin', 'size': 8},
],
});
expect(payload!.files.single.mime, 'application/octet-stream');
expect(payload.files.single.kind, SharedFileKind.file);
});
});
group('shareTitleFor', () {
test('photos use Russian plural forms', () {
expect(
shareTitleFor(photos: 1, videos: 0, documents: 0),
'Отправить фотографию',
);
expect(
shareTitleFor(photos: 3, videos: 0, documents: 0),
'Отправить 3 фотографии',
);
expect(
shareTitleFor(photos: 5, videos: 0, documents: 0),
'Отправить 5 фотографий',
);
expect(
shareTitleFor(photos: 11, videos: 0, documents: 0),
'Отправить 11 фотографий',
);
});
test('videos stay uninflected', () {
expect(
shareTitleFor(photos: 0, videos: 1, documents: 0),
'Отправить видео',
);
expect(
shareTitleFor(photos: 0, videos: 2, documents: 0),
'Отправить 2 видео',
);
});
test('documents and mixed sets fall back to file wording', () {
expect(
shareTitleFor(photos: 0, videos: 0, documents: 1),
'Отправить файл',
);
expect(
shareTitleFor(photos: 0, videos: 0, documents: 4),
'Отправить 4 файла',
);
expect(
shareTitleFor(photos: 1, videos: 1, documents: 0),
'Отправить 2 файла',
);
});
test('a text share has no media wording', () {
expect(
shareTitleFor(photos: 0, videos: 0, documents: 0, textOnly: true),
'Отправить сообщение',
);
});
});
group('shareSubtitleFor', () {
test('names are listed up to two recipients', () {
expect(shareSubtitleFor(const ['ЛУКА']), 'В чат ЛУКА');
expect(shareSubtitleFor(const ['ЛУКА', 'Zarub']), 'В чат ЛУКА, Zarub');
});
test('three or more recipients collapse to a count', () {
expect(shareSubtitleFor(const ['a', 'b', 'c']), 'В 3 чата');
expect(shareSubtitleFor(List.filled(5, 'x')), 'В 5 чатов');
});
test('an empty selection asks for one', () {
expect(shareSubtitleFor(const []), 'Выберите чат');
});
});
}