feat: добавил отображение + отправку animoji. Добавил матовое стекло для панели ответа при кастомизационном выборе 'прозрачность'
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:komet/core/utils/text_format.dart';
|
||||
|
||||
FormatRange _animoji(int start, int length, String url) => FormatRange(
|
||||
format: TextFormat.animoji,
|
||||
start: start,
|
||||
length: length,
|
||||
attributes: {'animojiLottieUrl': url},
|
||||
);
|
||||
|
||||
void main() {
|
||||
test('single animoji-only message is jumbo', () {
|
||||
expect(animojiOnlyLottieUrls('❤️', [_animoji(0, 2, 'L1')]), ['L1']);
|
||||
});
|
||||
|
||||
test('several animoji with no other text are jumbo, in order', () {
|
||||
final urls = animojiOnlyLottieUrls('❤️🔥', [
|
||||
_animoji(2, 2, 'L2'),
|
||||
_animoji(0, 2, 'L1'),
|
||||
]);
|
||||
expect(urls, ['L1', 'L2']);
|
||||
});
|
||||
|
||||
test('animoji mixed with real text is NOT jumbo', () {
|
||||
expect(
|
||||
animojiOnlyLottieUrls('animoji message🤣', [_animoji(15, 2, 'L1')]),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('plain emoji without an ANIMOJI element is NOT jumbo', () {
|
||||
expect(animojiOnlyLottieUrls('😀', const []), isNull);
|
||||
});
|
||||
|
||||
test('more than the limit is NOT jumbo', () {
|
||||
final ranges = [
|
||||
for (var i = 0; i < 5; i++) _animoji(i * 2, 2, 'L$i'),
|
||||
];
|
||||
expect(animojiOnlyLottieUrls('❤️❤️❤️❤️❤️', ranges), isNull);
|
||||
});
|
||||
|
||||
test('whitespace between animoji is allowed', () {
|
||||
expect(
|
||||
animojiOnlyLottieUrls('❤️ ❤️', [_animoji(0, 2, 'L1'), _animoji(3, 2, 'L2')]),
|
||||
['L1', 'L2'],
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:komet/frontend/widgets/rich_message_controller.dart';
|
||||
import 'package:komet/models/animoji.dart';
|
||||
|
||||
Animoji _a(int id, String emoji, String lottie) =>
|
||||
Animoji(id: id, emoji: emoji, lottieUrl: lottie);
|
||||
|
||||
void main() {
|
||||
test('standalone animoji builds one ANIMOJI element at offset 0', () {
|
||||
final c = RichMessageController();
|
||||
c.insertAnimoji(_a(125, '❤️', 'L1'));
|
||||
|
||||
final content = c.buildContent();
|
||||
expect(content.text, '❤️');
|
||||
expect(content.elements, [
|
||||
{
|
||||
'type': 'ANIMOJI',
|
||||
'from': 0,
|
||||
'length': 2,
|
||||
'entityId': 125,
|
||||
'attributes': {'animojiLottieUrl': 'L1'},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('animoji appended after text gets the correct utf16 offset', () {
|
||||
final c = RichMessageController();
|
||||
c.value = const TextEditingValue(
|
||||
text: 'test',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
);
|
||||
c.insertAnimoji(_a(7, '🤣', 'L2'));
|
||||
|
||||
final content = c.buildContent();
|
||||
expect(content.text, 'test🤣');
|
||||
expect(content.elements.single['from'], 4);
|
||||
expect(content.elements.single['length'], 2);
|
||||
expect(content.elements.single['type'], 'ANIMOJI');
|
||||
});
|
||||
|
||||
test('multiple animoji with surrounding text keep glyph offsets in order', () {
|
||||
final c = RichMessageController();
|
||||
c.value = const TextEditingValue(
|
||||
text: 'a',
|
||||
selection: TextSelection.collapsed(offset: 1),
|
||||
);
|
||||
c.insertAnimoji(_a(1, '❤️', 'L1'));
|
||||
// caret now after first placeholder; type "b"
|
||||
final t1 = c.value.text; // "a"
|
||||
c.value = TextEditingValue(
|
||||
text: '${t1}b',
|
||||
selection: TextSelection.collapsed(offset: t1.length + 1),
|
||||
);
|
||||
c.insertAnimoji(_a(2, '🔥', 'L3'));
|
||||
|
||||
final content = c.buildContent();
|
||||
expect(content.text, 'a❤️b🔥');
|
||||
|
||||
final froms = content.elements
|
||||
.where((e) => e['type'] == 'ANIMOJI')
|
||||
.map((e) => e['from'])
|
||||
.toList();
|
||||
expect(froms, [1, 4]);
|
||||
});
|
||||
|
||||
testWidgets('built span plain text matches controller text (caret invariant)', (
|
||||
tester,
|
||||
) async {
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(
|
||||
WidgetsApp(
|
||||
color: const Color(0xFF000000),
|
||||
builder: (context, _) {
|
||||
ctx = context;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final c = RichMessageController();
|
||||
c.value = const TextEditingValue(
|
||||
text: 'hi',
|
||||
selection: TextSelection.collapsed(offset: 2),
|
||||
);
|
||||
c.insertAnimoji(_a(1, '❤️', 'L1'));
|
||||
c.value = TextEditingValue(
|
||||
text: '${c.value.text}!',
|
||||
selection: TextSelection.collapsed(offset: c.value.text.length + 1),
|
||||
);
|
||||
|
||||
final span = c.buildTextSpan(
|
||||
context: ctx,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
withComposing: false,
|
||||
);
|
||||
expect(span.toPlainText(), c.text);
|
||||
});
|
||||
|
||||
test('deleting the placeholder char drops the entity', () {
|
||||
final c = RichMessageController();
|
||||
c.insertAnimoji(_a(1, '❤️', 'L1'));
|
||||
expect(c.value.text.length, 1);
|
||||
// backspace: remove the placeholder
|
||||
c.value = const TextEditingValue(
|
||||
text: '',
|
||||
selection: TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
final content = c.buildContent();
|
||||
expect(content.text, '');
|
||||
expect(content.elements, isEmpty);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user