From b3e8a490a35fe0446e6aa528cd0969f2bec91ef4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 26 May 2026 10:04:45 +0000 Subject: [PATCH] test(ensemble): assert Page cancels header storage timers on dispose Widget tests mount Page with storage-bound titleBarHeight and collapsible visibility, remove the widget, then advance fake clock past multiple poll intervals. Ensures dispose cancels periodic timers so no setState runs after dispose (regression guard for page header lifecycle fix). Co-authored-by: Sharjeel Yunus --- .../page_storage_timer_dispose_test.dart | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 modules/ensemble/test/widget/page_storage_timer_dispose_test.dart diff --git a/modules/ensemble/test/widget/page_storage_timer_dispose_test.dart b/modules/ensemble/test/widget/page_storage_timer_dispose_test.dart new file mode 100644 index 000000000..e32e83e73 --- /dev/null +++ b/modules/ensemble/test/widget/page_storage_timer_dispose_test.dart @@ -0,0 +1,106 @@ +import 'package:ensemble/framework/data_context.dart'; +import 'package:ensemble/framework/view/page.dart'; +import 'package:ensemble/page_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yaml/yaml.dart'; + +/// Regression coverage for [PageState] dispose: periodic timers and storage +/// subscriptions created for reactive header height / collapsible visibility +/// must be cancelled so callbacks never call [setState] after dispose. +void main() { + testWidgets( + 'Page cancels title-bar height poll timer and storage listener on dispose', + (tester) async { + final doc = loadYaml( + ''' +View: + body: + Column: + children: + - Text: + text: body + header: + titleText: Title + styles: + listenTitleBarHeightStorage: true + titleBarHeight: ensemble.storage.tbHeight +''', + ) as YamlMap; + + final model = PageModel.fromYaml(doc) as SinglePageModel; + expect(model.headerModel, isNotNull); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => Page( + dataContext: DataContext(buildContext: context), + pageModel: model, + onRendered: () {}, + ), + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + + for (var i = 0; i < 25; i++) { + await tester.pump(const Duration(milliseconds: 100)); + } + + expect(tester.takeException(), isNull); + }, + ); + + testWidgets( + 'Page cancels collapsible header visibility poll timer on dispose', + (tester) async { + final doc = loadYaml( + ''' +View: + body: + Column: + children: + - Text: + text: body + header: + titleText: Title + styles: + collapsibleHeader: + enabled: true + visible: ensemble.storage.hdrVis +''', + ) as YamlMap; + + final model = PageModel.fromYaml(doc) as SinglePageModel; + expect(model.headerModel, isNotNull); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => Page( + dataContext: DataContext(buildContext: context), + pageModel: model, + onRendered: () {}, + ), + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + + for (var i = 0; i < 25; i++) { + await tester.pump(const Duration(milliseconds: 100)); + } + + expect(tester.takeException(), isNull); + }, + ); +}