B6 scroll-to-latest: ScrollController on the timeline; a small arrow-down FAB fades in once scrolled >400px up and animates back to the newest message on tap (overlaid on the list, not the FAB slot) B5 panic hold-ring: 700ms AnimationController drives a red progress ring around the panic icon; haptic + fire on completion, silent reset on early release; tap-hint and configured guard preserved Panic ring verified in-browser (ring fills, alert fires to the room). Scroll-to-latest verified with a deterministic widget test because the CanvasKit surface rejects synthetic scroll input in headless Chromium. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
81 lines
2.7 KiB
Dart
81 lines
2.7 KiB
Dart
// Version: 1.0.0 | Created: 2026-07-04
|
|
// Deterministic test for the chat timeline's scroll-to-latest button.
|
|
// Playwright cannot drive scrolling on the Flutter CanvasKit surface, so the
|
|
// jump-to-latest behaviour is verified here against the real widget tree.
|
|
|
|
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/domain/message_model.dart';
|
|
import 'package:m8chat_app/features/chat/presentation/chat_controller.dart';
|
|
import 'package:m8chat_app/features/chat/presentation/chat_screen.dart';
|
|
|
|
void main() {
|
|
testWidgets(
|
|
'jump-to-latest button appears when scrolled up and returns to bottom',
|
|
(tester) async {
|
|
const roomId = '!scrolltest:server';
|
|
final base = DateTime(2026, 7, 4, 8);
|
|
// Index 0 is the newest message (the timeline stream is newest-first).
|
|
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,
|
|
),
|
|
];
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
matrixClientProvider.overrideWithValue(Client('test-client')),
|
|
chatTimelineProvider(
|
|
roomId,
|
|
).overrideWith((ref) => Stream.value(messages)),
|
|
],
|
|
child: const MaterialApp(home: ChatScreen(roomId: roomId)),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final listView = find.byType(ListView);
|
|
expect(listView, findsOneWidget);
|
|
|
|
double offset() =>
|
|
tester.state<ScrollableState>(find.byType(Scrollable).first)
|
|
.position
|
|
.pixels;
|
|
|
|
// Starts pinned to the newest message (offset 0 in a reverse list).
|
|
expect(offset(), 0);
|
|
|
|
// Scroll up toward older messages (reverse list: drag content down).
|
|
await tester.drag(listView, const Offset(0, 900));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
offset(),
|
|
greaterThan(400),
|
|
reason: 'should have scrolled well past the button threshold',
|
|
);
|
|
|
|
// The jump-to-latest button is now shown; tap it.
|
|
final jumpButton = find.byIcon(Icons.arrow_downward);
|
|
expect(jumpButton, findsOneWidget);
|
|
await tester.tap(jumpButton);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Back at the newest message.
|
|
expect(offset(), 0);
|
|
},
|
|
);
|
|
}
|