feat: The Big One UI
This commit is contained in:
committed by
Pim Hutting
parent
f9e0eb95f8
commit
82785dc8cb
@@ -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);
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -105,6 +105,8 @@ describe("SaveLoadPanel - combined tests", () => {
|
||||
});
|
||||
|
||||
test("onLoad with invalid JSON does not update store", async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const file = new File(["not json"], "bad.json", { type: "application/json" });
|
||||
file.text = jest.fn(() => Promise.resolve(`{"bad json`));
|
||||
|
||||
@@ -112,20 +114,19 @@ describe("SaveLoadPanel - combined tests", () => {
|
||||
|
||||
render(<SaveLoadPanel />);
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
expect(input).toBeTruthy();
|
||||
|
||||
// Give some input
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { files: [file] } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.alert).toHaveBeenCalledTimes(1);
|
||||
|
||||
const nodesAfter = useFlowStore.getState().nodes;
|
||||
expect(nodesAfter).toHaveLength(0);
|
||||
expect(input.value).toBe("");
|
||||
});
|
||||
|
||||
// Clean up the spy
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("onLoad resolves to null when no file is chosen (user cancels) and does not update store", async () => {
|
||||
|
||||
@@ -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