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>
105 lines
3.2 KiB
Dart
105 lines
3.2 KiB
Dart
// 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',
|
|
);
|
|
});
|
|
}
|