提交 6162c41d 编写于 作者: M Mouad Debbar 提交者: Flutter GitHub Bot

[web] Tests for browser history implementation (#15324)

上级 3a1a3ba6
......@@ -6,12 +6,12 @@ part of engine;
const bool _debugLogHistoryActions = false;
class _HistoryEntry {
class TestHistoryEntry {
final dynamic state;
final String title;
final String url;
const _HistoryEntry(this.state, this.title, this.url);
const TestHistoryEntry(this.state, this.title, this.url);
@override
String toString() {
......@@ -25,24 +25,30 @@ class _HistoryEntry {
/// It keeps a list of history entries and event listeners in memory and
/// manipulates them in order to achieve the desired functionality.
class TestLocationStrategy extends LocationStrategy {
/// Passing a [defaultRouteName] will make the app start at that route. The
/// way it does it is by using it as a path on the first history entry.
TestLocationStrategy([String defaultRouteName = ''])
/// Creates a instance of [TestLocationStrategy] with an empty string as the
/// path.
factory TestLocationStrategy() => TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, ''));
/// Creates an instance of [TestLocationStrategy] and populates it with a list
/// that has [initialEntry] as the only item.
TestLocationStrategy.fromEntry(TestHistoryEntry initialEntry)
: _currentEntryIndex = 0,
history = <_HistoryEntry>[_HistoryEntry(null, null, defaultRouteName)];
history = <TestHistoryEntry>[initialEntry];
@override
String get path => ensureLeading(currentEntry.url, '/');
int _currentEntryIndex;
final List<_HistoryEntry> history;
int get currentEntryIndex => _currentEntryIndex;
final List<TestHistoryEntry> history;
_HistoryEntry get currentEntry {
TestHistoryEntry get currentEntry {
assert(withinAppHistory);
return history[_currentEntryIndex];
}
set currentEntry(_HistoryEntry entry) {
set currentEntry(TestHistoryEntry entry) {
assert(withinAppHistory);
history[_currentEntryIndex] = entry;
}
......@@ -62,7 +68,7 @@ class TestLocationStrategy extends LocationStrategy {
// If the user goes A -> B -> C -> D, then goes back to B and pushes a new
// entry called E, we should end up with: A -> B -> E in the history list.
history.removeRange(_currentEntryIndex, history.length);
history.add(_HistoryEntry(state, title, url));
history.add(TestHistoryEntry(state, title, url));
if (_debugLogHistoryActions) {
print('$runtimeType.pushState(...) -> $this');
......@@ -72,7 +78,10 @@ class TestLocationStrategy extends LocationStrategy {
@override
void replaceState(dynamic state, String title, String url) {
assert(withinAppHistory);
currentEntry = _HistoryEntry(state, title, url);
if (url == null || url == '') {
url = currentEntry.url;
}
currentEntry = TestHistoryEntry(state, title, url);
if (_debugLogHistoryActions) {
print('$runtimeType.replaceState(...) -> $this');
......@@ -149,7 +158,7 @@ class TestLocationStrategy extends LocationStrategy {
String toString() {
final List<String> lines = List<String>(history.length);
for (int i = 0; i < history.length; i++) {
final _HistoryEntry entry = history[i];
final TestHistoryEntry entry = history[i];
lines[i] = _currentEntryIndex == i ? '* $entry' : ' $entry';
}
return '$runtimeType: [\n${lines.join('\n')}\n]';
......
// 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:typed_data';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../spy.dart';
TestLocationStrategy _strategy;
TestLocationStrategy get strategy => _strategy;
set strategy(TestLocationStrategy newStrategy) {
window.locationStrategy = _strategy = newStrategy;
}
const Map<String, bool> originState = <String, bool>{'origin': true};
const Map<String, bool> flutterState = <String, bool>{'flutter': true};
const MethodCodec codec = JSONMethodCodec();
void emptyCallback(ByteData date) {}
void main() {
group('BrowserHistory', () {
final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() {
spy.setUp();
});
tearDown(() {
spy.tearDown();
strategy = null;
});
test('basic setup works', () {
strategy = TestLocationStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'));
// There should be two entries: origin and flutter.
expect(strategy.history, hasLength(2));
// The origin entry is setup but its path should remain unchanged.
final TestHistoryEntry originEntry = strategy.history[0];
expect(originEntry.state, originState);
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('browser back button pops routes correctly', () async {
strategy =
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
// Initially, we should be on the flutter entry.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/home');
pushRoute('/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.back();
// 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 {
strategy =
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
pushRoute('/page1');
pushRoute('/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.back();
// 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.
popRoute('/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.back();
// 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.
popRoute('/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.
await strategy.back();
// 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(strategy.currentEntryIndex, -1);
});
test('handle user-provided url', () async {
strategy =
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
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.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.
pushRoute('/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.back();
// 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.
popRoute('/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 {
strategy =
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
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.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');
});
});
}
void pushRoute(String routeName) {
window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(MethodCall(
'routePushed',
<String, dynamic>{'previousRouteName': '/foo', 'routeName': routeName},
)),
emptyCallback,
);
}
void replaceRoute(String routeName) {
window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(MethodCall(
'routeReplaced',
<String, dynamic>{'previousRouteName': '/foo', 'routeName': routeName},
)),
emptyCallback,
);
}
void popRoute(String previousRouteName) {
window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(MethodCall(
'routePopped',
<String, dynamic>{
'previousRouteName': previousRouteName,
'routeName': '/foo'
},
)),
emptyCallback,
);
}
Future<void> systemNavigatorPop() {
final Completer<void> completer = Completer<void>();
window.sendPlatformMessage(
'flutter/platform',
codec.encodeMethodCall(MethodCall('SystemNavigator.pop')),
(_) => completer.complete(),
);
return completer.future;
}
// 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:typed_data';
import 'package:ui/src/engine.dart' hide window;
import 'package:ui/ui.dart';
/// Encapsulates the info of a platform message that was intercepted by
/// [PlatformMessagesSpy].
class PlatformMessage {
PlatformMessage(this.channel, this.methodCall);
/// The name of the channel on which the message was sent.
final String channel;
/// The [MethodCall] instance that was sent in the platform message.
final MethodCall methodCall;
/// Shorthand for getting the name of the method call.
String get methodName => methodCall.method;
/// Shorthand for getting the arguments of the method call.
String get methodArguments => methodCall.arguments;
}
/// Intercepts platform messages sent from the engine to the framework.
///
/// It holds all intercepted platform messages in a [messages] list that can
/// be inspected in tests.
class PlatformMessagesSpy {
PlatformMessageCallback _callback;
PlatformMessageCallback _backup;
bool get _isActive => _callback != null;
/// List of intercepted messages since the last [setUp] call.
final List<PlatformMessage> messages = <PlatformMessage>[];
/// Start spying on platform messages.
///
/// This is typically called inside a test's `setUp` callback.
void setUp() {
assert(!_isActive);
_callback = (String channel, ByteData data,
PlatformMessageResponseCallback callback) {
messages.add(PlatformMessage(
channel,
const JSONMethodCodec().decodeMethodCall(data),
));
};
_backup = window.onPlatformMessage;
window.onPlatformMessage = _callback;
}
/// Stop spying on platform messages and clear all intercepted messages.
///
/// Make sure this is called after each test that uses [PlatformMessagesSpy].
void tearDown() {
assert(_isActive);
// Make sure [window.onPlatformMessage] wasn't tampered with.
assert(window.onPlatformMessage == _callback);
_callback = null;
messages.clear();
window.onPlatformMessage = _backup;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册