Skip to content

Commit 3ddb96f

Browse files
authored
feat(vscode): Container support for extension dependencies (#8833)
* Added toggle for dev container creation in the create workspace webview and removed dead code * Added unit tests to cover the workspace/project creation and to test the new devcontainer experience * Adding Workspace Creation Integration Tests * Added the enableDevContainer command and additional integration tests to test adding a project to a devcontainer workspace and to verify the structure of the workspace * Corrected test * Block onboarding from executing when devcontainer project is recognized. Added additional tests to verify this behavior * Update to public image * Reverting unnecessary modifications from merge * Remove unneeded files * fixed failing tests * Correct mocks * test-setup updates * Added additional unit tests * Adding additional unit tests * Added unit tests for messages * exclude intl messages from code coverage * Exclude from coverage
1 parent 3d17190 commit 3ddb96f

File tree

49 files changed

+9680
-101
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+9680
-101
lines changed

.github/workflows/pr-coverage.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ jobs:
133133
**/providers/**
134134
# VS Code webview communication (uses acquireVsCodeApi runtime global, cannot be unit tested)
135135
**/webviewCommunication.tsx
136+
# VS Code React intl files (localization catalog, not unit-test target)
137+
apps/vs-code-react/src/intl/**
136138
137139
- name: Check coverage on changed files
138140
id: coverage-check

apps/vs-code-designer/TESTING.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Testing Guide for VS Code Designer Extension
2+
3+
## Running Tests
4+
5+
### VS Code Testing Tab (Recommended)
6+
Use the VS Code Testing tab in the sidebar for an interactive testing experience with debugging support.
7+
8+
### Command Line
9+
10+
#### Run All Tests
11+
```bash
12+
cd apps/vs-code-designer
13+
pnpm vitest run
14+
```
15+
16+
#### Run Specific Test File
17+
```bash
18+
cd apps/vs-code-designer
19+
pnpm vitest run <test-file-name>
20+
```
21+
22+
Examples:
23+
```bash
24+
pnpm vitest run binaries.test.ts
25+
pnpm vitest run enableDevContainerIntegration.test.ts
26+
```
27+
28+
#### Run Tests in Watch Mode
29+
```bash
30+
cd apps/vs-code-designer
31+
pnpm vitest
32+
```
33+
34+
#### Run Tests with Coverage
35+
```bash
36+
cd apps/vs-code-designer
37+
pnpm vitest run --coverage
38+
```
39+
40+
## Test Types
41+
42+
### Unit Tests
43+
- Location: `src/**/__test__/*.test.ts`
44+
- Framework: Vitest
45+
- Run with: `pnpm vitest run`
46+
47+
### Integration Tests
48+
- Location: `src/**/__test__/*Integration.test.ts`
49+
- Framework: Vitest with real file system operations
50+
- These tests may take longer due to actual file I/O
51+
52+
### E2E Tests (Extension UI)
53+
- Location: `out/test/**/*.js`
54+
- Framework: VS Code Extension Test Runner
55+
- Run with: `pnpm run vscode:designer:e2e:ui` (UI mode)
56+
- Run with: `pnpm run vscode:designer:e2e:headless` (Headless mode)
57+
58+
## Common Test Commands
59+
60+
| Command | Description |
61+
|---------|-------------|
62+
| `pnpm vitest run` | Run all unit tests once |
63+
| `pnpm vitest` | Run tests in watch mode |
64+
| `pnpm vitest run --ui` | Run tests with Vitest UI |
65+
| `pnpm vitest run <file>` | Run specific test file |
66+
| `pnpm run test:extension-unit` | Run unit tests with retries |
67+
68+
## Test Configuration
69+
70+
- **Vitest Config**: `vitest.workspace.ts` (workspace root)
71+
- **Test Timeout**: Default 5000ms (can be overridden per test)
72+
- **Coverage**: Enabled by default with Istanbul
73+
74+
## Tips
75+
76+
1. **Long-running tests**: Some integration tests may need extended timeouts. Add `{ timeout: 30000 }` to slow tests.
77+
78+
2. **Debugging tests**: Use VS Code's Testing tab and click the debug icon next to any test.
79+
80+
3. **Mocking**: Tests use Vitest's `vi.mock()` for mocking modules. See existing tests for patterns.
81+
82+
4. **File system tests**: Tests that interact with the file system use `fs-extra` and create temp directories that are cleaned up in `afterEach`.
83+
84+
## Troubleshooting
85+
86+
### Tests timing out
87+
- Increase timeout for specific test: `it('test name', { timeout: 30000 }, async () => { ... })`
88+
- Check for missing `await` keywords in async operations
89+
90+
### Module not found errors
91+
- Ensure all mocked modules are declared with `vi.mock('module-path')` at the top level
92+
- Check import paths are correct
93+
94+
### Mock issues
95+
- Use `vi.mocked()` to properly type mocked functions
96+
- Import actual implementations with `vi.importActual()` when needed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import type { IActionContext } from '@microsoft/vscode-azext-utils';
3+
4+
// Use factory mocks to prevent transitive module loading issues (e.g., AzureWizardPromptStep).
5+
// devContainerUtils and settings are leaf dependencies called by the real useBinariesDependencies/binariesExist.
6+
vi.mock('../app/utils/devContainerUtils', () => ({
7+
isDevContainerWorkspace: vi.fn(),
8+
}));
9+
vi.mock('../app/utils/vsCodeConfig/settings', () => ({
10+
getGlobalSetting: vi.fn(),
11+
getWorkspaceSetting: vi.fn(),
12+
updateGlobalSetting: vi.fn(),
13+
}));
14+
15+
// Fully mock binaries (synchronous factory, no importActual) to avoid module resolution deadlock.
16+
// Tests that need the REAL useBinariesDependencies/binariesExist use vi.importActual at test level.
17+
vi.mock('../app/utils/binaries', () => ({
18+
useBinariesDependencies: vi.fn(),
19+
binariesExist: vi.fn(),
20+
installBinaries: vi.fn(),
21+
downloadAndExtractDependency: vi.fn(),
22+
getLatestFunctionCoreToolsVersion: vi.fn(),
23+
getLatestDotNetVersion: vi.fn(),
24+
getLatestNodeJsVersion: vi.fn(),
25+
getNodeJsBinariesReleaseUrl: vi.fn(),
26+
getFunctionCoreToolsBinariesReleaseUrl: vi.fn(),
27+
getDotNetBinariesReleaseUrl: vi.fn(),
28+
getCpuArchitecture: vi.fn(),
29+
getDependencyTimeout: vi.fn(),
30+
}));
31+
32+
// Mock transitive dependencies of binaries.ts to prevent real module loading
33+
// when vi.importActual('../app/utils/binaries') is used at test level.
34+
vi.mock('../app/utils/codeless/startDesignTimeApi', () => ({
35+
promptStartDesignTimeOption: vi.fn(),
36+
startAllDesignTimeApis: vi.fn(),
37+
stopAllDesignTimeApis: vi.fn(),
38+
}));
39+
vi.mock('../app/utils/funcCoreTools/cpUtils', () => ({
40+
executeCommand: vi.fn(),
41+
}));
42+
vi.mock('../app/utils/funcCoreTools/funcVersion', () => ({
43+
setFunctionsCommand: vi.fn(),
44+
getFunctionsCommand: vi.fn(),
45+
}));
46+
vi.mock('../app/commands/nodeJs/validateNodeJsInstalled', () => ({
47+
isNodeJsInstalled: vi.fn(),
48+
}));
49+
vi.mock('../app/utils/nodeJs/nodeJsVersion', () => ({
50+
getNpmCommand: vi.fn(),
51+
}));
52+
// Fully mock onboarding to break circular dep (onboarding ↔ binaries).
53+
vi.mock('../onboarding', () => ({
54+
onboardBinaries: vi.fn(),
55+
startOnboarding: vi.fn(),
56+
}));
57+
vi.mock('../app/utils/vsCodeConfig/tasks', () => ({
58+
validateTasksJson: vi.fn(),
59+
}));
60+
vi.mock('../app/commands/binaries/validateAndInstallBinaries', () => ({
61+
validateAndInstallBinaries: vi.fn(),
62+
}));
63+
vi.mock('../app/utils/telemetry', () => ({
64+
runWithDurationTelemetry: vi.fn(async (_ctx: any, _cmd: any, callback: () => Promise<void>) => await callback()),
65+
}));
66+
vi.mock('@microsoft/vscode-azext-utils', () => ({
67+
callWithTelemetryAndErrorHandling: vi.fn(async (_cmd: string, callback: (ctx: IActionContext) => Promise<void>) => {
68+
return await callback({
69+
telemetry: { properties: {}, measurements: {} },
70+
errorHandling: {},
71+
ui: {},
72+
valuesToMask: [],
73+
} as any);
74+
}),
75+
}));
76+
77+
/**
78+
* Re-implements the core startOnboarding flow inline to test devContainer detection
79+
* without hitting the circular dependency between onboarding.ts ↔ binaries.ts.
80+
* This mirrors the real logic in onboarding.ts: check isDevContainerWorkspace,
81+
* skip if true, otherwise call installBinaries + promptStartDesignTimeOption.
82+
*/
83+
async function startOnboardingForTest(activateContext: IActionContext) {
84+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
85+
const { installBinaries } = await import('../app/utils/binaries');
86+
const { promptStartDesignTimeOption } = await import('../app/utils/codeless/startDesignTimeApi');
87+
88+
const isDevContainer = await isDevContainerWorkspace();
89+
if (isDevContainer) {
90+
activateContext.telemetry.properties.skippedOnboarding = 'true';
91+
activateContext.telemetry.properties.skippedReason = 'devContainer';
92+
return;
93+
}
94+
95+
const binariesInstallStartTime = Date.now();
96+
await installBinaries(activateContext as any);
97+
activateContext.telemetry.measurements.binariesInstallDuration = Date.now() - binariesInstallStartTime;
98+
99+
await promptStartDesignTimeOption(activateContext);
100+
}
101+
102+
describe('devContainer Integration Tests', () => {
103+
let mockContext: IActionContext;
104+
105+
beforeEach(() => {
106+
vi.clearAllMocks();
107+
mockContext = {
108+
telemetry: {
109+
properties: {},
110+
measurements: {},
111+
},
112+
errorHandling: {},
113+
ui: {},
114+
valuesToMask: [],
115+
} as any;
116+
});
117+
118+
describe('Complete onboarding flow - devContainer', () => {
119+
it('should skip entire onboarding flow in devContainer workspace', async () => {
120+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
121+
const { installBinaries } = await import('../app/utils/binaries');
122+
const { promptStartDesignTimeOption } = await import('../app/utils/codeless/startDesignTimeApi');
123+
124+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(true);
125+
126+
await startOnboardingForTest(mockContext);
127+
128+
expect(mockContext.telemetry.properties.skippedOnboarding).toBe('true');
129+
expect(mockContext.telemetry.properties.skippedReason).toBe('devContainer');
130+
expect(installBinaries).not.toHaveBeenCalled();
131+
expect(promptStartDesignTimeOption).not.toHaveBeenCalled();
132+
});
133+
134+
it('should complete onboarding flow in non-devContainer workspace', async () => {
135+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
136+
const { installBinaries } = await import('../app/utils/binaries');
137+
const { promptStartDesignTimeOption } = await import('../app/utils/codeless/startDesignTimeApi');
138+
139+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(false);
140+
vi.mocked(installBinaries).mockResolvedValue(undefined);
141+
vi.mocked(promptStartDesignTimeOption).mockResolvedValue(undefined);
142+
143+
await startOnboardingForTest(mockContext);
144+
145+
expect(mockContext.telemetry.properties.skippedOnboarding).toBeUndefined();
146+
expect(installBinaries).toHaveBeenCalled();
147+
expect(promptStartDesignTimeOption).toHaveBeenCalled();
148+
expect(mockContext.telemetry.measurements.binariesInstallDuration).toBeDefined();
149+
});
150+
});
151+
152+
describe('useBinariesDependencies - devContainer override', () => {
153+
it('should return false in devContainer regardless of global setting', async () => {
154+
// Use importActual to run the REAL useBinariesDependencies logic.
155+
// Its dependencies (isDevContainerWorkspace, getGlobalSetting) are still mocked.
156+
const { useBinariesDependencies: realUseBinariesDependencies } =
157+
await vi.importActual<typeof import('../app/utils/binaries')>('../app/utils/binaries');
158+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
159+
const settingsModule = await import('../app/utils/vsCodeConfig/settings');
160+
161+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(true);
162+
vi.mocked(settingsModule.getGlobalSetting).mockReturnValue(true);
163+
164+
const result = await realUseBinariesDependencies();
165+
166+
expect(result).toBe(false);
167+
});
168+
169+
it('should respect global setting in non-devContainer workspace', async () => {
170+
const { useBinariesDependencies: realUseBinariesDependencies } =
171+
await vi.importActual<typeof import('../app/utils/binaries')>('../app/utils/binaries');
172+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
173+
const settingsModule = await import('../app/utils/vsCodeConfig/settings');
174+
175+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(false);
176+
vi.mocked(settingsModule.getGlobalSetting).mockReturnValue(true);
177+
178+
const result = await realUseBinariesDependencies();
179+
180+
expect(result).toBe(true);
181+
});
182+
});
183+
184+
describe('binariesExist - devContainer early exit', () => {
185+
it('should return false immediately in devContainer without checking filesystem', async () => {
186+
const { binariesExist: realBinariesExist } = await vi.importActual<typeof import('../app/utils/binaries')>('../app/utils/binaries');
187+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
188+
const fs = await import('fs');
189+
190+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(true);
191+
vi.mocked(fs.existsSync).mockReturnValue(true);
192+
193+
const result = await realBinariesExist('dotnet');
194+
195+
expect(result).toBe(false);
196+
expect(fs.existsSync).not.toHaveBeenCalled();
197+
});
198+
199+
it('should check filesystem in non-devContainer workspace', async () => {
200+
const { binariesExist: realBinariesExist } = await vi.importActual<typeof import('../app/utils/binaries')>('../app/utils/binaries');
201+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
202+
const fs = await import('fs');
203+
const settingsModule = await import('../app/utils/vsCodeConfig/settings');
204+
205+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(false);
206+
vi.mocked(settingsModule.getGlobalSetting).mockReturnValue('test/path');
207+
vi.mocked(fs.existsSync).mockReturnValue(true);
208+
209+
const result = await realBinariesExist('dotnet');
210+
211+
expect(result).toBe(true);
212+
expect(fs.existsSync).toHaveBeenCalled();
213+
});
214+
});
215+
216+
describe('Telemetry tracking across devContainer detection', () => {
217+
it('should track skipped onboarding in telemetry for devContainer', async () => {
218+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
219+
220+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(true);
221+
222+
await startOnboardingForTest(mockContext);
223+
224+
expect(mockContext.telemetry.properties.skippedOnboarding).toBe('true');
225+
expect(mockContext.telemetry.properties.skippedReason).toBe('devContainer');
226+
});
227+
228+
it('should track binaries install duration for non-devContainer', async () => {
229+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
230+
const { installBinaries } = await import('../app/utils/binaries');
231+
232+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(false);
233+
vi.mocked(installBinaries).mockResolvedValue(undefined);
234+
235+
await startOnboardingForTest(mockContext);
236+
237+
expect(mockContext.telemetry.measurements.binariesInstallDuration).toBeDefined();
238+
expect(typeof mockContext.telemetry.measurements.binariesInstallDuration).toBe('number');
239+
expect(mockContext.telemetry.measurements.binariesInstallDuration).toBeGreaterThanOrEqual(0);
240+
});
241+
242+
it('should not track binaries install duration for devContainer', async () => {
243+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
244+
245+
vi.mocked(isDevContainerWorkspace).mockResolvedValue(true);
246+
247+
await startOnboardingForTest(mockContext);
248+
249+
expect(mockContext.telemetry.measurements.binariesInstallDuration).toBeUndefined();
250+
});
251+
});
252+
253+
describe('Error handling in devContainer detection', () => {
254+
it('should gracefully handle errors in devContainer detection', async () => {
255+
const { useBinariesDependencies: realUseBinariesDependencies } =
256+
await vi.importActual<typeof import('../app/utils/binaries')>('../app/utils/binaries');
257+
const { isDevContainerWorkspace } = await import('../app/utils/devContainerUtils');
258+
259+
vi.mocked(isDevContainerWorkspace).mockRejectedValue(new Error('File system error'));
260+
261+
// Should reject when the underlying detection fails
262+
await expect(realUseBinariesDependencies()).rejects.toThrow();
263+
});
264+
});
265+
});

0 commit comments

Comments
 (0)