From 3f02f79c53c5904494e7d8bc15ffab36971a7f26 Mon Sep 17 00:00:00 2001 From: Thomas Norling Date: Wed, 11 Oct 2023 15:22:03 -0700 Subject: [PATCH] Implement preconnect to speed up /token calls, refactor static class methods to exported methods for better minification (#6550) Adds a [preconnect](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preconnect) link element to the document header in flows where we're reasonably sure we will hit the /token endpoint (acquireTokenPopup, ssoSilent, acquireTokenSilent after RT expiration). This tells the browser to start resolving DNS and establishing the SSL connection so that when the /token call is made it doesn't need to wait for these steps to complete (can take up to 300ms). This is a short lived connection and the browser will kill it if not used promptly so I've included a cleanup function on a timer (10s) Also de-classed BrowserUtils for size reduction while I was adding a new function there anyway. --- ...-bbb9a122-00a3-40d3-8a8e-43e3c207732e.json | 7 + lib/msal-angular/src/msal.guard.spec.ts | 3 +- .../src/controllers/StandardController.ts | 2 +- lib/msal-browser/src/index.ts | 7 +- .../BaseInteractionClient.ts | 2 +- .../src/interaction_client/PopupClient.ts | 3 +- .../src/interaction_client/RedirectClient.ts | 2 +- .../interaction_client/SilentIframeClient.ts | 2 + .../StandardInteractionClient.ts | 2 +- lib/msal-browser/src/utils/BrowserUtils.ts | 258 +++++++++--------- .../test/app/PublicClientApplication.spec.ts | 12 +- .../interaction_client/RedirectClient.spec.ts | 2 +- .../test/utils/BrowserUtils.spec.ts | 44 +-- 13 files changed, 188 insertions(+), 158 deletions(-) create mode 100644 change/@azure-msal-browser-bbb9a122-00a3-40d3-8a8e-43e3c207732e.json diff --git a/change/@azure-msal-browser-bbb9a122-00a3-40d3-8a8e-43e3c207732e.json b/change/@azure-msal-browser-bbb9a122-00a3-40d3-8a8e-43e3c207732e.json new file mode 100644 index 0000000000..205ec8b98b --- /dev/null +++ b/change/@azure-msal-browser-bbb9a122-00a3-40d3-8a8e-43e3c207732e.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Preconnect to authority to speed up /token calls #6550", + "packageName": "@azure/msal-browser", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-angular/src/msal.guard.spec.ts b/lib/msal-angular/src/msal.guard.spec.ts index 76acf47b9a..23229f8530 100644 --- a/lib/msal-angular/src/msal.guard.spec.ts +++ b/lib/msal-angular/src/msal.guard.spec.ts @@ -5,7 +5,6 @@ import { RouterTestingModule } from '@angular/router/testing'; import { Location } from '@angular/common'; import { BrowserSystemOptions, - BrowserUtils, InteractionType, IPublicClientApplication, LogLevel, @@ -102,7 +101,7 @@ describe('MsalGuard', () => { it('returns false if page with MSAL Guard is set as redirectUri', (done) => { spyOn(UrlString, 'hashContainsKnownProperties').and.returnValue(true); - spyOn(BrowserUtils, 'isInIframe').and.returnValue(true); + spyOnProperty(window, 'parent', 'get').and.returnValue({ ...window }); guard.canActivate(routeMock, routeStateMock).subscribe((result) => { expect(result).toBeFalse(); diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index bd22b75150..f5cb045cd2 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -45,7 +45,7 @@ import { DEFAULT_REQUEST, BrowserConstants, } from "../utils/BrowserConstants"; -import { BrowserUtils } from "../utils/BrowserUtils"; +import * as BrowserUtils from "../utils/BrowserUtils"; import { RedirectRequest } from "../request/RedirectRequest"; import { PopupRequest } from "../request/PopupRequest"; import { SsoSilentRequest } from "../request/SsoSilentRequest"; diff --git a/lib/msal-browser/src/index.ts b/lib/msal-browser/src/index.ts index 767e5ee488..1c01dc019e 100644 --- a/lib/msal-browser/src/index.ts +++ b/lib/msal-browser/src/index.ts @@ -8,10 +8,8 @@ * @module @azure/msal-browser */ -/** - * Warning: This set of exports is purely intended to be used by other MSAL libraries, and should be considered potentially unstable. We strongly discourage using them directly, you do so at your own risk. - * Breaking changes to these APIs will be shipped under a minor version, instead of a major version. - */ +import * as BrowserUtils from "./utils/BrowserUtils"; +export { BrowserUtils }; export { PublicClientApplication } from "./app/PublicClientApplication"; export { IController } from "./controllers/IController"; @@ -32,7 +30,6 @@ export { ApiId, CacheLookupPolicy, } from "./utils/BrowserConstants"; -export { BrowserUtils } from "./utils/BrowserUtils"; /* * export { IController} from "./controllers/IController"; diff --git a/lib/msal-browser/src/interaction_client/BaseInteractionClient.ts b/lib/msal-browser/src/interaction_client/BaseInteractionClient.ts index e758521c30..94360b1370 100644 --- a/lib/msal-browser/src/interaction_client/BaseInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/BaseInteractionClient.ts @@ -32,7 +32,7 @@ import { PopupRequest } from "../request/PopupRequest"; import { SsoSilentRequest } from "../request/SsoSilentRequest"; import { version } from "../packageMetadata"; import { BrowserConstants } from "../utils/BrowserConstants"; -import { BrowserUtils } from "../utils/BrowserUtils"; +import * as BrowserUtils from "../utils/BrowserUtils"; import { INavigationClient } from "../navigation/INavigationClient"; import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler"; import { AuthenticationResult } from "../response/AuthenticationResult"; diff --git a/lib/msal-browser/src/interaction_client/PopupClient.ts b/lib/msal-browser/src/interaction_client/PopupClient.ts index 09b9496cde..3ecd56794b 100644 --- a/lib/msal-browser/src/interaction_client/PopupClient.ts +++ b/lib/msal-browser/src/interaction_client/PopupClient.ts @@ -30,7 +30,7 @@ import { } from "../utils/BrowserConstants"; import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest"; import { NavigationOptions } from "../navigation/NavigationOptions"; -import { BrowserUtils } from "../utils/BrowserUtils"; +import * as BrowserUtils from "../utils/BrowserUtils"; import { PopupRequest } from "../request/PopupRequest"; import { NativeInteractionClient } from "./NativeInteractionClient"; import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler"; @@ -211,6 +211,7 @@ export class PopupClient extends StandardInteractionClient { request, InteractionType.Popup ); + BrowserUtils.preconnect(validRequest.authority); this.browserStorage.updateCacheEntries( validRequest.state, validRequest.nonce, diff --git a/lib/msal-browser/src/interaction_client/RedirectClient.ts b/lib/msal-browser/src/interaction_client/RedirectClient.ts index a61a686fad..c5ff03f062 100644 --- a/lib/msal-browser/src/interaction_client/RedirectClient.ts +++ b/lib/msal-browser/src/interaction_client/RedirectClient.ts @@ -26,7 +26,7 @@ import { TemporaryCacheKeys, } from "../utils/BrowserConstants"; import { RedirectHandler } from "../interaction_handler/RedirectHandler"; -import { BrowserUtils } from "../utils/BrowserUtils"; +import * as BrowserUtils from "../utils/BrowserUtils"; import { EndSessionRequest } from "../request/EndSessionRequest"; import { EventType } from "../event/EventType"; import { NavigationOptions } from "../navigation/NavigationOptions"; diff --git a/lib/msal-browser/src/interaction_client/SilentIframeClient.ts b/lib/msal-browser/src/interaction_client/SilentIframeClient.ts index f89fc9a03a..1b565ca6e0 100644 --- a/lib/msal-browser/src/interaction_client/SilentIframeClient.ts +++ b/lib/msal-browser/src/interaction_client/SilentIframeClient.ts @@ -34,6 +34,7 @@ import { SsoSilentRequest } from "../request/SsoSilentRequest"; import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler"; import { NativeInteractionClient } from "./NativeInteractionClient"; import { AuthenticationResult } from "../response/AuthenticationResult"; +import * as BrowserUtils from "../utils/BrowserUtils"; export class SilentIframeClient extends StandardInteractionClient { protected apiId: ApiId; @@ -114,6 +115,7 @@ export class SilentIframeClient extends StandardInteractionClient { }, InteractionType.Silent ); + BrowserUtils.preconnect(silentRequest.authority); this.browserStorage.updateCacheEntries( silentRequest.state, silentRequest.nonce, diff --git a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts index 96709eb4b7..5b528f3139 100644 --- a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts @@ -37,7 +37,7 @@ import { BrowserStateObject, } from "../utils/BrowserProtocolUtils"; import { EndSessionRequest } from "../request/EndSessionRequest"; -import { BrowserUtils } from "../utils/BrowserUtils"; +import * as BrowserUtils from "../utils/BrowserUtils"; import { RedirectRequest } from "../request/RedirectRequest"; import { PopupRequest } from "../request/PopupRequest"; import { SsoSilentRequest } from "../request/SsoSilentRequest"; diff --git a/lib/msal-browser/src/utils/BrowserUtils.ts b/lib/msal-browser/src/utils/BrowserUtils.ts index e8fd024196..8832081ce7 100644 --- a/lib/msal-browser/src/utils/BrowserUtils.ts +++ b/lib/msal-browser/src/utils/BrowserUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Constants, UrlString } from "@azure/msal-common"; +import { UrlString } from "@azure/msal-common"; import { createBrowserAuthError, BrowserAuthErrorCodes, @@ -11,146 +11,154 @@ import { import { InteractionType, BrowserConstants } from "./BrowserConstants"; /** - * Utility class for browser specific functions + * Clears hash from window url. */ -export class BrowserUtils { - // #region Window Navigation and URL management - - /** - * Clears hash from window url. - */ - static clearHash(contentWindow: Window): void { - // Office.js sets history.replaceState to null - contentWindow.location.hash = Constants.EMPTY_STRING; - if (typeof contentWindow.history.replaceState === "function") { - // Full removes "#" from url - contentWindow.history.replaceState( - null, - Constants.EMPTY_STRING, - `${contentWindow.location.origin}${contentWindow.location.pathname}${contentWindow.location.search}` - ); - } +export function clearHash(contentWindow: Window): void { + // Office.js sets history.replaceState to null + contentWindow.location.hash = ""; + if (typeof contentWindow.history.replaceState === "function") { + // Full removes "#" from url + contentWindow.history.replaceState( + null, + "", + `${contentWindow.location.origin}${contentWindow.location.pathname}${contentWindow.location.search}` + ); } +} - /** - * Replaces current hash with hash from provided url - */ - static replaceHash(url: string): void { - const urlParts = url.split("#"); - urlParts.shift(); // Remove part before the hash - window.location.hash = - urlParts.length > 0 ? urlParts.join("#") : Constants.EMPTY_STRING; - } +/** + * Replaces current hash with hash from provided url + */ +export function replaceHash(url: string): void { + const urlParts = url.split("#"); + urlParts.shift(); // Remove part before the hash + window.location.hash = urlParts.length > 0 ? urlParts.join("#") : ""; +} - /** - * Returns boolean of whether the current window is in an iframe or not. - */ - static isInIframe(): boolean { - return window.parent !== window; - } +/** + * Returns boolean of whether the current window is in an iframe or not. + */ +export function isInIframe(): boolean { + return window.parent !== window; +} - /** - * Returns boolean of whether or not the current window is a popup opened by msal - */ - static isInPopup(): boolean { - return ( - typeof window !== "undefined" && - !!window.opener && - window.opener !== window && - typeof window.name === "string" && - window.name.indexOf(`${BrowserConstants.POPUP_NAME_PREFIX}.`) === 0 - ); - } +/** + * Returns boolean of whether or not the current window is a popup opened by msal + */ +export function isInPopup(): boolean { + return ( + typeof window !== "undefined" && + !!window.opener && + window.opener !== window && + typeof window.name === "string" && + window.name.indexOf(`${BrowserConstants.POPUP_NAME_PREFIX}.`) === 0 + ); +} - // #endregion +// #endregion - /** - * Returns current window URL as redirect uri - */ - static getCurrentUri(): string { - return window.location.href.split("?")[0].split("#")[0]; - } +/** + * Returns current window URL as redirect uri + */ +export function getCurrentUri(): string { + return window.location.href.split("?")[0].split("#")[0]; +} - /** - * Gets the homepage url for the current window location. - */ - static getHomepage(): string { - const currentUrl = new UrlString(window.location.href); - const urlComponents = currentUrl.getUrlComponents(); - return `${urlComponents.Protocol}//${urlComponents.HostNameAndPort}/`; - } +/** + * Gets the homepage url for the current window location. + */ +export function getHomepage(): string { + const currentUrl = new UrlString(window.location.href); + const urlComponents = currentUrl.getUrlComponents(); + return `${urlComponents.Protocol}//${urlComponents.HostNameAndPort}/`; +} - /** - * Throws error if we have completed an auth and are - * attempting another auth request inside an iframe. - */ - static blockReloadInHiddenIframes(): void { - const isResponseHash = UrlString.hashContainsKnownProperties( - window.location.hash - ); - // return an error if called from the hidden iframe created by the msal js silent calls - if (isResponseHash && BrowserUtils.isInIframe()) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.blockIframeReload - ); - } +/** + * Throws error if we have completed an auth and are + * attempting another auth request inside an iframe. + */ +export function blockReloadInHiddenIframes(): void { + const isResponseHash = UrlString.hashContainsKnownProperties( + window.location.hash + ); + // return an error if called from the hidden iframe created by the msal js silent calls + if (isResponseHash && isInIframe()) { + throw createBrowserAuthError(BrowserAuthErrorCodes.blockIframeReload); } +} - /** - * Block redirect operations in iframes unless explicitly allowed - * @param interactionType Interaction type for the request - * @param allowRedirectInIframe Config value to allow redirects when app is inside an iframe - */ - static blockRedirectInIframe( - interactionType: InteractionType, - allowRedirectInIframe: boolean - ): void { - const isIframedApp = BrowserUtils.isInIframe(); - if ( - interactionType === InteractionType.Redirect && - isIframedApp && - !allowRedirectInIframe - ) { - // If we are not in top frame, we shouldn't redirect. This is also handled by the service. - throw createBrowserAuthError( - BrowserAuthErrorCodes.redirectInIframe - ); - } +/** + * Block redirect operations in iframes unless explicitly allowed + * @param interactionType Interaction type for the request + * @param allowRedirectInIframe Config value to allow redirects when app is inside an iframe + */ +export function blockRedirectInIframe( + interactionType: InteractionType, + allowRedirectInIframe: boolean +): void { + const isIframedApp = isInIframe(); + if ( + interactionType === InteractionType.Redirect && + isIframedApp && + !allowRedirectInIframe + ) { + // If we are not in top frame, we shouldn't redirect. This is also handled by the service. + throw createBrowserAuthError(BrowserAuthErrorCodes.redirectInIframe); } +} - /** - * Block redirectUri loaded in popup from calling AcquireToken APIs - */ - static blockAcquireTokenInPopups(): void { - // Popups opened by msal popup APIs are given a name that starts with "msal." - if (BrowserUtils.isInPopup()) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.blockNestedPopups - ); - } +/** + * Block redirectUri loaded in popup from calling AcquireToken APIs + */ +export function blockAcquireTokenInPopups(): void { + // Popups opened by msal popup APIs are given a name that starts with "msal." + if (isInPopup()) { + throw createBrowserAuthError(BrowserAuthErrorCodes.blockNestedPopups); } +} - /** - * Throws error if token requests are made in non-browser environment - * @param isBrowserEnvironment Flag indicating if environment is a browser. - */ - static blockNonBrowserEnvironment(isBrowserEnvironment: boolean): void { - if (!isBrowserEnvironment) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.nonBrowserEnvironment - ); - } +/** + * Throws error if token requests are made in non-browser environment + * @param isBrowserEnvironment Flag indicating if environment is a browser. + */ +export function blockNonBrowserEnvironment( + isBrowserEnvironment: boolean +): void { + if (!isBrowserEnvironment) { + throw createBrowserAuthError( + BrowserAuthErrorCodes.nonBrowserEnvironment + ); } +} - /** - * Throws error if initialize hasn't been called - * @param initialized - */ - static blockAPICallsBeforeInitialize(initialized: boolean): void { - if (!initialized) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.uninitializedPublicClientApplication - ); - } +/** + * Throws error if initialize hasn't been called + * @param initialized + */ +export function blockAPICallsBeforeInitialize(initialized: boolean): void { + if (!initialized) { + throw createBrowserAuthError( + BrowserAuthErrorCodes.uninitializedPublicClientApplication + ); } } + +/** + * Adds a preconnect link element to the header which begins DNS resolution and SSL connection in anticipation of the /token request + * @param loginDomain Authority domain, including https protocol e.g. https://login.microsoftonline.com + * @returns + */ +export function preconnect(authority: string): void { + const link = document.createElement("link"); + link.rel = "preconnect"; + link.href = new URL(authority).origin; + link.crossOrigin = "anonymous"; + document.head.appendChild(link); + + // The browser will close connection if not used within a few seconds, remove element from the header after 10s + window.setTimeout(() => { + try { + document.head.removeChild(link); + } catch {} + }, 10000); // 10s Timeout +} diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 56a1e66dd8..318150535c 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -83,7 +83,7 @@ import { BrowserAuthErrorMessage, BrowserAuthErrorCodes, } from "../../src/error/BrowserAuthError"; -import { BrowserUtils } from "../../src/utils/BrowserUtils"; +import * as BrowserUtils from "../../src/utils/BrowserUtils"; import { RedirectClient } from "../../src/interaction_client/RedirectClient"; import { PopupClient } from "../../src/interaction_client/PopupClient"; import { SilentCacheClient } from "../../src/interaction_client/SilentCacheClient"; @@ -1339,7 +1339,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("throws an error if inside an iframe", async () => { - sinon.stub(BrowserUtils, "isInIframe").returns(true); + const mockParentWindow = { ...window }; + jest.spyOn(window, "parent", "get").mockReturnValue( + mockParentWindow + ); await expect( pca.acquireTokenRedirect({ scopes: [] }) ).rejects.toMatchObject( @@ -4769,7 +4772,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("throws an error if inside an iframe", async () => { - sinon.stub(BrowserUtils, "isInIframe").returns(true); + const mockParentWindow = { ...window }; + jest.spyOn(window, "parent", "get").mockReturnValue( + mockParentWindow + ); await expect(pca.logoutRedirect()).rejects.toMatchObject( createBrowserAuthError(BrowserAuthErrorCodes.redirectInIframe) ); diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index ca71bf4d69..af4b16311f 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -49,7 +49,7 @@ import { createClientConfigurationError, ClientConfigurationErrorCodes, } from "@azure/msal-common"; -import { BrowserUtils } from "../../src/utils/BrowserUtils"; +import * as BrowserUtils from "../../src/utils/BrowserUtils"; import { TemporaryCacheKeys, ApiId, diff --git a/lib/msal-browser/test/utils/BrowserUtils.spec.ts b/lib/msal-browser/test/utils/BrowserUtils.spec.ts index 926d64d8e0..e1adc3043f 100644 --- a/lib/msal-browser/test/utils/BrowserUtils.spec.ts +++ b/lib/msal-browser/test/utils/BrowserUtils.spec.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import sinon from "sinon"; -import { TEST_URIS } from "./StringConstants"; +import { TEST_CONFIG, TEST_URIS } from "./StringConstants"; import { BrowserUtils, BrowserAuthError, @@ -13,14 +12,10 @@ import { } from "../../src"; describe("BrowserUtils.ts Function Unit Tests", () => { - const oldWindow: Window & typeof globalThis = window; + const oldWindow = { ...window }; afterEach(() => { window = oldWindow; - //@ts-ignore - window.Headers = undefined; - //@ts-ignore - window.fetch = undefined; - sinon.restore(); + jest.restoreAllMocks(); }); it("clearHash() clears the window hash", () => { @@ -58,38 +53,39 @@ describe("BrowserUtils.ts Function Unit Tests", () => { }); it("isInIframe() returns false if window parent is the same as the current window", () => { - sinon.stub(window, "parent").value(window); + jest.spyOn(window, "parent", "get").mockReturnValue(window); expect(BrowserUtils.isInIframe()).toBe(false); }); it("isInIframe() returns true if window parent is not the same as the current window", () => { expect(BrowserUtils.isInIframe()).toBe(false); - sinon.stub(window, "parent").value(null); + // @ts-ignore + jest.spyOn(window, "parent", "get").mockReturnValue(null); expect(BrowserUtils.isInIframe()).toBe(true); }); it("isInPopup() returns false if window is undefined", () => { // @ts-ignore - window = undefined; + jest.spyOn(global, "window", "get").mockReturnValue(undefined); expect(BrowserUtils.isInPopup()).toBe(false); }); it("isInPopup() returns false if window opener is not the same as the current window but window name does not starts with 'msal.'", () => { window.opener = { ...window }; - sinon.stub(window, "name").value("non-msal-popup"); + window.name = "non-msal-popup"; expect(BrowserUtils.isInPopup()).toBe(false); }); it("isInPopup() returns false if window opener is the same as the current window", () => { window.opener = window; - sinon.stub(window, "name").value("msal."); + window.name = "msal."; expect(BrowserUtils.isInPopup()).toBe(false); }); it("isInPopup() returns true if window opener is not the same as the current window and the window name starts with 'msal.'", () => { expect(BrowserUtils.isInPopup()).toBe(false); window.opener = { ...window }; - sinon.stub(window, "name").value("msal.popupwindow"); + window.name = "msal.popupwindow"; expect(BrowserUtils.isInPopup()).toBe(true); }); @@ -99,7 +95,7 @@ describe("BrowserUtils.ts Function Unit Tests", () => { describe("blockRedirectInIframe", () => { it("throws when inside an iframe", (done) => { - sinon.stub(BrowserUtils, "isInIframe").returns(true); + jest.spyOn(window, "parent", "get").mockReturnValue({ ...window }); try { BrowserUtils.blockRedirectInIframe( InteractionType.Redirect, @@ -115,13 +111,27 @@ describe("BrowserUtils.ts Function Unit Tests", () => { }); it("doesnt throw when inside an iframe and redirects are allowed", () => { - sinon.stub(BrowserUtils, "isInIframe").returns(true); + jest.spyOn(window, "parent", "get").mockReturnValue({ ...window }); BrowserUtils.blockRedirectInIframe(InteractionType.Redirect, true); }); it("doesnt throw when not inside an iframe", () => { - sinon.stub(BrowserUtils, "isInIframe").returns(false); BrowserUtils.blockRedirectInIframe(InteractionType.Redirect, false); }); }); + + it("adds preconnect to header then removes after some time", () => { + jest.useFakeTimers(); + BrowserUtils.preconnect(TEST_CONFIG.validAuthority); + + const preconnectLink = document.querySelector("link"); + expect(preconnectLink).toBeTruthy(); + expect(preconnectLink?.getAttribute("rel")).toBe("preconnect"); + expect(preconnectLink?.getAttribute("href")).toBe( + new URL(TEST_CONFIG.validAuthority).origin + ); + + jest.runAllTimers(); + expect(document.querySelector("link")).toBeFalsy(); + }); });