1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:js_interop';
7import 'dart:ui' as ui;
8import 'dart:ui_web';
9
10import 'package:flutter/material.dart';
11import 'package:flutter_test/flutter_test.dart';
12import 'package:flutter_web_plugins/flutter_web_plugins.dart';
13import 'package:integration_test/integration_test.dart';
14import 'package:web/helpers.dart';
15import 'package:web/web.dart' as web;
16import 'package:web_e2e_tests/url_strategy_main.dart' as app;
17
18void main() {
19 IntegrationTestWidgetsFlutterBinding.ensureInitialized();
20
21 testWidgets('Can customize url strategy', (WidgetTester tester) async {
22 final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
23 const TestHistoryEntry('initial state', null, '/'),
24 );
25 setUrlStrategy(strategy);
26
27 app.appRoutes = <String, WidgetBuilder>{
28 '/': (BuildContext context) => Container(),
29 '/foo': (BuildContext context) => Container(),
30 };
31 app.main();
32 await tester.pumpAndSettle();
33
34 // checking whether the previously set strategy is properly preserved
35 expect(urlStrategy, strategy);
36
37 expect(strategy.getPath(), '/');
38
39 final NavigatorState navigator = app.navKey.currentState!;
40 navigator.pushNamed('/foo');
41 await tester.pump();
42 expect(strategy.getPath(), '/foo');
43 });
44}
45
46/// This URL strategy mimics the browser's history as closely as possible
47/// while doing it all in memory with no interaction with the browser.
48///
49/// It keeps a list of history entries and event listeners in memory and
50/// manipulates them in order to achieve the desired functionality.
51class TestUrlStrategy extends UrlStrategy {
52 /// Creates an instance of [TestUrlStrategy] and populates it with a list
53 /// that has [initialEntry] as the only item.
54 TestUrlStrategy.fromEntry(TestHistoryEntry initialEntry)
55 : _currentEntryIndex = 0,
56 history = <TestHistoryEntry>[initialEntry];
57
58 @override
59 String getPath() => currentEntry.url;
60
61 @override
62 Object? getState() => currentEntry.state;
63
64 int _currentEntryIndex;
65 final List<TestHistoryEntry> history;
66
67 TestHistoryEntry get currentEntry {
68 assert(withinAppHistory);
69 return history[_currentEntryIndex];
70 }
71
72 set currentEntry(TestHistoryEntry entry) {
73 assert(withinAppHistory);
74 history[_currentEntryIndex] = entry;
75 }
76
77 /// Whether we are still within the history of the Flutter Web app. This
78 /// remains true until we go back in history beyond the entry where the app
79 /// started.
80 bool get withinAppHistory => _currentEntryIndex >= 0;
81
82 @override
83 void pushState(dynamic state, String title, String url) {
84 assert(withinAppHistory);
85 _currentEntryIndex++;
86 // When pushing a new state, we need to remove all entries that exist after
87 // the current entry.
88 //
89 // If the user goes A -> B -> C -> D, then goes back to B and pushes a new
90 // entry called E, we should end up with: A -> B -> E in the history list.
91 history.removeRange(_currentEntryIndex, history.length);
92 history.add(TestHistoryEntry(state, title, url));
93 }
94
95 @override
96 void replaceState(dynamic state, String title, String url) {
97 assert(withinAppHistory);
98 if (url == '') {
99 url = currentEntry.url;
100 }
101 currentEntry = TestHistoryEntry(state, title, url);
102 }
103
104 @override
105 Future<void> go(int count) {
106 assert(withinAppHistory);
107 // Browsers don't move in history immediately. They do it at the next
108 // event loop. So let's simulate that.
109 return _nextEventLoop(() {
110 _currentEntryIndex = _currentEntryIndex + count;
111 if (withinAppHistory) {
112 _firePopStateEvent();
113 }
114 });
115 }
116
117 final List<PopStateListener> listeners = <PopStateListener>[];
118
119 @override
120 ui.VoidCallback addPopStateListener(PopStateListener fn) {
121 listeners.add(fn);
122 return () {
123 // Schedule a micro task here to avoid removing the listener during
124 // iteration in [_firePopStateEvent].
125 scheduleMicrotask(() => listeners.remove(fn));
126 };
127 }
128
129 /// Simulates the scheduling of a new event loop by creating a delayed future.
130 /// Details explained here: https://webdev.dartlang.org/articles/performance/event-loop
131 Future<void> _nextEventLoop(ui.VoidCallback callback) {
132 return Future<void>.delayed(Duration.zero).then((_) => callback());
133 }
134
135 /// Invokes all the attached event listeners in order of
136 /// attaching. This method should be called asynchronously to make it behave
137 /// like a real browser.
138 void _firePopStateEvent() {
139 assert(withinAppHistory);
140 final web.PopStateEvent event = web.PopStateEvent(
141 'popstate',
142 PopStateEventInit(state: currentEntry.state?.toJSBox),
143 );
144 for (int i = 0; i < listeners.length; i++) {
145 listeners[i](event);
146 }
147 }
148
149 @override
150 String prepareExternalUrl(String internalUrl) => internalUrl;
151
152 @override
153 String toString() {
154 final List<String> lines = <String>[];
155 for (int i = 0; i < history.length; i++) {
156 final TestHistoryEntry entry = history[i];
157 lines.add(_currentEntryIndex == i ? '* $entry' : ' $entry');
158 }
159 return '$runtimeType: [\n${lines.join('\n')}\n]';
160 }
161}
162
163class TestHistoryEntry {
164 const TestHistoryEntry(this.state, this.title, this.url);
165
166 final Object? state;
167 final String? title;
168 final String url;
169
170 @override
171 String toString() {
172 return '$runtimeType(state:$state, title:"$title", url:"$url")';
173 }
174}
175

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com