Skip to content

Commit 5651239

Browse files
amondnetthreepointone
authored andcommitted
fix(partysocket): add React Native environment detection for dispatchEvent
React Native/Expo environments have both `process` and `document` polyfilled, but not `process.versions.node`. This caused the library to incorrectly use browser-style event cloning, which produces events that fail `instanceof Event` checks in event-target-polyfill. This fix adds explicit React Native detection using the standard `navigator.product === "ReactNative"` check, and uses Node-style event cloning which creates proper Event instances. Fixes #257
1 parent 2a2928b commit 5651239

File tree

2 files changed

+109
-1
lines changed

2 files changed

+109
-1
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*
4+
* Tests for React Native environment detection and event cloning
5+
* See: https://github.com/cloudflare/partykit/issues/257
6+
*/
7+
8+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
9+
10+
describe("React Native environment detection", () => {
11+
const originalNavigator = globalThis.navigator;
12+
13+
beforeEach(() => {
14+
vi.resetModules();
15+
});
16+
17+
afterEach(() => {
18+
// Restore original navigator
19+
Object.defineProperty(globalThis, "navigator", {
20+
value: originalNavigator,
21+
writable: true,
22+
configurable: true
23+
});
24+
});
25+
26+
test("detects React Native environment via navigator.product", async () => {
27+
// Mock React Native environment
28+
Object.defineProperty(globalThis, "navigator", {
29+
value: { product: "ReactNative" },
30+
writable: true,
31+
configurable: true
32+
});
33+
34+
// Re-import the module to pick up the new navigator value
35+
const { default: ReconnectingWebSocket } = await import("../ws");
36+
37+
// The module should have been loaded with isReactNative = true
38+
// We verify this by checking that the class can be instantiated
39+
expect(ReconnectingWebSocket).toBeDefined();
40+
});
41+
42+
test("cloneEventNode creates valid Event instances", async () => {
43+
// Import the module in a standard environment first
44+
const wsModule = await import("../ws");
45+
46+
// Test that CloseEvent and ErrorEvent are proper Event subclasses
47+
const closeEvent = new wsModule.CloseEvent(1000, "test", {});
48+
expect(closeEvent).toBeInstanceOf(Event);
49+
expect(closeEvent.type).toBe("close");
50+
expect(closeEvent.code).toBe(1000);
51+
expect(closeEvent.reason).toBe("test");
52+
53+
const errorEvent = new wsModule.ErrorEvent(new Error("test error"), {});
54+
expect(errorEvent).toBeInstanceOf(Event);
55+
expect(errorEvent.type).toBe("error");
56+
expect(errorEvent.message).toBe("test error");
57+
});
58+
59+
test("event classes can be dispatched via EventTarget", async () => {
60+
const wsModule = await import("../ws");
61+
62+
const target = new EventTarget();
63+
let receivedEvent: Event | null = null;
64+
65+
target.addEventListener("close", (e) => {
66+
receivedEvent = e;
67+
});
68+
69+
const closeEvent = new wsModule.CloseEvent(1000, "normal closure", {});
70+
target.dispatchEvent(closeEvent);
71+
72+
expect(receivedEvent).not.toBeNull();
73+
expect(receivedEvent).toBeInstanceOf(Event);
74+
});
75+
});
76+
77+
describe("Event cloning for dispatchEvent", () => {
78+
test("cloned MessageEvent maintains data property", () => {
79+
const originalEvent = new MessageEvent("message", { data: "test data" });
80+
const clonedEvent = new MessageEvent(originalEvent.type, originalEvent);
81+
82+
expect(clonedEvent).toBeInstanceOf(Event);
83+
expect(clonedEvent).toBeInstanceOf(MessageEvent);
84+
expect(clonedEvent.data).toBe("test data");
85+
});
86+
87+
test("cloned Event can be dispatched", () => {
88+
const target = new EventTarget();
89+
let eventReceived = false;
90+
91+
target.addEventListener("open", () => {
92+
eventReceived = true;
93+
});
94+
95+
const originalEvent = new Event("open");
96+
const clonedEvent = new Event(originalEvent.type, originalEvent);
97+
98+
target.dispatchEvent(clonedEvent);
99+
expect(eventReceived).toBe(true);
100+
});
101+
});

packages/partysocket/src/ws.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,14 @@ const isNode =
9898
typeof process.versions?.node !== "undefined" &&
9999
typeof document === "undefined";
100100

101-
const cloneEvent = isNode ? cloneEventNode : cloneEventBrowser;
101+
// React Native has process and document polyfilled but not process.versions.node
102+
// It needs Node-style event cloning because browser-style cloning produces
103+
// events that fail instanceof Event checks in event-target-polyfill
104+
// See: https://github.com/cloudflare/partykit/issues/257
105+
const isReactNative =
106+
typeof navigator !== "undefined" && navigator.product === "ReactNative";
107+
108+
const cloneEvent = isNode || isReactNative ? cloneEventNode : cloneEventBrowser;
102109

103110
export type Options = {
104111
// biome-ignore lint/suspicious/noExplicitAny: legacy

0 commit comments

Comments
 (0)