Skip to content

feat(vscode): Introducing unit testing codeful experience to vscode #6845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1f2988c
fix(vscode): Introducing unit test codeful private preview (#6216)
ccastrotrejo Dec 6, 2024
4e4e5af
Merge branch 'main' of https://github.com/Azure/LogicAppsUX into deve…
ccastrotrejo Dec 17, 2024
cf86788
feat(vscode): Added Blank Unit Test Generation Option to "Show Run UI…
samikay101 Jan 7, 2025
a457339
feat(vscode): Add JSON Parsing for Blank Unit Test Generation for Log…
samikay101 Jan 14, 2025
118ade8
Merge branch 'main' of https://github.com/Azure/LogicAppsUX into deve…
ccastrotrejo Jan 15, 2025
f56665d
Revert designer.tsx
ccastrotrejo Jan 15, 2025
b94bbb7
Merge branch 'main' of https://github.com/Azure/LogicAppsUX into deve…
ccastrotrejo Jan 23, 2025
dbe9edc
Fix import
ccastrotrejo Jan 23, 2025
ac62dee
feat(vscode): Added Blank Unit Test Icon Option to Designer View (#6462)
samikay101 Jan 27, 2025
6cda6c0
feat(vscode): Refactored Unit Test Commands for Improved Maintainabil…
samikay101 Jan 27, 2025
ada90d5
feat(vscode): Add Logic to Create C# Classes for Blank Unit Tests (#6…
samikay101 Jan 29, 2025
fae7524
Merge branch 'main' of https://github.com/Azure/LogicAppsUX into deve…
ccastrotrejo Jan 29, 2025
d06d74e
feat(vscode): Update unit testing template file per new OperationMock…
andrew-eldridge Feb 3, 2025
ea0cca3
Merge branch 'main' of https://github.com/Azure/LogicAppsUX into deve…
ccastrotrejo Feb 3, 2025
0eccc5e
feat(vscode): Added workspace requirement for unit test (#6538)
lambrianmsft Feb 4, 2025
5b82232
feat(vscode): Improved Unit Test Generation UI & C# Class Structure (…
samikay101 Feb 4, 2025
645ef50
fix(vscode): Correct toPascalCase regex (#6525)
andrew-eldridge Feb 6, 2025
4b89bd9
feat(vscode): Use listMockableOperations API for create blank unit te…
andrew-eldridge Feb 13, 2025
d763fe9
Merge branch 'main' of https://github.com/Azure/LogicAppsUX into deve…
ccastrotrejo Feb 14, 2025
eab3141
Checkout designer
ccastrotrejo Feb 14, 2025
9427d5b
fix(vscode): Fix build and test errors for dev branch (#6619)
ccastrotrejo Feb 18, 2025
8685b12
feat(vscode): Add Unit Test Generation Classes for Workflow Run (#6580)
samikay101 Feb 20, 2025
e992e65
fix(vscode): default StatusCode in generated class ctors, remove dupl…
andrew-eldridge Feb 21, 2025
6b819b4
fix(vscode): bug where DateTime properties have invalid type in gener…
andrew-eldridge Feb 21, 2025
1dea9ea
feat(vscode): rename test to show button update (#6655)
samikay101 Feb 21, 2025
68b9657
feat(vscode): Template updates, mock file generation updates (#6647)
lambrianmsft Feb 27, 2025
c7590bd
fix(vscode): automated testing bug in config workspace path and C# cl…
andrew-eldridge Feb 28, 2025
4f3558a
fix(vscode): Adjust Folder Insertion to End of Workspace (#6705)
samikay101 Feb 28, 2025
d976f61
fix(vscode): use cleaned logicAppName in TestExecutor, update create …
andrew-eldridge Mar 1, 2025
821bf97
fix(vscode): updateCsprojFile not called in create from run (#6728)
andrew-eldridge Mar 4, 2025
477070c
feat(vscode): Add function to create and update solution file with Lo…
samikay101 Mar 10, 2025
29927af
Merge branch 'main' of https://github.com/Azure/LogicAppsUX into dev/…
ccastrotrejo Mar 11, 2025
d19dd43
Add back themeConnect
ccastrotrejo Mar 11, 2025
b1199fc
Merge branch 'main' into dev/unitTestCodeful
lambrianmsft Mar 20, 2025
c3e8a16
Address nits
lambrianmsft Mar 21, 2025
d92f846
Update snapshots
ccastrotrejo Mar 21, 2025
0e05ff2
Update unit test for vscode
ccastrotrejo Mar 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions Localize/lang/strings.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import {
updateParameterValidation,
openPanel,
useNodesInitialized,
serializeUnitTestDefinition,
useAssertionsValidationErrors,
getNodeOutputOperations,
getCustomCodeFilesWithData,
resetDesignerDirtyState,
collapsePanel,
Expand Down Expand Up @@ -69,6 +72,7 @@ export const DesignerCommandBar = ({
showRunHistory,
toggleRunHistory,
enableCopilot,
isUnitTest,
switchViews,
saveWorkflowFromCode,
toggleMonitoringView,
Expand All @@ -82,6 +86,7 @@ export const DesignerCommandBar = ({
isDesignerView?: boolean;
isMonitoringView?: boolean;
isDarkMode: boolean;
isUnitTest: boolean;
showConnectionsPanel?: boolean;
showRunHistory?: boolean;
toggleRunHistory: () => void;
Expand Down Expand Up @@ -135,6 +140,21 @@ export const DesignerCommandBar = ({
}
}
});
const { isLoading: isSavingUnitTest, mutate: saveUnitTestMutate } = useMutation(async () => {
const designerState = DesignerStore.getState();
const definition = await serializeUnitTestDefinition(designerState);

console.log(definition);
alert('Check console for unit test serialization');
});

const { isLoading: isSavingBlankUnitTest, mutate: saveBlankUnitTestMutate } = useMutation(async () => {
const designerState = DesignerStore.getState();
const operationContents = await getNodeOutputOperations(designerState);

console.log(operationContents);
alert('Check console for blank unit test operationContents');
});

const { isLoading: isDownloadingDocument, mutate: downloadDocument } = useMutation(async () => {
const designerState = DesignerStore.getState();
Expand Down Expand Up @@ -183,10 +203,13 @@ export const DesignerCommandBar = ({
});
const allWorkflowParameterErrors = useWorkflowParameterValidationErrors();
const haveWorkflowParameterErrors = Object.keys(allWorkflowParameterErrors ?? {}).length > 0;
const allAssertionsErrors = useAssertionsValidationErrors();
const haveAssertionErrors = Object.keys(allAssertionsErrors ?? {}).length > 0;
const allSettingsErrors = useAllSettingsValidationErrors();
const haveSettingsErrors = Object.keys(allSettingsErrors ?? {}).length > 0;
const allConnectionErrors = useAllConnectionErrors();
const haveConnectionErrors = Object.keys(allConnectionErrors ?? {}).length > 0;
const saveBlankUnitTestIsDisabled = !isUnitTest || isSavingBlankUnitTest || haveAssertionErrors;

const haveErrors = useMemo(
() => allInputErrors.length > 0 || haveWorkflowParameterErrors || haveSettingsErrors || haveConnectionErrors,
Expand All @@ -195,6 +218,7 @@ export const DesignerCommandBar = ({

const saveIsDisabled = isSaving || allInputErrors.length > 0 || haveWorkflowParameterErrors || haveSettingsErrors || !designerIsDirty;

const saveUnitTestIsDisabled = !isUnitTest || isSavingUnitTest || haveAssertionErrors;
const isUndoDisabled = !useCanUndo();
const isRedoDisabled = !useCanRedo();

Expand Down Expand Up @@ -289,6 +313,36 @@ export const DesignerCommandBar = ({
}
},
},
{
key: 'saveUnitTest',
text: 'Save Unit Test',
disabled: saveUnitTestIsDisabled,
onRenderIcon: () => {
return isSavingUnitTest ? (
<Spinner size={'extra-tiny'} />
) : (
<FontIcon aria-label="Save" iconName="Save" className={saveUnitTestIsDisabled ? classNames.azureGrey : classNames.azureBlue} />
);
},
onClick: () => {
saveUnitTestMutate();
},
},
{
key: 'saveBlankUnitTest',
text: 'Save Blank Unit Test',
disabled: saveBlankUnitTestIsDisabled,
onRenderIcon: () => {
return isSavingBlankUnitTest ? (
<Spinner size="small" />
) : (
<FontIcon aria-label="Save" iconName="Save" className={classNames.azureBlue} />
);
},
onClick: () => {
saveBlankUnitTestMutate();
},
},
{
key: 'discard',
disabled: isSaving || !isDesignerView,
Expand All @@ -306,6 +360,15 @@ export const DesignerCommandBar = ({
onClick: () => !!dispatch(openPanel({ panelMode: 'WorkflowParameters' })),
onRenderText: (item: { text: string }) => <CustomCommandBarButton text={item.text} showError={haveWorkflowParameterErrors} />,
},
{
key: 'Assertions',
text: 'Assertions',
ariaLabel: 'Assertions',
iconProps: { iconName: 'CheckMark' },
disabled: !isUnitTest,
onClick: () => !!dispatch(openPanel({ panelMode: 'Assertions' })),
onRenderText: (item: { text: string }) => <CustomCommandBarButton text={item.text} showError={haveAssertionErrors} />,
},
{
key: 'codeview',
text: isDesignerView ? 'Code View' : 'Designer View',
Expand Down Expand Up @@ -395,9 +458,11 @@ export const DesignerCommandBar = ({
isSaving,
isDesignerView,
showConnectionsPanel,
isSavingBlankUnitTest,
saveBlankUnitTestIsDisabled,
saveBlankUnitTestMutate,
haveErrors,
isDarkMode,
isCopilotReady,
isUndoDisabled,
isRedoDisabled,
saveWorkflowMutate,
Expand All @@ -408,6 +473,12 @@ export const DesignerCommandBar = ({
switchViews,
haveConnectionErrors,
enableCopilot,
isCopilotReady,
isUnitTest,
saveUnitTestMutate,
isSavingUnitTest,
saveUnitTestIsDisabled,
haveAssertionErrors,
isDownloadingDocument,
downloadDocument,
baseEndItems,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const DesignerEditor = () => {
const {
isReadOnly,
isDarkMode,
isUnitTest,
isMonitoringView,
runId,
appId,
Expand Down Expand Up @@ -397,6 +398,7 @@ const DesignerEditor = () => {
isDarkMode,
readOnly: isReadOnly,
isMonitoringView,
isUnitTest,
suppressDefaultNodeSelectFunctionality: suppressDefaultNodeSelect,
hostOptions: {
...hostOptions,
Expand Down Expand Up @@ -444,6 +446,7 @@ const DesignerEditor = () => {
discard={discardAllChanges}
location={canonicalLocation}
isReadOnly={isReadOnly}
isUnitTest={isUnitTest}
isDarkMode={isDarkMode}
isDesignerView={designerView}
showConnectionsPanel={showConnectionsPanel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ const DesignerEditorConsumption = () => {
isReadOnly={readOnly}
isDarkMode={isDarkMode}
isDesignerView={designerView}
isUnitTest={false}
isMonitoringView={isMonitoringView}
showConnectionsPanel={showConnectionsPanel}
enableCopilot={() => dispatch(setIsChatBotEnabled(!showChatBot))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useIsReadOnly,
useShowConnectionsPanel,
useHostOptions,
useIsUnitTestView,
useShowPerformanceDebug,
useSuppressDefaultNodeSelect,
useStringOverrides,
Expand All @@ -20,6 +21,7 @@ import {
setAreCustomEditorsEnabled,
setShowConnectionsPanel,
setHostOptions,
setUnitTest,
setShowPerformanceDebug,
setSuppressDefaultNodeSelect,
setStringOverrides,
Expand All @@ -33,6 +35,7 @@ import { useDispatch } from 'react-redux';
const ContextSettings = () => {
const isReadOnly = useIsReadOnly();
const isMonitoringView = useIsMonitoringView();
const isUnitTest = useIsUnitTestView();
const isDarkMode = useIsDarkMode();
const showConnectionsPanel = useShowConnectionsPanel();
const areCustomEditorsEnabled = useAreCustomEditorsEnabled();
Expand All @@ -54,6 +57,17 @@ const ContextSettings = () => {
[dispatch]
);

const changeUnitTestView = useCallback(
(_: unknown, checked?: boolean) => {
dispatch(setUnitTest(!!checked));
if (checked) {
dispatch(loadRun());
dispatch(loadWorkflow());
}
},
[dispatch]
);

return (
<div style={{ display: 'flex', gap: '24px', flexWrap: 'wrap' }}>
<Checkbox
Expand All @@ -63,6 +77,7 @@ const ContextSettings = () => {
onChange={(_, checked) => dispatch(setReadOnly(!!checked))}
/>
<Checkbox label="Monitoring View" checked={isMonitoringView} onChange={changeMonitoringView} />
<Checkbox label="Unit Test View" checked={isUnitTest} onChange={changeUnitTestView} />
<Checkbox label="Dark Mode" checked={isDarkMode} onChange={(_, checked) => dispatch(setDarkMode(!!checked))} />
<Checkbox
label="Custom Editors"
Expand Down
1 change: 1 addition & 0 deletions apps/Standalone/src/designer/state/historyHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const setStateHistory = (state: WorkflowLoadingState): void => {
isDarkMode: state.isDarkMode,
isReadOnly: state.isReadOnly,
isMonitoringView: state.isMonitoringView,
isUnitTest: state.isUnitTest,
};
window.localStorage.setItem('msla-standalone-stateHistory', JSON.stringify(filteredState));
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const useIsMonitoringView = () => {
return useSelector((state: RootState) => state.workflowLoader.isMonitoringView);
};

export const useIsUnitTestView = () => {
return useSelector((state: RootState) => state.workflowLoader.isUnitTest);
};

export const useResourcePath = () => {
return useSelector((state: RootState) => state.workflowLoader.resourcePath);
};
Expand Down
10 changes: 10 additions & 0 deletions apps/Standalone/src/designer/state/workflowLoadingSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface WorkflowLoadingState {
isDarkMode: boolean;
hostingPlan: HostingPlanTypes;
isLocal: boolean;
isUnitTest: boolean;
showChatBot?: boolean;
showRunHistory?: boolean;
parameters: Record<string, WorkflowParameter>;
Expand Down Expand Up @@ -51,6 +52,7 @@ const initialState: WorkflowLoadingState = {
resourcePath: '',
isReadOnly: false,
isMonitoringView: false,
isUnitTest: false,
isDarkMode: false,
hostingPlan: 'standard',
isLocal: false,
Expand Down Expand Up @@ -155,6 +157,12 @@ export const workflowLoadingSlice = createSlice({
state.isReadOnly = true;
}
},
setUnitTest: (state, action: PayloadAction<boolean>) => {
state.isUnitTest = action.payload;
if (action.payload) {
state.isReadOnly = true;
}
},
setDarkMode: (state, action: PayloadAction<boolean>) => {
state.isDarkMode = action.payload;
},
Expand Down Expand Up @@ -194,6 +202,7 @@ export const workflowLoadingSlice = createSlice({
state.isDarkMode = lastWorkflow.isDarkMode;
state.isReadOnly = lastWorkflow.isReadOnly;
state.isMonitoringView = lastWorkflow.isMonitoringView;
state.isUnitTest = lastWorkflow.isUnitTest;
// Clear these state values, they get built with the other values
state.workflowDefinition = null;
state.runInstance = null;
Expand Down Expand Up @@ -255,6 +264,7 @@ export const {
clearWorkflowDetails,
setReadOnly,
setMonitoringView,
setUnitTest,
setDarkMode,
setHostingPlan,
setIsLocalSelected,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as vscode from 'vscode';
import { FileManagement } from '../iacGestureHelperFunctions';
import { localize } from '../../../../localize';
import { ext } from '../../../../extensionVariables';

/**
* Helper to create a mock workspace folder.
* @param fsPath The folder path.
* @returns A mock WorkspaceFolder.
*/
function mockWorkspaceFolder(fsPath: string): vscode.WorkspaceFolder {
return {
uri: vscode.Uri.file(fsPath),
name: 'Test Folder',
index: 0,
} as vscode.WorkspaceFolder;
}

describe('FileManagement.addFolderToWorkspace', () => {
let updateWorkspaceFoldersSpy: ReturnType<typeof vi.spyOn>;
let appendLogSpy: ReturnType<typeof vi.spyOn>;
let showErrorMessageSpy: ReturnType<typeof vi.spyOn>;

const folderPathExisting = '/existing/folder';
const folderPathNew = '/new/folder';
const folderPathError = '/error/folder';

beforeEach(() => {
// Ensure that ext.outputChannel is defined before spying on it.
ext.outputChannel = {
name: 'OutputChannel',
appendLog: vi.fn(),
append: vi.fn(),
appendLine: vi.fn(),
replace: vi.fn(),
clear: vi.fn(),
show: vi.fn(),
hide: vi.fn(),
dispose: vi.fn(),
};

// Spy on the VS Code and extension logging methods.
updateWorkspaceFoldersSpy = vi.spyOn(vscode.workspace, 'updateWorkspaceFolders') as any;
appendLogSpy = vi.spyOn(ext.outputChannel, 'appendLog');
showErrorMessageSpy = vi.spyOn(vscode.window, 'showErrorMessage') as any;

// Ensure a clean workspaceFolders state.
(vscode.workspace as any).workspaceFolders = [];
});

afterEach(() => {
// Reset any modifications to the workspace folders.
(vscode.workspace as any).workspaceFolders = undefined;
vi.restoreAllMocks();
});

it('should log and not add the folder if it is already in the workspace', () => {
// Arrange: Create a workspace that already contains the folder.
const existingFolder = mockWorkspaceFolder(folderPathExisting);
(vscode.workspace as any).workspaceFolders = [existingFolder];

// Act
FileManagement.addFolderToWorkspace(folderPathExisting);

// Assert
// First, we log that we are attempting to add the folder.
expect(appendLogSpy).toHaveBeenCalledWith(localize('addingFolderToWorkspace', `Adding folder to workspace: ${folderPathExisting}`));
// Then, we log that the folder already exists.
expect(appendLogSpy).toHaveBeenCalledWith(
localize('folderAlreadyInWorkspace', `Folder is already in the workspace: ${folderPathExisting}`)
);
// And updateWorkspaceFolders should not have been called.
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
});

it('should catch and handle errors thrown during folder addition', () => {
// Arrange: Set up an empty workspace.
(vscode.workspace as any).workspaceFolders = [];
const testError = new Error('Test error');
// Stub updateWorkspaceFolders to throw an error.
updateWorkspaceFoldersSpy.mockImplementation(() => {
throw testError;
});
const showErrorSpy = vi.spyOn(vscode.window, 'showErrorMessage').mockImplementation(async () => undefined);

// Act
FileManagement.addFolderToWorkspace(folderPathError);

// Assert
// Verify that the error is logged.
expect(appendLogSpy).toHaveBeenCalledWith(localize('errorAddingFolder', `Error in addFolderToWorkspace: ${testError}`));
// And an error message is shown to the user.
expect(showErrorSpy).toHaveBeenCalledWith(
localize('errorMessageAddingFolder', 'Failed to add folder to workspace: ') + testError.message
);
});
});
Loading
Loading