Compare commits

...

4 Commits

Author SHA1 Message Date
JobvAlewijk
2ecb33dcde chore: now actually runs tests 2026-01-06 15:19:49 +01:00
Gerla, J. (Justin)
15c9fc6f4d Merge branch 'demo' into 'feat/simple-program-page'
# Conflicts:
#   test/utils/programStore.test.ts
2026-01-06 13:37:27 +00:00
JobvAlewijk
9dae45e398 Merge branch 'feat/make-program-data-available-on-all-pages' into 'demo'
feat: made (reduced) program data available on all pages

See merge request ics/sp/2025/n25b/pepperplus-ui!36
2026-01-06 12:27:22 +00:00
Gerla, J. (Justin)
bd93b04bfd feat: made (reduced) program data available on all pages 2026-01-06 12:27:22 +00:00
4 changed files with 199 additions and 90 deletions

View File

@@ -140,7 +140,7 @@ function VisualProgrammingUI() {
} }
// currently outputs the prepared program to the console // currently outputs the prepared program to the console
function runProgramm() { function runProgram() {
const phases = graphReducer(); const phases = graphReducer();
const program = {phases} const program = {phases}
console.log(JSON.stringify(program, null, 2)); console.log(JSON.stringify(program, null, 2));
@@ -183,11 +183,11 @@ function VisProgPage() {
const [showSimpleProgram, setShowSimpleProgram] = useState(false); const [showSimpleProgram, setShowSimpleProgram] = useState(false);
const setProgramState = useProgramStore((state) => state.setProgramState); const setProgramState = useProgramStore((state) => state.setProgramState);
const runProgram = () => { const onClick = () => {
const phases = graphReducer(); // reduce graph const phases = graphReducer(); // reduce graph
setProgramState({ phases }); // <-- save to store setProgramState({ phases }); // <-- save to store
setShowSimpleProgram(true); // show SimpleProgram setShowSimpleProgram(true); // show SimpleProgram
runProgramm(); // send to backend if needed runProgram(); // send to backend if needed
}; };
if (showSimpleProgram) { if (showSimpleProgram) {
@@ -204,7 +204,7 @@ function VisProgPage() {
return ( return (
<> <>
<VisualProgrammingUI/> <VisualProgrammingUI/>
<button onClick={runProgram}>run program</button> <button onClick={onClick}>run program</button>
</> </>
) )
} }

View File

@@ -0,0 +1,176 @@
import { render, screen, fireEvent } from "@testing-library/react";
import SimpleProgram from "../../../src/pages/SimpleProgram/SimpleProgram";
import useProgramStore from "../../../src/utils/programStore";
/**
* Helper to preload the program store before rendering.
*/
function loadProgram(phases: Record<string, unknown>[]) {
useProgramStore.getState().setProgramState({ phases });
}
describe("SimpleProgram", () => {
beforeEach(() => {
loadProgram([]);
});
test("shows empty state when no program is loaded", () => {
render(<SimpleProgram />);
expect(screen.getByText("No program loaded.")).toBeInTheDocument();
});
test("renders first phase content", () => {
loadProgram([
{
id: "phase-1",
norms: [{ id: "n1", norm: "Be polite" }],
goals: [{ id: "g1", description: "Finish task", achieved: true }],
triggers: [{ id: "t1", label: "Keyword trigger" }],
},
]);
render(<SimpleProgram />);
expect(screen.getByText("Phase 1 / 1")).toBeInTheDocument();
expect(screen.getByText("Be polite")).toBeInTheDocument();
expect(screen.getByText("Finish task")).toBeInTheDocument();
expect(screen.getByText("Keyword trigger")).toBeInTheDocument();
});
test("renders empty messages when phase has no data", () => {
loadProgram([
{
id: "phase-1",
norms: [],
goals: [],
triggers: [],
},
]);
render(<SimpleProgram />);
expect(screen.getAllByText("No norms defined.").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("No goals defined.").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("No triggers defined.").length).toBeGreaterThanOrEqual(1);
expect(
screen.getByText("No conditional norms defined.")
).toBeInTheDocument();
});
test("allows navigating between phases", () => {
loadProgram([
{
id: "phase-1",
norms: [],
goals: [],
triggers: [],
},
{
id: "phase-2",
norms: [{ id: "n2", norm: "Be careful" }],
goals: [],
triggers: [],
},
]);
render(<SimpleProgram />);
expect(screen.getByText("Phase 1 / 2")).toBeInTheDocument();
fireEvent.click(screen.getByText("Next ▶"));
expect(screen.getByText("Phase 2 / 2")).toBeInTheDocument();
expect(screen.getByText("Be careful")).toBeInTheDocument();
});
test("prev button is disabled on first phase", () => {
loadProgram([{ id: "phase-1", norms: [], goals: [], triggers: [] }]);
render(<SimpleProgram />);
expect(screen.getByText("◀ Prev")).toBeDisabled();
});
test("next button is disabled on last phase", () => {
loadProgram([{ id: "phase-1", norms: [], goals: [], triggers: [] }]);
render(<SimpleProgram />);
expect(screen.getByText("Next ▶")).toBeDisabled();
});
test("prev and next buttons enable/disable correctly when navigating", () => {
loadProgram([
{ id: "p1", norms: [], goals: [], triggers: [] },
{ id: "p2", norms: [], goals: [], triggers: [] },
]);
render(<SimpleProgram />);
const prev = screen.getByText("◀ Prev");
const next = screen.getByText("Next ▶");
expect(prev).toBeDisabled();
expect(next).not.toBeDisabled();
fireEvent.click(next);
expect(prev).not.toBeDisabled();
expect(next).toBeDisabled();
});
test("renders achieved and unachieved goals with correct icons", () => {
loadProgram([
{
id: "phase-1",
norms: [],
goals: [
{ id: "g1", description: "Done goal", achieved: true },
{ id: "g2", description: "Failed goal", achieved: false },
],
triggers: [],
},
]);
render(<SimpleProgram />);
expect(screen.getByText("✔")).toBeInTheDocument();
expect(screen.getByText("✖")).toBeInTheDocument();
expect(screen.getByText("Done goal")).toBeInTheDocument();
expect(screen.getByText("Failed goal")).toBeInTheDocument();
});
test("renders fallback labels when optional fields are missing", () => {
loadProgram([
{
id: "phase-1",
norms: [{}],
goals: [{}],
triggers: [{}],
},
]);
render(<SimpleProgram />);
expect(screen.getByText("Unnamed norm")).toBeInTheDocument();
expect(screen.getByText("Unnamed goal")).toBeInTheDocument();
expect(screen.getByText("Unnamed trigger")).toBeInTheDocument();
});
test("does not crash when navigating beyond boundaries", () => {
loadProgram([
{ id: "p1", norms: [], goals: [], triggers: [] },
{ id: "p2", norms: [], goals: [], triggers: [] },
]);
render(<SimpleProgram />);
fireEvent.click(screen.getByText("Next ▶"));
fireEvent.click(screen.getByText("Next ▶"));
expect(screen.getByText("Phase 2 / 2")).toBeInTheDocument();
fireEvent.click(screen.getByText("◀ Prev"));
fireEvent.click(screen.getByText("◀ Prev"));
expect(screen.getByText("Phase 1 / 2")).toBeInTheDocument();
});
});

View File

@@ -1,83 +0,0 @@
import { render, screen, fireEvent } from "@testing-library/react";
import SimpleProgram from "../../../src/pages/SimpleProgram/SimpleProgram";
import useProgramStore from "../../../src/utils/programStore";
/**
* Helper to preload the program store before rendering.
*/
function loadProgram(phases: Record<string, unknown>[]) {
useProgramStore.getState().setProgramState({ phases });
}
describe("SimpleProgram", () => {
beforeEach(() => {
loadProgram([]);
});
test("shows empty state when no program is loaded", () => {
render(<SimpleProgram />);
expect(screen.getByText("No program loaded.")).toBeInTheDocument();
});
test("renders first phase content", () => {
loadProgram([
{
id: "phase-1",
norms: [{ id: "n1", norm: "Be polite" }],
goals: [{ id: "g1", description: "Finish task", achieved: true }],
triggers: [{ id: "t1", label: "Keyword trigger" }],
},
]);
render(<SimpleProgram />);
expect(screen.getByText("Phase 1 / 1")).toBeInTheDocument();
expect(screen.getByText("Be polite")).toBeInTheDocument();
expect(screen.getByText("Finish task")).toBeInTheDocument();
expect(screen.getByText("Keyword trigger")).toBeInTheDocument();
});
test("allows navigating between phases", () => {
loadProgram([
{
id: "phase-1",
norms: [],
goals: [],
triggers: [],
},
{
id: "phase-2",
norms: [{ id: "n2", norm: "Be careful" }],
goals: [],
triggers: [],
},
]);
render(<SimpleProgram />);
expect(screen.getByText("Phase 1 / 2")).toBeInTheDocument();
fireEvent.click(screen.getByText("Next ▶"));
expect(screen.getByText("Phase 2 / 2")).toBeInTheDocument();
expect(screen.getByText("Be careful")).toBeInTheDocument();
});
test("prev button is disabled on first phase", () => {
loadProgram([
{ id: "phase-1", norms: [], goals: [], triggers: [] },
]);
render(<SimpleProgram />);
expect(screen.getByText("◀ Prev")).toBeDisabled();
});
test("next button is disabled on last phase", () => {
loadProgram([
{ id: "phase-1", norms: [], goals: [], triggers: [] },
]);
render(<SimpleProgram />);
expect(screen.getByText("Next ▶")).toBeDisabled();
});
});

View File

@@ -85,14 +85,30 @@ describe('useProgramStore', () => {
).toThrow('phase with id:"missing-phase" not found'); ).toThrow('phase with id:"missing-phase" not found');
}); });
// this test should be at the bottom to avoid conflicts with the previous tests
it('should clone program state when setting it (no shared references should exist)', () => { it('should clone program state when setting it (no shared references should exist)', () => {
useProgramStore.getState().setProgramState(mockProgram); const changeableMockProgram: ReducedProgram = {
phases: [
{
id: 'phase-1',
norms: [{ id: 'norm-1' }],
goals: [{ id: 'goal-1' }],
triggers: [{ id: 'trigger-1' }],
},
{
id: 'phase-2',
norms: [{ id: 'norm-2' }],
goals: [{ id: 'goal-2' }],
triggers: [{ id: 'trigger-2' }],
},
],
};
useProgramStore.getState().setProgramState(changeableMockProgram);
const storedProgram = useProgramStore.getState().getProgramState(); const storedProgram = useProgramStore.getState().getProgramState();
// mutate original // mutate original
(mockProgram.phases[0].norms as any[]).push({ id: 'norm-mutated' }); (changeableMockProgram.phases[0].norms as any[]).push({ id: 'norm-mutated' });
// store should NOT change // store should NOT change
expect(storedProgram.phases[0]['norms']).toHaveLength(1); expect(storedProgram.phases[0]['norms']).toHaveLength(1);