Merge branch 'feat/editor-user-feedback' into temp_screenshot_manual
# Conflicts: # src/pages/VisProgPage/VisProg.tsx # src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx
This commit is contained in:
@@ -34,10 +34,17 @@ describe("UndoRedo Middleware", () => {
|
||||
type: 'default',
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
},
|
||||
}
|
||||
],
|
||||
edges: []
|
||||
edges: [],
|
||||
warnings: {
|
||||
warningRegistry: new Map(),
|
||||
severityIndex: new Map()
|
||||
}
|
||||
}],
|
||||
ruleRegistry: new Map(),
|
||||
editorWarningRegistry: new Map(),
|
||||
severityIndex: new Map()
|
||||
});
|
||||
|
||||
act(() => {
|
||||
@@ -53,7 +60,11 @@ describe("UndoRedo Middleware", () => {
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
edges: [],
|
||||
warnings: {
|
||||
warningRegistry: {},
|
||||
severityIndex: {}
|
||||
}
|
||||
});
|
||||
expect(state.future).toEqual([]);
|
||||
});
|
||||
@@ -80,7 +91,9 @@ describe("UndoRedo Middleware", () => {
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
edges: [],
|
||||
editorWarningRegistry: new Map(),
|
||||
severityIndex: new Map()
|
||||
});
|
||||
|
||||
act(() => {
|
||||
@@ -114,7 +127,11 @@ describe("UndoRedo Middleware", () => {
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'B'}
|
||||
}],
|
||||
edges: []
|
||||
edges: [],
|
||||
warnings: {
|
||||
warningRegistry: {},
|
||||
severityIndex: {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,7 +157,9 @@ describe("UndoRedo Middleware", () => {
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
edges: [],
|
||||
editorWarningRegistry: new Map(),
|
||||
severityIndex: new Map()
|
||||
});
|
||||
|
||||
act(() => {
|
||||
@@ -176,7 +195,11 @@ describe("UndoRedo Middleware", () => {
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
edges: [],
|
||||
warnings: {
|
||||
warningRegistry: {},
|
||||
severityIndex: {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,7 +222,9 @@ describe("UndoRedo Middleware", () => {
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: 'A'}
|
||||
}],
|
||||
edges: []
|
||||
edges: [],
|
||||
editorWarningRegistry: new Map(),
|
||||
severityIndex: new Map()
|
||||
});
|
||||
|
||||
act(() => { store.getState().beginBatchAction(); });
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {act} from '@testing-library/react';
|
||||
import type {Connection, Edge, Node} from "@xyflow/react";
|
||||
import {
|
||||
type Connection,
|
||||
type Edge,
|
||||
type Node,
|
||||
} from "@xyflow/react";
|
||||
import type {HandleRule, RuleResult} from "../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
|
||||
import { NodeDisconnections } from "../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts";
|
||||
import type {PhaseNodeData} from "../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||
@@ -398,6 +402,7 @@ describe('FlowStore Functionality', () => {
|
||||
}]
|
||||
});
|
||||
|
||||
|
||||
act(()=> {
|
||||
deleteNode(nodeId);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { describe, it, expect} from '@jest/globals';
|
||||
import {
|
||||
type EditorWarning, warningSummary
|
||||
} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx";
|
||||
import useFlowStore from "../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
|
||||
|
||||
|
||||
function makeWarning(
|
||||
overrides?: Partial<EditorWarning>
|
||||
): EditorWarning {
|
||||
return {
|
||||
scope: { id: 'node-1' },
|
||||
type: 'MISSING_INPUT',
|
||||
severity: 'ERROR',
|
||||
description: 'Missing input',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("editorWarnings", () => {
|
||||
describe('registerWarning', () => {
|
||||
it('registers a node-level warning', () => {
|
||||
const warning = makeWarning();
|
||||
const {registerWarning, getWarnings} = useFlowStore.getState()
|
||||
registerWarning(warning);
|
||||
|
||||
const warnings = getWarnings();
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toEqual(warning);
|
||||
});
|
||||
|
||||
it('registers a handle-level warning with scoped key', () => {
|
||||
const warning = makeWarning({
|
||||
scope: { id: 'node-1', handleId: 'input-1' },
|
||||
});
|
||||
const {registerWarning} = useFlowStore.getState()
|
||||
registerWarning(warning);
|
||||
const nodeWarnings = useFlowStore.getState().editorWarningRegistry.get('node-1');
|
||||
expect(nodeWarnings?.has('MISSING_INPUT:input-1') === true).toBe(true);
|
||||
});
|
||||
|
||||
it('updates severityIndex correctly', () => {
|
||||
const {registerWarning, severityIndex} = useFlowStore.getState()
|
||||
registerWarning(makeWarning());
|
||||
expect(severityIndex.get('ERROR')!.size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWarningsBySeverity', () => {
|
||||
it('returns only warnings of requested severity', () => {
|
||||
const {registerWarning, getWarningsBySeverity} = useFlowStore.getState()
|
||||
registerWarning(
|
||||
makeWarning({ severity: 'ERROR' })
|
||||
);
|
||||
|
||||
registerWarning(
|
||||
makeWarning({
|
||||
severity: 'WARNING',
|
||||
type: 'MISSING_OUTPUT',
|
||||
})
|
||||
);
|
||||
|
||||
const errors = getWarningsBySeverity('ERROR');
|
||||
const warnings = getWarningsBySeverity('WARNING');
|
||||
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(warnings).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isProgramValid', () => {
|
||||
it('returns true when no ERROR warnings exist', () => {
|
||||
expect(useFlowStore.getState().isProgramValid()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when ERROR warnings exist', () => {
|
||||
const {registerWarning, isProgramValid} = useFlowStore.getState()
|
||||
registerWarning(makeWarning());
|
||||
expect(isProgramValid()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregisterWarning', () => {
|
||||
it('removes warning from registry and severityIndex', () => {
|
||||
const warning = makeWarning();
|
||||
const {
|
||||
registerWarning,
|
||||
getWarnings,
|
||||
unregisterWarning,
|
||||
severityIndex
|
||||
} = useFlowStore.getState()
|
||||
|
||||
registerWarning(warning);
|
||||
|
||||
unregisterWarning('node-1', 'MISSING_INPUT');
|
||||
|
||||
expect(getWarnings()).toHaveLength(0);
|
||||
expect(severityIndex.get('ERROR')!.size).toBe(0);
|
||||
});
|
||||
|
||||
it('does nothing if warning does not exist', () => {
|
||||
expect(() =>
|
||||
useFlowStore.getState().unregisterWarning('node-1', 'DOES_NOT_EXIST')
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregisterWarningsForId', () => {
|
||||
it('removes all warnings for a node', () => {
|
||||
const {registerWarning, unregisterWarningsForId, getWarnings, severityIndex} = useFlowStore.getState()
|
||||
registerWarning(
|
||||
makeWarning({
|
||||
scope: { id: 'node-1', handleId: 'h1' },
|
||||
})
|
||||
);
|
||||
|
||||
registerWarning(
|
||||
makeWarning({
|
||||
scope: { id: 'node-1' },
|
||||
type: 'MISSING_OUTPUT',
|
||||
severity: 'WARNING',
|
||||
})
|
||||
);
|
||||
|
||||
unregisterWarningsForId('node-1');
|
||||
|
||||
expect(getWarnings()).toHaveLength(0);
|
||||
expect(
|
||||
severityIndex.get('ERROR')!.size
|
||||
).toBe(0);
|
||||
expect(
|
||||
severityIndex.get('WARNING')!.size
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('warningSummary', () => {
|
||||
it('returns correct counts and validity', () => {
|
||||
const {registerWarning} = useFlowStore.getState()
|
||||
registerWarning(
|
||||
makeWarning({ severity: 'ERROR' })
|
||||
);
|
||||
|
||||
const summary = warningSummary();
|
||||
|
||||
expect(summary.error).toBe(1);
|
||||
expect(summary.warning).toBe(0);
|
||||
expect(summary.info).toBe(0);
|
||||
expect(summary.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -0,0 +1,138 @@
|
||||
import {fireEvent, render, screen} from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {useReactFlow, useStoreApi} from "@xyflow/react";
|
||||
import {
|
||||
type EditorWarning,
|
||||
globalWarning
|
||||
} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx";
|
||||
import {WarningsSidebar} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx";
|
||||
import useFlowStore from "../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
|
||||
|
||||
|
||||
jest.mock('@xyflow/react', () => ({
|
||||
useReactFlow: jest.fn(),
|
||||
useStoreApi: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx');
|
||||
|
||||
function makeWarning(
|
||||
overrides?: Partial<EditorWarning>
|
||||
): EditorWarning {
|
||||
return {
|
||||
scope: { id: 'node-1' },
|
||||
type: 'MISSING_INPUT',
|
||||
severity: 'ERROR',
|
||||
description: 'Missing input',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('WarningsSidebar', () => {
|
||||
let getStateSpy: jest.SpyInstance;
|
||||
|
||||
const setCenter = jest.fn(() => Promise.resolve());
|
||||
const getNode = jest.fn();
|
||||
const addSelectedNodes = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// React Flow hooks
|
||||
(useReactFlow as jest.Mock).mockReturnValue({
|
||||
getNode,
|
||||
setCenter,
|
||||
});
|
||||
(useStoreApi as jest.Mock).mockReturnValue({
|
||||
getState: () => ({ addSelectedNodes }),
|
||||
});
|
||||
|
||||
// Use spyOn to override store
|
||||
const mockWarnings = [
|
||||
makeWarning({ description: 'Node warning', scope: { id: 'node-1' } }),
|
||||
makeWarning({
|
||||
description: 'Global warning',
|
||||
scope: { id: globalWarning },
|
||||
type: 'INCOMPLETE_PROGRAM',
|
||||
severity: 'WARNING',
|
||||
}),
|
||||
makeWarning({
|
||||
description: 'Info warning',
|
||||
scope: { id: 'node-2' },
|
||||
severity: 'INFO',
|
||||
}),
|
||||
];
|
||||
|
||||
getStateSpy = jest
|
||||
.spyOn(useFlowStore, 'getState')
|
||||
.mockReturnValue({
|
||||
getWarnings: () => mockWarnings,
|
||||
} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getStateSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('renders warnings header', () => {
|
||||
render(<WarningsSidebar />);
|
||||
expect(screen.getByText('Warnings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all warning descriptions', () => {
|
||||
render(<WarningsSidebar />);
|
||||
expect(screen.getByText('Node warning')).toBeInTheDocument();
|
||||
expect(screen.getByText('Global warning')).toBeInTheDocument();
|
||||
expect(screen.getByText('Info warning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('splits global and other warnings correctly', () => {
|
||||
render(<WarningsSidebar />);
|
||||
expect(screen.getByText('global:')).toBeInTheDocument();
|
||||
expect(screen.getByText('other:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when no warnings exist', () => {
|
||||
getStateSpy.mockReturnValueOnce({
|
||||
getWarnings: () => [],
|
||||
} as any);
|
||||
|
||||
render(<WarningsSidebar />);
|
||||
expect(screen.getByText('No warnings!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters by severity', () => {
|
||||
render(<WarningsSidebar />);
|
||||
fireEvent.click(screen.getByText('ERROR'));
|
||||
|
||||
expect(screen.getByText('Node warning')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Global warning')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Info warning')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters INFO severity correctly', () => {
|
||||
render(<WarningsSidebar />);
|
||||
fireEvent.click(screen.getByText('INFO'));
|
||||
|
||||
expect(screen.getByText('Info warning')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Node warning')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Global warning')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking global warning does NOT jump', () => {
|
||||
render(<WarningsSidebar />);
|
||||
fireEvent.click(screen.getByText('Global warning'));
|
||||
|
||||
expect(setCenter).not.toHaveBeenCalled();
|
||||
expect(addSelectedNodes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing if node does not exist', () => {
|
||||
getNode.mockReturnValue(undefined);
|
||||
|
||||
render(<WarningsSidebar />);
|
||||
fireEvent.click(screen.getByText('Node warning'));
|
||||
|
||||
expect(setCenter).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user