blob: 775fa994f68877d51732732ba851d63a4e97e369 [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.
import 'dart:async';
import 'dart:js_util' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' hide window;
import 'package:ui/ui.dart' as ui;
import 'engine/history_test.dart';
import 'matchers.dart';
const MethodCodec codec = JSONMethodCodec();
Map<String, dynamic> _tagStateWithSerialCount(dynamic state, int serialCount) {
return <String, dynamic> {
'serialCount': serialCount,
'state': state,
};
}
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
late EngineSingletonFlutterWindow window;
setUpAll(() async {
await initializeEngine();
});
setUp(() {
window = EngineSingletonFlutterWindow(0, EnginePlatformDispatcher.instance);
});
tearDown(() async {
await window.resetHistory();
});
test('window.defaultRouteName should work with JsUrlStrategy', () async {
dynamic state = <dynamic, dynamic>{};
final JsUrlStrategy jsUrlStrategy = JsUrlStrategy(
getPath: allowInterop(() => '/initial'),
getState: allowInterop(() => state),
addPopStateListener: allowInterop((DomEventListener listener) => () {}),
prepareExternalUrl: allowInterop((String value) => ''),
pushState: allowInterop((Object? newState, String title, String url) {
expect(newState is Map, true);
}),
replaceState: allowInterop((Object? newState, String title, String url) {
expect(newState is Map, true);
state = newState;
}),
go: allowInterop(([int? delta]) async {
expect(delta, -1);
}));
final CustomUrlStrategy strategy =
CustomUrlStrategy.fromJs(jsUrlStrategy);
await window.debugInitializeHistory(strategy, useSingle: true);
expect(window.defaultRouteName, '/initial');
});
test('window.defaultRouteName should not change', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
expect(window.defaultRouteName, '/initial');
// Changing the URL in the address bar later shouldn't affect [window.defaultRouteName].
strategy.replaceState(null, '', '/newpath');
expect(window.defaultRouteName, '/initial');
});
// window.defaultRouteName is now permanently decoupled from the history,
// even in subsequent tests, because the PlatformDispatcher caches it.
test('window.defaultRouteName should reset after navigation platform message',
() async {
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
// The URL here does not set the PlatformDispatcher's defaultRouteName,
// since it got cached as soon as we read it above.
const TestHistoryEntry('initial state', null, '/not-really-inital/THIS_IS_IGNORED'),
), useSingle: true);
// Reading it multiple times should return the same value.
expect(window.defaultRouteName, '/initial');
expect(window.defaultRouteName, '/initial');
final Completer<void> callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeUpdated',
<String, dynamic>{'routeName': '/bar'},
)),
(_) { callback.complete(); },
);
await callback.future;
// After a navigation platform message, the PlatformDispatcher's
// defaultRouteName resets to "/".
expect(window.defaultRouteName, '/');
});
// window.defaultRouteName is now '/'.
test('can switch history mode', () async {
Completer<void> callback;
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/initial'),
), useSingle: false);
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
Future<void> check<T>(String method, Object? arguments) async {
callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(MethodCall(method, arguments)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory, isA<T>());
}
// These may be initialized as `null`
// See https://github.com/flutter/flutter/issues/83158#issuecomment-847483010
await check<SingleEntryBrowserHistory>('selectSingleEntryHistory', null); // -> single
await check<MultiEntriesBrowserHistory>('selectMultiEntryHistory', null); // -> multi
await check<SingleEntryBrowserHistory>('selectSingleEntryHistory', <String, dynamic>{}); // -> single
await check<MultiEntriesBrowserHistory>('selectMultiEntryHistory', <String, dynamic>{}); // -> multi
await check<SingleEntryBrowserHistory>('routeUpdated', <String, dynamic>{'routeName': '/bar'}); // -> single
await check<SingleEntryBrowserHistory>('routeInformationUpdated', <String, dynamic>{'location': '/bar'}); // does not change mode
await check<MultiEntriesBrowserHistory>('selectMultiEntryHistory', <String, dynamic>{}); // -> multi
await check<MultiEntriesBrowserHistory>('routeInformationUpdated', <String, dynamic>{'location': '/bar'}); // does not change mode
});
test('handleNavigationMessage throws for route update methods called with null arguments',
() async {
expect(() async {
await window.handleNavigationMessage(
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeUpdated',
))
);
}, throwsAssertionError);
expect(() async {
await window.handleNavigationMessage(
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeInformationUpdated',
))
);
}, throwsAssertionError);
});
test('handleNavigationMessage execute request in order.', () async {
// Start with multi entries.
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/initial'),
), useSingle: false);
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
final List<String> executionOrder = <String>[];
await window.handleNavigationMessage(
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'selectSingleEntryHistory',
))
).then<void>((bool data) {
executionOrder.add('1');
});
await window.handleNavigationMessage(
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'selectMultiEntryHistory',
))
).then<void>((bool data) {
executionOrder.add('2');
});
await window.handleNavigationMessage(
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'selectSingleEntryHistory',
))
).then<void>((bool data) {
executionOrder.add('3');
});
await window.handleNavigationMessage(
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeInformationUpdated',
<String, dynamic>{
'location': '/baz',
'state': null,
}, // boom
))
).then<void>((bool data) {
executionOrder.add('4');
});
// The routeInformationUpdated should finish after the browser history
// has been set to single entry.
expect(executionOrder.length, 4);
expect(executionOrder[0], '1');
expect(executionOrder[1], '2');
expect(executionOrder[2], '3');
expect(executionOrder[3], '4');
});
test('should not throw when using nav1 and nav2 together',
() async {
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/initial'),
), useSingle: false);
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
// routeUpdated resets the history type
Completer<void> callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeUpdated',
<String, dynamic>{'routeName': '/bar'},
)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory, isA<SingleEntryBrowserHistory>());
expect(window.browserHistory.urlStrategy!.getPath(), '/bar');
// routeInformationUpdated does not
callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeInformationUpdated',
<String, dynamic>{
'location': '/baz',
'state': null,
},
)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory, isA<SingleEntryBrowserHistory>());
expect(window.browserHistory.urlStrategy!.getPath(), '/baz');
// they can be interleaved safely
await window.handleNavigationMessage(
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeUpdated',
<String, dynamic>{'routeName': '/foo'},
))
);
expect(window.browserHistory, isA<SingleEntryBrowserHistory>());
expect(window.browserHistory.urlStrategy!.getPath(), '/foo');
});
test('should not throw when state is complex json object',
() async {
// Regression test https://github.com/flutter/flutter/issues/87823.
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/initial'),
), useSingle: false);
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
// routeInformationUpdated does not
final Completer<void> callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeInformationUpdated',
<String, dynamic>{
'location': '/baz',
'state': <String, dynamic>{
'state1': true,
'state2': 1,
'state3': 'string',
'state4': <String, dynamic> {
'substate1': 1.0,
'substate2': 'string2',
}
},
},
)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
expect(window.browserHistory.urlStrategy!.getPath(), '/baz');
final dynamic wrappedState = window.browserHistory.urlStrategy!.getState();
final dynamic actualState = wrappedState['state'];
expect(actualState['state1'], true);
expect(actualState['state2'], 1);
expect(actualState['state3'], 'string');
expect(actualState['state4']['substate1'], 1.0);
expect(actualState['state4']['substate2'], 'string2');
});
test('can replace in MultiEntriesBrowserHistory',
() async {
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/initial'),
), useSingle: false);
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
Completer<void> callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeInformationUpdated',
<String, dynamic>{
'location': '/baz',
'state': '/state',
},
)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory.urlStrategy!.getPath(), '/baz');
expect(window.browserHistory.urlStrategy!.getState(), _tagStateWithSerialCount('/state', 1));
callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeInformationUpdated',
<String, dynamic>{
'location': '/baz',
'state': '/state1',
'replace': true
},
)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory.urlStrategy!.getPath(), '/baz');
expect(window.browserHistory.urlStrategy!.getState(), _tagStateWithSerialCount('/state1', 1));
callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeInformationUpdated',
<String, dynamic>{
'location': '/foo',
'state': '/foostate1',
},
)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory.urlStrategy!.getPath(), '/foo');
expect(window.browserHistory.urlStrategy!.getState(), _tagStateWithSerialCount('/foostate1', 2));
await window.browserHistory.back();
expect(window.browserHistory.urlStrategy!.getPath(), '/baz');
expect(window.browserHistory.urlStrategy!.getState(), _tagStateWithSerialCount('/state1', 1));
});
test('initialize browser history with default url strategy (single)', () async {
// On purpose, we don't initialize history on the window. We want to let the
// window to self-initialize when it receives a navigation message.
// Without initializing history, the default route name should be
// initialized to "/" in tests.
expect(window.defaultRouteName, '/');
final Completer<void> callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeUpdated',
<String, dynamic>{'routeName': '/bar'},
)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory, isA<SingleEntryBrowserHistory>());
// The url strategy should've been set to the default, and the path
// should've been correctly set to "/bar".
expect(window.browserHistory.urlStrategy, isNot(isNull));
expect(window.browserHistory.urlStrategy!.getPath(), '/bar');
}, skip: isSafari); // https://github.com/flutter/flutter/issues/50836
test('initialize browser history with default url strategy (multiple)', () async {
// On purpose, we don't initialize history on the window. We want to let the
// window to self-initialize when it receives a navigation message.
// Without initializing history, the default route name should be
// initialized to "/" in tests.
expect(window.defaultRouteName, '/');
final Completer<void> callback = Completer<void>();
window.sendPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(const MethodCall(
'routeInformationUpdated',
<String, dynamic>{
'location': '/baz',
'state': null,
},
)),
(_) { callback.complete(); },
);
await callback.future;
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
// The url strategy should've been set to the default, and the path
// should've been correctly set to "/baz".
expect(window.browserHistory.urlStrategy, isNot(isNull));
expect(window.browserHistory.urlStrategy!.getPath(), '/baz');
}, skip: isSafari); // https://github.com/flutter/flutter/issues/50836
test('can disable location strategy', () async {
// Disable URL strategy.
expect(() => jsSetUrlStrategy(null), returnsNormally);
// History should be initialized.
expect(window.browserHistory, isNotNull);
// But without a URL strategy.
expect(window.browserHistory.urlStrategy, isNull);
// Current path is always "/" in this case.
expect(window.browserHistory.currentPath, '/');
// Perform some navigation operations.
await routeInformationUpdated('/foo/bar', null);
// Path should not be updated because URL strategy is disabled.
expect(window.browserHistory.currentPath, '/');
});
test('js interop throws on wrong type', () {
expect(() => jsSetUrlStrategy(123), throwsA(anything));
expect(() => jsSetUrlStrategy('foo'), throwsA(anything));
expect(() => jsSetUrlStrategy(false), throwsA(anything));
});
test('cannot set url strategy after it is initialized', () async {
final TestUrlStrategy testStrategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/'),
);
await window.debugInitializeHistory(testStrategy, useSingle: true);
expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError));
});
test('cannot set url strategy more than once', () async {
// First time is okay.
expect(() => jsSetUrlStrategy(null), returnsNormally);
// Second time is not allowed.
expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError));
});
// Regression test for https://github.com/flutter/flutter/issues/77817
test('window.locale(s) are not nullable', () {
// If the getters were nullable, these expressions would result in compiler errors.
ui.window.locale.countryCode;
ui.window.locales.first.countryCode;
});
}
void jsSetUrlStrategy(dynamic strategy) {
js_util.callMethod<void>(
domWindow,
'_flutter_web_set_location_strategy',
<dynamic>[strategy],
);
}