diff --git a/workspaces/mi/mi-extension/package.json b/workspaces/mi/mi-extension/package.json index 1e780aa5954..69a4d111919 100644 --- a/workspaces/mi/mi-extension/package.json +++ b/workspaces/mi/mi-extension/package.json @@ -3,7 +3,7 @@ "displayName": "WSO2 Integrator: MI", "description": "An extension which gives a development environment for designing, developing, debugging, and testing integration solutions.", "icon": "resources/images/wso2-micro-integrator-image.png", - "version": "3.1.5", + "version": "3.1.526032514", "publisher": "wso2", "engines": { "vscode": "^1.100.0" diff --git a/workspaces/mi/mi-extension/src/stateMachine.ts b/workspaces/mi/mi-extension/src/stateMachine.ts index 22d9a3f4d06..4d1e25c9990 100644 --- a/workspaces/mi/mi-extension/src/stateMachine.ts +++ b/workspaces/mi/mi-extension/src/stateMachine.ts @@ -323,7 +323,7 @@ const stateMachine = createMachine({ }, newProject: { entry: () => logDebug("State Machine: Entering 'newProject' state"), - initial: "viewLoading", + initial: "viewReady", states: { viewLoading: { entry: () => logDebug("State Machine: Entering 'newProject.viewLoading' state"), diff --git a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/nonMiProjectActivation.spec.ts b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/nonMiProjectActivation.spec.ts new file mode 100644 index 00000000000..8b6d19ddd5e --- /dev/null +++ b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/nonMiProjectActivation.spec.ts @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * E2E Tests for Issue #1371: + * MI Welcome Page opens when accessing VS Code Test Explorer (even for non-MI projects) + * + * These tests verify that when the MI extension activates in a workspace that + * contains no WSO2 MI artefacts (no pom.xml with integration-project type, + * no .project file), the MI Welcome Page webview panel does NOT open + * automatically — specifically not when the user opens the VS Code Test + * Explorer view. + * + * Setup: + * - VS Code is launched with a fresh, empty workspace (no pom.xml → not an MI project). + * - `initTest(true, true)` cleans `newProjectPath` and starts VS Code without + * creating an MI project, giving us a plain non-MI workspace. + * + * Run: + * pnpm run compile-tests && pnpm exec playwright test --grep "Issue #1371" + */ + +import { test, expect } from '@playwright/test'; +import * as path from 'path'; +import * as fs from 'fs'; +import { initTest, newProjectPath, page } from './Utils'; + +export default function createTests() { + test.describe('Issue #1371 — Non-MI Project Activation', { + tag: '@issue1371', + }, async () => { + // `newProject: true` → always start with a clean workspace directory + // `skipProjectCreation: true` → do NOT run createProject(); the workspace + // stays empty (no pom.xml) so the MI extension + // will not detect an MI project. + initTest(true, true, false, undefined, undefined, 'issue1371'); + + // ── Helper: populate a minimal JS project layout ───────────────────── + + async function populateNonMiWorkspace(): Promise { + // Write minimal JS project files so that VS Code's built-in Testing + // view controller can register a test provider (making the Testing + // beaker icon reliably appear in the activity bar). + fs.writeFileSync( + path.join(newProjectPath, 'package.json'), + JSON.stringify({ name: 'my-js-app', version: '1.0.0', scripts: { test: 'echo "no tests"' } }) + ); + fs.writeFileSync( + path.join(newProjectPath, 'index.js'), + 'console.log("Hello World");\n' + ); + // Explicitly ensure NO pom.xml is present. + const pomPath = path.join(newProjectPath, 'pom.xml'); + if (fs.existsSync(pomPath)) { + fs.unlinkSync(pomPath); + } + } + + // ── Helper: collect all visible tab titles in the editor area ──────── + + async function getEditorTabTitles(): Promise { + const tabLabels = page.page.locator('.tab-label'); + const count = await tabLabels.count(); + const titles: string[] = []; + for (let i = 0; i < count; i++) { + const title = await tabLabels.nth(i).textContent(); + if (title) { + titles.push(title.trim()); + } + } + return titles; + } + + // ── Helper: check all webview iframes for MI Welcome content ───────── + + async function miWelcomeFoundInWebviews(): Promise { + const iframes = page.page.locator('iframe.webview'); + const count = await iframes.count(); + for (let i = 0; i < count; i++) { + try { + const frame = iframes.nth(i).contentFrame(); + if (!frame) { + continue; + } + const welcomeLocator = frame.locator('text=Welcome to WSO2 Integrator: MI'); + const welcomeCount = await welcomeLocator.count(); + if (welcomeCount > 0) { + return true; + } + } catch { + // Frame might be cross-origin or not yet ready; skip it. + } + } + return false; + } + + // ──────────────────────────────────────────────────────────────────── + + test( + 'MI Welcome Page does not appear when Test Explorer is opened on a non-MI project', + async ({}, testInfo) => { + console.log(`>>> Test attempt: ${testInfo.retry + 1}`); + + // Ensure the workspace is a plain JS project (no MI artefacts). + await populateNonMiWorkspace(); + + // ── Step 1: Open the VS Code Test Explorer ─────────────────── + // The Testing view is a built-in VS Code container. Clicking its + // activity bar icon fires the implicit `onView:MI.mock-services` + // activation event (because the MI extension registers a view in + // the `test` container via package.json → contributes.views.test). + // This is exactly the trigger described in Issue #1371. + await test.step('Open VS Code Test Explorer via command palette', async () => { + console.log('Opening VS Code Test Explorer'); + await page.executePaletteCommand('View: Show Testing'); + // Brief wait for the view to render and for any extension + // activation triggered by revealing the Testing container. + await page.page.waitForTimeout(4000); + console.log('Test Explorer command issued; waiting for extension activation to settle'); + }); + + // ── Step 2: Also try clicking the Testing activity bar tab ─── + // In addition to the palette command, directly clicking the + // Testing beaker in the activity bar can independently fire the + // `onView` activation event. + await test.step('Click Testing activity bar tab if visible', async () => { + const testingTab = page.page.getByRole('tab', { name: 'Testing' }); + const isVisible = await testingTab.isVisible().catch(() => false); + if (isVisible) { + console.log('Testing activity bar tab found; clicking it'); + const tabBtn = testingTab.locator('a'); + await tabBtn.click({ timeout: 5000 }).catch(() => { + console.log('Tab click timed out; continuing'); + }); + await page.page.waitForTimeout(3000); + } else { + console.log('Testing activity bar tab not visible in this session; skipping direct click'); + } + }); + + // ── Step 3: Assert — no MI Welcome Page tab in editor area ─── + await test.step('Assert no MI Welcome Page tab is visible in the editor', async () => { + const tabTitles = await getEditorTabTitles(); + console.log('Editor tab titles found:', tabTitles); + + const miWelcomeTabs = tabTitles.filter(t => + t.includes('WSO2 Integrator: MI') || + t.includes('Welcome to WSO2') || + // MACHINE_VIEW.Welcome is 'WSO2 Integrator: MI' + t === 'WSO2 Integrator: MI' + ); + + expect(miWelcomeTabs).toHaveLength(0); + console.log('PASS: No MI Welcome Page tab found in the editor — Issue #1371 regression guard passed'); + }); + + // ── Step 4: Assert — no MI Welcome content in any webview ──── + await test.step('Assert MI Welcome Page content is absent from all webview frames', async () => { + const miWelcomeInWebview = await miWelcomeFoundInWebviews(); + expect(miWelcomeInWebview).toBe(false); + console.log('PASS: No MI Welcome Page content found in any webview iframe'); + }); + + // ── Step 5: Assert — MI status context is unknownProject ───── + // As a secondary verification, confirm the Test Explorer sidebar + // does NOT show the MI Mock Services section (which only appears + // when MI.status == 'projectLoaded'). + await test.step('Assert MI Mock Services section is not visible in Test Explorer', async () => { + const mockServicesSection = page.page.locator('[aria-label*="Mock Services"]'); + const count = await mockServicesSection.count(); + expect(count).toBe(0); + console.log('PASS: MI Mock Services section is not visible in Test Explorer (correct for non-MI workspace)'); + }); + } + ); + + // ── Negative/edge case: non-MI Maven project ───────────────────────── + + test( + 'MI Welcome Page does not appear when a non-MI Maven pom.xml is present and Test Explorer opens', + async ({}, testInfo) => { + console.log(`>>> Test attempt: ${testInfo.retry + 1}`); + + // Write a plain Maven pom.xml (no MI projectType). + fs.writeFileSync( + path.join(newProjectPath, 'pom.xml'), + 'com.exampleplain-maven-app' + ); + + await test.step('Open VS Code Test Explorer', async () => { + await page.executePaletteCommand('View: Show Testing'); + await page.page.waitForTimeout(4000); + }); + + await test.step('Assert no MI Welcome Page tab appears', async () => { + const tabTitles = await getEditorTabTitles(); + const miWelcomeTabs = tabTitles.filter(t => + t.includes('WSO2 Integrator: MI') || t.includes('Welcome to WSO2') + ); + expect(miWelcomeTabs).toHaveLength(0); + }); + + await test.step('Cleanup: remove non-MI pom.xml', async () => { + const pomPath = path.join(newProjectPath, 'pom.xml'); + if (fs.existsSync(pomPath)) { + fs.unlinkSync(pomPath); + } + }); + } + ); + }); +} + diff --git a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/test.list.ts b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/test.list.ts index 2e0491645de..46fcc4405b6 100644 --- a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/test.list.ts +++ b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/test.list.ts @@ -35,6 +35,7 @@ import overviewPageTests from './overviewPageTests/projectSettingPage.spec'; import openEntryPointArtifact from './overviewPageTests/openEntryPointArtifact.spec'; import multiWorkspaceTests from './multiWorkspaceTests/multiWorkspace.spec'; import unitTestSuitTests from './unitTestSuite.spec'; +import nonMiProjectActivationTests from './nonMiProjectActivation.spec'; import { page } from './Utils'; const fs = require('fs'); const path = require('path'); @@ -68,6 +69,7 @@ test.describe(validateMediatorTests); test.describe(dataMapperMediatorTests); test.describe(unitTestSuitTests); test.describe(dbReportMediatorTests); +test.describe(nonMiProjectActivationTests); test.describe(artifact430Tests); test.afterAll(async () => { diff --git a/workspaces/mi/mi-extension/src/test/suite/nonMiProjectActivation.test.ts b/workspaces/mi/mi-extension/src/test/suite/nonMiProjectActivation.test.ts new file mode 100644 index 00000000000..9e1c26da7ad --- /dev/null +++ b/workspaces/mi/mi-extension/src/test/suite/nonMiProjectActivation.test.ts @@ -0,0 +1,266 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Tests for Issue #1371: MI Welcome Page opens when accessing VS Code Test + * Explorer even for non-MI projects. + * + * Root cause: The state machine's `newProject` state previously used + * `initial: "viewLoading"`, which immediately invoked `openWebPanel` on every + * non-MI activation. The fix sets `initial: "viewReady"` so that + * `openWebPanel` is only called in response to an explicit `OPEN_VIEW` event. + * + * These tests are regression guards that will fail if the initial substate of + * `newProject` is ever reverted to `viewLoading`, or if any other code path + * causes the VisualizerWebview (Welcome Page) to be created without user + * interaction for a non-MI workspace. + */ + +import * as assert from 'assert'; +import * as path from 'path'; +import * as os from 'os'; +import * as fs from 'fs'; +import { isOldProjectOrWorkspace, getStateMachine, deleteStateMachine } from '../../stateMachine'; +import { webviews } from '../../visualizer/webview'; + +// Maximum time (ms) we wait for the state machine to settle after starting. +const STATE_SETTLE_TIMEOUT_MS = 8000; + +/** + * Resolves when the state machine reaches the given top-level state. + * Rejects with a descriptive error if it has not arrived within + * STATE_SETTLE_TIMEOUT_MS milliseconds. + */ +function waitForTopState( + sm: ReturnType, + targetState: string +): Promise { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout( + () => + reject( + new Error( + `State machine did not reach '${targetState}' within ` + + `${STATE_SETTLE_TIMEOUT_MS} ms. ` + + `Current state: ${JSON.stringify(sm.state())}` + ) + ), + STATE_SETTLE_TIMEOUT_MS + ); + + const service = sm.service(); + service.onTransition((state: any) => { + const val = state.value; + const topState = typeof val === 'object' ? Object.keys(val)[0] : val; + if (topState === targetState) { + clearTimeout(timeoutHandle); + resolve(); + } + }); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── + +suite('Issue #1371 — Non-MI Project Activation', () => { + let tempDir: string; + + setup(() => { + // Create a fresh temp directory that has NO pom.xml and NO .project + // file — this simulates a plain JavaScript / Maven project that is + // NOT a WSO2 MI integration project. + tempDir = path.join(os.tmpdir(), `mi-test-non-mi-1371-${Date.now()}`); + fs.mkdirSync(tempDir, { recursive: true }); + }); + + teardown(async () => { + // Stop and remove the state machine entry to avoid polluting other tests. + deleteStateMachine(tempDir); + + // Dispose any webview that might have been accidentally created. + const webview = webviews.get(tempDir); + if (webview) { + webview.dispose(); + webviews.delete(tempDir); + } + + // Remove the temp directory. + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch { + // Best-effort; ignore cleanup errors. + } + }); + + // ── isOldProjectOrWorkspace ────────────────────────────────────────────── + + test('isOldProjectOrWorkspace returns false for an empty (non-MI) directory', async () => { + // Empty directory — no pom.xml, no .project. + const result = await isOldProjectOrWorkspace(tempDir); + assert.strictEqual( + result, + false, + 'An empty directory must not be classified as an old Integration Studio project or workspace' + ); + }); + + test('isOldProjectOrWorkspace returns false for a plain Maven pom.xml', async () => { + // A Maven pom.xml that does NOT contain MI-specific content. + fs.writeFileSync( + path.join(tempDir, 'pom.xml'), + 'com.examplemy-app' + ); + const result = await isOldProjectOrWorkspace(tempDir); + assert.strictEqual( + result, + false, + 'A plain Maven project without Integration Studio multi-module nature must not be classified as an old MI project' + ); + }); + + // ── State machine transitions ──────────────────────────────────────────── + + test('state machine transitions to newProject state for a non-MI directory', async () => { + const sm = getStateMachine(tempDir); + await waitForTopState(sm, 'newProject'); + + const state = sm.state() as any; + const topState = typeof state === 'object' ? Object.keys(state)[0] : state; + assert.strictEqual( + topState, + 'newProject', + 'State machine must enter the newProject state when the workspace contains no MI artefacts' + ); + }); + + test('newProject state machine context carries Welcome view type', async () => { + // After transitioning to newProject, context.view should be set to + // MACHINE_VIEW.Welcome — but the webview must NOT be opened yet. + const sm = getStateMachine(tempDir); + await waitForTopState(sm, 'newProject'); + + const ctx = sm.context() as any; + assert.ok( + ctx.view !== undefined, + 'State machine context must have a view property after entering newProject' + ); + }); + + // ── viewReady vs. viewLoading initial substate ─────────────────────────── + + test('newProject initial substate is viewReady, not viewLoading', async () => { + // This is the PRIMARY regression guard for Issue #1371. + // + // With the bug: initial substate was `viewLoading`, causing `openWebPanel` + // to fire immediately → MI Welcome Page appeared without user action. + // + // With the fix: initial substate is `viewReady` → `openWebPanel` is + // only invoked after an explicit OPEN_VIEW event from the user. + const sm = getStateMachine(tempDir); + await waitForTopState(sm, 'newProject'); + + const state = sm.state() as any; + assert.ok( + typeof state === 'object', + 'State value should be an object when inside the newProject compound state' + ); + assert.strictEqual( + state.newProject, + 'viewReady', + 'newProject must start in the viewReady substate; ' + + 'viewLoading (which invokes openWebPanel) must only be reached via an explicit OPEN_VIEW event' + ); + }); + + test('state machine does not auto-transition to newProject.viewLoading without OPEN_VIEW', async () => { + const sm = getStateMachine(tempDir); + await waitForTopState(sm, 'newProject'); + + // Allow the machine a brief period to settle in case it spontaneously + // transitions further. + await new Promise(r => setTimeout(r, 600)); + + const state = sm.state() as any; + const substate = typeof state === 'object' ? state.newProject : state; + assert.notStrictEqual( + substate, + 'viewLoading', + 'State machine must not enter newProject.viewLoading without an explicit OPEN_VIEW event; ' + + 'doing so would open the MI Welcome Page without user interaction' + ); + }); + + // ── No webview created on activation ──────────────────────────────────── + + test('no VisualizerWebview panel is created on activation for an empty non-MI directory', async () => { + // THE CORE BUG GUARD: The MI Welcome Page must never be opened + // automatically when the extension activates on a non-MI workspace. + const sm = getStateMachine(tempDir); + await waitForTopState(sm, 'newProject'); + + assert.strictEqual( + webviews.has(tempDir), + false, + 'A VisualizerWebview (MI Welcome Page) must NOT be created automatically ' + + 'when the extension activates on a non-MI workspace; ' + + 'it must only appear after an explicit user action' + ); + }); + + test('no VisualizerWebview panel is created for a non-MI directory with a plain pom.xml', async () => { + // Edge case: directory has pom.xml but it is not an MI integration project. + fs.writeFileSync( + path.join(tempDir, 'pom.xml'), + 'com.exampleplain-app' + ); + + const sm = getStateMachine(tempDir); + await waitForTopState(sm, 'newProject'); + + assert.strictEqual( + webviews.has(tempDir), + false, + 'A plain Maven project (pom.xml without MI projectType) must not cause the ' + + 'MI Welcome webview to open automatically' + ); + }); + + test('no VisualizerWebview panel is created for a non-MI JS project layout', async () => { + // Simulates a JavaScript project that would host a jest test controller, + // which is the exact scenario that triggers onView:MI.mock-services + // activation in VS Code desktop (the bug trigger from Issue #1371). + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ name: 'my-js-app', version: '1.0.0', scripts: { test: 'jest' } }) + ); + fs.writeFileSync( + path.join(tempDir, 'jest.config.js'), + 'module.exports = { testEnvironment: "node" };' + ); + + const sm = getStateMachine(tempDir); + await waitForTopState(sm, 'newProject'); + + assert.strictEqual( + webviews.has(tempDir), + false, + 'A JavaScript project with jest config must not open the MI Welcome Page ' + + 'when the MI extension is activated (e.g., via the VS Code Test Explorer)' + ); + }); +});