From d65ccd5acdbf9cb2217357a2d16e75c208631cd0 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Thu, 16 Jul 2026 14:34:50 +0200 Subject: [PATCH 1/2] properly unmount components between test runs --- packages/jaspr/CHANGELOG.md | 5 +++++ .../jaspr/lib/src/framework/components_binding.dart | 12 ++++++++++++ .../jaspr/test/client/basic/basic_browser_test.dart | 5 +++++ packages/jaspr_router/CHANGELOG.md | 5 +++++ packages/jaspr_router/lib/src/platform/platform.dart | 3 +++ .../lib/src/platform/platform_server.dart | 5 +++++ .../jaspr_router/lib/src/platform/platform_web.dart | 12 +++++++++++- packages/jaspr_router/lib/src/router.dart | 6 ++++++ packages/jaspr_router/test/utils.dart | 5 +++++ packages/jaspr_test/lib/src/finders.dart | 6 +++++- .../jaspr_test/lib/src/testers/client_tester.dart | 8 +++++--- 11 files changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/jaspr/CHANGELOG.md b/packages/jaspr/CHANGELOG.md index e3ac1aa70..095bfd1a1 100644 --- a/packages/jaspr/CHANGELOG.md +++ b/packages/jaspr/CHANGELOG.md @@ -1,3 +1,8 @@ +## Unreleased patch + +- Added `detachRootComponent()` to `ComponentsBinding` to cleanly unmount the root component. +- Updated `testClient` in `jaspr_test` to automatically unmount the root component and clean up `document.body` between test cases. + ## 0.23.2 - Added `basePath` property to `AppBinding` to support hosting applications under a sub-path. diff --git a/packages/jaspr/lib/src/framework/components_binding.dart b/packages/jaspr/lib/src/framework/components_binding.dart index 7ab93205d..c96b154ec 100644 --- a/packages/jaspr/lib/src/framework/components_binding.dart +++ b/packages/jaspr/lib/src/framework/components_binding.dart @@ -15,6 +15,18 @@ mixin ComponentsBinding on AppBinding { buildOwner.performInitialBuild(element, completeInitialFrame); } + /// Detaches the current root component from the tree and disposes/unmounts it. + void detachRootComponent() { + if (_rootElement != null) { + final buildOwner = _rootElement!._owner!; + buildOwner.lockState(() { + buildOwner._inactiveElements.add(_rootElement!); + buildOwner._inactiveElements._unmountAll(); + }); + _rootElement = null; + } + } + RenderObject createRootRenderObject(); BuildOwner createRootBuildOwner() { diff --git a/packages/jaspr/test/client/basic/basic_browser_test.dart b/packages/jaspr/test/client/basic/basic_browser_test.dart index 7fea510d4..e93b9f546 100644 --- a/packages/jaspr/test/client/basic/basic_browser_test.dart +++ b/packages/jaspr/test/client/basic/basic_browser_test.dart @@ -16,5 +16,10 @@ void main() { expect(find.text('Count: 1'), findsOneComponent); }); + + testClient('should clean up and unmount after previous test', (tester) async { + expect(find.text('Count: 0'), findsNothing); + expect(find.text('Count: 1'), findsNothing); + }); }); } diff --git a/packages/jaspr_router/CHANGELOG.md b/packages/jaspr_router/CHANGELOG.md index 890fb7d30..3cafdf393 100644 --- a/packages/jaspr_router/CHANGELOG.md +++ b/packages/jaspr_router/CHANGELOG.md @@ -1,3 +1,8 @@ +## Unreleased patch + +- Added `dispose()` to the `HistoryManager` interface. +- Implemented `RouterState.dispose()` to cancel `onPopState` subscriptions when the router is unmounted. + ## 0.8.3 - Fixed routing and redirect bug to respect the `` configuration by prepending `basePath` on client history operations, server redirect headers, and `Link` hrefs. diff --git a/packages/jaspr_router/lib/src/platform/platform.dart b/packages/jaspr_router/lib/src/platform/platform.dart index 8059d1619..6b88ba6f0 100644 --- a/packages/jaspr_router/lib/src/platform/platform.dart +++ b/packages/jaspr_router/lib/src/platform/platform.dart @@ -28,6 +28,9 @@ abstract class HistoryManager { /// Go back in the history void back(); + + /// Dispose any listeners or resources + void dispose(); } abstract class RouteRegistry { diff --git a/packages/jaspr_router/lib/src/platform/platform_server.dart b/packages/jaspr_router/lib/src/platform/platform_server.dart index a80f0c9cc..8fa726af8 100644 --- a/packages/jaspr_router/lib/src/platform/platform_server.dart +++ b/packages/jaspr_router/lib/src/platform/platform_server.dart @@ -39,6 +39,11 @@ class HistoryManagerImpl implements HistoryManager { void init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) { // No-op } + + @override + void dispose() { + // No-op + } } class RouteRegistryImpl implements RouteRegistry { diff --git a/packages/jaspr_router/lib/src/platform/platform_web.dart b/packages/jaspr_router/lib/src/platform/platform_web.dart index 3c61bd136..a6e214f4e 100644 --- a/packages/jaspr_router/lib/src/platform/platform_web.dart +++ b/packages/jaspr_router/lib/src/platform/platform_web.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:js_interop'; import 'package:jaspr/jaspr.dart'; @@ -22,10 +23,13 @@ class PlatformRouterImpl implements PlatformRouter { /// Browser implementation of HistoryManager /// Accesses the window.history api class HistoryManagerImpl implements HistoryManager { + StreamSubscription? _subscription; + @override void init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) { if (onChangeState != null) { - window.onPopState.listen((event) { + _subscription?.cancel(); + _subscription = window.onPopState.listen((event) { onChangeState(window.history.state); }); } @@ -45,6 +49,12 @@ class HistoryManagerImpl implements HistoryManager { void back() { window.history.back(); } + + @override + void dispose() { + _subscription?.cancel(); + _subscription = null; + } } class RouteRegistryImpl implements RouteRegistry { diff --git a/packages/jaspr_router/lib/src/router.dart b/packages/jaspr_router/lib/src/router.dart index 39c1b222c..6de5eb821 100644 --- a/packages/jaspr_router/lib/src/router.dart +++ b/packages/jaspr_router/lib/src/router.dart @@ -223,6 +223,12 @@ class RouterState extends State with PreloadStateMixin { return prefix + location; } + @override + void dispose() { + PlatformRouter.instance.history.dispose(); + super.dispose(); + } + @override Component build(BuildContext context) { return Component.fragment([ diff --git a/packages/jaspr_router/test/utils.dart b/packages/jaspr_router/test/utils.dart index 5e773da6d..320bbb3f2 100644 --- a/packages/jaspr_router/test/utils.dart +++ b/packages/jaspr_router/test/utils.dart @@ -70,6 +70,11 @@ class MockHistoryManager implements HistoryManager { history.removeLast(); onChangeState?.call(null, url: history.last); } + + @override + void dispose() { + onChangeState = null; + } } class MockRouteRegistry implements RouteRegistry { diff --git a/packages/jaspr_test/lib/src/finders.dart b/packages/jaspr_test/lib/src/finders.dart index 2c3cdcd73..efc2e07c8 100644 --- a/packages/jaspr_test/lib/src/finders.dart +++ b/packages/jaspr_test/lib/src/finders.dart @@ -228,7 +228,11 @@ abstract class Finder { /// See [collectAllElementsFrom]. @protected Iterable get allCandidates { - return collectAllElementsFrom(TestBinding.currentRootElement!); + final root = TestBinding.currentRootElement; + if (root == null) { + return const []; + } + return collectAllElementsFrom(root); } Iterable? _cachedResult; diff --git a/packages/jaspr_test/lib/src/testers/client_tester.dart b/packages/jaspr_test/lib/src/testers/client_tester.dart index 5f5821ccf..ba022f2c9 100644 --- a/packages/jaspr_test/lib/src/testers/client_tester.dart +++ b/packages/jaspr_test/lib/src/testers/client_tester.dart @@ -30,12 +30,14 @@ void testClient( final binding = ClientAppBinding(); final tester = ClientTester._(binding); + addTearDown(() { + binding.detachRootComponent(); + web.document.body?.replaceChildren([].toJS); + }); + await binding.runTest(() async { await callback(tester); }); - - // Clear all nodes - web.document.body?.replaceChildren([].toJS); }, skip: skip, timeout: timeout, From e2c5ae74c345923ff0316639c6d8b14a1aabadd3 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Fri, 17 Jul 2026 13:17:20 +0200 Subject: [PATCH 2/2] change history dispose through callback --- .../jaspr_router/lib/src/platform/platform.dart | 9 ++++----- .../lib/src/platform/platform_server.dart | 8 ++------ .../lib/src/platform/platform_web.dart | 15 ++++----------- packages/jaspr_router/lib/src/router.dart | 6 ++++-- packages/jaspr_router/test/utils.dart | 10 ++++------ pubspec.lock | 10 +++++----- 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/packages/jaspr_router/lib/src/platform/platform.dart b/packages/jaspr_router/lib/src/platform/platform.dart index 6b88ba6f0..27a6f0cf9 100644 --- a/packages/jaspr_router/lib/src/platform/platform.dart +++ b/packages/jaspr_router/lib/src/platform/platform.dart @@ -17,8 +17,10 @@ abstract class PlatformRouter { /// Interface for history management /// Will be implemented separately on browser and server abstract class HistoryManager { - /// Initialize the history manager and setup any listeners to history changes - void init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}); + /// Initialize the history manager and setup any listeners to history changes. + /// + /// Returns a callback that can be used to stop listening. + VoidCallback? init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}); /// Push a new state to the history void push(String url, {String? title, Object? data}); @@ -28,9 +30,6 @@ abstract class HistoryManager { /// Go back in the history void back(); - - /// Dispose any listeners or resources - void dispose(); } abstract class RouteRegistry { diff --git a/packages/jaspr_router/lib/src/platform/platform_server.dart b/packages/jaspr_router/lib/src/platform/platform_server.dart index 8fa726af8..b627fa821 100644 --- a/packages/jaspr_router/lib/src/platform/platform_server.dart +++ b/packages/jaspr_router/lib/src/platform/platform_server.dart @@ -36,13 +36,9 @@ class HistoryManagerImpl implements HistoryManager { } @override - void init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) { - // No-op - } - - @override - void dispose() { + VoidCallback? init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) { // No-op + return null; } } diff --git a/packages/jaspr_router/lib/src/platform/platform_web.dart b/packages/jaspr_router/lib/src/platform/platform_web.dart index a6e214f4e..94094ddbe 100644 --- a/packages/jaspr_router/lib/src/platform/platform_web.dart +++ b/packages/jaspr_router/lib/src/platform/platform_web.dart @@ -23,16 +23,15 @@ class PlatformRouterImpl implements PlatformRouter { /// Browser implementation of HistoryManager /// Accesses the window.history api class HistoryManagerImpl implements HistoryManager { - StreamSubscription? _subscription; - @override - void init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) { + VoidCallback? init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) { if (onChangeState != null) { - _subscription?.cancel(); - _subscription = window.onPopState.listen((event) { + final subscription = window.onPopState.listen((event) { onChangeState(window.history.state); }); + return subscription.cancel; } + return null; } @override @@ -49,12 +48,6 @@ class HistoryManagerImpl implements HistoryManager { void back() { window.history.back(); } - - @override - void dispose() { - _subscription?.cancel(); - _subscription = null; - } } class RouteRegistryImpl implements RouteRegistry { diff --git a/packages/jaspr_router/lib/src/router.dart b/packages/jaspr_router/lib/src/router.dart index 6de5eb821..25409c1bc 100644 --- a/packages/jaspr_router/lib/src/router.dart +++ b/packages/jaspr_router/lib/src/router.dart @@ -52,6 +52,7 @@ class RouterState extends State with PreloadStateMixin { RouteMatchList get matchList => _matchList ?? RouteMatchList.empty; Map routeLoaders = {}; + VoidCallback? _routeChangeDispose; @override Future preloadState() async { @@ -64,7 +65,7 @@ class RouterState extends State with PreloadStateMixin { @override void initState() { super.initState(); - PlatformRouter.instance.history.init( + _routeChangeDispose = PlatformRouter.instance.history.init( context, onChangeState: (state, {url}) { _update(url ?? context.url, extra: state, updateHistory: false, replace: true); @@ -225,7 +226,8 @@ class RouterState extends State with PreloadStateMixin { @override void dispose() { - PlatformRouter.instance.history.dispose(); + _routeChangeDispose?.call(); + _routeChangeDispose = null; super.dispose(); } diff --git a/packages/jaspr_router/test/utils.dart b/packages/jaspr_router/test/utils.dart index 320bbb3f2..7663e8c25 100644 --- a/packages/jaspr_router/test/utils.dart +++ b/packages/jaspr_router/test/utils.dart @@ -48,9 +48,12 @@ class MockHistoryManager implements HistoryManager { late void Function(Object? state, {String? url})? onChangeState; @override - void init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) { + VoidCallback? init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) { history = [context.url]; this.onChangeState = onChangeState; + return () { + onChangeState = null; + }; } @override @@ -70,11 +73,6 @@ class MockHistoryManager implements HistoryManager { history.removeLast(); onChangeState?.call(null, url: history.last); } - - @override - void dispose() { - onChangeState = null; - } } class MockRouteRegistry implements RouteRegistry { diff --git a/pubspec.lock b/pubspec.lock index fab3d7aa6..57b0e7403 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: build_web_compilers - sha256: "3489d44e7a03b68f254c75d5db6dd6172fcbb892528275c3ed968f7e72ede716" + sha256: ef4bc35e0e335f8520692a333b872446697adce0618cf23c3084715ace641721 url: "https://pub.dev" source: hosted - version: "4.8.4" + version: "4.8.5" built_collection: dependency: transitive description: @@ -1179,10 +1179,10 @@ packages: dependency: transitive description: name: meta - sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.3" + version: "1.19.0" mime: dependency: transitive description: @@ -2021,5 +2021,5 @@ packages: source: hosted version: "2.2.4" sdks: - dart: ">=3.13.0-107.0.dev <3.13.0-z" + dart: ">=3.13.0-107.0.dev <3.14.0-z" flutter: ">=3.44.0"