| // 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() => '/'; |
| } |