[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