From e8bd545048826a9d90042aa589925341f9d183fd Mon Sep 17 00:00:00 2001 From: Konstantin Shabelko Date: Thu, 11 Sep 2025 14:55:20 -0400 Subject: [PATCH 1/6] Clean up BroadcastChannel resources to fix hanging tests --- lib/msal-browser/package.json | 2 +- .../test/app/PCANonBrowser.spec.ts | 35 ++++++++++ lib/msal-browser/test/utils/BridgeSetup.ts | 19 ++++- shared-configs/jest-config/setupGlobals.cjs | 70 ++++++++++++++++--- 4 files changed, 113 insertions(+), 13 deletions(-) diff --git a/lib/msal-browser/package.json b/lib/msal-browser/package.json index 600dd5af35..bc1c609c05 100644 --- a/lib/msal-browser/package.json +++ b/lib/msal-browser/package.json @@ -68,7 +68,7 @@ "clean:coverage": "rimraf ../../.nyc_output/*", "lint": "eslint src --ext .ts", "lint:fix": "npm run lint -- --fix", - "test": "jest --forceExit", + "test": "jest", "test:coverage": "jest --coverage", "test:coverage:only": "npm run clean:coverage && npm run test:coverage", "build:all": "cd ../.. && npm run build --workspace=@azure/msal-common --workspace=@azure/msal-browser", diff --git a/lib/msal-browser/test/app/PCANonBrowser.spec.ts b/lib/msal-browser/test/app/PCANonBrowser.spec.ts index 5e59e0e5bd..f7600d7803 100644 --- a/lib/msal-browser/test/app/PCANonBrowser.spec.ts +++ b/lib/msal-browser/test/app/PCANonBrowser.spec.ts @@ -20,6 +20,36 @@ import { AuthenticationResult } from "../../src/response/AuthenticationResult.js import { TestTimeUtils } from "msal-test-utils"; import { BrowserAuthErrorCodes } from "../../src/error/BrowserAuthError.js"; +// Set up BroadcastChannel tracking for Node environment +const { BroadcastChannel } = require('worker_threads'); +const activeBroadcastChannels = new Set(); + +class TrackedBroadcastChannel extends BroadcastChannel { + constructor(name: string) { + super(name); + activeBroadcastChannels.add(this); + } + + close() { + super.close(); + activeBroadcastChannels.delete(this); + } +} + +// Replace global BroadcastChannel with tracked version +(global as any).BroadcastChannel = TrackedBroadcastChannel; + +function cleanupAllBroadcastChannels() { + activeBroadcastChannels.forEach((channel: any) => { + try { + channel.close(); + } catch (error) { + // Ignore cleanup errors + } + }); + activeBroadcastChannels.clear(); +} + /** * Tests for PublicClientApplication.ts when run in a non-browser environment * @@ -32,6 +62,11 @@ import { BrowserAuthErrorCodes } from "../../src/error/BrowserAuthError.js"; */ describe("Non-browser environment", () => { + afterEach(() => { + // Clean up all BroadcastChannel instances after each test + cleanupAllBroadcastChannels(); + }); + it("Constructor doesnt throw if window is undefined", () => { new PublicClientApplication({ auth: { diff --git a/lib/msal-browser/test/utils/BridgeSetup.ts b/lib/msal-browser/test/utils/BridgeSetup.ts index a68a66e66f..15b5d240c7 100644 --- a/lib/msal-browser/test/utils/BridgeSetup.ts +++ b/lib/msal-browser/test/utils/BridgeSetup.ts @@ -23,4 +23,21 @@ beforeAll(() => { window.nestedAppAuthBridge = new MockBridge(); } }); -afterAll(() => {}); + +afterEach(() => { + // Clean up BroadcastChannels after each test + if ((global as any).forceCleanupMsalBroadcastChannels) { + (global as any).forceCleanupMsalBroadcastChannels(); + } else if ((global as any).cleanupMsalBroadcastChannels) { + (global as any).cleanupMsalBroadcastChannels(); + } +}); + +afterAll(() => { + // Final aggressive cleanup + if ((global as any).forceCleanupMsalBroadcastChannels) { + (global as any).forceCleanupMsalBroadcastChannels(); + } else if ((global as any).cleanupMsalBroadcastChannels) { + (global as any).cleanupMsalBroadcastChannels(); + } +}); diff --git a/shared-configs/jest-config/setupGlobals.cjs b/shared-configs/jest-config/setupGlobals.cjs index a4a36f9ed6..bcb552364a 100644 --- a/shared-configs/jest-config/setupGlobals.cjs +++ b/shared-configs/jest-config/setupGlobals.cjs @@ -7,9 +7,57 @@ const crypto = require("crypto"); const { TextDecoder, TextEncoder } = require("util"); const { BroadcastChannel, MessageChannel } = require("worker_threads"); +// Track active BroadcastChannel instances for cleanup +const activeBroadcastChannels = new Set(); + +// Store the original BroadcastChannel +const OriginalBroadcastChannel = BroadcastChannel; + +// Create a wrapped BroadcastChannel that tracks instances +class TrackedBroadcastChannel extends OriginalBroadcastChannel { + constructor(name) { + super(name); + activeBroadcastChannels.add(this); + } + + close() { + super.close(); + activeBroadcastChannels.delete(this); + } +} + +// Add cleanup function to global +global.cleanupMsalBroadcastChannels = function () { + activeBroadcastChannels.forEach((channel) => { + try { + channel.close(); + } catch (error) { + // Ignore cleanup errors + } + }); + activeBroadcastChannels.clear(); +}; + +// Enhanced cleanup function that also cleans up listeners +global.forceCleanupMsalBroadcastChannels = function () { + // First try graceful cleanup + global.cleanupMsalBroadcastChannels(); + + // Add more aggressive cleanup if needed + if (typeof window !== "undefined" && window.BroadcastChannel) { + // Reset to original BroadcastChannel to prevent further leaks + window.BroadcastChannel = OriginalBroadcastChannel; + // Then restore tracked version + window.BroadcastChannel = TrackedBroadcastChannel; + } + + // Clear any remaining instances in case some weren't tracked + activeBroadcastChannels.clear(); +}; + try { Object?.defineProperties(global.self, { - "crypto": { + crypto: { value: { subtle: crypto.webcrypto.subtle, getRandomValues(dataBuffer) { @@ -18,20 +66,20 @@ try { randomUUID() { return crypto.randomUUID(); }, - } + }, }, - "TextDecoder": { - value: TextDecoder + TextDecoder: { + value: TextDecoder, }, - "TextEncoder": { - value: TextEncoder + TextEncoder: { + value: TextEncoder, }, - "BroadcastChannel": { - value: BroadcastChannel + BroadcastChannel: { + value: TrackedBroadcastChannel, + }, + MessageChannel: { + value: MessageChannel, }, - "MessageChannel": { - value: MessageChannel - } }); } catch (e) { // catch silently for non-browser tests From bdce8d8fd58b90a3204a6635ecdbd95c9029f10a Mon Sep 17 00:00:00 2001 From: Konstantin Shabelko Date: Thu, 11 Sep 2025 15:00:32 -0400 Subject: [PATCH 2/6] Change files --- ...-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json diff --git a/change/@azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json b/change/@azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json new file mode 100644 index 0000000000..f321a6fca5 --- /dev/null +++ b/change/@azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Clean up BroadcastChannel resources to fix hanging tests #8044", + "packageName": "@azure/msal-browser", + "email": "kshabelko@microsoft.com", + "dependentChangeType": "patch" +} From a392985e371eb5e968e1c3bd13fe8ed564f2b713 Mon Sep 17 00:00:00 2001 From: Konstantin Shabelko Date: Thu, 11 Sep 2025 15:15:39 -0400 Subject: [PATCH 3/6] Address comments and minify changes --- .../jest-config/BroadcastChannelTracker.js | 94 +++++++++++++++++++ shared-configs/jest-config/setupGlobals.cjs | 60 +++--------- 2 files changed, 106 insertions(+), 48 deletions(-) create mode 100644 shared-configs/jest-config/BroadcastChannelTracker.js diff --git a/shared-configs/jest-config/BroadcastChannelTracker.js b/shared-configs/jest-config/BroadcastChannelTracker.js new file mode 100644 index 0000000000..1650d7a0ed --- /dev/null +++ b/shared-configs/jest-config/BroadcastChannelTracker.js @@ -0,0 +1,94 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Shared BroadcastChannel tracking utility for test environments. + * This module provides consistent tracking and cleanup functionality + * across different test environments (browser/jsdom and Node.js). + */ + +/** + * Creates a BroadcastChannel tracker for a given environment + * @param {Function} getBroadcastChannel - Function that returns the BroadcastChannel constructor for the environment + * @returns {Object} Object containing TrackedBroadcastChannel class and cleanup functions + */ +function createBroadcastChannelTracker(getBroadcastChannel) { + const activeBroadcastChannels = new Set(); + const OriginalBroadcastChannel = getBroadcastChannel(); + + class TrackedBroadcastChannel extends OriginalBroadcastChannel { + constructor(name) { + super(name); + activeBroadcastChannels.add(this); + } + + close() { + super.close(); + activeBroadcastChannels.delete(this); + } + } + + // Basic cleanup function + function cleanupBroadcastChannels() { + activeBroadcastChannels.forEach((channel) => { + try { + channel.close(); + } catch (error) { + // Ignore cleanup errors + } + }); + activeBroadcastChannels.clear(); + } + + // Enhanced cleanup function with additional reset logic + function forceCleanupBroadcastChannels() { + // First try graceful cleanup + cleanupBroadcastChannels(); + + // Add more aggressive cleanup if needed for browser environments + if (typeof window !== "undefined" && window.BroadcastChannel) { + // Reset to original BroadcastChannel to prevent further leaks + window.BroadcastChannel = OriginalBroadcastChannel; + // Then restore tracked version + window.BroadcastChannel = TrackedBroadcastChannel; + } + + // Clear any remaining instances in case some weren't tracked + activeBroadcastChannels.clear(); + } + + return { + TrackedBroadcastChannel, + cleanupBroadcastChannels, + forceCleanupBroadcastChannels, + OriginalBroadcastChannel + }; +} + +// Node.js-specific setup for @jest-environment node tests +// This automatically sets up BroadcastChannel tracking when this module is imported +if (typeof window === "undefined" && typeof global !== "undefined") { + // Create BroadcastChannel tracker for Node.js environment + const { + TrackedBroadcastChannel, + cleanupBroadcastChannels + } = createBroadcastChannelTracker(() => { + const { BroadcastChannel } = require('worker_threads'); + return BroadcastChannel; + }); + + // Replace global BroadcastChannel with tracked version + global.BroadcastChannel = TrackedBroadcastChannel; + + // Add cleanup hooks for Node.js test environment + if (typeof afterEach !== "undefined" && typeof afterAll !== "undefined") { + afterEach(cleanupBroadcastChannels); + afterAll(cleanupBroadcastChannels); + } +} + +module.exports = { + createBroadcastChannelTracker +}; diff --git a/shared-configs/jest-config/setupGlobals.cjs b/shared-configs/jest-config/setupGlobals.cjs index bcb552364a..ea3cc1ceda 100644 --- a/shared-configs/jest-config/setupGlobals.cjs +++ b/shared-configs/jest-config/setupGlobals.cjs @@ -6,54 +6,18 @@ const crypto = require("crypto"); const { TextDecoder, TextEncoder } = require("util"); const { BroadcastChannel, MessageChannel } = require("worker_threads"); - -// Track active BroadcastChannel instances for cleanup -const activeBroadcastChannels = new Set(); - -// Store the original BroadcastChannel -const OriginalBroadcastChannel = BroadcastChannel; - -// Create a wrapped BroadcastChannel that tracks instances -class TrackedBroadcastChannel extends OriginalBroadcastChannel { - constructor(name) { - super(name); - activeBroadcastChannels.add(this); - } - - close() { - super.close(); - activeBroadcastChannels.delete(this); - } -} - -// Add cleanup function to global -global.cleanupMsalBroadcastChannels = function () { - activeBroadcastChannels.forEach((channel) => { - try { - channel.close(); - } catch (error) { - // Ignore cleanup errors - } - }); - activeBroadcastChannels.clear(); -}; - -// Enhanced cleanup function that also cleans up listeners -global.forceCleanupMsalBroadcastChannels = function () { - // First try graceful cleanup - global.cleanupMsalBroadcastChannels(); - - // Add more aggressive cleanup if needed - if (typeof window !== "undefined" && window.BroadcastChannel) { - // Reset to original BroadcastChannel to prevent further leaks - window.BroadcastChannel = OriginalBroadcastChannel; - // Then restore tracked version - window.BroadcastChannel = TrackedBroadcastChannel; - } - - // Clear any remaining instances in case some weren't tracked - activeBroadcastChannels.clear(); -}; +const { createBroadcastChannelTracker } = require("./BroadcastChannelTracker"); + +// Create BroadcastChannel tracker for browser/jsdom environment +const { + TrackedBroadcastChannel, + cleanupBroadcastChannels, + forceCleanupBroadcastChannels +} = createBroadcastChannelTracker(() => BroadcastChannel); + +// Add cleanup functions to global for use in test setup files +global.cleanupMsalBroadcastChannels = cleanupBroadcastChannels; +global.forceCleanupMsalBroadcastChannels = forceCleanupBroadcastChannels; try { Object?.defineProperties(global.self, { From 9202e67dac6e3be5615a59003f0223d331dd77d8 Mon Sep 17 00:00:00 2001 From: Konstantin Shabelko Date: Thu, 11 Sep 2025 15:20:01 -0400 Subject: [PATCH 4/6] - Fix formatting --- lib/msal-browser/test/app/PCANonBrowser.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-browser/test/app/PCANonBrowser.spec.ts b/lib/msal-browser/test/app/PCANonBrowser.spec.ts index f7600d7803..ba9b1a88dc 100644 --- a/lib/msal-browser/test/app/PCANonBrowser.spec.ts +++ b/lib/msal-browser/test/app/PCANonBrowser.spec.ts @@ -21,7 +21,7 @@ import { TestTimeUtils } from "msal-test-utils"; import { BrowserAuthErrorCodes } from "../../src/error/BrowserAuthError.js"; // Set up BroadcastChannel tracking for Node environment -const { BroadcastChannel } = require('worker_threads'); +const { BroadcastChannel } = require("worker_threads"); const activeBroadcastChannels = new Set(); class TrackedBroadcastChannel extends BroadcastChannel { From cc2efe19fe914e462c9b02ebbbf4fa80d1c38b7b Mon Sep 17 00:00:00 2001 From: Konstantin Shabelko Date: Thu, 11 Sep 2025 17:32:55 -0400 Subject: [PATCH 5/6] - Update change file --- ...azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change/@azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json b/change/@azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json index f321a6fca5..a424e7c7cd 100644 --- a/change/@azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json +++ b/change/@azure-msal-browser-cc7b4b74-d9b1-4614-ab89-ac8dfb792a3f.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Clean up BroadcastChannel resources to fix hanging tests #8044", + "comment": "Clean up BroadcastChannel resources to fix hanging tests #8045", "packageName": "@azure/msal-browser", "email": "kshabelko@microsoft.com", "dependentChangeType": "patch" From 483d9b2563691c0ff66a4dae1674380542227b3d Mon Sep 17 00:00:00 2001 From: Konstantin Shabelko Date: Thu, 11 Sep 2025 17:37:59 -0400 Subject: [PATCH 6/6] - Remove redundant code --- .../test/app/PCANonBrowser.spec.ts | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/lib/msal-browser/test/app/PCANonBrowser.spec.ts b/lib/msal-browser/test/app/PCANonBrowser.spec.ts index ba9b1a88dc..5e59e0e5bd 100644 --- a/lib/msal-browser/test/app/PCANonBrowser.spec.ts +++ b/lib/msal-browser/test/app/PCANonBrowser.spec.ts @@ -20,36 +20,6 @@ import { AuthenticationResult } from "../../src/response/AuthenticationResult.js import { TestTimeUtils } from "msal-test-utils"; import { BrowserAuthErrorCodes } from "../../src/error/BrowserAuthError.js"; -// Set up BroadcastChannel tracking for Node environment -const { BroadcastChannel } = require("worker_threads"); -const activeBroadcastChannels = new Set(); - -class TrackedBroadcastChannel extends BroadcastChannel { - constructor(name: string) { - super(name); - activeBroadcastChannels.add(this); - } - - close() { - super.close(); - activeBroadcastChannels.delete(this); - } -} - -// Replace global BroadcastChannel with tracked version -(global as any).BroadcastChannel = TrackedBroadcastChannel; - -function cleanupAllBroadcastChannels() { - activeBroadcastChannels.forEach((channel: any) => { - try { - channel.close(); - } catch (error) { - // Ignore cleanup errors - } - }); - activeBroadcastChannels.clear(); -} - /** * Tests for PublicClientApplication.ts when run in a non-browser environment * @@ -62,11 +32,6 @@ function cleanupAllBroadcastChannels() { */ describe("Non-browser environment", () => { - afterEach(() => { - // Clean up all BroadcastChannel instances after each test - cleanupAllBroadcastChannels(); - }); - it("Constructor doesnt throw if window is undefined", () => { new PublicClientApplication({ auth: {