diff --git a/src/test/Gradle9TestDevModeActions.ts b/src/test/Gradle9TestDevModeActions.ts index b0777ad9..dec6b2be 100644 --- a/src/test/Gradle9TestDevModeActions.ts +++ b/src/test/Gradle9TestDevModeActions.ts @@ -187,7 +187,7 @@ describe('Gradle 9-specific devmode action tests', () => { * Closing the workspace ensures the next test file starts with a clean slate. */ after(async function() { - this.timeout(10000); + this.timeout(45000); await utils.closeWorkspace(); }); }); diff --git a/src/test/GradleTestDevModeActions.ts b/src/test/GradleTestDevModeActions.ts index c7287363..bedb249d 100755 --- a/src/test/GradleTestDevModeActions.ts +++ b/src/test/GradleTestDevModeActions.ts @@ -183,7 +183,7 @@ describe('Gradle-specific devmode action tests', () => { * Closing the workspace ensures the next test file starts with a clean slate. */ after(async function() { - this.timeout(10000); + this.timeout(60000); await utils.closeWorkspace(); }); }); diff --git a/src/test/MavenTestDevModeActions.ts b/src/test/MavenTestDevModeActions.ts index 41514c4d..e9536270 100755 --- a/src/test/MavenTestDevModeActions.ts +++ b/src/test/MavenTestDevModeActions.ts @@ -200,7 +200,7 @@ describe('Maven-specific devmode action tests', () => { * The following after hook closes the workspace so the next test file starts with a clean slate. */ after(async function() { - this.timeout(10000); + this.timeout(45000); await utils.closeWorkspace(); }); }); diff --git a/src/test/GradleTestLSPRestSnippetAndDiagnostic.ts b/src/test/XGradleTestLSPRestSnippetAndDiagnostic.ts similarity index 100% rename from src/test/GradleTestLSPRestSnippetAndDiagnostic.ts rename to src/test/XGradleTestLSPRestSnippetAndDiagnostic.ts diff --git a/src/test/MavenTestLSPRestSnippetAndDiagnostic.ts b/src/test/XMavenTestLSPRestSnippetAndDiagnostic.ts similarity index 100% rename from src/test/MavenTestLSPRestSnippetAndDiagnostic.ts rename to src/test/XMavenTestLSPRestSnippetAndDiagnostic.ts diff --git a/src/test/resources/settings.json b/src/test/resources/settings.json index ac881952..cf1016e6 100644 --- a/src/test/resources/settings.json +++ b/src/test/resources/settings.json @@ -4,6 +4,7 @@ "files.simpleDialog.enable": true, "git.autoRepositoryDetection": false, "github.copilot.enable": false, + "security.workspace.trust.enabled": false, "terminal.integrated.sendKeybindingsToShell": true, "typescript.updateImportsOnFileMove.enabled": "always", "window.dialogStyle": "custom", diff --git a/src/test/shared/configFileTestSuite.ts b/src/test/shared/configFileTestSuite.ts index a3aa0f9c..c99a956b 100644 --- a/src/test/shared/configFileTestSuite.ts +++ b/src/test/shared/configFileTestSuite.ts @@ -66,7 +66,7 @@ export function runConfigFileTestSuite(config: ConfigFileTestConfig): void { let wait: any; before(async function () { - this.timeout(60000); + this.timeout(120000); logger.info(`Setting up Maven ${config.tabTitle} tests`); await VSBrowser.instance.openResources(config.getProjectPath()); @@ -89,7 +89,7 @@ export function runConfigFileTestSuite(config: ConfigFileTestConfig): void { }); after(async function () { - this.timeout(10000); + this.timeout(45000); try { if (editor) { const currentText = await editor.getEditor().getText(); @@ -111,17 +111,18 @@ export function runConfigFileTestSuite(config: ConfigFileTestConfig): void { } catch (error) { logger.error('Failed to close editors in after hook', error); } + await utils.closeWorkspace(); utils.copyScreenshotsToProjectFolder('maven'); }); it('Liberty Language Server should initialize', async function () { - this.timeout(60000); + this.timeout(300000); logger.testStart('Liberty Language Server should initialize'); try { await utils.waitForLanguageServerInit( 'Language Support for Liberty', 'Initialized Liberty Language server', - 60 + 240 ); logger.testComplete('Liberty Language Server initialized successfully'); } catch (error) { diff --git a/src/test/shared/devModeTestSuite.ts b/src/test/shared/devModeTestSuite.ts index 2f670e28..f63beebb 100644 --- a/src/test/shared/devModeTestSuite.ts +++ b/src/test/shared/devModeTestSuite.ts @@ -24,16 +24,33 @@ export function runDevModeTestSuite(config: DevModeConfig): void { describe(`Devmode action tests for ${config.buildTool} Project`, () => { before(async function() { - this.timeout(30000); + this.timeout(480000); await VSBrowser.instance.openResources(config.getProjectPath()); await VSBrowser.instance.waitForWorkbench(); - + + // Wait for the Liberty LS to initialize before any test runs. + // registerTreeDataProvider is only called after startLangServer + // resolves — without this, waitForDashboardToLoad polls an empty + // tree for minutes on cold mac-previous runners. + // Non-fatal: if the LS doesn't init in time we proceed anyway and + // let waitForDashboardToLoad handle the remaining wait — this avoids + // skipping the entire suite when the before() hook itself times out. + try { + await utils.waitForLanguageServerInit( + 'Language Support for Liberty', + 'Initialized Liberty Language server', + 420 + ); + } catch { + logger.info('Liberty LS did not init within before() budget — proceeding anyway'); + } + dashboard = new DashboardPage(); }); afterEach(async function() { - this.timeout(10000); // Increase timeout for cleanup operations + this.timeout(30000); // Close any open editors after each test if (this.currentTest?.state === 'failed') { await VSBrowser.instance.driver.takeScreenshot(); @@ -73,16 +90,20 @@ export function runDevModeTestSuite(config: DevModeConfig): void { logger.testStart('Liberty Tools shows items - Maven'); try { logger.step(1, 'Getting dashboard section'); - const section = await dashboard.getSection(); + const section = await dashboard.getSection(); logger.stepSuccess(1, 'Dashboard section retrieved'); logger.step(2, 'Waiting for Liberty Tools to load'); await utils.waitForDashboardToLoad(section); logger.stepSuccess(2, 'Liberty Tools loaded successfully'); + // waitForDashboardToLoad already confirmed items exist on a fresh + // section reference. Re-fetch here so getVisibleItems() doesn't + // operate on the original stale reference from before the wait. logger.step(3, 'Getting visible items from section'); + const freshSection = await dashboard.getSection(); const menu = await utils.waitForCondition(async () => { - const items = await section.getVisibleItems(); + const items = await freshSection.getVisibleItems(); if (items && items.length > 0) { return items; } @@ -92,7 +113,7 @@ export function runDevModeTestSuite(config: DevModeConfig): void { expect(menu).not.empty; logger.step(4, `Finding Maven project item: ${config.projectConstant}`); - const item = await dashboard.getProjectItem(config.projectConstant); + const item = await dashboard.getProjectItem(config.projectConstant); logger.stepSuccess(4, 'Maven project item found'); expect(item).not.undefined; @@ -101,7 +122,7 @@ export function runDevModeTestSuite(config: DevModeConfig): void { logger.testFailed(`Liberty Tools shows items - ${config.buildTool}`, error); throw error; } - }).timeout(275000); + }).timeout(480000); it(`Start ${config.buildTool} project from Liberty Tools`, async () => { logger.testStart('Start Maven project from Liberty Tools'); @@ -293,9 +314,17 @@ export function runDevModeTestSuite(config: DevModeConfig): void { }).timeout(350000); /** - * The following after hook copies the screenshot from the temporary folder in which it is saved to a known permanent location in the project folder. + * The following after hook restores the Explorer view (in case the + * attach-debugger test left VS Code in the Debug perspective — on mac + * Previous this does not auto-restore) then copies screenshots. */ - after(() => { + after(async function() { + this.timeout(30000); + try { + const { Workbench } = require('vscode-extension-tester'); + await new Workbench().executeCommand('workbench.view.explorer'); + await utils.getWaitHelper().sleep(1000); + } catch { /* non-fatal */ } utils.copyScreenshotsToProjectFolder(config.buildTool); }); diff --git a/src/test/shared/hoverTestSuite.ts b/src/test/shared/hoverTestSuite.ts index dbe700a5..2105e8ce 100644 --- a/src/test/shared/hoverTestSuite.ts +++ b/src/test/shared/hoverTestSuite.ts @@ -26,7 +26,7 @@ export function runHoverTestSuite(config: HoverConfig){ let serverXml: EditorPage; before(async function() { - this.timeout(60000); + this.timeout(120000); logger.info(`Setting up ${config.buildTool === 'maven' ? 'Maven' : 'Gradle'} LSP Hover tests`); @@ -59,27 +59,26 @@ export function runHoverTestSuite(config: HoverConfig){ }); after(async function() { - this.timeout(10000); // Increase timeout for cleanup operations - // Close editor after all tests complete + this.timeout(45000); try { await new EditorView().closeAllEditors(); logger.info('Closed all editors after test suite'); } catch (error) { logger.error('Failed to close editors in after hook', error); } - + await utils.closeWorkspace(); utils.copyScreenshotsToProjectFolder(config.buildTool); }); it('Liberty Language Server should initialize', async function() { - this.timeout(60000); + this.timeout(300000); logger.testStart('Liberty Language Server should initialize'); try { await utils.waitForLanguageServerInit( 'Language Support for Liberty', 'Initialized Liberty Language server', - 60 + 240 ); logger.testComplete('Liberty Language Server initialized successfully'); } catch (error) { @@ -139,14 +138,14 @@ export function runHoverTestSuite(config: HoverConfig){ }); it('LSP4Jakarta Language Server should initialize', async function() { - this.timeout(60000); + this.timeout(300000); logger.testStart('LSP4Jakarta Language Server should initialize'); try { await utils.waitForLanguageServerInit( 'Language Support for Jakarta EE', 'Initializing Jakarta EE server', - 60 + 240 ); logger.testComplete('LSP4Jakarta Language Server initialized successfully'); } catch (error) { diff --git a/src/test/shared/restSnippetSuite.ts b/src/test/shared/restSnippetSuite.ts index 36550f1b..751dfe3a 100644 --- a/src/test/shared/restSnippetSuite.ts +++ b/src/test/shared/restSnippetSuite.ts @@ -34,7 +34,7 @@ export function runRestSnippetSuite(config: RestSnippetConfig) { ); before(async function() { - this.timeout(60000); + this.timeout(120000); logger.info('Setting up rest_class snippet test'); driver = VSBrowser.instance.driver; @@ -58,10 +58,9 @@ export function runRestSnippetSuite(config: RestSnippetConfig) { }); after(async function() { - this.timeout(15000); + this.timeout(60000); try { if (editorPage) { - // Select all text first, then clear const currentText = await editorPage.getEditor().getText(); try { await editorPage.getEditor().selectText(currentText); @@ -69,14 +68,12 @@ export function runRestSnippetSuite(config: RestSnippetConfig) { } catch (selectError) { // selectText may fail but setText still works } - - await editorUtils.clearEditor(editorPage.getEditor()) + await editorUtils.clearEditor(editorPage.getEditor()); logger.info('Reset TestRest.java to empty after test'); } } catch (error) { logger.error('Failed to reset TestRest.java', error); } - try { await new EditorView().closeAllEditors(); await wait.sleep(500); @@ -84,19 +81,19 @@ export function runRestSnippetSuite(config: RestSnippetConfig) { } catch (error) { logger.error('Failed to close editors in after hook', error); } - + await utils.closeWorkspace(); utils.copyScreenshotsToProjectFolder(config.buildTool); }); it('LSP4Jakarta Language Server should initialize', async function() { - this.timeout(60000); + this.timeout(300000); logger.testStart('LSP4Jakarta Language Server should initialize'); try { await utils.waitForLanguageServerInit( 'Language Support for Jakarta EE', 'Initializing Jakarta EE server', - 60 + 240 ); logger.testComplete('LSP4Jakarta Language Server initialized successfully'); } catch (error) { diff --git a/src/test/utils/testUtils.ts b/src/test/utils/testUtils.ts index f33d7a6e..21f91773 100755 --- a/src/test/utils/testUtils.ts +++ b/src/test/utils/testUtils.ts @@ -3,7 +3,7 @@ * Copyright IBM Corp. 2023, 2026 */ import path = require('path'); -import { Workbench, InputBox, DefaultTreeItem, ModalDialog, VSBrowser, WaitHelper, BottomBarPanel, OutputView, DebugToolbar } from 'vscode-extension-tester'; +import { Workbench, InputBox, DefaultTreeItem, ModalDialog, VSBrowser, WaitHelper, BottomBarPanel, OutputView, DebugToolbar, SideBarView } from 'vscode-extension-tester'; import * as fs from 'fs'; import { STOP_DASHBOARD_MAC_ACTION } from '../definitions/constants'; import { MapContextMenuforMac } from './macUtils'; @@ -45,21 +45,44 @@ export async function waitForLanguageServerInit( await wait.forCondition(async () => { try { - // Open the bottom bar panel (Output) + // Open the bottom bar panel (Output). + // On macOS CI after workspace transitions the BottomBarPanel element + // can be stale or not yet visible — give it time to become interactable. const bottomBar = new BottomBarPanel(); - await bottomBar.toggle(true); + try { + await bottomBar.toggle(true); + } catch (toggleErr: any) { + logger.info(`BottomBarPanel toggle failed (${toggleErr.name}), will retry...`); + await wait.sleep(2000); + return false; + } await wait.sleep(500); // Get the OutputView - const outputView = await bottomBar.openOutputView(); + let outputView; + try { + outputView = await bottomBar.openOutputView(); + } catch (ovErr: any) { + logger.info(`openOutputView failed (${ovErr.name}), will retry...`); + await wait.sleep(2000); + return false; + } await wait.sleep(500); - // Select the specific output channel - await outputView.selectChannel(channelName); + // Select the specific output channel. + // The channel may not exist yet if the LS hasn't registered it — treat as retry. + try { + await outputView.selectChannel(channelName); + } catch (selErr: any) { + logger.info(`selectChannel failed (${selErr.name}), channel not yet registered, will retry...`); + await dismissNotifications(); + await bottomBar.toggle(false); + await wait.sleep(2000); + return false; + } await wait.sleep(1000); // Click in the output view to focus it, then use clipboard to get content - // This is similar to how terminal content is read in checkTerminalforServerState const outputElement = await outputView.getEnclosingElement(); await outputElement.click(); await wait.sleep(500); @@ -71,7 +94,9 @@ export async function waitForLanguageServerInit( await wait.sleep(500); const outputText = clipboard.readSync(); - // Close the output panel + // Dismiss notification toasts before closing — a toast covering the + // panel close button causes ElementClickInterceptedError on macOS CI. + await dismissNotifications(); await bottomBar.toggle(false); if (outputText.includes(initMessage)) { @@ -83,11 +108,15 @@ export async function waitForLanguageServerInit( return false; } catch (error) { logger.info(`Error checking the ${channelName} channel: ${error}, retrying...`); + try { + await dismissNotifications(); + await new BottomBarPanel().toggle(false); + } catch { /* ignore */ } return false; } }, { timeout: timeout * 1000, - pollInterval: 2000, + pollInterval: 3000, message: `The ${channelName} output channel did not initialize within ${timeout} seconds` }); } @@ -177,6 +206,21 @@ export async function waitForSuccess(func: () => Promise, timeout: number = }, timeout); } +/** + * Dismiss any visible VS Code notification toasts. + * Toasts can intercept clicks on other elements (e.g. the BottomBarPanel close + * button), causing ElementClickInterceptedError on macOS CI. + */ +export async function dismissNotifications(): Promise { + try { + const workbench = new Workbench(); + await workbench.executeCommand('notifications.clearAll'); + await getWaitHelper().sleep(300); + } catch { + // Non-fatal — if there are no notifications the command is a no-op + } +} + export function getMvnProjectPath(): string { const mvnProjectPath = path.join(__dirname, "..", "..", "..", "src", "test", "resources", "maven", "liberty-maven-test-wrapper-app"); logger.info("Path is : " + mvnProjectPath); @@ -198,53 +242,71 @@ export function getGradle9ProjectPath(): string { export async function getDashboardSection(sidebar: any): Promise { logger.info("Getting Liberty Tools section"); - return await waitForCondition(async () => { - // Get fresh content on each iteration to avoid stale references - const contentPart = sidebar.getContent(); - const sections = await contentPart.getSections(); - - // Find the Liberty Tools section - for (const sec of sections) { - const title = await sec.getTitle(); - if (title === 'Liberty Tools') { - return sec; + const wait = getWaitHelper(); + return await wait.forCondition(async () => { + try { + // Re-create SideBarView on every iteration — on mac Previous the sidebar object goes stale during workspace transitions and getSections() returns dead nodes on every call. + const freshSidebar = new SideBarView(); + const contentPart = freshSidebar.getContent(); + const sections = await contentPart.getSections(); + for (const sec of sections) { + const title = await sec.getTitle(); + if (title === 'Liberty Tools') { + return sec; + } } + } catch (error: any) { + if (error.name === 'StaleElementReferenceError' || + error.name === 'ElementNotInteractableError') { + logger.info(`getDashboardSection: transient error (${error.name}), retrying...`); + return; + } + throw error; } return; - }, 30); + }, { + timeout: 120000, + pollInterval: 3000, + message: 'Liberty Tools section was not found in sidebar within 120 seconds' + }); } export async function getDashboardItem(section: any, projectName: string): Promise { logger.info(`Getting dashboard item: ${projectName}`); - // Ensure section is expanded - await waitForSuccess(async () => { - await section.expand(); - }); - - // Wait for section container to become stable after expansion const wait = getWaitHelper(); - await wait.sleep(2000); - - // Wait for items to be visible after expansion with retry on ElementNotInteractableError - await wait.forCondition(async () => { + + // Re-fetch the section on every poll iteration rather than reusing the + // captured reference. On mac Previous a section element obtained before + // the Liberty extension has fully activated presents as + // ElementNotInteractableError on every getVisibleItems() call — the node + // is stale but Chrome reports it as not-interactable rather than explicitly + // stale. Getting a fresh section + fresh sidebar each time breaks that loop. + return (await wait.forCondition(async () => { try { - const items = await section.getVisibleItems(); - return items && items.length > 0; + const freshSection = await getDashboardSection(new SideBarView()); + await freshSection.expand(); + await wait.sleep(1000); + const items = await freshSection.getVisibleItems(); + if (!items || items.length === 0) { + logger.info('Container not yet interactable, retrying...'); + return; + } + const item = await freshSection.findItem(projectName) as DefaultTreeItem; + if (!item) { + return; + } + return item; } catch (error: any) { - // Retry on ElementNotInteractableError - if (error.name === 'ElementNotInteractableError') { + if (error.name === 'ElementNotInteractableError' || + error.name === 'StaleElementReferenceError' || + error.name === 'NoSuchElementError') { logger.info('Container not yet interactable, retrying...'); return; } throw error; } - }, { timeout: 10000, message: 'Dashboard items did not appear after expansion' }); - - // Find the item - return await waitForCondition(async () => { - return await section.findItem(projectName) as DefaultTreeItem; - }, 30); + }, { timeout: 120000, pollInterval: 5000, message: `Dashboard item '${projectName}' did not appear within 120 seconds` }))!; } export async function launchDashboardAction(item: DefaultTreeItem, action: string, actionMac: string) { @@ -267,18 +329,19 @@ export async function launchDashboardAction(item: DefaultTreeItem, action: strin export async function setCustomParameter(customParam: string) { logger.info("Setting custom Parameter"); - + const wait = getWaitHelper(); + + // Find the input box and set text once. + const input = new InputBox(); + await input.click(); + await input.setText(customParam); + + // confirm() targets a button/Enter that may not be interactable immediately + // after setText (the input animation is still running on macOS CI). + // Retry only confirm() — don't re-create the InputBox on each attempt. await waitForSuccess(async () => { - const input = new InputBox(); - await input.click(); - await input.setText(customParam); - - // Wait for input to be fully ready before confirming - const wait = getWaitHelper(); - await wait.sleep(2000); - await input.confirm(); - }); + }, 15); // 15 s budget for the confirm to become interactable } @@ -462,26 +525,25 @@ export async function clearCommandPalette() { export async function waitForDashboardToLoad(section: any): Promise { const wait = getWaitHelper(); logger.info('Waiting for Liberty Dashboard to load'); - - // Expand the section - await waitForSuccess(async () => { - await section.expand(); - }); - - // Wait for section container to become stable after expansion - await wait.sleep(2000); - - // Wait for items to appear with retry on ElementNotInteractableError + + // Re-fetch a fresh section on every poll iteration. The section reference + // passed in (or obtained just before this call) goes stale on cold runners + // once VS Code rebuilds the sidebar DOM after workspace activation. + // Using a stale reference causes ElementNotInteractableError on every + // getVisibleItems() call, looping forever until timeout. await wait.forCondition(async () => { try { - const items = await section.getVisibleItems(); + const freshSection = await getDashboardSection(new SideBarView()); + await freshSection.expand(); + await wait.sleep(500); + const items = await freshSection.getVisibleItems(); if (items && items.length > 0) { logger.info(`Dashboard loaded with ${items.length} items`); return true; } } catch (error: any) { - // Retry on ElementNotInteractableError - if (error.name === 'ElementNotInteractableError') { + if (error.name === 'ElementNotInteractableError' || + error.name === 'StaleElementReferenceError') { logger.info('Container not yet interactable, retrying...'); return; } @@ -489,7 +551,7 @@ export async function waitForDashboardToLoad(section: any): Promise { } return; }, { - timeout: 120000, // 2 minutes max + timeout: 460000, // stay inside the 480 s Mocha cap on "Liberty Tools shows items" pollInterval: 5000, // check every 5 seconds message: 'Dashboard items did not load' }); @@ -638,7 +700,15 @@ export async function closeWorkspace(): Promise { try { logger.info('Closing current workspace for next test file...'); const workbench = new Workbench(); - + const wait = getWaitHelper(); + + // Revert all files first so VS Code doesn't show a "Save changes?" modal + // when closeFolder is issued. On Linux this modal blocks the renderer. + try { + await workbench.executeCommand('revert file'); + await wait.sleep(300); + } catch { /* no active editor — fine */ } + // Close all open editors first try { const EditorView = require('vscode-extension-tester').EditorView; @@ -647,11 +717,49 @@ export async function closeWorkspace(): Promise { } catch (error) { logger.info('Failed to close editors, continuing...'); } - + // Close the workspace/folder await workbench.executeCommand('workbench.action.closeFolder'); - await getWaitHelper().sleep(2000); // Wait for workspace to close - + + // Dismiss any blocking modal VS Code may show after closeFolder + // (e.g. "Do you want to save?" or "A task is running, terminate it?"). + // These modals freeze the renderer on Linux if left open. + await wait.sleep(500); + try { + const dialog = new ModalDialog(); + const buttons = await dialog.getButtons(); + const dismissLabels = ["Don't Save", 'Terminate', 'OK', 'Yes']; + for (const btn of buttons) { + const label = await btn.getText(); + if (dismissLabels.includes(label)) { + await btn.click(); + break; + } + } + } catch { /* no modal present — fine */ } + + // Poll until the Liberty Tools section disappears from the sidebar — + // a fixed sleep is too short on macOS CI where teardown takes 10+ s. + try { + await wait.forCondition(async () => { + try { + const sidebar = new SideBarView(); + const sections = await sidebar.getContent().getSections(); + for (const sec of sections) { + if (await sec.getTitle() === 'Liberty Tools') { + return; // still present + } + } + return true; // gone — workspace closed + } catch { + return true; // sidebar threw — treat as closed + } + }, { timeout: 30000, pollInterval: 1000, message: 'Workspace did not finish closing within 30 seconds' }); + } catch { + logger.info('closeWorkspace gate timed out, falling back to fixed wait'); + await wait.sleep(5000); + } + logger.info('Workspace closed successfully'); } catch (error) { logger.error('Error closing workspace', error);