Skip to content

Commit f7d5009

Browse files
authored
feat: add app lifecycle observer to pause and resume presubmit timer (#5027)
add app lifecycle observer to pause and resume presubmit timer fix: flutter/flutter#184529
1 parent 710bc91 commit f7d5009

4 files changed

Lines changed: 119 additions & 68 deletions

File tree

dashboard/lib/state/presubmit.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,17 @@ class PresubmitState extends ChangeNotifier {
519519
false;
520520
}
521521

522+
void resume() {
523+
if (!_active) return;
524+
_startTimer();
525+
_fetchRefreshUpdate();
526+
}
527+
528+
void pause() {
529+
refreshTimer?.cancel();
530+
refreshTimer = null;
531+
}
532+
522533
void _fetchRefreshUpdate() {
523534
if (!_active) return;
524535
fetchIfNeeded();

dashboard/lib/views/presubmit_view.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,16 @@ final class PreSubmitView extends StatefulWidget {
4242
State<PreSubmitView> createState() => _PreSubmitViewState();
4343
}
4444

45-
class _PreSubmitViewState extends State<PreSubmitView> {
45+
class _PreSubmitViewState extends State<PreSubmitView>
46+
with WidgetsBindingObserver {
4647
PresubmitState? _presubmitState;
4748

49+
@override
50+
void initState() {
51+
super.initState();
52+
WidgetsBinding.instance.addObserver(this);
53+
}
54+
4855
@override
4956
void didChangeDependencies() {
5057
super.didChangeDependencies();
@@ -59,10 +66,20 @@ class _PreSubmitViewState extends State<PreSubmitView> {
5966

6067
@override
6168
void dispose() {
69+
WidgetsBinding.instance.removeObserver(this);
6270
_presubmitState?.removeListener(_onStateChanged);
6371
super.dispose();
6472
}
6573

74+
@override
75+
void didChangeAppLifecycleState(AppLifecycleState state) {
76+
if (state == AppLifecycleState.resumed) {
77+
_presubmitState?.resume();
78+
} else {
79+
_presubmitState?.pause();
80+
}
81+
}
82+
6683
@override
6784
void didUpdateWidget(PreSubmitView oldWidget) {
6885
super.didUpdateWidget(oldWidget);

dashboard/test/state/presubmit_test.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,17 @@ void main() {
267267
expect(presubmitState.refreshTimer?.isActive, isFalse);
268268
});
269269

270+
test('PresubmitState pause and resume timer management', () async {
271+
presubmitState.addListener(() {}); // Trigger timer start
272+
expect(presubmitState.refreshTimer, isNotNull);
273+
274+
presubmitState.pause();
275+
expect(presubmitState.refreshTimer, isNull);
276+
277+
presubmitState.resume();
278+
expect(presubmitState.refreshTimer, isNotNull);
279+
});
280+
270281
test(
271282
'PresubmitState refreshes on auth change when becoming authenticated',
272283
() async {

dashboard/test/views/presubmit_view_test.dart

Lines changed: 79 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -281,82 +281,94 @@ void main() {
281281
expect(find.textContaining('Status: Failed'), findsOneWidget);
282282
});
283283

284-
testWidgets('PreSubmitView displays default job details when summary is empty', (
285-
WidgetTester tester,
286-
) async {
287-
tester.view.physicalSize = const Size(2000, 1080);
288-
tester.view.devicePixelRatio = 1.0;
289-
addTearDown(tester.view.resetPhysicalSize);
290-
addTearDown(tester.view.resetDevicePixelRatio);
291-
292-
const mockSha = 'decaf_3_real_sha';
293-
const guardResponse = PresubmitGuardResponse(
294-
prNum: 123,
295-
author: 'dash',
296-
guardStatus: GuardStatus.failed,
297-
checkRunId: 456,
298-
stages: [
299-
PresubmitGuardStage(
300-
name: 'Engine',
301-
createdAt: 0,
302-
builds: {'Mac mac_host_engine 1': TaskStatus.failed},
303-
),
304-
],
305-
);
284+
testWidgets(
285+
'PreSubmitView displays default job details when summary is empty',
286+
(WidgetTester tester) async {
287+
tester.view.physicalSize = const Size(2000, 1080);
288+
tester.view.devicePixelRatio = 1.0;
289+
addTearDown(tester.view.resetPhysicalSize);
290+
addTearDown(tester.view.resetDevicePixelRatio);
306291

307-
when(
308-
mockCocoonService.fetchPresubmitGuard(
309-
repo: anyNamed('repo'),
310-
sha: mockSha,
311-
),
312-
).thenAnswer((_) async => const CocoonResponse.data(guardResponse));
292+
const mockSha = 'decaf_3_real_sha';
293+
const guardResponse = PresubmitGuardResponse(
294+
prNum: 123,
295+
author: 'dash',
296+
guardStatus: GuardStatus.failed,
297+
checkRunId: 456,
298+
stages: [
299+
PresubmitGuardStage(
300+
name: 'Engine',
301+
createdAt: 0,
302+
builds: {'Mac mac_host_engine 1': TaskStatus.failed},
303+
),
304+
],
305+
);
313306

314-
when(
315-
mockCocoonService.fetchPresubmitJobDetails(
316-
checkRunId: anyNamed('checkRunId'),
317-
jobName: argThat(contains('mac_host_engine'), named: 'jobName'),
318-
),
319-
).thenAnswer(
320-
(_) async => CocoonResponse.data([
321-
PresubmitJobResponse(
322-
attemptNumber: 1,
323-
jobName: 'Mac mac_host_engine 1',
324-
creationTime: 0,
325-
status: TaskStatus.failed,
326-
summary: '', // Empty summary
307+
when(
308+
mockCocoonService.fetchPresubmitGuard(
309+
repo: anyNamed('repo'),
310+
sha: mockSha,
327311
),
328-
]),
329-
);
312+
).thenAnswer((_) async => const CocoonResponse.data(guardResponse));
330313

331-
await tester.runAsync(() async {
332-
await tester.pumpWidget(
333-
createPreSubmitView({'repo': 'flutter', 'pr': '123'}),
314+
when(
315+
mockCocoonService.fetchPresubmitJobDetails(
316+
checkRunId: anyNamed('checkRunId'),
317+
jobName: argThat(contains('mac_host_engine'), named: 'jobName'),
318+
),
319+
).thenAnswer(
320+
(_) async => CocoonResponse.data([
321+
PresubmitJobResponse(
322+
attemptNumber: 1,
323+
jobName: 'Mac mac_host_engine 1',
324+
creationTime: 0,
325+
status: TaskStatus.failed,
326+
summary: '', // Empty summary
327+
),
328+
]),
334329
);
335-
for (var i = 0; i < 50; i++) {
336-
await tester.pump();
337-
await Future<void>.delayed(const Duration(milliseconds: 50));
338-
if (find.textContaining('by dash').evaluate().isNotEmpty) break;
339-
}
340-
});
341-
await tester.pumpAndSettle();
342330

343-
expect(find.textContaining('PR #123'), findsOneWidget);
331+
await tester.runAsync(() async {
332+
await tester.pumpWidget(
333+
createPreSubmitView({'repo': 'flutter', 'pr': '123'}),
334+
);
335+
for (var i = 0; i < 50; i++) {
336+
await tester.pump();
337+
await Future<void>.delayed(const Duration(milliseconds: 50));
338+
if (find.textContaining('by dash').evaluate().isNotEmpty) break;
339+
}
340+
});
341+
await tester.pumpAndSettle();
344342

345-
await tester.tap(find.textContaining('mac_host_engine').first);
346-
await tester.runAsync(() async {
347-
for (var i = 0; i < 50; i++) {
348-
await tester.pump();
349-
await Future<void>.delayed(const Duration(milliseconds: 50));
350-
if (find.textContaining('Mac mac_host_engine 1 failed.').evaluate().isNotEmpty) {
351-
break;
343+
expect(find.textContaining('PR #123'), findsOneWidget);
344+
345+
await tester.tap(find.textContaining('mac_host_engine').first);
346+
await tester.runAsync(() async {
347+
for (var i = 0; i < 50; i++) {
348+
await tester.pump();
349+
await Future<void>.delayed(const Duration(milliseconds: 50));
350+
if (find
351+
.textContaining('Mac mac_host_engine 1 failed.')
352+
.evaluate()
353+
.isNotEmpty) {
354+
break;
355+
}
352356
}
353-
}
354-
});
355-
await tester.pumpAndSettle();
357+
});
358+
await tester.pumpAndSettle();
356359

357-
expect(find.textContaining('Mac mac_host_engine 1 failed.'), findsOneWidget);
358-
expect(find.textContaining('Click "View more details on LUCI UI" button below for more details.'), findsOneWidget);
359-
});
360+
expect(
361+
find.textContaining('Mac mac_host_engine 1 failed.'),
362+
findsOneWidget,
363+
);
364+
expect(
365+
find.textContaining(
366+
'Click "View more details on LUCI UI" button below for more details.',
367+
),
368+
findsOneWidget,
369+
);
370+
},
371+
);
360372

361373
testWidgets(
362374
'PreSubmitView automatically selects latest SHA and updates sidebar when opened with PR only',

0 commit comments

Comments
 (0)