/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { observer } from 'mobx-react-lite';
import {
BlankPanelPlaceholder,
BlankPanelContent,
ContextMenu,
CustomSelectorInput,
Dialog,
ErrorWarnIcon,
FilledWindowMaximizeIcon,
MenuContent,
MenuContentItem,
Modal,
ModalBody,
ModalFooter,
ModalFooterButton,
ModalHeader,
ModalTitle,
Panel,
PanelFormTextField,
PanelHeader,
PanelHeaderActionItem,
PanelHeaderActions,
PlayIcon,
PlusIcon,
RefreshIcon,
ResizablePanel,
ResizablePanelGroup,
ResizablePanelSplitter,
ResizablePanelSplitterLine,
RunAllIcon,
RunErrorsIcon,
TimesIcon,
clsx,
} from '@finos/legend-art';
import { forwardRef, useEffect, useRef, useState } from 'react';
import {
type FunctionTestSuite,
type DataElement,
type EmbeddedData,
type ValueSpecification,
DataElementReference,
PackageableElementExplicitReference,
RelationalCSVData,
ModelStoreData,
ExternalFormatData,
ModelEmbeddedData,
PrimitiveInstanceValue,
PrimitiveType,
} from '@finos/legend-graph';
import {
FunctionValueSpecificationTestParameterState,
type FunctionStoreTestDataState,
type FunctionTestState,
type FunctionTestSuiteState,
type FunctionTestableState,
} from '../../../../../stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js';
import {
TESTABLE_RESULT,
getTestableResultFromTestResult,
} from '../../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js';
import { flowResult } from 'mobx';
import { getTestableResultIcon } from '../../../side-bar/testable/GlobalTestRunner.js';
import {
ExternalFormatParameterEditorModal,
RenameModal,
SharedDataElementModal,
TestAssertionEditor,
} from '../../testable/TestableSharedComponents.js';
import {
filterByType,
prettyCONSTName,
returnUndefOnError,
} from '@finos/legend-shared';
import {
EmbeddedDataCreatorFromEmbeddedData,
validateTestableId,
type TestParamContentType,
getContentTypeWithParamFromQuery,
} from '../../../../../stores/editor/utils/TestableUtils.js';
import { EmbeddedDataEditor } from '../../data-editor/EmbeddedDataEditor.js';
import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../../../../__lib__/LegendStudioApplicationNavigationContext.js';
import { useApplicationNavigationContext } from '@finos/legend-application';
import {
BasicValueSpecificationEditor,
instanceValue_setValue,
} from '@finos/legend-query-builder';
import { TESTABLE_TEST_TAB } from '../../../../../stores/editor/editor-state/element-editor-state/testable/TestableEditorState.js';
import { atomicTest_setDoc } from '../../../../../stores/graph-modifier/Testable_GraphModifierHelper.js';
const FunctionTestableContextMenu = observer(
forwardRef<
HTMLDivElement,
{
addName?: string;
_delete?: () => void;
rename?: () => void;
add?: () => void;
}
>(function TestContainerContextMenu(props, ref) {
const { addName, add, rename, _delete } = props;
const addTest = (): void => {
add?.();
};
const remove = (): void => _delete?.();
const handleRename = (): void => rename?.();
return (
{rename && (
Rename
)}
{_delete && Delete}
{addName && rename && (
{`Add ${addName}`}
)}
);
}),
);
const FunctionTestDataStateEditor = observer(
(props: {
functionTestSuiteState: FunctionTestSuiteState;
storeTestDataState: FunctionStoreTestDataState;
}) => {
const { functionTestSuiteState, storeTestDataState } = props;
const functionTestableState = functionTestSuiteState.functionTestableState;
const isReadOnly = functionTestableState.functionEditorState.isReadOnly;
const embeddedState = storeTestDataState.embeddedEditorState;
const currentData = embeddedState.embeddedData;
const isUsingReference = currentData instanceof DataElementReference;
const open = (): void => storeTestDataState.setDataElementModal(true);
const close = (): void => storeTestDataState.setDataElementModal(false);
const changeToUseMyOwn = (): void => {
if (isUsingReference) {
const newBare = returnUndefOnError(() =>
currentData.accept_EmbeddedDataVisitor(
new EmbeddedDataCreatorFromEmbeddedData(
functionTestableState.editorStore,
),
),
);
if (newBare) {
storeTestDataState.changeEmbeddedData(newBare);
}
}
};
const sharedDataHandler = (val: DataElement): void => {
const dataRef = new DataElementReference();
dataRef.dataElement = PackageableElementExplicitReference.create(val);
const dataElementValue = val.data;
let embeddedData: EmbeddedData = dataRef;
if (
currentData instanceof ModelStoreData &&
dataElementValue instanceof ExternalFormatData
) {
const modelStoreVal = currentData.modelData?.[0];
if (modelStoreVal instanceof ModelEmbeddedData) {
const newModelEmbeddedData = new ModelEmbeddedData();
newModelEmbeddedData.model =
PackageableElementExplicitReference.create(
modelStoreVal.model.value,
);
newModelEmbeddedData.data = dataRef;
const modelStoreData = new ModelStoreData();
modelStoreData.modelData = [newModelEmbeddedData];
embeddedData = modelStoreData;
}
}
storeTestDataState.changeEmbeddedData(embeddedData);
};
const dataElements =
functionTestSuiteState.editorStore.graphManagerState.graph.dataElements;
const filter = (val: DataElement): boolean => {
const dataElementData = val.data;
if (currentData instanceof RelationalCSVData) {
if (dataElementData instanceof RelationalCSVData) {
return true;
}
return false;
} else if (currentData instanceof ModelStoreData) {
if (
dataElementData instanceof ExternalFormatData ||
dataElementData instanceof ModelStoreData
) {
return true;
}
return false;
}
return true;
};
return (
{isUsingReference ? (
) : (
)}
{storeTestDataState.dataElementModal && (
)}
);
},
);
const FunctionTestParameterEditor = observer(
(props: {
isReadOnly: boolean;
paramState: FunctionValueSpecificationTestParameterState;
functionTestState: FunctionTestState;
contentTypeParamPair: TestParamContentType | undefined;
}) => {
const { functionTestState, paramState, isReadOnly, contentTypeParamPair } =
props;
const [showPopUp, setShowPopUp] = useState(false);
const paramIsRequired =
paramState.varExpression.multiplicity.lowerBound > 0;
const type = contentTypeParamPair
? contentTypeParamPair.contentType
: (paramState.varExpression.genericType?.value.rawType.name ?? 'unknown');
const paramValue =
paramState.varExpression.genericType?.value.rawType === PrimitiveType.BYTE
? atob(
(paramState.valueSpec as PrimitiveInstanceValue)
.values[0] as string,
)
: ((paramState.valueSpec as PrimitiveInstanceValue)
.values[0] as string);
const openInPopUp = (): void => setShowPopUp(!showPopUp);
const closePopUp = (): void => setShowPopUp(false);
const updateParamValue = (val: string): void => {
if (paramState.valueSpec instanceof PrimitiveInstanceValue) {
instanceValue_setValue(
paramState.valueSpec,
paramState.varExpression.genericType?.value.rawType ===
PrimitiveType.BYTE
? btoa(val)
: val,
0,
functionTestState.editorStore.changeDetectionState.observerContext,
);
paramState.updateValueSpecification(paramState.valueSpec);
}
};
return (
{paramState.parameterValue.name}
<>
{contentTypeParamPair ? (
) : (
{
paramState.updateValueSpecification(val);
}}
graph={functionTestState.editorStore.graphManagerState.graph}
observerContext={
functionTestState.editorStore.changeDetectionState
.observerContext
}
typeCheckOption={{
expectedType:
paramState.varExpression.genericType?.value.rawType ??
PrimitiveType.STRING,
}}
className="query-builder__parameters__value__editor"
resetValue={(): void => {
paramState.resetValueSpec();
}}
/>
)}
>
);
},
);
const NewParameterModal = observer(
(props: { functionTestState: FunctionTestState; isReadOnly: boolean }) => {
const { functionTestState, isReadOnly } = props;
const applicationStore = functionTestState.editorStore.applicationStore;
const currentOption = {
value: functionTestState.newParameterValueName,
label: functionTestState.newParameterValueName,
};
const options = functionTestState.newParamOptions;
const closeModal = (): void =>
functionTestState.setShowNewParameterModal(false);
const onChange = (val: { label: string; value: string } | null): void => {
if (val === null) {
functionTestState.setNewParameterValueName('');
} else if (val.value !== functionTestState.newParameterValueName) {
functionTestState.setNewParameterValueName(val.value);
}
};
return (
);
},
);
const FunctionTestEditor = observer(
(props: { functionTestState: FunctionTestState }) => {
const { functionTestState } = props;
const selectedTab = functionTestState.selectedTab;
const addParameter = (): void => {
functionTestState.setShowNewParameterModal(true);
};
const generateParameterValues = (): void => {
functionTestState.generateTestParameterValues();
};
useEffect(() => {
functionTestState.syncWithQuery();
}, [functionTestState]);
return (
{Object.values(TESTABLE_TEST_TAB).map((tab) => (
functionTestState.setSelectedTab(tab)}
className={clsx('service-test-editor__tab', {
'service-test-editor__tab--active':
tab === functionTestState.selectedTab,
})}
>
{prettyCONSTName(tab)}
))}
{selectedTab === TESTABLE_TEST_TAB.ASSERTION &&
functionTestState.selectedAsertionState && (
)}
{selectedTab === TESTABLE_TEST_TAB.SETUP &&
functionTestState.selectedAsertionState && (
<>
{Boolean(functionTestState.parameterValueStates.length) && (
{functionTestState.parameterValueStates
.filter(
filterByType(
FunctionValueSpecificationTestParameterState,
),
)
.map((paramState) => (
pair.param === paramState.parameterValue.name,
)}
/>
))}
{functionTestState.showNewParameterModal && (
)}
)}
>
)}
);
},
);
const FunctionTestItem = observer(
(props: {
suiteState: FunctionTestSuiteState;
functionTestState: FunctionTestState;
}) => {
const { functionTestState, suiteState } = props;
const functionTest = functionTestState.test;
const isRunning = functionTestState.runningTestAction.isInProgress;
const [isSelectedFromContextMenu, setIsSelectedFromContextMenu] =
useState(false);
const isReadOnly =
suiteState.functionTestableState.functionEditorState.isReadOnly;
const openTest = (): void => suiteState.changeTest(functionTest);
const isActive = suiteState.selectTestState?.test === functionTest;
const _testableResult = getTestableResultFromTestResult(
functionTestState.testResultState.result,
);
const testableResult = isRunning
? TESTABLE_RESULT.IN_PROGRESS
: _testableResult;
const resultIcon = getTestableResultIcon(testableResult);
const onContextMenuOpen = (): void => setIsSelectedFromContextMenu(true);
const onContextMenuClose = (): void => setIsSelectedFromContextMenu(false);
const add = (): void => {
// TODO
};
const _delete = (): void => {
suiteState.deleteTest(functionTest);
};
const rename = (): void => {
suiteState.functionTestableState.setRenameComponent(functionTest);
};
const runTest = (): void => {
flowResult(functionTestState.runTest()).catch(
functionTestState.editorStore.applicationStore.alertUnhandledError,
);
};
return (
}
menuProps={{ elevation: 7 }}
onOpen={onContextMenuOpen}
onClose={onContextMenuClose}
>
{resultIcon}
{functionTest.id}
);
},
);
const CreateTestModal = observer(
(props: { functionSuiteState: FunctionTestSuiteState }) => {
const { functionSuiteState } = props;
const applicationStore = functionSuiteState.editorStore.applicationStore;
const suite = functionSuiteState.suite;
// test name
const [id, setId] = useState(undefined);
const isValid = id && !id.includes(' ');
const errorMessage = validateTestableId(
id,
suite.tests.map((t) => t.id),
);
const close = (): void => functionSuiteState.setShowModal(false);
const create = (): void => {
if (id) {
flowResult(functionSuiteState.addNewTest(id))
.then(() => close())
.catch(
functionSuiteState.editorStore.applicationStore.alertUnhandledError,
);
}
};
return (
);
},
);
// ──────────────────────────────────────────────────────────────────────────────
// Add Element Modal
// ──────────────────────────────────────────────────────────────────────────────
const AddDataElementModal = observer(
(props: { functionTestSuiteState: FunctionTestSuiteState }) => {
const { functionTestSuiteState } = props;
const dataState = functionTestSuiteState.dataState;
const applicationStore =
functionTestSuiteState.editorStore.applicationStore;
const options = dataState.availableElementsToAdd.map((e) => ({
value: e.path,
label: e.path,
}));
const [selectedPath, setSelectedPath] = useState(
options[0]?.value,
);
const close = (): void => dataState.setShowAddElementModal(false);
const add = (): void => {
if (selectedPath) {
dataState.addDataElement(selectedPath);
close();
}
};
const onChange = (val: { label: string; value: string } | null): void => {
setSelectedPath(val?.value);
};
return (
);
},
);
// ──────────────────────────────────────────────────────────────────────────────
// Test Data Editor (left panel) — elements list + per-element data editor
// ──────────────────────────────────────────────────────────────────────────────
const FunctionTestDataPanel = observer(
(props: { functionTestSuiteState: FunctionTestSuiteState }) => {
const { functionTestSuiteState } = props;
const dataState = functionTestSuiteState.dataState;
const hasTestData = Boolean(dataState.baseTestData.length);
const addStoreTestData = (): void => {
dataState.setShowAddElementModal(true);
};
return (
{!hasTestData ? (
Add an element to configure test data
) : (
{dataState.baseTestData.map((td) => (
dataState.openStoreTestData(td)}
tabIndex={-1}
>
{td.element.value.name}
))}
)}
{dataState.selectedDataState ? (
) : (
Select an element to configure its test data
)}
{dataState.showAddElementModal && (
)}
);
},
);
// ──────────────────────────────────────────────────────────────────────────────
// Tests Editor (right panel) — tests list + test detail (assertion/setup)
// ──────────────────────────────────────────────────────────────────────────────
const FunctionTestsPanel = observer(
(props: { functionTestSuiteState: FunctionTestSuiteState }) => {
const { functionTestSuiteState } = props;
const editorStore = functionTestSuiteState.editorStore;
const isReadOnly =
functionTestSuiteState.functionTestableState.functionEditorState
.isReadOnly;
const selectedTestState = functionTestSuiteState.selectTestState;
const addTest = (): void => {
functionTestSuiteState.setShowModal(true);
};
const runTests = (): void => {
flowResult(functionTestSuiteState.runSuite()).catch(
editorStore.applicationStore.alertUnhandledError,
);
};
const runFailingTests = (): void => {
flowResult(functionTestSuiteState.runFailingTests()).catch(
editorStore.applicationStore.alertUnhandledError,
);
};
return (
{functionTestSuiteState.testStates.map((test) => (
))}
{selectedTestState ? (
) : (
)}
{functionTestSuiteState.showCreateModal && (
)}
);
},
);
// ──────────────────────────────────────────────────────────────────────────────
// Suite Editor — horizontal split: test data (left) + tests (right)
// ──────────────────────────────────────────────────────────────────────────────
const FunctionTestSuiteEditor = observer(
(props: { functionTestSuiteState: FunctionTestSuiteState }) => {
const { functionTestSuiteState } = props;
return (
);
},
);
// ──────────────────────────────────────────────────────────────────────────────
// Suite Tab Context Menu (rename/delete)
// ──────────────────────────────────────────────────────────────────────────────
const SuiteHeaderTabContextMenu = observer(
forwardRef<
HTMLDivElement,
{
suite: FunctionTestSuite;
functionTestableState: FunctionTestableState;
}
>(function SuiteHeaderTabContextMenu(props, ref) {
const { suite, functionTestableState } = props;
const deleteSuite = (): void => {
functionTestableState.deleteTestSuite(suite);
};
const rename = (): void => {
functionTestableState.setRenameComponent(suite);
};
return (
Rename
Delete
);
}),
);
// ──────────────────────────────────────────────────────────────────────────────
// Create Suite Modal
// ──────────────────────────────────────────────────────────────────────────────
const CreateFunctionTestSuiteModal = observer(
(props: { functionTestableEditorState: FunctionTestableState }) => {
const { functionTestableEditorState } = props;
const applicationStore =
functionTestableEditorState.editorStore.applicationStore;
const inputRef = useRef(null);
const handleEnter = (): void => inputRef.current?.focus();
const [suiteName, setSuiteName] = useState(undefined);
const [testName, setTestName] = useState(undefined);
const isValid = suiteName && testName;
const close = (): void => functionTestableEditorState.setCreateSuite(false);
const create = (): void => {
if (suiteName && testName) {
flowResult(
functionTestableEditorState.createSuite(suiteName, testName),
).catch(
functionTestableEditorState.editorStore.applicationStore
.alertUnhandledError,
);
}
};
return (
);
},
);
// ──────────────────────────────────────────────────────────────────────────────
// Main Testing Tab — suite tabs at top, suite editor below
// ──────────────────────────────────────────────────────────────────────────────
export const FunctionTestableEditor = observer(
(props: { functionTestableState: FunctionTestableState }) => {
const { functionTestableState } = props;
const suites = functionTestableState.function.tests;
const functionEditorState = functionTestableState.functionEditorState;
const isReadOnly = functionEditorState.isReadOnly;
const selectedSuiteState = functionTestableState.selectedTestSuite;
useEffect(() => {
functionTestableState.init();
}, [functionTestableState]);
const addSuite = (): void => {
functionTestableState.setCreateSuite(true);
};
const changeSuite = (suite: FunctionTestSuite): void => {
functionTestableState.changeSuite(suite);
};
const runSuites = (): void => {
functionTestableState.runTestable();
};
const renameTestingComponent = (val: string): void => {
functionTestableState.renameTestableComponent(val);
};
useApplicationNavigationContext(
LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.FUNCTION_EDITOR_TEST,
);
return (
{functionTestableState.createSuiteModal && (
)}
{suites.length ? (
{suites.map((suite) => (
changeSuite(suite)}
className={clsx('service-test-suite-editor__tab', {
'service-test-suite-editor__tab--active':
selectedSuiteState?.suite === suite,
})}
>
}
>
{suite.id}
))}
) : (
)}
{selectedSuiteState && (
)}
{!suites.length && (
)}
{functionTestableState.testableComponentToRename && (
functionTestableState.setRenameComponent(undefined)
}
setValue={(val: string): void => renameTestingComponent(val)}
errorMessageFunc={(_val: string | undefined) =>
validateTestableId(_val, undefined)
}
/>
)}
);
},
);