Merge branch 'dev' of ssh://git.science.uu.nl/ics/sp/2025/n25b/pepperplus-ui into fix/deep-clone-data
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
import {act} from '@testing-library/react';
|
||||
import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
||||
import { mockReactFlow } from '../../../setupFlowTests.ts';
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
mockReactFlow();
|
||||
});
|
||||
|
||||
describe("UndoRedo Middleware", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
test("pushSnapshot adds a snapshot to past and clears future", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
store.setState({
|
||||
nodes: [{
|
||||
id: 'A',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: [],
|
||||
past: [],
|
||||
future: [{
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
},
|
||||
],
|
||||
edges: []
|
||||
}],
|
||||
});
|
||||
|
||||
act(() => {
|
||||
store.getState().pushSnapshot();
|
||||
})
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.past.length).toBe(1);
|
||||
expect(state.past[0]).toEqual({
|
||||
nodes: [{
|
||||
id: 'A',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
});
|
||||
expect(state.future).toEqual([]);
|
||||
});
|
||||
|
||||
test("pushSnapshot does nothing during batch action", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
act(() => {
|
||||
store.setState({ isBatchAction: true });
|
||||
store.getState().pushSnapshot();
|
||||
})
|
||||
|
||||
expect(store.getState().past.length).toBe(0);
|
||||
});
|
||||
|
||||
test("undo restores last snapshot and pushes current snapshot to future", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
// initial state
|
||||
store.setState({
|
||||
nodes: [{
|
||||
id: 'A',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
});
|
||||
|
||||
act(() => {
|
||||
store.getState().pushSnapshot();
|
||||
|
||||
// modified state
|
||||
store.setState({
|
||||
nodes: [{
|
||||
id: 'B',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'B'}
|
||||
}],
|
||||
edges: []
|
||||
});
|
||||
|
||||
store.getState().undo();
|
||||
})
|
||||
|
||||
expect(store.getState().nodes).toEqual([{
|
||||
id: 'A',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}]);
|
||||
expect(store.getState().future.length).toBe(1);
|
||||
expect(store.getState().future[0]).toEqual({
|
||||
nodes: [{
|
||||
id: 'B',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'B'}
|
||||
}],
|
||||
edges: []
|
||||
});
|
||||
});
|
||||
|
||||
test("undo does nothing when past is empty", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
store.setState({past: []});
|
||||
|
||||
act(() => { store.getState().undo(); });
|
||||
|
||||
expect(store.getState().nodes).toEqual([]);
|
||||
expect(store.getState().future).toEqual([]);
|
||||
});
|
||||
|
||||
test("redo restores last future snapshot and pushes current to past", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
// initial
|
||||
store.setState({
|
||||
nodes: [{
|
||||
id: 'A',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
});
|
||||
|
||||
act(() => {
|
||||
store.getState().pushSnapshot();
|
||||
store.setState({
|
||||
nodes: [{
|
||||
id: 'B',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'B'}
|
||||
}],
|
||||
edges: []
|
||||
});
|
||||
|
||||
|
||||
store.getState().undo();
|
||||
|
||||
// redo should restore node with id 'B'
|
||||
store.getState().redo();
|
||||
})
|
||||
|
||||
expect(store.getState().nodes).toEqual([{
|
||||
id: 'B',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'B'}
|
||||
}]);
|
||||
expect(store.getState().past.length).toBe(1); // snapshot A stored
|
||||
expect(store.getState().past[0]).toEqual({
|
||||
nodes: [{
|
||||
id: 'A',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
});
|
||||
});
|
||||
|
||||
test("redo does nothing when future is empty", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
store.setState({past: []});
|
||||
act(() => { store.getState().redo(); });
|
||||
|
||||
expect(store.getState().nodes).toEqual([]);
|
||||
});
|
||||
|
||||
test("beginBatchAction pushes snapshot and sets isBatchAction=true", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
store.setState({
|
||||
nodes: [{
|
||||
id: 'A',
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
});
|
||||
|
||||
act(() => { store.getState().beginBatchAction(); });
|
||||
|
||||
expect(store.getState().isBatchAction).toBe(true);
|
||||
expect(store.getState().past.length).toBe(1);
|
||||
});
|
||||
|
||||
test("endBatchAction sets isBatchAction=false after timeout", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
store.setState({ isBatchAction: true });
|
||||
act(() => { store.getState().endBatchAction(); });
|
||||
|
||||
// isBatchAction should remain true before the timer has advanced
|
||||
expect(store.getState().isBatchAction).toBe(true);
|
||||
|
||||
jest.advanceTimersByTime(10);
|
||||
|
||||
// it should now be set to false as the timer has advanced enough
|
||||
expect(store.getState().isBatchAction).toBe(false);
|
||||
});
|
||||
|
||||
test("multiple beginBatchAction calls clear the timeout", () => {
|
||||
const store = useFlowStore;
|
||||
|
||||
act(() => {
|
||||
store.getState().beginBatchAction();
|
||||
store.getState().endBatchAction(); // starts timeout
|
||||
store.getState().beginBatchAction(); // should clear previous timeout
|
||||
});
|
||||
|
||||
|
||||
jest.advanceTimersByTime(10);
|
||||
|
||||
// After advancing the timers, isBatchAction should still be true,
|
||||
// as the timeout should have been cleared
|
||||
expect(store.getState().isBatchAction).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getByTestId, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
import VisProgPage from '../../../../../src/pages/VisProgPage/VisProg';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
|
||||
|
||||
@@ -52,13 +52,7 @@ jest.mock('@xyflow/react', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Reset Zustand state helper
|
||||
function resetStore() {
|
||||
useFlowStore.setState({ nodes: [], edges: [] });
|
||||
}
|
||||
|
||||
describe("Drag & drop node creation", () => {
|
||||
beforeEach(() => resetStore());
|
||||
|
||||
test("drops a phase node inside the canvas and adds it with transformed position", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, beforeEach } from '@jest/globals';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithProviders, resetFlowStore } from '../.././/./../../test-utils/test-utils';
|
||||
import { renderWithProviders } from '../.././/./../../test-utils/test-utils';
|
||||
import NormNode, { NormReduce, NormConnects, type NormNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode'
|
||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
import type { Node } from '@xyflow/react';
|
||||
@@ -13,7 +13,6 @@ describe('NormNode', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
resetFlowStore();
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { describe, it, beforeEach } from '@jest/globals';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithProviders, resetFlowStore } from '../.././/./../../test-utils/test-utils';
|
||||
import StartNode, { StartReduce, StartConnects } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode';
|
||||
import type { Node } from '@xyflow/react';
|
||||
import { describe, it } from '@jest/globals';
|
||||
import '@testing-library/jest-dom';
|
||||
import { screen } from '@testing-library/react';
|
||||
import type { Node } from '@xyflow/react';
|
||||
import { renderWithProviders } from '../.././/./../../test-utils/test-utils';
|
||||
import StartNode, { StartReduce, StartConnects } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode';
|
||||
|
||||
|
||||
describe('StartNode', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
resetFlowStore();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders the StartNode correctly', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, beforeEach } from '@jest/globals';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithProviders, resetFlowStore } from '../.././/./../../test-utils/test-utils';
|
||||
import { renderWithProviders } from '../.././/./../../test-utils/test-utils';
|
||||
import TriggerNode, { TriggerReduce, TriggerConnects, TriggerNodeCanConnect, type TriggerNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode';
|
||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
import type { Node } from '@xyflow/react';
|
||||
@@ -11,7 +11,6 @@ describe('TriggerNode', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
resetFlowStore();
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
@@ -193,22 +192,19 @@ describe('TriggerNode', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const allNodes: Node[] = [triggerNode];
|
||||
const result = TriggerReduce(triggerNode, allNodes);
|
||||
const allNodes: Node[] = [triggerNode];
|
||||
const result = TriggerReduce(triggerNode, allNodes);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "trigger-1",
|
||||
type: "keywords",
|
||||
label: 'Keyword Trigger',
|
||||
keywords: [
|
||||
{
|
||||
"id": "kw1",
|
||||
"keyword": "hello",
|
||||
},],
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: 'trigger-1',
|
||||
type: 'keywords',
|
||||
label: 'Keyword Trigger',
|
||||
keywords: [{ id: 'kw1', keyword: 'hello' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('TriggerConnects Function', () => {
|
||||
it('should handle connection without errors', () => {
|
||||
const node1: Node = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, beforeEach } from '@jest/globals';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithProviders, resetFlowStore } from '../.././/./../../test-utils/test-utils';
|
||||
import { renderWithProviders } from '../.././/./../../test-utils/test-utils';
|
||||
import type { XYPosition } from '@xyflow/react';
|
||||
import { NodeTypes, NodeDefaults, NodeConnects, NodeReduces, NodesInPhase } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry';
|
||||
import '@testing-library/jest-dom'
|
||||
@@ -10,12 +10,11 @@ import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgramming
|
||||
|
||||
describe('NormNode', () => {
|
||||
beforeEach(() => {
|
||||
resetFlowStore();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function createNode(id: string, type: string, position: XYPosition, data: Record<string, unknown>, deletable?: boolean) {
|
||||
const defaultData = JSON.parse(JSON.stringify(NodeDefaults[type as keyof typeof NodeDefaults]))
|
||||
const defaultData = NodeDefaults[type as keyof typeof NodeDefaults]
|
||||
const newData = {
|
||||
id: id,
|
||||
type: type,
|
||||
@@ -46,30 +45,35 @@ describe('NormNode', () => {
|
||||
|
||||
describe('Rendering', () => {
|
||||
test.each(getAllTypes())('it should render %s node with the default data', (nodeType) => {
|
||||
const lengthBefore = screen.getAllByText(/.*/).length;
|
||||
const lengthBefore = screen.getAllByText(/.*/).length;
|
||||
|
||||
const newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {})
|
||||
const uiElement = Object.entries(NodeTypes).find(([t])=>t==nodeType)?.[1];
|
||||
expect(uiElement).toBeDefined();
|
||||
const props = {
|
||||
id:newNode.id,
|
||||
type:newNode.type as string,
|
||||
data:newNode.data as any,
|
||||
selected:false,
|
||||
isConnectable:true,
|
||||
zIndex:0,
|
||||
dragging:false,
|
||||
selectable:true,
|
||||
deletable:true,
|
||||
draggable:true,
|
||||
positionAbsoluteX:0,
|
||||
positionAbsoluteY:0,}
|
||||
const newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {});
|
||||
|
||||
renderWithProviders(createElement(uiElement as React.ComponentType<any>, props));
|
||||
const lengthAfter = screen.getAllByText(/.*/).length;
|
||||
|
||||
expect(lengthBefore + 1 == lengthAfter)
|
||||
});
|
||||
const found = Object.entries(NodeTypes).find(([t]) => t === nodeType);
|
||||
const uiElement = found ? found[1] : null;
|
||||
|
||||
expect(uiElement).not.toBeNull();
|
||||
const props = {
|
||||
id: newNode.id,
|
||||
type: newNode.type as string,
|
||||
data: newNode.data as any,
|
||||
selected: false,
|
||||
isConnectable: true,
|
||||
zIndex: 0,
|
||||
dragging: false,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
positionAbsoluteX: 0,
|
||||
positionAbsoluteY: 0,
|
||||
};
|
||||
|
||||
renderWithProviders(createElement(uiElement as React.ComponentType<any>, props));
|
||||
const lengthAfter = screen.getAllByText(/.*/).length;
|
||||
|
||||
expect(lengthBefore + 1 === lengthAfter);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ beforeAll(() => {
|
||||
useFlowStore.setState({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
past: [],
|
||||
future: [],
|
||||
isBatchAction: false,
|
||||
edgeReconnectSuccessful: true
|
||||
});
|
||||
});
|
||||
@@ -78,6 +81,9 @@ afterEach(() => {
|
||||
useFlowStore.setState({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
past: [],
|
||||
future: [],
|
||||
isBatchAction: false,
|
||||
edgeReconnectSuccessful: true
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { render, type RenderOptions } from '@testing-library/react';
|
||||
import { type ReactElement, type ReactNode } from 'react';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
import useFlowStore from '../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
|
||||
/**
|
||||
* Custom render function that wraps components with necessary providers
|
||||
@@ -19,15 +18,7 @@ export function renderWithProviders(
|
||||
return render(ui, { wrapper: Wrapper, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to reset the Zustand store between tests
|
||||
* This ensures test isolation
|
||||
*/
|
||||
export function resetFlowStore() {
|
||||
useFlowStore.setState({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
edgeReconnectSuccessful: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Re-export everything from testing library
|
||||
//eslint-disable-next-line react-refresh/only-export-components
|
||||
export * from '@testing-library/react';
|
||||
|
||||
Reference in New Issue
Block a user