blob: d63422a88826a3b4a2086ea9469481f20f506a9a [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(mdebbar): https://github.com/flutter/flutter/issues/51169
@TestOn('!safari')
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 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';
import '../common/spy.dart';
Map<String, dynamic> _wrapOriginState(dynamic state) {
return <String, dynamic>{'origin': true, 'state': state};
}
Map<String, dynamic> _tagStateWithSerialCount(dynamic state, int serialCount) {
return <String, dynamic> {
'serialCount': serialCount,
'state': state,
};
}
const Map<String, bool> originState = <String, bool>{'origin': true};
const Map<String, bool> flutterState = <String, bool>{'flutter': true};
const MethodCodec codec = JSONMethodCodec();
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
test('createHistoryForExistingState', () {
TestUrlStrategy strategy;
BrowserHistory history;
// No url strategy.
history = createHistoryForExistingState(null);
expect(history, isA<MultiEntriesBrowserHistory>());
expect(history.urlStrategy, isNull);
// Random history state.
strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(<dynamic, dynamic>{'foo': 123.0}, null, '/'),
);
history = createHistoryForExistingState(strategy);
expect(history, isA<MultiEntriesBrowserHistory>());
expect(history.urlStrategy, strategy);
// Multi-entry history state.
final Map<dynamic, dynamic> state = <dynamic, dynamic>{
'serialCount': 1.0,
'state': <dynamic, dynamic>{'foo': 123.0},
};
strategy = TestUrlStrategy.fromEntry(TestHistoryEntry(state, null, '/'));
history = createHistoryForExistingState(strategy);
expect(history, isA<MultiEntriesBrowserHistory>());
expect(history.urlStrategy, strategy);
// Single-entry history "origin" state.
strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(<dynamic, dynamic>{'origin': true}, null, '/'),
);
history = createHistoryForExistingState(strategy);
expect(history, isA<SingleEntryBrowserHistory>());
expect(history.urlStrategy, strategy);
// Single-entry history "flutter" state.
strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(<dynamic, dynamic>{'flutter': true}, null, '/'),
);
history = createHistoryForExistingState(strategy);
expect(history, isA<SingleEntryBrowserHistory>());
expect(history.urlStrategy, strategy);
});
group('$SingleEntryBrowserHistory', () {
final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() async {
spy.setUp();
});
tearDown(() async {
spy.tearDown();
await window.resetHistory();
});
test('basic setup works', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
// There should be two entries: origin and flutter.
expect(strategy.history, hasLength(2));
// The origin entry is set up but its path should remain unchanged.
final TestHistoryEntry originEntry = strategy.history[0];
expect(originEntry.state, _wrapOriginState('initial state'));
expect(originEntry.url, '/initial');
// The flutter entry is pushed and its path should be derived from the
// origin entry.
final TestHistoryEntry flutterEntry = strategy.history[1];
expect(flutterEntry.state, flutterState);
expect(flutterEntry.url, '/initial');
// The flutter entry is the current entry.
expect(strategy.currentEntry, flutterEntry);
});
test('disposes of its listener without touching history', () async {
const String unwrappedOriginState = 'initial state';
final Map<String, dynamic> wrappedOriginState = _wrapOriginState(unwrappedOriginState);
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(unwrappedOriginState, null, '/initial'),
);
expect(strategy.listeners, isEmpty);
await window.debugInitializeHistory(strategy, useSingle: true);
// There should be one `popstate` listener and two history entries.
expect(strategy.listeners, hasLength(1));
expect(strategy.history, hasLength(2));
expect(strategy.history[0].state, wrappedOriginState);
expect(strategy.history[0].url, '/initial');
expect(strategy.history[1].state, flutterState);
expect(strategy.history[1].url, '/initial');
FakeAsync().run((FakeAsync fakeAsync) {
window.browserHistory.dispose();
// The `TestUrlStrategy` implementation uses microtasks to schedule the
// removal of event listeners.
fakeAsync.flushMicrotasks();
});
// After disposing, there should no listeners, and the history entries
// remain unaffected.
expect(strategy.listeners, isEmpty);
expect(strategy.history, hasLength(2));
expect(strategy.history[0].state, wrappedOriginState);
expect(strategy.history[0].url, '/initial');
expect(strategy.history[1].state, flutterState);
expect(strategy.history[1].url, '/initial');
// An extra call to dispose should be safe.
FakeAsync().run((FakeAsync fakeAsync) {
expect(() => window.browserHistory.dispose(), returnsNormally);
fakeAsync.flushMicrotasks();
});
// Same expectations should remain true after the second dispose.
expect(strategy.listeners, isEmpty);
expect(strategy.history, hasLength(2));
expect(strategy.history[0].state, wrappedOriginState);
expect(strategy.history[0].url, '/initial');
expect(strategy.history[1].state, flutterState);
expect(strategy.history[1].url, '/initial');
// Can still teardown after being disposed.
await window.browserHistory.tearDown();
expect(strategy.history, hasLength(2));
expect(strategy.currentEntry.state, unwrappedOriginState);
expect(strategy.currentEntry.url, '/initial');
});
test('disposes gracefully when url strategy is null', () async {
await window.debugInitializeHistory(null, useSingle: true);
expect(() => window.browserHistory.dispose(), returnsNormally);
});
test('browser back button pops routes correctly', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
// Initially, we should be on the flutter entry.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/home');
await routeUpdated('/page1');
// The number of entries shouldn't change.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
// But the url of the current entry (flutter entry) should be updated.
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/page1');
// No platform messages have been sent so far.
expect(spy.messages, isEmpty);
// Clicking back should take us to page1.
await strategy.go(-1);
// First, the framework should've received a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'popRoute');
expect(spy.messages[0].methodArguments, isNull);
// We still have 2 entries.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
// The url of the current entry (flutter entry) should go back to /home.
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/home');
});
test('multiple browser back clicks', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await routeUpdated('/page1');
await routeUpdated('/page2');
// Make sure we are on page2.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.go(-1);
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'popRoute');
expect(spy.messages[0].methodArguments, isNull);
spy.messages.clear();
// 2. The framework sends a `routePopped` platform message.
await routeUpdated('/page1');
// 3. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/page1');
// Back to home.
await strategy.go(-1);
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'popRoute');
expect(spy.messages[0].methodArguments, isNull);
spy.messages.clear();
// 2. The framework sends a `routePopped` platform message.
await routeUpdated('/home');
// 3. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/home');
// The next browser back will exit the app. We store the strategy locally
// because it will be remove from the browser history class once it exits
// the app.
final TestUrlStrategy originalStrategy = strategy;
await originalStrategy.go(-1);
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'popRoute');
expect(spy.messages[0].methodArguments, isNull);
spy.messages.clear();
// 2. The framework sends a `SystemNavigator.pop` platform message
// because there are no more routes to pop.
await systemNavigatorPop();
// 3. The active entry doesn't belong to our history anymore because we
// navigated past it.
expect(originalStrategy.currentEntryIndex, -1);
}, skip: browserEngine == BrowserEngine.webkit);
test('handle user-provided url', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await strategy.simulateUserTypingUrl('/page3');
// This delay is necessary to wait for [BrowserHistory] because it
// performs a `back` operation which results in a new event loop.
await Future<void>.delayed(Duration.zero);
// 1. The engine sends a `pushRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRoute');
expect(spy.messages[0].methodArguments, '/page3');
spy.messages.clear();
// 2. The framework sends a `routePushed` platform message.
await routeUpdated('/page3');
// 3. The history state should reflect that /page3 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/page3');
// Back to home.
await strategy.go(-1);
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'popRoute');
expect(spy.messages[0].methodArguments, isNull);
spy.messages.clear();
// 2. The framework sends a `routePopped` platform message.
await routeUpdated('/home');
// 3. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/home');
});
test('user types unknown url', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await strategy.simulateUserTypingUrl('/unknown');
// This delay is necessary to wait for [BrowserHistory] because it
// performs a `back` operation which results in a new event loop.
await Future<void>.delayed(Duration.zero);
// 1. The engine sends a `pushRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRoute');
expect(spy.messages[0].methodArguments, '/unknown');
spy.messages.clear();
// 2. The framework doesn't recognize the route name and ignores it.
// 3. The history state should reflect that /home is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/home');
});
});
group('$MultiEntriesBrowserHistory', () {
final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() async {
spy.setUp();
});
tearDown(() async {
spy.tearDown();
await window.resetHistory();
});
test('basic setup works', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
// There should be only one entry.
expect(strategy.history, hasLength(1));
// The origin entry is tagged and its path should remain unchanged.
final TestHistoryEntry taggedOriginEntry = strategy.history[0];
expect(taggedOriginEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(taggedOriginEntry.url, '/initial');
});
test('disposes of its listener without touching history', () async {
const String untaggedState = 'initial state';
final Map<String, dynamic> taggedState = _tagStateWithSerialCount(untaggedState, 0);
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry(untaggedState, null, '/initial'),
);
expect(strategy.listeners, isEmpty);
await window.debugInitializeHistory(strategy, useSingle: false);
// There should be one `popstate` listener and one history entry.
expect(strategy.listeners, hasLength(1));
expect(strategy.history, hasLength(1));
expect(strategy.history.single.state, taggedState);
expect(strategy.history.single.url, '/initial');
FakeAsync().run((FakeAsync fakeAsync) {
window.browserHistory.dispose();
// The `TestUrlStrategy` implementation uses microtasks to schedule the
// removal of event listeners.
fakeAsync.flushMicrotasks();
});
// After disposing, there should no listeners, and the history entries
// remain unaffected.
expect(strategy.listeners, isEmpty);
expect(strategy.history, hasLength(1));
expect(strategy.history.single.state, taggedState);
expect(strategy.history.single.url, '/initial');
// An extra call to dispose should be safe.
FakeAsync().run((FakeAsync fakeAsync) {
expect(() => window.browserHistory.dispose(), returnsNormally);
fakeAsync.flushMicrotasks();
});
// Same expectations should remain true after the second dispose.
expect(strategy.listeners, isEmpty);
expect(strategy.history, hasLength(1));
expect(strategy.history.single.state, taggedState);
expect(strategy.history.single.url, '/initial');
// Can still teardown after being disposed.
await window.browserHistory.tearDown();
expect(strategy.history, hasLength(1));
expect(strategy.history.single.state, untaggedState);
expect(strategy.history.single.url, '/initial');
});
test('disposes gracefully when url strategy is null', () async {
await window.debugInitializeHistory(null, useSingle: false);
expect(() => window.browserHistory.dispose(), returnsNormally);
});
test('browser back button push route information correctly', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
// Initially, we should be on the flutter entry.
expect(strategy.history, hasLength(1));
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(strategy.currentEntry.url, '/home');
await routeInformationUpdated('/page1', 'page1 state');
// Should have two history entries now.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
// But the url of the current entry (flutter entry) should be updated.
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1));
expect(strategy.currentEntry.url, '/page1');
// No platform messages have been sent so far.
expect(spy.messages, isEmpty);
// Clicking back should take us to page1.
await strategy.go(-1);
// First, the framework should've received a `pushRouteInformation`
// platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/home',
'state': 'initial state',
});
// There are still two browser history entries, but we are back to the
// original state.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 0);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(strategy.currentEntry.url, '/home');
});
test('multiple browser back clicks', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await routeInformationUpdated('/page1', 'page1 state');
await routeInformationUpdated('/page2', 'page2 state');
// Make sure we are on page2.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 2);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page2 state', 2));
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.go(-1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/page1',
'state': 'page1 state',
});
spy.messages.clear();
// 2. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1));
expect(strategy.currentEntry.url, '/page1');
// Back to home.
await strategy.go(-1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/home',
'state': 'initial state',
});
spy.messages.clear();
// 2. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 0);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(strategy.currentEntry.url, '/home');
}, skip: browserEngine == BrowserEngine.webkit);
test('handle user-provided url', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await strategy.simulateUserTypingUrl('/page3');
// This delay is necessary to wait for [BrowserHistory] because it
// performs a `back` operation which results in a new event loop.
await Future<void>.delayed(Duration.zero);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/page3',
'state': null,
});
spy.messages.clear();
// 2. The history state should reflect that /page3 is currently active.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, _tagStateWithSerialCount(null, 1));
expect(strategy.currentEntry.url, '/page3');
// Back to home.
await strategy.go(-1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/home',
'state': 'initial state',
});
spy.messages.clear();
// 2. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 0);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(strategy.currentEntry.url, '/home');
});
test('forward button works', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await routeInformationUpdated('/page1', 'page1 state');
await routeInformationUpdated('/page2', 'page2 state');
// Make sure we are on page2.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 2);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page2 state', 2));
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.go(-1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/page1',
'state': 'page1 state',
});
spy.messages.clear();
// 2. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1));
expect(strategy.currentEntry.url, '/page1');
// Forward to page2
await strategy.go(1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/page2',
'state': 'page2 state',
});
spy.messages.clear();
// 2. The history state should reflect that /page2 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 2);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page2 state', 2));
expect(strategy.currentEntry.url, '/page2');
});
});
group('$HashUrlStrategy', () {
late TestPlatformLocation location;
setUp(() {
location = TestPlatformLocation();
});
tearDown(() {
location = TestPlatformLocation();
});
test('leading slash is optional', () {
final HashUrlStrategy strategy = HashUrlStrategy(location);
location.hash = '#/';
expect(strategy.getPath(), '/');
location.hash = '#/foo';
expect(strategy.getPath(), '/foo');
location.hash = '#foo';
expect(strategy.getPath(), 'foo');
});
test('path should not be empty', () {
final HashUrlStrategy strategy = HashUrlStrategy(location);
location.hash = '';
expect(strategy.getPath(), '/');
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);
});
});
}
Future<void> routeUpdated(String routeName) {
final Completer<void> completer = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(MethodCall(
'routeUpdated',
<String, dynamic>{'routeName': routeName},
)),
(_) => completer.complete(),
);
return completer.future;
}
Future<void> routeInformationUpdated(String location, dynamic state) {
final Completer<void> completer = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(MethodCall(
'routeInformationUpdated',
<String, dynamic>{'location': location, 'state': state},
)),
(_) => completer.complete(),
);
return completer.future;
}
Future<void> systemNavigatorPop() {
final Completer<void> completer = Completer<void>();
window.sendPlatformMessage(
'flutter/platform',
codec.encodeMethodCall(const MethodCall('SystemNavigator.pop')),
(_) => completer.complete(),
);
return completer.future;
}
/// A mock implementation of [PlatformLocation] that doesn't access the browser.
class TestPlatformLocation extends PlatformLocation {
@override
String? hash;
@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) {
popStateListeners.add(fn);
}
@override
void removePopStateListener(DomEventListener fn) {
throw UnimplementedError();
}
@override
void pushState(dynamic state, String title, String url) {
throw UnimplementedError();
}
@override
void replaceState(dynamic state, String title, String url) {
throw UnimplementedError();
}
@override
void go(double count) {
throw UnimplementedError();
}
@override
String getBaseHref() => '/';
}