blob: ef03789c869d8b4da66f175fbee86f08167e3ea1 [file] [log] [blame]
// Copyright 2014 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:html' as html;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:integration_test/integration_test.dart';
import 'package:web_e2e_tests/url_strategy_main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Can customize url strategy', (WidgetTester tester) async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/'),
);
setUrlStrategy(strategy);
app.appRoutes = <String, WidgetBuilder>{
'/': (BuildContext context) => Container(),
'/foo': (BuildContext context) => Container(),
};
app.main();
await tester.pumpAndSettle();
// checking whether the previously set strategy is properly preserved
expect(urlStrategy, strategy);
expect(strategy.getPath(), '/');
final NavigatorState navigator = app.navKey.currentState!;
navigator.pushNamed('/foo');
await tester.pump();
expect(strategy.getPath(), '/foo');
});
}
/// This URL strategy mimics the browser's history as closely as possible
/// while doing it all in memory with no interaction with the browser.
///
/// It keeps a list of history entries and event listeners in memory and
/// manipulates them in order to achieve the desired functionality.
class TestUrlStrategy extends UrlStrategy {
/// Creates a instance of [TestUrlStrategy] with an empty string as the
/// path.
factory TestUrlStrategy() =>
TestUrlStrategy.fromEntry(const TestHistoryEntry(null, null, ''));
/// Creates an instance of [TestUrlStrategy] and populates it with a list
/// that has [initialEntry] as the only item.
TestUrlStrategy.fromEntry(TestHistoryEntry initialEntry)
: _currentEntryIndex = 0,
history = <TestHistoryEntry>[initialEntry];
@override
String getPath() => currentEntry.url;
@override
dynamic getState() => currentEntry.state;
int _currentEntryIndex;
int get currentEntryIndex => _currentEntryIndex;
final List<TestHistoryEntry> history;
TestHistoryEntry get currentEntry {
assert(withinAppHistory);
return history[_currentEntryIndex];
}
set currentEntry(TestHistoryEntry entry) {
assert(withinAppHistory);
history[_currentEntryIndex] = entry;
}
/// Whether we are still within the history of the Flutter Web app. This
/// remains true until we go back in history beyond the entry where the app
/// started.
bool get withinAppHistory => _currentEntryIndex >= 0;
@override
void pushState(dynamic state, String title, String url) {
assert(withinAppHistory);
_currentEntryIndex++;
// When pushing a new state, we need to remove all entries that exist after
// the current entry.
//
// 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(TestHistoryEntry(state, title, url));
}
@override
void replaceState(dynamic state, String title, String url) {
assert(withinAppHistory);
if (url == null || url == '') {
url = currentEntry.url;
}
currentEntry = TestHistoryEntry(state, title, url);
}
/// This simulates the case where a user types in a url manually. It causes
/// a new state to be pushed, and all event listeners will be invoked.
Future<void> simulateUserTypingUrl(String url) {
assert(withinAppHistory);
return _nextEventLoop(() {
pushState(null, '', url);
_firePopStateEvent();
});
}
@override
Future<void> go(int count) {
assert(withinAppHistory);
// Browsers don't move in history immediately. They do it at the next
// event loop. So let's simulate that.
return _nextEventLoop(() {
_currentEntryIndex = _currentEntryIndex + count;
if (withinAppHistory) {
_firePopStateEvent();
}
});
}
final List<html.EventListener> listeners = <html.EventListener>[];
@override
ui.VoidCallback addPopStateListener(html.EventListener fn) {
listeners.add(fn);
return () {
// Schedule a micro task here to avoid removing the listener during
// iteration in [_firePopStateEvent].
scheduleMicrotask(() => listeners.remove(fn));
};
}
/// Simulates the scheduling of a new event loop by creating a delayed future.
/// Details explained here: https://webdev.dartlang.org/articles/performance/event-loop
Future<void> _nextEventLoop(ui.VoidCallback callback) {
return Future<void>.delayed(Duration.zero).then((_) => callback());
}
/// Invokes all the attached event listeners in order of
/// attaching. This method should be called asynchronously to make it behave
/// like a real browser.
void _firePopStateEvent() {
assert(withinAppHistory);
final html.PopStateEvent event = html.PopStateEvent(
'popstate',
<String, dynamic>{'state': currentEntry.state},
);
for (int i = 0; i < listeners.length; i++) {
listeners[i](event);
}
}
@override
String prepareExternalUrl(String internalUrl) => internalUrl;
@override
String toString() {
final List<String> lines = <String>[];
for (int i = 0; i < history.length; i++) {
final TestHistoryEntry entry = history[i];
lines.add(_currentEntryIndex == i ? '* $entry' : ' $entry');
}
return '$runtimeType: [\n${lines.join('\n')}\n]';
}
}
class TestHistoryEntry {
const TestHistoryEntry(this.state, this.title, this.url);
final dynamic state;
final String? title;
final String url;
@override
String toString() {
return '$runtimeType(state:$state, title:"$title", url:"$url")';
}
}