feat: make experiment control buttons prettier

Use actual icons for the play, pause, skip, etc. buttons.
This commit is contained in:
Twirre Meulenbelt
2026-02-04 11:00:14 +01:00
parent b826b8ae47
commit 8fa2adc973
7 changed files with 59 additions and 25 deletions

22
src/App.module.css Normal file
View File

@@ -0,0 +1,22 @@
/* Spread header items. Make home button centered until there is not enough space. */
.header {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
/* Center each */
& > div {
flex: 1;
display: flex;
justify-content: center;
}
/* Except first and last */
& > div:first-child {
justify-content: flex-start;
}
& > div:last-child {
justify-content: flex-end;
}
}

View File

@@ -8,6 +8,7 @@ import UserManual from './pages/Manuals/Manuals.tsx';
import VisProg from "./pages/VisProgPage/VisProg.tsx";
import {useState} from "react";
import Logging from "./components/Logging/Logging.tsx";
import styles from "./App.module.css";
function App(){
@@ -15,10 +16,10 @@ function App(){
return (
<>
<header>
<span>© Utrecht University (ICS)</span>
<Link to={"/"}>Home</Link>
<button onClick={() => setShowLogs(!showLogs)}>Developer Logs</button>
<header className={styles.header}>
<div><span>© Utrecht University (ICS)</span></div>
<div><Link to={"/"}>Home</Link></div>
<div><button onClick={() => setShowLogs(!showLogs)}>Developer Logs</button></div>
</header>
<div className={"flex-row justify-center flex-1 min-height-0"}>
<main className={"flex-col align-center flex-1 scroll-y"}>

View File

@@ -1,5 +0,0 @@
export default function Redo({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M390.98-191.87q-98.44 0-168.77-65.27-70.34-65.27-70.34-161.43 0-96.15 70.34-161.54 70.33-65.39 168.77-65.39h244.11l-98.98-98.98 63.65-63.65L808.13-600 599.76-391.87l-63.65-63.65 98.98-98.98H390.98q-60.13 0-104.12 38.92-43.99 38.93-43.99 96.78 0 57.84 43.99 96.89 43.99 39.04 104.12 39.04h286.15v91H390.98Z"/>
</svg>;
}

View File

@@ -0,0 +1,5 @@
export default function Stop({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M240-240v-480h480v480H240Z"/>
</svg>;
}

View File

@@ -46,6 +46,7 @@ University within the Software Project course.
display: flex;
justify-content: center;
align-items: center;
padding: .5rem;
}
}

View File

@@ -35,6 +35,11 @@ import {
RobotConnected
} from './MonitoringPageComponents';
import ExperimentLogs from "./components/ExperimentLogs.tsx";
import Pause from "../../components/Icons/Pause.tsx";
import Play from "../../components/Icons/Play.tsx";
import Next from "../../components/Icons/Next.tsx";
import Replay from "../../components/Icons/Replay.tsx";
import Stop from "../../components/Icons/Stop.tsx";
// ----------------------------------------------------------------------
// 1. State management
@@ -238,34 +243,39 @@ function ControlPanel({
<h3>Experiment Controls</h3>
<div className={styles.controlsButtons}>
<button
aria-label="Pause"
className={!isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
onClick={() => onAction("pause")}
disabled={loading}
></button>
><Pause fill={"white"} /></button>
<button
aria-label="Play"
className={isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
onClick={() => onAction("play")}
disabled={loading}
></button>
><Play fill={"white"} /></button>
<button
aria-label="Next phase"
className={styles.next}
onClick={() => onAction("nextPhase")}
disabled={loading}
></button>
><Next fill={"white"} /></button>
<button
aria-label="Reset experiment"
className={styles.restartExperiment}
onClick={onReset}
disabled={loading}
></button>
><Replay fill={"white"} /></button>
<button
aria-label="Stop"
className={styles.stop}
onClick={() => onAction("stop")}
disabled={loading}
></button>
><Stop fill={"white"} /></button>
</div>
</div>
);

View File

@@ -122,8 +122,8 @@ describe('MonitoringPage', () => {
describe('Control Buttons', () => {
test('Pause calls API and updates UI', async () => {
render(<MonitoringPage />);
const pauseBtn = screen.getByText('❚❚');
const pauseBtn = screen.getByRole('button', { name: /pause/i });
await act(async () => {
fireEvent.click(pauseBtn);
});
@@ -134,8 +134,8 @@ describe('MonitoringPage', () => {
test('Play calls API and updates UI', async () => {
render(<MonitoringPage />);
const playBtn = screen.getByText('▶');
const playBtn = screen.getByRole('button', { name: /play/i });
await act(async () => {
fireEvent.click(playBtn);
});
@@ -146,35 +146,35 @@ describe('MonitoringPage', () => {
test('Next Phase calls API', async () => {
render(<MonitoringPage />);
await act(async () => {
fireEvent.click(screen.getByText('⏭'));
fireEvent.click(screen.getByRole('button', { name: /next phase/i }));
});
expect(MonitoringAPI.nextPhase).toHaveBeenCalled();
});
test('Reset Experiment calls logic and resets state', async () => {
render(<MonitoringPage />);
// Mock graph reducer return
(VisProg.graphReducer as jest.Mock).mockReturnValue([{ id: 'new-phase' }]);
await act(async () => {
fireEvent.click(screen.getByText('⟲'));
fireEvent.click(screen.getByRole('button', { name: /reset experiment/i }));
});
expect(VisProg.graphReducer).toHaveBeenCalled();
expect(mockSetProgramState).toHaveBeenCalledWith({ phases: [{ id: 'new-phase' }] });
expect(VisProg.runProgram).toHaveBeenCalled();
});
test('Reset Experiment handles errors gracefully', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
(VisProg.runProgram as jest.Mock).mockRejectedValue(new Error('Fail'));
render(<MonitoringPage />);
await act(async () => {
fireEvent.click(screen.getByText('⟲'));
fireEvent.click(screen.getByRole('button', { name: /reset experiment/i }));
});
expect(consoleSpy).toHaveBeenCalledWith('Failed to reset program:', expect.any(Error));
consoleSpy.mockRestore();
});