/**
* 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 {
type TreeData,
type TreeNodeContainerProps,
clsx,
PanelLoadingIndicator,
PlayIcon,
TreeView,
ChevronDownIcon,
ChevronRightIcon,
RefreshIcon,
TimesCircleIcon,
CheckCircleIcon,
ContextMenu,
MenuContent,
MenuContentItem,
Dialog,
WarningIcon,
CircleNotchIcon,
EmptyCircleIcon,
PanelContent,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
ModalFooterButton,
Panel,
PanelHeader,
OffIcon,
ControlledDropdownMenu,
MenuContentItemIcon,
CheckIcon,
MenuContentItemLabel,
MoreVerticalIcon,
ResizablePanelGroup,
ResizablePanel,
ResizablePanelSplitter,
} from '@finos/legend-art';
import {
AssertFail,
AssertionStatus,
EqualToJsonAssertFail,
PackageableElement,
TestError,
} from '@finos/legend-graph';
import {
type GeneratorFn,
isNonNullable,
prettyCONSTName,
} from '@finos/legend-shared';
import { observer } from 'mobx-react-lite';
import React, { forwardRef, useEffect, useState } from 'react';
import { TEST_RUNNER_TABS } from '../../../../stores/editor/EditorConfig.js';
import {
type TestableExplorerTreeNodeData,
type GlobalTestRunnerState,
type TestableState,
TESTABLE_RESULT,
AtomicTestTreeNodeData,
AssertionTestTreeNodeData,
TestableTreeNodeData,
TestTreeNodeData,
getNodeTestableResult,
getAtomicTest_TestResult,
getAssertionStatus,
} from '../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js';
import { LEGEND_STUDIO_TEST_ID } from '../../../../__lib__/LegendStudioTesting.js';
import { getElementTypeIcon } from '../../../ElementIconUtils.js';
import { UnsupportedEditorPanel } from '../../editor-group/UnsupportedElementEditor.js';
import { useEditorStore } from '../../EditorStoreProvider.js';
import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor';
import { CodeEditor, CodeDiffView } from '@finos/legend-lego/code-editor';
export const getTestableResultIcon = (
testableResult: TESTABLE_RESULT,
): React.ReactNode => {
switch (testableResult) {
case TESTABLE_RESULT.PASSED:
return (
);
case TESTABLE_RESULT.IN_PROGRESS:
return (
);
case TESTABLE_RESULT.FAILED:
return (
);
case TESTABLE_RESULT.ERROR:
return (
);
case TESTABLE_RESULT.NO_TESTS:
return (
);
default:
return (
);
}
};
const getOptionalError = (
node: TestableExplorerTreeNodeData,
testableState: TestableState,
): TestError | AssertFail | Map | undefined => {
if (node instanceof AtomicTestTreeNodeData) {
const result = getAtomicTest_TestResult(
node.atomicTest,
testableState.results,
);
if (result instanceof TestError) {
return result;
}
} else if (node instanceof AssertionTestTreeNodeData) {
const status = getAssertionStatus(node.assertion, testableState.results);
if (status instanceof AssertFail) {
return status;
} else if (status && !(status instanceof AssertionStatus)) {
const errorState = new Map();
Array.from(status.entries()).forEach(([key, assertionStatus]) => {
if (assertionStatus instanceof AssertFail) {
errorState.set(key, assertionStatus);
}
});
return errorState;
}
}
return undefined;
};
const TestFailViewer = observer(
(props: {
globalTestRunnerState: GlobalTestRunnerState;
failure: TestError | AssertFail;
}) => {
const { globalTestRunnerState, failure } = props;
const applicationStore = globalTestRunnerState.editorStore.applicationStore;
const id =
failure instanceof TestError
? failure.atomicTest.id
: failure.assertion.id;
const closeLogViewer = (): void =>
globalTestRunnerState.setFailureViewing(undefined);
return (
{failure instanceof TestError && (
)}
{failure instanceof EqualToJsonAssertFail && (
)}
{failure instanceof AssertFail &&
!(failure instanceof EqualToJsonAssertFail) && (
)}
);
},
);
const TestableExplorerContextMenu = observer(
forwardRef<
HTMLDivElement,
{
globalTestRunnerState: GlobalTestRunnerState;
testableState: TestableState;
node: TestableExplorerTreeNodeData;
treeData: TreeData;
error?: TestError | AssertFail | Map | undefined;
}
>(function TestableExplorerContextMenu(props, ref) {
const { node, error, globalTestRunnerState, testableState } = props;
const runTest = (): void => {
testableState.run(node);
};
const viewError = (err: TestError | AssertFail): void =>
globalTestRunnerState.setFailureViewing(err);
const visitTestable = (): void => {
globalTestRunnerState.visitTestable(
testableState.testableMetadata.testable,
);
};
return (
Run
Open Testable
{error &&
(error instanceof TestError || error instanceof AssertFail) && (
viewError(error)}>
{error instanceof TestError ? 'View Error' : 'View assert fail'}
)}
{error &&
!(error instanceof TestError || error instanceof AssertFail) &&
Array.from(error.entries()).map(([key, testError]) => (
viewError(testError)}
>
{testError instanceof TestError
? `View Error for ${key}`
: `View assert fail for ${key}`}
))}
);
}),
);
const TestableTreeNodeContainer: React.FC<
TreeNodeContainerProps<
TestableExplorerTreeNodeData,
{
globalTestRunnerState: GlobalTestRunnerState;
testableState: TestableState;
treeData: TreeData;
isDependency?: boolean | undefined;
}
>
> = (props) => {
const { node, level, stepPaddingInRem, onNodeSelect } = props;
const { treeData, testableState, globalTestRunnerState, isDependency } =
props.innerProps;
const editorStore = useEditorStore();
const results = testableState.results;
const expandIcon =
node instanceof AssertionTestTreeNodeData ? (
) : node.isOpen ? (
) : (
);
const nodeIcon =
node instanceof TestableTreeNodeData
? node.testableMetadata.testable instanceof PackageableElement
? getElementTypeIcon(
editorStore.graphState.getPackageableElementType(
node.testableMetadata.testable,
),
editorStore,
)
: null
: null;
const resultIcon = getTestableResultIcon(
getNodeTestableResult(
node,
isDependency
? testableState.globalTestRunnerState.isRunningDependencyTests
.isInProgress
: testableState.globalTestRunnerState.isRunningTests.isInProgress,
results,
),
);
const optionalError = getOptionalError(node, testableState);
const selectNode: React.MouseEventHandler = (event) => onNodeSelect?.(node);
const openTestable: React.MouseEventHandler = (event) => {
event.stopPropagation();
event.preventDefault();
globalTestRunnerState.visitTestable(
testableState.testableMetadata.testable,
);
};
const dblClick = (): void => {
if (
optionalError instanceof TestError ||
optionalError instanceof AssertFail
) {
globalTestRunnerState.setFailureViewing(optionalError);
}
};
return (
}
menuProps={{ elevation: 7 }}
>
{expandIcon}
{resultIcon}
{nodeIcon && (
{nodeIcon}
)}
{node instanceof TestableTreeNodeData && (
{node.testableMetadata.name}
)}
{node instanceof TestTreeNodeData && (
{node.label}
)}
{node instanceof AssertionTestTreeNodeData && (
{node.label}
)}
);
};
export const TestablePanelRunner = observer(
(props: { globalTestRunnerState: GlobalTestRunnerState }) => {
const { globalTestRunnerState } = props;
const isDispatchingAction =
globalTestRunnerState.isDispatchingOwnProjectAction;
const showDependencyPanel = globalTestRunnerState.showDependencyPanel;
const toggleShowDependencyTests = (): void => {
globalTestRunnerState.setShowDependencyPanel(!showDependencyPanel);
};
const runAllTests = (): GeneratorFn =>
globalTestRunnerState.runAllTests();
const reset = (): void => globalTestRunnerState.initOwnTestables(true);
//
const runDependencyTests = (): GeneratorFn =>
globalTestRunnerState.runDependenciesTests();
const isDispatchingDependencyAction =
globalTestRunnerState.isDispatchingDependencyAction;
const renderTestables = (): React.ReactNode => (
<>
{globalTestRunnerState.testableStates?.length ?? '0'}
{showDependencyPanel ? : null}
Show from dependency tests
}
menuProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
transformOrigin: {
vertical: 'top',
horizontal: 'left',
},
elevation: 7,
}}
>
{(globalTestRunnerState.testableStates ?? []).map((testableState) => {
const onNodeSelect = (node: TestableExplorerTreeNodeData): void => {
testableState.onTreeNodeSelect(node, testableState.treeData);
};
const getChildNodes = (
node: TestableExplorerTreeNodeData,
): TestableExplorerTreeNodeData[] => {
if (node.childrenIds) {
return node.childrenIds
.map((id) => testableState.treeData.nodes.get(id))
.filter(isNonNullable);
}
return [];
};
return (
);
})}
>
);
const renderDependenciesTestables = (): React.ReactNode => (
<>
{globalTestRunnerState.dependencyTestableStates?.length ?? '0'}
{(globalTestRunnerState.dependencyTestableStates ?? []).map(
(testableState) => {
const onNodeSelect = (node: TestableExplorerTreeNodeData): void => {
testableState.onTreeNodeSelect(node, testableState.treeData);
};
const getChildNodes = (
node: TestableExplorerTreeNodeData,
): TestableExplorerTreeNodeData[] => {
if (node.childrenIds) {
return node.childrenIds
.map((id) => testableState.treeData.nodes.get(id))
.filter(isNonNullable);
}
return [];
};
return (
);
},
)}
>
);
return (
{!showDependencyPanel && (
{renderTestables()}
)}
{showDependencyPanel && (
{renderTestables()}
{renderDependenciesTestables()}
)}
{globalTestRunnerState.failureViewing && (
)}
);
},
);
// TODO:
// - Handle Multi Execution Test Results
export const GlobalTestRunner = observer(
(props: { globalTestRunnerState: GlobalTestRunnerState }) => {
const editorStore = useEditorStore();
const globalTestRunnerState = props.globalTestRunnerState;
const isDispatchingAction =
globalTestRunnerState.isDispatchingOwnProjectAction;
const [selectedTab, setSelectedTab] = useState(
TEST_RUNNER_TABS.TEST_RUNNER.valueOf(),
);
const extractTestRunnerTabConfigurations = editorStore.pluginManager
.getApplicationPlugins()
.flatMap(
(plugin) => plugin.getExtraTestRunnerViewConfigurations?.() ?? [],
)
.filter((configuration) => Boolean(configuration.renderer(editorStore)));
const testRunnerTabs = (Object.values(TEST_RUNNER_TABS) as string[])
.map((e) => ({
value: e,
label: prettyCONSTName(e),
}))
.concat(
extractTestRunnerTabConfigurations.map((config) => ({
value: config.key,
label: config.title,
})),
);
const changeTab = (tab: string): void => {
setSelectedTab(tab);
};
useEffect(() => {
editorStore.globalTestRunnerState.initOwnTestables();
}, [editorStore.globalTestRunnerState]);
const renderSelectedTab = (): React.ReactNode => {
if (selectedTab === TEST_RUNNER_TABS.TEST_RUNNER) {
return (
<>
>
);
}
for (const testRunnerTabConfiguration of extractTestRunnerTabConfigurations) {
if (testRunnerTabConfiguration.key === selectedTab) {
return testRunnerTabConfiguration.renderer(editorStore);
}
}
return (
);
};
return (
{testRunnerTabs.map((tab) => (
changeTab(tab.value)}
className={clsx('panel__header__tab', {
['panel__header__tab--active']: tab.value === selectedTab,
})}
>
{tab.label}
))}
{renderSelectedTab()}
);
},
);