Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions lib/page/dashboard/views/components/effects/jiggle_shake.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class _JiggleShakeState extends State<JiggleShake>
late final Animation<double> _animation;
final _random = Random();

/// Platform "reduce motion" accessibility flag. Read from MediaQuery in
/// didChangeDependencies (not initState, where inherited widgets are unsafe).
bool _reduceMotion = false;

@override
void initState() {
super.initState();
Expand All @@ -42,36 +46,45 @@ class _JiggleShakeState extends State<JiggleShake>
begin: startPositive ? -maxRad : maxRad,
end: startPositive ? maxRad : -maxRad,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}

if (widget.active) {
_startShaking();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// MediaQuery is only safe to read here — not in initState.
_reduceMotion = MediaQuery.maybeDisableAnimationsOf(context) ?? false;
_syncAnimation();
}

@override
void didUpdateWidget(covariant JiggleShake oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.active != oldWidget.active) {
if (widget.active) {
_startShaking();
} else {
_stopShaking();
}
_syncAnimation();
}
}

/// Starts or stops the shake based on [active] and the reduce-motion flag.
void _syncAnimation() {
if (widget.active && !_reduceMotion) {
_startShaking();
} else {
_stopShaking();
}
}

void _startShaking() {
if (_controller.isAnimating) return;
// Add a tiny random delay before starting to enhance the organic feel
Future.delayed(Duration(milliseconds: _random.nextInt(50)), () {
if (mounted && widget.active) {
if (mounted && widget.active && !_reduceMotion) {
_controller.repeat(reverse: true);
}
});
}

void _stopShaking() {
_controller.stop();
_controller.animateTo(0.5); // Reset to center (approx) or just reset
_controller.reset();
}

Expand All @@ -83,13 +96,14 @@ class _JiggleShakeState extends State<JiggleShake>

@override
Widget build(BuildContext context) {
// Optimization: If not active and not animating, return child directly (or Rotation with 0)
// But RotationTransition with 0 turns is cheap.
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
// If inactive, force 0 rotation
final turns = widget.active ? _animation.value : 0.0;
// build() is the single source of truth for the rendered angle:
// force 0 when inactive or when reduce-motion is on (deterministic,
// static rendering — also what golden tests rely on).
final turns =
(widget.active && !_reduceMotion) ? _animation.value : 0.0;
return Transform.rotate(
angle: turns,
child: child,
Expand Down
14 changes: 3 additions & 11 deletions run_golden_verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,13 @@ else
DART="dart"
fi

EMBED_FLAG=""

while getopts l:s:f:v:-: flag
while getopts l:s:f:v: flag
do
case "${flag}" in
l) locales=${OPTARG};;
s) screens=${OPTARG};;
f) file=${OPTARG};;
v) version=${OPTARG};;
-)
case "${OPTARG}" in
embed) EMBED_FLAG="--embed";;
esac
;;
esac
done

Expand All @@ -43,7 +36,6 @@ echo "*********************Golden Test Verification********************"
echo "Locales: $locales"
echo "Screens: $screens"
echo "Version: $version"
echo "Embed images: ${EMBED_FLAG:-no}"

if [ -z "$file" ]; then
IFS=',' read -ra LOCS <<< "$locales"
Expand All @@ -57,7 +49,7 @@ if [ -z "$file" ]; then
rm -f $REPORT_DIR/tests.json
done

$DART run test_scripts/combine_results.dart $REPORT_DIR "$version" $EMBED_FLAG || FAILED=1
$DART run test_scripts/combine_results.dart $REPORT_DIR "$version" || FAILED=1
echo ""
echo "Report generated: $REPORT_DIR/golden_verify_report.html"
else
Expand All @@ -68,7 +60,7 @@ else
--dart-define=visualEffects=0 || FAILED=1
$DART run test_scripts/test_result_parser.dart $REPORT_DIR/tests.json "$locales" || FAILED=1
rm -f $REPORT_DIR/tests.json
$DART run test_scripts/combine_results.dart $REPORT_DIR "$version" $EMBED_FLAG || FAILED=1
$DART run test_scripts/combine_results.dart $REPORT_DIR "$version" || FAILED=1
echo ""
echo "Report generated: $REPORT_DIR/golden_verify_report.html"
fi
Expand Down
40 changes: 40 additions & 0 deletions test/golden_test/golden_framework/golden_interactions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

/// Shared interaction helpers for golden `Interaction.steps`.
///
/// These exist to keep interaction steps locale-independent and free of
/// brittle geometry-based gestures.

/// Switches to the tab at [index] by driving the [TabController] directly,
/// then pumps until the tab-change animation settles.
///
/// Why not `tester.tap(find.byType(Tab).at(index))`? The USP pages use a
/// non-scrollable TabBar (`TabAlignment.fill`), so long localized labels
/// (e.g. Danish, German, Russian) push the rightmost tab's center off-screen.
/// A geometric tap then lands outside the surface, misses, and the tab never
/// switches — breaking every interaction that expects that tab's content, but
/// only in the locales with long labels. Driving the controller expresses the
/// real intent ("show the Nth tab") and works in every locale.
///
/// Assumes exactly one [TabBar] is on screen (the common case for USP detail
/// views). Pass [tabBarFinder] to disambiguate if a view ever nests TabBars.
Future<void> switchToTab(
WidgetTester tester,
int index, {
Finder? tabBarFinder,
}) async {
final finder = tabBarFinder ?? find.byType(TabBar);
final tabBar = tester.widget<TabBar>(finder.first);
final controller = tabBar.controller;
assert(controller != null, 'TabBar has no controller to drive.');
controller!.animateTo(index);

// Let the tab-change + TabBarView transition settle. TabController animations
// run ~300ms; pump generously past that so the target tab's content is fully
// built and interactive before the caller queries the tree.
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
}
11 changes: 11 additions & 0 deletions test/golden_test/golden_framework/golden_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,17 @@ Widget _buildGoldenWidget(
themeMode: brightness == Brightness.dark
? ThemeMode.dark
: ThemeMode.light,
// Force "reduce motion" for every golden so non-deterministic /
// looping animations (e.g. dashboard JiggleShake) render at a
// fixed, static frame. Applied inside the app via builder so the
// views under test actually observe disableAnimations: true
// (a MediaQuery wrapped outside MaterialApp would be overridden).
// This does NOT touch the global diffThreshold; it only removes the
// animation source of flakiness.
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(disableAnimations: true),
child: child ?? const SizedBox.shrink(),
),
routerConfig: router,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:privacy_gui/page/port_forwarding/views/components/usp_single_por
import 'package:privacy_gui/page/port_forwarding/views/usp_port_forwarding_detail_view.dart';
import 'package:ui_kit_library/ui_kit.dart' show AppIconButton;

import '../../../golden_framework/golden_interactions.dart';
import '../../../golden_framework/golden_runner.dart';
import '../../../golden_framework/golden_test_config.dart';
import '../../../golden_framework/mocks/mock_port_forwarding.dart';
Expand Down Expand Up @@ -38,19 +39,15 @@ void main() {
portForwardingOverrides(dataState()),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(1));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
await switchToTab(tester, 1);
},
),
'tab_port_triggering': Interaction(
setup: (overrides) => overrides.addAll(
portForwardingOverrides(dataState()),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(2));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
await switchToTab(tester, 2);
},
),
// --- Empty tab views ---
Expand All @@ -59,19 +56,15 @@ void main() {
portForwardingOverrides(emptyDataState),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(1));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
await switchToTab(tester, 1);
},
),
'empty_port_triggering': Interaction(
setup: (overrides) => overrides.addAll(
portForwardingOverrides(emptyDataState),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(2));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
await switchToTab(tester, 2);
},
),
// --- Add dialogs ---
Expand All @@ -94,11 +87,7 @@ void main() {
portForwardingOverrides(dataState()),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(1));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 1);
final addBtn = find.descendant(
of: find.byType(UspPortRangeTab),
matching: find.byType(AppIconButton),
Expand All @@ -115,11 +104,7 @@ void main() {
portForwardingOverrides(dataState()),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(2));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 2);
final addBtn = find.descendant(
of: find.byType(UspPortTriggeringTab),
matching: find.byType(AppIconButton),
Expand Down Expand Up @@ -154,11 +139,7 @@ void main() {
portForwardingOverrides(dataState()),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(1));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 1);
final buttons = find.descendant(
of: find.byType(UspPortRangeTab),
matching: find.byType(AppIconButton),
Expand All @@ -175,11 +156,7 @@ void main() {
portForwardingOverrides(dataState()),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(2));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 2);
final buttons = find.descendant(
of: find.byType(UspPortTriggeringTab),
matching: find.byType(AppIconButton),
Expand Down Expand Up @@ -255,11 +232,7 @@ void main() {
portForwardingOverrides(dataState()),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(1));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 1);
final addBtn = find.descendant(
of: find.byType(UspPortRangeTab),
matching: find.byType(AppIconButton),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:privacy_gui/page/statistics/views/usp_statistics_view.dart';

import '../../../golden_framework/golden_interactions.dart';
import '../../../golden_framework/golden_runner.dart';
import '../../../golden_framework/golden_test_config.dart';
import '../../../golden_framework/mocks/mock_statistics.dart';
Expand Down Expand Up @@ -35,11 +34,7 @@ void main() {
),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(1));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 1);
},
),
'tab_system': Interaction(
Expand All @@ -51,11 +46,7 @@ void main() {
),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(2));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 2);
},
),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:privacy_gui/page/wifi_settings/views/usp_wifi_settings_view.dart';

import '../../../golden_framework/golden_interactions.dart';
import '../../../golden_framework/golden_runner.dart';
import '../../../golden_framework/golden_test_config.dart';
import '../../../golden_framework/mocks/mock_wifi_settings.dart';
Expand Down Expand Up @@ -43,11 +43,7 @@ void main() {
),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(1));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 1);
},
),
'tab_advanced_dfs_off': Interaction(
Expand All @@ -58,11 +54,7 @@ void main() {
),
),
steps: (tester) async {
await tester.tap(find.byType(Tab).at(1));
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
}
await switchToTab(tester, 1);
},
),
'dialog_edit_name': Interaction(
Expand All @@ -73,7 +65,11 @@ void main() {
),
),
steps: (tester) async {
await tester.tap(find.bySemanticsLabel('wifi-name-2.4GHz').first);
// SSID value from fixture data — locale-independent. The golden
// runner does not build a semantics tree, so bySemanticsLabel finds
// nothing; tap the SSID text like the sibling dialog interactions.
// First per-band card is 2.4GHz (ssid: 'HomeNetwork').
await tester.tap(find.text('HomeNetwork').first);
await tester.pump();
for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 50));
Expand Down
Loading
Loading