diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 118254179ec..a6635e98313 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -128,6 +128,8 @@ jobs: **/*Provider.ts **/*Provider.tsx **/providers/** + # VS Code webview communication (uses acquireVsCodeApi runtime global, cannot be unit tested) + **/webviewCommunication.tsx - name: Check coverage on changed files id: coverage-check diff --git a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerBase.test.ts b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerBase.test.ts new file mode 100644 index 00000000000..ab2dad22df9 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerBase.test.ts @@ -0,0 +1,238 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as vscode from 'vscode'; +import { designerVersionSetting, defaultDesignerVersion } from '../../../../../constants'; +import { ext } from '../../../../../extensionVariables'; + +// Mock dependencies before importing the class +vi.mock('../../../../../localize', () => ({ + localize: (_key: string, defaultMsg: string) => defaultMsg, +})); + +vi.mock('../../../../utils/codeless/common', () => ({ + tryGetWebviewPanel: vi.fn(), +})); + +vi.mock('../../../../utils/codeless/getWebViewHTML', () => ({ + getWebViewHTML: vi.fn().mockResolvedValue(''), +})); + +vi.mock('@microsoft/logic-apps-shared', () => ({ + getRecordEntry: vi.fn((obj: any, key: string) => obj?.[key]), + isEmptyString: vi.fn((s: any) => !s || (typeof s === 'string' && s.trim().length === 0)), + resolveConnectionsReferences: vi.fn(() => ({})), +})); + +// Import the actual class after mocks +import { OpenDesignerBase } from '../openDesignerBase'; + +// Concrete subclass to test the abstract class +class TestDesigner extends OpenDesignerBase { + constructor(context?: any) { + super( + context ?? { telemetry: { properties: {}, measurements: {} } }, + 'test-workflow', + 'test-panel', + '2018-11-01', + 'test-key', + false, + true, + false, + '' + ); + } + + async createPanel(): Promise {} + + // Expose protected methods for testing + public testGetDesignerVersion() { + return this.getDesignerVersion(); + } + public async testShowDesignerVersionNotification() { + return this.showDesignerVersionNotification(); + } + public testNormalizeLocation(location: string) { + return this.normalizeLocation(location); + } + public testGetPanelOptions() { + return this.getPanelOptions(); + } + public testGetApiHubServiceDetails(azureDetails: any, localSettings: any) { + return this.getApiHubServiceDetails(azureDetails, localSettings); + } + public testGetInterpolateConnectionData(data: string) { + return this.getInterpolateConnectionData(data); + } + public setTestPanel(panel: any) { + this.panel = panel; + } +} + +describe('OpenDesignerBase', () => { + const mockGetConfiguration = vi.mocked(vscode.workspace.getConfiguration); + const mockShowInformationMessage = vi.mocked(vscode.window.showInformationMessage); + const mockConfig = { + get: vi.fn(), + update: vi.fn().mockResolvedValue(undefined), + }; + let designer: TestDesigner; + + beforeEach(() => { + vi.clearAllMocks(); + mockGetConfiguration.mockReturnValue(mockConfig as any); + designer = new TestDesigner(); + }); + + describe('constructor', () => { + it('should initialize with correct properties', () => { + expect(designer).toBeDefined(); + }); + }); + + describe('getDesignerVersion', () => { + it('should return version from config when set to 2', () => { + mockConfig.get.mockReturnValue(2); + expect(designer.testGetDesignerVersion()).toBe(2); + expect(mockGetConfiguration).toHaveBeenCalledWith(ext.prefix); + expect(mockConfig.get).toHaveBeenCalledWith(designerVersionSetting); + }); + + it('should return version from config when set to 1', () => { + mockConfig.get.mockReturnValue(1); + expect(designer.testGetDesignerVersion()).toBe(1); + }); + + it('should return default version when config is undefined', () => { + mockConfig.get.mockReturnValue(undefined); + expect(designer.testGetDesignerVersion()).toBe(defaultDesignerVersion); + }); + + it('should return default version when config is null', () => { + mockConfig.get.mockReturnValue(null); + expect(designer.testGetDesignerVersion()).toBe(defaultDesignerVersion); + }); + }); + + describe('showDesignerVersionNotification', () => { + it('should show preview available message when version is 1', async () => { + mockConfig.get.mockReturnValue(1); + mockShowInformationMessage.mockResolvedValue(undefined); + designer.setTestPanel({ dispose: vi.fn() }); + + await designer.testShowDesignerVersionNotification(); + + expect(mockShowInformationMessage).toHaveBeenCalledWith('A new Logic Apps experience is available for preview!', 'Enable preview'); + }); + + it('should show previewing message when version is 2', async () => { + mockConfig.get.mockReturnValue(2); + mockShowInformationMessage.mockResolvedValue(undefined); + designer.setTestPanel({ dispose: vi.fn() }); + + await designer.testShowDesignerVersionNotification(); + + expect(mockShowInformationMessage).toHaveBeenCalledWith( + 'You are previewing the new Logic Apps experience.', + 'Go back to previous version' + ); + }); + + it('should update setting to version 2 when Enable preview is clicked', async () => { + mockConfig.get.mockReturnValue(1); + mockShowInformationMessage.mockResolvedValueOnce('Enable preview' as any).mockResolvedValueOnce(undefined); + designer.setTestPanel({ dispose: vi.fn() }); + + await designer.testShowDesignerVersionNotification(); + + expect(mockConfig.update).toHaveBeenCalledWith(designerVersionSetting, 2, expect.anything()); + }); + + it('should update setting to version 1 when Go back is clicked', async () => { + mockConfig.get.mockReturnValue(2); + mockShowInformationMessage.mockResolvedValueOnce('Go back to previous version' as any).mockResolvedValueOnce(undefined); + designer.setTestPanel({ dispose: vi.fn() }); + + await designer.testShowDesignerVersionNotification(); + + expect(mockConfig.update).toHaveBeenCalledWith(designerVersionSetting, 1, expect.anything()); + }); + + it('should not update setting when notification is dismissed', async () => { + mockConfig.get.mockReturnValue(1); + mockShowInformationMessage.mockResolvedValue(undefined); + designer.setTestPanel({ dispose: vi.fn() }); + + await designer.testShowDesignerVersionNotification(); + + expect(mockConfig.update).not.toHaveBeenCalled(); + }); + + it('should dispose panel when Close is clicked after enabling preview', async () => { + mockConfig.get.mockReturnValue(1); + const mockDispose = vi.fn(); + mockShowInformationMessage.mockResolvedValueOnce('Enable preview' as any).mockResolvedValueOnce('Close' as any); + designer.setTestPanel({ dispose: mockDispose }); + + await designer.testShowDesignerVersionNotification(); + + expect(mockDispose).toHaveBeenCalled(); + }); + }); + + describe('normalizeLocation', () => { + it('should lowercase and remove spaces', () => { + expect(designer.testNormalizeLocation('West US')).toBe('westus'); + }); + + it('should handle already normalized location', () => { + expect(designer.testNormalizeLocation('westus')).toBe('westus'); + }); + + it('should return empty string for empty input', () => { + expect(designer.testNormalizeLocation('')).toBe(''); + }); + }); + + describe('getPanelOptions', () => { + it('should return options with scripts enabled and context retained', () => { + const options = designer.testGetPanelOptions(); + expect(options.enableScripts).toBe(true); + expect(options.retainContextWhenHidden).toBe(true); + }); + }); + + describe('getApiHubServiceDetails', () => { + it('should return service details when API hub is enabled', () => { + const azureDetails = { + enabled: true, + subscriptionId: 'sub-123', + location: 'westus', + resourceGroupName: 'rg-test', + tenantId: 'tenant-123', + accessToken: 'token-123', + }; + const result = designer.testGetApiHubServiceDetails(azureDetails, {}); + + expect(result).toBeDefined(); + expect(result.subscriptionId).toBe('sub-123'); + expect(result.apiVersion).toBe('2018-07-01-preview'); + }); + + it('should return undefined when API hub is disabled', () => { + const azureDetails = { enabled: false }; + const result = designer.testGetApiHubServiceDetails(azureDetails, {}); + expect(result).toBeUndefined(); + }); + }); + + describe('getInterpolateConnectionData', () => { + it('should return falsy data as-is', () => { + expect(designer.testGetInterpolateConnectionData('')).toBe(''); + }); + + it('should handle connections data with no managed API connections', () => { + const data = JSON.stringify({ serviceProviderConnections: {} }); + const result = designer.testGetInterpolateConnectionData(data); + expect(JSON.parse(result)).toEqual({ serviceProviderConnections: {} }); + }); + }); +}); diff --git a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerForAzureResource.test.ts b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerForAzureResource.test.ts new file mode 100644 index 00000000000..5fb8519716e --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerForAzureResource.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as vscode from 'vscode'; +import { ext } from '../../../../../extensionVariables'; + +// Mock dependencies before importing the class +vi.mock('../../../../../localize', () => ({ + localize: (_key: string, defaultMsg: string) => defaultMsg, +})); + +vi.mock('../../../../utils/codeless/common', () => ({ + tryGetWebviewPanel: vi.fn(), + cacheWebviewPanel: vi.fn(), + removeWebviewPanelFromCache: vi.fn(), + getStandardAppData: vi.fn(() => ({ definition: {}, kind: 'Stateful' })), + getWorkflowManagementBaseURI: vi.fn(() => 'https://management.azure.com/test'), +})); + +vi.mock('../../../../utils/codeless/getWebViewHTML', () => ({ + getWebViewHTML: vi.fn().mockResolvedValue(''), +})); + +vi.mock('@microsoft/logic-apps-shared', () => ({ + getRecordEntry: vi.fn((obj: any, key: string) => obj?.[key]), + isEmptyString: vi.fn((s: any) => !s || (typeof s === 'string' && s.trim().length === 0)), + resolveConnectionsReferences: vi.fn(() => ({})), +})); + +vi.mock('../../../../utils/codeless/getAuthorizationToken', () => ({ + getAuthorizationTokenFromNode: vi.fn().mockResolvedValue('mock-token'), +})); + +import { OpenDesignerForAzureResource } from '../openDesignerForAzureResource'; + +const createMockNode = (overrides: Record = {}) => ({ + name: 'test-workflow', + workflowFileContent: { definition: {} }, + subscription: { + subscriptionId: 'sub-123', + credentials: { getToken: vi.fn().mockResolvedValue('token') }, + }, + parent: { + parent: { + site: { + location: 'West US', + resourceGroup: 'test-rg', + defaultHostName: 'myapp.azurewebsites.net', + ...overrides, + }, + }, + subscription: { + environment: { resourceManagerEndpointUrl: 'https://management.azure.com' }, + tenantId: 'tenant-123', + }, + }, + getConnectionsData: vi.fn().mockResolvedValue('{}'), + getParametersData: vi.fn().mockResolvedValue({}), + getAppSettings: vi.fn().mockResolvedValue({}), + getArtifacts: vi.fn().mockResolvedValue({ maps: {}, schemas: [] }), + getChildWorkflows: vi.fn().mockResolvedValue({}), +}); + +describe('OpenDesignerForAzureResource', () => { + const mockContext = { telemetry: { properties: {}, measurements: {} } } as any; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should construct with correct workflow name from node', () => { + const mockNode = createMockNode(); + const instance = new OpenDesignerForAzureResource(mockContext, mockNode as any); + expect(instance).toBeDefined(); + }); + + it('should set base URL from node', () => { + const mockNode = createMockNode(); + const instance = new OpenDesignerForAzureResource(mockContext, mockNode as any); + expect(instance).toBeDefined(); + }); + }); + + describe('createPanel', () => { + it('should reveal existing panel if one exists', async () => { + const { tryGetWebviewPanel } = await import('../../../../utils/codeless/common'); + const mockReveal = vi.fn(); + vi.mocked(tryGetWebviewPanel).mockReturnValue({ active: false, reveal: mockReveal } as any); + + const mockNode = createMockNode(); + const instance = new OpenDesignerForAzureResource(mockContext, mockNode as any); + await instance.createPanel(); + + expect(mockReveal).toHaveBeenCalled(); + }); + + it('should create new panel and call showDesignerVersionNotification', async () => { + const { tryGetWebviewPanel, cacheWebviewPanel } = await import('../../../../utils/codeless/common'); + vi.mocked(tryGetWebviewPanel).mockReturnValue(undefined); + + const mockPostMessage = vi.fn(); + const mockPanel = { + webview: { html: '', onDidReceiveMessage: vi.fn(), postMessage: mockPostMessage }, + onDidDispose: vi.fn(), + iconPath: undefined, + }; + vi.mocked(vscode.window as any).createWebviewPanel = vi.fn().mockReturnValue(mockPanel); + ext.context = { extensionPath: '/test', subscriptions: [] } as any; + + const mockShowInfo = vi.mocked(vscode.window.showInformationMessage); + const mockGetConfig = vi.mocked(vscode.workspace.getConfiguration); + mockGetConfig.mockReturnValue({ get: vi.fn().mockReturnValue(1), update: vi.fn() } as any); + mockShowInfo.mockResolvedValue(undefined); + + const mockNode = createMockNode(); + const instance = new OpenDesignerForAzureResource(mockContext, mockNode as any); + await instance.createPanel(); + + expect(cacheWebviewPanel).toHaveBeenCalled(); + // showDesignerVersionNotification was called (shows v1 message) + expect(mockShowInfo).toHaveBeenCalledWith('A new Logic Apps experience is available for preview!', 'Enable preview'); + }); + }); +}); diff --git a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerForLocalProject.test.ts b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerForLocalProject.test.ts new file mode 100644 index 00000000000..21d2cc90192 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/__test__/openDesignerForLocalProject.test.ts @@ -0,0 +1,131 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ext } from '../../../../../extensionVariables'; + +// Mock dependencies before importing the class +vi.mock('../../../../../localize', () => ({ + localize: (_key: string, defaultMsg: string) => defaultMsg, +})); + +vi.mock('../../../../utils/codeless/common', () => ({ + tryGetWebviewPanel: vi.fn(), + cacheWebviewPanel: vi.fn(), + removeWebviewPanelFromCache: vi.fn(), + getStandardAppData: vi.fn(() => ({ definition: {}, kind: 'Stateful' })), + getWorkflowManagementBaseURI: vi.fn(() => 'https://management.azure.com/test'), + getManualWorkflowsInLocalProject: vi.fn().mockResolvedValue({}), + getAzureConnectorDetailsForLocalProject: vi.fn().mockResolvedValue({ enabled: false }), +})); + +vi.mock('../../../../utils/codeless/getWebViewHTML', () => ({ + getWebViewHTML: vi.fn().mockResolvedValue(''), +})); + +vi.mock('@microsoft/logic-apps-shared', () => ({ + getRecordEntry: vi.fn((obj: any, key: string) => obj?.[key]), + isEmptyString: vi.fn((s: any) => !s || (typeof s === 'string' && s.trim().length === 0)), + resolveConnectionsReferences: vi.fn(() => ({})), + HTTP_METHODS: { POST: 'POST', GET: 'GET' }, +})); + +vi.mock('../../../../utils/codeless/connection', () => ({ + getConnectionsFromFile: vi.fn().mockResolvedValue('{}'), + getCustomCodeFromFiles: vi.fn().mockResolvedValue({}), + getLogicAppProjectRoot: vi.fn().mockResolvedValue('/test/project'), + getParametersFromFile: vi.fn().mockResolvedValue({}), + addConnectionData: vi.fn(), + getConnectionsAndSettingsToUpdate: vi.fn(), + saveConnectionReferences: vi.fn(), + getCustomCodeToUpdate: vi.fn(), + saveCustomCodeStandard: vi.fn(), +})); + +vi.mock('../../../../utils/codeless/startDesignTimeApi', () => ({ + startDesignTimeApi: vi.fn(), +})); + +vi.mock('../../../../utils/requestUtils', () => ({ + sendRequest: vi.fn(), +})); + +vi.mock('../../../dataMapper/dataMapper', () => ({ + createNewDataMapCmd: vi.fn(), +})); + +vi.mock('../../../../utils/codeless/parameter', () => ({ + saveWorkflowParameter: vi.fn(), +})); + +vi.mock('../../../../utils/codeless/artifacts', () => ({ + getArtifactsInLocalProject: vi.fn().mockResolvedValue({ maps: {}, schemas: [] }), +})); + +vi.mock('../../../../utils/bundleFeed', () => ({ + getBundleVersionNumber: vi.fn().mockResolvedValue('1.0.0'), +})); + +vi.mock('../../../../utils/appSettings/localSettings', () => ({ + getLocalSettingsJson: vi.fn().mockResolvedValue({ Values: {} }), +})); + +vi.mock('@azure/core-rest-pipeline', () => ({ + createHttpHeaders: vi.fn(), +})); + +vi.mock('../../unitTest/codefulUnitTest/createUnitTest', () => ({ + createUnitTest: vi.fn(), +})); + +vi.mock('../../../../utils/unitTest/codelessUnitTest', () => ({ + saveUnitTestDefinition: vi.fn(), +})); + +vi.mock('../../../../utils/codeless/getAuthorizationToken', () => ({ + getAuthorizationTokenFromNode: vi.fn().mockResolvedValue('mock-token'), +})); + +// Import after mocks +import OpenDesignerForLocalProject from '../openDesignerForLocalProject'; + +describe('OpenDesignerForLocalProject', () => { + const mockContext = { telemetry: { properties: {}, measurements: {} } } as any; + const mockUri = { fsPath: '/test/project/myWorkflow/workflow.json' } as any; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should construct with correct workflow name from file path', () => { + const instance = new OpenDesignerForLocalProject(mockContext, mockUri); + expect(instance).toBeDefined(); + }); + + it('should set isLocal to true', () => { + const instance = new OpenDesignerForLocalProject(mockContext, mockUri); + expect(instance).toBeDefined(); + }); + + it('should handle unit test mode', () => { + const instance = new OpenDesignerForLocalProject(mockContext, mockUri, 'test-unit-test', { assertions: [] }); + expect(instance).toBeDefined(); + }); + + it('should handle run ID parameter', () => { + const instance = new OpenDesignerForLocalProject(mockContext, mockUri, undefined, undefined, 'workflows/wf/runs/run123'); + expect(instance).toBeDefined(); + }); + }); + + describe('createPanel', () => { + it('should return early if existing panel is found', async () => { + const { tryGetWebviewPanel } = await import('../../../../utils/codeless/common'); + const mockPanel = { active: false, reveal: vi.fn() }; + vi.mocked(tryGetWebviewPanel).mockReturnValue(mockPanel as any); + + const instance = new OpenDesignerForLocalProject(mockContext, mockUri); + await instance.createPanel(); + + expect(mockPanel.reveal).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerBase.ts b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerBase.ts index 71861ddd8fb..ca5990dfb2c 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerBase.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerBase.ts @@ -8,8 +8,11 @@ import type { IAzureConnectorsContext } from '../azureConnectorWizard'; import { getRecordEntry, isEmptyString, resolveConnectionsReferences } from '@microsoft/logic-apps-shared'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import type { Artifacts, AzureConnectorDetails, ConnectionsData, FileDetails, Parameter } from '@microsoft/vscode-extension-logic-apps'; -import { azurePublicBaseUrl, workflowManagementBaseURIKey } from '../../../../constants'; +import { azurePublicBaseUrl, workflowManagementBaseURIKey, designerVersionSetting, defaultDesignerVersion } from '../../../../constants'; +import { ext } from '../../../../extensionVariables'; +import { localize } from '../../../../localize'; import type { WebviewPanel, WebviewOptions, WebviewPanelOptions } from 'vscode'; +import { workspace, window, ConfigurationTarget } from 'vscode'; export interface IDesignerOptions { references?: any; @@ -179,4 +182,47 @@ export abstract class OpenDesignerBase { } return location.toLowerCase().replace(/ /g, ''); } + + protected getDesignerVersion(): number { + const config = workspace.getConfiguration(ext.prefix); + return config.get(designerVersionSetting) ?? defaultDesignerVersion; + } + + protected async showDesignerVersionNotification(): Promise { + const currentVersion = this.getDesignerVersion(); + const config = workspace.getConfiguration(ext.prefix); + + if (currentVersion === 1) { + const enablePreview = localize('enablePreview', 'Enable preview'); + const message = localize('previewAvailable', 'A new Logic Apps experience is available for preview!'); + + const selection = await window.showInformationMessage(message, enablePreview); + if (selection === enablePreview) { + await config.update(designerVersionSetting, 2, ConfigurationTarget.Global); + const closeButton = localize('close', 'Close'); + const reopenMessage = localize( + 'closeToApply', + 'Setting updated. Please close and reopen the workflow to apply the new experience.' + ); + const reopenSelection = await window.showInformationMessage(reopenMessage, closeButton); + if (reopenSelection === closeButton) { + this.panel?.dispose(); + } + } + } else { + const goBack = localize('goBack', 'Go back to previous version'); + const message = localize('previewingNew', 'You are previewing the new Logic Apps experience.'); + + const selection = await window.showInformationMessage(message, goBack); + if (selection === goBack) { + await config.update(designerVersionSetting, 1, ConfigurationTarget.Global); + const closeButton = localize('close', 'Close'); + const reopenMessage = localize('closeToApply', 'Setting updated. Please close and reopen the workflow to apply the change.'); + const reopenSelection = await window.showInformationMessage(reopenMessage, closeButton); + if (reopenSelection === closeButton) { + this.panel?.dispose(); + } + } + } + } } diff --git a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForAzureResource.ts b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForAzureResource.ts index dbb4f4e49d2..6642d7608ee 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForAzureResource.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForAzureResource.ts @@ -74,6 +74,9 @@ export class OpenDesignerForAzureResource extends OpenDesignerBase { cacheWebviewPanel(this.panelGroupKey, this.panelName, this.panel); ext.context.subscriptions.push(this.panel); + + // Show notification about designer version + this.showDesignerVersionNotification(); } private async _handleWebviewMsg(msg: any) { @@ -106,6 +109,13 @@ export class OpenDesignerForAzureResource extends OpenDesignerBase { await openUrl('https://github.com/Azure/LogicAppsUX/issues/new?template=bug_report.yml'); break; } + case ExtensionCommand.getDesignerVersion: { + this.sendMsgToWebview({ + command: ExtensionCommand.getDesignerVersion, + data: this.getDesignerVersion(), + }); + break; + } default: break; } @@ -130,6 +140,7 @@ export class OpenDesignerForAzureResource extends OpenDesignerBase { workflowManagementBaseUrl: this.node?.parent?.subscription?.environment?.resourceManagerEndpointUrl, tenantId: this.node?.parent?.subscription?.tenantId, resourceGroupName: this.node?.parent?.parent?.site.resourceGroup, + defaultHostName: this.node?.parent?.parent?.site.defaultHostName, }, workflowName: this.workflowName, standardApp: getStandardAppData(this.workflowName, this.workflow), diff --git a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForLocalProject.ts b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForLocalProject.ts index e656edd48e9..e487ebdac08 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForLocalProject.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/openDesigner/openDesignerForLocalProject.ts @@ -181,6 +181,9 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase { cacheWebviewPanel(this.panelGroupKey, this.panelName, this.panel); ext.context.subscriptions.push(this.panel); + + // Show notification about designer version + this.showDesignerVersionNotification(); } private async _handleWebviewMsg(msg: any) { @@ -312,6 +315,14 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase { break; } + case ExtensionCommand.getDesignerVersion: { + this.sendMsgToWebview({ + command: ExtensionCommand.getDesignerVersion, + data: this.getDesignerVersion(), + }); + break; + } + default: break; } diff --git a/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/__test__/openMonitoringViewForAzureResource.test.ts b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/__test__/openMonitoringViewForAzureResource.test.ts new file mode 100644 index 00000000000..7e482081b7e --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/__test__/openMonitoringViewForAzureResource.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as vscode from 'vscode'; +import { ext } from '../../../../../extensionVariables'; + +// Mock dependencies before importing the class +vi.mock('../../../../../localize', () => ({ + localize: (_key: string, defaultMsg: string) => defaultMsg, +})); + +vi.mock('../../../../utils/codeless/common', () => ({ + tryGetWebviewPanel: vi.fn(), + cacheWebviewPanel: vi.fn(), + removeWebviewPanelFromCache: vi.fn(), + getStandardAppData: vi.fn(() => ({ definition: {}, kind: 'Stateful' })), + getWorkflowManagementBaseURI: vi.fn(() => 'https://management.azure.com/test'), +})); + +vi.mock('../../../../utils/codeless/getWebViewHTML', () => ({ + getWebViewHTML: vi.fn().mockResolvedValue(''), +})); + +vi.mock('@microsoft/logic-apps-shared', () => ({ + getRecordEntry: vi.fn((obj: any, key: string) => obj?.[key]), + isEmptyString: vi.fn((s: any) => !s || (typeof s === 'string' && s.trim().length === 0)), + resolveConnectionsReferences: vi.fn(() => ({})), + getTriggerName: vi.fn(() => 'manual'), + HTTP_METHODS: { POST: 'POST', GET: 'GET' }, +})); + +vi.mock('../../../../utils/codeless/getAuthorizationToken', () => ({ + getAuthorizationTokenFromNode: vi.fn().mockResolvedValue('mock-token'), +})); + +vi.mock('../../../../utils/requestUtils', () => ({ + sendAzureRequest: vi.fn(), + sendRequest: vi.fn(), +})); + +import openMonitoringViewForAzureResource from '../openMonitoringViewForAzureResource'; + +const createMockNode = () => ({ + name: 'test-workflow', + workflowFileContent: { definition: { triggers: { manual: { type: 'Request' } } } }, + subscription: { + subscriptionId: 'sub-123', + credentials: { getToken: vi.fn().mockResolvedValue('token') }, + }, + parent: { + parent: { + site: { + location: 'West US', + resourceGroup: 'test-rg', + defaultHostName: 'myapp.azurewebsites.net', + }, + }, + subscription: { + environment: { resourceManagerEndpointUrl: 'https://management.azure.com' }, + tenantId: 'tenant-123', + }, + }, + getConnectionsData: vi.fn().mockResolvedValue('{}'), + getParametersData: vi.fn().mockResolvedValue({}), + getAppSettings: vi.fn().mockResolvedValue({}), + getArtifacts: vi.fn().mockResolvedValue({ maps: {}, schemas: [] }), + getChildWorkflows: vi.fn().mockResolvedValue({}), +}); + +describe('openMonitoringViewForAzureResource', () => { + const mockContext = { telemetry: { properties: {}, measurements: {} } } as any; + const mockRunId = 'workflows/test-workflow/runs/run-123'; + const mockWorkflowFilePath = '/test/workflow.json'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should construct with correct parameters', () => { + const mockNode = createMockNode(); + const instance = new openMonitoringViewForAzureResource(mockContext, mockRunId, mockWorkflowFilePath, mockNode as any); + expect(instance).toBeDefined(); + }); + }); + + describe('createPanel', () => { + it('should reveal existing panel if one exists', async () => { + const { tryGetWebviewPanel } = await import('../../../../utils/codeless/common'); + const mockReveal = vi.fn(); + vi.mocked(tryGetWebviewPanel).mockReturnValue({ active: false, reveal: mockReveal } as any); + + const mockNode = createMockNode(); + const instance = new openMonitoringViewForAzureResource(mockContext, mockRunId, mockWorkflowFilePath, mockNode as any); + await instance.createPanel(); + + expect(mockReveal).toHaveBeenCalled(); + }); + + it('should create new panel with azureDetails including defaultHostName', async () => { + const { tryGetWebviewPanel, cacheWebviewPanel } = await import('../../../../utils/codeless/common'); + vi.mocked(tryGetWebviewPanel).mockReturnValue(undefined); + + const mockPostMessage = vi.fn(); + const mockPanel = { + webview: { html: '', onDidReceiveMessage: vi.fn(), postMessage: mockPostMessage }, + onDidDispose: vi.fn(), + iconPath: undefined, + }; + vi.mocked(vscode.window as any).createWebviewPanel = vi.fn().mockReturnValue(mockPanel); + ext.context = { extensionPath: '/test', subscriptions: [] } as any; + + const mockNode = createMockNode(); + const instance = new openMonitoringViewForAzureResource(mockContext, mockRunId, mockWorkflowFilePath, mockNode as any); + await instance.createPanel(); + + expect(cacheWebviewPanel).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/__test__/openMonitoringViewForLocal.test.ts b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/__test__/openMonitoringViewForLocal.test.ts new file mode 100644 index 00000000000..9b6e3622dbb --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/__test__/openMonitoringViewForLocal.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ext } from '../../../../../extensionVariables'; + +// Mock dependencies before importing the class +vi.mock('../../../../../localize', () => ({ + localize: (_key: string, defaultMsg: string) => defaultMsg, +})); + +vi.mock('../../../../utils/codeless/common', () => ({ + tryGetWebviewPanel: vi.fn(), + cacheWebviewPanel: vi.fn(), + removeWebviewPanelFromCache: vi.fn(), + getStandardAppData: vi.fn(() => ({ definition: {}, kind: 'Stateful' })), + getWorkflowManagementBaseURI: vi.fn(() => 'https://management.azure.com/test'), + getAzureConnectorDetailsForLocalProject: vi.fn().mockResolvedValue({ enabled: false }), +})); + +vi.mock('../../../../utils/codeless/getWebViewHTML', () => ({ + getWebViewHTML: vi.fn().mockResolvedValue(''), +})); + +vi.mock('@microsoft/logic-apps-shared', () => ({ + getRecordEntry: vi.fn((obj: any, key: string) => obj?.[key]), + isEmptyString: vi.fn((s: any) => !s || (typeof s === 'string' && s.trim().length === 0)), + resolveConnectionsReferences: vi.fn(() => ({})), + getTriggerName: vi.fn(() => 'manual'), + HTTP_METHODS: { POST: 'POST', GET: 'GET' }, +})); + +vi.mock('../../../../utils/codeless/connection', () => ({ + getConnectionsFromFile: vi.fn().mockResolvedValue('{}'), + getCustomCodeFromFiles: vi.fn().mockResolvedValue({}), + getLogicAppProjectRoot: vi.fn().mockResolvedValue('/test/project'), + getParametersFromFile: vi.fn().mockResolvedValue({}), +})); + +vi.mock('../../../../utils/appSettings/localSettings', () => ({ + getLocalSettingsJson: vi.fn().mockResolvedValue({ Values: {} }), +})); + +vi.mock('../../../../utils/requestUtils', () => ({ + sendRequest: vi.fn(), +})); + +vi.mock('../../../../utils/codeless/artifacts', () => ({ + getArtifactsInLocalProject: vi.fn().mockResolvedValue({ maps: {}, schemas: [] }), +})); + +vi.mock('../../../../utils/bundleFeed', () => ({ + getBundleVersionNumber: vi.fn().mockResolvedValue('1.0.0'), +})); + +vi.mock('../../unitTest/codefulUnitTest/createUnitTestFromRun', () => ({ + createUnitTestFromRun: vi.fn(), +})); + +vi.mock('../../../../utils/codeless/getAuthorizationToken', () => ({ + getAuthorizationTokenFromNode: vi.fn().mockResolvedValue('mock-token'), +})); + +import OpenMonitoringViewForLocal from '../openMonitoringViewForLocal'; + +describe('OpenMonitoringViewForLocal', () => { + const mockContext = { telemetry: { properties: {}, measurements: {} } } as any; + const mockRunId = 'workflows/test-workflow/runs/run-123'; + const mockWorkflowFilePath = '/test/project/test-workflow/workflow.json'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should construct with correct parameters', () => { + const instance = new OpenMonitoringViewForLocal(mockContext, mockRunId, mockWorkflowFilePath); + expect(instance).toBeDefined(); + }); + + it('should set isLocal to true', () => { + const instance = new OpenMonitoringViewForLocal(mockContext, mockRunId, mockWorkflowFilePath); + expect(instance).toBeDefined(); + }); + }); + + describe('createPanel', () => { + it('should reveal existing panel if one exists', async () => { + const { tryGetWebviewPanel } = await import('../../../../utils/codeless/common'); + const mockReveal = vi.fn(); + vi.mocked(tryGetWebviewPanel).mockReturnValue({ active: false, reveal: mockReveal } as any); + + const instance = new OpenMonitoringViewForLocal(mockContext, mockRunId, mockWorkflowFilePath); + await instance.createPanel(); + + expect(mockReveal).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForAzureResource.ts b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForAzureResource.ts index d043bb6619e..a79e80cdeee 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForAzureResource.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForAzureResource.ts @@ -75,6 +75,7 @@ export default class openMonitoringViewForAzureResource extends OpenMonitoringVi resourceGroupName: this.node?.parent?.parent?.site.resourceGroup, location: this.normalizeLocation(this.node?.parent?.parent?.site.location), workflowManagementBaseUrl: this.node?.parent?.subscription?.environment?.resourceManagerEndpointUrl, + defaultHostName: this.node?.parent?.parent?.site.defaultHostName, }, artifacts: await this.node.getArtifacts(), }); @@ -140,6 +141,13 @@ export default class openMonitoringViewForAzureResource extends OpenMonitoringVi await openUrl('https://github.com/Azure/LogicAppsUX/issues/new?template=bug_report.yml'); break; } + case ExtensionCommand.getDesignerVersion: { + this.sendMsgToWebview({ + command: ExtensionCommand.getDesignerVersion, + data: this.getDesignerVersion(), + }); + break; + } default: break; } @@ -182,6 +190,7 @@ export default class openMonitoringViewForAzureResource extends OpenMonitoringVi location: this.normalizeLocation(this.node?.parent?.parent?.site.location), workflowManagementBaseUrl: this.node?.parent?.subscription?.environment?.resourceManagerEndpointUrl, tenantId: this.node?.parent?.subscription?.tenantId, + defaultHostName: this.node?.parent?.parent?.site.defaultHostName, }, workflowName: this.workflowName, standardApp: getStandardAppData(this.workflowName, { ...this.workflow, definition: {} }), diff --git a/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForLocal.ts b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForLocal.ts index c204f95ddf1..2f9d7ba6eac 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForLocal.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/openMonitoringView/openMonitoringViewForLocal.ts @@ -155,6 +155,13 @@ export default class OpenMonitoringViewForLocal extends OpenMonitoringViewBase { await openUrl('https://github.com/Azure/LogicAppsUX/issues/new?template=bug_report.yml'); break; } + case ExtensionCommand.getDesignerVersion: { + this.sendMsgToWebview({ + command: ExtensionCommand.getDesignerVersion, + data: this.getDesignerVersion(), + }); + break; + } default: break; } diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index 2308ba5a281..aa6349043d1 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -239,6 +239,7 @@ export const gitCommand = 'git'; // Project settings export const projectLanguageSetting = 'projectLanguage'; export const dataMapperVersionSetting = 'dataMapperVersion'; +export const designerVersionSetting = 'designerVersion'; export const funcVersionSetting = 'projectRuntime'; export const projectSubpathSetting = 'projectSubpath'; export const projectTemplateKeySetting = 'projectTemplateKey'; @@ -308,6 +309,7 @@ export const defaultExtensionBundlePathValue = path.join( extensionBundleId ); export const defaultDataMapperVersion = 2; +export const defaultDesignerVersion = 1; // Fallback Dependency Versions export const DependencyVersion = { diff --git a/apps/vs-code-designer/src/package.json b/apps/vs-code-designer/src/package.json index e3106862b2b..8869d69775d 100644 --- a/apps/vs-code-designer/src/package.json +++ b/apps/vs-code-designer/src/package.json @@ -822,6 +822,13 @@ "enumDescriptions": ["Version 1 (Deprecated)", "Version 2"], "default": 2 }, + "azureLogicAppsStandard.designerVersion": { + "type": "number", + "enum": [1, 2], + "description": "The version of the Logic Apps Designer to use.", + "enumDescriptions": ["Version 1", "Version 2 (Preview)"], + "default": 1 + }, "azureLogicAppsStandard.deploySubpath": { "scope": "resource", "type": "string", diff --git a/apps/vs-code-designer/test-setup.ts b/apps/vs-code-designer/test-setup.ts index d6c1b852ae9..53bbcab1a41 100644 --- a/apps/vs-code-designer/test-setup.ts +++ b/apps/vs-code-designer/test-setup.ts @@ -94,9 +94,18 @@ vi.mock('vscode', () => ({ window: { showInformationMessage: vi.fn(), showErrorMessage: vi.fn(), + showWarningMessage: vi.fn(), + createWebviewPanel: vi.fn(() => ({ + webview: { html: '', onDidReceiveMessage: vi.fn(), postMessage: vi.fn() }, + onDidDispose: vi.fn(), + onDidChangeViewState: vi.fn(), + iconPath: undefined, + })), + withProgress: vi.fn((_opts: any, task: any) => task({ report: vi.fn() })), }, workspace: { workspaceFolders: [], + name: 'test-workspace', updateWorkspaceFolders: vi.fn(), fs: { readFile: vi.fn(), @@ -106,6 +115,7 @@ vi.mock('vscode', () => ({ }, Uri: { file: (p: string) => ({ fsPath: p, toString: () => p }), + parse: (s: string) => ({ fsPath: s, toString: () => s }), }, commands: { executeCommand: vi.fn(), @@ -117,12 +127,31 @@ vi.mock('vscode', () => ({ File: 'file', Directory: 'directory', }, + ConfigurationTarget: { + Global: 1, + Workspace: 2, + WorkspaceFolder: 3, + }, + ViewColumn: { + Active: -1, + Beside: -2, + One: 1, + Two: 2, + }, + ProgressLocation: { + Notification: 15, + SourceControl: 1, + Window: 10, + }, env: { clipboard: { writeText: vi.fn(), }, sessionId: 'test-session-id', appName: 'Visual Studio Code', + uriScheme: 'vscode', + asExternalUri: vi.fn((uri: any) => Promise.resolve(uri)), + openExternal: vi.fn(), }, version: '1.85.0', })); diff --git a/apps/vs-code-react/src/app/designer/__test__/app.test.tsx b/apps/vs-code-react/src/app/designer/__test__/app.test.tsx new file mode 100644 index 00000000000..c7187283139 --- /dev/null +++ b/apps/vs-code-react/src/app/designer/__test__/app.test.tsx @@ -0,0 +1,215 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { configureStore, createSlice } from '@reduxjs/toolkit'; +import { projectSlice } from '../../../state/projectSlice'; + +// Use vi.hoisted to define mock variables that are used in vi.mock factories +const { mockPostMessage } = vi.hoisted(() => { + return { mockPostMessage: vi.fn() }; +}); + +// Mock webviewCommunication to avoid acquireVsCodeApi global +vi.mock('../../../webviewCommunication', async () => { + const React = await import('react'); + return { + VSCodeContext: React.createContext({ postMessage: mockPostMessage }), + }; +}); + +// Mock appV2 to render a simple component +vi.mock('../appV2', () => ({ + DesignerApp: () =>
Designer V2
, +})); + +// Mock all heavy dependencies used by DesignerAppV1 +vi.mock('../servicesHelper', () => ({ + getDesignerServices: vi.fn(() => ({ + connectionService: {}, + connectorService: {}, + operationManifestService: {}, + searchService: {}, + oAuthService: {}, + gatewayService: {}, + tenantService: {}, + workflowService: { getAgentUrl: vi.fn() }, + hostService: {}, + runService: { getRun: vi.fn().mockResolvedValue(null) }, + roleService: {}, + editorService: {}, + apimService: {}, + loggerService: {}, + connectionParameterEditorService: {}, + cognitiveServiceService: {}, + functionService: {}, + })), +})); + +vi.mock('../DesignerCommandBar', () => ({ + DesignerCommandBar: () => null, +})); + +vi.mock('../utilities/runInstance', () => ({ + getRunInstanceMocks: vi.fn(), +})); + +vi.mock('../utilities/workflow', () => ({ + convertConnectionsDataToReferences: vi.fn(() => ({})), +})); + +vi.mock('@microsoft/logic-apps-designer', () => ({ + DesignerProvider: ({ children }: any) =>
{children}
, + BJSWorkflowProvider: ({ children }: any) =>
{children}
, + Designer: () =>
V1
, + getTheme: vi.fn(() => 'light'), + useThemeObserver: vi.fn(), + getReactQueryClient: vi.fn(() => ({ removeQueries: vi.fn() })), + runsQueriesKeys: {}, +})); + +vi.mock('@microsoft/logic-apps-shared', () => ({ + BundleVersionRequirements: { MULTI_VARIABLE: '1.0.0', NESTED_AGENT_LOOPS: '1.0.0' }, + equals: vi.fn(), + isEmptyString: vi.fn(() => true), + isNullOrUndefined: vi.fn(() => true), + isRuntimeUp: vi.fn(), + isVersionSupported: vi.fn(() => false), + Theme: { Dark: 'dark', Light: 'light' }, + InitLoggerService: vi.fn(), +})); + +vi.mock('@microsoft/vscode-extension-logic-apps', () => ({ + ExtensionCommand: { + createFileSystemConnection: 'createFileSystemConnection', + getDesignerVersion: 'getDesignerVersion', + }, +})); + +vi.mock('@tanstack/react-query', () => ({ + useQuery: vi.fn(() => ({ + refetch: vi.fn(), + isError: false, + isFetching: false, + isLoading: false, + isRefetching: false, + data: null, + })), + useQueryClient: vi.fn(() => ({})), +})); + +vi.mock('@fluentui/react-components', () => ({ + Spinner: () =>
Loading...
, +})); + +vi.mock('@microsoft/designer-ui', () => ({ + XLargeText: ({ text }: any) =>
{text}
, +})); + +vi.mock('../appStyles', () => ({ + useAppStyles: vi.fn(() => ({})), +})); + +vi.mock('../../../intl', () => ({ + useIntlMessages: vi.fn(() => ({ SOMETHING_WENT_WRONG: 'Error', LOADING_DESIGNER: 'Loading' })), + commonMessages: {}, +})); + +// Import after mocks +import { DesignerApp } from '../app'; + +const designerInitialState = { + panelMetaData: null, + connectionData: {}, + baseUrl: '/url', + workflowRuntimeBaseUrl: '', + apiVersion: '2018-11-01', + apiHubServiceDetails: { + apiVersion: '2018-07-01-preview', + baseUrl: '/url', + subscriptionId: 'subscriptionId', + resourceGroup: '', + location: '', + tenantId: '', + httpClient: null, + }, + readOnly: false, + isLocal: true, + isMonitoringView: false, + callbackInfo: { value: '', method: '' }, + runId: '', + fileSystemConnections: {}, + iaMapArtifacts: [], + oauthRedirectUrl: '', + hostVersion: '', + isUnitTest: false, + unitTestDefinition: null, +}; + +const createTestStore = (designerVersion?: number) => { + return configureStore({ + reducer: { + project: projectSlice.reducer, + designer: createSlice({ + name: 'designer', + initialState: designerInitialState, + reducers: {}, + }).reducer, + }, + preloadedState: { + project: { + initialized: true, + project: 'designer', + designerVersion, + }, + }, + }); +}; + +describe('DesignerApp', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render null when designerVersion is undefined', () => { + const store = createTestStore(undefined); + const { container } = render( + + + + ); + expect(container.innerHTML).toBe(''); + }); + + it('should send getDesignerVersion message on mount', () => { + const store = createTestStore(undefined); + render( + + + + ); + expect(mockPostMessage).toHaveBeenCalledWith({ command: 'getDesignerVersion' }); + }); + + it('should render DesignerAppV2 when designerVersion is 2', () => { + const store = createTestStore(2); + render( + + + + ); + expect(screen.getByTestId('designer-v2')).toBeDefined(); + }); + + it('should render DesignerAppV1 when designerVersion is 1', () => { + const store = createTestStore(1); + const { container } = render( + + + + ); + // Should not render V2 or be empty + expect(container.innerHTML).not.toBe(''); + expect(screen.queryByTestId('designer-v2')).toBeNull(); + }); +}); diff --git a/apps/vs-code-react/src/app/designer/__test__/servicesHelper.test.ts b/apps/vs-code-react/src/app/designer/__test__/servicesHelper.test.ts new file mode 100644 index 00000000000..3547feb3277 --- /dev/null +++ b/apps/vs-code-react/src/app/designer/__test__/servicesHelper.test.ts @@ -0,0 +1,291 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock all service class constructors +vi.mock('@microsoft/logic-apps-shared', () => ({ + StandardConnectionService: vi.fn().mockImplementation((opts: any) => ({ type: 'connection', opts })), + StandardOperationManifestService: vi.fn().mockImplementation((opts: any) => ({ type: 'operationManifest', opts })), + StandardSearchService: vi.fn().mockImplementation((opts: any) => ({ type: 'search', opts })), + BaseGatewayService: vi.fn().mockImplementation((opts: any) => ({ type: 'gateway', opts })), + StandardRunService: vi.fn().mockImplementation((opts: any) => ({ type: 'run', opts, getRun: vi.fn() })), + StandardArtifactService: vi.fn().mockImplementation((opts: any) => ({ type: 'artifact', opts })), + BaseApiManagementService: vi + .fn() + .mockImplementation((opts: any) => ({ type: 'apim', opts, getOperationSchema: vi.fn(), getOperations: vi.fn() })), + BaseFunctionService: vi.fn().mockImplementation((opts: any) => ({ type: 'function', opts })), + BaseAppServiceService: vi.fn().mockImplementation((opts: any) => ({ + type: 'appService', + opts, + getOperationSchema: vi.fn(), + getOperations: vi.fn(), + })), + BaseTenantService: vi.fn().mockImplementation((opts: any) => ({ type: 'tenant', opts })), + BaseCognitiveServiceService: vi.fn().mockImplementation((opts: any) => ({ type: 'cognitive', opts })), + BaseRoleService: vi.fn().mockImplementation((opts: any) => ({ type: 'role', opts })), + HTTP_METHODS: { POST: 'POST', GET: 'GET' }, + clone: vi.fn((obj: any) => JSON.parse(JSON.stringify(obj))), + isEmptyString: vi.fn((s: any) => !s || (typeof s === 'string' && s.trim().length === 0)), + resolveConnectionsReferences: vi.fn(() => ({})), + InitLoggerService: vi.fn(), +})); + +vi.mock('@microsoft/vscode-extension-logic-apps', () => ({ + ExtensionCommand: { + addConnection: 'addConnection', + showContent: 'showContent', + openRelativeLink: 'openRelativeLink', + createFileSystemConnection: 'createFileSystemConnection', + }, + HttpClient: vi.fn().mockImplementation(() => ({ + get: vi.fn(), + post: vi.fn(), + })), +})); + +vi.mock('../constants', () => ({ + clientSupportedOperations: [], +})); + +vi.mock('../services/oAuth', () => ({ + BaseOAuthService: vi.fn().mockImplementation(() => ({ type: 'oauth' })), +})); + +const { mockFetchAgentUrl } = vi.hoisted(() => { + return { + mockFetchAgentUrl: vi.fn().mockResolvedValue({ agentUrl: 'http://agent', chatUrl: 'http://chat', hostName: 'host' }), + }; +}); + +vi.mock('../services/workflowService', () => ({ + fetchAgentUrl: mockFetchAgentUrl, +})); + +vi.mock('../customEditorService', () => ({ + CustomEditorService: vi.fn().mockImplementation(() => ({ type: 'editor' })), +})); + +vi.mock('../../services/Logger', () => ({ + LoggerService: vi.fn().mockImplementation(() => ({ type: 'logger' })), +})); + +vi.mock('../services/customConnectionParameterEditorService', () => ({ + CustomConnectionParameterEditorService: vi.fn().mockImplementation(() => ({ type: 'connectionParam' })), +})); + +vi.mock('../services/connector', () => ({ + StandardVSCodeConnectorService: vi.fn().mockImplementation(() => ({ type: 'connector' })), +})); + +vi.mock('../../../../package.json', () => ({ + default: { version: '1.0.0' }, +})); + +import { getDesignerServices } from '../servicesHelper'; + +describe('getDesignerServices', () => { + const mockVscode = { postMessage: vi.fn() } as any; + const mockQueryClient = {} as any; + const mockSendMsg = vi.fn(); + const mockSetRunId = vi.fn(); + const mockCreateFSConnection = vi.fn(); + + const defaultArgs = { + baseUrl: 'http://localhost:7071', + workflowRuntimeBaseUrl: 'http://localhost:7071/runtime', + isWorkflowRuntimeRunning: true, + apiVersion: '2018-11-01', + apiHubDetails: { + apiVersion: '2018-07-01-preview', + baseUrl: 'http://hub', + subscriptionId: 'sub-123', + resourceGroup: 'rg-test', + location: 'westus', + tenantId: 'tenant-123', + httpClient: null as any, + }, + isLocal: true, + connectionData: {}, + panelMetadata: { + accessToken: 'mock-token', + panelId: 'panel-1', + workflowDetails: { workflow1: {} }, + workflowName: 'testWorkflow', + localSettings: {}, + standardApp: { stateful: true }, + azureDetails: { + tenantId: 'tenant-123', + clientId: 'client-123', + defaultHostName: 'myapp.azurewebsites.net', + }, + } as any, + oauthRedirectUrl: 'http://redirect', + hostVersion: '1.0.0', + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return an object with all expected service keys', () => { + const services = getDesignerServices( + defaultArgs.baseUrl, + defaultArgs.workflowRuntimeBaseUrl, + defaultArgs.isWorkflowRuntimeRunning, + defaultArgs.apiVersion, + defaultArgs.apiHubDetails, + defaultArgs.isLocal, + defaultArgs.connectionData as any, + defaultArgs.panelMetadata, + mockCreateFSConnection, + mockVscode, + defaultArgs.oauthRedirectUrl, + defaultArgs.hostVersion, + mockQueryClient, + mockSendMsg, + mockSetRunId + ); + + expect(services.connectionService).toBeDefined(); + expect(services.connectorService).toBeDefined(); + expect(services.operationManifestService).toBeDefined(); + expect(services.searchService).toBeDefined(); + expect(services.oAuthService).toBeDefined(); + expect(services.gatewayService).toBeDefined(); + expect(services.tenantService).toBeDefined(); + expect(services.workflowService).toBeDefined(); + expect(services.hostService).toBeDefined(); + expect(services.runService).toBeDefined(); + expect(services.roleService).toBeDefined(); + expect(services.editorService).toBeDefined(); + expect(services.apimService).toBeDefined(); + expect(services.loggerService).toBeDefined(); + expect(services.connectionParameterEditorService).toBeDefined(); + expect(services.cognitiveServiceService).toBeDefined(); + expect(services.functionService).toBeDefined(); + }); + + it('should define workflowService.getAgentUrl that calls fetchAgentUrl with defaultHostName', async () => { + const services = getDesignerServices( + defaultArgs.baseUrl, + defaultArgs.workflowRuntimeBaseUrl, + defaultArgs.isWorkflowRuntimeRunning, + defaultArgs.apiVersion, + defaultArgs.apiHubDetails, + defaultArgs.isLocal, + defaultArgs.connectionData as any, + defaultArgs.panelMetadata, + mockCreateFSConnection, + mockVscode, + defaultArgs.oauthRedirectUrl, + defaultArgs.hostVersion, + mockQueryClient, + mockSendMsg, + mockSetRunId + ); + + expect(services.workflowService.getAgentUrl).toBeDefined(); + await services.workflowService.getAgentUrl!(); + + expect(mockFetchAgentUrl).toHaveBeenCalledWith( + 'testWorkflow', + 'http://localhost:7071/runtime', + expect.anything(), + 'client-123', + 'tenant-123', + true, + 'myapp.azurewebsites.net' + ); + }); + + it('should pass undefined defaultHostName when panelMetadata has no azureDetails', async () => { + const panelMetadataNoAzure = { + ...defaultArgs.panelMetadata, + azureDetails: { tenantId: '', clientId: '' }, + }; + + const services = getDesignerServices( + defaultArgs.baseUrl, + defaultArgs.workflowRuntimeBaseUrl, + defaultArgs.isWorkflowRuntimeRunning, + defaultArgs.apiVersion, + defaultArgs.apiHubDetails, + defaultArgs.isLocal, + defaultArgs.connectionData as any, + panelMetadataNoAzure as any, + mockCreateFSConnection, + mockVscode, + defaultArgs.oauthRedirectUrl, + defaultArgs.hostVersion, + mockQueryClient, + mockSendMsg, + mockSetRunId + ); + + await services.workflowService.getAgentUrl!(); + + expect(mockFetchAgentUrl).toHaveBeenCalledWith( + 'testWorkflow', + 'http://localhost:7071/runtime', + expect.anything(), + '', + '', + true, + undefined + ); + }); + + it('should use baseUrl as fallback when workflowRuntimeBaseUrl is empty', async () => { + const services = getDesignerServices( + defaultArgs.baseUrl, + '', + defaultArgs.isWorkflowRuntimeRunning, + defaultArgs.apiVersion, + defaultArgs.apiHubDetails, + defaultArgs.isLocal, + defaultArgs.connectionData as any, + defaultArgs.panelMetadata, + mockCreateFSConnection, + mockVscode, + defaultArgs.oauthRedirectUrl, + defaultArgs.hostVersion, + mockQueryClient, + mockSendMsg, + mockSetRunId + ); + + await services.workflowService.getAgentUrl!(); + + expect(mockFetchAgentUrl).toHaveBeenCalledWith( + 'testWorkflow', + 'http://localhost:7071', + expect.anything(), + 'client-123', + 'tenant-123', + true, + 'myapp.azurewebsites.net' + ); + }); + + it('should define workflowService with getCallbackUrl and getAppIdentity', () => { + const services = getDesignerServices( + defaultArgs.baseUrl, + defaultArgs.workflowRuntimeBaseUrl, + defaultArgs.isWorkflowRuntimeRunning, + defaultArgs.apiVersion, + defaultArgs.apiHubDetails, + defaultArgs.isLocal, + defaultArgs.connectionData as any, + defaultArgs.panelMetadata, + mockCreateFSConnection, + mockVscode, + defaultArgs.oauthRedirectUrl, + defaultArgs.hostVersion, + mockQueryClient, + mockSendMsg, + mockSetRunId + ); + + expect(services.workflowService.getCallbackUrl).toBeDefined(); + expect(services.workflowService.getAppIdentity).toBeDefined(); + expect(services.workflowService.isExplicitAuthRequiredForManagedIdentity).toBeDefined(); + }); +}); diff --git a/apps/vs-code-react/src/app/designer/app.tsx b/apps/vs-code-react/src/app/designer/app.tsx index 5ff0cecc9f1..2a4bc868444 100644 --- a/apps/vs-code-react/src/app/designer/app.tsx +++ b/apps/vs-code-react/src/app/designer/app.tsx @@ -34,8 +34,9 @@ import { XLargeText } from '@microsoft/designer-ui'; import { Spinner } from '@fluentui/react-components'; import { useAppStyles } from './appStyles'; import { useIntlMessages, commonMessages } from '../../intl'; +import { DesignerApp as DesignerAppV2 } from './appV2'; -export const DesignerApp = () => { +const DesignerAppV1 = () => { const vscode = useContext(VSCodeContext); const dispatch: AppDispatch = useDispatch(); const vscodeState = useSelector((state: RootState) => state.designer); @@ -296,3 +297,25 @@ export const DesignerApp = () => { ); }; + +export const DesignerApp = () => { + const vscode = useContext(VSCodeContext); + const designerVersion = useSelector((state: RootState) => state.project.designerVersion); + + const sendMsgToVsix = useCallback( + (msg: MessageToVsix) => { + vscode.postMessage(msg); + }, + [vscode] + ); + + useEffect(() => { + sendMsgToVsix({ command: ExtensionCommand.getDesignerVersion }); + }, [sendMsgToVsix]); + + if (designerVersion === undefined) { + return null; + } + + return designerVersion === 2 ? : ; +}; diff --git a/apps/vs-code-react/src/app/designer/services/__test__/workflowService.test.ts b/apps/vs-code-react/src/app/designer/services/__test__/workflowService.test.ts new file mode 100644 index 00000000000..a704f3637c7 --- /dev/null +++ b/apps/vs-code-react/src/app/designer/services/__test__/workflowService.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock the dependencies before importing the module +vi.mock('@microsoft/logic-apps-designer', () => ({ + getReactQueryClient: vi.fn(() => ({ + fetchQuery: vi.fn((_key: any, queryFn: any) => queryFn()), + })), +})); + +vi.mock('@microsoft/logic-apps-shared', () => ({ + LogEntryLevel: { Error: 'Error' }, + LoggerService: vi.fn(() => ({ + log: vi.fn(), + })), +})); + +// Import the actual function after mocks +import { fetchAgentUrl } from '../workflowService'; + +describe('workflowService', () => { + const mockHttpClient = { + post: vi.fn().mockResolvedValue({ key: 'test-key' }), + get: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockHttpClient.post.mockResolvedValue({ key: 'test-key' }); + }); + + describe('fetchAgentUrl', () => { + it('should return empty URLs when workflowName is empty', async () => { + const result = await fetchAgentUrl('', 'http://localhost:7071', mockHttpClient as any, 'client-id', 'tenant-id'); + expect(result).toEqual({ agentUrl: '', chatUrl: '', hostName: '' }); + }); + + it('should return empty URLs when neither runtimeUrl nor defaultHostName is provided', async () => { + const result = await fetchAgentUrl('myWorkflow', '', mockHttpClient as any, 'client-id', 'tenant-id', false, undefined); + expect(result).toEqual({ agentUrl: '', chatUrl: '', hostName: '' }); + }); + + it('should construct HTTP URLs for local workflows', async () => { + const result = await fetchAgentUrl( + 'myWorkflow', + 'http://localhost:7071/runtime/webhooks/workflow/api/management', + mockHttpClient as any, + 'client-id', + 'tenant-id' + ); + + expect(result.agentUrl).toBe('http://localhost:7071/api/Agents/myWorkflow'); + expect(result.chatUrl).toBe('http://localhost:7071/api/agentsChat/myWorkflow/IFrame'); + expect(result.hostName).toBe('http://localhost:7071/runtime/webhooks/workflow/api/management'); + }); + + it('should construct HTTPS URLs when defaultHostName is provided', async () => { + const result = await fetchAgentUrl( + 'myWorkflow', + 'http://localhost:7071', + mockHttpClient as any, + 'client-id', + 'tenant-id', + false, + 'myapp.azurewebsites.net' + ); + + expect(result.agentUrl).toBe('https://myapp.azurewebsites.net/api/Agents/myWorkflow'); + expect(result.chatUrl).toBe('https://myapp.azurewebsites.net/api/agentsChat/myWorkflow/IFrame'); + expect(result.hostName).toBe('myapp.azurewebsites.net'); + }); + + it('should handle defaultHostName that already includes https://', async () => { + const result = await fetchAgentUrl( + 'myWorkflow', + 'http://localhost:7071', + mockHttpClient as any, + 'client-id', + 'tenant-id', + false, + 'https://myapp.azurewebsites.net' + ); + + expect(result.agentUrl).toBe('https://myapp.azurewebsites.net/api/Agents/myWorkflow'); + expect(result.hostName).toBe('https://myapp.azurewebsites.net'); + }); + + it('should include auth key in queryParams when available', async () => { + mockHttpClient.post.mockResolvedValue({ key: 'my-auth-key' }); + + const result = await fetchAgentUrl('myWorkflow', 'http://localhost:7071', mockHttpClient as any, 'client-id', 'tenant-id'); + + expect(result.queryParams).toEqual({ apiKey: 'my-auth-key' }); + }); + + it('should handle errors gracefully and return fallback URLs', async () => { + mockHttpClient.post.mockRejectedValue(new Error('Network error')); + + const result = await fetchAgentUrl('myWorkflow', 'http://localhost:7071', mockHttpClient as any, 'client-id', 'tenant-id'); + + expect(result.agentUrl).toBe(''); + expect(result.hostName).toBe('http://localhost:7071'); + }); + + it('should return defaultHostName in error fallback when provided', async () => { + mockHttpClient.post.mockRejectedValue(new Error('Network error')); + + const result = await fetchAgentUrl( + 'myWorkflow', + 'http://localhost:7071', + mockHttpClient as any, + 'client-id', + 'tenant-id', + false, + 'myapp.azurewebsites.net' + ); + + expect(result.hostName).toBe('myapp.azurewebsites.net'); + }); + }); +}); diff --git a/apps/vs-code-react/src/app/designer/services/workflowService.ts b/apps/vs-code-react/src/app/designer/services/workflowService.ts index 452ed5f3038..5efdf45a596 100644 --- a/apps/vs-code-react/src/app/designer/services/workflowService.ts +++ b/apps/vs-code-react/src/app/designer/services/workflowService.ts @@ -8,46 +8,62 @@ export const fetchAgentUrl = ( httpClient: IHttpClient, clientId: string, tenantId: string, - isWorkflowRuntimeRunning?: boolean + isWorkflowRuntimeRunning?: boolean, + defaultHostName?: string ): Promise => { const queryClient = getReactQueryClient(); - return queryClient.fetchQuery(['agentUrl', workflowName, runtimeUrl, isWorkflowRuntimeRunning], async (): Promise => { - if (!workflowName || !runtimeUrl) { - return { agentUrl: '', chatUrl: '', hostName: '' }; - } + return queryClient.fetchQuery( + ['agentUrl', workflowName, runtimeUrl, isWorkflowRuntimeRunning, defaultHostName], + async (): Promise => { + if (!workflowName || (!runtimeUrl && !defaultHostName)) { + return { agentUrl: '', chatUrl: '', hostName: '' }; + } - try { - const baseUrl = `${new URL(runtimeUrl).origin}`; - const agentBaseUrl = baseUrl.startsWith('http://') ? baseUrl : `http://${baseUrl}`; - const agentUrl = `${agentBaseUrl}/api/Agents/${workflowName}`; - const chatUrl = `${agentBaseUrl}/api/agentsChat/${workflowName}/IFrame`; - let queryParams: AgentQueryParams | undefined = undefined; + try { + let agentBaseUrl: string; + let hostName: string; - // Get A2A authentication key - const a2aData = await fetchA2AAuthKey(workflowName, runtimeUrl, httpClient, clientId, tenantId); + if (defaultHostName) { + // Azure remote workflow - use the app's defaultHostName with HTTPS + agentBaseUrl = defaultHostName.startsWith('https://') ? defaultHostName : `https://${defaultHostName}`; + hostName = defaultHostName; + } else { + // Local workflow - use the runtime URL with HTTP + const baseUrl = `${new URL(runtimeUrl).origin}`; + agentBaseUrl = baseUrl.startsWith('http://') ? baseUrl : `http://${baseUrl}`; + hostName = runtimeUrl; + } - // Add authentication tokens if available - const a2aKey = a2aData?.key; - if (a2aKey) { - queryParams = { apiKey: a2aKey }; - } + const agentUrl = `${agentBaseUrl}/api/Agents/${workflowName}`; + const chatUrl = `${agentBaseUrl}/api/agentsChat/${workflowName}/IFrame`; + let queryParams: AgentQueryParams | undefined = undefined; + + // Get A2A authentication key + const a2aData = await fetchA2AAuthKey(workflowName, runtimeUrl, httpClient, clientId, tenantId); - return { - agentUrl, - chatUrl, - queryParams, - hostName: runtimeUrl, - }; - } catch (error) { - LoggerService().log({ - level: LogEntryLevel.Error, - message: `Failed to get agent URL: ${error}`, - area: 'vscode: fetchAgentUrl', - }); - return { agentUrl: '', chatUrl: '', hostName: runtimeUrl }; + // Add authentication tokens if available + const a2aKey = a2aData?.key; + if (a2aKey) { + queryParams = { apiKey: a2aKey }; + } + + return { + agentUrl, + chatUrl, + queryParams, + hostName, + }; + } catch (error) { + LoggerService().log({ + level: LogEntryLevel.Error, + message: `Failed to get agent URL: ${error}`, + area: 'vscode: fetchAgentUrl', + }); + return { agentUrl: '', chatUrl: '', hostName: defaultHostName ?? runtimeUrl }; + } } - }); + ); }; // Helper function to fetch A2A authentication key diff --git a/apps/vs-code-react/src/app/designer/servicesHelper.ts b/apps/vs-code-react/src/app/designer/servicesHelper.ts index cc29903f195..d51ae277766 100644 --- a/apps/vs-code-react/src/app/designer/servicesHelper.ts +++ b/apps/vs-code-react/src/app/designer/servicesHelper.ts @@ -345,7 +345,8 @@ export const getDesignerServices = ( httpClient, clientId, tenantId, - isWorkflowRuntimeRunning + isWorkflowRuntimeRunning, + panelMetadata?.azureDetails?.defaultHostName ), }; diff --git a/apps/vs-code-react/src/run-service/types.ts b/apps/vs-code-react/src/run-service/types.ts index 3db975219df..217877f4d53 100644 --- a/apps/vs-code-react/src/run-service/types.ts +++ b/apps/vs-code-react/src/run-service/types.ts @@ -266,6 +266,11 @@ export interface GetDataMapperVersionMessage { data: number; } +export interface GetDesignerVersionMessage { + command: typeof ExtensionCommand.getDesignerVersion; + data: number; +} + // Designer Message Interfaces export interface ReceiveCallbackMessage { command: typeof ExtensionCommand.receiveCallback; diff --git a/apps/vs-code-react/src/state/__test__/projectSlice.test.ts b/apps/vs-code-react/src/state/__test__/projectSlice.test.ts new file mode 100644 index 00000000000..572cf601114 --- /dev/null +++ b/apps/vs-code-react/src/state/__test__/projectSlice.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest'; +import projectReducer, { initialize, changeDataMapperVersion, changeDesignerVersion } from '../projectSlice'; +import type { ProjectState } from '../projectSlice'; + +describe('projectSlice', () => { + const initialState: ProjectState = { + initialized: false, + }; + + describe('initialize', () => { + it('should set initialized to true and project name', () => { + const result = projectReducer(initialState, initialize('designer')); + expect(result.initialized).toBe(true); + expect(result.project).toBe('designer'); + }); + + it('should handle undefined project name', () => { + const result = projectReducer(initialState, initialize(undefined)); + expect(result.initialized).toBe(true); + expect(result.project).toBeUndefined(); + }); + }); + + describe('changeDataMapperVersion', () => { + it('should update dataMapperVersion', () => { + const result = projectReducer(initialState, changeDataMapperVersion(2)); + expect(result.dataMapperVersion).toBe(2); + }); + }); + + describe('changeDesignerVersion', () => { + it('should update designerVersion to 1', () => { + const result = projectReducer(initialState, changeDesignerVersion(1)); + expect(result.designerVersion).toBe(1); + }); + + it('should update designerVersion to 2', () => { + const result = projectReducer(initialState, changeDesignerVersion(2)); + expect(result.designerVersion).toBe(2); + }); + + it('should preserve other state when updating designerVersion', () => { + const stateWithProject: ProjectState = { + initialized: true, + project: 'designer', + dataMapperVersion: 1, + }; + const result = projectReducer(stateWithProject, changeDesignerVersion(2)); + expect(result.designerVersion).toBe(2); + expect(result.initialized).toBe(true); + expect(result.project).toBe('designer'); + expect(result.dataMapperVersion).toBe(1); + }); + }); +}); diff --git a/apps/vs-code-react/src/state/projectSlice.ts b/apps/vs-code-react/src/state/projectSlice.ts index 7ab8cd190fe..2da61928276 100644 --- a/apps/vs-code-react/src/state/projectSlice.ts +++ b/apps/vs-code-react/src/state/projectSlice.ts @@ -5,6 +5,7 @@ export interface ProjectState { initialized: boolean; project?: string; dataMapperVersion?: number; + designerVersion?: number; } const initialState: ProjectState = { @@ -22,9 +23,12 @@ export const projectSlice = createSlice({ changeDataMapperVersion: (state, action: PayloadAction) => { state.dataMapperVersion = action.payload; }, + changeDesignerVersion: (state, action: PayloadAction) => { + state.designerVersion = action.payload; + }, }, }); -export const { initialize, changeDataMapperVersion } = projectSlice.actions; +export const { initialize, changeDataMapperVersion, changeDesignerVersion } = projectSlice.actions; export default projectSlice.reducer; diff --git a/apps/vs-code-react/src/webviewCommunication.tsx b/apps/vs-code-react/src/webviewCommunication.tsx index a5b7fc072d6..1b70e586977 100644 --- a/apps/vs-code-react/src/webviewCommunication.tsx +++ b/apps/vs-code-react/src/webviewCommunication.tsx @@ -70,7 +70,7 @@ import { updateCallbackInfo, updateBaseUrl, } from './state/WorkflowSlice'; -import { changeDataMapperVersion, initialize } from './state/projectSlice'; +import { changeDataMapperVersion, changeDesignerVersion, initialize } from './state/projectSlice'; import type { AppDispatch, RootState } from './state/store'; import { SchemaType } from '@microsoft/logic-apps-shared'; import { ExtensionCommand, ProjectName } from '@microsoft/vscode-extension-logic-apps'; @@ -187,6 +187,10 @@ export const WebViewCommunication: React.FC<{ children: ReactNode }> = ({ childr designerDispatch(resetDesignerDirtyState(undefined)); break; } + case ExtensionCommand.getDesignerVersion: { + dispatch(changeDesignerVersion(message.data)); + break; + } default: throw new Error('Unknown post message received'); } diff --git a/apps/vs-code-react/vitest.config.ts b/apps/vs-code-react/vitest.config.ts index fd727308e4c..6f284770835 100644 --- a/apps/vs-code-react/vitest.config.ts +++ b/apps/vs-code-react/vitest.config.ts @@ -8,7 +8,12 @@ export default defineProject({ name: packageJson.name, environment: 'jsdom', setupFiles: ['test-setup.ts'], - coverage: { enabled: true, provider: 'istanbul', include: ['src/app/**/*'], reporter: ['html', 'cobertura', 'lcov'] }, + coverage: { + enabled: true, + provider: 'istanbul', + include: ['src/app/**/*', 'src/state/**/*'], + reporter: ['html', 'cobertura', 'lcov'], + }, restoreMocks: true, }, }); diff --git a/libs/vscode-extension/src/lib/models/extensioncommand.ts b/libs/vscode-extension/src/lib/models/extensioncommand.ts index deae476678a..ad9b4e7648a 100644 --- a/libs/vscode-extension/src/lib/models/extensioncommand.ts +++ b/libs/vscode-extension/src/lib/models/extensioncommand.ts @@ -18,6 +18,7 @@ export const ExtensionCommand = { export_package: 'export-package', getFunctionDisplayExpanded: 'getFunctionDisplayExpanded', getDataMapperVersion: 'getDataMapperVersion', + getDesignerVersion: 'getDesignerVersion', add_status: 'add-status', saveDataMapDefinition: 'saveDataMapDefinition', saveDataMapMetadata: 'saveDataMapMetadata', diff --git a/libs/vscode-extension/src/lib/models/workflow.ts b/libs/vscode-extension/src/lib/models/workflow.ts index 500a24b37e6..0dcca04dcf4 100644 --- a/libs/vscode-extension/src/lib/models/workflow.ts +++ b/libs/vscode-extension/src/lib/models/workflow.ts @@ -53,6 +53,7 @@ export interface AzureConnectorDetails { tenantId?: string; clientId?: string; workflowManagementBaseUrl?: string; + defaultHostName?: string; } export interface WorkflowParameter {