[CP] [web] Fix HashUrlStrategy.addPopStateListener (#41459)
This PR cherry-picks:
https://github.com/flutter/engine/commit/8d9f17e07633b0f8f2400bf00c95c4fac18f599e
Fixes https://github.com/flutter/flutter/issues/125317
---
During a JS-interop refactor, we introduced a small bug in the
`addPopStateListener` method of the `HashUrlStrategy` object (web).
This wasn't caught before because the existing tests were mocking the
refactored code.
## Issues
Fixes: https://github.com/flutter/flutter/issues/125228
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or Hixie said the PR is test-exempt. See [testing the engine]
for instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
diff --git a/lib/web_ui/lib/src/engine/navigation/url_strategy.dart b/lib/web_ui/lib/src/engine/navigation/url_strategy.dart
index 0d49ed6..36b815c 100644
--- a/lib/web_ui/lib/src/engine/navigation/url_strategy.dart
+++ b/lib/web_ui/lib/src/engine/navigation/url_strategy.dart
@@ -35,7 +35,10 @@
@override
ui.VoidCallback addPopStateListener(ui_web.PopStateListener fn) {
- final DomEventListener wrappedFn = createDomEventListener(fn);
+ final DomEventListener wrappedFn = createDomEventListener((DomEvent event) {
+ // `fn` expects `event.state`, not a `DomEvent`.
+ fn((event as DomPopStateEvent).state);
+ });
_platformLocation.addPopStateListener(wrappedFn);
return () => _platformLocation.removePopStateListener(wrappedFn);
}
diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart
index 203ab8b..d63422a 100644
--- a/lib/web_ui/test/engine/history_test.dart
+++ b/lib/web_ui/test/engine/history_test.dart
@@ -7,12 +7,16 @@
library;
import 'dart:async';
+import 'dart:js_interop'
+ show JSExportedDartFunction, JSExportedDartFunctionToFunction;
import 'package:quiver/testing/async.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
-import 'package:ui/src/engine.dart' show DomEventListener, window;
+import 'package:ui/src/engine.dart' show window;
import 'package:ui/src/engine/browser_detection.dart';
+import 'package:ui/src/engine/dom.dart'
+ show DomEvent, DomEventListener, createDomPopStateEvent;
import 'package:ui/src/engine/navigation.dart';
import 'package:ui/src/engine/services.dart';
import 'package:ui/src/engine/test_embedding.dart';
@@ -647,6 +651,31 @@
location.hash = '#';
expect(strategy.getPath(), '/');
});
+
+ test('addPopStateListener fn unwraps DomPopStateEvent state', () {
+ final HashUrlStrategy strategy = HashUrlStrategy(location);
+ const String expected = 'expected value';
+ final List<Object?> states = <Object?>[];
+
+ // Put the popStates received from the `location` in a list
+ strategy.addPopStateListener(states.add);
+
+ // Simulate a popstate with a null state:
+ location.debugTriggerPopState(null);
+
+ expect(states, hasLength(1));
+ expect(states[0], isNull);
+
+ // Simulate a popstate event with `expected` as its 'state'.
+ location.debugTriggerPopState(expected);
+
+ expect(states, hasLength(2));
+ final Object? state = states[1];
+ expect(state, isNotNull);
+ // flutter/flutter#125228
+ expect(state, isNot(isA<DomEvent>()));
+ expect(state, expected);
+ });
});
}
@@ -694,15 +723,32 @@
@override
dynamic state;
+ List<DomEventListener> popStateListeners = <DomEventListener>[];
+
@override
String get pathname => throw UnimplementedError();
@override
String get search => throw UnimplementedError();
+ /// Calls all the registered `popStateListeners` with a 'popstate'
+ /// event with value `state`
+ void debugTriggerPopState(Object? state) {
+ final DomEvent event = createDomPopStateEvent(
+ 'popstate',
+ <Object, Object>{
+ if (state != null) 'state': state,
+ },
+ );
+ for (final DomEventListener listener in popStateListeners) {
+ final Function fn = (listener as JSExportedDartFunction).toDart;
+ fn(event);
+ }
+ }
+
@override
void addPopStateListener(DomEventListener fn) {
- throw UnimplementedError();
+ popStateListeners.add(fn);
}
@override