diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index a3eb85e3ffd..65aa230c671 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -41,11 +41,27 @@ jobs: - name: Build run: pnpm turbo run build --cache-dir=.turbo + - name: Configure VS Code designer coverage scope + run: | + CHANGED_DESIGNER_FILES=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }} "$GITHUB_SHA" -- 'apps/vs-code-designer/src/**/*.ts' 'apps/vs-code-designer/src/**/*.tsx' \ + | grep -Ev 'apps/vs-code-designer/src/test/(e2e|ui)/|\.test\.tsx?$|\.spec\.tsx?$|test-setup\.ts|extensionVariables\.ts|main\.ts' \ + | sed 's#^apps/vs-code-designer/##' \ + | paste -sd, -) + if [ -n "$CHANGED_DESIGNER_FILES" ]; then + echo "VSCODE_DESIGNER_COVERAGE_INCLUDE=$CHANGED_DESIGNER_FILES" >> "$GITHUB_ENV" + echo "$CHANGED_DESIGNER_FILES" + else + echo "VSCODE_DESIGNER_COVERAGE_INCLUDE=__no_changed_source__/**" >> "$GITHUB_ENV" + echo "No changed VS Code designer source files require coverage." + fi + - name: Run tests with coverage run: | pnpm turbo run test:lib --cache-dir=.turbo pnpm turbo run test:iframe-app --cache-dir=.turbo pnpm turbo run test:extension-unit --cache-dir=.turbo + env: + NODE_OPTIONS: --max-old-space-size=8192 - name: Merge coverage reports run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba1b82cdd6f..03d12c2bc7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,4 +45,7 @@ jobs: - run: pnpm turbo run test:lib --cache-dir=.turbo - run: pnpm turbo run build:extension --cache-dir=.turbo - run: pnpm turbo run test:extension-unit --cache-dir=.turbo + env: + NODE_OPTIONS: --max-old-space-size=8192 + VITEST_COVERAGE: false - run: pnpm turbo run test:iframe-app --cache-dir=.turbo diff --git a/Localize/lang/strings.json b/Localize/lang/strings.json index 9d1a44a56bd..f19de25fe5d 100644 --- a/Localize/lang/strings.json +++ b/Localize/lang/strings.json @@ -1105,7 +1105,6 @@ "JqiwYx": "Review + create", "JrAqnE": "Run with payload", "JrDiMJ": "Package path cannot be empty", - "JsTRX9": ".NET 10", "JsUu6b": "Workflow", "JyYLq1": "Zoom out", "JzRzVp": "(UTC-09:00) Alaska", @@ -1410,7 +1409,6 @@ "Q0xpPQ": "Required. The object to check if it is less or equal to the comparing object.", "Q13J5V": "Create deployment model", "Q1LEiE": "Previous", - "Q1tyGI": "Use the latest .NET 10 for modern development and performance", "Q2X3qQ": "Actions need to be triggered by another node, e.g. at regular intervals with the Schedule node", "Q2p4Zh": "An error occurred while generating keys. Error details: {errorMessage}", "Q4TUFX": "Discard", @@ -3042,7 +3040,6 @@ "_JqiwYx.comment": "Review and create step title", "_JrAqnE.comment": "Tooltip for Run with payload button", "_JrDiMJ.comment": "Package path empty validation message", - "_JsTRX9.comment": ".NET 10 option", "_JsUu6b.comment": "Label for workflow template which contains single workflow", "_JyYLq1.comment": "Aria label for a button that zooms out on the workflow", "_JzRzVp.comment": "Time zone value ", @@ -3347,7 +3344,6 @@ "_Q0xpPQ.comment": "Required object parameter to check if less than or equal to using lessOrEqual function", "_Q13J5V.comment": "Create deployment model resource label", "_Q1LEiE.comment": "Button text for going back to the previous tab", - "_Q1tyGI.comment": ".NET 10 description", "_Q2X3qQ.comment": "Message explaining that actions need triggers", "_Q2p4Zh.comment": "General error message for key generation failure", "_Q4TUFX.comment": "Button text for discard the unsaved changes", diff --git a/apps/vs-code-designer/package.json b/apps/vs-code-designer/package.json index c292ac24355..dd7c2c123be 100644 --- a/apps/vs-code-designer/package.json +++ b/apps/vs-code-designer/package.json @@ -66,7 +66,7 @@ "vscode:designer:pack:step1": "cd ./dist && npm install", "vscode:designer:pack:step2": "cd ./dist && vsce package", "lint": "eslint . --report-unused-disable-directives --max-warnings 0", - "test:extension-unit": "vitest run --retry=3", + "test:extension-unit": "node ./scripts/run-vitest-isolated.mjs --retry=3", "test:e2e-cli": "vscode-test", "test:e2e-cli:compile": "tsc -p ./tsconfig.e2e.json", "pretest:e2e-cli": "pnpm run test:e2e-cli:compile", diff --git a/apps/vs-code-designer/scripts/run-vitest-isolated.mjs b/apps/vs-code-designer/scripts/run-vitest-isolated.mjs new file mode 100644 index 00000000000..5f3a1569fd9 --- /dev/null +++ b/apps/vs-code-designer/scripts/run-vitest-isolated.mjs @@ -0,0 +1,112 @@ +import { spawn } from 'node:child_process'; +import { existsSync, mkdirSync, readdirSync, rmSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const vitestPath = fileURLToPath(new URL('../../../node_modules/vitest/vitest.mjs', import.meta.url)); +const args = process.argv.slice(2); +const coverageEnabled = process.env.VITEST_COVERAGE !== 'false'; +const coverageDir = join(process.cwd(), 'coverage'); +const shardCoverageDir = join(process.cwd(), '.vitest-coverage-shards'); + +const isTestFile = (file) => /(^|[/\\])src[/\\].*\.test\.tsx?$/.test(file); +const isExcludedTestHarness = (file) => file.startsWith('src/test/'); +const fileArgs = args.filter(isTestFile).map((file) => file.replace(/\\/g, '/')); +const baseArgs = args.filter((arg) => !isTestFile(arg)); + +const findTestFiles = (directory) => { + const files = []; + + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const entryPath = join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...findTestFiles(entryPath)); + } else if (/\.test\.tsx?$/.test(entry.name)) { + const testFile = relative(process.cwd(), entryPath).replace(/\\/g, '/'); + if (!isExcludedTestHarness(testFile)) { + files.push(testFile); + } + } + } + + return files; +}; + +const runVitest = (testFile) => + new Promise((resolve) => { + const child = spawn(process.execPath, [vitestPath, 'run', ...baseArgs, testFile], { + env: process.env, + stdio: 'inherit', + }); + + child.on('exit', (code, signal) => { + resolve({ code: code ?? 1, signal }); + }); + }); + +const saveCoverage = (index) => { + const lcovPath = join(coverageDir, 'lcov.info'); + if (existsSync(lcovPath)) { + mkdirSync(shardCoverageDir, { recursive: true }); + copyFileSync(lcovPath, join(shardCoverageDir, `lcov-${String(index).padStart(4, '0')}.info`)); + } +}; + +const mergeCoverage = () => { + if (!existsSync(shardCoverageDir)) { + return; + } + + const lcovFiles = readdirSync(shardCoverageDir) + .filter((file) => file.endsWith('.info')) + .sort(); + + if (lcovFiles.length === 0) { + return; + } + + mkdirSync(coverageDir, { recursive: true }); + const merged = lcovFiles + .map((file) => readFileSync(join(shardCoverageDir, file), 'utf8').trim()) + .filter(Boolean) + .join('\n'); + writeFileSync(join(coverageDir, 'lcov.info'), `${merged}\n`); +}; + +const run = async () => { + const testFiles = (fileArgs.length ? fileArgs : findTestFiles(join(process.cwd(), 'src')).sort()).filter( + (file) => !isExcludedTestHarness(file) + ); + + if (coverageEnabled) { + rmSync(shardCoverageDir, { recursive: true, force: true }); + } + + for (const [index, testFile] of testFiles.entries()) { + console.log(`Running VS Code designer unit test ${index + 1}/${testFiles.length}: ${testFile}`); + if (coverageEnabled) { + rmSync(coverageDir, { recursive: true, force: true }); + } + + const result = await runVitest(testFile); + + if (result.signal) { + process.kill(process.pid, result.signal); + return; + } + + if (result.code !== 0) { + process.exit(result.code); + } + + if (coverageEnabled) { + saveCoverage(index); + } + } + + if (coverageEnabled) { + mergeCoverage(); + } +}; + +run(); diff --git a/apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/__test__/functionFileStep.test.ts b/apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/__test__/functionFileStep.test.ts index ba98966de7f..b9ece0b0df0 100644 --- a/apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/__test__/functionFileStep.test.ts +++ b/apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/__test__/functionFileStep.test.ts @@ -1,48 +1,110 @@ -import { describe, it, expect, vi } from 'vitest'; import { TargetFramework } from '@microsoft/vscode-extension-logic-apps'; +import * as fs from 'fs-extra'; +import { window } from 'vscode'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ext } from '../../../../../extensionVariables'; +import { FunctionFileStep } from '../functionFileStep'; + +vi.mock('fs-extra', () => ({ + pathExists: vi.fn(), + stat: vi.fn(), + readFile: vi.fn(), + writeFile: vi.fn(), +})); + +vi.mock('vscode', () => ({ + window: { + showErrorMessage: vi.fn(), + }, +})); -vi.mock('fs-extra'); -vi.mock('vscode'); vi.mock('../../../../../extensionVariables', () => ({ - ext: { outputChannel: { appendLog: vi.fn() } }, + ext: { + outputChannel: { + appendLog: vi.fn(), + }, + }, })); + vi.mock('../../../../../localize', () => ({ - localize: (_key: string, msg: string) => msg, + localize: (_key: string, message: string) => message, })); -import { FunctionFileStep } from '../functionFileStep'; - describe('FunctionFileStep', () => { - describe('csTemplateFileName mapping', () => { - it('should map Net10 to FunctionsFileNet10', () => { - const step = new FunctionFileStep(); - const mapping = (step as any).csTemplateFileName; - expect(mapping[TargetFramework.Net10]).toBe('FunctionsFileNet10'); - }); - - it('should preserve Net8 mapping', () => { - const step = new FunctionFileStep(); - const mapping = (step as any).csTemplateFileName; - expect(mapping[TargetFramework.Net8]).toBe('FunctionsFileNet8'); - }); - - it('should preserve NetFx mapping', () => { - const step = new FunctionFileStep(); - const mapping = (step as any).csTemplateFileName; - expect(mapping[TargetFramework.NetFx]).toBe('FunctionsFileNetFx'); - }); - - it('should contain exactly three framework entries', () => { - const step = new FunctionFileStep(); - const mapping = (step as any).csTemplateFileName; - expect(Object.keys(mapping)).toHaveLength(3); - }); + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(fs.pathExists).mockResolvedValue(true); + vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as any); + vi.mocked(fs.readFile).mockResolvedValue('namespace <%= namespace %> { public class <%= methodName %> {} }'); + vi.mocked(fs.writeFile).mockResolvedValue(undefined); }); - describe('shouldPrompt', () => { - it('should always return true', () => { - const step = new FunctionFileStep(); - expect(step.shouldPrompt()).toBe(true); - }); + it('should always prompt', () => { + expect(new FunctionFileStep().shouldPrompt()).toBe(true); + }); + + it('should log and return when the functions folder is missing', async () => { + vi.mocked(fs.pathExists).mockResolvedValue(false); + const context = createContext(); + + await new FunctionFileStep().prompt(context); + + expect(ext.outputChannel.appendLog).toHaveBeenCalledWith(expect.stringContaining('is not a valid directory')); + expect(fs.writeFile).not.toHaveBeenCalled(); + }); + + it('should log and return when the functions path is not a directory', async () => { + vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => false } as any); + const context = createContext(); + + await new FunctionFileStep().prompt(context); + + expect(ext.outputChannel.appendLog).toHaveBeenCalledWith(expect.stringContaining('is not a valid directory')); + expect(fs.writeFile).not.toHaveBeenCalled(); + }); + + it('should create a .NET 8 function file from the template', async () => { + vi.mocked(fs.pathExists).mockResolvedValueOnce(true).mockResolvedValueOnce(false); + const context = createContext(); + + await new FunctionFileStep().prompt(context); + + expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('FunctionsFileNet8'), 'utf-8'); + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining('ProcessOrder.cs'), + 'namespace Contoso.Functions { public class ProcessOrder {} }' + ); + }); + + it('should create a .NET Framework function file from the template', async () => { + vi.mocked(fs.pathExists).mockResolvedValueOnce(true).mockResolvedValueOnce(false); + const context = createContext({ targetFramework: TargetFramework.NetFx }); + + await new FunctionFileStep().prompt(context); + + expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('FunctionsFileNetFx'), 'utf-8'); + expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('ProcessOrder.cs'), expect.any(String)); + }); + + it('should not overwrite an existing function file', async () => { + vi.mocked(fs.pathExists).mockResolvedValueOnce(true).mockResolvedValueOnce(true); + const context = createContext(); + + await new FunctionFileStep().prompt(context); + + expect(ext.outputChannel.appendLog).toHaveBeenCalledWith(expect.stringContaining('already exists')); + expect(window.showErrorMessage).toHaveBeenCalledWith(expect.stringContaining('already exists in the target functions project')); + expect(fs.writeFile).not.toHaveBeenCalled(); }); }); + +function createContext(overrides: Record = {}) { + return { + workspacePath: 'C:\\workspace', + functionAppName: 'Functions', + customCodeFunctionName: 'ProcessOrder', + functionAppNamespace: 'Contoso.Functions', + targetFramework: TargetFramework.Net8, + ...overrides, + } as any; +} diff --git a/apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/functionFileStep.ts b/apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/functionFileStep.ts index cc80f422392..d96458245e2 100644 --- a/apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/functionFileStep.ts +++ b/apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/functionFileStep.ts @@ -21,7 +21,6 @@ export class FunctionFileStep extends AzureWizardPromptStep { ); }); - it('should create custom code project with Net10 target framework', async () => { - const customCodeOptions = { - ...mockOptions, - logicAppType: ProjectType.customCode, - targetFramework: 'net10.0', - }; - - const mockSetup = vi.fn().mockResolvedValue(undefined); - (CreateFunctionAppFiles as Mock).mockImplementation(() => ({ - setup: mockSetup, - })); - - await createLogicAppProject(mockContext, customCodeOptions, workspaceRootFolder); - - expect(mockSetup).toHaveBeenCalledWith( - expect.objectContaining({ - projectType: ProjectType.customCode, - targetFramework: 'net10.0', - }) - ); - }); - it('should pass correct function parameters to custom code project', async () => { const customCodeOptions = { ...mockOptions, @@ -747,7 +725,6 @@ describe('createLogicAppProject - Integration Tests', () => { // Create .cs file from template using correct path const csTemplateMap: Record = { - 'net10.0': 'FunctionsFileNet10', net8: 'FunctionsFileNet8', net472: 'FunctionsFileNetFx', rulesEngine: 'RulesFunctionsFile', @@ -760,13 +737,6 @@ describe('createLogicAppProject - Integration Tests', () => { const csContent = await processTemplate(csTemplatePath, { methodName, namespace }); await fse.writeFile(path.join(functionFolderPath, `${methodName}.cs`), csContent); - // Create Program.cs for .NET 10 custom code projects - if (targetFramework === 'net10.0' && projectType !== ProjectType.rulesEngine) { - const programTemplatePath = path.join(functionTemplatesPath, 'ProgramFileNet10'); - const programContent = await processTemplate(programTemplatePath, { namespace }); - await fse.writeFile(path.join(functionFolderPath, 'Program.cs'), programContent); - } - // Create rules files for rulesEngine if (projectType === ProjectType.rulesEngine) { const contosoPurchasePath = path.join(rulesTemplatesPath, 'ContosoPurchase'); @@ -779,7 +749,6 @@ describe('createLogicAppProject - Integration Tests', () => { // Create .csproj file from template using correct path const csprojTemplateMap: Record = { - 'net10.0': 'FunctionsProjNet10', net8: 'FunctionsProjNet8', net472: 'FunctionsProjNetFx', rulesEngine: 'RulesFunctionsProj', @@ -792,7 +761,7 @@ describe('createLogicAppProject - Integration Tests', () => { let csprojContent = await fse.readFile(csprojTemplatePath, 'utf-8'); // Replace LogicApp folder references - if ((targetFramework === 'net8' || targetFramework === 'net10.0') && projectType === ProjectType.customCode) { + if (targetFramework === 'net8' && projectType === ProjectType.customCode) { csprojContent = csprojContent.replace( /\$\(MSBuildProjectDirectory\)\\..\\LogicApp<\/LogicAppFolderToPublish>/g, `$(MSBuildProjectDirectory)\\..\\${logicAppName}` @@ -1187,7 +1156,7 @@ local.settings.json` Always false - + @@ -1195,12 +1164,17 @@ local.settings.json` - + `; - expect(csprojContent.trim()).toBe(expectedCsproj.trim()); + const normalizeXml = (value: string) => + value + .replace(/\r\n/g, '\n') + .replace(/[ \t]+$/gm, '') + .trim(); + expect(normalizeXml(csprojContent)).toBe(normalizeXml(expectedCsproj)); }); it('should create .csproj file for NetFx custom code project', async () => { @@ -1281,108 +1255,6 @@ local.settings.json` const settingsContent = await fse.readJSON(settingsPath); expect(settingsContent).toHaveProperty('azureFunctions.projectRuntime'); }); - - it('should create Program.cs for Net10 custom code project with correct namespace', async () => { - const options: IWebviewProjectContext = { - workspaceProjectPath: { fsPath: tempDir } as vscode.Uri, - workspaceName: 'TestWorkspace', - logicAppName: 'TestLogicApp', - logicAppType: ProjectType.customCode, - workflowName: 'MyWorkflow', - workflowType: 'Stateful', - functionFolderName: 'Functions', - functionName: 'MyFunction', - functionNamespace: 'MyCompany.Functions', - targetFramework: 'net10.0', - } as any; - - const functionAppFiles = createTestFunctionAppFiles(); - vi.mocked(CreateFunctionAppFiles).mockImplementation( - () => - ({ - setup: (ctx: IProjectWizardContext) => functionAppFiles.setup(ctx), - hideStepCount: true, - }) as any - ); - - await createLogicAppProject(mockContext, options, workspaceRootFolder); - - // Verify Program.cs was created - const functionsFolderPath = path.join(workspaceRootFolder, 'Functions'); - const programCsPath = path.join(functionsFolderPath, 'Program.cs'); - const programCsExists = await fse.pathExists(programCsPath); - expect(programCsExists).toBe(true); - - // Verify Program.cs content has namespace replaced - const programContent = await fse.readFile(programCsPath, 'utf-8'); - expect(programContent).toContain('namespace MyCompany.Functions'); - expect(programContent).not.toContain('<%= namespace %>'); - expect(programContent).toContain('HostBuilder'); - }); - - it('should not create Program.cs for Net8 custom code project', async () => { - const options: IWebviewProjectContext = { - workspaceProjectPath: { fsPath: tempDir } as vscode.Uri, - workspaceName: 'TestWorkspace', - logicAppName: 'TestLogicApp', - logicAppType: ProjectType.customCode, - workflowName: 'MyWorkflow', - workflowType: 'Stateful', - functionFolderName: 'Functions', - functionName: 'MyFunction', - functionNamespace: 'MyNamespace', - targetFramework: 'net8', - } as any; - - const functionAppFiles = createTestFunctionAppFiles(); - vi.mocked(CreateFunctionAppFiles).mockImplementation( - () => - ({ - setup: (ctx: IProjectWizardContext) => functionAppFiles.setup(ctx), - hideStepCount: true, - }) as any - ); - - await createLogicAppProject(mockContext, options, workspaceRootFolder); - - // Verify Program.cs was NOT created for Net8 - const functionsFolderPath = path.join(workspaceRootFolder, 'Functions'); - const programCsPath = path.join(functionsFolderPath, 'Program.cs'); - const programCsExists = await fse.pathExists(programCsPath); - expect(programCsExists).toBe(false); - }); - - it('should not create Program.cs for NetFx custom code project', async () => { - const options: IWebviewProjectContext = { - workspaceProjectPath: { fsPath: tempDir } as vscode.Uri, - workspaceName: 'TestWorkspace', - logicAppName: 'TestLogicApp', - logicAppType: ProjectType.customCode, - workflowName: 'MyWorkflow', - workflowType: 'Stateful', - functionFolderName: 'Functions', - functionName: 'MyFunction', - functionNamespace: 'MyNamespace', - targetFramework: 'net472', - } as any; - - const functionAppFiles = createTestFunctionAppFiles(); - vi.mocked(CreateFunctionAppFiles).mockImplementation( - () => - ({ - setup: (ctx: IProjectWizardContext) => functionAppFiles.setup(ctx), - hideStepCount: true, - }) as any - ); - - await createLogicAppProject(mockContext, options, workspaceRootFolder); - - // Verify Program.cs was NOT created for NetFx - const functionsFolderPath = path.join(workspaceRootFolder, 'Functions'); - const programCsPath = path.join(functionsFolderPath, 'Program.cs'); - const programCsExists = await fse.pathExists(programCsPath); - expect(programCsExists).toBe(false); - }); }); describe('Rules Engine Project Integration', () => { diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppVSCodeContents.test.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppVSCodeContents.test.ts index 394ceb45bc1..3723299d6d5 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppVSCodeContents.test.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppVSCodeContents.test.ts @@ -31,13 +31,6 @@ describe('CreateLogicAppVSCodeContents', () => { isDevContainerProject: false, } as any; - const mockContextCustomCodeNet10: IWebviewProjectContext = { - logicAppName: 'TestLogicAppCustomCodeNet10', - logicAppType: ProjectType.customCode, - targetFramework: TargetFramework.Net10, - isDevContainerProject: false, - } as any; - const mockContextCustomCodeNetFx: IWebviewProjectContext = { logicAppName: 'TestLogicAppCustomCodeNetFx', logicAppType: ProjectType.customCode, @@ -125,22 +118,6 @@ describe('CreateLogicAppVSCodeContents', () => { expect(Object.keys(settingsData)).toHaveLength(4); }); - it('should create settings.json without deploySubpath for net10 custom code project', async () => { - await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCodeNet10, logicAppFolderPath); - - const settingsJsonPath = path.join(logicAppFolderPath, '.vscode', 'settings.json'); - const settingsCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === settingsJsonPath); - const settingsCallback = settingsCall[2]; - const settingsData = settingsCallback({}); - - expect(settingsData).toHaveProperty('azureFunctions.suppressProject', true); - expect(settingsData).toHaveProperty('azureLogicAppsStandard.projectLanguage', 'JavaScript'); - expect(settingsData).toHaveProperty('azureLogicAppsStandard.projectRuntime', '~4'); - expect(settingsData).toHaveProperty('debug.internalConsoleOptions', 'neverOpen'); - expect(settingsData).not.toHaveProperty('azureLogicAppsStandard.deploySubpath'); - expect(Object.keys(settingsData)).toHaveLength(4); - }); - it('should create settings.json without deploySubpath for netfx custom code project', async () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCodeNetFx, logicAppFolderPath); @@ -222,25 +199,6 @@ describe('CreateLogicAppVSCodeContents', () => { }); }); - it('should create launch.json with logicapp configuration for .NET 10 custom code projects', async () => { - await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCodeNet10, logicAppFolderPath); - - const launchJsonPath = path.join(logicAppFolderPath, '.vscode', 'launch.json'); - const launchCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === launchJsonPath); - const launchCallback = launchCall[2]; - const launchData = launchCallback({ configurations: [] }); - - const config = launchData.configurations[0]; - expect(config).toMatchObject({ - name: expect.stringContaining('Run/Debug logic app with local function TestLogicAppCustomCodeNet10'), - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - it('should create launch.json with clr runtime for NetFx rules engine projects', async () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextRulesEngine, logicAppFolderPath); @@ -368,18 +326,6 @@ describe('CreateLogicAppVSCodeContents', () => { }); }); - it('should return logicapp configuration with coreclr for Net10 custom code', () => { - const config = CreateLogicAppVSCodeContentsModule.getDebugConfiguration('TestLogicApp', TargetFramework.Net10); - - expect(config).toMatchObject({ - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - it('should return logicapp configuration with clr for NetFx custom code', () => { const config = CreateLogicAppVSCodeContentsModule.getDebugConfiguration('TestLogicApp', TargetFramework.NetFx); diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/functionAppFilesStep.test.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/functionAppFilesStep.test.ts index 39bad5dbff0..451d017dbe2 100644 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/functionAppFilesStep.test.ts +++ b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/functionAppFilesStep.test.ts @@ -1,196 +1,175 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { TargetFramework, ProjectType } from '@microsoft/vscode-extension-logic-apps'; +import { FuncVersion, ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; +import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; +import * as fs from 'fs-extra'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getDebugConfiguration } from '../../../../utils/debug'; +import { getDebugConfigs, updateDebugConfigs } from '../../../../utils/vsCodeConfig/launch'; +import { isMultiRootWorkspace } from '../../../../utils/workspace'; +import { FunctionAppFilesStep } from '../functionAppFilesStep'; vi.mock('fs-extra', () => ({ - writeFile: vi.fn(() => Promise.resolve()), - ensureDir: vi.fn(() => Promise.resolve()), - readFile: vi.fn(() => Promise.resolve('')), - pathExists: vi.fn(() => Promise.resolve(false)), - existsSync: vi.fn(() => false), - readdir: vi.fn(), - stat: vi.fn(), - writeJson: vi.fn(() => Promise.resolve()), - copyFile: vi.fn(() => Promise.resolve()), - readJson: vi.fn(() => Promise.resolve({})), -})); -vi.mock('vscode'); -vi.mock('../../../../../constants', async (importOriginal) => { - const actual = (await importOriginal()) as Record; - return { - ...actual, - }; -}); -vi.mock('../../../../../extensionVariables', () => ({ - ext: { outputChannel: { appendLog: vi.fn() } }, + ensureDir: vi.fn(), + readFile: vi.fn(), + writeFile: vi.fn(), + writeJson: vi.fn(), + copyFile: vi.fn(), + pathExists: vi.fn(), + readJson: vi.fn(), })); + +vi.mock('vscode', () => ({})); + vi.mock('../../../../../localize', () => ({ - localize: (_key: string, msg: string) => msg, + localize: (_key: string, message: string) => message, })); + vi.mock('../../../../utils/vsCodeConfig/launch', () => ({ - getDebugConfigs: vi.fn().mockReturnValue([]), + getDebugConfigs: vi.fn(), updateDebugConfigs: vi.fn(), })); + vi.mock('../../../../utils/workspace', () => ({ getContainingWorkspace: vi.fn(), - isMultiRootWorkspace: vi.fn().mockReturnValue(false), + isMultiRootWorkspace: vi.fn(), })); + vi.mock('../../../../utils/funcCoreTools/funcVersion', () => ({ - tryGetLocalFuncVersion: vi.fn().mockResolvedValue('~4'), + tryGetLocalFuncVersion: vi.fn().mockResolvedValue(FuncVersion.v4), })); + vi.mock('../../../../utils/debug', () => ({ - getCustomCodeRuntime: vi.fn((tf: string) => (tf === 'net472' ? 'clr' : 'coreclr')), - getDebugConfiguration: vi.fn().mockReturnValue({}), - usesPublishFolderProperty: vi.fn((pt: string, tf: string) => pt === 'customCode' && tf !== 'net472'), + getDebugConfiguration: vi.fn().mockReturnValue({ name: 'generated launch' }), + usesPublishFolderProperty: vi.fn((projectType: ProjectType, targetFramework: TargetFramework) => { + return projectType === ProjectType.customCode && targetFramework !== TargetFramework.NetFx; + }), })); -import { FunctionAppFilesStep } from '../functionAppFilesStep'; -import { csTemplateFileNames, csprojTemplateFileNames } from '../../../../utils/functionProjectFiles'; -import * as fs from 'fs-extra'; -import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; - describe('FunctionAppFilesStep', () => { beforeEach(() => { vi.clearAllMocks(); - }); - - describe('template name mappings', () => { - it('should map Net10 to FunctionsFileNet10 in csTemplateFileNames', () => { - expect(csTemplateFileNames[TargetFramework.Net10]).toBe('FunctionsFileNet10'); - }); - - it('should preserve Net8 mapping in csTemplateFileNames', () => { - expect(csTemplateFileNames[TargetFramework.Net8]).toBe('FunctionsFileNet8'); - }); - - it('should preserve NetFx mapping in csTemplateFileNames', () => { - expect(csTemplateFileNames[TargetFramework.NetFx]).toBe('FunctionsFileNetFx'); - }); - - it('should preserve rulesEngine mapping in csTemplateFileNames', () => { - expect(csTemplateFileNames[ProjectType.rulesEngine]).toBe('RulesFunctionsFile'); - }); - - it('should map Net10 to FunctionsProjNet10 in csprojTemplateFileNames', () => { - expect(csprojTemplateFileNames[TargetFramework.Net10]).toBe('FunctionsProjNet10'); - }); - - it('should preserve Net8 mapping in csprojTemplateFileNames', () => { - expect(csprojTemplateFileNames[TargetFramework.Net8]).toBe('FunctionsProjNet8'); - }); - - it('should preserve NetFx mapping in csprojTemplateFileNames', () => { - expect(csprojTemplateFileNames[TargetFramework.NetFx]).toBe('FunctionsProjNetFx'); - }); - - it('should preserve rulesEngine mapping in csprojTemplateFileNames', () => { - expect(csprojTemplateFileNames[ProjectType.rulesEngine]).toBe('RulesFunctionsProj'); + vi.mocked(fs.ensureDir).mockResolvedValue(undefined); + vi.mocked(fs.copyFile).mockResolvedValue(undefined); + vi.mocked(fs.pathExists).mockResolvedValue(false); + vi.mocked(fs.readJson).mockResolvedValue({}); + vi.mocked(fs.writeFile).mockResolvedValue(undefined); + vi.mocked(fs.writeJson).mockResolvedValue(undefined); + vi.mocked(getDebugConfigs).mockReturnValue([]); + vi.mocked(getDebugConfiguration).mockReturnValue({ name: 'generated launch' }); + vi.mocked(isMultiRootWorkspace).mockReturnValue(false); + vi.mocked(fs.readFile).mockImplementation((filePath: any) => { + const pathText = String(filePath); + if (pathText.includes('FunctionsProjNet8')) { + return Promise.resolve('$(MSBuildProjectDirectory)\\..\\LogicApp'); + } + if (pathText.includes('FunctionsProjNetFx')) { + return Promise.resolve('LogicApp'); + } + if (pathText.includes('RulesFunctionsProj')) { + return Promise.resolve(''); + } + return Promise.resolve('namespace <%= namespace %> { public class <%= methodName %> {} }'); }); }); - describe('shouldPrompt', () => { - it('should always return true', () => { - const step = new FunctionAppFilesStep(); - expect(step.shouldPrompt()).toBe(true); - }); + it('should always prompt', () => { + expect(new FunctionAppFilesStep().shouldPrompt()).toBe(true); }); - describe('Program.cs generation via prompt', () => { - function createMockContext(overrides: Partial = {}): IProjectWizardContext { - return { - functionAppName: 'TestFunction', - functionAppNamespace: 'TestNamespace', - targetFramework: TargetFramework.Net10, - logicAppName: 'TestLogicApp', - version: '~4', - workspacePath: '/mock/workspace', - projectType: ProjectType.customCode, - shouldCreateLogicAppProject: true, - ...overrides, - } as IProjectWizardContext; - } - - beforeEach(() => { - vi.mocked(fs.ensureDir).mockResolvedValue(undefined); - vi.mocked(fs.readFile).mockResolvedValue('template with <%= namespace %> placeholder'); - vi.mocked(fs.writeFile).mockResolvedValue(undefined); - vi.mocked(fs.writeJson).mockResolvedValue(undefined); - vi.mocked(fs.copyFile).mockResolvedValue(undefined); - vi.mocked(fs.pathExists).mockResolvedValue(false); + it('should create .NET 8 custom code project files and VS Code configuration', async () => { + await new FunctionAppFilesStep().prompt(createContext()); + + expect(fs.ensureDir).toHaveBeenCalledWith(expect.stringContaining('ProcessOrder')); + expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('FunctionsFileNet8'), 'utf-8'); + expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('FunctionsProjNet8'), 'utf-8'); + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining('ProcessOrder.cs'), + 'namespace Contoso.Functions { public class ProcessOrder {} }' + ); + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining('ProcessOrder.csproj'), + '$(MSBuildProjectDirectory)\\..\\SalesLogicApp' + ); + expect(fs.writeJson).toHaveBeenCalledWith( + expect.stringContaining('extensions.json'), + expect.objectContaining({ recommendations: expect.any(Array) }), + { + spaces: 2, + } + ); + expect(fs.writeJson).toHaveBeenCalledWith( + expect.stringContaining('settings.json'), + expect.objectContaining({ + 'azureFunctions.deploySubpath': 'bin/Release/net8/publish', + 'azureFunctions.projectSubpath': 'bin\\Release\\net8\\publish', + }), + { spaces: 2 } + ); + expect(fs.writeJson).toHaveBeenCalledWith(expect.stringContaining('tasks.json'), expect.objectContaining({ version: '2.0.0' }), { + spaces: 2, }); + }); - it('should create Program.cs for Net10 custom code project', async () => { - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.Net10, - projectType: ProjectType.customCode, - }); - - await step.prompt(context); - - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeDefined(); - expect(String(programCsCall![1])).not.toContain('<%= namespace %>'); - }); + it('should create .NET Framework project files with LogicAppFolder replacement', async () => { + await new FunctionAppFilesStep().prompt(createContext({ targetFramework: TargetFramework.NetFx })); - it('should replace namespace placeholder in Program.cs', async () => { - vi.mocked(fs.readFile).mockResolvedValue('namespace <%= namespace %>\n{\n class Program {}\n}'); - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.Net10, - projectType: ProjectType.customCode, - functionAppNamespace: 'MyCompany.Functions', - }); - - await step.prompt(context); - - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeDefined(); - expect(String(programCsCall![1])).toContain('namespace MyCompany.Functions'); - expect(String(programCsCall![1])).not.toContain('<%= namespace %>'); - }); + expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('FunctionsFileNetFx'), 'utf-8'); + expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('FunctionsProjNetFx'), 'utf-8'); + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining('ProcessOrder.csproj'), + 'SalesLogicApp' + ); + }); - it('should not create Program.cs for Net8 custom code project', async () => { - const step = new FunctionAppFilesStep(); - const context = createMockContext({ + it('should create rules engine project files and copy the sample rule file', async () => { + await new FunctionAppFilesStep().prompt( + createContext({ + projectType: ProjectType.rulesEngine, targetFramework: TargetFramework.Net8, - projectType: ProjectType.customCode, - }); - - await step.prompt(context); - - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeUndefined(); - }); - - it('should not create Program.cs for NetFx custom code project', async () => { - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.NetFx, - projectType: ProjectType.customCode, - }); + }) + ); - await step.prompt(context); + expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('RulesFunctionsFile'), 'utf-8'); + expect(fs.readFile).toHaveBeenCalledWith(expect.stringContaining('RulesFunctionsProj'), 'utf-8'); + expect(fs.copyFile).toHaveBeenCalledWith(expect.stringContaining('ContosoPurchase'), expect.stringContaining('ContosoPurchase.cs')); + }); - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeUndefined(); - }); + it('should update launch configuration for existing logic app projects', async () => { + vi.mocked(getDebugConfigs).mockReturnValue([{ type: 'node', request: 'launch' }]); - it('should not create Program.cs for rulesEngine project even with Net10', async () => { - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.Net10, - projectType: ProjectType.rulesEngine, - }); + await new FunctionAppFilesStep().prompt(createContext({ shouldCreateLogicAppProject: false })); - await step.prompt(context); + expect(getDebugConfiguration).toHaveBeenCalledWith(FuncVersion.v4, 'SalesLogicApp', TargetFramework.Net8); + expect(updateDebugConfigs).toHaveBeenCalledWith(undefined, expect.arrayContaining([{ name: 'generated launch' }])); + }); - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeUndefined(); - }); + it('should write launch.json in multi-root workspaces', async () => { + vi.mocked(isMultiRootWorkspace).mockReturnValue(true); + vi.mocked(fs.pathExists).mockResolvedValue(true); + vi.mocked(fs.readJson).mockResolvedValue({ configurations: [] }); + + await new FunctionAppFilesStep().prompt(createContext({ shouldCreateLogicAppProject: false })); + + expect(fs.writeJson).toHaveBeenCalledWith( + expect.stringContaining('launch.json'), + expect.objectContaining({ configurations: expect.any(Array) }), + { + spaces: 2, + } + ); + expect(updateDebugConfigs).not.toHaveBeenCalled(); }); }); + +function createContext(overrides: Partial = {}): IProjectWizardContext { + return { + functionAppName: 'ProcessOrder', + functionAppNamespace: 'Contoso.Functions', + targetFramework: TargetFramework.Net8, + logicAppName: 'SalesLogicApp', + version: FuncVersion.v4, + workspacePath: 'C:\\workspace', + projectType: ProjectType.customCode, + shouldCreateLogicAppProject: true, + ...overrides, + } as IProjectWizardContext; +} diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/targetFrameworkStep.test.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/targetFrameworkStep.test.ts index b79a1f184d6..939de83bda5 100644 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/targetFrameworkStep.test.ts +++ b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/targetFrameworkStep.test.ts @@ -1,105 +1,64 @@ -import { describe, it, expect, vi } from 'vitest'; -import { ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; +import { Platform, ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; +import { describe, expect, it, vi } from 'vitest'; +import { TargetFrameworkStep } from '../targetFrameworkStep'; vi.mock('../../../../../localize', () => ({ - localize: (_key: string, msg: string) => msg, + localize: (_key: string, message: string) => message, })); -import { TargetFrameworkStep } from '../targetFrameworkStep'; - describe('TargetFrameworkStep', () => { - describe('shouldPrompt', () => { - it('should return true for customCode projects', () => { - const step = new TargetFrameworkStep(); - const context = { projectType: ProjectType.customCode } as any; - expect(step.shouldPrompt(context)).toBe(true); - }); + it('should prompt only for custom code projects', () => { + const step = new TargetFrameworkStep(); - it('should return false for logicApp projects', () => { - const step = new TargetFrameworkStep(); - const context = { projectType: ProjectType.logicApp } as any; - expect(step.shouldPrompt(context)).toBe(false); - }); - - it('should return false for rulesEngine projects', () => { - const step = new TargetFrameworkStep(); - const context = { projectType: ProjectType.rulesEngine } as any; - expect(step.shouldPrompt(context)).toBe(false); - }); + expect(step.shouldPrompt({ projectType: ProjectType.customCode } as any)).toBe(true); + expect(step.shouldPrompt({ projectType: ProjectType.logicApp } as any)).toBe(false); + expect(step.shouldPrompt({ projectType: ProjectType.rulesEngine } as any)).toBe(false); }); - describe('prompt', () => { - it('should offer .NET 8 and .NET 10 picks on non-Windows platforms', async () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { value: 'darwin' }); - - const step = new TargetFrameworkStep(); - let capturedPicks: any[] = []; - const context = { - projectType: ProjectType.customCode, - ui: { - showQuickPick: vi.fn((picks: any[]) => { - capturedPicks = picks; - return Promise.resolve(picks[0]); - }), - }, - } as any; - - await step.prompt(context); - - expect(capturedPicks).toHaveLength(2); - expect(capturedPicks[0].data).toBe(TargetFramework.Net8); - expect(capturedPicks[1].data).toBe(TargetFramework.Net10); - - Object.defineProperty(process, 'platform', { value: originalPlatform }); + it('should offer only .NET 8 on non-Windows platforms', async () => { + const restorePlatform = mockPlatform(Platform.mac); + const context = createContext(); + let capturedPicks: any[] = []; + context.ui.showQuickPick.mockImplementation((picks: any[]) => { + capturedPicks = picks; + return Promise.resolve(picks[0]); }); - it('should include .NET Framework on Windows', async () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { value: 'win32' }); - - const step = new TargetFrameworkStep(); - let capturedPicks: any[] = []; - const context = { - projectType: ProjectType.customCode, - ui: { - showQuickPick: vi.fn((picks: any[]) => { - capturedPicks = picks; - return Promise.resolve(picks[0]); - }), - }, - } as any; + await new TargetFrameworkStep().prompt(context); - await step.prompt(context); - - expect(capturedPicks).toHaveLength(3); - expect(capturedPicks[0].data).toBe(TargetFramework.NetFx); - expect(capturedPicks[1].data).toBe(TargetFramework.Net8); - expect(capturedPicks[2].data).toBe(TargetFramework.Net10); + expect(capturedPicks.map((pick) => pick.data)).toEqual([TargetFramework.Net8]); + expect(context.targetFramework).toBe(TargetFramework.Net8); + restorePlatform(); + }); - Object.defineProperty(process, 'platform', { value: originalPlatform }); + it('should offer .NET Framework before .NET 8 on Windows', async () => { + const restorePlatform = mockPlatform(Platform.windows); + const context = createContext(); + let capturedPicks: any[] = []; + context.ui.showQuickPick.mockImplementation((picks: any[]) => { + capturedPicks = picks; + return Promise.resolve(picks[1]); }); - it('should set context.targetFramework to the selected value', async () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { value: 'darwin' }); + await new TargetFrameworkStep().prompt(context); - const step = new TargetFrameworkStep(); - const context = { - projectType: ProjectType.customCode, - ui: { - showQuickPick: vi.fn((picks: any[]) => { - // Simulate selecting .NET 10 - const net10Pick = picks.find((p: any) => p.data === TargetFramework.Net10); - return Promise.resolve(net10Pick); - }), - }, - } as any; - - await step.prompt(context); - expect(context.targetFramework).toBe(TargetFramework.Net10); - - Object.defineProperty(process, 'platform', { value: originalPlatform }); - }); + expect(capturedPicks.map((pick) => pick.data)).toEqual([TargetFramework.NetFx, TargetFramework.Net8]); + expect(context.targetFramework).toBe(TargetFramework.Net8); + restorePlatform(); }); }); + +function createContext() { + return { + projectType: ProjectType.customCode, + ui: { + showQuickPick: vi.fn(), + }, + } as any; +} + +function mockPlatform(platform: NodeJS.Platform): () => void { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: platform, configurable: true }); + return () => Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); +} diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppFilesStep.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppFilesStep.ts index 10c613498ce..d398d56678e 100644 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppFilesStep.ts +++ b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppFilesStep.ts @@ -22,7 +22,7 @@ import { getDebugConfigs, updateDebugConfigs } from '../../../utils/vsCodeConfig import { getContainingWorkspace, isMultiRootWorkspace } from '../../../utils/workspace'; import { tryGetLocalFuncVersion } from '../../../utils/funcCoreTools/funcVersion'; import { getCustomCodeRuntime, getDebugConfiguration } from '../../../utils/debug'; -import { createCsFile, createProgramFile, createRulesFiles, createCsprojFile } from '../../../utils/functionProjectFiles'; +import { createCsFile, createRulesFiles, createCsprojFile } from '../../../utils/functionProjectFiles'; /** * This class represents a prompt step that allows the user to set up an Azure Function project. @@ -51,7 +51,6 @@ export class FunctionAppFilesStep extends AzureWizardPromptStep { const placeHolder: string = localize('selectTargetFramework', 'Select a target framework.'); - const picks: IAzureQuickPickItem[] = [ - { label: localize('Net8', '.NET 8'), data: TargetFramework.Net8 }, - { label: localize('Net10', '.NET 10'), data: TargetFramework.Net10 }, - ]; + const picks: IAzureQuickPickItem[] = [{ label: localize('Net8', '.NET 8'), data: TargetFramework.Net8 }]; if (process.platform === Platform.windows) { picks.unshift({ label: localize('NetFx', '.NET Framework'), data: TargetFramework.NetFx }); } diff --git a/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/__test__/codefulWorkflowCreateStep.test.ts b/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/__test__/codefulWorkflowCreateStep.test.ts index fa20bc690ce..efab9934f12 100644 --- a/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/__test__/codefulWorkflowCreateStep.test.ts +++ b/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/__test__/codefulWorkflowCreateStep.test.ts @@ -1,49 +1,302 @@ -import { describe, it, expect, vi, Mock } from 'vitest'; -import { CodefulWorkflowCreateStep } from '../codefulWorkflowCreateStep'; -import { IFunctionWizardContext, WorkerRuntime } from '@microsoft/vscode-extension-logic-apps'; -import { setLocalAppSetting } from '../../../../../utils/appSettings/localSettings'; +import { FuncVersion, WorkerRuntime, WorkflowProjectType } from '@microsoft/vscode-extension-logic-apps'; +import type { IFunctionWizardContext } from '@microsoft/vscode-extension-logic-apps'; +import * as fse from 'fs-extra'; +import { writeFileSync } from 'fs'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { appKindSetting, azureWebJobsStorageKey, + codefulWorkflowFileName, functionsInprocNet8Enabled, functionsInprocNet8EnabledTrue, localEmulatorConnectionString, logicAppKind, workerRuntimeKey, } from '../../../../../../constants'; +import { setLocalAppSetting, removeAppKindFromLocalSettings } from '../../../../../utils/appSettings/localSettings'; +import { createConnectionsJson } from '../../../../../utils/codeless/connection'; +import { createEmptyParametersJson } from '../../../../../utils/codeless/parameter'; +import { getCodefulWorkflowTemplate } from '../../../../../utils/codeless/templates'; +import { getDebugConfiguration } from '../../../../../utils/debug'; +import { validateDotnetInstalled } from '../../../../../utils/dotnet/executeDotnetTemplateCommand'; +import { getDebugConfigs } from '../../../../../utils/vsCodeConfig/launch'; +import { getWorkspaceFolder, isMultiRootWorkspace } from '../../../../../utils/workspace'; +import { switchToDotnetProject } from '../../../../workflows/switchToDotnetProject'; +import { CodefulWorkflowCreateStep } from '../codefulWorkflowCreateStep'; + +vi.mock('fs-extra', () => ({ + ensureDir: vi.fn(), + writeFile: vi.fn(), + writeJson: vi.fn(), + readJson: vi.fn(), +})); + +vi.mock('fs', () => ({ + writeFileSync: vi.fn(), +})); + +vi.mock('@microsoft/vscode-azext-utils', () => ({ + AzureWizardExecuteStep: class {}, + callWithTelemetryAndErrorHandling: vi.fn(), + DialogResponses: { cancel: { title: 'Cancel' } }, + nonNullProp: (object: Record, property: string) => object[property], + parseError: (error: Error) => error, +})); + +vi.mock('../../../../../utils/appSettings/localSettings', () => ({ + setLocalAppSetting: vi.fn(), + removeAppKindFromLocalSettings: vi.fn(), +})); + +vi.mock('../../../../../utils/codeless/templates', () => ({ + getCodefulWorkflowTemplate: vi.fn().mockResolvedValue('{"definition":{}}'), +})); + +vi.mock('../../../../../utils/codeless/connection', () => ({ + createConnectionsJson: vi.fn(), +})); + +vi.mock('../../../../../utils/codeless/common', () => ({ + createJsonFileIfDoesNotExist: vi.fn(), +})); + +vi.mock('../../../../../utils/codeless/parameter', () => ({ + createEmptyParametersJson: vi.fn(), +})); + +vi.mock('../../../../../utils/dotnet/executeDotnetTemplateCommand', () => ({ + validateDotnetInstalled: vi.fn(), +})); + +vi.mock('../../../../workflows/switchToDotnetProject', () => ({ + switchToDotnetProject: vi.fn(), +})); + +vi.mock('../../../../../utils/vsCodeConfig/launch', () => ({ + getDebugConfigs: vi.fn().mockReturnValue([]), + updateDebugConfigs: vi.fn(), +})); + +vi.mock('../../../../../utils/workspace', () => ({ + getWorkspaceFolder: vi.fn().mockResolvedValue(undefined), + isMultiRootWorkspace: vi.fn().mockReturnValue(false), +})); + +vi.mock('../../../../../utils/debug', () => ({ + getDebugConfiguration: vi.fn().mockReturnValue({ name: 'generated launch' }), +})); + +vi.mock('../../../../../utils/fs', () => ({ + writeFormattedJson: vi.fn(), +})); + +vi.mock('vscode', () => ({ + Uri: { + file: (fsPath: string) => ({ fsPath }), + }, +})); + +describe('CodefulWorkflowCreateStep', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(fse.ensureDir).mockResolvedValue(undefined); + vi.mocked(fse.writeFile).mockResolvedValue(undefined); + vi.mocked(fse.writeJson).mockResolvedValue(undefined); + vi.mocked(fse.readJson).mockResolvedValue({}); + vi.mocked(getCodefulWorkflowTemplate).mockResolvedValue('{"definition":{}}'); + vi.mocked(setLocalAppSetting).mockResolvedValue(undefined); + vi.mocked(removeAppKindFromLocalSettings).mockResolvedValue(undefined); + vi.mocked(getDebugConfigs).mockReturnValue([]); + vi.mocked(getDebugConfiguration).mockReturnValue({ name: 'generated launch' }); + vi.mocked(isMultiRootWorkspace).mockReturnValue(false); + vi.mocked(getWorkspaceFolder).mockResolvedValue(undefined); + }); + + it('should create a codeful workflow and return its workflow file path', async () => { + const step = new CodefulWorkflowCreateStep(); + const context = createContext(); + const updateLogicAppLaunchJson = vi.spyOn(step as any, 'updateLogicAppLaunchJson').mockResolvedValue(undefined); + vi.spyOn(step, 'createSystemArtifacts').mockResolvedValue(undefined); + + const workflowFilePath = await step.executeCore(context); + + expect(validateDotnetInstalled).toHaveBeenCalledWith(context); + expect(getCodefulWorkflowTemplate).toHaveBeenCalled(); + expectPath(vi.mocked(fse.ensureDir).mock.calls[0][0] as string).toBe('C:/project/ProcessOrder'); + expectPath(vi.mocked(fse.writeFile).mock.calls[0][0] as string).toBe(`C:/project/ProcessOrder/${codefulWorkflowFileName}`); + expect(vi.mocked(fse.writeFile).mock.calls[0][1]).toBe('{"definition":{}}'); + expect(createConnectionsJson).toHaveBeenCalledWith(context.projectPath); + expect(createEmptyParametersJson).toHaveBeenCalledWith(context.projectPath); + expectPath(vi.mocked(writeFileSync).mock.calls[0][0] as string).toBe('C:/project/nuget.config'); + expect(vi.mocked(writeFileSync).mock.calls[0][1]).toEqual(expect.stringContaining('LocalPackages')); + expect(step.createSystemArtifacts).toHaveBeenCalledWith(context); + expect(getWorkspaceFolder).toHaveBeenCalledWith(context); + expect(updateLogicAppLaunchJson).toHaveBeenCalledWith( + context.projectPath, + context.targetFramework, + undefined, + FuncVersion.v4, + context.logicAppName + ); + expectPath(workflowFilePath).toBe(`C:/project/ProcessOrder/${codefulWorkflowFileName}`); + }); + + it('should update app settings for codeful workflows', async () => { + const context = createContext(); + + await new CodefulWorkflowCreateStep().updateAppSettings(context); + + expect(setLocalAppSetting).toHaveBeenCalledWith( + context, + context.projectPath, + workerRuntimeKey, + WorkerRuntime.Dotnet, + expect.anything() + ); + expect(setLocalAppSetting).toHaveBeenCalledWith( + context, + context.projectPath, + functionsInprocNet8Enabled, + functionsInprocNet8EnabledTrue, + expect.anything() + ); + expect(setLocalAppSetting).toHaveBeenCalledWith(context, context.projectPath, appKindSetting, logicAppKind, expect.anything()); + expect(setLocalAppSetting).toHaveBeenCalledWith( + context, + context.projectPath, + azureWebJobsStorageKey, + localEmulatorConnectionString, + expect.anything() + ); + expect(removeAppKindFromLocalSettings).toHaveBeenCalledWith(context.projectPath, context); + }); + + it('should create system artifacts with .NET 8 codeful conversion', async () => { + const step = new CodefulWorkflowCreateStep(); + const context = createContext(); + vi.spyOn(step as any, 'updateHostJson').mockResolvedValue(undefined); + vi.spyOn(step, 'updateAppSettings').mockResolvedValue(undefined); -describe('CodefulWorkflowCreateStep', async () => { - describe('updateAppSettings', async () => { - it('update app settings with mock setLocalAppSetting', async () => { - const mockContext: Partial = { projectPath: 'testPath' }; - const testCodefulWorkflowCreateStep = new CodefulWorkflowCreateStep(); - - vi.mock('../../../../../utils/appSettings/localSettings', () => ({ - setLocalAppSetting: vi.fn().mockReturnValue(Promise.resolve()), - removeAppKindFromLocalSettings: vi.fn(), - })); - - testCodefulWorkflowCreateStep.updateAppSettings(mockContext as IFunctionWizardContext); - expect( - (setLocalAppSetting as unknown as Mock).mock.calls.some((call) => { - return call[2] == workerRuntimeKey && call[3] == WorkerRuntime.Dotnet; - }) - ); - expect( - (setLocalAppSetting as unknown as Mock).mock.calls.some((call) => { - return call[2] == functionsInprocNet8Enabled && call[3] == functionsInprocNet8EnabledTrue; - }) - ); - expect( - (setLocalAppSetting as unknown as Mock).mock.calls.some((call) => { - return call[2] == appKindSetting && call[3] == logicAppKind; - }) - ); - expect( - (setLocalAppSetting as unknown as Mock).mock.calls.some((call) => { - return call[2] == azureWebJobsStorageKey && call[1] == localEmulatorConnectionString; - }) - ); + await step.createSystemArtifacts(context); + + expect(switchToDotnetProject).toHaveBeenCalledWith(context, { fsPath: context.projectPath }, '8', true); + expect((step as any).updateHostJson).toHaveBeenCalledWith(context, 'host.json'); + expect(step.updateAppSettings).toHaveBeenCalledWith(context); + }); + + it('should add the temporary NuGet config', () => { + const step = new CodefulWorkflowCreateStep(); + + (step as any).addNugetConfig('C:\\project'); + + expectPath(vi.mocked(writeFileSync).mock.calls[0][0] as string).toBe('C:/project/nuget.config'); + expect(vi.mocked(writeFileSync).mock.calls[0][1]).toEqual(expect.stringContaining('nuget.org')); + }); + + it('should remove extension bundles from host.json for codeful workflows', async () => { + const step = new CodefulWorkflowCreateStep(); + const context = createContext(); + context.workflowProjectType = WorkflowProjectType.Bundle; + vi.spyOn(step as any, 'getHostJson').mockResolvedValue({ + extensionBundle: { + id: 'legacy.bundle', + version: '[0.*, 1.0.0)', + }, }); + const { writeFormattedJson } = await import('../../../../../utils/fs'); + + await (step as any).updateHostJson(context, 'host.json'); + + expectPath(vi.mocked(writeFormattedJson).mock.calls[0][0] as string).toBe('C:/project/host.json'); + expect(vi.mocked(writeFormattedJson).mock.calls[0][1]).toEqual( + expect.objectContaining({ + extensionBundle: undefined, + }) + ); + }); + + it('should update launch.json in multi-root workspaces', async () => { + vi.mocked(isMultiRootWorkspace).mockReturnValue(true); + + await (new CodefulWorkflowCreateStep() as any).updateLogicAppLaunchJson( + 'C:\\project', + 'net8', + undefined, + FuncVersion.v4, + 'SalesLogicApp' + ); + + expectPath(vi.mocked(fse.writeJson).mock.calls[0][0] as string).toBe('C:/project/.vscode/launch.json'); + expect(vi.mocked(fse.writeJson).mock.calls[0][1]).toEqual( + expect.objectContaining({ + configurations: expect.arrayContaining([expect.objectContaining({ name: 'generated launch' })]), + }) + ); + expect(vi.mocked(fse.writeJson).mock.calls[0][2]).toEqual({ spaces: 2 }); + }); + + it('should update existing launch configs in single-root workspaces', async () => { + vi.mocked(getDebugConfigs).mockReturnValue([{ type: 'logicapp', request: 'launch' }]); + const { updateDebugConfigs } = await import('../../../../../utils/vsCodeConfig/launch'); + + await (new CodefulWorkflowCreateStep() as any).updateLogicAppLaunchJson( + 'C:\\project', + 'net8', + undefined, + FuncVersion.v4, + 'SalesLogicApp' + ); + + expect(updateDebugConfigs).toHaveBeenCalledWith( + undefined, + expect.arrayContaining([expect.objectContaining({ customCodeRuntime: 'coreclr', funcRuntime: 'coreclr', isCodeless: false })]) + ); + }); + + it('should preserve non-logicapp debug configs when updating existing launch configs', async () => { + const existingDebugConfig = { type: 'node', request: 'launch' }; + vi.mocked(getDebugConfigs).mockReturnValue([{ type: 'logicapp', request: 'launch' }, existingDebugConfig]); + const { updateDebugConfigs } = await import('../../../../../utils/vsCodeConfig/launch'); + + await (new CodefulWorkflowCreateStep() as any).updateLogicAppLaunchJson( + 'C:\\project', + 'net8', + undefined, + FuncVersion.v4, + 'SalesLogicApp' + ); + + expect(updateDebugConfigs).toHaveBeenCalledWith(undefined, expect.arrayContaining([existingDebugConfig])); + }); + + it('should filter attach pick-process configs when generating launch configs', async () => { + const attachConfig = { request: 'attach', processId: '${command:azureLogicAppsStandard.pickProcess}' }; + const keepConfig = { type: 'node', request: 'launch' }; + vi.mocked(getDebugConfigs).mockReturnValue([attachConfig, keepConfig]); + const { updateDebugConfigs } = await import('../../../../../utils/vsCodeConfig/launch'); + + await (new CodefulWorkflowCreateStep() as any).updateLogicAppLaunchJson( + 'C:\\project', + 'net8', + undefined, + FuncVersion.v4, + 'SalesLogicApp' + ); + + expect(updateDebugConfigs).toHaveBeenCalledWith(undefined, [expect.objectContaining({ name: 'generated launch' }), keepConfig]); }); }); + +function createContext(): IFunctionWizardContext { + return { + projectPath: 'C:\\project', + functionName: 'ProcessOrder', + logicAppName: 'SalesLogicApp', + targetFramework: 'net8', + workspaceFolder: undefined, + } as IFunctionWizardContext; +} + +function expectPath(actualPath: string) { + return expect(actualPath.replace(/\\/g, '/')); +} diff --git a/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/codefulWorkflowCreateStep.ts b/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/codefulWorkflowCreateStep.ts index e674f710846..e1cb5c184e6 100644 --- a/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/codefulWorkflowCreateStep.ts +++ b/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/codefulWorkflowCreateStep.ts @@ -169,7 +169,7 @@ export class CodefulWorkflowCreateStep extends WorkflowCreateStepBase { const target = vscode.Uri.file(context.projectPath); - await switchToDotnetProject(context, target, '10', true); + await switchToDotnetProject(context, target, '8', true); await this.updateHostJson(context, hostFileName); diff --git a/apps/vs-code-designer/src/app/commands/workflows/__test__/switchToDotnetProject.test.ts b/apps/vs-code-designer/src/app/commands/workflows/__test__/switchToDotnetProject.test.ts index 43fb722fe00..f20e81d8e95 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/__test__/switchToDotnetProject.test.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/__test__/switchToDotnetProject.test.ts @@ -316,6 +316,15 @@ describe('switchToDotnetProject', () => { }); describe('binaries handling', () => { + it('should request .NET 8 from managed binaries by default', async () => { + vi.mocked(useBinariesDependencies).mockResolvedValue(true); + vi.mocked(getLocalDotNetVersionFromBinaries).mockResolvedValue('8.0.100'); + + await switchToDotnetProject(mockContext, mockTarget); + + expect(getLocalDotNetVersionFromBinaries).toHaveBeenCalledWith('8'); + }); + it('should create global.json when using binaries', async () => { vi.mocked(useBinariesDependencies).mockResolvedValue(true); vi.mocked(getLocalDotNetVersionFromBinaries).mockResolvedValue('8.0.100'); @@ -338,6 +347,41 @@ describe('switchToDotnetProject', () => { }); }); + describe('build file updates', () => { + it('should add codeful package references and target framework for codeful conversion', async () => { + await switchToDotnetProject(mockContext, mockTarget, '8', true); + + expect(addNugetPackagesToBuildFileByName).toHaveBeenCalledTimes(3); + expect(addNugetPackagesToBuildFileByName).toHaveBeenCalledWith(expect.any(Object), 'DurableTask', '1.0.0'); + expect(addNugetPackagesToBuildFileByName).toHaveBeenCalledWith(expect.any(Object), 'WorkflowsWebJobs', '1.0.0'); + expect(addNugetPackagesToBuildFileByName).toHaveBeenCalledWith(expect.any(Object), 'WorkflowsSDK', '1.0.0'); + expect(addNugetPackagesToBuildFile).not.toHaveBeenCalled(); + }); + + it('should add discovered workflows, artifacts, connections, parameters, and lib folder to the build file', async () => { + (fse.readdir as unknown as Mock).mockImplementation(async (folderPath: string) => { + if (folderPath === mockTarget.fsPath) { + return ['Stateful1', 'Artifacts', 'lib', 'connections.json', 'parameters.json']; + } + if (folderPath === `${mockTarget.fsPath}/Stateful1`) { + return ['workflow.json']; + } + return []; + }); + (fse.stat as unknown as Mock).mockImplementation(async (filePath: string) => ({ + isDirectory: () => filePath.endsWith('Stateful1'), + })); + + await switchToDotnetProject(mockContext, mockTarget); + + expect(addFolderToBuildPath).toHaveBeenCalledWith(expect.any(Object), 'Stateful1'); + expect(addFolderToBuildPath).toHaveBeenCalledWith(expect.any(Object), 'Artifacts'); + expect(addFileToBuildPath).toHaveBeenCalledWith(expect.any(Object), 'connections.json'); + expect(addFileToBuildPath).toHaveBeenCalledWith(expect.any(Object), 'parameters.json'); + expect(addLibToPublishPath).toHaveBeenCalledWith(expect.any(Object)); + }); + }); + describe('switchToDotnetProjectCommand', () => { it('should delegate to switchToDotnetProject', async () => { // switchToDotnetProjectCommand just calls switchToDotnetProject diff --git a/apps/vs-code-designer/src/app/commands/workflows/switchToDotnetProject.ts b/apps/vs-code-designer/src/app/commands/workflows/switchToDotnetProject.ts index a89465f229f..8d087d50450 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/switchToDotnetProject.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/switchToDotnetProject.ts @@ -56,7 +56,7 @@ export async function switchToDotnetProjectCommand(context: IProjectWizardContex export async function switchToDotnetProject( context: IProjectWizardContext, target: vscode.Uri, - localDotNetMajorVersion = '10', + localDotNetMajorVersion = '8', isCodeful = false ) { if (target === undefined || Object.keys(target).length === 0) { diff --git a/apps/vs-code-designer/src/app/utils/__test__/customCodeUtils.test.ts b/apps/vs-code-designer/src/app/utils/__test__/customCodeUtils.test.ts index bbc94b8b8ff..9a11b3a3eee 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/customCodeUtils.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/customCodeUtils.test.ts @@ -5,6 +5,7 @@ import * as workspaceUtils from '../workspace'; import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest'; import { CustomCodeFunctionsProjectMetadata, + customCodeArtifactsExist, getCustomCodeFunctionsProjectMetadata, getAllCustomCodeFunctionsProjects, isCustomCodeFunctionsProject, @@ -21,6 +22,7 @@ vi.mock('fs-extra', () => ({ readdir: vi.fn(), readFile: vi.fn(), pathExists: vi.fn(), + pathExistsSync: vi.fn(), })); vi.mock('verifyProjectUtils', () => ({ @@ -41,18 +43,15 @@ vi.mock('../../../extensionVariables', () => ({ describe('customCodeUtils', () => { let validNet8CsprojContent: string; - let validNet10CsprojContent: string; let validNetFxCsprojContent: string; let invalidCsprojContent: string; beforeAll(async () => { const realFs = await vi.importActual('fs-extra'); const assetsFolderPath = path.join(__dirname, '..', '..', '..', assetsFolderName); - const net10CsprojTemplatePath = path.join(assetsFolderPath, 'FunctionProjectTemplate', 'FunctionsProjNet10'); const net8CsprojTemplatePath = path.join(assetsFolderPath, 'FunctionProjectTemplate', 'FunctionsProjNet8'); const netFxCsprojTemplatePath = path.join(assetsFolderPath, 'FunctionProjectTemplate', 'FunctionsProjNetFx'); - validNet10CsprojContent = await realFs.readFile(net10CsprojTemplatePath, 'utf8'); validNet8CsprojContent = await realFs.readFile(net8CsprojTemplatePath, 'utf8'); validNetFxCsprojContent = await realFs.readFile(netFxCsprojTemplatePath, 'utf8'); invalidCsprojContent = ` @@ -134,6 +133,63 @@ describe('customCodeUtils', () => { }); }); + describe('customCodeArtifactsExist', () => { + const logicAppPath = path.join('test', 'LogicApp'); + const customArtifactsPath = path.join(logicAppPath, 'lib', 'custom'); + const peerProjectPath = path.join('test', 'Functions'); + + it('should return false when custom artifacts folder is missing', async () => { + vi.spyOn(fse, 'pathExists').mockResolvedValue(false); + + await expect(customCodeArtifactsExist(logicAppPath)).resolves.toBe(false); + }); + + it('should return false when a custom code artifact function.json is missing', async () => { + vi.spyOn(fse, 'pathExists').mockImplementation(async (p: string) => p === customArtifactsPath); + vi.spyOn(verifyProjectUtils, 'isLogicAppProject').mockResolvedValue(true); + vi.spyOn(fse, 'readdir').mockImplementation(async (p: string) => { + if (p === path.dirname(logicAppPath)) { + return ['LogicApp', 'Functions']; + } + if (p === peerProjectPath) { + return ['Functions.csproj']; + } + if (p === customArtifactsPath) { + return ['Functions']; + } + return []; + }); + vi.spyOn(fse, 'statSync').mockReturnValue({ isDirectory: () => true } as any); + vi.spyOn(fse, 'readFile').mockResolvedValue(validNet8CsprojContent); + + await expect(customCodeArtifactsExist(logicAppPath)).resolves.toBe(false); + }); + + it('should return true when every custom code project has generated artifacts', async () => { + vi.spyOn(fse, 'pathExists').mockImplementation( + async (p: string) => p === customArtifactsPath || p.endsWith(path.join('Functions', 'function.json')) + ); + vi.spyOn(fse, 'pathExistsSync').mockImplementation((p: string) => p.endsWith(path.join('Functions', 'function.json'))); + vi.spyOn(verifyProjectUtils, 'isLogicAppProject').mockResolvedValue(true); + vi.spyOn(fse, 'readdir').mockImplementation(async (p: string) => { + if (p === path.dirname(logicAppPath)) { + return ['LogicApp', 'Functions']; + } + if (p === peerProjectPath) { + return ['Functions.csproj']; + } + if (p === customArtifactsPath) { + return ['Functions']; + } + return []; + }); + vi.spyOn(fse, 'statSync').mockReturnValue({ isDirectory: () => true } as any); + vi.spyOn(fse, 'readFile').mockResolvedValue(validNet8CsprojContent); + + await expect(customCodeArtifactsExist(logicAppPath)).resolves.toBe(true); + }); + }); + describe('isCustomCodeFunctionsProject', () => { const testFolderPath = path.join('test', 'folder', 'path'); const testCsprojFile = 'Function.csproj'; @@ -159,14 +215,6 @@ describe('customCodeUtils', () => { expect(result).toBe(true); }); - it('should return true for a valid net10 csproj file', async () => { - vi.spyOn(fse, 'statSync').mockReturnValue({ isDirectory: () => true } as any); - vi.spyOn(fse, 'readdir').mockResolvedValue([testCsprojFile]); - vi.spyOn(fse, 'readFile').mockResolvedValue(validNet10CsprojContent); - const result = await isCustomCodeFunctionsProject(testFolderPath); - expect(result).toBe(true); - }); - it('should return true for a valid netfx csproj file', async () => { vi.spyOn(fse, 'statSync').mockReturnValue({ isDirectory: () => true } as any); vi.spyOn(fse, 'readdir').mockResolvedValue([testCsprojFile]); @@ -262,25 +310,6 @@ describe('customCodeUtils', () => { } as CustomCodeFunctionsProjectMetadata); }); - it('should return metadata for a valid net10 csproj file', async () => { - vi.spyOn(fse, 'readdir').mockResolvedValue([testCsFile, testCsprojFile]); - vi.spyOn(fse, 'readFile').mockImplementation(async (p: string) => { - if (p.endsWith('.csproj')) { - return validNet10CsprojContent; - } - return `namespace ${testNamespace} {}`; - }); - - const result = await getCustomCodeFunctionsProjectMetadata(testFolderPath); - expect(result).toEqual({ - projectPath: testFolderPath, - functionAppName: testFunctionName, - logicAppName: 'LogicApp', - targetFramework: TargetFramework.Net10, - namespace: testNamespace, - } as CustomCodeFunctionsProjectMetadata); - }); - it('should return metadata for a valid netfx csproj file', async () => { vi.spyOn(fse, 'readdir').mockResolvedValue([testCsFile, testCsprojFile]); vi.spyOn(fse, 'readFile').mockImplementation(async (p: string) => { diff --git a/apps/vs-code-designer/src/app/utils/__test__/debug.test.ts b/apps/vs-code-designer/src/app/utils/__test__/debug.test.ts index 9b83b0f0e37..acc983cd892 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/debug.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/debug.test.ts @@ -1,43 +1,23 @@ -import { getCustomCodeRuntime, getDebugConfiguration, usesPublishFolderProperty } from '../debug'; +import { getDebugConfiguration, getDebugSymbolDll } from '../debug'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { extensionCommand } from '../../../constants'; -import { FuncVersion, ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; +import { debugSymbolDll, extensionBundleId, extensionCommand } from '../../../constants'; +import { FuncVersion, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; -describe('debug', () => { - describe('getCustomCodeRuntime', () => { - it('should return coreclr for .NET 8', () => { - expect(getCustomCodeRuntime(TargetFramework.Net8)).toBe('coreclr'); - }); +vi.mock('../bundleFeed', () => ({ + getBundleVersionNumber: vi.fn(), + getExtensionBundleFolder: vi.fn(), +})); - it('should return coreclr for .NET 10', () => { - expect(getCustomCodeRuntime(TargetFramework.Net10)).toBe('coreclr'); - }); +import { getBundleVersionNumber, getExtensionBundleFolder } from '../bundleFeed'; - it('should return clr for .NET Framework', () => { - expect(getCustomCodeRuntime(TargetFramework.NetFx)).toBe('clr'); - }); - }); - - describe('usesPublishFolderProperty', () => { - it('should return true for custom code with .NET 8', () => { - expect(usesPublishFolderProperty(ProjectType.customCode, TargetFramework.Net8)).toBe(true); - }); - - it('should return true for custom code with .NET 10', () => { - expect(usesPublishFolderProperty(ProjectType.customCode, TargetFramework.Net10)).toBe(true); - }); - - it('should return false for custom code with .NET Framework', () => { - expect(usesPublishFolderProperty(ProjectType.customCode, TargetFramework.NetFx)).toBe(false); - }); +describe('debug', () => { + it('should build the debug symbol dll path from the extension bundle folder and version', async () => { + vi.mocked(getExtensionBundleFolder).mockResolvedValue('C:\\bundles'); + vi.mocked(getBundleVersionNumber).mockResolvedValue('1.2.3'); - it('should return false for rules engine projects', () => { - expect(usesPublishFolderProperty(ProjectType.rulesEngine, TargetFramework.Net8)).toBe(false); - }); + const result = await getDebugSymbolDll(); - it('should return false for standard logic app projects', () => { - expect(usesPublishFolderProperty(ProjectType.logicApp, TargetFramework.Net8)).toBe(false); - }); + expect(result.replace(/\\/g, '/')).toContain(`${extensionBundleId}/1.2.3/bin/${debugSymbolDll}`); }); describe('getDebugConfiguration', () => { @@ -58,19 +38,6 @@ describe('debug', () => { }); }); - it('should return launch configuration for .NET 10 custom code with v4 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v4, 'TestLogicApp', TargetFramework.Net10); - - expect(result).toEqual({ - name: 'Run/Debug logic app with local function TestLogicApp', - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - it('should return launch configuration for .NET Framework custom code with v1 function runtime', () => { const result = getDebugConfiguration(FuncVersion.v1, 'TestLogicApp', TargetFramework.NetFx); diff --git a/apps/vs-code-designer/src/app/utils/codeless/__test__/updateBuildFile.test.ts b/apps/vs-code-designer/src/app/utils/codeless/__test__/updateBuildFile.test.ts index c0b61e13e1d..9e6676bce60 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/__test__/updateBuildFile.test.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/__test__/updateBuildFile.test.ts @@ -1,15 +1,31 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { addFileToBuildPath, addFolderToBuildPath, addLibToPublishPath, + addNewFileInCSharpProject, addNugetPackagesToBuildFile, + addNugetPackagesToBuildFileByName, allowLocalSettingsToPublishDirectory, + getDotnetBuildFile, + getXMLString, suppressJavaScriptBuildWarnings, updateFunctionsSDKVersion, + writeBuildFileToDisk, } from '../updateBuildFile'; import { DotnetVersion, localSettingsFileName } from '../../../../constants'; import path from 'path'; +import * as fs from 'fs'; +import { getProjFiles } from '../../dotnet/dotnet'; + +vi.mock('../../dotnet/dotnet', () => ({ + getProjFiles: vi.fn(), +})); + +vi.mock('fs', () => ({ + readFileSync: vi.fn(), + writeFileSync: vi.fn(), +})); describe('utils/codeless/updateBuildFile', () => { describe('addNugetPackagesToBuildFile', () => { @@ -74,7 +90,7 @@ describe('utils/codeless/updateBuildFile', () => { }); describe('updateFunctionsSDKVersion', () => { - it('Should update the package version to 4.1.3 for .NET 6', () => { + it('Should update the package version to 4.5.0 for .NET 8', () => { const xmlBuildFile = { Project: { ItemGroup: [ @@ -82,7 +98,7 @@ describe('utils/codeless/updateBuildFile', () => { PackageReference: { $: { Include: 'Microsoft.NET.Sdk.Functions', - Version: '3.0.13', + Version: '4.1.3', }, }, }, @@ -90,15 +106,15 @@ describe('utils/codeless/updateBuildFile', () => { }, }; - const updatedXmlBuildFile = updateFunctionsSDKVersion(xmlBuildFile, DotnetVersion.net6); + const updatedXmlBuildFile = updateFunctionsSDKVersion(xmlBuildFile, DotnetVersion.net8); expect(updatedXmlBuildFile.Project.ItemGroup[0].PackageReference.$).toMatchObject({ Include: 'Microsoft.NET.Sdk.Functions', - Version: '4.1.3', + Version: '4.5.0', }); }); - it('Should update the package version to 3.0.13 for .NET 3', () => { + it('Should update the package version to 4.1.3 for .NET 6', () => { const xmlBuildFile = { Project: { ItemGroup: [ @@ -106,7 +122,7 @@ describe('utils/codeless/updateBuildFile', () => { PackageReference: { $: { Include: 'Microsoft.NET.Sdk.Functions', - Version: '4.1.3', + Version: '3.0.13', }, }, }, @@ -114,15 +130,15 @@ describe('utils/codeless/updateBuildFile', () => { }, }; - const updatedXmlBuildFile = updateFunctionsSDKVersion(xmlBuildFile, DotnetVersion.net3); + const updatedXmlBuildFile = updateFunctionsSDKVersion(xmlBuildFile, DotnetVersion.net6); expect(updatedXmlBuildFile.Project.ItemGroup[0].PackageReference.$).toMatchObject({ Include: 'Microsoft.NET.Sdk.Functions', - Version: '3.0.13', + Version: '4.1.3', }); }); - it('Should update the package version to 4.5.0 for .NET 8', () => { + it('Should update the package version to 3.0.13 for .NET 3', () => { const xmlBuildFile = { Project: { ItemGroup: [ @@ -130,7 +146,7 @@ describe('utils/codeless/updateBuildFile', () => { PackageReference: { $: { Include: 'Microsoft.NET.Sdk.Functions', - Version: '3.0.13', + Version: '4.1.3', }, }, }, @@ -138,23 +154,40 @@ describe('utils/codeless/updateBuildFile', () => { }, }; - const updatedXmlBuildFile = updateFunctionsSDKVersion(xmlBuildFile, DotnetVersion.net8); + const updatedXmlBuildFile = updateFunctionsSDKVersion(xmlBuildFile, DotnetVersion.net3); expect(updatedXmlBuildFile.Project.ItemGroup[0].PackageReference.$).toMatchObject({ Include: 'Microsoft.NET.Sdk.Functions', - Version: '4.5.0', + Version: '3.0.13', + }); + }); + }); + + describe('addNugetPackagesToBuildFileByName', () => { + it('Should add a named package reference when absent', () => { + const xmlBuildFile = { + Project: { + ItemGroup: [], + }, + }; + + const updatedXmlBuildFile = addNugetPackagesToBuildFileByName(xmlBuildFile, 'Contoso.Package', '1.2.3'); + + expect(updatedXmlBuildFile.Project.ItemGroup[0].PackageReference.$).toEqual({ + Include: 'Contoso.Package', + Version: '1.2.3', }); }); - it('Should update the package version to 4.5.0 for .NET 10', () => { + it('Should not duplicate an existing named package reference', () => { const xmlBuildFile = { Project: { ItemGroup: [ { PackageReference: { $: { - Include: 'Microsoft.NET.Sdk.Functions', - Version: '3.0.13', + Include: 'Contoso.Package', + Version: '1.2.3', }, }, }, @@ -162,12 +195,9 @@ describe('utils/codeless/updateBuildFile', () => { }, }; - const updatedXmlBuildFile = updateFunctionsSDKVersion(xmlBuildFile, DotnetVersion.net10); + const updatedXmlBuildFile = addNugetPackagesToBuildFileByName(xmlBuildFile, 'Contoso.Package', '1.2.3'); - expect(updatedXmlBuildFile.Project.ItemGroup[0].PackageReference.$).toMatchObject({ - Include: 'Microsoft.NET.Sdk.Functions', - Version: '4.5.0', - }); + expect(updatedXmlBuildFile.Project.ItemGroup).toHaveLength(1); }); }); @@ -287,4 +317,60 @@ describe('utils/codeless/updateBuildFile', () => { expect(context.telemetry.properties.allowSettingsToPublish).toBe('false'); }); }); + + describe('getXMLString', () => { + it('Should parse XML into an object', async () => { + await expect(getXMLString('', { explicitArray: false })).resolves.toEqual({ + Project: { + PropertyGroup: '', + }, + }); + }); + + it('Should resolve undefined for invalid XML', async () => { + await expect(getXMLString('', { explicitArray: false })).resolves.toBeUndefined(); + }); + }); + + describe('build file disk helpers', () => { + const context: any = { + telemetry: { + properties: {}, + }, + }; + + it('Should read and parse the single project file', async () => { + vi.mocked(getProjFiles).mockResolvedValue([{ name: 'Functions.csproj' }] as any); + vi.mocked(fs.readFileSync).mockReturnValue('' as any); + + await expect(getDotnetBuildFile(context, 'C:\\project')).resolves.toBe(JSON.stringify({ Project: { ItemGroup: '' } })); + }); + + it('Should write the updated build file to disk', async () => { + vi.mocked(getProjFiles).mockResolvedValue([{ name: 'Functions.csproj' }] as any); + + await writeBuildFileToDisk(context, { Project: { ItemGroup: [] } }, 'C:\\project'); + + expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('Functions.csproj'), expect.stringContaining(' { + vi.mocked(getProjFiles).mockResolvedValue([{ name: 'Functions.csproj' }] as any); + vi.mocked(fs.readFileSync).mockReturnValue('' as any); + + await addNewFileInCSharpProject(context, 'custom\\function.json', 'C:\\project'); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('Functions.csproj'), + expect.stringContaining('custom\\function.json') + ); + }); + + it('Should throw when no project file can be found', async () => { + vi.mocked(getProjFiles).mockResolvedValue([]); + + await expect(getDotnetBuildFile(context, 'C:\\project')).rejects.toThrow('Dotnet project file could not be found'); + await expect(writeBuildFileToDisk(context, { Project: {} }, 'C:\\project')).rejects.toThrow('Dotnet project file could not be found'); + }); + }); }); diff --git a/apps/vs-code-designer/src/app/utils/codeless/updateBuildFile.ts b/apps/vs-code-designer/src/app/utils/codeless/updateBuildFile.ts index 4ae9db78ba9..e02a1de5420 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/updateBuildFile.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/updateBuildFile.ts @@ -140,7 +140,6 @@ export function updateFunctionsSDKVersion(xmlBuildFile: Record, dot case DotnetVersion.net6: packageVersion = '4.1.3'; break; - case DotnetVersion.net10: case DotnetVersion.net8: packageVersion = '4.5.0'; break; diff --git a/apps/vs-code-designer/src/app/utils/customCodeUtils.ts b/apps/vs-code-designer/src/app/utils/customCodeUtils.ts index a13d25ed15d..47caefd5891 100644 --- a/apps/vs-code-designer/src/app/utils/customCodeUtils.ts +++ b/apps/vs-code-designer/src/app/utils/customCodeUtils.ts @@ -152,7 +152,7 @@ export async function getCustomCodeFunctionsProjectMetadata(folderPath: string): } ext.outputChannel.appendLog( - `The csproj file in ${folderPath} does not match the expected format for a .NET 8, .NET 10, or .NET Framework custom code functions project.` + `The csproj file in ${folderPath} does not match the expected format for a .NET 8 or .NET Framework custom code functions project.` ); resolve(undefined); }); @@ -160,10 +160,6 @@ export async function getCustomCodeFunctionsProjectMetadata(folderPath: string): } function getCustomCodeTargetFramework(csprojContent: string): TargetFramework | undefined { - if (isCustomCodeNet10Csproj(csprojContent)) { - return TargetFramework.Net10; - } - if (isCustomCodeNet8Csproj(csprojContent)) { return TargetFramework.Net8; } @@ -179,7 +175,7 @@ function usesLogicAppFolderToPublish(targetFramework: TargetFramework): boolean return targetFramework !== TargetFramework.NetFx; } -function isCustomCodeNetCoreCsproj(csprojContent: string, targetFramework: TargetFramework.Net8 | TargetFramework.Net10): boolean { +function isCustomCodeNetCoreCsproj(csprojContent: string, targetFramework: TargetFramework): boolean { return ( csprojContent.includes(`${targetFramework}`) && csprojContent.includes('Microsoft.Azure.Workflows.Webjobs.Sdk') && @@ -187,10 +183,6 @@ function isCustomCodeNetCoreCsproj(csprojContent: string, targetFramework: Targe ); } -function isCustomCodeNet10Csproj(csprojContent: string): boolean { - return isCustomCodeNetCoreCsproj(csprojContent, TargetFramework.Net10); -} - function isCustomCodeNet8Csproj(csprojContent: string): boolean { return isCustomCodeNetCoreCsproj(csprojContent, TargetFramework.Net8); } diff --git a/apps/vs-code-designer/src/app/utils/debug.ts b/apps/vs-code-designer/src/app/utils/debug.ts index b6bd6ee9784..f816c5762b2 100644 --- a/apps/vs-code-designer/src/app/utils/debug.ts +++ b/apps/vs-code-designer/src/app/utils/debug.ts @@ -24,7 +24,7 @@ export function getCustomCodeRuntime(targetFramework: TargetFramework): 'coreclr /** * Determines whether the given project type and target framework use the modern * LogicAppFolderToPublish csproj property (as opposed to the legacy LogicAppFolder). - * Modern .NET frameworks (Net8, Net10, etc.) use LogicAppFolderToPublish for custom code projects. + * Modern .NET frameworks (for example, Net8) use LogicAppFolderToPublish for custom code projects. */ export function usesPublishFolderProperty(projectType: ProjectType, targetFramework: TargetFramework): boolean { return projectType === ProjectType.customCode && targetFramework !== TargetFramework.NetFx; diff --git a/apps/vs-code-designer/src/app/utils/dotnet/__test__/dotnet.test.ts b/apps/vs-code-designer/src/app/utils/dotnet/__test__/dotnet.test.ts index 88efdbcf1f7..eedf65de018 100644 --- a/apps/vs-code-designer/src/app/utils/dotnet/__test__/dotnet.test.ts +++ b/apps/vs-code-designer/src/app/utils/dotnet/__test__/dotnet.test.ts @@ -1,88 +1,199 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { FuncVersion, ProjectLanguage } from '@microsoft/vscode-extension-logic-apps'; +import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; +import { FuncVersion, Platform, ProjectLanguage } from '@microsoft/vscode-extension-logic-apps'; +import * as fs from 'fs'; +import * as semver from 'semver'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { DotnetVersion } from '../../../../constants'; - -const { mockPathExists, mockGetProjFiles } = vi.hoisted(() => ({ - mockPathExists: vi.fn(), - mockGetProjFiles: vi.fn(), -})); +import { executeCommand } from '../../funcCoreTools/cpUtils'; +import { getGlobalSetting, updateGlobalSetting, updateWorkspaceSetting } from '../../vsCodeConfig/settings'; +import { findFiles, getWorkspaceLogicAppFolders } from '../../workspace'; +import { + getDotnetDebugSubpath, + getLocalDotNetVersionFromBinaries, + getProjFiles, + getTemplateKeyFromFeedEntry, + getTemplateKeyFromProjFile, + ProjectFile, + setDotNetCommand, + tryGetFuncVersion, +} from '../dotnet'; vi.mock('@microsoft/vscode-azext-utils', () => ({ AzExtFsExtra: { - pathExists: mockPathExists, + pathExists: vi.fn(), readFile: vi.fn(), }, })); -vi.mock('../../../extensionVariables', () => ({ - ext: { outputChannel: { appendLog: vi.fn() }, dotNetCliPath: 'dotnet' }, +vi.mock('../../../../extensionVariables', () => ({ + ext: { + outputChannel: { appendLog: vi.fn(), appendLine: vi.fn() }, + dotNetCliPath: 'dotnet', + }, })); -vi.mock('../../../localize', () => ({ - localize: (_key: string, msg: string) => msg, + +vi.mock('../../../../localize', () => ({ + localize: (_key: string, message: string) => message, })); + vi.mock('../../telemetry', () => ({ - runWithDurationTelemetry: (_ctx: any, _name: string, fn: () => any) => fn(), + runWithDurationTelemetry: (_context: unknown, _name: string, callback: () => unknown) => callback(), })); + vi.mock('../../workspace', () => ({ - findFiles: vi.fn().mockResolvedValue([]), + findFiles: vi.fn(), + getWorkspaceLogicAppFolders: vi.fn(), })); + vi.mock('../../funcCoreTools/cpUtils', () => ({ executeCommand: vi.fn(), })); + vi.mock('../../vsCodeConfig/settings', () => ({ getGlobalSetting: vi.fn(), updateGlobalSetting: vi.fn(), updateWorkspaceSetting: vi.fn(), })); + vi.mock('fs', () => ({ - existsSync: vi.fn().mockReturnValue(false), - readdirSync: vi.fn().mockReturnValue([]), + existsSync: vi.fn(), + readdirSync: vi.fn(), chmodSync: vi.fn(), })); + vi.mock('semver', () => ({ clean: vi.fn(), maxSatisfying: vi.fn(), })); -import { getTemplateKeyFromProjFile } from '../dotnet'; - describe('dotnet utilities', () => { - const createContext = () => - ({ - telemetry: { properties: {}, measurements: {} }, - }) as any; + const context = { telemetry: { properties: {}, measurements: {} } } as any; beforeEach(() => { vi.clearAllMocks(); - mockPathExists.mockResolvedValue(false); - mockGetProjFiles.mockResolvedValue([]); + vi.mocked(AzExtFsExtra.pathExists).mockResolvedValue(false); + vi.mocked(AzExtFsExtra.readFile).mockResolvedValue( + 'net8.0' + ); + vi.mocked(findFiles).mockResolvedValue([]); + vi.mocked(getGlobalSetting).mockReturnValue(undefined); + vi.mocked(getWorkspaceLogicAppFolders).mockResolvedValue([]); + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(fs.readdirSync).mockReturnValue([]); + vi.mocked(semver.clean).mockReturnValue('8.0.100'); + vi.mocked(semver.maxSatisfying).mockReturnValue(null); + vi.mocked(executeCommand).mockResolvedValue('8.0.100'); }); - describe('getTemplateKeyFromProjFile', () => { - it('should default to net10.0 for FuncVersion.v4 when no project path exists', async () => { - const result = await getTemplateKeyFromProjFile(createContext(), undefined, FuncVersion.v4, ProjectLanguage.CSharp); - expect(result).toBe(DotnetVersion.net10); - }); - - it('should default to net10.0 for FuncVersion.v4 when project path does not exist', async () => { - mockPathExists.mockResolvedValue(false); - const result = await getTemplateKeyFromProjFile(createContext(), '/nonexistent', FuncVersion.v4, ProjectLanguage.CSharp); - expect(result).toBe(DotnetVersion.net10); - }); - - it('should default to netcoreapp3.1 for FuncVersion.v3', async () => { - const result = await getTemplateKeyFromProjFile(createContext(), undefined, FuncVersion.v3, ProjectLanguage.CSharp); - expect(result).toBe(DotnetVersion.net3); - }); - - it('should default to netcoreapp2.1 for FuncVersion.v2', async () => { - const result = await getTemplateKeyFromProjFile(createContext(), undefined, FuncVersion.v2, ProjectLanguage.CSharp); - expect(result).toBe(DotnetVersion.net2); - }); - - it('should default to net48 for FuncVersion.v1', async () => { - const result = await getTemplateKeyFromProjFile(createContext(), undefined, FuncVersion.v1, ProjectLanguage.CSharp); - expect(result).toBe(DotnetVersion.net48); - }); + it('should get non-extension C# project files', async () => { + vi.mocked(findFiles).mockResolvedValue([{ fsPath: '/project/extensions.csproj' }, { fsPath: '/project/Functions.csproj' }] as any); + + const result = await getProjFiles(context, ProjectLanguage.CSharp, '/project'); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Functions.csproj'); + }); + + it('should cache project file contents', async () => { + const projectFile = new ProjectFile('Functions.csproj', 'C:\\project'); + + await expect(projectFile.getContents()).resolves.toContain('TargetFramework'); + await projectFile.getContents(); + + expect(AzExtFsExtra.readFile).toHaveBeenCalledTimes(1); + }); + + it('should default Functions v4 template key to .NET 8', async () => { + await expect(getTemplateKeyFromProjFile(context, undefined, FuncVersion.v4, ProjectLanguage.CSharp)).resolves.toBe(DotnetVersion.net8); + }); + + it('should default older Functions versions to their expected frameworks', async () => { + await expect(getTemplateKeyFromProjFile(context, undefined, FuncVersion.v3, ProjectLanguage.CSharp)).resolves.toBe(DotnetVersion.net3); + await expect(getTemplateKeyFromProjFile(context, undefined, FuncVersion.v2, ProjectLanguage.CSharp)).resolves.toBe(DotnetVersion.net2); + await expect(getTemplateKeyFromProjFile(context, undefined, FuncVersion.v1, ProjectLanguage.CSharp)).resolves.toBe(DotnetVersion.net48); + }); + + it('should read template key from a single project file', async () => { + vi.mocked(AzExtFsExtra.pathExists).mockResolvedValue(true); + vi.mocked(findFiles).mockResolvedValue([{ fsPath: 'C:\\project\\Functions.csproj' }] as any); + vi.mocked(AzExtFsExtra.readFile).mockResolvedValue( + 'net6.0' + ); + + await expect(getTemplateKeyFromProjFile(context, 'C:\\project', FuncVersion.v4, ProjectLanguage.CSharp)).resolves.toBe('net6.0'); + }); + + it('should append isolated suffix for isolated SDK project files and feed entries', async () => { + vi.mocked(AzExtFsExtra.pathExists).mockResolvedValue(true); + vi.mocked(findFiles).mockResolvedValue([{ fsPath: 'C:\\project\\Functions.csproj' }] as any); + vi.mocked(AzExtFsExtra.readFile).mockResolvedValue( + 'net8.0' + ); + + await expect(getTemplateKeyFromProjFile(context, 'C:\\project', FuncVersion.v4, ProjectLanguage.CSharp)).resolves.toBe( + 'net8.0-isolated' + ); + expect(getTemplateKeyFromFeedEntry({ targetFramework: 'net8.0', sdk: { name: 'Microsoft.Azure.Functions.Worker.Sdk' } } as any)).toBe( + 'net8.0-isolated' + ); + }); + + it('should return undefined when AzureFunctionsVersion is missing', async () => { + const projectFile = new ProjectFile('Functions.csproj', 'C:\\project'); + + await expect(tryGetFuncVersion(projectFile)).resolves.toBeUndefined(); + }); + + it('should build debug subpath with POSIX separators', () => { + expect(getDotnetDebugSubpath('net8.0')).toBe('bin/Debug/net8.0'); + }); + + it('should get local SDK version from dotnet command when no major version is specified', async () => { + await expect(getLocalDotNetVersionFromBinaries()).resolves.toBe('8.0.100'); + }); + + it('should get matching local SDK version from managed binaries', async () => { + vi.mocked(getGlobalSetting).mockReturnValue('C:\\deps'); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readdirSync).mockReturnValue([{ isDirectory: () => true, name: '8.0.100' }] as any); + vi.mocked(semver.maxSatisfying).mockReturnValue('8.0.100'); + + await expect(getLocalDotNetVersionFromBinaries('8')).resolves.toBe('8.0.100'); + }); + + it('should set global dotnet command when managed binaries are not configured', async () => { + await setDotNetCommand(); + + expect(updateGlobalSetting).toHaveBeenCalledWith('dotnetBinaryPath', 'dotnet'); + expect(updateWorkspaceSetting).not.toHaveBeenCalled(); + }); + + it('should set workspace terminal and omnisharp paths when managed binaries exist', async () => { + vi.mocked(getGlobalSetting).mockReturnValue('C:\\deps'); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(getWorkspaceLogicAppFolders).mockResolvedValue(['C:\\workspace\\LogicApp']); + const restorePlatform = mockPlatform(Platform.windows); + + await setDotNetCommand(); + + expect(updateWorkspaceSetting).toHaveBeenCalledWith( + 'integrated.env.windows', + expect.objectContaining({ PATH: expect.any(String) }), + 'C:\\workspace\\LogicApp', + 'terminal' + ); + expect(updateWorkspaceSetting).toHaveBeenCalledWith( + 'dotNetCliPaths', + [expect.stringContaining('DotNetSDK')], + 'C:\\workspace\\LogicApp', + 'omnisharp' + ); + restorePlatform(); }); }); + +function mockPlatform(platform: NodeJS.Platform): () => void { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: platform, configurable: true }); + return () => Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); +} diff --git a/apps/vs-code-designer/src/app/utils/dotnet/__test__/executeDotnetTemplateCommand.test.ts b/apps/vs-code-designer/src/app/utils/dotnet/__test__/executeDotnetTemplateCommand.test.ts index 3e05c2cc528..5d41b96f782 100644 --- a/apps/vs-code-designer/src/app/utils/dotnet/__test__/executeDotnetTemplateCommand.test.ts +++ b/apps/vs-code-designer/src/app/utils/dotnet/__test__/executeDotnetTemplateCommand.test.ts @@ -69,7 +69,6 @@ function createActionContext(): IActionContext { // Static import - uses the mocked modules defined above import { getFramework, - getJsonCliFramework, getDotnetTemplateDir, getDotnetItemTemplatePath, getDotnetProjectTemplatePath, @@ -96,18 +95,7 @@ describe('executeDotnetTemplateCommand', () => { // allowing each test to independently verify version detection logic. describe('getFramework', () => { - it('should pick .NET 10 when available (highest priority)', async () => { - const ctx = createActionContext(); - - mockExecuteCommand - .mockResolvedValueOnce('10.0.100\n') // --version - .mockResolvedValueOnce('10.0.100 [/usr/share/dotnet/sdk]\n8.0.100 [/usr/share/dotnet/sdk]\n'); // --list-sdks - - const result = await getFramework(ctx, '/workspace', true); - expect(result).toBe('net10.0'); - }); - - it('should pick .NET 8 when .NET 10 is not available', async () => { + it('should pick .NET 8 when available (highest priority)', async () => { const ctx = createActionContext(); mockExecuteCommand @@ -151,15 +139,15 @@ describe('executeDotnetTemplateCommand', () => { expect(result).toBe('netcoreapp2.0'); }); - it('should pick .NET 8 ahead of .NET 9 when .NET 10 is unavailable', async () => { + it('should pick .NET 9 as lower priority than 8', async () => { const ctx = createActionContext(); mockExecuteCommand - .mockResolvedValueOnce('8.0.100\n') // --version - .mockResolvedValueOnce('9.0.100 [/usr/share/dotnet/sdk]\n8.0.100 [/usr/share/dotnet/sdk]\n'); // --list-sdks + .mockResolvedValueOnce('9.0.100\n') // --version + .mockResolvedValueOnce('9.0.100 [/usr/share/dotnet/sdk]\n'); // --list-sdks const result = await getFramework(ctx, '/workspace', true); - expect(result).toBe('net8.0'); + expect(result).toBe('net9.0'); }); it('should prefer GA over preview versions', async () => { @@ -208,20 +196,6 @@ describe('executeDotnetTemplateCommand', () => { expect(result).toBe('net8.0'); }); - it('should use binaries with .NET 10 when useBinariesDependencies returns true', async () => { - const ctx = createActionContext(); - - mockUseBinariesDependencies.mockResolvedValue(true); - mockGetLocalDotNetVersionFromBinaries.mockResolvedValue('10.0.100\n'); - mockExecuteCommand - .mockResolvedValueOnce('') // --version - .mockResolvedValueOnce(''); // --list-sdks - - const result = await getFramework(ctx, '/workspace', true); - expect(mockGetLocalDotNetVersionFromBinaries).toHaveBeenCalled(); - expect(result).toBe('net10.0'); - }); - it('should not use binaries when useBinariesDependencies returns false', async () => { const ctx = createActionContext(); @@ -235,47 +209,6 @@ describe('executeDotnetTemplateCommand', () => { expect(result).toBe('net8.0'); }); - it('should detect .NET 10 when version sources lack trailing newlines', async () => { - const ctx = createActionContext(); - - // Simulate outputs without trailing newlines — prior to the delimiter fix, - // these would concatenate into "2.0.10010.0.100 [path]" hiding .NET 10 - mockUseBinariesDependencies.mockResolvedValue(true); - mockGetLocalDotNetVersionFromBinaries.mockResolvedValue('2.0.100'); - mockExecuteCommand - .mockResolvedValueOnce('') // --version - .mockResolvedValueOnce('10.0.100 [/usr/share/dotnet/sdk]'); // --list-sdks (no trailing newline) - - const result = await getFramework(ctx, '/workspace', true); - expect(result).toBe('net10.0'); - }); - - it('should detect correct version when all sources lack trailing newlines', async () => { - const ctx = createActionContext(); - - mockUseBinariesDependencies.mockResolvedValue(true); - mockGetLocalDotNetVersionFromBinaries.mockResolvedValue('6.0.400'); - mockExecuteCommand - .mockResolvedValueOnce('8.0.100') // --version (no newline) - .mockResolvedValueOnce('8.0.100 [/sdk]'); // --list-sdks (no newline) - - const result = await getFramework(ctx, '/workspace', true); - expect(result).toBe('net8.0'); - }); - - it('should not create false match from concatenated version strings', async () => { - const ctx = createActionContext(); - - // "8.0.100" + "6.0.400" without delimiter could form "8.0.1006.0.400" - // which should NOT accidentally match .NET 10 - mockExecuteCommand - .mockResolvedValueOnce('8.0.100') // --version (no newline) - .mockResolvedValueOnce('6.0.400 [/sdk]'); // --list-sdks - - const result = await getFramework(ctx, '/workspace', true); - expect(result).toBe('net8.0'); - }); - it('should handle executeCommand failures gracefully', async () => { const ctx = createActionContext(); @@ -304,36 +237,6 @@ describe('executeDotnetTemplateCommand', () => { }); }); - describe('getJsonCliFramework', () => { - it('should return net8.0 as-is', () => { - expect(getJsonCliFramework('net8.0')).toBe('net8.0'); - }); - - it('should return net6.0 as-is', () => { - expect(getJsonCliFramework('net6.0')).toBe('net6.0'); - }); - - it('should return netcoreapp3.0 as-is', () => { - expect(getJsonCliFramework('netcoreapp3.0')).toBe('netcoreapp3.0'); - }); - - it('should return netcoreapp2.0 as-is', () => { - expect(getJsonCliFramework('netcoreapp2.0')).toBe('netcoreapp2.0'); - }); - - it('should fall back to net8.0 for net10.0', () => { - expect(getJsonCliFramework('net10.0')).toBe('net8.0'); - }); - - it('should fall back to net8.0 for net9.0', () => { - expect(getJsonCliFramework('net9.0')).toBe('net8.0'); - }); - - it('should fall back to net8.0 for unknown frameworks', () => { - expect(getJsonCliFramework('net99.0')).toBe('net8.0'); - }); - }); - describe('getDotnetTemplateDir', () => { it('should return correct directory path', () => { const result = getDotnetTemplateDir('~4', 'myTemplateKey'); diff --git a/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts b/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts index 11070e98513..c763de7e4ff 100644 --- a/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts +++ b/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts @@ -131,7 +131,7 @@ export async function getTemplateKeyFromProjFile( switch (version) { case FuncVersion.v4: { - targetFramework = DotnetVersion.net10; + targetFramework = DotnetVersion.net8; break; } case FuncVersion.v3: { diff --git a/apps/vs-code-designer/src/app/utils/dotnet/executeDotnetTemplateCommand.ts b/apps/vs-code-designer/src/app/utils/dotnet/executeDotnetTemplateCommand.ts index 9a45866bc87..26325545ad6 100644 --- a/apps/vs-code-designer/src/app/utils/dotnet/executeDotnetTemplateCommand.ts +++ b/apps/vs-code-designer/src/app/utils/dotnet/executeDotnetTemplateCommand.ts @@ -55,15 +55,14 @@ export async function executeDotnetTemplateCommand( /** * Maps a detected .NET framework version to the corresponding dotnetJsonCli asset folder. - * The JsonCli DLLs are framework-agnostic and forward-compatible, so newer frameworks - * (e.g. net10.0) can reuse the net8.0 binaries. + * The JsonCli DLLs are framework-agnostic and forward-compatible, so newer frameworks can reuse the net8.0 binaries. */ export function getJsonCliFramework(framework: string): string { const supportedJsonCliFrameworks = ['net8.0', 'net6.0', 'netcoreapp3.0', 'netcoreapp2.0']; if (supportedJsonCliFrameworks.includes(framework)) { return framework; } - // Fall back to net8.0 for newer frameworks (e.g. net10.0) + // Fall back to net8.0 for newer frameworks. return 'net8.0'; } @@ -115,7 +114,7 @@ export async function getFramework(context: IActionContext, workingDirectory: st const versions = versionSources.join('\n'); // Prioritize "LTS", then "Current", then "Preview" - const netVersions: string[] = ['10', '8', '6', '3', '2', '9']; + const netVersions: string[] = ['8', '6', '3', '2', '9']; const semVersions: SemVer[] = netVersions.map((v) => semVerCoerce(v) as SemVer); diff --git a/apps/vs-code-designer/src/app/utils/functionProjectFiles.ts b/apps/vs-code-designer/src/app/utils/functionProjectFiles.ts index d7ea0c29b1f..9d1e61dbc33 100644 --- a/apps/vs-code-designer/src/app/utils/functionProjectFiles.ts +++ b/apps/vs-code-designer/src/app/utils/functionProjectFiles.ts @@ -13,7 +13,6 @@ import * as path from 'path'; export const csTemplateFileNames: Record = { [TargetFramework.NetFx]: 'FunctionsFileNetFx', [TargetFramework.Net8]: 'FunctionsFileNet8', - [TargetFramework.Net10]: 'FunctionsFileNet10', [ProjectType.rulesEngine]: 'RulesFunctionsFile', }; @@ -23,7 +22,6 @@ export const csTemplateFileNames: Record = { export const csprojTemplateFileNames: Record = { [TargetFramework.NetFx]: 'FunctionsProjNetFx', [TargetFramework.Net8]: 'FunctionsProjNet8', - [TargetFramework.Net10]: 'FunctionsProjNet10', [ProjectType.rulesEngine]: 'RulesFunctionsProj', }; @@ -69,32 +67,6 @@ export async function createCsFile( await fs.writeFile(csFilePath, csFileContent); } -/** - * Creates the Program.cs file for .NET 10 isolated worker model. - * Only generates for .NET 10 custom code projects (not rules engine). - * @param assetsPath - Base path to the assets directory. - * @param functionFolderPath - The path to the functions folder. - * @param namespace - The name of the namespace. - * @param projectType - The workspace project type. - * @param targetFramework - The target framework. - */ -export async function createProgramFile( - assetsPath: string, - functionFolderPath: string, - namespace: string, - projectType: ProjectType, - targetFramework: TargetFramework -): Promise { - if (targetFramework !== TargetFramework.Net10 || projectType === ProjectType.rulesEngine) { - return; - } - - const templatePath = path.join(assetsPath, 'FunctionProjectTemplate', 'ProgramFileNet10'); - const templateContent = await fs.readFile(templatePath, 'utf-8'); - const content = templateContent.replace(/<%= namespace %>/g, namespace); - await fs.writeFile(path.join(functionFolderPath, 'Program.cs'), content); -} - /** * Creates the ContosoPurchase.cs rules file for rules engine projects. * @param assetsPath - Base path to the assets directory. diff --git a/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsFileNet10 b/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsFileNet10 deleted file mode 100644 index 34030996929..00000000000 --- a/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsFileNet10 +++ /dev/null @@ -1,80 +0,0 @@ -//------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -//------------------------------------------------------------ - -namespace <%= namespace %> -{ - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using Microsoft.Azure.Functions.Extensions.Workflows; - using Microsoft.Azure.Functions.Worker; - using Microsoft.Extensions.Logging; - - /// - /// Represents the <%= methodName %> flow invoked function. - /// - public class <%= methodName %> - { - private readonly ILogger<<%= methodName %>> logger; - - public <%= methodName %>(ILoggerFactory loggerFactory) - { - logger = loggerFactory.CreateLogger<<%= methodName %>>(); - } - - /// - /// Executes the logic app workflow. - /// - /// The zip code. - /// The temperature scale (e.g., Celsius or Fahrenheit). - [Function("<%= methodName %>")] - public Task Run([WorkflowActionTrigger] int zipCode, string temperatureScale) - { - this.logger.LogInformation("Starting <%= methodName %> with Zip Code: " + zipCode + " and Scale: " + temperatureScale); - - // Generate random temperature within a range based on the temperature scale - Random rnd = new Random(); - var currentTemp = temperatureScale == "Celsius" ? rnd.Next(1, 30) : rnd.Next(40, 90); - var lowTemp = currentTemp - 10; - var highTemp = currentTemp + 10; - - // Create a Weather object with the temperature information - var weather = new Weather() - { - ZipCode = zipCode, - CurrentWeather = $"The current weather is {currentTemp} {temperatureScale}", - DayLow = $"The low for the day is {lowTemp} {temperatureScale}", - DayHigh = $"The high for the day is {highTemp} {temperatureScale}" - }; - - return Task.FromResult(weather); - } - - /// - /// Represents the weather information for <%= methodName %>. - /// - public class Weather - { - /// - /// Gets or sets the zip code. - /// - public int ZipCode { get; set; } - - /// - /// Gets or sets the current weather. - /// - public string CurrentWeather { get; set; } - - /// - /// Gets or sets the low temperature for the day. - /// - public string DayLow { get; set; } - - /// - /// Gets or sets the high temperature for the day. - /// - public string DayHigh { get; set; } - } - } -} diff --git a/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsProjNet10 b/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsProjNet10 deleted file mode 100644 index d94404e6379..00000000000 --- a/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsProjNet10 +++ /dev/null @@ -1,43 +0,0 @@ - - - net10.0 - v4 - Library - AnyCPU - $(MSBuildProjectDirectory)\..\LogicApp - Always - false - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsProjNet8 b/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsProjNet8 index 737dc5ee526..4599b0aad42 100644 --- a/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsProjNet8 +++ b/apps/vs-code-designer/src/assets/FunctionProjectTemplate/FunctionsProjNet8 @@ -9,7 +9,7 @@ Always false - + @@ -17,8 +17,8 @@ - + - \ No newline at end of file + diff --git a/apps/vs-code-designer/src/assets/FunctionProjectTemplate/ProgramFileNet10 b/apps/vs-code-designer/src/assets/FunctionProjectTemplate/ProgramFileNet10 deleted file mode 100644 index 04defbd9288..00000000000 --- a/apps/vs-code-designer/src/assets/FunctionProjectTemplate/ProgramFileNet10 +++ /dev/null @@ -1,25 +0,0 @@ -// ----------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// ----------------------------------------------------------- - -namespace <%= namespace %> -{ - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - - internal static class Program - { - private static void Main(string[] args) - { - var host = new HostBuilder() - .ConfigureFunctionsWebApplication() - .ConfigureServices(services => - { - services.AddApplicationInsightsTelemetryWorkerService(); - }) - .Build(); - - host.Run(); - } - } -} diff --git a/apps/vs-code-designer/src/assets/backupTemplates/dotnet/~4/net10.0/item.nupkg b/apps/vs-code-designer/src/assets/backupTemplates/dotnet/~4/net10.0/item.nupkg deleted file mode 100644 index 1e68594c817..00000000000 Binary files a/apps/vs-code-designer/src/assets/backupTemplates/dotnet/~4/net10.0/item.nupkg and /dev/null differ diff --git a/apps/vs-code-designer/src/assets/backupTemplates/dotnet/~4/net10.0/project.nupkg b/apps/vs-code-designer/src/assets/backupTemplates/dotnet/~4/net10.0/project.nupkg deleted file mode 100644 index ea569b5ade9..00000000000 Binary files a/apps/vs-code-designer/src/assets/backupTemplates/dotnet/~4/net10.0/project.nupkg and /dev/null differ diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index e01c52feeb0..4302cc18f1f 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -346,7 +346,6 @@ export const DependencyDefaultPath = { export type DependencyDefaultPath = (typeof DependencyDefaultPath)[keyof typeof DependencyDefaultPath]; // .NET export const DotnetVersion = { - net10: 'net10.0', net8: 'net8.0', net6: 'net6.0', net3: 'netcoreapp3.1', diff --git a/apps/vs-code-designer/src/test/ui/designerActions.test.ts b/apps/vs-code-designer/src/test/ui/designerActions.test.ts index c898f7b4ee1..87b450aa49f 100644 --- a/apps/vs-code-designer/src/test/ui/designerActions.test.ts +++ b/apps/vs-code-designer/src/test/ui/designerActions.test.ts @@ -2200,6 +2200,62 @@ async function waitForRuntimeReady(driver: WebDriver, timeoutMs = 90_000): Promi return false; } +async function waitForWorkflowRuntimeReady(driver: WebDriver, workflowName: string, timeoutMs = 30_000): Promise { + const t0 = Date.now(); + const deadline = t0 + timeoutMs; + const encodedWorkflowName = encodeURIComponent(workflowName); + + while (Date.now() < deadline) { + try { + const ready = await driver.executeScript( + ` + const workflowName = arguments[0]; + const requestJson = (url) => { + try { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + xhr.timeout = 3000; + xhr.send(); + if (xhr.status !== 200) { + return { ok: false, status: xhr.status }; + } + try { + JSON.parse(xhr.responseText || '{}'); + } catch { + return { ok: false, status: xhr.status }; + } + return { ok: true, status: xhr.status }; + } catch (e) { + return { ok: false, status: 0 }; + } + }; + + const host = requestJson('http://localhost:7071/admin/host/status'); + if (!host.ok) { + return false; + } + + const runs = requestJson('http://localhost:7071/runtime/webhooks/workflow/api/management/workflows/' + workflowName + '/runs?api-version=2019-10-01-edge-preview'); + return runs.ok; + `, + encodedWorkflowName + ); + + if (ready) { + console.log(`[debug] Workflow runtime management endpoint ready for "${workflowName}" (${Date.now() - t0}ms)`); + return true; + } + } catch { + /* ignore */ + } + + await sleep(3000); + } + + console.log(`[debug] Timeout waiting for workflow runtime readiness for "${workflowName}" after ${timeoutMs}ms`); + return false; +} + /** * Open the overview page by right-clicking on workflow.json in the Explorer * and selecting "Overview" from the context menu. @@ -2489,6 +2545,8 @@ async function clickRefresh(driver: WebDriver): Promise { async function getLatestRunStatus(driver: WebDriver): Promise { try { return await driver.executeScript(` + var loading = document.querySelector('.ms-Shimmer, .ms-Shimmer-container, [class*="shimmer"], [class*="Shimmer"]'); + if (loading) return ''; var rows = document.querySelectorAll('[role="row"], .ms-DetailsRow, tr'); for (var i = 0; i < rows.length; i++) { var text = rows[i].textContent || ''; @@ -2514,7 +2572,7 @@ async function getLatestRunStatus(driver: WebDriver): Promise { async function waitForRunStatusInList( driver: WebDriver, targetStatus: string, - timeoutMs = 90_000 + timeoutMs = 180_000 ): Promise<{ found: boolean; lastStatus: string }> { const t0 = Date.now(); const deadline = t0 + timeoutMs; @@ -2540,7 +2598,7 @@ async function waitForRunStatusInList( } // Refresh every 3 seconds to get updated status - if (Date.now() - t0 > (refreshCount + 1) * 3000) { + if (Date.now() - t0 > (refreshCount + 1) * 5000) { await clickRefresh(driver); refreshCount++; } @@ -2865,6 +2923,7 @@ describe('Designer Actions Tests', function () { const runtimeReady = await waitForRuntimeReady(driver); await captureScreenshot(driver, 'test1-step8-after-debug-start'); assert.ok(runtimeReady, 'Functions runtime should start and become ready'); + await waitForWorkflowRuntimeReady(driver, entry.wfName); // Assertion 9: Open overview page via right-click on workflow.json // First, close all editors (including the designer webview) so that @@ -3189,6 +3248,7 @@ describe('Designer Actions Tests', function () { const runtimeReady = await waitForRuntimeReady(driver); await captureScreenshot(driver, 'test2-step7-after-debug-start'); assert.ok(runtimeReady, 'Functions runtime should start and become ready'); + await waitForWorkflowRuntimeReady(driver, entry.wfName); // Extra stabilization for custom code: the func host needs time to // initialize the custom code worker process and register the workflow's diff --git a/apps/vs-code-designer/src/test/ui/helpers.ts b/apps/vs-code-designer/src/test/ui/helpers.ts index b319a35a1ed..4f46a3b7999 100644 --- a/apps/vs-code-designer/src/test/ui/helpers.ts +++ b/apps/vs-code-designer/src/test/ui/helpers.ts @@ -91,6 +91,52 @@ export async function dismissNotifications(driver: WebDriver): Promise { } } +/** Dismiss the Azure Functions project prompt without mutating the legacy project. */ +export async function dismissAzureFunctionsProjectPrompt(driver: WebDriver): Promise { + const promptText = 'Detected an Azure Functions Project'; + const actionText = "Don't warn again"; + + try { + const toasts = await driver.findElements(By.css('.notification-toast, .notification-list-item')); + for (const toast of toasts) { + let text = ''; + try { + text = await toast.getText(); + } catch { + continue; + } + + if (!text.includes(promptText) || !text.includes('Initialize for optimal use with VS Code')) { + continue; + } + + const actions = await toast.findElements(By.css('.monaco-button, .monaco-text-button, .action-label, button')); + for (const action of actions) { + const label = await action.getText().catch(() => ''); + const title = label || (await action.getAttribute('title').catch(() => '')); + if (title.includes(actionText)) { + await action.click(); + await sleep(500); + console.log('[dismissAzureFunctionsProjectPrompt] Clicked "Don\'t warn again"'); + return true; + } + } + + const closeButtons = await toast.findElements(By.css('.codicon-close, [aria-label="Close"]')); + if (closeButtons.length > 0) { + await closeButtons[0].click(); + await sleep(500); + console.log('[dismissAzureFunctionsProjectPrompt] Closed Azure Functions project prompt'); + return true; + } + } + } catch { + // Prompt is absent or already dismissed. + } + + return false; +} + /** * Dismiss any VS Code modal dialog (auth sign-in, workspace trust, etc.). * diff --git a/apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts b/apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts index 3b3131adb31..09bbfb3e7fd 100644 --- a/apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts +++ b/apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts @@ -44,6 +44,7 @@ import { import { startDebugging, waitForRuntimeReady, + waitForWorkflowRuntimeReady, openOverviewPage, switchToOverviewWebview, clickRunTrigger, @@ -211,6 +212,7 @@ describe('Inline JavaScript Tests', function () { workbench = new Workbench(); await startDebugging(workbench, driver); assert.ok(await waitForRuntimeReady(driver), 'Runtime should start'); + await waitForWorkflowRuntimeReady(driver, entry.wfName); try { await new EditorView().closeAllEditors(); await sleep(1000); diff --git a/apps/vs-code-designer/src/test/ui/runHelpers.ts b/apps/vs-code-designer/src/test/ui/runHelpers.ts index c681961248e..b3fb1cdb63e 100644 --- a/apps/vs-code-designer/src/test/ui/runHelpers.ts +++ b/apps/vs-code-designer/src/test/ui/runHelpers.ts @@ -163,6 +163,62 @@ export async function waitForRuntimeReady(driver: WebDriver, timeoutMs = 90_000) return false; } +export async function waitForWorkflowRuntimeReady(driver: WebDriver, workflowName: string, timeoutMs = 30_000): Promise { + const t0 = Date.now(); + const deadline = t0 + timeoutMs; + const encodedWorkflowName = encodeURIComponent(workflowName); + + while (Date.now() < deadline) { + try { + const ready = await driver.executeScript( + ` + const workflowName = arguments[0]; + const requestJson = (url) => { + try { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + xhr.timeout = 3000; + xhr.send(); + if (xhr.status !== 200) { + return { ok: false, status: xhr.status }; + } + try { + JSON.parse(xhr.responseText || '{}'); + } catch { + return { ok: false, status: xhr.status }; + } + return { ok: true, status: xhr.status }; + } catch (e) { + return { ok: false, status: 0 }; + } + }; + + const host = requestJson('http://localhost:7071/admin/host/status'); + if (!host.ok) { + return false; + } + + const runs = requestJson('http://localhost:7071/runtime/webhooks/workflow/api/management/workflows/' + workflowName + '/runs?api-version=2019-10-01-edge-preview'); + return runs.ok; + `, + encodedWorkflowName + ); + + if (ready) { + console.log(`[debug] Workflow runtime management endpoint ready for "${workflowName}" (${Date.now() - t0}ms)`); + return true; + } + } catch { + /* ignore */ + } + + await sleep(3000); + } + + console.log(`[debug] Timeout waiting for workflow runtime readiness for "${workflowName}" after ${timeoutMs}ms`); + return false; +} + // =========================================================================== // Overview helpers // =========================================================================== @@ -436,6 +492,8 @@ export async function clickRefresh(driver: WebDriver): Promise { export async function getLatestRunStatus(driver: WebDriver): Promise { try { return await driver.executeScript(` + var loading = document.querySelector('.ms-Shimmer, .ms-Shimmer-container, [class*="shimmer"], [class*="Shimmer"]'); + if (loading) return ''; var rows = document.querySelectorAll('[role="row"], .ms-DetailsRow, tr'); for (var i = 0; i < rows.length; i++) { var text = rows[i].textContent || ''; @@ -458,7 +516,7 @@ export async function getLatestRunStatus(driver: WebDriver): Promise { export async function waitForRunStatusInList( driver: WebDriver, targetStatus: string, - timeoutMs = 90_000 + timeoutMs = 180_000 ): Promise<{ found: boolean; lastStatus: string }> { const t0 = Date.now(); const deadline = t0 + timeoutMs; @@ -482,7 +540,7 @@ export async function waitForRunStatusInList( return { found: false, lastStatus: status }; } - if (Date.now() - t0 > (refreshCount + 1) * 3000) { + if (Date.now() - t0 > (refreshCount + 1) * 5000) { await clickRefresh(driver); refreshCount++; } diff --git a/apps/vs-code-designer/src/test/ui/statelessVariables.test.ts b/apps/vs-code-designer/src/test/ui/statelessVariables.test.ts index dbbff38b201..e69177cda62 100644 --- a/apps/vs-code-designer/src/test/ui/statelessVariables.test.ts +++ b/apps/vs-code-designer/src/test/ui/statelessVariables.test.ts @@ -38,6 +38,7 @@ import { import { startDebugging, waitForRuntimeReady, + waitForWorkflowRuntimeReady, openOverviewPage, switchToOverviewWebview, clickRunTrigger, @@ -341,6 +342,7 @@ describe('Stateless Variable Tests', function () { workbench = new Workbench(); await startDebugging(workbench, driver); assert.ok(await waitForRuntimeReady(driver), 'Runtime should start'); + await waitForWorkflowRuntimeReady(driver, entry.wfName); try { await new EditorView().closeAllEditors(); await sleep(1000); diff --git a/apps/vs-code-designer/src/test/ui/workspaceConversionCreate.test.ts b/apps/vs-code-designer/src/test/ui/workspaceConversionCreate.test.ts index 2b50dd61402..17136d96b7a 100644 --- a/apps/vs-code-designer/src/test/ui/workspaceConversionCreate.test.ts +++ b/apps/vs-code-designer/src/test/ui/workspaceConversionCreate.test.ts @@ -23,7 +23,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as assert from 'assert'; import { Workbench, WebView, type WebDriver, VSBrowser, ModalDialog, By, Key } from 'vscode-extension-tester'; -import { sleep, captureScreenshot, dismissNotifications, openFolderInSession } from './helpers'; +import { sleep, captureScreenshot, dismissNotifications, openFolderInSession, dismissAzureFunctionsProjectPrompt } from './helpers'; const TEST_TIMEOUT = 180_000; const EXTENSION_BUNDLE_ID = 'Microsoft.Azure.Functions.ExtensionBundle.Workflows'; @@ -297,6 +297,7 @@ async function dismissOuterNotificationsAndReturnToWebview(driver: WebDriver, we await webview.switchBack(); await driver.switchTo().defaultContent(); await dismissQuickInputIfVisible(driver); + await dismissAzureFunctionsProjectPrompt(driver); await dismissNotifications(driver); await driver .executeScript( diff --git a/apps/vs-code-designer/vitest.config.ts b/apps/vs-code-designer/vitest.config.ts index eaf9666226b..42dd6382d32 100644 --- a/apps/vs-code-designer/vitest.config.ts +++ b/apps/vs-code-designer/vitest.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from 'vitest/config'; import packageJson from './package.json'; import path from 'path'; +const coverageInclude = process.env.VSCODE_DESIGNER_COVERAGE_INCLUDE?.split(',').filter(Boolean); + export default defineConfig({ plugins: [], resolve: { @@ -15,7 +17,12 @@ export default defineConfig({ environment: 'node', include: ['src/**/*.test.ts', 'src/**/*.test.tsx'], setupFiles: ['test-setup.ts'], - coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura', 'lcov'] }, + coverage: { + enabled: process.env.VITEST_COVERAGE !== 'false', + provider: 'istanbul', + include: coverageInclude?.length ? coverageInclude : ['src/**/*'], + reporter: ['html', 'cobertura', 'lcov'], + }, restoreMocks: true, // Exclude E2E tests that use Mocha instead of Vitest exclude: [ diff --git a/apps/vs-code-react/src/app/createWorkspace/steps/__test__/dotNetFrameworkStep.test.tsx b/apps/vs-code-react/src/app/createWorkspace/steps/__test__/dotNetFrameworkStep.test.tsx index bffddd26cef..a23ad46ec40 100644 --- a/apps/vs-code-react/src/app/createWorkspace/steps/__test__/dotNetFrameworkStep.test.tsx +++ b/apps/vs-code-react/src/app/createWorkspace/steps/__test__/dotNetFrameworkStep.test.tsx @@ -1,10 +1,11 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Platform, ProjectType } from '@microsoft/vscode-extension-logic-apps'; +import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createWorkspaceSlice, type CreateWorkspaceState } from '../../../../state/createWorkspaceSlice'; -import { ProjectType } from '@microsoft/vscode-extension-logic-apps'; +import { DotNetFrameworkStep } from '../dotNetFrameworkStep'; vi.mock('../../createWorkspaceStyles', () => ({ useCreateWorkspaceStyles: () => @@ -16,163 +17,131 @@ vi.mock('../../createWorkspaceStyles', () => ({ ), })); -import { DotNetFrameworkStep } from '../dotNetFrameworkStep'; +describe('DotNetFrameworkStep', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); -const createTestStore = (overrides: Partial = {}) => { - const defaultState: CreateWorkspaceState = { - currentStep: 1, - packagePath: { fsPath: '', path: '' }, - workspaceProjectPath: { fsPath: '', path: '' }, - workspaceName: '', - logicAppType: ProjectType.customCode, - functionNamespace: 'TestNamespace', - functionName: 'TestFunction', - functionFolderName: 'TestFolder', - workflowType: '', - workflowName: '', - targetFramework: '', - logicAppName: 'TestLogicApp', - projectType: '', - openBehavior: '', - isLoading: false, - isComplete: false, - workspaceFileJson: '', - logicAppsWithoutCustomCode: undefined, - flowType: 'createWorkspace', - pathValidationResults: {}, - packageValidationResults: {}, - workspaceExistenceResults: {}, - isValidatingWorkspace: false, - isValidatingPackage: false, - separator: '/', - platform: null, - isDevContainerProject: false, - ...overrides, - }; + it('should render .NET 8 for custom code on non-Windows platforms', () => { + renderWithStore({ logicAppType: ProjectType.customCode, platform: Platform.mac }); - return configureStore({ - reducer: { - createWorkspace: createWorkspaceSlice.reducer, - }, - preloadedState: { - createWorkspace: defaultState, - }, + fireEvent.click(screen.getByRole('combobox')); + + expect(screen.getByText('.NET 8')).toBeInTheDocument(); + expect(screen.queryByText('.NET Framework')).not.toBeInTheDocument(); + expect(screen.queryByText('.NET 10')).not.toBeInTheDocument(); }); -}; -const renderWithStore = (overrides: Partial = {}) => { - const store = createTestStore(overrides); - return { - store, - ...render( - - - - ), - }; -}; + it('should render .NET Framework before .NET 8 for custom code on Windows', () => { + renderWithStore({ logicAppType: ProjectType.customCode, platform: Platform.windows }); -describe('DotNetFrameworkStep', () => { - beforeEach(() => { - vi.clearAllMocks(); + fireEvent.click(screen.getByRole('combobox')); + + expect(screen.getByText('.NET Framework')).toBeInTheDocument(); + expect(screen.getByText('.NET 8')).toBeInTheDocument(); + expect(screen.queryByText('.NET 10')).not.toBeInTheDocument(); }); - describe('rendering for customCode project type', () => { - it('should render the dotnet version dropdown', () => { - renderWithStore({ logicAppType: ProjectType.customCode }); - expect(screen.getByRole('combobox')).toBeInTheDocument(); - }); + it('should display the selected .NET 8 label and description', () => { + renderWithStore({ logicAppType: ProjectType.customCode, targetFramework: 'net8' }); - it('should render .NET 8 and .NET 10 options on non-Windows', () => { - renderWithStore({ logicAppType: ProjectType.customCode, platform: null }); - const combobox = screen.getByRole('combobox'); - fireEvent.click(combobox); - expect(screen.getByText('.NET 8')).toBeInTheDocument(); - expect(screen.getByText('.NET 10')).toBeInTheDocument(); - }); + expect(screen.getByRole('combobox')).toHaveTextContent('.NET 8'); + expect(screen.getByText(/latest \.NET 8/i)).toBeInTheDocument(); + }); - it('should also render .NET Framework option on Windows', () => { - renderWithStore({ logicAppType: ProjectType.customCode, platform: 'win32' as any }); - const combobox = screen.getByRole('combobox'); - fireEvent.click(combobox); - expect(screen.getByText('.NET Framework')).toBeInTheDocument(); - expect(screen.getByText('.NET 8')).toBeInTheDocument(); - expect(screen.getByText('.NET 10')).toBeInTheDocument(); - }); + it('should dispatch the selected .NET 8 framework', () => { + const { store } = renderWithStore({ logicAppType: ProjectType.customCode, targetFramework: '' }); - it('should not render .NET Framework option on non-Windows', () => { - renderWithStore({ logicAppType: ProjectType.customCode, platform: 'darwin' as any }); - const combobox = screen.getByRole('combobox'); - fireEvent.click(combobox); - expect(screen.queryByText('.NET Framework')).not.toBeInTheDocument(); - }); + fireEvent.click(screen.getByRole('combobox')); + fireEvent.click(screen.getByText('.NET 8')); + + expect(store.getState().createWorkspace.targetFramework).toBe('net8'); }); - describe('selected framework display', () => { - it('should show .NET 10 label when net10.0 is selected', () => { - renderWithStore({ - logicAppType: ProjectType.customCode, - targetFramework: 'net10.0', - }); - const combobox = screen.getByRole('combobox'); - expect(combobox).toHaveTextContent('.NET 10'); - }); + it('should render nothing for codeless logic app projects', () => { + const { container } = renderWithStore({ logicAppType: ProjectType.logicApp }); - it('should show .NET 8 label when net8 is selected', () => { - renderWithStore({ - logicAppType: ProjectType.customCode, - targetFramework: 'net8', - }); - const combobox = screen.getByRole('combobox'); - expect(combobox).toHaveTextContent('.NET 8'); - }); + expect(container).toBeEmptyDOMElement(); + }); - it('should show description text when a framework is selected', () => { - renderWithStore({ - logicAppType: ProjectType.customCode, - targetFramework: 'net10.0', - }); - // Description text should appear below the dropdown - expect(screen.getByText(/modern development and performance/)).toBeInTheDocument(); + it('should render rules engine function fields without the framework dropdown', () => { + renderWithStore({ + logicAppType: ProjectType.rulesEngine, + functionFolderName: 'RulesFolder', + functionNamespace: 'Contoso.Rules', + functionName: 'EvaluateRule', }); + + expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); + expect(screen.getAllByRole('textbox')).toHaveLength(3); }); - describe('non-customCode project types', () => { - it('should return null for logicApp project type', () => { - const { container } = renderWithStore({ logicAppType: ProjectType.logicApp }); - expect(container.querySelector('[role="combobox"]')).toBeNull(); + it('should validate duplicate function folder names in the workspace file', async () => { + renderWithStore({ + logicAppType: ProjectType.customCode, + functionFolderName: 'ExistingFunctions', + workspaceFileJson: { folders: [{ name: 'ExistingFunctions' }] } as any, }); + + expect(await screen.findByText(/already exists/i)).toBeInTheDocument(); }); - describe('rules engine project type', () => { - it('should render function configuration fields for rulesEngine', () => { - renderWithStore({ - logicAppType: ProjectType.rulesEngine, - functionFolderName: 'RulesFolder', - functionNamespace: 'Rules.Namespace', - functionName: 'EvalRule', - }); - // Rules engine renders inputs but no dotnet version dropdown - expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); - expect(screen.getAllByRole('textbox').length).toBeGreaterThan(0); + it('should validate function folder names matching the logic app name', async () => { + renderWithStore({ + logicAppType: ProjectType.customCode, + logicAppName: 'SalesLogicApp', + functionFolderName: 'saleslogicapp', }); + + expect(await screen.findByText(/same as the logic app/i)).toBeInTheDocument(); }); +}); - describe('framework selection dispatch', () => { - it('should dispatch setTargetFramework when an option is selected', () => { - const { store } = renderWithStore({ +function renderWithStore(overrides: Partial = {}) { + const store = configureStore({ + reducer: { + createWorkspace: createWorkspaceSlice.reducer, + }, + preloadedState: { + createWorkspace: { + currentStep: 1, + packagePath: { fsPath: '', path: '' }, + workspaceProjectPath: { fsPath: '', path: '' }, + workspaceName: '', logicAppType: ProjectType.customCode, + functionNamespace: 'Contoso.Functions', + functionName: 'ProcessOrder', + functionFolderName: 'Functions', + workflowType: '', + workflowName: '', targetFramework: '', - }); - - const combobox = screen.getByRole('combobox'); - fireEvent.click(combobox); - - const net10Option = screen.getByText('.NET 10'); - fireEvent.click(net10Option); - - const state = store.getState().createWorkspace; - expect(state.targetFramework).toBe('net10.0'); - }); + logicAppName: 'SalesLogicApp', + projectType: '', + openBehavior: '', + isLoading: false, + isComplete: false, + workspaceFileJson: '', + logicAppsWithoutCustomCode: undefined, + flowType: 'createWorkspace', + pathValidationResults: {}, + packageValidationResults: {}, + workspaceExistenceResults: {}, + isValidatingWorkspace: false, + isValidatingPackage: false, + separator: '/', + platform: Platform.mac, + isDevContainerProject: false, + ...overrides, + }, + }, }); -}); + + return { + store, + ...render( + + + + ), + }; +} diff --git a/apps/vs-code-react/src/app/createWorkspace/steps/__test__/reviewCreateStep.test.tsx b/apps/vs-code-react/src/app/createWorkspace/steps/__test__/reviewCreateStep.test.tsx index 54aba714964..026c96b1e5f 100644 --- a/apps/vs-code-react/src/app/createWorkspace/steps/__test__/reviewCreateStep.test.tsx +++ b/apps/vs-code-react/src/app/createWorkspace/steps/__test__/reviewCreateStep.test.tsx @@ -186,15 +186,6 @@ describe('ReviewCreateStep', () => { }); expect(screen.getByText('.NET 8')).toBeInTheDocument(); }); - - it('should display dot net framework correctly for net10.0', () => { - renderWithStore({ - flowType: 'createWorkspace', - logicAppType: ProjectType.customCode, - targetFramework: 'net10.0', - }); - expect(screen.getByText('.NET 10')).toBeInTheDocument(); - }); }); describe('rules engine configuration', () => { diff --git a/apps/vs-code-react/src/app/createWorkspace/steps/dotNetFrameworkStep.tsx b/apps/vs-code-react/src/app/createWorkspace/steps/dotNetFrameworkStep.tsx index 0e3b0a61f46..d0ea59b90f8 100644 --- a/apps/vs-code-react/src/app/createWorkspace/steps/dotNetFrameworkStep.tsx +++ b/apps/vs-code-react/src/app/createWorkspace/steps/dotNetFrameworkStep.tsx @@ -52,11 +52,6 @@ export const DotNetFrameworkStep: React.FC = () => { label: intlText.DOTNET_8, description: intlText.DOTNET_8_DESCRIPTION, }, - { - value: TargetFramework.Net10, - label: intlText.DOTNET_10, - description: intlText.DOTNET_10_DESCRIPTION, - }, ]; const selectedTargetFramework = targetFrameworkOptions.find((option) => option.value === targetFramework); diff --git a/apps/vs-code-react/src/app/createWorkspace/steps/reviewCreateStep.tsx b/apps/vs-code-react/src/app/createWorkspace/steps/reviewCreateStep.tsx index f36662edaae..45fc2ffa028 100644 --- a/apps/vs-code-react/src/app/createWorkspace/steps/reviewCreateStep.tsx +++ b/apps/vs-code-react/src/app/createWorkspace/steps/reviewCreateStep.tsx @@ -47,6 +47,7 @@ export const ReviewCreateStep: React.FC = () => { const shouldShowLogicAppSection = flowType === 'createWorkspace' || flowType === 'createLogicApp' || flowType === 'createWorkspaceFromPackage'; const shouldShowWorkflowSection = (flowType === 'createWorkspace' || flowType === 'createLogicApp') && !isUsingExistingLogicApp; + const workspaceBasePath = workspaceProjectPath.fsPath && workspaceName ? `${workspaceProjectPath.fsPath}${separator}${workspaceName}` : ''; const workspaceFilePath = workspaceBasePath ? `${workspaceBasePath}${separator}${workspaceName}.code-workspace` : ''; @@ -57,7 +58,6 @@ export const ReviewCreateStep: React.FC = () => { const frameworkDisplayMap: Record = { [TargetFramework.NetFx]: intlText.DOTNET_FRAMEWORK_OPTION, [TargetFramework.Net8]: intlText.DOTNET_8, - [TargetFramework.Net10]: intlText.DOTNET_10, }; return frameworkDisplayMap[framework] ?? framework; diff --git a/apps/vs-code-react/src/intl/messages.ts b/apps/vs-code-react/src/intl/messages.ts index 99660842ecf..cf3fd93700e 100644 --- a/apps/vs-code-react/src/intl/messages.ts +++ b/apps/vs-code-react/src/intl/messages.ts @@ -360,16 +360,6 @@ export const workspaceMessages = defineMessages({ id: 'q1dxkD', description: '.NET 8 description', }, - DOTNET_10: { - defaultMessage: '.NET 10', - id: 'JsTRX9', - description: '.NET 10 option', - }, - DOTNET_10_DESCRIPTION: { - defaultMessage: 'Use the latest .NET 10 for modern development and performance', - id: 'Q1tyGI', - description: '.NET 10 description', - }, FUNCTION_NAMESPACE: { defaultMessage: 'Function namespace', id: 'mr/BC/', diff --git a/apps/vs-code-react/vitest.config.ts b/apps/vs-code-react/vitest.config.ts index 22dca15a01f..909965e7d34 100644 --- a/apps/vs-code-react/vitest.config.ts +++ b/apps/vs-code-react/vitest.config.ts @@ -9,7 +9,7 @@ export default defineProject({ environment: 'jsdom', setupFiles: ['test-setup.ts'], coverage: { - enabled: true, + enabled: process.env.VITEST_COVERAGE !== 'false', provider: 'istanbul', include: ['src/app/**/*', 'src/state/**/*'], exclude: ['src/intl/**/*'], diff --git a/libs/vscode-extension/src/lib/models/workflow.ts b/libs/vscode-extension/src/lib/models/workflow.ts index ce2acc2399b..0dcca04dcf4 100644 --- a/libs/vscode-extension/src/lib/models/workflow.ts +++ b/libs/vscode-extension/src/lib/models/workflow.ts @@ -123,6 +123,5 @@ export type MismatchBehavior = (typeof MismatchBehavior)[keyof typeof MismatchBe export const TargetFramework = { NetFx: 'net472', Net8: 'net8', - Net10: 'net10.0', } as const; export type TargetFramework = (typeof TargetFramework)[keyof typeof TargetFramework]; diff --git a/turbo.json b/turbo.json index 5b62a455553..e2ebf00f3e6 100644 --- a/turbo.json +++ b/turbo.json @@ -41,6 +41,7 @@ "outputs": ["coverage/**"] }, "test:extension-unit": { + "env": ["VITEST_COVERAGE", "VSCODE_DESIGNER_COVERAGE_INCLUDE"], "outputs": ["coverage/**"] }, "test:iframe-app": {