add e2e testing (#167)

Reviewed-on: #167
This commit is contained in:
Henry Hiles 2026-11-10 18:29:02 -05:00 • committed by Henry Hiles
commit af99de75da

View file

@ -0,0 +1,115 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "package:material_ui/material_ui.dart";
import "package:nexus/widgets/room_chat/room_chat.dart";
import "package:nexus/widgets/threads/thread_drawer.dart";
import "package:nexus/widgets/threads/threads_drawer.dart";
import "helpers/matrix_user.dart";
import "helpers/test_app.dart";
import "helpers/pump_until.dart";
import "helpers/threads.dart";
/// A thread longer than gomuks' initial sync, which gets the latest 100
/// events, so its start only reaches Nexus through `/relations`. That needs
/// an account whose first sync is after the thread exists, so it's not the one
/// other tests use.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final app = TestApp("carol", "carolpass");
setUpAll(app.setUp);
final run = DateTime.now().millisecondsSinceEpoch;
final roomName = "Long thread $run";
final rootBody = "Long thread root $run";
const replyCount = 120;
// Among the first replies, which aren't synced
const jumpedTo = 10;
// Padded, so no reply's text contains another's (oops those failures were confusing when I didn't do that)
String reply(int number) => "Long reply ${number.toString().padLeft(3, "0")}";
setUpAll(() async {
final bob = await MatrixUser.login("bob", "bobpass");
final roomId = await bob.createRoom(roomName);
await app.join(bob, roomId);
final thread = await bob.sendThread(
roomId,
rootBody,
Iterable.generate(replyCount, (index) => reply(index + 1)).toIList(),
);
// In the room's timeline, so there's something to jump from, to a reply
// with others on both sides that aren't synced either
await bob.send(
roomId,
"Quoting an old reply",
replyTo: thread.replyIds[jumpedTo - 1],
);
});
/// Drags the open thread by [offset] until [finder] is shown in it, loading
/// more replies as it goes, for up to 30 seconds.
Future<void> scrollThread(
WidgetTester tester,
Finder finder,
Offset offset,
) => tester.dragUntilVisible(
finder.inside(ThreadDrawer),
find.byType(CustomScrollView).inside(ThreadDrawer).first,
offset,
maxIteration: 60,
duration: const .new(milliseconds: 500),
);
Future<void> openThreadsDrawer(WidgetTester tester) async {
await app.openRoom(tester, roomName);
await tester.tapWhenShown(find.byTooltip("Open threads"));
await tester.pumpUntil(find.message(rootBody).inside(ThreadsDrawer));
}
testWidgets("lists the thread with all of its replies", (tester) async {
await openThreadsDrawer(tester);
await tester.expectReplyCount(ThreadsDrawer, replyCount);
});
// Before the whole thread is loaded by scrolling through it below
testWidgets("jumps to a reply in the thread that isn't loaded", (
tester,
) async {
await app.openRoom(tester, roomName);
// The quote of the reply is fetched on its own, then tapped
await tester.tapWhenShown(
find.message(reply(jumpedTo)).inside(RoomChat).inkWell,
);
// The thread opens around the reply, with replies on both sides
await tester.pumpUntil(
find.message(reply(jumpedTo)).inside(ThreadDrawer).hitTestable(),
);
await tester.pumpUntil(
find.message(reply(jumpedTo - 1)).inside(ThreadDrawer),
);
await tester.pumpUntil(
find.message(reply(jumpedTo + 1)).inside(ThreadDrawer),
);
await scrollThread(tester, find.message(reply(60)), const .new(0, -400));
});
testWidgets("loads older replies back to the root", (tester) async {
await openThreadsDrawer(tester);
await tester.tapWhenShown(
find.message(rootBody).inside(ThreadsDrawer).inkWell,
);
await tester.pumpUntil(
find.message(reply(replyCount)).inside(ThreadDrawer),
);
// Dragging down scrolls up the reversed timeline, to older replies
await scrollThread(tester, find.message(rootBody), const .new(0, 400));
expect(find.message(reply(1)).inside(ThreadDrawer), findsWidgets);
});
}