feat(chat): load older messages on scroll-to-top (history pagination) (v1.11.0+15)
Repository keeps one live Timeline per room and re-yields on its onUpdate callback (fires for both live events AND requestHistory), not just onSync which never fires for pagination. loadMoreMessages now paginates that same instance; added canLoadMore. Timeline widget requests older history when scrolled within 400px of the oldest end, guarded against re-entry, with a top loading spinner. Verified: widget test drives a drag-to-top and asserts loadMoreMessages fires; live smoke test confirms the onUpdate delivery path (reused by history) still shows new messages after the refactor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
104
test/history_pagination_test.dart
Normal file
104
test/history_pagination_test.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
// Version: 1.0.0 | Created: 2026-07-04
|
||||
// Verifies the chat timeline requests older history when scrolled near the
|
||||
// top (oldest end) of the reverse list. Driven through the real widget tree
|
||||
// with a fake repository, since Playwright cannot scroll the CanvasKit surface.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:m8chat_app/core/network/matrix_client.dart';
|
||||
import 'package:m8chat_app/features/chat/data/chat_repository.dart';
|
||||
import 'package:m8chat_app/features/chat/domain/message_model.dart';
|
||||
import 'package:m8chat_app/features/chat/presentation/chat_controller.dart';
|
||||
import 'package:m8chat_app/features/chat/presentation/chat_screen.dart';
|
||||
|
||||
class _FakeChatRepository extends ChatRepository {
|
||||
_FakeChatRepository() : super(client: Client('test-client'));
|
||||
|
||||
int loadMoreCalls = 0;
|
||||
bool _hasMore = true;
|
||||
|
||||
@override
|
||||
bool canLoadMore(String roomId) => _hasMore;
|
||||
|
||||
@override
|
||||
Future<void> loadMoreMessages(String roomId) async {
|
||||
loadMoreCalls++;
|
||||
_hasMore = false; // one page then exhausted
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('scrolling to the top requests older history once', (
|
||||
tester,
|
||||
) async {
|
||||
const roomId = '!paginate:server';
|
||||
final base = DateTime(2026, 7, 4, 8);
|
||||
final messages = [
|
||||
for (var i = 40; i >= 1; i--)
|
||||
MessageModel(
|
||||
eventId: '\$e$i',
|
||||
roomId: roomId,
|
||||
senderId: '@me:server',
|
||||
senderDisplayName: 'Me',
|
||||
timestamp: base.add(Duration(seconds: i)),
|
||||
type: MessageType.text,
|
||||
body: 'Message $i',
|
||||
isMine: true,
|
||||
),
|
||||
];
|
||||
|
||||
final repo = _FakeChatRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
matrixClientProvider.overrideWithValue(Client('test-client')),
|
||||
chatRepositoryProvider.overrideWithValue(repo),
|
||||
chatTimelineProvider(
|
||||
roomId,
|
||||
).overrideWith((ref) => Stream.value(messages)),
|
||||
],
|
||||
child: const MaterialApp(home: ChatScreen(roomId: roomId)),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repo.loadMoreCalls, 0);
|
||||
|
||||
// Confirm the override is actually in effect.
|
||||
final container = ProviderScope.containerOf(
|
||||
tester.element(find.byType(ChatScreen)),
|
||||
);
|
||||
expect(identical(container.read(chatRepositoryProvider), repo), isTrue);
|
||||
|
||||
double offset() => tester
|
||||
.state<ScrollableState>(find.byType(Scrollable).first)
|
||||
.position
|
||||
.pixels;
|
||||
double maxExtent() => tester
|
||||
.state<ScrollableState>(find.byType(Scrollable).first)
|
||||
.position
|
||||
.maxScrollExtent;
|
||||
|
||||
// Scroll up toward the oldest end (reverse list: drag content down).
|
||||
await tester.drag(find.byType(ListView), const Offset(0, 3000));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
offset(),
|
||||
greaterThan(maxExtent() - 400),
|
||||
reason:
|
||||
'drag should have reached the oldest end (offset=${offset()}, '
|
||||
'max=${maxExtent()})',
|
||||
);
|
||||
|
||||
expect(
|
||||
repo.loadMoreCalls,
|
||||
greaterThan(0),
|
||||
reason: 'reaching the top should request older history',
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user