Files
plezy/test/widgets/player_chrome_controller_test.dart
T
edde746 7658587584 refactor(player): one subscription list, bool play-pause focus request, one desktop click contract, one sync-offset layout
The screen's player stream subscriptions were enumerated by hand in four places (re-wire, rollback, dispose, plus a parallel nulling list) kept in sync only by prose — _cancelPlayerStreamSubscriptions is now the single cancel-and-null authority, living beside the fields. PlayerChromeFocusTarget was a one-value enum with request/consume plumbing, now a bool with identical post-frame consumption timing. The desktop tap and controls-overlay tap duplicated the click-toggles-playback/double-click-fullscreen contract around shared state, now one helper. SyncOffsetControl shipped two layouts of which production only ever rendered compact — the full layout existed solely for tests, which now cover the shipped layout. TrackControlsState's never-non-null onLoadSeekTimes/onSyncOffsetChanged sheet-compat callbacks are gone.
2026-08-17 19:02:23 +02:00

215 lines
7.8 KiB
Dart

import 'dart:ui' show PointerDeviceKind;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/widgets/video_controls/player_chrome_controller.dart';
void main() {
group('PlayerChromeController', () {
testWidgets('auto-hides visible controls while playing', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.configure(hideDelay: const Duration(milliseconds: 100));
controller.setPlaying(true);
expect(controller.controlsVisible, isTrue);
await tester.pump(const Duration(milliseconds: 99));
expect(controller.controlsVisible, isTrue);
await tester.pump(const Duration(milliseconds: 1));
expect(controller.controlsVisible, isFalse);
});
testWidgets('visible holds suppress auto-hide until released', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.configure(hideDelay: const Duration(milliseconds: 100));
controller.setPlaying(true);
controller.hold(PlayerChromeHold.promptInteraction);
await tester.pump(const Duration(milliseconds: 200));
expect(controller.controlsVisible, isTrue);
controller.release(PlayerChromeHold.promptInteraction);
await tester.pump(const Duration(milliseconds: 100));
expect(controller.controlsVisible, isFalse);
});
testWidgets('releasing a hold while paused restarts paused auto-hide', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.configure(hideDelay: const Duration(milliseconds: 100));
controller.setPlaying(true);
controller.setPlaying(false);
controller.hold(PlayerChromeHold.promptInteraction);
await tester.pump(const Duration(milliseconds: 200));
expect(controller.controlsVisible, isTrue);
controller.release(PlayerChromeHold.promptInteraction);
await tester.pump(const Duration(milliseconds: 99));
expect(controller.controlsVisible, isTrue);
await tester.pump(const Duration(milliseconds: 1));
expect(controller.controlsVisible, isFalse);
});
testWidgets('changing hide delay restarts paused auto-hide timer', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.configure(hideDelay: const Duration(milliseconds: 200));
controller.setPlaying(true);
controller.setPlaying(false);
await tester.pump(const Duration(milliseconds: 100));
controller.configure(hideDelay: const Duration(milliseconds: 300));
await tester.pump(const Duration(milliseconds: 299));
expect(controller.controlsVisible, isTrue);
await tester.pump(const Duration(milliseconds: 1));
expect(controller.controlsVisible, isFalse);
});
testWidgets('show while paused restarts paused auto-hide timer', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.configure(hideDelay: const Duration(milliseconds: 100));
controller.setPlaying(true);
controller.setPlaying(false);
await tester.pump(const Duration(milliseconds: 50));
controller.show();
await tester.pump(const Duration(milliseconds: 99));
expect(controller.controlsVisible, isTrue);
await tester.pump(const Duration(milliseconds: 1));
expect(controller.controlsVisible, isFalse);
});
testWidgets('pointer activity while paused restarts paused auto-hide timer', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.configure(hideDelay: const Duration(milliseconds: 100));
controller.setPlaying(true);
controller.setPlaying(false);
await tester.pump(const Duration(milliseconds: 50));
expect(controller.recordPointerActivity(), isTrue);
await tester.pump(const Duration(milliseconds: 99));
expect(controller.controlsVisible, isTrue);
await tester.pump(const Duration(milliseconds: 1));
expect(controller.controlsVisible, isFalse);
});
test('show stores play/pause focus request and notifies even when already visible', () {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
var notifications = 0;
controller.addListener(() => notifications++);
controller.show(focusPlayPause: true);
expect(notifications, 1);
expect(controller.pendingPlayPauseFocus, isTrue);
expect(controller.takePlayPauseFocus(), isTrue);
expect(controller.takePlayPauseFocus(), isFalse);
});
test('hide keeps controls presented until the opacity animation completes', () {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.hide();
expect(controller.controlsVisible, isFalse);
expect(controller.controlsPresented, isTrue);
controller.markControlsHidden();
expect(controller.controlsPresented, isFalse);
});
test('a hidden start is also unpresented, so back is not classified as hide-the-chrome', () {
final controller = PlayerChromeController(initiallyVisible: false);
addTearDown(controller.dispose);
expect(controller.controlsVisible, isFalse);
expect(controller.controlsPresented, isFalse);
expect(controller.hide(), isFalse, reason: 'there is nothing to hide, so back must fall through to the route');
});
test('showing after a hidden start restores both visibility and presentation', () {
final controller = PlayerChromeController(initiallyVisible: false);
addTearDown(controller.dispose);
controller.show();
expect(controller.controlsVisible, isTrue);
expect(controller.controlsPresented, isTrue);
});
test('a stale fade-out completion cannot hide controls that were shown again', () {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.hide();
controller.show();
controller.markControlsHidden();
expect(controller.controlsVisible, isTrue);
expect(controller.controlsPresented, isTrue);
});
test('silent release removes hold without notifying listeners', () {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.hold(PlayerChromeHold.promptInteraction);
var notifications = 0;
controller.addListener(() => notifications++);
controller.release(PlayerChromeHold.promptInteraction, notify: false, restartAutoHide: false);
expect(controller.isHeld(PlayerChromeHold.promptInteraction), isFalse);
expect(notifications, 0);
});
testWidgets('interaction region shows on hover and hides on exit', (tester) async {
final controller = PlayerChromeController();
addTearDown(controller.dispose);
controller.hide();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 200,
height: 200,
child: PlayerChromeInteractionRegion(
controller: controller,
hideOnExit: true,
child: const ColoredBox(color: Colors.black),
),
),
),
),
);
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(mouse.removePointer);
await mouse.addPointer(location: const Offset(250, 250));
await tester.pump();
await mouse.moveTo(const Offset(20, 20));
await tester.pump();
expect(controller.controlsVisible, isTrue);
await mouse.moveTo(const Offset(250, 250));
await tester.pump();
expect(controller.controlsVisible, isFalse);
});
});
}