diff --git a/package-lock.json b/package-lock.json index a1ed79f..40f413f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3324,12 +3324,12 @@ } }, "node_modules/@xyflow/react": { - "version": "12.8.6", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.6.tgz", - "integrity": "sha512-SksAm2m4ySupjChphMmzvm55djtgMDPr+eovPDdTnyGvShf73cvydfoBfWDFllooIQ4IaiUL5yfxHRwU0c37EA==", + "version": "12.9.1", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.9.1.tgz", + "integrity": "sha512-JRPCT5p7NnPdVSIh15AFvUSSm+8GUyz2I6iuBEC1LG2lKgig/L48AM/ImMHCc3ZUCg+AgTOJDaX2fcRyPA9BTA==", "license": "MIT", "dependencies": { - "@xyflow/system": "0.0.70", + "@xyflow/system": "0.0.72", "classcat": "^5.0.3", "zustand": "^4.4.0" }, @@ -3367,9 +3367,9 @@ } }, "node_modules/@xyflow/system": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.70.tgz", - "integrity": "sha512-PpC//u9zxdjj0tfTSmZrg3+sRbTz6kop/Amky44U2Dl51sxzDTIUfXMwETOYpmr2dqICWXBIJwXL2a9QWtX2XA==", + "version": "0.0.72", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.72.tgz", + "integrity": "sha512-WBI5Aau0fXTXwxHPzceLNS6QdXggSWnGjDtj/gG669crApN8+SCmEtkBth1m7r6pStNo/5fI9McEi7Dk0ymCLA==", "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", @@ -7738,9 +7738,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/src/App.tsx b/src/App.tsx index 70fc815..75d423d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' import Robot from './pages/Robot/Robot.tsx'; +import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx' import VisProg from "./pages/VisProgPage/VisProg.tsx"; import {useState} from "react"; import Logging from "./components/Logging/Logging.tsx"; @@ -23,7 +24,8 @@ function App(){ } /> } /> } /> - + } /> + {showLogs && } diff --git a/src/pages/ConnectedRobots/ConnectedRobots.tsx b/src/pages/ConnectedRobots/ConnectedRobots.tsx new file mode 100644 index 0000000..b7ec65f --- /dev/null +++ b/src/pages/ConnectedRobots/ConnectedRobots.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react' + +export default function ConnectedRobots() { + + const [connected, setConnected] = useState(null); + + useEffect(() => { + // We're excepting a stream of data like that looks like this: `data = False` or `data = True` + const eventSource = new EventSource("http://localhost:8000/robot/ping_stream"); + eventSource.onmessage = (event) => { + + // Receive message and parse + console.log("received message:", event.data); + try { + const data = JSON.parse(event.data); + + // Set connected to value. + try { + setConnected(data) + } + catch { + console.log("couldnt extract connected from incoming ping data") + } + + } catch { + console.log("Ping message not in correct format:", event.data); + } + }; + return () => eventSource.close(); + }, []); + + return ( +
+

Is robot currently connected?

+
+

Robot is currently: {connected == null ? "checking..." : (connected ? "connected! 🟢" : "not connected... 🔴")}

+

+ {connected == null ? "If checking continues, make sure CB is properly loaded with robot at least once." : ""} +

+
+
+ ); +} diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 3a74d8e..c6afa92 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -14,6 +14,7 @@ function Home() { Robot Interaction → Editor → Template → + Connected Robots → ) diff --git a/test/pages/connectedRobots/ConnectedRobots.test.tsx b/test/pages/connectedRobots/ConnectedRobots.test.tsx new file mode 100644 index 0000000..017b2a2 --- /dev/null +++ b/test/pages/connectedRobots/ConnectedRobots.test.tsx @@ -0,0 +1,104 @@ +import { render, screen, act, cleanup, waitFor } from '@testing-library/react'; +import ConnectedRobots from '../../../src/pages/ConnectedRobots/ConnectedRobots'; + +// Mock event source +const mockInstances: MockEventSource[] = []; +class MockEventSource { + url: string; + onmessage: ((event: MessageEvent) => void) | null = null; + closed = false; + + constructor(url: string) { + this.url = url; + mockInstances.push(this); + } + + sendMessage(data: string) { + // Trigger whatever the component listens to + this.onmessage?.({ data } as MessageEvent); + } + + close() { + this.closed = true; + } +} + +// mock event source generation with fake function that returns our fake mock source +beforeAll(() => { + // Cast globalThis to a type exposing EventSource and assign a mocked constructor. + (globalThis as unknown as { EventSource?: typeof EventSource }).EventSource = + jest.fn((url: string) => new MockEventSource(url)) as unknown as typeof EventSource; +}); + +// clean after tests +afterEach(() => { + cleanup(); + jest.restoreAllMocks(); + mockInstances.length = 0; +}); + +describe('ConnectedRobots', () => { + test('renders initial state correctly', () => { + render(); + + // Check initial texts (before connection) + expect(screen.getByText('Is robot currently connected?')).toBeInTheDocument(); + expect(screen.getByText(/Robot is currently:\s*checking/i)).toBeInTheDocument(); + expect( + screen.getByText(/If checking continues, make sure CB is properly loaded/i) + ).toBeInTheDocument(); + }); + + test('updates to connected when message data is true', async () => { + render(); + const eventSource = mockInstances[0]; + expect(eventSource).toBeDefined(); + + // Check state after getting 'true' message + await act(async () => { + eventSource.sendMessage('true'); + }); + + await waitFor(() => { + expect(screen.getByText(/connected! 🟢/i)).toBeInTheDocument(); + }); + }); + + test('updates to not connected when message data is false', async () => { + render(); + const eventSource = mockInstances[0]; + + // Check statew after getting 'false' message + await act(async () => { + eventSource.sendMessage('false'); + }); + + await waitFor(() => { + expect(screen.getByText(/not connected.*🔴/i)).toBeInTheDocument(); + }); + }); + + test('handles invalid JSON gracefully', async () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + render(); + const eventSource = mockInstances[0]; + + await act(async () => { + eventSource.sendMessage('not-json'); + }); + + expect(logSpy).toHaveBeenCalledWith( + 'Ping message not in correct format:', + 'not-json' + ); + }); + + test('closes EventSource on unmount', () => { + render(); + const eventSource = mockInstances[0]; + const closeSpy = jest.spyOn(eventSource, 'close'); + cleanup(); + expect(closeSpy).toHaveBeenCalled(); + expect(eventSource.closed).toBe(true); + }); +}); \ No newline at end of file