@angular/common 17.2.0-next.0 → 17.2.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/esm2022/http/public_api.mjs +6 -6
  2. package/esm2022/http/src/client.mjs +6 -6
  3. package/esm2022/http/src/fetch.mjs +12 -11
  4. package/esm2022/http/src/headers.mjs +7 -9
  5. package/esm2022/http/src/interceptor.mjs +11 -10
  6. package/esm2022/http/src/jsonp.mjs +11 -13
  7. package/esm2022/http/src/module.mjs +18 -28
  8. package/esm2022/http/src/params.mjs +15 -14
  9. package/esm2022/http/src/private_export.mjs +2 -2
  10. package/esm2022/http/src/provider.mjs +13 -13
  11. package/esm2022/http/src/request.mjs +19 -21
  12. package/esm2022/http/src/response.mjs +3 -3
  13. package/esm2022/http/src/transfer_cache.mjs +15 -11
  14. package/esm2022/http/src/xhr.mjs +11 -10
  15. package/esm2022/http/src/xsrf.mjs +17 -14
  16. package/esm2022/http/testing/src/api.mjs +1 -1
  17. package/esm2022/http/testing/src/backend.mjs +11 -11
  18. package/esm2022/http/testing/src/module.mjs +7 -13
  19. package/esm2022/http/testing/src/provider.mjs +1 -1
  20. package/esm2022/http/testing/src/request.mjs +11 -8
  21. package/esm2022/src/common.mjs +6 -6
  22. package/esm2022/src/common_module.mjs +5 -5
  23. package/esm2022/src/cookie.mjs +1 -1
  24. package/esm2022/src/directives/index.mjs +1 -1
  25. package/esm2022/src/directives/ng_class.mjs +6 -6
  26. package/esm2022/src/directives/ng_component_outlet.mjs +11 -13
  27. package/esm2022/src/directives/ng_for_of.mjs +5 -5
  28. package/esm2022/src/directives/ng_if.mjs +7 -9
  29. package/esm2022/src/directives/ng_optimized_image/asserts.mjs +1 -1
  30. package/esm2022/src/directives/ng_optimized_image/error_helper.mjs +4 -2
  31. package/esm2022/src/directives/ng_optimized_image/image_loaders/cloudflare_loader.mjs +1 -1
  32. package/esm2022/src/directives/ng_optimized_image/image_loaders/cloudinary_loader.mjs +9 -8
  33. package/esm2022/src/directives/ng_optimized_image/image_loaders/image_loader.mjs +2 -2
  34. package/esm2022/src/directives/ng_optimized_image/image_loaders/imagekit_loader.mjs +1 -1
  35. package/esm2022/src/directives/ng_optimized_image/image_loaders/imgix_loader.mjs +2 -2
  36. package/esm2022/src/directives/ng_optimized_image/index.mjs +1 -1
  37. package/esm2022/src/directives/ng_optimized_image/lcp_image_observer.mjs +6 -6
  38. package/esm2022/src/directives/ng_optimized_image/ng_optimized_image.mjs +160 -28
  39. package/esm2022/src/directives/ng_optimized_image/preconnect_link_checker.mjs +12 -14
  40. package/esm2022/src/directives/ng_optimized_image/preload-link-creator.mjs +4 -4
  41. package/esm2022/src/directives/ng_optimized_image/tokens.mjs +5 -2
  42. package/esm2022/src/directives/ng_optimized_image/url.mjs +2 -2
  43. package/esm2022/src/directives/ng_plural.mjs +7 -7
  44. package/esm2022/src/directives/ng_style.mjs +5 -5
  45. package/esm2022/src/directives/ng_switch.mjs +17 -15
  46. package/esm2022/src/directives/ng_switch_equality.mjs +1 -1
  47. package/esm2022/src/directives/ng_template_outlet.mjs +5 -5
  48. package/esm2022/src/dom_adapter.mjs +2 -4
  49. package/esm2022/src/dom_tokens.mjs +2 -2
  50. package/esm2022/src/errors.mjs +1 -1
  51. package/esm2022/src/i18n/format_date.mjs +62 -43
  52. package/esm2022/src/i18n/format_number.mjs +14 -13
  53. package/esm2022/src/i18n/locale_data.mjs +1 -1
  54. package/esm2022/src/i18n/locale_data_api.mjs +14 -8
  55. package/esm2022/src/i18n/localization.mjs +7 -7
  56. package/esm2022/src/location/hash_location_strategy.mjs +6 -8
  57. package/esm2022/src/location/index.mjs +2 -2
  58. package/esm2022/src/location/location.mjs +9 -11
  59. package/esm2022/src/location/location_strategy.mjs +14 -11
  60. package/esm2022/src/location/platform_location.mjs +9 -9
  61. package/esm2022/src/location/util.mjs +2 -2
  62. package/esm2022/src/navigation/navigation_types.mjs +9 -0
  63. package/esm2022/src/navigation/platform_navigation.mjs +4 -4
  64. package/esm2022/src/pipes/async_pipe.mjs +7 -7
  65. package/esm2022/src/pipes/case_conversion_pipes.mjs +11 -11
  66. package/esm2022/src/pipes/date_pipe.mjs +70 -71
  67. package/esm2022/src/pipes/i18n_plural_pipe.mjs +4 -5
  68. package/esm2022/src/pipes/i18n_select_pipe.mjs +4 -5
  69. package/esm2022/src/pipes/index.mjs +1 -1
  70. package/esm2022/src/pipes/invalid_pipe_argument_error.mjs +1 -1
  71. package/esm2022/src/pipes/json_pipe.mjs +3 -3
  72. package/esm2022/src/pipes/keyvalue_pipe.mjs +7 -9
  73. package/esm2022/src/pipes/number_pipe.mjs +13 -13
  74. package/esm2022/src/pipes/slice_pipe.mjs +4 -4
  75. package/esm2022/src/private_export.mjs +3 -2
  76. package/esm2022/src/version.mjs +1 -1
  77. package/esm2022/src/viewport_scroller.mjs +6 -5
  78. package/esm2022/testing/src/location_mock.mjs +34 -19
  79. package/esm2022/testing/src/mock_location_strategy.mjs +6 -6
  80. package/esm2022/testing/src/mock_platform_location.mjs +112 -13
  81. package/esm2022/testing/src/navigation/fake_navigation.mjs +11 -11
  82. package/esm2022/testing/src/navigation/navigation_types.mjs +9 -0
  83. package/esm2022/testing/src/navigation/provide_fake_platform_navigation.mjs +8 -5
  84. package/esm2022/testing/src/private_export.mjs +9 -0
  85. package/esm2022/testing/src/testing.mjs +3 -2
  86. package/esm2022/upgrade/src/index.mjs +2 -2
  87. package/esm2022/upgrade/src/location_shim.mjs +11 -13
  88. package/esm2022/upgrade/src/location_upgrade_module.mjs +15 -18
  89. package/esm2022/upgrade/src/params.mjs +6 -6
  90. package/esm2022/upgrade/src/utils.mjs +2 -2
  91. package/fesm2022/common.mjs +506 -340
  92. package/fesm2022/common.mjs.map +1 -1
  93. package/fesm2022/http/testing.mjs +26 -29
  94. package/fesm2022/http/testing.mjs.map +1 -1
  95. package/fesm2022/http.mjs +133 -138
  96. package/fesm2022/http.mjs.map +1 -1
  97. package/fesm2022/testing.mjs +993 -199
  98. package/fesm2022/testing.mjs.map +1 -1
  99. package/fesm2022/upgrade.mjs +30 -35
  100. package/fesm2022/upgrade.mjs.map +1 -1
  101. package/http/index.d.ts +2 -2
  102. package/http/testing/index.d.ts +3 -3
  103. package/index.d.ts +265 -70
  104. package/locales/ff-CM.mjs +31 -11
  105. package/locales/ff-GN.mjs +31 -11
  106. package/locales/ff-MR.mjs +31 -11
  107. package/locales/global/ff-CM.js +58 -44
  108. package/locales/global/ff-GN.js +58 -44
  109. package/locales/global/ff-MR.js +58 -44
  110. package/package.json +2 -2
  111. package/testing/index.d.ts +6 -1
  112. package/upgrade/index.d.ts +1 -1
@@ -1,244 +1,684 @@
1
1
  /**
2
- * @license Angular v17.2.0-next.0
2
+ * @license Angular v17.2.0-next.1
3
3
  * (c) 2010-2022 Google LLC. https://angular.io/
4
4
  * License: MIT
5
5
  */
6
6
 
7
- import { ɵnormalizeQueryParams, LocationStrategy, Location } from '@angular/common';
7
+ import { ɵPlatformNavigation, DOCUMENT, PlatformLocation, ɵnormalizeQueryParams, LocationStrategy, Location } from '@angular/common';
8
8
  import * as i0 from '@angular/core';
9
- import { EventEmitter, Injectable, InjectionToken, Inject, Optional } from '@angular/core';
9
+ import { Injectable, InjectionToken, Inject, Optional, inject, EventEmitter } from '@angular/core';
10
10
  import { Subject } from 'rxjs';
11
11
 
12
12
  /**
13
- * A spy for {@link Location} that allows tests to fire simulated location events.
14
- *
15
- * @publicApi
13
+ * This class wraps the platform Navigation API which allows server-specific and test
14
+ * implementations.
16
15
  */
17
- class SpyLocation {
18
- constructor() {
19
- this.urlChanges = [];
20
- this._history = [new LocationState('', '', null)];
21
- this._historyIndex = 0;
22
- /** @internal */
23
- this._subject = new EventEmitter();
24
- /** @internal */
25
- this._basePath = '';
26
- /** @internal */
27
- this._locationStrategy = null;
28
- /** @internal */
29
- this._urlChangeListeners = [];
30
- /** @internal */
31
- this._urlChangeSubscription = null;
16
+ class PlatformNavigation {
17
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: PlatformNavigation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
18
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: PlatformNavigation, providedIn: 'platform', useFactory: () => window.navigation }); }
19
+ }
20
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: PlatformNavigation, decorators: [{
21
+ type: Injectable,
22
+ args: [{ providedIn: 'platform', useFactory: () => window.navigation }]
23
+ }] });
24
+
25
+ /**
26
+ * Fake implementation of user agent history and navigation behavior. This is a
27
+ * high-fidelity implementation of browser behavior that attempts to emulate
28
+ * things like traversal delay.
29
+ */
30
+ class FakeNavigation {
31
+ /** Equivalent to `navigation.currentEntry`. */
32
+ get currentEntry() {
33
+ return this.entriesArr[this.currentEntryIndex];
32
34
  }
33
- /** @nodoc */
34
- ngOnDestroy() {
35
- this._urlChangeSubscription?.unsubscribe();
36
- this._urlChangeListeners = [];
35
+ get canGoBack() {
36
+ return this.currentEntryIndex > 0;
37
37
  }
38
- setInitialPath(url) {
39
- this._history[this._historyIndex].path = url;
38
+ get canGoForward() {
39
+ return this.currentEntryIndex < this.entriesArr.length - 1;
40
40
  }
41
- setBaseHref(url) {
42
- this._basePath = url;
41
+ constructor(window, startURL) {
42
+ this.window = window;
43
+ /**
44
+ * The fake implementation of an entries array. Only same-document entries
45
+ * allowed.
46
+ */
47
+ this.entriesArr = [];
48
+ /**
49
+ * The current active entry index into `entriesArr`.
50
+ */
51
+ this.currentEntryIndex = 0;
52
+ /**
53
+ * The current navigate event.
54
+ */
55
+ this.navigateEvent = undefined;
56
+ /**
57
+ * A Map of pending traversals, so that traversals to the same entry can be
58
+ * re-used.
59
+ */
60
+ this.traversalQueue = new Map();
61
+ /**
62
+ * A Promise that resolves when the previous traversals have finished. Used to
63
+ * simulate the cross-process communication necessary for traversals.
64
+ */
65
+ this.nextTraversal = Promise.resolve();
66
+ /**
67
+ * A prospective current active entry index, which includes unresolved
68
+ * traversals. Used by `go` to determine where navigations are intended to go.
69
+ */
70
+ this.prospectiveEntryIndex = 0;
71
+ /**
72
+ * A test-only option to make traversals synchronous, rather than emulate
73
+ * cross-process communication.
74
+ */
75
+ this.synchronousTraversals = false;
76
+ /** Whether to allow a call to setInitialEntryForTesting. */
77
+ this.canSetInitialEntry = true;
78
+ /** `EventTarget` to dispatch events. */
79
+ this.eventTarget = this.window.document.createElement('div');
80
+ /** The next unique id for created entries. Replace recreates this id. */
81
+ this.nextId = 0;
82
+ /** The next unique key for created entries. Replace inherits this id. */
83
+ this.nextKey = 0;
84
+ /** Whether this fake is disposed. */
85
+ this.disposed = false;
86
+ // First entry.
87
+ this.setInitialEntryForTesting(startURL);
43
88
  }
44
- path() {
45
- return this._history[this._historyIndex].path;
89
+ /**
90
+ * Sets the initial entry.
91
+ */
92
+ setInitialEntryForTesting(url, options = { historyState: null }) {
93
+ if (!this.canSetInitialEntry) {
94
+ throw new Error('setInitialEntryForTesting can only be called before any ' + 'navigation has occurred');
95
+ }
96
+ const currentInitialEntry = this.entriesArr[0];
97
+ this.entriesArr[0] = new FakeNavigationHistoryEntry(new URL(url).toString(), {
98
+ index: 0,
99
+ key: currentInitialEntry?.key ?? String(this.nextKey++),
100
+ id: currentInitialEntry?.id ?? String(this.nextId++),
101
+ sameDocument: true,
102
+ historyState: options?.historyState,
103
+ state: options.state,
104
+ });
46
105
  }
47
- getState() {
48
- return this._history[this._historyIndex].state;
106
+ /** Returns whether the initial entry is still eligible to be set. */
107
+ canSetInitialEntryForTesting() {
108
+ return this.canSetInitialEntry;
49
109
  }
50
- isCurrentPathEqualTo(path, query = '') {
51
- const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
52
- const currPath = this.path().endsWith('/') ? this.path().substring(0, this.path().length - 1) : this.path();
53
- return currPath == givenPath + (query.length > 0 ? ('?' + query) : '');
110
+ /**
111
+ * Sets whether to emulate traversals as synchronous rather than
112
+ * asynchronous.
113
+ */
114
+ setSynchronousTraversalsForTesting(synchronousTraversals) {
115
+ this.synchronousTraversals = synchronousTraversals;
54
116
  }
55
- simulateUrlPop(pathname) {
56
- this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
117
+ /** Equivalent to `navigation.entries()`. */
118
+ entries() {
119
+ return this.entriesArr.slice();
57
120
  }
58
- simulateHashChange(pathname) {
59
- const path = this.prepareExternalUrl(pathname);
60
- this.pushHistory(path, '', null);
61
- this.urlChanges.push('hash: ' + pathname);
62
- // the browser will automatically fire popstate event before each `hashchange` event, so we need
63
- // to simulate it.
64
- this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
65
- this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'hashchange' });
121
+ /** Equivalent to `navigation.navigate()`. */
122
+ navigate(url, options) {
123
+ const fromUrl = new URL(this.currentEntry.url);
124
+ const toUrl = new URL(url, this.currentEntry.url);
125
+ let navigationType;
126
+ if (!options?.history || options.history === 'auto') {
127
+ // Auto defaults to push, but if the URLs are the same, is a replace.
128
+ if (fromUrl.toString() === toUrl.toString()) {
129
+ navigationType = 'replace';
130
+ }
131
+ else {
132
+ navigationType = 'push';
133
+ }
134
+ }
135
+ else {
136
+ navigationType = options.history;
137
+ }
138
+ const hashChange = isHashChange(fromUrl, toUrl);
139
+ const destination = new FakeNavigationDestination({
140
+ url: toUrl.toString(),
141
+ state: options?.state,
142
+ sameDocument: hashChange,
143
+ historyState: null,
144
+ });
145
+ const result = new InternalNavigationResult();
146
+ this.userAgentNavigate(destination, result, {
147
+ navigationType,
148
+ cancelable: true,
149
+ canIntercept: true,
150
+ // Always false for navigate().
151
+ userInitiated: false,
152
+ hashChange,
153
+ info: options?.info,
154
+ });
155
+ return {
156
+ committed: result.committed,
157
+ finished: result.finished,
158
+ };
66
159
  }
67
- prepareExternalUrl(url) {
68
- if (url.length > 0 && !url.startsWith('/')) {
69
- url = '/' + url;
160
+ /** Equivalent to `history.pushState()`. */
161
+ pushState(data, title, url) {
162
+ this.pushOrReplaceState('push', data, title, url);
163
+ }
164
+ /** Equivalent to `history.replaceState()`. */
165
+ replaceState(data, title, url) {
166
+ this.pushOrReplaceState('replace', data, title, url);
167
+ }
168
+ pushOrReplaceState(navigationType, data, _title, url) {
169
+ const fromUrl = new URL(this.currentEntry.url);
170
+ const toUrl = url ? new URL(url, this.currentEntry.url) : fromUrl;
171
+ const hashChange = isHashChange(fromUrl, toUrl);
172
+ const destination = new FakeNavigationDestination({
173
+ url: toUrl.toString(),
174
+ sameDocument: true,
175
+ historyState: data,
176
+ });
177
+ const result = new InternalNavigationResult();
178
+ this.userAgentNavigate(destination, result, {
179
+ navigationType,
180
+ cancelable: true,
181
+ canIntercept: true,
182
+ // Always false for pushState() or replaceState().
183
+ userInitiated: false,
184
+ hashChange,
185
+ skipPopState: true,
186
+ });
187
+ }
188
+ /** Equivalent to `navigation.traverseTo()`. */
189
+ traverseTo(key, options) {
190
+ const fromUrl = new URL(this.currentEntry.url);
191
+ const entry = this.findEntry(key);
192
+ if (!entry) {
193
+ const domException = new DOMException('Invalid key', 'InvalidStateError');
194
+ const committed = Promise.reject(domException);
195
+ const finished = Promise.reject(domException);
196
+ committed.catch(() => { });
197
+ finished.catch(() => { });
198
+ return {
199
+ committed,
200
+ finished,
201
+ };
70
202
  }
71
- return this._basePath + url;
203
+ if (entry === this.currentEntry) {
204
+ return {
205
+ committed: Promise.resolve(this.currentEntry),
206
+ finished: Promise.resolve(this.currentEntry),
207
+ };
208
+ }
209
+ if (this.traversalQueue.has(entry.key)) {
210
+ const existingResult = this.traversalQueue.get(entry.key);
211
+ return {
212
+ committed: existingResult.committed,
213
+ finished: existingResult.finished,
214
+ };
215
+ }
216
+ const hashChange = isHashChange(fromUrl, new URL(entry.url, this.currentEntry.url));
217
+ const destination = new FakeNavigationDestination({
218
+ url: entry.url,
219
+ state: entry.getState(),
220
+ historyState: entry.getHistoryState(),
221
+ key: entry.key,
222
+ id: entry.id,
223
+ index: entry.index,
224
+ sameDocument: entry.sameDocument,
225
+ });
226
+ this.prospectiveEntryIndex = entry.index;
227
+ const result = new InternalNavigationResult();
228
+ this.traversalQueue.set(entry.key, result);
229
+ this.runTraversal(() => {
230
+ this.traversalQueue.delete(entry.key);
231
+ this.userAgentNavigate(destination, result, {
232
+ navigationType: 'traverse',
233
+ cancelable: true,
234
+ canIntercept: true,
235
+ // Always false for traverseTo().
236
+ userInitiated: false,
237
+ hashChange,
238
+ info: options?.info,
239
+ });
240
+ });
241
+ return {
242
+ committed: result.committed,
243
+ finished: result.finished,
244
+ };
72
245
  }
73
- go(path, query = '', state = null) {
74
- path = this.prepareExternalUrl(path);
75
- this.pushHistory(path, query, state);
76
- const locationState = this._history[this._historyIndex - 1];
77
- if (locationState.path == path && locationState.query == query) {
246
+ /** Equivalent to `navigation.back()`. */
247
+ back(options) {
248
+ if (this.currentEntryIndex === 0) {
249
+ const domException = new DOMException('Cannot go back', 'InvalidStateError');
250
+ const committed = Promise.reject(domException);
251
+ const finished = Promise.reject(domException);
252
+ committed.catch(() => { });
253
+ finished.catch(() => { });
254
+ return {
255
+ committed,
256
+ finished,
257
+ };
258
+ }
259
+ const entry = this.entriesArr[this.currentEntryIndex - 1];
260
+ return this.traverseTo(entry.key, options);
261
+ }
262
+ /** Equivalent to `navigation.forward()`. */
263
+ forward(options) {
264
+ if (this.currentEntryIndex === this.entriesArr.length - 1) {
265
+ const domException = new DOMException('Cannot go forward', 'InvalidStateError');
266
+ const committed = Promise.reject(domException);
267
+ const finished = Promise.reject(domException);
268
+ committed.catch(() => { });
269
+ finished.catch(() => { });
270
+ return {
271
+ committed,
272
+ finished,
273
+ };
274
+ }
275
+ const entry = this.entriesArr[this.currentEntryIndex + 1];
276
+ return this.traverseTo(entry.key, options);
277
+ }
278
+ /**
279
+ * Equivalent to `history.go()`.
280
+ * Note that this method does not actually work precisely to how Chrome
281
+ * does, instead choosing a simpler model with less unexpected behavior.
282
+ * Chrome has a few edge case optimizations, for instance with repeated
283
+ * `back(); forward()` chains it collapses certain traversals.
284
+ */
285
+ go(direction) {
286
+ const targetIndex = this.prospectiveEntryIndex + direction;
287
+ if (targetIndex >= this.entriesArr.length || targetIndex < 0) {
78
288
  return;
79
289
  }
80
- const url = path + (query.length > 0 ? ('?' + query) : '');
81
- this.urlChanges.push(url);
82
- this._notifyUrlChangeListeners(path + ɵnormalizeQueryParams(query), state);
290
+ this.prospectiveEntryIndex = targetIndex;
291
+ this.runTraversal(() => {
292
+ // Check again that destination is in the entries array.
293
+ if (targetIndex >= this.entriesArr.length || targetIndex < 0) {
294
+ return;
295
+ }
296
+ const fromUrl = new URL(this.currentEntry.url);
297
+ const entry = this.entriesArr[targetIndex];
298
+ const hashChange = isHashChange(fromUrl, new URL(entry.url, this.currentEntry.url));
299
+ const destination = new FakeNavigationDestination({
300
+ url: entry.url,
301
+ state: entry.getState(),
302
+ historyState: entry.getHistoryState(),
303
+ key: entry.key,
304
+ id: entry.id,
305
+ index: entry.index,
306
+ sameDocument: entry.sameDocument,
307
+ });
308
+ const result = new InternalNavigationResult();
309
+ this.userAgentNavigate(destination, result, {
310
+ navigationType: 'traverse',
311
+ cancelable: true,
312
+ canIntercept: true,
313
+ // Always false for go().
314
+ userInitiated: false,
315
+ hashChange,
316
+ });
317
+ });
83
318
  }
84
- replaceState(path, query = '', state = null) {
85
- path = this.prepareExternalUrl(path);
86
- const history = this._history[this._historyIndex];
87
- history.state = state;
88
- if (history.path == path && history.query == query) {
319
+ /** Runs a traversal synchronously or asynchronously */
320
+ runTraversal(traversal) {
321
+ if (this.synchronousTraversals) {
322
+ traversal();
89
323
  return;
90
324
  }
91
- history.path = path;
92
- history.query = query;
93
- const url = path + (query.length > 0 ? ('?' + query) : '');
94
- this.urlChanges.push('replace: ' + url);
95
- this._notifyUrlChangeListeners(path + ɵnormalizeQueryParams(query), state);
325
+ // Each traversal occupies a single timeout resolution.
326
+ // This means that Promises added to commit and finish should resolve
327
+ // before the next traversal.
328
+ this.nextTraversal = this.nextTraversal.then(() => {
329
+ return new Promise((resolve) => {
330
+ setTimeout(() => {
331
+ resolve();
332
+ traversal();
333
+ });
334
+ });
335
+ });
96
336
  }
97
- forward() {
98
- if (this._historyIndex < (this._history.length - 1)) {
99
- this._historyIndex++;
100
- this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
337
+ /** Equivalent to `navigation.addEventListener()`. */
338
+ addEventListener(type, callback, options) {
339
+ this.eventTarget.addEventListener(type, callback, options);
340
+ }
341
+ /** Equivalent to `navigation.removeEventListener()`. */
342
+ removeEventListener(type, callback, options) {
343
+ this.eventTarget.removeEventListener(type, callback, options);
344
+ }
345
+ /** Equivalent to `navigation.dispatchEvent()` */
346
+ dispatchEvent(event) {
347
+ return this.eventTarget.dispatchEvent(event);
348
+ }
349
+ /** Cleans up resources. */
350
+ dispose() {
351
+ // Recreate eventTarget to release current listeners.
352
+ // `document.createElement` because NodeJS `EventTarget` is incompatible with Domino's `Event`.
353
+ this.eventTarget = this.window.document.createElement('div');
354
+ this.disposed = true;
355
+ }
356
+ /** Returns whether this fake is disposed. */
357
+ isDisposed() {
358
+ return this.disposed;
359
+ }
360
+ /** Implementation for all navigations and traversals. */
361
+ userAgentNavigate(destination, result, options) {
362
+ // The first navigation should disallow any future calls to set the initial
363
+ // entry.
364
+ this.canSetInitialEntry = false;
365
+ if (this.navigateEvent) {
366
+ this.navigateEvent.cancel(new DOMException('Navigation was aborted', 'AbortError'));
367
+ this.navigateEvent = undefined;
368
+ }
369
+ const navigateEvent = createFakeNavigateEvent({
370
+ navigationType: options.navigationType,
371
+ cancelable: options.cancelable,
372
+ canIntercept: options.canIntercept,
373
+ userInitiated: options.userInitiated,
374
+ hashChange: options.hashChange,
375
+ signal: result.signal,
376
+ destination,
377
+ info: options.info,
378
+ sameDocument: destination.sameDocument,
379
+ skipPopState: options.skipPopState,
380
+ result,
381
+ userAgentCommit: () => {
382
+ this.userAgentCommit();
383
+ },
384
+ });
385
+ this.navigateEvent = navigateEvent;
386
+ this.eventTarget.dispatchEvent(navigateEvent);
387
+ navigateEvent.dispatchedNavigateEvent();
388
+ if (navigateEvent.commitOption === 'immediate') {
389
+ navigateEvent.commit(/* internal= */ true);
101
390
  }
102
391
  }
103
- back() {
104
- if (this._historyIndex > 0) {
105
- this._historyIndex--;
106
- this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
392
+ /** Implementation to commit a navigation. */
393
+ userAgentCommit() {
394
+ if (!this.navigateEvent) {
395
+ return;
396
+ }
397
+ const from = this.currentEntry;
398
+ if (!this.navigateEvent.sameDocument) {
399
+ const error = new Error('Cannot navigate to a non-same-document URL.');
400
+ this.navigateEvent.cancel(error);
401
+ throw error;
402
+ }
403
+ if (this.navigateEvent.navigationType === 'push' ||
404
+ this.navigateEvent.navigationType === 'replace') {
405
+ this.userAgentPushOrReplace(this.navigateEvent.destination, {
406
+ navigationType: this.navigateEvent.navigationType,
407
+ });
408
+ }
409
+ else if (this.navigateEvent.navigationType === 'traverse') {
410
+ this.userAgentTraverse(this.navigateEvent.destination);
411
+ }
412
+ this.navigateEvent.userAgentNavigated(this.currentEntry);
413
+ const currentEntryChangeEvent = createFakeNavigationCurrentEntryChangeEvent({
414
+ from,
415
+ navigationType: this.navigateEvent.navigationType,
416
+ });
417
+ this.eventTarget.dispatchEvent(currentEntryChangeEvent);
418
+ if (!this.navigateEvent.skipPopState) {
419
+ const popStateEvent = createPopStateEvent({
420
+ state: this.navigateEvent.destination.getHistoryState(),
421
+ });
422
+ this.window.dispatchEvent(popStateEvent);
107
423
  }
108
424
  }
109
- historyGo(relativePosition = 0) {
110
- const nextPageIndex = this._historyIndex + relativePosition;
111
- if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
112
- this._historyIndex = nextPageIndex;
113
- this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
425
+ /** Implementation for a push or replace navigation. */
426
+ userAgentPushOrReplace(destination, { navigationType }) {
427
+ if (navigationType === 'push') {
428
+ this.currentEntryIndex++;
429
+ this.prospectiveEntryIndex = this.currentEntryIndex;
430
+ }
431
+ const index = this.currentEntryIndex;
432
+ const key = navigationType === 'push' ? String(this.nextKey++) : this.currentEntry.key;
433
+ const entry = new FakeNavigationHistoryEntry(destination.url, {
434
+ id: String(this.nextId++),
435
+ key,
436
+ index,
437
+ sameDocument: true,
438
+ state: destination.getState(),
439
+ historyState: destination.getHistoryState(),
440
+ });
441
+ if (navigationType === 'push') {
442
+ this.entriesArr.splice(index, Infinity, entry);
443
+ }
444
+ else {
445
+ this.entriesArr[index] = entry;
114
446
  }
115
447
  }
116
- onUrlChange(fn) {
117
- this._urlChangeListeners.push(fn);
118
- if (!this._urlChangeSubscription) {
119
- this._urlChangeSubscription = this.subscribe(v => {
120
- this._notifyUrlChangeListeners(v.url, v.state);
121
- });
448
+ /** Implementation for a traverse navigation. */
449
+ userAgentTraverse(destination) {
450
+ this.currentEntryIndex = destination.index;
451
+ }
452
+ /** Utility method for finding entries with the given `key`. */
453
+ findEntry(key) {
454
+ for (const entry of this.entriesArr) {
455
+ if (entry.key === key)
456
+ return entry;
122
457
  }
123
- return () => {
124
- const fnIndex = this._urlChangeListeners.indexOf(fn);
125
- this._urlChangeListeners.splice(fnIndex, 1);
126
- if (this._urlChangeListeners.length === 0) {
127
- this._urlChangeSubscription?.unsubscribe();
128
- this._urlChangeSubscription = null;
129
- }
130
- };
458
+ return undefined;
131
459
  }
132
- /** @internal */
133
- _notifyUrlChangeListeners(url = '', state) {
134
- this._urlChangeListeners.forEach(fn => fn(url, state));
460
+ set onnavigate(_handler) {
461
+ throw new Error('unimplemented');
135
462
  }
136
- subscribe(onNext, onThrow, onReturn) {
137
- return this._subject.subscribe({ next: onNext, error: onThrow, complete: onReturn });
463
+ get onnavigate() {
464
+ throw new Error('unimplemented');
138
465
  }
139
- normalize(url) {
140
- return null;
466
+ set oncurrententrychange(_handler) {
467
+ throw new Error('unimplemented');
141
468
  }
142
- pushHistory(path, query, state) {
143
- if (this._historyIndex > 0) {
144
- this._history.splice(this._historyIndex + 1);
145
- }
146
- this._history.push(new LocationState(path, query, state));
147
- this._historyIndex = this._history.length - 1;
469
+ get oncurrententrychange() {
470
+ throw new Error('unimplemented');
148
471
  }
149
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: SpyLocation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
150
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: SpyLocation }); }
151
- }
152
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: SpyLocation, decorators: [{
153
- type: Injectable
154
- }] });
155
- class LocationState {
156
- constructor(path, query, state) {
157
- this.path = path;
158
- this.query = query;
159
- this.state = state;
472
+ set onnavigatesuccess(_handler) {
473
+ throw new Error('unimplemented');
474
+ }
475
+ get onnavigatesuccess() {
476
+ throw new Error('unimplemented');
477
+ }
478
+ set onnavigateerror(_handler) {
479
+ throw new Error('unimplemented');
480
+ }
481
+ get onnavigateerror() {
482
+ throw new Error('unimplemented');
483
+ }
484
+ get transition() {
485
+ throw new Error('unimplemented');
486
+ }
487
+ updateCurrentEntry(_options) {
488
+ throw new Error('unimplemented');
489
+ }
490
+ reload(_options) {
491
+ throw new Error('unimplemented');
160
492
  }
161
493
  }
162
-
163
494
  /**
164
- * A mock implementation of {@link LocationStrategy} that allows tests to fire simulated
165
- * location events.
166
- *
167
- * @publicApi
495
+ * Fake equivalent of `NavigationHistoryEntry`.
168
496
  */
169
- class MockLocationStrategy extends LocationStrategy {
170
- constructor() {
171
- super();
172
- this.internalBaseHref = '/';
173
- this.internalPath = '/';
174
- this.internalTitle = '';
175
- this.urlChanges = [];
176
- /** @internal */
177
- this._subject = new EventEmitter();
178
- this.stateChanges = [];
179
- }
180
- simulatePopState(url) {
181
- this.internalPath = url;
182
- this._subject.emit(new _MockPopStateEvent(this.path()));
183
- }
184
- path(includeHash = false) {
185
- return this.internalPath;
497
+ class FakeNavigationHistoryEntry {
498
+ constructor(url, { id, key, index, sameDocument, state, historyState, }) {
499
+ this.url = url;
500
+ // tslint:disable-next-line:no-any
501
+ this.ondispose = null;
502
+ this.id = id;
503
+ this.key = key;
504
+ this.index = index;
505
+ this.sameDocument = sameDocument;
506
+ this.state = state;
507
+ this.historyState = historyState;
186
508
  }
187
- prepareExternalUrl(internal) {
188
- if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
189
- return this.internalBaseHref + internal.substring(1);
190
- }
191
- return this.internalBaseHref + internal;
509
+ getState() {
510
+ // Budget copy.
511
+ return this.state ? JSON.parse(JSON.stringify(this.state)) : this.state;
192
512
  }
193
- pushState(ctx, title, path, query) {
194
- // Add state change to changes array
195
- this.stateChanges.push(ctx);
196
- this.internalTitle = title;
197
- const url = path + (query.length > 0 ? ('?' + query) : '');
198
- this.internalPath = url;
199
- const externalUrl = this.prepareExternalUrl(url);
200
- this.urlChanges.push(externalUrl);
513
+ getHistoryState() {
514
+ // Budget copy.
515
+ return this.historyState ? JSON.parse(JSON.stringify(this.historyState)) : this.historyState;
201
516
  }
202
- replaceState(ctx, title, path, query) {
203
- // Reset the last index of stateChanges to the ctx (state) object
204
- this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
205
- this.internalTitle = title;
206
- const url = path + (query.length > 0 ? ('?' + query) : '');
207
- this.internalPath = url;
208
- const externalUrl = this.prepareExternalUrl(url);
209
- this.urlChanges.push('replace: ' + externalUrl);
517
+ addEventListener(type, callback, options) {
518
+ throw new Error('unimplemented');
210
519
  }
211
- onPopState(fn) {
212
- this._subject.subscribe({ next: fn });
520
+ removeEventListener(type, callback, options) {
521
+ throw new Error('unimplemented');
213
522
  }
214
- getBaseHref() {
215
- return this.internalBaseHref;
523
+ dispatchEvent(event) {
524
+ throw new Error('unimplemented');
216
525
  }
217
- back() {
218
- if (this.urlChanges.length > 0) {
219
- this.urlChanges.pop();
220
- this.stateChanges.pop();
221
- const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
222
- this.simulatePopState(nextUrl);
526
+ }
527
+ /**
528
+ * Create a fake equivalent of `NavigateEvent`. This is not a class because ES5
529
+ * transpiled JavaScript cannot extend native Event.
530
+ */
531
+ function createFakeNavigateEvent({ cancelable, canIntercept, userInitiated, hashChange, navigationType, signal, destination, info, sameDocument, skipPopState, result, userAgentCommit, }) {
532
+ const event = new Event('navigate', { bubbles: false, cancelable });
533
+ event.canIntercept = canIntercept;
534
+ event.userInitiated = userInitiated;
535
+ event.hashChange = hashChange;
536
+ event.navigationType = navigationType;
537
+ event.signal = signal;
538
+ event.destination = destination;
539
+ event.info = info;
540
+ event.downloadRequest = null;
541
+ event.formData = null;
542
+ event.sameDocument = sameDocument;
543
+ event.skipPopState = skipPopState;
544
+ event.commitOption = 'immediate';
545
+ let handlerFinished = undefined;
546
+ let interceptCalled = false;
547
+ let dispatchedNavigateEvent = false;
548
+ let commitCalled = false;
549
+ event.intercept = function (options) {
550
+ interceptCalled = true;
551
+ event.sameDocument = true;
552
+ const handler = options?.handler;
553
+ if (handler) {
554
+ handlerFinished = handler();
223
555
  }
224
- }
225
- forward() {
226
- throw 'not implemented';
556
+ if (options?.commit) {
557
+ event.commitOption = options.commit;
558
+ }
559
+ if (options?.focusReset !== undefined || options?.scroll !== undefined) {
560
+ throw new Error('unimplemented');
561
+ }
562
+ };
563
+ event.scroll = function () {
564
+ throw new Error('unimplemented');
565
+ };
566
+ event.commit = function (internal = false) {
567
+ if (!internal && !interceptCalled) {
568
+ throw new DOMException(`Failed to execute 'commit' on 'NavigateEvent': intercept() must be ` +
569
+ `called before commit().`, 'InvalidStateError');
570
+ }
571
+ if (!dispatchedNavigateEvent) {
572
+ throw new DOMException(`Failed to execute 'commit' on 'NavigateEvent': commit() may not be ` +
573
+ `called during event dispatch.`, 'InvalidStateError');
574
+ }
575
+ if (commitCalled) {
576
+ throw new DOMException(`Failed to execute 'commit' on 'NavigateEvent': commit() already ` + `called.`, 'InvalidStateError');
577
+ }
578
+ commitCalled = true;
579
+ userAgentCommit();
580
+ };
581
+ // Internal only.
582
+ event.cancel = function (reason) {
583
+ result.committedReject(reason);
584
+ result.finishedReject(reason);
585
+ };
586
+ // Internal only.
587
+ event.dispatchedNavigateEvent = function () {
588
+ dispatchedNavigateEvent = true;
589
+ if (event.commitOption === 'after-transition') {
590
+ // If handler finishes before commit, call commit.
591
+ handlerFinished?.then(() => {
592
+ if (!commitCalled) {
593
+ event.commit(/* internal */ true);
594
+ }
595
+ }, () => { });
596
+ }
597
+ Promise.all([result.committed, handlerFinished]).then(([entry]) => {
598
+ result.finishedResolve(entry);
599
+ }, (reason) => {
600
+ result.finishedReject(reason);
601
+ });
602
+ };
603
+ // Internal only.
604
+ event.userAgentNavigated = function (entry) {
605
+ result.committedResolve(entry);
606
+ };
607
+ return event;
608
+ }
609
+ /**
610
+ * Create a fake equivalent of `NavigationCurrentEntryChange`. This does not use
611
+ * a class because ES5 transpiled JavaScript cannot extend native Event.
612
+ */
613
+ function createFakeNavigationCurrentEntryChangeEvent({ from, navigationType, }) {
614
+ const event = new Event('currententrychange', {
615
+ bubbles: false,
616
+ cancelable: false,
617
+ });
618
+ event.from = from;
619
+ event.navigationType = navigationType;
620
+ return event;
621
+ }
622
+ /**
623
+ * Create a fake equivalent of `PopStateEvent`. This does not use a class
624
+ * because ES5 transpiled JavaScript cannot extend native Event.
625
+ */
626
+ function createPopStateEvent({ state }) {
627
+ const event = new Event('popstate', {
628
+ bubbles: false,
629
+ cancelable: false,
630
+ });
631
+ event.state = state;
632
+ return event;
633
+ }
634
+ /**
635
+ * Fake equivalent of `NavigationDestination`.
636
+ */
637
+ class FakeNavigationDestination {
638
+ constructor({ url, sameDocument, historyState, state, key = null, id = null, index = -1, }) {
639
+ this.url = url;
640
+ this.sameDocument = sameDocument;
641
+ this.state = state;
642
+ this.historyState = historyState;
643
+ this.key = key;
644
+ this.id = id;
645
+ this.index = index;
227
646
  }
228
647
  getState() {
229
- return this.stateChanges[(this.stateChanges.length || 1) - 1];
648
+ return this.state;
649
+ }
650
+ getHistoryState() {
651
+ return this.historyState;
230
652
  }
231
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: MockLocationStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
232
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: MockLocationStrategy }); }
233
653
  }
234
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: MockLocationStrategy, decorators: [{
235
- type: Injectable
236
- }], ctorParameters: () => [] });
237
- class _MockPopStateEvent {
238
- constructor(newUrl) {
239
- this.newUrl = newUrl;
240
- this.pop = true;
241
- this.type = 'popstate';
654
+ /** Utility function to determine whether two UrlLike have the same hash. */
655
+ function isHashChange(from, to) {
656
+ return (to.hash !== from.hash &&
657
+ to.hostname === from.hostname &&
658
+ to.pathname === from.pathname &&
659
+ to.search === from.search);
660
+ }
661
+ /** Internal utility class for representing the result of a navigation. */
662
+ class InternalNavigationResult {
663
+ get signal() {
664
+ return this.abortController.signal;
665
+ }
666
+ constructor() {
667
+ this.abortController = new AbortController();
668
+ this.committed = new Promise((resolve, reject) => {
669
+ this.committedResolve = resolve;
670
+ this.committedReject = reject;
671
+ });
672
+ this.finished = new Promise(async (resolve, reject) => {
673
+ this.finishedResolve = resolve;
674
+ this.finishedReject = (reason) => {
675
+ reject(reason);
676
+ this.abortController.abort(reason);
677
+ };
678
+ });
679
+ // All rejections are handled.
680
+ this.committed.catch(() => { });
681
+ this.finished.catch(() => { });
242
682
  }
243
683
  }
244
684
 
@@ -293,9 +733,9 @@ function parseUrl(urlStr, baseHref) {
293
733
  parsedUrl.pathname = parsedUrl.pathname.substring(baseHref.length);
294
734
  }
295
735
  return {
296
- hostname: !serverBase && parsedUrl.hostname || '',
297
- protocol: !serverBase && parsedUrl.protocol || '',
298
- port: !serverBase && parsedUrl.port || '',
736
+ hostname: (!serverBase && parsedUrl.hostname) || '',
737
+ protocol: (!serverBase && parsedUrl.protocol) || '',
738
+ port: (!serverBase && parsedUrl.port) || '',
299
739
  pathname: parsedUrl.pathname || '/',
300
740
  search: parsedUrl.search || '',
301
741
  hash: parsedUrl.hash || '',
@@ -372,15 +812,26 @@ class MockPlatformLocation {
372
812
  }
373
813
  replaceState(state, title, newUrl) {
374
814
  const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
375
- this.urlChanges[this.urlChangeIndex] =
376
- { ...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState };
815
+ this.urlChanges[this.urlChangeIndex] = {
816
+ ...this.urlChanges[this.urlChangeIndex],
817
+ pathname,
818
+ search,
819
+ hash,
820
+ state: parsedState,
821
+ };
377
822
  }
378
823
  pushState(state, title, newUrl) {
379
824
  const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
380
825
  if (this.urlChangeIndex > 0) {
381
826
  this.urlChanges.splice(this.urlChangeIndex + 1);
382
827
  }
383
- this.urlChanges.push({ ...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState });
828
+ this.urlChanges.push({
829
+ ...this.urlChanges[this.urlChangeIndex],
830
+ pathname,
831
+ search,
832
+ hash,
833
+ state: parsedState,
834
+ });
384
835
  this.urlChangeIndex = this.urlChanges.length - 1;
385
836
  }
386
837
  forward() {
@@ -422,15 +873,25 @@ class MockPlatformLocation {
422
873
  * https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#when_popstate_is_sent
423
874
  */
424
875
  emitEvents(oldHash, oldUrl) {
425
- this.popStateSubject.next({ type: 'popstate', state: this.getState(), oldUrl, newUrl: this.url });
876
+ this.popStateSubject.next({
877
+ type: 'popstate',
878
+ state: this.getState(),
879
+ oldUrl,
880
+ newUrl: this.url,
881
+ });
426
882
  if (oldHash !== this.hash) {
427
- this.hashUpdate.next({ type: 'hashchange', state: null, oldUrl, newUrl: this.url });
883
+ this.hashUpdate.next({
884
+ type: 'hashchange',
885
+ state: null,
886
+ oldUrl,
887
+ newUrl: this.url,
888
+ });
428
889
  }
429
890
  }
430
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: MockPlatformLocation, deps: [{ token: MOCK_PLATFORM_LOCATION_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
431
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: MockPlatformLocation }); }
891
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: MockPlatformLocation, deps: [{ token: MOCK_PLATFORM_LOCATION_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
892
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: MockPlatformLocation }); }
432
893
  }
433
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.0", ngImport: i0, type: MockPlatformLocation, decorators: [{
894
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: MockPlatformLocation, decorators: [{
434
895
  type: Injectable
435
896
  }], ctorParameters: () => [{ type: undefined, decorators: [{
436
897
  type: Inject,
@@ -438,6 +899,339 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.0",
438
899
  }, {
439
900
  type: Optional
440
901
  }] }] });
902
+ /**
903
+ * Mock implementation of URL state.
904
+ */
905
+ class FakeNavigationPlatformLocation {
906
+ constructor() {
907
+ this._platformNavigation = inject(ɵPlatformNavigation);
908
+ this.window = inject(DOCUMENT).defaultView;
909
+ this.config = inject(MOCK_PLATFORM_LOCATION_CONFIG, { optional: true });
910
+ if (!(this._platformNavigation instanceof FakeNavigation)) {
911
+ throw new Error('FakePlatformNavigation cannot be used without FakeNavigation. Use ' +
912
+ '`provideFakeNavigation` to have all these services provided together.');
913
+ }
914
+ }
915
+ getBaseHrefFromDOM() {
916
+ return this.config?.appBaseHref ?? '';
917
+ }
918
+ onPopState(fn) {
919
+ this.window.addEventListener('popstate', fn);
920
+ return () => this.window.removeEventListener('popstate', fn);
921
+ }
922
+ onHashChange(fn) {
923
+ this.window.addEventListener('hashchange', fn);
924
+ return () => this.window.removeEventListener('hashchange', fn);
925
+ }
926
+ get href() {
927
+ return this._platformNavigation.currentEntry.url;
928
+ }
929
+ get protocol() {
930
+ return new URL(this._platformNavigation.currentEntry.url).protocol;
931
+ }
932
+ get hostname() {
933
+ return new URL(this._platformNavigation.currentEntry.url).hostname;
934
+ }
935
+ get port() {
936
+ return new URL(this._platformNavigation.currentEntry.url).port;
937
+ }
938
+ get pathname() {
939
+ return new URL(this._platformNavigation.currentEntry.url).pathname;
940
+ }
941
+ get search() {
942
+ return new URL(this._platformNavigation.currentEntry.url).search;
943
+ }
944
+ get hash() {
945
+ return new URL(this._platformNavigation.currentEntry.url).hash;
946
+ }
947
+ pushState(state, title, url) {
948
+ this._platformNavigation.pushState(state, title, url);
949
+ }
950
+ replaceState(state, title, url) {
951
+ this._platformNavigation.replaceState(state, title, url);
952
+ }
953
+ forward() {
954
+ this._platformNavigation.forward();
955
+ }
956
+ back() {
957
+ this._platformNavigation.back();
958
+ }
959
+ historyGo(relativePosition = 0) {
960
+ this._platformNavigation.go(relativePosition);
961
+ }
962
+ getState() {
963
+ return this._platformNavigation.currentEntry.getHistoryState();
964
+ }
965
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: FakeNavigationPlatformLocation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
966
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: FakeNavigationPlatformLocation }); }
967
+ }
968
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: FakeNavigationPlatformLocation, decorators: [{
969
+ type: Injectable
970
+ }], ctorParameters: () => [] });
971
+
972
+ /**
973
+ * Return a provider for the `FakeNavigation` in place of the real Navigation API.
974
+ */
975
+ function provideFakePlatformNavigation() {
976
+ return [
977
+ {
978
+ provide: PlatformNavigation,
979
+ useFactory: () => {
980
+ const config = inject(MOCK_PLATFORM_LOCATION_CONFIG, { optional: true });
981
+ return new FakeNavigation(inject(DOCUMENT).defaultView, config?.startUrl ?? 'http://_empty_/');
982
+ },
983
+ },
984
+ { provide: PlatformLocation, useClass: FakeNavigationPlatformLocation },
985
+ ];
986
+ }
987
+
988
+ /**
989
+ * A spy for {@link Location} that allows tests to fire simulated location events.
990
+ *
991
+ * @publicApi
992
+ */
993
+ class SpyLocation {
994
+ constructor() {
995
+ this.urlChanges = [];
996
+ this._history = [new LocationState('', '', null)];
997
+ this._historyIndex = 0;
998
+ /** @internal */
999
+ this._subject = new EventEmitter();
1000
+ /** @internal */
1001
+ this._basePath = '';
1002
+ /** @internal */
1003
+ this._locationStrategy = null;
1004
+ /** @internal */
1005
+ this._urlChangeListeners = [];
1006
+ /** @internal */
1007
+ this._urlChangeSubscription = null;
1008
+ }
1009
+ /** @nodoc */
1010
+ ngOnDestroy() {
1011
+ this._urlChangeSubscription?.unsubscribe();
1012
+ this._urlChangeListeners = [];
1013
+ }
1014
+ setInitialPath(url) {
1015
+ this._history[this._historyIndex].path = url;
1016
+ }
1017
+ setBaseHref(url) {
1018
+ this._basePath = url;
1019
+ }
1020
+ path() {
1021
+ return this._history[this._historyIndex].path;
1022
+ }
1023
+ getState() {
1024
+ return this._history[this._historyIndex].state;
1025
+ }
1026
+ isCurrentPathEqualTo(path, query = '') {
1027
+ const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
1028
+ const currPath = this.path().endsWith('/')
1029
+ ? this.path().substring(0, this.path().length - 1)
1030
+ : this.path();
1031
+ return currPath == givenPath + (query.length > 0 ? '?' + query : '');
1032
+ }
1033
+ simulateUrlPop(pathname) {
1034
+ this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
1035
+ }
1036
+ simulateHashChange(pathname) {
1037
+ const path = this.prepareExternalUrl(pathname);
1038
+ this.pushHistory(path, '', null);
1039
+ this.urlChanges.push('hash: ' + pathname);
1040
+ // the browser will automatically fire popstate event before each `hashchange` event, so we need
1041
+ // to simulate it.
1042
+ this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
1043
+ this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'hashchange' });
1044
+ }
1045
+ prepareExternalUrl(url) {
1046
+ if (url.length > 0 && !url.startsWith('/')) {
1047
+ url = '/' + url;
1048
+ }
1049
+ return this._basePath + url;
1050
+ }
1051
+ go(path, query = '', state = null) {
1052
+ path = this.prepareExternalUrl(path);
1053
+ this.pushHistory(path, query, state);
1054
+ const locationState = this._history[this._historyIndex - 1];
1055
+ if (locationState.path == path && locationState.query == query) {
1056
+ return;
1057
+ }
1058
+ const url = path + (query.length > 0 ? '?' + query : '');
1059
+ this.urlChanges.push(url);
1060
+ this._notifyUrlChangeListeners(path + ɵnormalizeQueryParams(query), state);
1061
+ }
1062
+ replaceState(path, query = '', state = null) {
1063
+ path = this.prepareExternalUrl(path);
1064
+ const history = this._history[this._historyIndex];
1065
+ history.state = state;
1066
+ if (history.path == path && history.query == query) {
1067
+ return;
1068
+ }
1069
+ history.path = path;
1070
+ history.query = query;
1071
+ const url = path + (query.length > 0 ? '?' + query : '');
1072
+ this.urlChanges.push('replace: ' + url);
1073
+ this._notifyUrlChangeListeners(path + ɵnormalizeQueryParams(query), state);
1074
+ }
1075
+ forward() {
1076
+ if (this._historyIndex < this._history.length - 1) {
1077
+ this._historyIndex++;
1078
+ this._subject.emit({
1079
+ 'url': this.path(),
1080
+ 'state': this.getState(),
1081
+ 'pop': true,
1082
+ 'type': 'popstate',
1083
+ });
1084
+ }
1085
+ }
1086
+ back() {
1087
+ if (this._historyIndex > 0) {
1088
+ this._historyIndex--;
1089
+ this._subject.emit({
1090
+ 'url': this.path(),
1091
+ 'state': this.getState(),
1092
+ 'pop': true,
1093
+ 'type': 'popstate',
1094
+ });
1095
+ }
1096
+ }
1097
+ historyGo(relativePosition = 0) {
1098
+ const nextPageIndex = this._historyIndex + relativePosition;
1099
+ if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
1100
+ this._historyIndex = nextPageIndex;
1101
+ this._subject.emit({
1102
+ 'url': this.path(),
1103
+ 'state': this.getState(),
1104
+ 'pop': true,
1105
+ 'type': 'popstate',
1106
+ });
1107
+ }
1108
+ }
1109
+ onUrlChange(fn) {
1110
+ this._urlChangeListeners.push(fn);
1111
+ this._urlChangeSubscription ??= this.subscribe((v) => {
1112
+ this._notifyUrlChangeListeners(v.url, v.state);
1113
+ });
1114
+ return () => {
1115
+ const fnIndex = this._urlChangeListeners.indexOf(fn);
1116
+ this._urlChangeListeners.splice(fnIndex, 1);
1117
+ if (this._urlChangeListeners.length === 0) {
1118
+ this._urlChangeSubscription?.unsubscribe();
1119
+ this._urlChangeSubscription = null;
1120
+ }
1121
+ };
1122
+ }
1123
+ /** @internal */
1124
+ _notifyUrlChangeListeners(url = '', state) {
1125
+ this._urlChangeListeners.forEach((fn) => fn(url, state));
1126
+ }
1127
+ subscribe(onNext, onThrow, onReturn) {
1128
+ return this._subject.subscribe({ next: onNext, error: onThrow, complete: onReturn });
1129
+ }
1130
+ normalize(url) {
1131
+ return null;
1132
+ }
1133
+ pushHistory(path, query, state) {
1134
+ if (this._historyIndex > 0) {
1135
+ this._history.splice(this._historyIndex + 1);
1136
+ }
1137
+ this._history.push(new LocationState(path, query, state));
1138
+ this._historyIndex = this._history.length - 1;
1139
+ }
1140
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: SpyLocation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1141
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: SpyLocation }); }
1142
+ }
1143
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: SpyLocation, decorators: [{
1144
+ type: Injectable
1145
+ }] });
1146
+ class LocationState {
1147
+ constructor(path, query, state) {
1148
+ this.path = path;
1149
+ this.query = query;
1150
+ this.state = state;
1151
+ }
1152
+ }
1153
+
1154
+ /**
1155
+ * A mock implementation of {@link LocationStrategy} that allows tests to fire simulated
1156
+ * location events.
1157
+ *
1158
+ * @publicApi
1159
+ */
1160
+ class MockLocationStrategy extends LocationStrategy {
1161
+ constructor() {
1162
+ super();
1163
+ this.internalBaseHref = '/';
1164
+ this.internalPath = '/';
1165
+ this.internalTitle = '';
1166
+ this.urlChanges = [];
1167
+ /** @internal */
1168
+ this._subject = new EventEmitter();
1169
+ this.stateChanges = [];
1170
+ }
1171
+ simulatePopState(url) {
1172
+ this.internalPath = url;
1173
+ this._subject.emit(new _MockPopStateEvent(this.path()));
1174
+ }
1175
+ path(includeHash = false) {
1176
+ return this.internalPath;
1177
+ }
1178
+ prepareExternalUrl(internal) {
1179
+ if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
1180
+ return this.internalBaseHref + internal.substring(1);
1181
+ }
1182
+ return this.internalBaseHref + internal;
1183
+ }
1184
+ pushState(ctx, title, path, query) {
1185
+ // Add state change to changes array
1186
+ this.stateChanges.push(ctx);
1187
+ this.internalTitle = title;
1188
+ const url = path + (query.length > 0 ? '?' + query : '');
1189
+ this.internalPath = url;
1190
+ const externalUrl = this.prepareExternalUrl(url);
1191
+ this.urlChanges.push(externalUrl);
1192
+ }
1193
+ replaceState(ctx, title, path, query) {
1194
+ // Reset the last index of stateChanges to the ctx (state) object
1195
+ this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
1196
+ this.internalTitle = title;
1197
+ const url = path + (query.length > 0 ? '?' + query : '');
1198
+ this.internalPath = url;
1199
+ const externalUrl = this.prepareExternalUrl(url);
1200
+ this.urlChanges.push('replace: ' + externalUrl);
1201
+ }
1202
+ onPopState(fn) {
1203
+ this._subject.subscribe({ next: fn });
1204
+ }
1205
+ getBaseHref() {
1206
+ return this.internalBaseHref;
1207
+ }
1208
+ back() {
1209
+ if (this.urlChanges.length > 0) {
1210
+ this.urlChanges.pop();
1211
+ this.stateChanges.pop();
1212
+ const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
1213
+ this.simulatePopState(nextUrl);
1214
+ }
1215
+ }
1216
+ forward() {
1217
+ throw 'not implemented';
1218
+ }
1219
+ getState() {
1220
+ return this.stateChanges[(this.stateChanges.length || 1) - 1];
1221
+ }
1222
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: MockLocationStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1223
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: MockLocationStrategy }); }
1224
+ }
1225
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.0-next.1", ngImport: i0, type: MockLocationStrategy, decorators: [{
1226
+ type: Injectable
1227
+ }], ctorParameters: () => [] });
1228
+ class _MockPopStateEvent {
1229
+ constructor(newUrl) {
1230
+ this.newUrl = newUrl;
1231
+ this.pop = true;
1232
+ this.type = 'popstate';
1233
+ }
1234
+ }
441
1235
 
442
1236
  /**
443
1237
  * Returns mock providers for the `Location` and `LocationStrategy` classes.
@@ -471,5 +1265,5 @@ function provideLocationMocks() {
471
1265
  * Generated bundle index. Do not edit.
472
1266
  */
473
1267
 
474
- export { MOCK_PLATFORM_LOCATION_CONFIG, MockLocationStrategy, MockPlatformLocation, SpyLocation, provideLocationMocks };
1268
+ export { MOCK_PLATFORM_LOCATION_CONFIG, MockLocationStrategy, MockPlatformLocation, SpyLocation, provideLocationMocks, provideFakePlatformNavigation as ɵprovideFakePlatformNavigation };
475
1269
  //# sourceMappingURL=testing.mjs.map