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..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});
diff --git a/packages/jaspr_router/lib/src/platform/platform_server.dart b/packages/jaspr_router/lib/src/platform/platform_server.dart
index a80f0c9cc..b627fa821 100644
--- a/packages/jaspr_router/lib/src/platform/platform_server.dart
+++ b/packages/jaspr_router/lib/src/platform/platform_server.dart
@@ -36,8 +36,9 @@ class HistoryManagerImpl implements HistoryManager {
}
@override
- void init(BuildContext context, {void Function(Object? state, {String? url})? onChangeState}) {
+ 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 3c61bd136..94094ddbe 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';
@@ -23,12 +24,14 @@ class PlatformRouterImpl implements PlatformRouter {
/// Accesses the window.history api
class HistoryManagerImpl implements HistoryManager {
@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) {
- window.onPopState.listen((event) {
+ final subscription = window.onPopState.listen((event) {
onChangeState(window.history.state);
});
+ return subscription.cancel;
}
+ return null;
}
@override
diff --git a/packages/jaspr_router/lib/src/router.dart b/packages/jaspr_router/lib/src/router.dart
index 39c1b222c..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