Files
buzz/mobile/test/features/channels/date_header_scroll_activity_test.dart
klopez4212 0a4d209046 Show mobile floating dates while scrolling (#9185)
Floating dates in conversations and threads now appear during reader
scrolling, then fade to zero over 250 ms after one second idle.
Automatic positioning stays quiet, and renewed scrolling reveals the
date immediately. Reduced-motion behavior is preserved.

Validation: mobile analysis, scroll/timing widget tests, and iPhone Air
preview. Full mobile tests run through the push gate.

Home and Inbox styling has moved to
[#9187](https://github.com/block/buzz/pull/9187); this diff only
contains scrolling-date behavior and its supporting tests/helper
extraction.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-11-06 09:35:52 -08:00

170 lines
5.3 KiB
Dart

import 'package:buzz/features/channels/date_header_scroll_activity.dart';
import 'package:buzz/features/channels/sticky_date_header.dart';
import 'package:buzz/shared/theme/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('date follows reader scrolling, expires, and returns immediately', (
tester,
) async {
final activity = DateHeaderScrollActivity();
final state = ValueNotifier(
const StickyDateHeaderState(label: 'Yesterday'),
);
addTearDown(activity.dispose);
addTearDown(state.dispose);
late BuildContext scrollContext;
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: Column(
children: [
StickyDateHeader(state: state, scrollVisible: activity),
Builder(
builder: (context) {
scrollContext = context;
return const SizedBox();
},
),
],
),
),
),
);
final metrics = FixedScrollMetrics(
minScrollExtent: 0,
maxScrollExtent: 1000,
pixels: 100,
viewportDimension: 500,
axisDirection: AxisDirection.down,
devicePixelRatio: 1,
);
double opacity() =>
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).opacity;
void update() => activity.onNotification(
ScrollUpdateNotification(
metrics: metrics,
context: scrollContext,
scrollDelta: 10,
),
);
void start() => activity.onNotification(
ScrollStartNotification(
metrics: metrics,
context: scrollContext,
dragDetails: DragStartDetails(),
),
);
void end() => activity.onNotification(
ScrollEndNotification(metrics: metrics, context: scrollContext),
);
expect(opacity(), 0);
update(); // Initial positioning and incoming-message autoscroll stay quiet.
await tester.pump();
expect(opacity(), 0);
start();
await tester.pump();
expect(opacity(), 1);
expect(
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).duration,
Duration.zero,
);
await tester.pump(const Duration(milliseconds: 800));
update(); // Momentum refreshes the deadline, even without drag details.
await tester.pump(const Duration(milliseconds: 800));
expect(opacity(), 1);
end();
await tester.pump(const Duration(milliseconds: 999));
expect(opacity(), 1);
await tester.pump(const Duration(milliseconds: 1));
expect(opacity(), 0);
expect(
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).duration,
const Duration(milliseconds: 250),
);
expect(
find.text('Yesterday'),
findsOneWidget,
); // Keep the label during fade.
await tester.pump(const Duration(milliseconds: 125));
final fade = tester.widget<FadeTransition>(
find.descendant(
of: find.byType(AnimatedOpacity),
matching: find.byType(FadeTransition),
),
);
expect(fade.opacity.value, closeTo(0.5, 0.05));
start(); // Interrupt the fade without waiting for it to finish.
await tester.pump();
expect(opacity(), 1);
end();
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(milliseconds: 250));
update();
await tester.pump();
expect(opacity(), 0);
});
testWidgets('horizontal and nested scrolling do not reveal the date', (
tester,
) async {
final activity = DateHeaderScrollActivity();
addTearDown(activity.dispose);
await tester.pumpWidget(const MaterialApp(home: SizedBox()));
final context = tester.element(find.byType(SizedBox));
for (final direction in [AxisDirection.right, AxisDirection.down]) {
final notification = ScrollStartNotification(
metrics: FixedScrollMetrics(
minScrollExtent: 0,
maxScrollExtent: 1000,
pixels: 100,
viewportDimension: 500,
axisDirection: direction,
devicePixelRatio: 1,
),
context: context,
dragDetails: DragStartDetails(),
);
if (direction == AxisDirection.down) {
activity.onNotification(
ScrollUpdateNotification(
metrics: notification.metrics,
context: context,
depth: 1,
dragDetails: DragUpdateDetails(globalPosition: Offset.zero),
scrollDelta: 10,
),
);
} else {
activity.onNotification(notification);
}
expect(activity.value, false);
}
});
testWidgets('disposing cancels an outstanding idle deadline', (tester) async {
final activity = DateHeaderScrollActivity();
await tester.pumpWidget(const MaterialApp(home: SizedBox()));
activity.onNotification(
ScrollStartNotification(
metrics: FixedScrollMetrics(
minScrollExtent: 0,
maxScrollExtent: 1000,
pixels: 100,
viewportDimension: 500,
axisDirection: AxisDirection.down,
devicePixelRatio: 1,
),
context: tester.element(find.byType(SizedBox)),
dragDetails: DragStartDetails(),
),
);
activity.dispose();
await tester.pump(const Duration(seconds: 2));
expect(tester.takeException(), isNull);
});
}