Skip to content

Commit 51a9a5a

Browse files
authored
fix(vscode): Handle extension initialization when there aren't logic apps projects (#6536)
* Add scenario for non logic apps workspaces * Add unit tests
1 parent d7e9bb4 commit 51a9a5a

File tree

8 files changed

+202
-10
lines changed

8 files changed

+202
-10
lines changed

apps/vs-code-designer/src/app/commands/createCodeless/createCodeless.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getWorkspaceSetting } from '../../utils/vsCodeConfig/settings';
1010
import { verifyInitForVSCode } from '../../utils/vsCodeConfig/verifyInitForVSCode';
1111
import { getContainingWorkspace, getWorkspaceFolder } from '../../utils/workspace';
1212
import { WorkflowStateTypeStep } from './createCodelessSteps/WorkflowStateTypeStep';
13-
import { isString } from '@microsoft/logic-apps-shared';
13+
import { isNullOrUndefined, isString } from '@microsoft/logic-apps-shared';
1414
import type { IActionContext } from '@microsoft/vscode-azext-utils';
1515
import { AzureWizard } from '@microsoft/vscode-azext-utils';
1616
import type { IFunctionWizardContext, FuncVersion } from '@microsoft/vscode-extension-logic-apps';
@@ -32,7 +32,11 @@ export async function createCodeless(
3232
workspacePath = isString(workspacePath) ? workspacePath : undefined;
3333
if (workspacePath === undefined) {
3434
workspaceFolder = await getWorkspaceFolder(context);
35-
workspacePath = isString(workspaceFolder) ? workspaceFolder : workspaceFolder.uri.fsPath;
35+
workspacePath = isNullOrUndefined(workspaceFolder)
36+
? undefined
37+
: isString(workspaceFolder)
38+
? workspaceFolder
39+
: workspaceFolder.uri.fsPath;
3640
} else {
3741
workspaceFolder = getContainingWorkspace(workspacePath);
3842
}

apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export const createNewDataMapCmd = async (context: IActionContext) => {
1818
context,
1919
localize('openLogicAppsProject', 'You must have a logic apps project open to use the Data Mapper.')
2020
);
21-
const projectPath: string | undefined = await verifyAndPromptToCreateProject(context, workspaceFolder.uri.fsPath);
21+
const projectPath: string | undefined =
22+
!isNullOrUndefined(workspaceFolder) && (await verifyAndPromptToCreateProject(context, workspaceFolder?.uri?.fsPath));
2223
if (!projectPath) {
2324
return;
2425
}
@@ -35,7 +36,8 @@ export const loadDataMapFileCmd = async (context: IActionContext, uri: Uri) => {
3536
context,
3637
localize('openLogicAppsProject', 'You must have a logic apps project open to use the Data Mapper.')
3738
);
38-
const projectPath: string | undefined = await verifyAndPromptToCreateProject(context, workspaceFolder.uri.fsPath);
39+
const projectPath: string | undefined =
40+
!isNullOrUndefined(workspaceFolder) && (await verifyAndPromptToCreateProject(context, workspaceFolder?.uri?.fsPath));
3941
if (!projectPath) {
4042
return;
4143
}

apps/vs-code-designer/src/app/commands/workflows/configureWebhookRedirectEndpoint/configureWebhookRedirectEndpoint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function configureWebhookRedirectEndpoint(context: IActionContext,
2222
workspaceFolder = await getWorkspaceFolder(context);
2323
}
2424

25-
const workspacePath = workspaceFolder.uri.fsPath;
25+
const workspacePath = workspaceFolder?.uri?.fsPath;
2626
const projectPath = (await tryGetLogicAppProjectRoot(context, workspacePath)) || workspacePath;
2727
const localSettings: ILocalSettingsJson = await getLocalSettingsJson(context, path.join(projectPath, localSettingsFileName));
2828
const redirectEndpoint: string = localSettings.Values[webhookRedirectHostUri] || '';

apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { ext } from '../../../extensionVariables';
2020
import { DependencyVersion, Platform } from '../../../constants';
2121
import { executeCommand } from '../funcCoreTools/cpUtils';
2222
import { getNpmCommand } from '../nodeJs/nodeJsVersion';
23-
import { getGlobalSetting, getWorkspaceSetting, updateGlobalSetting } from '../vsCodeConfig/settings';
23+
import { getGlobalSetting, getWorkspaceSetting } from '../vsCodeConfig/settings';
2424
import type { IActionContext } from '@microsoft/vscode-azext-utils';
2525
import { isNodeJsInstalled } from '../../commands/nodeJs/validateNodeJsInstalled';
2626

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
2+
import * as vscode from 'vscode';
3+
import * as workspaceUtils from '../workspace';
4+
import { promptOpenProject, tryGetLogicAppProjectRoot } from '../verifyIsProject';
5+
6+
vi.mock('../verifyIsProject');
7+
8+
describe('workspaceUtils.getWorkspaceFolder', () => {
9+
const originalWorkspace = { ...vscode.workspace };
10+
11+
const mockContext: any = {
12+
telemetry: { properties: {}, measurements: {} },
13+
errorHandling: { issueProperties: {} },
14+
ui: {
15+
showQuickPick: vi.fn(),
16+
showOpenDialog: vi.fn(),
17+
onDidFinishPrompt: vi.fn(),
18+
showInputBox: vi.fn(),
19+
showWarningMessage: vi.fn(),
20+
},
21+
};
22+
23+
const mockFolder = (fsPath: string): vscode.WorkspaceFolder => ({ uri: { fsPath } }) as vscode.WorkspaceFolder;
24+
25+
beforeEach(() => {
26+
// Reset workspace state and mocks before each test
27+
vi.restoreAllMocks();
28+
(vscode.workspace as any).workspaceFolders = undefined;
29+
(vscode.workspace as any).workspaceFile = undefined;
30+
});
31+
32+
afterEach(() => {
33+
// Restore original workspace
34+
Object.assign(vscode, { workspace: originalWorkspace });
35+
});
36+
37+
it('should prompt to open project if no workspace folders are open', async () => {
38+
const folder1 = mockFolder('/path/one');
39+
(vscode.workspace as any).workspaceFolders = [];
40+
41+
const promptOpenProjectSpy = vi.fn(() => {
42+
(vscode.workspace as any).workspaceFolders = [folder1];
43+
});
44+
45+
(promptOpenProject as Mock).mockImplementation(promptOpenProjectSpy);
46+
47+
await workspaceUtils.getWorkspaceFolder(mockContext);
48+
expect(promptOpenProjectSpy).toHaveBeenCalled();
49+
});
50+
51+
it('should prompt to open project if workspace folders are undefined', async () => {
52+
const folder1 = mockFolder('/path/one');
53+
(vscode.workspace as any).workspaceFolders = undefined;
54+
55+
const promptOpenProjectSpy = vi.fn(() => {
56+
(vscode.workspace as any).workspaceFolders = [folder1];
57+
});
58+
59+
(promptOpenProject as Mock).mockImplementation(promptOpenProjectSpy);
60+
61+
await workspaceUtils.getWorkspaceFolder(mockContext);
62+
expect(promptOpenProjectSpy).toHaveBeenCalled();
63+
});
64+
65+
it('should return the only workspace folder if there is only one', async () => {
66+
const folder1 = mockFolder('/path/one');
67+
(vscode.workspace as any).workspaceFolders = [folder1];
68+
69+
const promptOpenProjectSpy = vi.fn(() => {
70+
(vscode.workspace as any).workspaceFolders = [folder1];
71+
});
72+
73+
(promptOpenProject as Mock).mockImplementation(promptOpenProjectSpy);
74+
75+
const result = await workspaceUtils.getWorkspaceFolder(mockContext);
76+
expect(result).toEqual(folder1);
77+
expect(promptOpenProjectSpy).not.toHaveBeenCalled();
78+
});
79+
80+
it('should return the only workspace folder if there is only one after prompting', async () => {
81+
const folder1 = mockFolder('/path/one');
82+
(vscode.workspace as any).workspaceFolders = undefined;
83+
84+
const promptOpenProjectSpy = vi.fn(() => {
85+
(vscode.workspace as any).workspaceFolders = [folder1];
86+
});
87+
88+
(promptOpenProject as Mock).mockImplementation(promptOpenProjectSpy);
89+
90+
const result = await workspaceUtils.getWorkspaceFolder(mockContext);
91+
expect(result).toEqual(folder1);
92+
});
93+
94+
it('should return undefined if no logic app project is found among multiple folders', async () => {
95+
const folder1 = mockFolder('/path/one');
96+
const folder2 = mockFolder('/path/two');
97+
(vscode.workspace as any).workspaceFolders = [folder1, folder2];
98+
99+
const tryGetLogicAppProjectRootSpy = vi.fn(() => {
100+
return undefined;
101+
});
102+
(tryGetLogicAppProjectRoot as Mock).mockImplementation(tryGetLogicAppProjectRootSpy);
103+
104+
const result = await workspaceUtils.getWorkspaceFolder(mockContext);
105+
expect(tryGetLogicAppProjectRootSpy).toHaveBeenCalledTimes(2);
106+
expect(result).toBeUndefined();
107+
});
108+
109+
it('should return the only logic app project if there is only one', async () => {
110+
const folder1 = mockFolder('/logic/path');
111+
const folder2 = mockFolder('/nonlogic/path');
112+
(vscode.workspace as any).workspaceFolders = [folder1, folder2];
113+
114+
// For folder1 return a valid project root, for folder2 return undefined.
115+
const tryGetLogicAppProjectRootSpy = vi.fn(async (_context, folder) => {
116+
return folder.uri.fsPath === '/logic/path' ? folder.uri.fsPath : undefined;
117+
});
118+
(tryGetLogicAppProjectRoot as Mock).mockImplementation(tryGetLogicAppProjectRootSpy);
119+
120+
const result = await workspaceUtils.getWorkspaceFolder(mockContext);
121+
122+
expect(result).toBe('/logic/path');
123+
expect(tryGetLogicAppProjectRootSpy).toHaveBeenCalledTimes(2);
124+
});
125+
126+
it('should return the first logic app project if skipPromptOnMultipleFolders is true', async () => {
127+
const folder1 = mockFolder('/logic/path1');
128+
const folder2 = mockFolder('/logic/path2');
129+
(vscode.workspace as any).workspaceFolders = [folder1, folder2];
130+
131+
// Both folders return a valid project root.
132+
const tryGetLogicAppProjectRootSpy = vi.fn(async (_context, folder) => {
133+
return folder.uri.fsPath;
134+
});
135+
(tryGetLogicAppProjectRoot as Mock).mockImplementation(tryGetLogicAppProjectRootSpy);
136+
137+
const result = await workspaceUtils.getWorkspaceFolder(mockContext, undefined, true);
138+
// Expect the first folder (or its project root) to be returned.
139+
expect(result).toBe('/logic/path1');
140+
expect(tryGetLogicAppProjectRootSpy).toHaveBeenCalledTimes(2);
141+
});
142+
143+
it('should prompt the user to select a logic app project if there are multiple', async () => {
144+
const folder1 = mockFolder('/logic/path1');
145+
const folder2 = mockFolder('/logic/path2');
146+
(vscode.workspace as any).workspaceFolders = [folder1, folder2];
147+
// Both folders are logic app projects.
148+
const tryGetLogicAppProjectRootSpy = vi.fn(async (_context, folder) => {
149+
return folder.uri.fsPath;
150+
});
151+
(tryGetLogicAppProjectRoot as Mock).mockImplementation(tryGetLogicAppProjectRootSpy);
152+
153+
const quickPickSpy = vi.spyOn(mockContext.ui, 'showQuickPick').mockResolvedValue({ data: folder2 });
154+
155+
const result = await workspaceUtils.getWorkspaceFolder(mockContext);
156+
expect(quickPickSpy).toHaveBeenCalled();
157+
expect(result).toBe(folder2);
158+
});
159+
160+
it('should throw UserCancelledError if user cancels the prompt', async () => {
161+
const folder1 = mockFolder('/logic/path1');
162+
const folder2 = mockFolder('/logic/path2');
163+
(vscode.workspace as any).workspaceFolders = [folder1, folder2];
164+
165+
const tryGetLogicAppProjectRootSpy = vi.fn(async (_context, folder) => {
166+
return folder.uri.fsPath;
167+
});
168+
(tryGetLogicAppProjectRoot as Mock).mockImplementation(tryGetLogicAppProjectRootSpy);
169+
vi.spyOn(mockContext.ui, 'showQuickPick').mockResolvedValue(undefined);
170+
171+
await expect(workspaceUtils.getWorkspaceFolder(mockContext)).rejects.toThrowError();
172+
});
173+
});

apps/vs-code-designer/src/app/utils/verifyIsProject.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { extensionBundleId, hostFileName, extensionCommand } from '../../constants';
66
import { localize } from '../../localize';
77
import { getWorkspaceSetting, updateWorkspaceSetting } from './vsCodeConfig/settings';
8-
import { isString } from '@microsoft/logic-apps-shared';
8+
import { isNullOrUndefined, isString } from '@microsoft/logic-apps-shared';
99
import type { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
1010
import * as fse from 'fs-extra';
1111
import * as path from 'path';
@@ -57,7 +57,10 @@ export async function isLogicAppProject(folderPath: string): Promise<boolean> {
5757
* Checks root folder and subFolders one level down
5858
* If any logic app projects are found return true.
5959
*/
60-
export async function isLogicAppProjectInRoot(workspaceFolder: WorkspaceFolder | string): Promise<boolean | undefined> {
60+
export async function isLogicAppProjectInRoot(workspaceFolder: WorkspaceFolder | string | undefined): Promise<boolean | undefined> {
61+
if (isNullOrUndefined(workspaceFolder)) {
62+
return false;
63+
}
6164
const subpath: string | undefined = getWorkspaceSetting(projectSubpathKey, workspaceFolder);
6265
const folderPath = isString(workspaceFolder) ? workspaceFolder : workspaceFolder.uri.fsPath;
6366
if (!subpath) {
@@ -93,9 +96,12 @@ export async function isLogicAppProjectInRoot(workspaceFolder: WorkspaceFolder |
9396
*/
9497
export async function tryGetLogicAppProjectRoot(
9598
context: IActionContext,
96-
workspaceFolder: WorkspaceFolder | string,
99+
workspaceFolder: WorkspaceFolder | string | undefined,
97100
suppressPrompt = false
98101
): Promise<string | undefined> {
102+
if (isNullOrUndefined(workspaceFolder)) {
103+
return undefined;
104+
}
99105
let subpath: string | undefined = getWorkspaceSetting(projectSubpathKey, workspaceFolder);
100106
const folderPath = isString(workspaceFolder) ? workspaceFolder : workspaceFolder.uri.fsPath;
101107
if (!subpath) {

apps/vs-code-designer/src/app/utils/workspace.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export async function getWorkspaceFolder(
106106
context: IActionContext,
107107
message?: string,
108108
skipPromptOnMultipleFolders?: boolean
109-
): Promise<vscode.WorkspaceFolder> {
109+
): Promise<vscode.WorkspaceFolder | string | undefined> {
110110
const promptMessage: string = message ?? localize('noWorkspaceWarning', 'You must have a project open to create a workflow.');
111111
let folder: vscode.WorkspaceFolder | undefined;
112112
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
@@ -124,6 +124,10 @@ export async function getWorkspaceFolder(
124124
}
125125
}
126126

127+
if (logicAppsWorkspaces.length === 0) {
128+
return undefined;
129+
}
130+
127131
if (logicAppsWorkspaces.length === 1) {
128132
folder = logicAppsWorkspaces[0];
129133
} else if (skipPromptOnMultipleFolders) {

apps/vs-code-designer/test-setup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,7 @@ vi.mock('axios');
3737

3838
vi.mock('vscode', () => ({
3939
window: {},
40+
workspace: {
41+
workspaceFolders: [],
42+
},
4043
}));

0 commit comments

Comments
 (0)