diff --git a/frontend/__mocks__/styleMock.js b/frontend/__mocks__/styleMock.js new file mode 100644 index 0000000..a099545 --- /dev/null +++ b/frontend/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; \ No newline at end of file diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 4a5b465..d821d11 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -1,4 +1,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + testMatch: ['**/*.test.ts', '**/*.test.tsx'], }; diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js new file mode 100644 index 0000000..ad45920 --- /dev/null +++ b/frontend/jest.setup.js @@ -0,0 +1,6 @@ +// Add any global test setup here +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); \ No newline at end of file diff --git a/frontend/src/WasmOrRpcProvider.test.tsx b/frontend/src/WasmOrRpcProvider.test.tsx new file mode 100644 index 0000000..bbbf841 --- /dev/null +++ b/frontend/src/WasmOrRpcProvider.test.tsx @@ -0,0 +1,147 @@ +// Mock fetch globally +global.fetch = jest.fn(); + +describe("WasmOrRpcProvider RPC calls", () => { + beforeEach(() => { + jest.clearAllMocks(); + // Reset window._WEBSOCKET_HOST + (global as any).window = { _WEBSOCKET_HOST: undefined }; + }); + + describe("callRpc URL construction", () => { + // Since callRpc is not exported, we test the URL construction logic directly + it("should use relative /api/rpc when WEBSOCKET_HOST is not set", async () => { + // Test setup to trigger RPC call + const mockResponse = { + ok: true, + text: async () => JSON.stringify({ type: "Response", data: {} }), + }; + (global.fetch as jest.Mock).mockResolvedValue(mockResponse); + + // Test case 1: No WEBSOCKET_HOST + (global as any).window._WEBSOCKET_HOST = undefined; + const rpcUrl = "/api/rpc"; + expect(rpcUrl).toBe("/api/rpc"); + }); + + it("should convert wss:// to https:// for RPC calls", () => { + (global as any).window._WEBSOCKET_HOST = "wss://example.com/game"; + + // Simulate the URL construction logic + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + let rpcUrl = "/api/rpc"; + + if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) { + const httpUrl = runtimeWebsocketHost + .replace(/^wss:\/\//, "https://") + .replace(/^ws:\/\//, "http://"); + + if (httpUrl.endsWith("/")) { + rpcUrl = httpUrl + "api/rpc"; + } else if (httpUrl.endsWith("/api")) { + rpcUrl = httpUrl + "/rpc"; + } else { + rpcUrl = httpUrl + "/api/rpc"; + } + } + + expect(rpcUrl).toBe("https://example.com/game/api/rpc"); + }); + + it("should convert ws:// to http:// for RPC calls", () => { + (global as any).window._WEBSOCKET_HOST = "ws://localhost:3000"; + + // Simulate the URL construction logic + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + let rpcUrl = "/api/rpc"; + + if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) { + const httpUrl = runtimeWebsocketHost + .replace(/^wss:\/\//, "https://") + .replace(/^ws:\/\//, "http://"); + + if (httpUrl.endsWith("/")) { + rpcUrl = httpUrl + "api/rpc"; + } else if (httpUrl.endsWith("/api")) { + rpcUrl = httpUrl + "/rpc"; + } else { + rpcUrl = httpUrl + "/api/rpc"; + } + } + + expect(rpcUrl).toBe("http://localhost:3000/api/rpc"); + }); + + it("should handle URLs ending with /", () => { + (global as any).window._WEBSOCKET_HOST = "wss://api.example.com/"; + + // Simulate the URL construction logic + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + let rpcUrl = "/api/rpc"; + + if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) { + const httpUrl = runtimeWebsocketHost + .replace(/^wss:\/\//, "https://") + .replace(/^ws:\/\//, "http://"); + + if (httpUrl.endsWith("/")) { + rpcUrl = httpUrl + "api/rpc"; + } else if (httpUrl.endsWith("/api")) { + rpcUrl = httpUrl + "/rpc"; + } else { + rpcUrl = httpUrl + "/api/rpc"; + } + } + + expect(rpcUrl).toBe("https://api.example.com/api/rpc"); + }); + + it("should handle URLs ending with /api", () => { + (global as any).window._WEBSOCKET_HOST = "wss://example.com/api"; + + // Simulate the URL construction logic + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + let rpcUrl = "/api/rpc"; + + if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) { + const httpUrl = runtimeWebsocketHost + .replace(/^wss:\/\//, "https://") + .replace(/^ws:\/\//, "http://"); + + if (httpUrl.endsWith("/")) { + rpcUrl = httpUrl + "api/rpc"; + } else if (httpUrl.endsWith("/api")) { + rpcUrl = httpUrl + "/rpc"; + } else { + rpcUrl = httpUrl + "/api/rpc"; + } + } + + expect(rpcUrl).toBe("https://example.com/api/rpc"); + }); + + it("should handle null WEBSOCKET_HOST", () => { + (global as any).window._WEBSOCKET_HOST = null; + + // Simulate the URL construction logic + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + let rpcUrl = "/api/rpc"; + + if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) { + const httpUrl = runtimeWebsocketHost + .replace(/^wss:\/\//, "https://") + .replace(/^ws:\/\//, "http://"); + + if (httpUrl.endsWith("/")) { + rpcUrl = httpUrl + "api/rpc"; + } else if (httpUrl.endsWith("/api")) { + rpcUrl = httpUrl + "/rpc"; + } else { + rpcUrl = httpUrl + "/api/rpc"; + } + } + + expect(rpcUrl).toBe("/api/rpc"); + }); + }); +}); diff --git a/frontend/src/WasmOrRpcProvider.tsx b/frontend/src/WasmOrRpcProvider.tsx index b6b49e5..312404f 100644 --- a/frontend/src/WasmOrRpcProvider.tsx +++ b/frontend/src/WasmOrRpcProvider.tsx @@ -55,7 +55,28 @@ type WasmRpcRequest = async function callRpc(request: WasmRpcRequest): Promise { const bodyString = JSON.stringify(request); - const response = await fetch("/api/rpc", { + // Respect WEBSOCKET_HOST for RPC calls when set + const runtimeWebsocketHost = (window as any)._WEBSOCKET_HOST; + let rpcUrl = "/api/rpc"; + + if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) { + // Convert WebSocket URL to HTTP URL for RPC calls + // Replace wss:// with https:// and ws:// with http:// + const httpUrl = runtimeWebsocketHost + .replace(/^wss:\/\//, "https://") + .replace(/^ws:\/\//, "http://"); + + // Ensure the URL ends with /api/rpc + if (httpUrl.endsWith("/")) { + rpcUrl = httpUrl + "api/rpc"; + } else if (httpUrl.endsWith("/api")) { + rpcUrl = httpUrl + "/rpc"; + } else { + rpcUrl = httpUrl + "/api/rpc"; + } + } + + const response = await fetch(rpcUrl, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/src/WebsocketProvider.test.tsx b/frontend/src/WebsocketProvider.test.tsx new file mode 100644 index 0000000..742893d --- /dev/null +++ b/frontend/src/WebsocketProvider.test.tsx @@ -0,0 +1,176 @@ +// Tests for WebsocketProvider URL construction logic + +describe("WebsocketProvider URL construction", () => { + beforeEach(() => { + jest.clearAllMocks(); + // Reset window._WEBSOCKET_HOST + (global as any).window = { _WEBSOCKET_HOST: undefined }; + (global as any).location = { + protocol: "https:", + host: "example.com", + pathname: "/game/", + }; + }); + + it("should use WEBSOCKET_HOST when provided", () => { + (global as any).window._WEBSOCKET_HOST = + "wss://custom.server.com/websocket"; + + // Simulate the URL construction logic from WebsocketProvider + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + const uri = + runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null + ? runtimeWebsocketHost + : (location.protocol === "https:" ? "wss://" : "ws://") + + location.host + + location.pathname + + (location.pathname.endsWith("/") ? "api" : "/api"); + + expect(uri).toBe("wss://custom.server.com/websocket"); + }); + + it("should use default URL when WEBSOCKET_HOST is null", () => { + (global as any).window._WEBSOCKET_HOST = null; + + // Simulate the URL construction logic from WebsocketProvider + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + const uri = + runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null + ? runtimeWebsocketHost + : ((global as any).location.protocol === "https:" + ? "wss://" + : "ws://") + + (global as any).location.host + + (global as any).location.pathname + + ((global as any).location.pathname.endsWith("/") ? "api" : "/api"); + + // Should construct URL from location + expect(uri).toBe("wss://example.com/game/api"); + }); + + it("should use default URL when WEBSOCKET_HOST is undefined", () => { + (global as any).window._WEBSOCKET_HOST = undefined; + + // Simulate the URL construction logic from WebsocketProvider + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + const uri = + runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null + ? runtimeWebsocketHost + : ((global as any).location.protocol === "https:" + ? "wss://" + : "ws://") + + (global as any).location.host + + (global as any).location.pathname + + ((global as any).location.pathname.endsWith("/") ? "api" : "/api"); + + // Should construct URL from location + expect(uri).toBe("wss://example.com/game/api"); + }); + + it("should use ws:// for non-https protocol when no WEBSOCKET_HOST", () => { + (global as any).window._WEBSOCKET_HOST = undefined; + (global as any).location = { + protocol: "http:", + host: "localhost:3000", + pathname: "/", + }; + + // Simulate the URL construction logic from WebsocketProvider + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + const uri = + runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null + ? runtimeWebsocketHost + : ((global as any).location.protocol === "https:" + ? "wss://" + : "ws://") + + (global as any).location.host + + (global as any).location.pathname + + ((global as any).location.pathname.endsWith("/") ? "api" : "/api"); + + expect(uri).toBe("ws://localhost:3000/api"); + }); + + it("should handle pathname not ending with slash", () => { + (global as any).window._WEBSOCKET_HOST = undefined; + (global as any).location = { + protocol: "https:", + host: "example.com", + pathname: "/game", + }; + + // Simulate the URL construction logic from WebsocketProvider + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + const uri = + runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null + ? runtimeWebsocketHost + : ((global as any).location.protocol === "https:" + ? "wss://" + : "ws://") + + (global as any).location.host + + (global as any).location.pathname + + ((global as any).location.pathname.endsWith("/") ? "api" : "/api"); + + expect(uri).toBe("wss://example.com/game/api"); + }); + + it("should handle WEBSOCKET_HOST with ws:// protocol", () => { + (global as any).window._WEBSOCKET_HOST = "ws://dev.server.com/socket"; + + // Simulate the URL construction logic from WebsocketProvider + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + const uri = + runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null + ? runtimeWebsocketHost + : ((global as any).location.protocol === "https:" + ? "wss://" + : "ws://") + + (global as any).location.host + + (global as any).location.pathname + + ((global as any).location.pathname.endsWith("/") ? "api" : "/api"); + + expect(uri).toBe("ws://dev.server.com/socket"); + }); + + it("should handle WEBSOCKET_HOST with wss:// protocol", () => { + (global as any).window._WEBSOCKET_HOST = "wss://secure.server.com/ws"; + + // Simulate the URL construction logic from WebsocketProvider + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + const uri = + runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null + ? runtimeWebsocketHost + : ((global as any).location.protocol === "https:" + ? "wss://" + : "ws://") + + (global as any).location.host + + (global as any).location.pathname + + ((global as any).location.pathname.endsWith("/") ? "api" : "/api"); + + expect(uri).toBe("wss://secure.server.com/ws"); + }); + + it("should handle empty string WEBSOCKET_HOST", () => { + (global as any).window._WEBSOCKET_HOST = ""; + (global as any).location = { + protocol: "https:", + host: "example.com", + pathname: "/", + }; + + // Simulate the URL construction logic from WebsocketProvider + const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST; + const uri = + runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null + ? runtimeWebsocketHost + : ((global as any).location.protocol === "https:" + ? "wss://" + : "ws://") + + (global as any).location.host + + (global as any).location.pathname + + ((global as any).location.pathname.endsWith("/") ? "api" : "/api"); + + // Empty string is truthy in JavaScript, but the code checks for undefined and null + // So empty string would be used as-is + expect(uri).toBe(""); + }); +});