feat(apps/home): 新增 HomeScreen 录音交互与导航组件
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/features/home/ui/controllers/home_message_viewport_controller.dart';
|
||||
|
||||
void main() {
|
||||
ViewportEvent buildEvent({
|
||||
required ViewportEventType type,
|
||||
required String conversationId,
|
||||
required int eventSeq,
|
||||
required double distanceToBottomPx,
|
||||
bool isFirstEnter = false,
|
||||
bool hasSavedViewport = false,
|
||||
bool hasAnchor = false,
|
||||
int deltaCount = 0,
|
||||
ViewportTriggerSource source = ViewportTriggerSource.system,
|
||||
}) {
|
||||
return ViewportEvent(
|
||||
type: type,
|
||||
conversationId: conversationId,
|
||||
eventSeq: eventSeq,
|
||||
triggerSource: source,
|
||||
deltaCount: deltaCount,
|
||||
anchor: const ViewportAnchor(messageId: null, offsetPx: null),
|
||||
timestamp: 1000 + eventSeq,
|
||||
viewportContext: ViewportContext(
|
||||
distanceToBottomPx: distanceToBottomPx,
|
||||
isFirstEnter: isFirstEnter,
|
||||
hasSavedViewport: hasSavedViewport,
|
||||
hasAnchor: hasAnchor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
test('distance<=96 and new message => animateBottom', () {
|
||||
final controller = HomeMessageViewportController();
|
||||
final decision = controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.newMessageAppended,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 1,
|
||||
distanceToBottomPx: 80,
|
||||
deltaCount: 1,
|
||||
),
|
||||
);
|
||||
|
||||
expect(decision.action, ViewportAction.animateBottom);
|
||||
});
|
||||
|
||||
test('distance>96 and new message => showUnreadBadge', () {
|
||||
final controller = HomeMessageViewportController();
|
||||
controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.userScrollStateChanged,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 1,
|
||||
distanceToBottomPx: 200,
|
||||
source: ViewportTriggerSource.user,
|
||||
),
|
||||
);
|
||||
|
||||
final decision = controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.newMessageAppended,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 2,
|
||||
distanceToBottomPx: 200,
|
||||
deltaCount: 1,
|
||||
),
|
||||
);
|
||||
|
||||
expect(decision.action, ViewportAction.showUnreadBadge);
|
||||
expect(controller.unreadCount, 1);
|
||||
});
|
||||
|
||||
test('stale event is dropped by sequence', () {
|
||||
final controller = HomeMessageViewportController();
|
||||
controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.historyInitialLoaded,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 10,
|
||||
distanceToBottomPx: 0,
|
||||
isFirstEnter: true,
|
||||
),
|
||||
);
|
||||
|
||||
final decision = controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.newMessageAppended,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 9,
|
||||
distanceToBottomPx: 0,
|
||||
),
|
||||
);
|
||||
|
||||
expect(decision.action, ViewportAction.none);
|
||||
expect(decision.reason, 'stale-event');
|
||||
});
|
||||
|
||||
test('different conversations keep independent sequence', () {
|
||||
final controller = HomeMessageViewportController();
|
||||
controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.historyInitialLoaded,
|
||||
conversationId: 'A',
|
||||
eventSeq: 10,
|
||||
distanceToBottomPx: 0,
|
||||
isFirstEnter: true,
|
||||
),
|
||||
);
|
||||
|
||||
final decision = controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.historyInitialLoaded,
|
||||
conversationId: 'B',
|
||||
eventSeq: 1,
|
||||
distanceToBottomPx: 0,
|
||||
isFirstEnter: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(decision.action, ViewportAction.jumpBottom);
|
||||
});
|
||||
|
||||
test('refresh keeps reading history position when far from bottom', () {
|
||||
final controller = HomeMessageViewportController();
|
||||
controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.userScrollStateChanged,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 1,
|
||||
distanceToBottomPx: 180,
|
||||
source: ViewportTriggerSource.user,
|
||||
),
|
||||
);
|
||||
|
||||
final decision = controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.sessionRefreshCompleted,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 2,
|
||||
distanceToBottomPx: 180,
|
||||
),
|
||||
);
|
||||
|
||||
expect(decision.action, ViewportAction.none);
|
||||
});
|
||||
|
||||
test('resume with saved viewport restores anchor', () {
|
||||
final controller = HomeMessageViewportController();
|
||||
final decision = controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.screenResumedFromSubRoute,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 1,
|
||||
distanceToBottomPx: 180,
|
||||
hasSavedViewport: true,
|
||||
hasAnchor: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(decision.action, ViewportAction.restoreAnchor);
|
||||
expect(decision.reason, 'resume-restore-saved-viewport');
|
||||
});
|
||||
|
||||
test('prepend finish without anchor exits restoring state', () {
|
||||
final controller = HomeMessageViewportController();
|
||||
controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.historyPagePrependStarted,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 1,
|
||||
distanceToBottomPx: 200,
|
||||
hasAnchor: true,
|
||||
),
|
||||
);
|
||||
controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.historyPagePrependFinished,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 2,
|
||||
distanceToBottomPx: 200,
|
||||
hasAnchor: false,
|
||||
),
|
||||
);
|
||||
|
||||
final decision = controller.apply(
|
||||
buildEvent(
|
||||
type: ViewportEventType.newMessageAppended,
|
||||
conversationId: 'c1',
|
||||
eventSeq: 3,
|
||||
distanceToBottomPx: 200,
|
||||
deltaCount: 1,
|
||||
),
|
||||
);
|
||||
expect(decision.action, ViewportAction.showUnreadBadge);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user