diff --git a/dashboard/lib/state/presubmit.dart b/dashboard/lib/state/presubmit.dart index c491381f3..421cc7047 100644 --- a/dashboard/lib/state/presubmit.dart +++ b/dashboard/lib/state/presubmit.dart @@ -519,6 +519,17 @@ class PresubmitState extends ChangeNotifier { false; } + void resume() { + if (!_active) return; + _startTimer(); + _fetchRefreshUpdate(); + } + + void pause() { + refreshTimer?.cancel(); + refreshTimer = null; + } + void _fetchRefreshUpdate() { if (!_active) return; fetchIfNeeded(); diff --git a/dashboard/lib/views/presubmit_view.dart b/dashboard/lib/views/presubmit_view.dart index 076d62564..36aff0651 100644 --- a/dashboard/lib/views/presubmit_view.dart +++ b/dashboard/lib/views/presubmit_view.dart @@ -42,9 +42,16 @@ final class PreSubmitView extends StatefulWidget { State createState() => _PreSubmitViewState(); } -class _PreSubmitViewState extends State { +class _PreSubmitViewState extends State + with WidgetsBindingObserver { PresubmitState? _presubmitState; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -59,10 +66,20 @@ class _PreSubmitViewState extends State { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _presubmitState?.removeListener(_onStateChanged); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _presubmitState?.resume(); + } else { + _presubmitState?.pause(); + } + } + @override void didUpdateWidget(PreSubmitView oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/dashboard/test/state/presubmit_test.dart b/dashboard/test/state/presubmit_test.dart index 32abef494..b1dfd5f37 100644 --- a/dashboard/test/state/presubmit_test.dart +++ b/dashboard/test/state/presubmit_test.dart @@ -267,6 +267,17 @@ void main() { expect(presubmitState.refreshTimer?.isActive, isFalse); }); + test('PresubmitState pause and resume timer management', () async { + presubmitState.addListener(() {}); // Trigger timer start + expect(presubmitState.refreshTimer, isNotNull); + + presubmitState.pause(); + expect(presubmitState.refreshTimer, isNull); + + presubmitState.resume(); + expect(presubmitState.refreshTimer, isNotNull); + }); + test( 'PresubmitState refreshes on auth change when becoming authenticated', () async { diff --git a/dashboard/test/views/presubmit_view_test.dart b/dashboard/test/views/presubmit_view_test.dart index 85ee42aa1..e3b1e4bdc 100644 --- a/dashboard/test/views/presubmit_view_test.dart +++ b/dashboard/test/views/presubmit_view_test.dart @@ -281,82 +281,94 @@ void main() { expect(find.textContaining('Status: Failed'), findsOneWidget); }); - testWidgets('PreSubmitView displays default job details when summary is empty', ( - WidgetTester tester, - ) async { - tester.view.physicalSize = const Size(2000, 1080); - tester.view.devicePixelRatio = 1.0; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - - const mockSha = 'decaf_3_real_sha'; - const guardResponse = PresubmitGuardResponse( - prNum: 123, - author: 'dash', - guardStatus: GuardStatus.failed, - checkRunId: 456, - stages: [ - PresubmitGuardStage( - name: 'Engine', - createdAt: 0, - builds: {'Mac mac_host_engine 1': TaskStatus.failed}, - ), - ], - ); + testWidgets( + 'PreSubmitView displays default job details when summary is empty', + (WidgetTester tester) async { + tester.view.physicalSize = const Size(2000, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); - when( - mockCocoonService.fetchPresubmitGuard( - repo: anyNamed('repo'), - sha: mockSha, - ), - ).thenAnswer((_) async => const CocoonResponse.data(guardResponse)); + const mockSha = 'decaf_3_real_sha'; + const guardResponse = PresubmitGuardResponse( + prNum: 123, + author: 'dash', + guardStatus: GuardStatus.failed, + checkRunId: 456, + stages: [ + PresubmitGuardStage( + name: 'Engine', + createdAt: 0, + builds: {'Mac mac_host_engine 1': TaskStatus.failed}, + ), + ], + ); - when( - mockCocoonService.fetchPresubmitJobDetails( - checkRunId: anyNamed('checkRunId'), - jobName: argThat(contains('mac_host_engine'), named: 'jobName'), - ), - ).thenAnswer( - (_) async => CocoonResponse.data([ - PresubmitJobResponse( - attemptNumber: 1, - jobName: 'Mac mac_host_engine 1', - creationTime: 0, - status: TaskStatus.failed, - summary: '', // Empty summary + when( + mockCocoonService.fetchPresubmitGuard( + repo: anyNamed('repo'), + sha: mockSha, ), - ]), - ); + ).thenAnswer((_) async => const CocoonResponse.data(guardResponse)); - await tester.runAsync(() async { - await tester.pumpWidget( - createPreSubmitView({'repo': 'flutter', 'pr': '123'}), + when( + mockCocoonService.fetchPresubmitJobDetails( + checkRunId: anyNamed('checkRunId'), + jobName: argThat(contains('mac_host_engine'), named: 'jobName'), + ), + ).thenAnswer( + (_) async => CocoonResponse.data([ + PresubmitJobResponse( + attemptNumber: 1, + jobName: 'Mac mac_host_engine 1', + creationTime: 0, + status: TaskStatus.failed, + summary: '', // Empty summary + ), + ]), ); - for (var i = 0; i < 50; i++) { - await tester.pump(); - await Future.delayed(const Duration(milliseconds: 50)); - if (find.textContaining('by dash').evaluate().isNotEmpty) break; - } - }); - await tester.pumpAndSettle(); - expect(find.textContaining('PR #123'), findsOneWidget); + await tester.runAsync(() async { + await tester.pumpWidget( + createPreSubmitView({'repo': 'flutter', 'pr': '123'}), + ); + for (var i = 0; i < 50; i++) { + await tester.pump(); + await Future.delayed(const Duration(milliseconds: 50)); + if (find.textContaining('by dash').evaluate().isNotEmpty) break; + } + }); + await tester.pumpAndSettle(); - await tester.tap(find.textContaining('mac_host_engine').first); - await tester.runAsync(() async { - for (var i = 0; i < 50; i++) { - await tester.pump(); - await Future.delayed(const Duration(milliseconds: 50)); - if (find.textContaining('Mac mac_host_engine 1 failed.').evaluate().isNotEmpty) { - break; + expect(find.textContaining('PR #123'), findsOneWidget); + + await tester.tap(find.textContaining('mac_host_engine').first); + await tester.runAsync(() async { + for (var i = 0; i < 50; i++) { + await tester.pump(); + await Future.delayed(const Duration(milliseconds: 50)); + if (find + .textContaining('Mac mac_host_engine 1 failed.') + .evaluate() + .isNotEmpty) { + break; + } } - } - }); - await tester.pumpAndSettle(); + }); + await tester.pumpAndSettle(); - expect(find.textContaining('Mac mac_host_engine 1 failed.'), findsOneWidget); - expect(find.textContaining('Click "View more details on LUCI UI" button below for more details.'), findsOneWidget); - }); + expect( + find.textContaining('Mac mac_host_engine 1 failed.'), + findsOneWidget, + ); + expect( + find.textContaining( + 'Click "View more details on LUCI UI" button below for more details.', + ), + findsOneWidget, + ); + }, + ); testWidgets( 'PreSubmitView automatically selects latest SHA and updates sidebar when opened with PR only',