Merge branch 'feat/show-connected-robots' into 'dev'

feat: show connected robots in ui when getting cb pings

See merge request ics/sp/2025/n25b/pepperplus-ui!15
This commit was merged in pull request #15.
This commit is contained in:
Pim Hutting
2025-11-14 13:09:19 +00:00
5 changed files with 161 additions and 11 deletions

20
package-lock.json generated
View File

@@ -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"

View File

@@ -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(){
<Route path="/template" element={<TemplatePage />} />
<Route path="/editor" element={<VisProg />} />
<Route path="/robot" element={<Robot />} />
</Routes>
<Route path="/ConnectedRobots" element={<ConnectedRobots />} />
</Routes>
</main>
{showLogs && <Logging />}
</div>

View File

@@ -0,0 +1,43 @@
import { useEffect, useState } from 'react'
export default function ConnectedRobots() {
const [connected, setConnected] = useState<boolean | null>(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 (
<div>
<h1>Is robot currently connected?</h1>
<div>
<h2>Robot is currently: {connected == null ? "checking..." : (connected ? "connected! 🟢" : "not connected... 🔴")} </h2>
<h3>
{connected == null ? "If checking continues, make sure CB is properly loaded with robot at least once." : ""}
</h3>
</div>
</div>
);
}

View File

@@ -14,6 +14,7 @@ function Home() {
<Link to={"/robot"}>Robot Interaction </Link>
<Link to={"/editor"}>Editor </Link>
<Link to={"/template"}>Template </Link>
<Link to={"/ConnectedRobots"}>Connected Robots </Link>
</div>
</div>
)

View File

@@ -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(<ConnectedRobots />);
// 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(<ConnectedRobots />);
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(<ConnectedRobots />);
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(<ConnectedRobots />);
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(<ConnectedRobots />);
const eventSource = mockInstances[0];
const closeSpy = jest.spyOn(eventSource, 'close');
cleanup();
expect(closeSpy).toHaveBeenCalled();
expect(eventSource.closed).toBe(true);
});
});