Compare commits
4 Commits
feat/monit
...
feat/simpl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ecb33dcde | ||
|
|
15c9fc6f4d | ||
|
|
9dae45e398 | ||
|
|
bd93b04bfd |
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
176
test/pages/simpleProgram/SimpleProgram.test.tsx
Normal file
176
test/pages/simpleProgram/SimpleProgram.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user