feat: create tests, more integration testing, fix ID tests, use UUID (almost) everywhere

ref: N25B-412
This commit is contained in:
Björn Otgaar
2026-01-04 18:29:19 +01:00
parent c5f44536b7
commit 149b82cb66
11 changed files with 332 additions and 146 deletions

View File

@@ -95,7 +95,11 @@ describe("Drag & drop node creation", () => {
const node = nodes[0];
expect(node.type).toBe("phase");
expect(node.id).toBe("phase-1");
// UUID Expression
expect(node.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
// screenToFlowPosition was mocked to subtract 100
expect(node.position).toEqual({

View File

@@ -0,0 +1,131 @@
import { useState } from 'react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders, screen } from '../../../../test-utils/test-utils.tsx';
import GestureValueEditor from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/GestureValueEditor';
function TestHarness({ initialValue = '', placeholder = 'Gesture name' } : { initialValue?: string, placeholder?: string }) {
const [value, setValue] = useState(initialValue);
return (
<GestureValueEditor value={value} setValue={setValue} placeholder={placeholder} />
);
}
describe('GestureValueEditor', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
});
test('renders in tag mode by default and allows selecting a tag via button and select', async () => {
renderWithProviders(<TestHarness />);
// Tag selector should be present
const select = screen.getByTestId('tagSelectorTestID') as HTMLSelectElement;
expect(select).toBeInTheDocument();
expect(select.value).toBe('');
// Choose a tag via select
await user.selectOptions(select, 'happy');
expect(select.value).toBe('happy');
// The corresponding tag button should reflect the selection (have the selected class)
const happyButton = screen.getByRole('button', { name: /happy/i });
expect(happyButton).toBeInTheDocument();
expect(happyButton.className).toMatch(/selected/);
});
test('switches to single mode and shows suggestions list', async () => {
renderWithProviders(<TestHarness initialValue={'happy'} />);
const singleButton = screen.getByRole('button', { name: /^single$/i });
await user.click(singleButton);
// Input should be present with placeholder
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
expect(input).toBeInTheDocument();
// Because switching to single populates suggestions, we expect at least one suggestion item
const suggestion = await screen.findByText(/Listening_1/);
expect(suggestion).toBeInTheDocument();
});
test('typing filters suggestions and selecting a suggestion commits the value and hides the list', async () => {
renderWithProviders(<TestHarness />);
// Switch to single mode
await user.click(screen.getByRole('button', { name: /^single$/i }));
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
// Type a substring that matches some suggestions
await user.type(input, 'Listening_2');
// The suggestion should appear and include the text we typed
const matching = await screen.findByText(/Listening_2/);
expect(matching).toBeInTheDocument();
// Click the suggestion
await user.click(matching);
// After selecting, input should contain that suggestion and suggestions should be hidden
expect(input.value).toContain('Listening_2');
expect(screen.queryByText(/Listening_1/)).toBeNull();
});
test('typing a non-matching string hides the suggestions list', async () => {
renderWithProviders(<TestHarness />);
await user.click(screen.getByRole('button', { name: /^single$/i }));
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
await user.type(input, 'no-match-zzz');
// There should be no suggestion that includes that gibberish
expect(screen.queryByText(/no-match-zzz/)).toBeNull();
});
test('switching back to tag mode clears value when it is not a valid tag and preserves it when it is', async () => {
renderWithProviders(<TestHarness />);
// Switch to single mode and pick a suggestion (which is not a semantic tag)
await user.click(screen.getByRole('button', { name: /^single$/i }));
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
await user.type(input, 'Listening_3');
const suggestion = await screen.findByText(/Listening_3/);
await user.click(suggestion);
// Switch back to tag mode -> value should be cleared (not in tag list)
await user.click(screen.getByRole('button', { name: /^tag$/i }));
const select = screen.getByTestId('tagSelectorTestID') as HTMLSelectElement;
expect(select.value).toBe('');
// Now pick a valid tag and switch to single then back to tag
await user.selectOptions(select, 'happy');
expect(select.value).toBe('happy');
// Switch to single and then back to tag; since 'happy' is a valid tag, it should remain
await user.click(screen.getByRole('button', { name: /^single$/i }));
await user.click(screen.getByRole('button', { name: /^tag$/i }));
expect(select.value).toBe('happy');
});
test('focus on input re-shows filtered suggestions when customValue is present', async () => {
renderWithProviders(<TestHarness />);
// Switch to single mode and type to filter
await user.click(screen.getByRole('button', { name: /^single$/i }));
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
await user.type(input, 'Listening_4');
const found = await screen.findByText(/Listening_4/);
expect(found).toBeInTheDocument();
// Blur the input
input.blur();
expect(found).toBeInTheDocument();
// Focus the input again and ensure the suggestions remain or reappear
await user.click(input);
const foundAgain = await screen.findByText(/Listening_4/);
expect(foundAgain).toBeInTheDocument();
});
});

View File

@@ -1,30 +1,17 @@
// PlanEditorDialog.test.tsx
import { describe, it, beforeEach, jest } from '@jest/globals';
import { screen } from '@testing-library/react';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
import PlanEditorDialog from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor';
import type { Plan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan';
import '@testing-library/jest-dom';
// Mock crypto.randomUUID for consistent IDs in tests
const mockUUID = 'test-uuid-123';
Object.defineProperty(globalThis, 'crypto', {
value: {
randomUUID: () => mockUUID,
},
writable: true,
});
// Mock structuredClone
(globalThis as any).structuredClone = jest.fn((val) => JSON.parse(JSON.stringify(val)));
// Mock HTMLDialogElement methods
const mockDialogMethods = {
showModal: jest.fn(),
close: jest.fn(),
};
// UUID Regex for checking ID's
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
describe('PlanEditorDialog', () => {
let user: ReturnType<typeof userEvent.setup>;
@@ -33,9 +20,6 @@ describe('PlanEditorDialog', () => {
beforeEach(() => {
user = userEvent.setup();
jest.clearAllMocks();
// Mock dialog element methods
HTMLDialogElement.prototype.showModal = mockDialogMethods.showModal;
HTMLDialogElement.prototype.close = mockDialogMethods.close;
});
const defaultPlan: Plan = {
@@ -90,7 +74,6 @@ describe('PlanEditorDialog', () => {
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
expect(mockDialogMethods.showModal).toHaveBeenCalled();
// One for button, one for dialog.
expect(screen.getAllByText('Create Plan').length).toEqual(2);
@@ -101,7 +84,6 @@ describe('PlanEditorDialog', () => {
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
expect(mockDialogMethods.showModal).toHaveBeenCalled();
// One for button, one for dialog
expect(screen.getAllByText('Edit Plan').length).toEqual(2);
});
@@ -121,7 +103,7 @@ describe('PlanEditorDialog', () => {
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
await user.click(screen.getByText('Cancel'));
expect(mockDialogMethods.close).toHaveBeenCalled();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});
@@ -198,7 +180,7 @@ describe('PlanEditorDialog', () => {
it('should add a gesture action to the plan', async () => {
renderDialog({ plan: defaultPlan });
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
await user.click(screen.getByRole('button', { name: /edit plan/i }));
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
const addButton = screen.getByText('Add Step');
@@ -207,14 +189,14 @@ describe('PlanEditorDialog', () => {
await user.selectOptions(actionTypeSelect, 'gesture');
// Find the input field after type change
const gestureInput = screen.getByPlaceholderText(/Gesture name|text/i);
await user.type(gestureInput, 'Wave hand');
const select = screen.getByTestId("tagSelectorTestID")
const options = within(select).getAllByRole('option')
await user.selectOptions(select, options[1])
await user.click(addButton);
// Check if step was added
expect(screen.getByText('gesture:')).toBeInTheDocument();
expect(screen.getByText('Wave hand')).toBeInTheDocument();
});
it('should add an LLM action to the plan', async () => {
@@ -329,17 +311,17 @@ describe('PlanEditorDialog', () => {
await user.click(screen.getByText('Create'));
expect(mockOnSave).toHaveBeenCalledWith({
id: mockUUID,
id: expect.stringMatching(uuidRegex),
name: 'My New Plan',
steps: [
{
id: mockUUID,
id: expect.stringMatching(uuidRegex),
text: 'First step',
type: 'speech',
},
],
});
expect(mockDialogMethods.close).toHaveBeenCalled();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('should call onSave with updated plan when editing', async () => {
@@ -366,13 +348,13 @@ describe('PlanEditorDialog', () => {
name: 'Updated Plan Name',
steps: [
{
id: mockUUID,
id: expect.stringMatching(uuidRegex),
text: 'New speech action',
type: 'speech',
},
],
});
expect(mockDialogMethods.close).toHaveBeenCalled();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('should call onSave with undefined when reset button is clicked', async () => {
@@ -382,7 +364,7 @@ describe('PlanEditorDialog', () => {
await user.click(screen.getByText('Reset'));
expect(mockOnSave).toHaveBeenCalledWith(undefined);
expect(mockDialogMethods.close).toHaveBeenCalled();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('should disable save button when no draft plan exists', async () => {
@@ -438,7 +420,7 @@ describe('PlanEditorDialog', () => {
// Check gesture placeholder
await user.selectOptions(actionTypeSelect, 'gesture');
const gestureInput = screen.getByPlaceholderText(/Gesture|text/i);
const gestureInput = screen.getByTestId("valueEditorTestID")
expect(gestureInput).toBeInTheDocument();
// Check LLM placeholder

View File

@@ -404,6 +404,16 @@ describe('NormNode', () => {
describe('NormReduce Function', () => {
it('should reduce a norm node to its essential data', () => {
const condition: Node = {
id: "belief-1",
type: 'basic_belief',
position: {x: 10, y: 10},
data: {
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
}
}
const normNode: Node = {
id: 'norm-1',
type: 'norm',
@@ -414,17 +424,21 @@ describe('NormNode', () => {
droppable: true,
norm: 'Never harm humans',
hasReduce: true,
condition: "belief-1"
},
};
const allNodes: Node[] = [normNode];
const allNodes: Node[] = [normNode, condition];
const result = NormReduce(normNode, allNodes);
expect(result).toEqual({
id: 'norm-1',
label: 'Safety Norm',
norm: 'Never harm humans',
critical: false,
condition: {
id: "belief-1",
keyword: "help"
},
});
});

View File

@@ -76,8 +76,10 @@ describe('PhaseNode', () => {
// Find nodes
const nodes = useFlowStore.getState().nodes;
const p1 = nodes.find((x) => x.id === 'phase-1')!;
const p2 = nodes.find((x) => x.id === 'phase-2')!;
const phaseNodes = nodes.filter((x) => x.type === 'phase');
const p1 = phaseNodes[0];
const p2 = phaseNodes[1];
// expect same value, not same reference
expect(p1.data.children).not.toBe(p2.data.children);

View File

@@ -61,6 +61,7 @@ export const mockReactFlow = () => {
width: 200,
height: 200,
});
};
@@ -88,3 +89,16 @@ afterEach(() => {
});
});
if (typeof HTMLDialogElement !== 'undefined') {
if (!HTMLDialogElement.prototype.showModal) {
HTMLDialogElement.prototype.showModal = function () {
// basic behavior: mark as open
this.setAttribute('open', '');
};
}
if (!HTMLDialogElement.prototype.close) {
HTMLDialogElement.prototype.close = function () {
this.removeAttribute('open');
};
}
}