perf(tv): cut semantics work during card navigation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/focus/card_focus_scope.dart';
|
||||
import 'package:plezy/focus/focusable_wrapper.dart';
|
||||
import 'package:plezy/focus/input_mode_tracker.dart';
|
||||
|
||||
@@ -25,7 +26,7 @@ void main() {
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pump();
|
||||
|
||||
expect(chromeIn(Transform), findsOneWidget);
|
||||
expect(chromeIn(AnimatedBuilder), findsOneWidget);
|
||||
expect(chromeIn(AnimatedContainer), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -78,4 +79,46 @@ void main() {
|
||||
expect(longPressed, 1);
|
||||
expect(selected, 1);
|
||||
});
|
||||
|
||||
testWidgets('focus scale animation keeps child semantics geometry stable', (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
final node = FocusNode(debugLabel: 'card');
|
||||
addTearDown(node.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
InputModeTracker(
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: FocusableWrapper(
|
||||
focusNode: node,
|
||||
focusScale: 1.2,
|
||||
delegateFocusBorder: true,
|
||||
child: CardFocusBorder(
|
||||
child: Semantics(label: 'card content', child: SizedBox(width: 100, height: 100)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
node.unfocus();
|
||||
await tester.pumpAndSettle();
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
final semanticsOwner = tester.binding.rootPipelineOwner.semanticsOwner!;
|
||||
var semanticsUpdates = 0;
|
||||
void countSemanticsUpdate() => semanticsUpdates++;
|
||||
semanticsOwner.addListener(countSemanticsUpdate);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
semanticsUpdates = 0;
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
|
||||
expect(semanticsUpdates, 0);
|
||||
semanticsOwner.removeListener(countSemanticsUpdate);
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,6 +316,50 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('TV cards collapse pointer-only detail semantics without a screen reader', (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
TvDetectionService.debugSetAppleTVOverride(true);
|
||||
final item = testMediaItem(
|
||||
id: 'tv_semantic_movie',
|
||||
kind: MediaKind.movie,
|
||||
title: 'TV Semantic Movie',
|
||||
summary: 'TV decorative summary',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_TestApp(
|
||||
child: SizedBox(width: 200, height: 330, child: MediaCard(item: item, forceGridMode: true, isOffline: true)),
|
||||
),
|
||||
);
|
||||
|
||||
final card = tester.getSemantics(find.bySemanticsLabel(mediaCardSemanticLabel(item))).getSemanticsData();
|
||||
expect(card.flagsCollection.isButton, isTrue);
|
||||
expect(card.hasAction(ui.SemanticsAction.tap), isTrue);
|
||||
expect(find.bySemanticsLabel('TV Semantic Movie'), findsNothing);
|
||||
expect(find.bySemanticsLabel(RegExp('TV decorative summary')), findsNothing);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('TV cards preserve detail semantics for accessible navigation', (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
TvDetectionService.debugSetAppleTVOverride(true);
|
||||
final item = testMediaItem(id: 'tv_accessible_movie', kind: MediaKind.movie, title: 'Accessible TV Movie');
|
||||
|
||||
await tester.pumpWidget(
|
||||
_TestApp(
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(accessibleNavigation: true),
|
||||
child: SizedBox(width: 200, height: 330, child: MediaCard(item: item, forceGridMode: true, isOffline: true)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final detail = tester.getSemantics(find.bySemanticsLabel('Accessible TV Movie')).getSemanticsData();
|
||||
expect(detail.flagsCollection.isButton, isTrue);
|
||||
expect(detail.hasAction(ui.SemanticsAction.tap), isTrue);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('custom card actions keep detail-link semantics disabled in grid and list modes', (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
final item = testMediaItem(
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/device_performance.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
@@ -396,6 +397,69 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('low-end snapshot optimization preserves vertical scroll animation', (tester) async {
|
||||
DevicePerformance.debugReset(autoReduced: true);
|
||||
addTearDown(DevicePerformance.debugReset);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(1280, 720);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
final serverManager = MultiServerManager();
|
||||
final hubs = List.generate(6, (hubIndex) {
|
||||
final item = testMediaItem(
|
||||
id: 'movie_$hubIndex',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
title: 'Movie $hubIndex',
|
||||
);
|
||||
return MediaHub(id: 'hub_$hubIndex', title: 'Hub $hubIndex', type: 'movie', items: [item], size: 1);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 1280,
|
||||
height: 720,
|
||||
child: TvBrowseRail(
|
||||
focusMemory: focusMemory,
|
||||
hubs: hubs,
|
||||
autofocus: true,
|
||||
iconForHub: (_, _) => Icons.movie_rounded,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
tester.state<TvBrowseRailState>(find.byType(TvBrowseRail)).requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
final position = _verticalRailPosition(tester);
|
||||
final initialOffset = position.pixels;
|
||||
tester.widget<Semantics>(find.byKey(const ValueKey('tv_browse_rail_semantic_proxy'))).properties.onScrollDown!();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 80));
|
||||
await tester.pump(const Duration(milliseconds: 80));
|
||||
final animatedOffset = position.pixels;
|
||||
expect(animatedOffset, greaterThan(initialOffset));
|
||||
expect(tester.hasRunningAnimations, isTrue);
|
||||
|
||||
final snapshots = tester.widgetList<SnapshotWidget>(find.byType(SnapshotWidget));
|
||||
expect(snapshots, isNotEmpty);
|
||||
expect(snapshots.every((widget) => widget.controller.allowSnapshotting), isTrue);
|
||||
await tester.pumpAndSettle();
|
||||
expect(position.pixels, greaterThan(animatedOffset));
|
||||
expect(snapshots.every((widget) => widget.controller.allowSnapshotting), isFalse);
|
||||
});
|
||||
|
||||
testWidgets('active hub header uses theme foreground in light mode', (tester) async {
|
||||
final serverManager = MultiServerManager();
|
||||
final theme = monoTheme(dark: false);
|
||||
|
||||
Reference in New Issue
Block a user