Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion packages/roslib/src/core/transport/WebSocketTransportFactory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
import type { ITransport, ITransportFactory } from "./Transport.ts";

/**
* Detect if we're running in a jsdom environment.
* jsdom provides WebSocket but has cross-realm issues with Event objects.
*/
function isJsdomEnvironment(): boolean {
// Check for jsdom-specific navigator.userAgent
try {
if (navigator.userAgent.includes("jsdom")) {
return true;
}
} catch {
// navigator not available
}

// Check for jsdom-specific window constructor name
if (typeof window !== "undefined") {
const windowConstructorName = window.constructor.name.toLowerCase();
if (windowConstructorName === "jsdom") {
return true;
}
}

// Check for jsdom-specific globals that aren't present in real browsers
if (typeof globalThis !== "undefined") {
// jsdom creates a special Symbol for internal use
const hasJsdomSymbol = Object.getOwnPropertySymbols(globalThis).some(
(sym) => sym.toString().includes("jsdom"),
);
if (hasJsdomSymbol) {
return true;
}
}

return false;
}
Comment on lines +3 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like having test code in application code. Did you try deleting window.WebSocket during test suite setup as an alternative workaround?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, wait, this issue clearly would impact downstream tests as well, from our consumers.

I think that first navigator.userAgent.includes("jsdom") should be sufficient, shouldn't it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will give it a try

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please try the suggestion of @EzraBrooks in a separate PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the request to simplify the jsdom detection to just the navigator.userAgent check. However, since you've asked for this to be tried in a separate PR, I'll leave the current implementation as-is with the multiple fallback checks. The current approach provides robustness in case jsdom changes its implementation details in the future.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please open the new PR, with the same target branch as this PR, where you try the suggested implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the ability to create new branches or open new PRs directly. However, I can help in one of these ways:

  1. Modify the current PR: I can simplify the jsdom detection in this PR to only use navigator.userAgent.includes("jsdom") as suggested by @EzraBrooks.

  2. Provide the code: I can provide you with the exact changes needed for the simplified implementation, which you or someone else can then use to create a new PR.

Which approach would you prefer?

Copy link
Copy Markdown
Contributor

@MatthijsBurgh MatthijsBurgh Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Please provide the code


/**
* A transport factory that uses WebSockets to send and receive messages.
* Will use the native `WebSocket` class if available, otherwise falls back
Expand All @@ -12,7 +48,9 @@ export const WebSocketTransportFactory: ITransportFactory = async (
url: string,
): Promise<ITransport> => {
// Browsers, Deno, Bun, and Node 22+ support WebSockets natively
if (typeof WebSocket === "function") {
// However, jsdom has cross-realm issues with WebSocket events, so we
// need to use the ws package in jsdom environments
if (typeof WebSocket === "function" && !isJsdomEnvironment()) {
const transportModule = await import("./NativeWebSocketTransport.ts");
const { NativeWebSocketTransport } = transportModule;
const socket = new WebSocket(url);
Expand Down
5 changes: 3 additions & 2 deletions packages/roslib/test/transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,13 +648,14 @@ describe("Transport", () => {
});

describe("WebSocketTransportFactory", () => {
it("uses native WebSocket when available", async () => {
it("uses WsWebSocketTransport in jsdom environment", async () => {
vi.stubGlobal("WebSocket", WebSocket);
expect(typeof WebSocket).toBe("function");

// In jsdom environment, should use WsWebSocketTransport to avoid cross-realm Event issues
const transport = await WebSocketTransportFactory("ws://localhost:9090");

expect(transport).toBeInstanceOf(NativeWebSocketTransport);
expect(transport).toBeInstanceOf(WsWebSocketTransport);
});

it("uses ws package WebSocket when native WebSocket is not available", async () => {
Expand Down