Files
Qlyra/test/message_bubble_layout_test.dart
sevenhill f74057ce9b
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Rebrand application as Qlyra
2026-08-30 13:16:17 +03:00

315 lines
9.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:qlyra/backend/modules/messages.dart';
import 'package:qlyra/frontend/widgets/message_bubble.dart';
import 'package:qlyra/l10n/app_localizations.dart';
import 'package:qlyra/models/attachment.dart';
const int _me = 1;
const int _peer = 7;
const double _photoWidth = 180;
const double _maxBubbleWidth = 324;
CachedMessage _message({
required String text,
bool withReply = false,
int senderId = _peer,
String replyText = 'Пётр Синицын написал очень длинный ответ',
}) => CachedMessage(
id: '1',
accountId: _me,
chatId: 2,
senderId: senderId,
text: text,
time: DateTime(2026, 1, 1, 5, 46).millisecondsSinceEpoch,
status: 'sent',
payload: withReply
? {
'link': {
'type': 'REPLY',
'message': {
'id': '9',
'sender': _me,
'text': replyText,
'time': 0,
'attaches': [],
},
},
}
: null,
);
CachedMessage _photoReply() => CachedMessage(
id: '1',
accountId: _me,
chatId: 2,
senderId: _peer,
text: 'Вот те раз, не может быть',
time: DateTime(2026, 1, 1, 5, 46).millisecondsSinceEpoch,
status: 'sent',
attachments: [
PhotoAttachment(
baseUrl: 'https://example.com/synthetic.jpg',
width: _photoWidth.toInt(),
height: 240,
),
],
payload: {
'link': {
'type': 'REPLY',
'message': {
'id': '9',
'sender': _me,
'text':
'Эта функция, она для «спамеров - скамеров» и «мутных - анонимов»',
'time': 0,
'attaches': [],
},
},
},
);
Future<void> _pumpColumn(
WidgetTester tester,
List<CachedMessage> messages, {
required String chatType,
double textScale = 1,
}) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2.5;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
locale: const Locale('ru'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: MediaQuery(
data: MediaQueryData(textScaler: TextScaler.linear(textScale)),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < messages.length; i++)
MessageBubble(
key: ValueKey('bubble$i'),
message: messages[i],
prevMessage: i > 0 ? messages[i - 1] : null,
nextMessage: i < messages.length - 1 ? messages[i + 1] : null,
isMe: false,
myId: _me,
chatType: chatType,
),
],
),
),
),
),
);
await tester.pump();
}
Rect _bubbleRect(WidgetTester tester, int index, String text) {
final label = find.descendant(
of: find.byKey(ValueKey('bubble$index')),
matching: find.textContaining(text, findRichText: true),
);
final box = find.ancestor(of: label, matching: find.byType(Container)).first;
return tester.getTopLeft(box) & tester.getSize(box);
}
Future<void> _pumpBubble(
WidgetTester tester,
CachedMessage message, {
String chatType = 'CHAT',
}) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2.5;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
locale: const Locale('ru'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: MessageBubble(
message: message,
isMe: false,
myId: _me,
chatType: chatType,
),
),
),
),
);
await tester.pump();
}
Rect _rectOf(WidgetTester tester, Finder finder) {
final size = tester.getSize(finder);
final topLeft = tester.getTopLeft(finder);
return topLeft & size;
}
Rect _clockRect(WidgetTester tester) {
var rect = Rect.zero;
for (final element in find.textContaining('05:46').evaluate()) {
final box = element.renderObject! as RenderBox;
final candidate = box.localToGlobal(Offset.zero) & box.size;
if (candidate.right > rect.right) rect = candidate;
}
return rect;
}
void main() {
setUp(() => ContactCache.put(_peer, 'Пётр Синицын'));
testWidgets('a long sender name pushes the clock to the bubble edge', (
tester,
) async {
await _pumpBubble(tester, _message(text: 'нет'));
final header = _rectOf(tester, find.text('Пётр Синицын'));
final clock = _clockRect(tester);
final body = _rectOf(
tester,
find.textContaining('нет', findRichText: true),
);
expect(header.width, greaterThan(body.width));
expect(clock.right, closeTo(header.right, 1));
});
testWidgets('a short reply quote fills the width the sender name opened up', (
tester,
) async {
await _pumpBubble(
tester,
_message(text: 'нет', withReply: true, replyText: 'ок'),
);
final header = _rectOf(tester, find.text('Пётр Синицын'));
final label = _rectOf(tester, find.text('Вы'));
final quote = _rectOf(
tester,
find
.ancestor(of: find.text('Вы'), matching: find.byType(Container))
.first,
);
final clock = _clockRect(tester);
expect(quote.right, greaterThan(label.right));
expect(quote.right, closeTo(header.right, 1));
expect(clock.right, closeTo(header.right, 1));
});
testWidgets('a long reply quote widens the bubble past its own text', (
tester,
) async {
await _pumpBubble(tester, _message(text: 'т'));
final withoutReply = _clockRect(tester).right;
await _pumpBubble(tester, _message(text: 'т', withReply: true));
final header = _rectOf(tester, find.text('Пётр Синицын'));
final quote = _rectOf(
tester,
find
.ancestor(of: find.text('Вы'), matching: find.byType(Container))
.first,
);
final clock = _clockRect(tester);
expect(clock.right, greaterThan(withoutReply + 40));
expect(quote.right, greaterThan(header.right));
expect(clock.right, closeTo(quote.right, 1));
expect(quote.width, lessThanOrEqualTo(_maxBubbleWidth * 0.75 + 1));
});
testWidgets('grouped bubbles keep the same gap with and without avatars', (
tester,
) async {
final stream = [
for (var i = 0; i < 4; i++)
CachedMessage(
id: '$i',
accountId: _me,
chatId: 2,
senderId: 404,
text: 'm$i',
time: DateTime(2026, 1, 1, 12, 54).millisecondsSinceEpoch + i * 1000,
status: 'sent',
),
];
double gapAt(WidgetTester tester, int index) =>
_bubbleRect(tester, index + 1, 'm${index + 1}').top -
_bubbleRect(tester, index, 'm$index').bottom;
await _pumpColumn(tester, stream, chatType: 'DIALOG', textScale: 0.35);
final dialogGaps = [for (var i = 0; i < 3; i++) gapAt(tester, i)];
await _pumpColumn(tester, stream, chatType: 'CHAT', textScale: 0.35);
final groupGaps = [for (var i = 0; i < 3; i++) gapAt(tester, i)];
expect(dialogGaps, everyElement(2.0));
expect(groupGaps, dialogGaps);
});
testWidgets('a reply above a photo stays inside the photo width', (
tester,
) async {
await _pumpBubble(tester, _photoReply());
final quote = _rectOf(
tester,
find
.ancestor(of: find.text('Вы'), matching: find.byType(Container))
.first,
);
final caption = _rectOf(
tester,
find.textContaining('Вот те раз', findRichText: true),
);
expect(quote.width, closeTo(_photoWidth - 16, 1));
expect(quote.left, greaterThan(0));
expect(quote.right, lessThanOrEqualTo(caption.left + _photoWidth));
});
testWidgets('a bubble without a header or reply still hugs its text', (
tester,
) async {
await _pumpBubble(tester, _message(text: 'нет', senderId: 404));
final clock = _clockRect(tester);
final body = _rectOf(
tester,
find.textContaining('нет', findRichText: true),
);
expect(clock.left, closeTo(body.right + 8, 1));
expect(clock.center.dy, closeTo(body.center.dy, 4));
});
testWidgets('the clock drops below wrapped text instead of widening it', (
tester,
) async {
await _pumpBubble(
tester,
_message(text: '${'ф' * 14}\n${'ф' * 14}', senderId: 404),
chatType: 'DIALOG',
);
final clock = _clockRect(tester);
final body = _rectOf(tester, find.textContaining('ф', findRichText: true));
expect(clock.top, greaterThanOrEqualTo(body.bottom - 2));
expect(clock.right, lessThanOrEqualTo(body.right + 1));
});
}