testing.mjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. /**
  2. * @license Angular v16.2.9
  3. * (c) 2010-2022 Google LLC. https://angular.io/
  4. * License: MIT
  5. */
  6. import * as i0 from '@angular/core';
  7. import { EventEmitter, Injectable, InjectionToken, Inject, Optional } from '@angular/core';
  8. import { LocationStrategy, Location } from '@angular/common';
  9. import { Subject } from 'rxjs';
  10. /**
  11. * Joins two parts of a URL with a slash if needed.
  12. *
  13. * @param start URL string
  14. * @param end URL string
  15. *
  16. *
  17. * @returns The joined URL string.
  18. */
  19. function joinWithSlash(start, end) {
  20. if (start.length == 0) {
  21. return end;
  22. }
  23. if (end.length == 0) {
  24. return start;
  25. }
  26. let slashes = 0;
  27. if (start.endsWith('/')) {
  28. slashes++;
  29. }
  30. if (end.startsWith('/')) {
  31. slashes++;
  32. }
  33. if (slashes == 2) {
  34. return start + end.substring(1);
  35. }
  36. if (slashes == 1) {
  37. return start + end;
  38. }
  39. return start + '/' + end;
  40. }
  41. /**
  42. * Removes a trailing slash from a URL string if needed.
  43. * Looks for the first occurrence of either `#`, `?`, or the end of the
  44. * line as `/` characters and removes the trailing slash if one exists.
  45. *
  46. * @param url URL string.
  47. *
  48. * @returns The URL string, modified if needed.
  49. */
  50. function stripTrailingSlash(url) {
  51. const match = url.match(/#|\?|$/);
  52. const pathEndIdx = match && match.index || url.length;
  53. const droppedSlashIdx = pathEndIdx - (url[pathEndIdx - 1] === '/' ? 1 : 0);
  54. return url.slice(0, droppedSlashIdx) + url.slice(pathEndIdx);
  55. }
  56. /**
  57. * Normalizes URL parameters by prepending with `?` if needed.
  58. *
  59. * @param params String of URL parameters.
  60. *
  61. * @returns The normalized URL parameters string.
  62. */
  63. function normalizeQueryParams(params) {
  64. return params && params[0] !== '?' ? '?' + params : params;
  65. }
  66. /**
  67. * A spy for {@link Location} that allows tests to fire simulated location events.
  68. *
  69. * @publicApi
  70. */
  71. class SpyLocation {
  72. constructor() {
  73. this.urlChanges = [];
  74. this._history = [new LocationState('', '', null)];
  75. this._historyIndex = 0;
  76. /** @internal */
  77. this._subject = new EventEmitter();
  78. /** @internal */
  79. this._basePath = '';
  80. /** @internal */
  81. this._locationStrategy = null;
  82. /** @internal */
  83. this._urlChangeListeners = [];
  84. /** @internal */
  85. this._urlChangeSubscription = null;
  86. }
  87. /** @nodoc */
  88. ngOnDestroy() {
  89. this._urlChangeSubscription?.unsubscribe();
  90. this._urlChangeListeners = [];
  91. }
  92. setInitialPath(url) {
  93. this._history[this._historyIndex].path = url;
  94. }
  95. setBaseHref(url) {
  96. this._basePath = url;
  97. }
  98. path() {
  99. return this._history[this._historyIndex].path;
  100. }
  101. getState() {
  102. return this._history[this._historyIndex].state;
  103. }
  104. isCurrentPathEqualTo(path, query = '') {
  105. const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
  106. const currPath = this.path().endsWith('/') ? this.path().substring(0, this.path().length - 1) : this.path();
  107. return currPath == givenPath + (query.length > 0 ? ('?' + query) : '');
  108. }
  109. simulateUrlPop(pathname) {
  110. this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
  111. }
  112. simulateHashChange(pathname) {
  113. const path = this.prepareExternalUrl(pathname);
  114. this.pushHistory(path, '', null);
  115. this.urlChanges.push('hash: ' + pathname);
  116. // the browser will automatically fire popstate event before each `hashchange` event, so we need
  117. // to simulate it.
  118. this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
  119. this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'hashchange' });
  120. }
  121. prepareExternalUrl(url) {
  122. if (url.length > 0 && !url.startsWith('/')) {
  123. url = '/' + url;
  124. }
  125. return this._basePath + url;
  126. }
  127. go(path, query = '', state = null) {
  128. path = this.prepareExternalUrl(path);
  129. this.pushHistory(path, query, state);
  130. const locationState = this._history[this._historyIndex - 1];
  131. if (locationState.path == path && locationState.query == query) {
  132. return;
  133. }
  134. const url = path + (query.length > 0 ? ('?' + query) : '');
  135. this.urlChanges.push(url);
  136. this._notifyUrlChangeListeners(path + normalizeQueryParams(query), state);
  137. }
  138. replaceState(path, query = '', state = null) {
  139. path = this.prepareExternalUrl(path);
  140. const history = this._history[this._historyIndex];
  141. history.state = state;
  142. if (history.path == path && history.query == query) {
  143. return;
  144. }
  145. history.path = path;
  146. history.query = query;
  147. const url = path + (query.length > 0 ? ('?' + query) : '');
  148. this.urlChanges.push('replace: ' + url);
  149. this._notifyUrlChangeListeners(path + normalizeQueryParams(query), state);
  150. }
  151. forward() {
  152. if (this._historyIndex < (this._history.length - 1)) {
  153. this._historyIndex++;
  154. this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
  155. }
  156. }
  157. back() {
  158. if (this._historyIndex > 0) {
  159. this._historyIndex--;
  160. this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
  161. }
  162. }
  163. historyGo(relativePosition = 0) {
  164. const nextPageIndex = this._historyIndex + relativePosition;
  165. if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
  166. this._historyIndex = nextPageIndex;
  167. this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
  168. }
  169. }
  170. onUrlChange(fn) {
  171. this._urlChangeListeners.push(fn);
  172. if (!this._urlChangeSubscription) {
  173. this._urlChangeSubscription = this.subscribe(v => {
  174. this._notifyUrlChangeListeners(v.url, v.state);
  175. });
  176. }
  177. return () => {
  178. const fnIndex = this._urlChangeListeners.indexOf(fn);
  179. this._urlChangeListeners.splice(fnIndex, 1);
  180. if (this._urlChangeListeners.length === 0) {
  181. this._urlChangeSubscription?.unsubscribe();
  182. this._urlChangeSubscription = null;
  183. }
  184. };
  185. }
  186. /** @internal */
  187. _notifyUrlChangeListeners(url = '', state) {
  188. this._urlChangeListeners.forEach(fn => fn(url, state));
  189. }
  190. subscribe(onNext, onThrow, onReturn) {
  191. return this._subject.subscribe({ next: onNext, error: onThrow, complete: onReturn });
  192. }
  193. normalize(url) {
  194. return null;
  195. }
  196. pushHistory(path, query, state) {
  197. if (this._historyIndex > 0) {
  198. this._history.splice(this._historyIndex + 1);
  199. }
  200. this._history.push(new LocationState(path, query, state));
  201. this._historyIndex = this._history.length - 1;
  202. }
  203. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: SpyLocation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
  204. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: SpyLocation }); }
  205. }
  206. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: SpyLocation, decorators: [{
  207. type: Injectable
  208. }] });
  209. class LocationState {
  210. constructor(path, query, state) {
  211. this.path = path;
  212. this.query = query;
  213. this.state = state;
  214. }
  215. }
  216. /**
  217. * A mock implementation of {@link LocationStrategy} that allows tests to fire simulated
  218. * location events.
  219. *
  220. * @publicApi
  221. */
  222. class MockLocationStrategy extends LocationStrategy {
  223. constructor() {
  224. super();
  225. this.internalBaseHref = '/';
  226. this.internalPath = '/';
  227. this.internalTitle = '';
  228. this.urlChanges = [];
  229. /** @internal */
  230. this._subject = new EventEmitter();
  231. this.stateChanges = [];
  232. }
  233. simulatePopState(url) {
  234. this.internalPath = url;
  235. this._subject.emit(new _MockPopStateEvent(this.path()));
  236. }
  237. path(includeHash = false) {
  238. return this.internalPath;
  239. }
  240. prepareExternalUrl(internal) {
  241. if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
  242. return this.internalBaseHref + internal.substring(1);
  243. }
  244. return this.internalBaseHref + internal;
  245. }
  246. pushState(ctx, title, path, query) {
  247. // Add state change to changes array
  248. this.stateChanges.push(ctx);
  249. this.internalTitle = title;
  250. const url = path + (query.length > 0 ? ('?' + query) : '');
  251. this.internalPath = url;
  252. const externalUrl = this.prepareExternalUrl(url);
  253. this.urlChanges.push(externalUrl);
  254. }
  255. replaceState(ctx, title, path, query) {
  256. // Reset the last index of stateChanges to the ctx (state) object
  257. this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
  258. this.internalTitle = title;
  259. const url = path + (query.length > 0 ? ('?' + query) : '');
  260. this.internalPath = url;
  261. const externalUrl = this.prepareExternalUrl(url);
  262. this.urlChanges.push('replace: ' + externalUrl);
  263. }
  264. onPopState(fn) {
  265. this._subject.subscribe({ next: fn });
  266. }
  267. getBaseHref() {
  268. return this.internalBaseHref;
  269. }
  270. back() {
  271. if (this.urlChanges.length > 0) {
  272. this.urlChanges.pop();
  273. this.stateChanges.pop();
  274. const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
  275. this.simulatePopState(nextUrl);
  276. }
  277. }
  278. forward() {
  279. throw 'not implemented';
  280. }
  281. getState() {
  282. return this.stateChanges[(this.stateChanges.length || 1) - 1];
  283. }
  284. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: MockLocationStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
  285. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: MockLocationStrategy }); }
  286. }
  287. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: MockLocationStrategy, decorators: [{
  288. type: Injectable
  289. }], ctorParameters: function () { return []; } });
  290. class _MockPopStateEvent {
  291. constructor(newUrl) {
  292. this.newUrl = newUrl;
  293. this.pop = true;
  294. this.type = 'popstate';
  295. }
  296. }
  297. /**
  298. * Parser from https://tools.ietf.org/html/rfc3986#appendix-B
  299. * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
  300. * 12 3 4 5 6 7 8 9
  301. *
  302. * Example: http://www.ics.uci.edu/pub/ietf/uri/#Related
  303. *
  304. * Results in:
  305. *
  306. * $1 = http:
  307. * $2 = http
  308. * $3 = //www.ics.uci.edu
  309. * $4 = www.ics.uci.edu
  310. * $5 = /pub/ietf/uri/
  311. * $6 = <undefined>
  312. * $7 = <undefined>
  313. * $8 = #Related
  314. * $9 = Related
  315. */
  316. const urlParse = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
  317. function parseUrl(urlStr, baseHref) {
  318. const verifyProtocol = /^((http[s]?|ftp):\/\/)/;
  319. let serverBase;
  320. // URL class requires full URL. If the URL string doesn't start with protocol, we need to add
  321. // an arbitrary base URL which can be removed afterward.
  322. if (!verifyProtocol.test(urlStr)) {
  323. serverBase = 'http://empty.com/';
  324. }
  325. let parsedUrl;
  326. try {
  327. parsedUrl = new URL(urlStr, serverBase);
  328. }
  329. catch (e) {
  330. const result = urlParse.exec(serverBase || '' + urlStr);
  331. if (!result) {
  332. throw new Error(`Invalid URL: ${urlStr} with base: ${baseHref}`);
  333. }
  334. const hostSplit = result[4].split(':');
  335. parsedUrl = {
  336. protocol: result[1],
  337. hostname: hostSplit[0],
  338. port: hostSplit[1] || '',
  339. pathname: result[5],
  340. search: result[6],
  341. hash: result[8],
  342. };
  343. }
  344. if (parsedUrl.pathname && parsedUrl.pathname.indexOf(baseHref) === 0) {
  345. parsedUrl.pathname = parsedUrl.pathname.substring(baseHref.length);
  346. }
  347. return {
  348. hostname: !serverBase && parsedUrl.hostname || '',
  349. protocol: !serverBase && parsedUrl.protocol || '',
  350. port: !serverBase && parsedUrl.port || '',
  351. pathname: parsedUrl.pathname || '/',
  352. search: parsedUrl.search || '',
  353. hash: parsedUrl.hash || '',
  354. };
  355. }
  356. /**
  357. * Provider for mock platform location config
  358. *
  359. * @publicApi
  360. */
  361. const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken('MOCK_PLATFORM_LOCATION_CONFIG');
  362. /**
  363. * Mock implementation of URL state.
  364. *
  365. * @publicApi
  366. */
  367. class MockPlatformLocation {
  368. constructor(config) {
  369. this.baseHref = '';
  370. this.hashUpdate = new Subject();
  371. this.popStateSubject = new Subject();
  372. this.urlChangeIndex = 0;
  373. this.urlChanges = [{ hostname: '', protocol: '', port: '', pathname: '/', search: '', hash: '', state: null }];
  374. if (config) {
  375. this.baseHref = config.appBaseHref || '';
  376. const parsedChanges = this.parseChanges(null, config.startUrl || 'http://_empty_/', this.baseHref);
  377. this.urlChanges[0] = { ...parsedChanges };
  378. }
  379. }
  380. get hostname() {
  381. return this.urlChanges[this.urlChangeIndex].hostname;
  382. }
  383. get protocol() {
  384. return this.urlChanges[this.urlChangeIndex].protocol;
  385. }
  386. get port() {
  387. return this.urlChanges[this.urlChangeIndex].port;
  388. }
  389. get pathname() {
  390. return this.urlChanges[this.urlChangeIndex].pathname;
  391. }
  392. get search() {
  393. return this.urlChanges[this.urlChangeIndex].search;
  394. }
  395. get hash() {
  396. return this.urlChanges[this.urlChangeIndex].hash;
  397. }
  398. get state() {
  399. return this.urlChanges[this.urlChangeIndex].state;
  400. }
  401. getBaseHrefFromDOM() {
  402. return this.baseHref;
  403. }
  404. onPopState(fn) {
  405. const subscription = this.popStateSubject.subscribe(fn);
  406. return () => subscription.unsubscribe();
  407. }
  408. onHashChange(fn) {
  409. const subscription = this.hashUpdate.subscribe(fn);
  410. return () => subscription.unsubscribe();
  411. }
  412. get href() {
  413. let url = `${this.protocol}//${this.hostname}${this.port ? ':' + this.port : ''}`;
  414. url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`;
  415. return url;
  416. }
  417. get url() {
  418. return `${this.pathname}${this.search}${this.hash}`;
  419. }
  420. parseChanges(state, url, baseHref = '') {
  421. // When the `history.state` value is stored, it is always copied.
  422. state = JSON.parse(JSON.stringify(state));
  423. return { ...parseUrl(url, baseHref), state };
  424. }
  425. replaceState(state, title, newUrl) {
  426. const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
  427. this.urlChanges[this.urlChangeIndex] =
  428. { ...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState };
  429. }
  430. pushState(state, title, newUrl) {
  431. const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
  432. if (this.urlChangeIndex > 0) {
  433. this.urlChanges.splice(this.urlChangeIndex + 1);
  434. }
  435. this.urlChanges.push({ ...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState });
  436. this.urlChangeIndex = this.urlChanges.length - 1;
  437. }
  438. forward() {
  439. const oldUrl = this.url;
  440. const oldHash = this.hash;
  441. if (this.urlChangeIndex < this.urlChanges.length) {
  442. this.urlChangeIndex++;
  443. }
  444. this.emitEvents(oldHash, oldUrl);
  445. }
  446. back() {
  447. const oldUrl = this.url;
  448. const oldHash = this.hash;
  449. if (this.urlChangeIndex > 0) {
  450. this.urlChangeIndex--;
  451. }
  452. this.emitEvents(oldHash, oldUrl);
  453. }
  454. historyGo(relativePosition = 0) {
  455. const oldUrl = this.url;
  456. const oldHash = this.hash;
  457. const nextPageIndex = this.urlChangeIndex + relativePosition;
  458. if (nextPageIndex >= 0 && nextPageIndex < this.urlChanges.length) {
  459. this.urlChangeIndex = nextPageIndex;
  460. }
  461. this.emitEvents(oldHash, oldUrl);
  462. }
  463. getState() {
  464. return this.state;
  465. }
  466. /**
  467. * Browsers are inconsistent in when they fire events and perform the state updates
  468. * The most easiest thing to do in our mock is synchronous and that happens to match
  469. * Firefox and Chrome, at least somewhat closely
  470. *
  471. * https://github.com/WICG/navigation-api#watching-for-navigations
  472. * https://docs.google.com/document/d/1Pdve-DJ1JCGilj9Yqf5HxRJyBKSel5owgOvUJqTauwU/edit#heading=h.3ye4v71wsz94
  473. * popstate is always sent before hashchange:
  474. * https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#when_popstate_is_sent
  475. */
  476. emitEvents(oldHash, oldUrl) {
  477. this.popStateSubject.next({ type: 'popstate', state: this.getState(), oldUrl, newUrl: this.url });
  478. if (oldHash !== this.hash) {
  479. this.hashUpdate.next({ type: 'hashchange', state: null, oldUrl, newUrl: this.url });
  480. }
  481. }
  482. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: MockPlatformLocation, deps: [{ token: MOCK_PLATFORM_LOCATION_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
  483. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: MockPlatformLocation }); }
  484. }
  485. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.9", ngImport: i0, type: MockPlatformLocation, decorators: [{
  486. type: Injectable
  487. }], ctorParameters: function () { return [{ type: undefined, decorators: [{
  488. type: Inject,
  489. args: [MOCK_PLATFORM_LOCATION_CONFIG]
  490. }, {
  491. type: Optional
  492. }] }]; } });
  493. /**
  494. * Returns mock providers for the `Location` and `LocationStrategy` classes.
  495. * The mocks are helpful in tests to fire simulated location events.
  496. *
  497. * @publicApi
  498. */
  499. function provideLocationMocks() {
  500. return [
  501. { provide: Location, useClass: SpyLocation },
  502. { provide: LocationStrategy, useClass: MockLocationStrategy },
  503. ];
  504. }
  505. /**
  506. * @module
  507. * @description
  508. * Entry point for all public APIs of the common/testing package.
  509. */
  510. /**
  511. * @module
  512. * @description
  513. * Entry point for all public APIs of this package.
  514. */
  515. // This file only reexports content of the `src` folder. Keep it that way.
  516. // This file is not used to build this module. It is only used during editing
  517. /**
  518. * Generated bundle index. Do not edit.
  519. */
  520. export { MOCK_PLATFORM_LOCATION_CONFIG, MockLocationStrategy, MockPlatformLocation, SpyLocation, provideLocationMocks };
  521. //# sourceMappingURL=testing.mjs.map