diff --git a/.changeset/chubby-boats-melt.md b/.changeset/chubby-boats-melt.md new file mode 100644 index 0000000..abdefe7 --- /dev/null +++ b/.changeset/chubby-boats-melt.md @@ -0,0 +1,9 @@ +--- +"@tapsioss/client-socket-manager": minor +--- + +- [BREAKING CHANGE] The `devtool` field within the `ClientSocketManager`'s constructor `options` has been updated from a boolean to an object. This change allows for more granular control over development tools. + - Previously: `devtool: boolean` + - Now: `devtool: { enabled: boolean; zIndex?: number; }` + - The `enabled` property maintains the previous boolean functionality, while the optional `zIndex` property allows for specifying the stacking order of devtool elements. + diff --git a/packages/core/README.md b/packages/core/README.md index fde0365..73eec8f 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -213,11 +213,16 @@ and cleaned up. #### `showDevtool` ```ts -showDevtool(): void; +showDevtool(options): void; ``` Show devtool in the browser programmatically. +##### Parameters + +- `options`: Optional parameters. + - `zIndex`: Z-index of the devtool, overrides the previous z-index of the devtool. + #### `hideDevtool` ```ts diff --git a/packages/core/src/ClientSocketManager.test.ts b/packages/core/src/ClientSocketManager.test.ts index ed4582b..f6a38e5 100644 --- a/packages/core/src/ClientSocketManager.test.ts +++ b/packages/core/src/ClientSocketManager.test.ts @@ -299,7 +299,9 @@ describe("ClientSocketManager: unit tests", () => { socketManager.dispose(); socketManager = new ClientSocketManager(socketServerUri, { - devtool: true, + devtool: { + enabled: true, + }, }); expect(devtool.getDevtoolElement()).not.toBeNull(); }); @@ -311,7 +313,9 @@ describe("ClientSocketManager: unit tests", () => { const diposeResolver = createPromiseResolvers(); socketManager = new ClientSocketManager(socketServerUri, { - devtool: true, + devtool: { + enabled: true, + }, eventHandlers: { onSocketConnection() { connectResolver.resolve(); @@ -393,4 +397,148 @@ describe("ClientSocketManager: unit tests", () => { expect(socketManager.connected).toBe(false); expect(socketManager.disposed).toBe(true); }); + + it("should apply the specified z-index to the devtool and ensure correct click behavior", async () => { + const connectResolver = createPromiseResolvers(); + const zIndex = 20; + + // Create a div with a higher z-index to be on top of the devtool + const overlayDiv = document.createElement("div"); + + overlayDiv.style.position = "fixed"; + overlayDiv.style.top = "0"; + overlayDiv.style.left = "0"; + overlayDiv.style.width = "100%"; + overlayDiv.style.height = "100%"; + overlayDiv.style.backgroundColor = "rgba(0,0,0,0.1)"; // Semi-transparent + overlayDiv.style.zIndex = (zIndex + 1).toString(); // Higher z-index + overlayDiv.id = "overlay-div"; + document.body.appendChild(overlayDiv); + + // Track clicks on the overlay and the devtool + let overlayClicked = false; + let devtoolClicked = false; + + overlayDiv.addEventListener("click", () => { + overlayClicked = true; + }); + + socketManager = new ClientSocketManager(socketServerUri, { + devtool: { + enabled: true, + zIndex, + }, + eventHandlers: { + onSocketConnection() { + connectResolver.resolve(); + }, + }, + }); + + await connectResolver.promise; + + const devtoolElement = devtool.getDevtoolElement(); + + expect(devtoolElement).not.toBeNull(); + + // Add a click listener to the devtool itself to detect if it's reachable + devtoolElement?.addEventListener("click", () => { + devtoolClicked = true; + }); + + if (overlayDiv) { + const { left, top, width, height } = overlayDiv.getBoundingClientRect(); + const clientX = left + width / 2; + const clientY = top + height / 2; + + const clickEvent = new MouseEvent("click", { + bubbles: true, + cancelable: true, + clientX, + clientY, + }); + + overlayDiv.dispatchEvent(clickEvent); // Dispatch click on the overlay + } + + // Assert that only the overlay was clicked, indicating it was on top + expect(overlayClicked).toBe(true); + expect(devtoolClicked).toBe(false); // This should now pass if the overlay intercepted the click + + // Clean up the overlay div + document.body.removeChild(overlayDiv); + }); + + it("should capture click when devtool z-index is higher than overlay", async () => { + const connectResolver = createPromiseResolvers(); + const devtoolZIndex = 30; // Devtool will have a higher z-index + + // Create an overlay div with a lower z-index + const overlayDiv = document.createElement("div"); + + overlayDiv.style.position = "fixed"; + overlayDiv.style.top = "0"; + overlayDiv.style.left = "0"; + overlayDiv.style.width = "100%"; + overlayDiv.style.height = "100%"; + overlayDiv.style.backgroundColor = "rgba(0,0,0,0.1)"; // Semi-transparent + overlayDiv.style.zIndex = (devtoolZIndex - 1).toString(); // Lower z-index than devtool + overlayDiv.id = "lower-overlay-div"; + document.body.appendChild(overlayDiv); + + let overlayClicked = false; + let devtoolClicked = false; + + overlayDiv.addEventListener("click", () => { + overlayClicked = true; + }); + + socketManager = new ClientSocketManager(socketServerUri, { + devtool: { + enabled: true, + zIndex: devtoolZIndex, // Set devtool's z-index to be higher + }, + eventHandlers: { + onSocketConnection() { + connectResolver.resolve(); + }, + }, + }); + + await connectResolver.promise; + + const devtoolElement = devtool.getDevtoolElement(); + + expect(devtoolElement).not.toBeNull(); + + devtoolElement?.addEventListener("click", () => { + devtoolClicked = true; + }); + + // Simulate a click on the devtool. + // Since the devtool's z-index is higher, it should receive the click. + if (devtoolElement) { + const { left, top, width, height } = + devtoolElement.getBoundingClientRect(); + + const clientX = left + width / 2; + const clientY = top + height / 2; + + const clickEvent = new MouseEvent("click", { + bubbles: true, + cancelable: true, + clientX, + clientY, + }); + + devtoolElement.dispatchEvent(clickEvent); // Dispatch click directly on the devtool + } + + // Assert that the devtool was clicked, and the overlay was not + expect(devtoolClicked).toBe(true); + expect(overlayClicked).toBe(false); + + // Clean up the overlay div + document.body.removeChild(overlayDiv); + }); }); diff --git a/packages/core/src/ClientSocketManager.ts b/packages/core/src/ClientSocketManager.ts index 1df2b45..171ef45 100644 --- a/packages/core/src/ClientSocketManager.ts +++ b/packages/core/src/ClientSocketManager.ts @@ -28,10 +28,13 @@ class ClientSocketManager< reconnectionDelay = 500, reconnectionDelayMax = 2000, eventHandlers, - devtool: devtoolOpt = false, + devtool: devtoolOpt, ...restOptions } = options ?? {}; + const { enabled: devtoolEnabled = false, zIndex: devtoolZIndex = 999999 } = + devtoolOpt ?? {}; + try { this._socket = io(uri, { ...restOptions, @@ -49,7 +52,8 @@ class ClientSocketManager< this._inputListeners.onInit?.call(this); - if (devtoolOpt) { + if (devtoolEnabled) { + devtool.setZIndex(devtoolZIndex); this.showDevtool(); } } catch (err) { diff --git a/packages/core/src/devtool/devtool.test.ts b/packages/core/src/devtool/devtool.test.ts index c9c159b..9182be2 100644 --- a/packages/core/src/devtool/devtool.test.ts +++ b/packages/core/src/devtool/devtool.test.ts @@ -10,6 +10,7 @@ import * as devtool from "./devtool.ts"; describe("Devtool", () => { beforeEach(() => { + devtool.setZIndex(99999); devtool.render({ force: true, action: s => { diff --git a/packages/core/src/devtool/devtool.ts b/packages/core/src/devtool/devtool.ts index 08ee506..84ac623 100644 --- a/packages/core/src/devtool/devtool.ts +++ b/packages/core/src/devtool/devtool.ts @@ -54,6 +54,7 @@ const devtool: DevtoolState = { let active = false; let expanded = false; +let zIndex: number = NaN; export const renderDivider = () => { return `