diff --git a/src/test/MavenTestServerXmlLCLS.ts b/src/test/MavenTestServerXmlLCLS.ts index 048ba6a1..a79a8d67 100644 --- a/src/test/MavenTestServerXmlLCLS.ts +++ b/src/test/MavenTestServerXmlLCLS.ts @@ -34,6 +34,7 @@ describe('Liberty Config Language Server Tests for Maven Project', () => { await VSBrowser.instance.openResources(serverXmlPath, async () => { await wait.sleep(3000); }); + editor = await editorView.openEditor('server.xml') as TextEditor; originalContent = await editor.getText(); @@ -47,6 +48,9 @@ describe('Liberty Config Language Server Tests for Maven Project', () => { logger.error(`Test failed: ${this.currentTest?.title}`); } + // Dismiss notification toasts before closing the panel — a toast covering + // the close button causes ElementClickInterceptedError on macOS CI + await utils.dismissNotifications(); // Close the bottom bar and re-focus the editor await new BottomBarPanel().toggle(false); editor = await editorView.openEditor('server.xml') as TextEditor; @@ -79,12 +83,12 @@ describe('Liberty Config Language Server Tests for Maven Project', () => { }); it('Liberty Language Server should initialize', async function() { - this.timeout(60000); + this.timeout(300000); logger.testStart('Liberty Language Server should initialize'); await utils.waitForLanguageServerInit( 'Language Support for Liberty', 'Initialized Liberty Language server', - 60 + 240 ); logger.testComplete('Liberty Language Server initialized successfully'); }); 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/pages/CodeAssistPage.ts b/src/test/pages/CodeAssistPage.ts index 6efa5611..21233458 100644 --- a/src/test/pages/CodeAssistPage.ts +++ b/src/test/pages/CodeAssistPage.ts @@ -12,6 +12,19 @@ export class CodeAssistPage { await editor.getEditor().typeText(snippetTrigger); const assist = await editor.getEditor().toggleContentAssist(true); if (assist) { + // Wait until the target item is interactable before selecting. + // Newer VS Code virtualises the content-assist list — the item exists + // in the DOM immediately but is not scrolled into view/clickable until + // the list has fully rendered. + const deadline = Date.now() + 15000; + while (Date.now() < deadline) { + try { + await assist.getItem(fullSnippet); + break; // item is present and didn't throw + } catch { + await new Promise(res => setTimeout(res, 500)); + } + } await assist.select(fullSnippet); await new Promise(res => setTimeout(res, 600)); // 300–800ms } diff --git a/src/test/pages/EditorPage.ts b/src/test/pages/EditorPage.ts index 4c116769..03e26b64 100644 --- a/src/test/pages/EditorPage.ts +++ b/src/test/pages/EditorPage.ts @@ -20,7 +20,15 @@ export class EditorPage { await VSBrowser.instance.openResources(filePath, async () => { await utils.getWaitHelper().sleep(loadDelay); }); - this.editor = await this.editorView.openEditor(tabTitle) as TextEditor; + // openEditor can fail on slow CI if the tab hasn't rendered yet — + // retry for up to 30 s before giving up. + this.editor = await utils.waitForCondition(async () => { + try { + return await this.editorView.openEditor(tabTitle) as TextEditor; + } catch { + return undefined; + } + }, 30) as TextEditor; return this; } 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..f747293f 100644 --- a/src/test/shared/devModeTestSuite.ts +++ b/src/test/shared/devModeTestSuite.ts @@ -24,7 +24,7 @@ export function runDevModeTestSuite(config: DevModeConfig): void { describe(`Devmode action tests for ${config.buildTool} Project`, () => { before(async function() { - this.timeout(30000); + this.timeout(60000); await VSBrowser.instance.openResources(config.getProjectPath()); await VSBrowser.instance.waitForWorkbench(); @@ -33,7 +33,7 @@ export function runDevModeTestSuite(config: DevModeConfig): void { }); 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(); @@ -295,7 +295,17 @@ export function runDevModeTestSuite(config: DevModeConfig): void { /** * 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. */ - after(() => { + after(async function() { + this.timeout(30000); + // Restore the Explorer view in case the attach-debugger test left + // VS Code in the Debug perspective. On mac Previous this does not + // happen automatically and every subsequent sidebar call would query + // the wrong panel, causing ElementNotInteractableError forever. + 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..2e6bb97b 100644 --- a/src/test/shared/restSnippetSuite.ts +++ b/src/test/shared/restSnippetSuite.ts @@ -34,12 +34,12 @@ 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; - wait = utils.getWaitHelper(); - // Open folder, wait for workbench + wait = utils.getWaitHelper(); + // Open folder, wait for workbench await VSBrowser.instance.openResources(config.getProjectPath()); await VSBrowser.instance.waitForWorkbench(); @@ -58,7 +58,7 @@ export function runRestSnippetSuite(config: RestSnippetConfig) { }); after(async function() { - this.timeout(15000); + this.timeout(60000); try { if (editorPage) { // Select all text first, then clear @@ -85,25 +85,26 @@ export function runRestSnippetSuite(config: RestSnippetConfig) { 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) { logger.testFailed('LSP4Jakarta Language Server should initialize', error); throw error; } - }); + }); it('rest_class snippet populates correct REST class', async function () { this.timeout(275000); diff --git a/src/test/utils/testUtils.ts b/src/test/utils/testUtils.ts index f33d7a6e..529844d7 100755 --- a/src/test/utils/testUtils.ts +++ b/src/test/utils/testUtils.ts @@ -45,21 +45,48 @@ 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 — create a fresh reference each + // iteration and give it time to become interactable. const bottomBar = new BottomBarPanel(); - await bottomBar.toggle(true); + try { + await bottomBar.toggle(true); + } catch (toggleErr: any) { + // If the panel element itself is not visible/interactable, wait + // briefly and signal retry rather than immediately failing. + 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 option may not exist yet if the language server hasn't + // registered its output channel — 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,6 +98,9 @@ export async function waitForLanguageServerInit( await wait.sleep(500); const outputText = clipboard.readSync(); + // Dismiss any notification toasts before closing — a toast covering + // the panel close button causes ElementClickInterceptedError on macOS CI + await dismissNotifications(); // Close the output panel await bottomBar.toggle(false); @@ -83,11 +113,16 @@ export async function waitForLanguageServerInit( return false; } catch (error) { logger.info(`Error checking the ${channelName} channel: ${error}, retrying...`); + // Ensure panel is closed so the next iteration starts clean + 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 +212,22 @@ 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. Call this before toggle(false) + * when the panel was opened for reading output. + */ +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 +249,84 @@ 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 itself goes stale during a workspace transition and + // getSections() returns dead nodes on every call until VS Code + // finishes rebuilding the DOM. + const { SideBarView } = require('vscode-extension-tester'); + const freshSidebar = new SideBarView(); + const contentPart = freshSidebar.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; + } + } + } catch (error: any) { + // Sidebar DOM may be stale during a workspace transition — retry + 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 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 even though + // the section header is visible — the underlying DOM node is stale but + // Chrome reports it as not-interactable rather than explicitly stale. + // Getting a fresh section reference each time breaks that loop. + const getSidebar = () => { + const { SideBarView } = require('vscode-extension-tester'); + return new SideBarView(); + }; + + return (await wait.forCondition(async () => { try { - const items = await section.getVisibleItems(); - return items && items.length > 0; + const freshSection = await getDashboardSection(getSidebar()); + 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) { @@ -638,7 +720,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 entirely. + 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 +737,63 @@ 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 that 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 */ } + + // Wait until VS Code has actually finished closing the workspace. + // On macOS CI runners the teardown can take 10+ seconds; a fixed sleep + // is insufficient. Poll until the Explorer sidebar no longer shows the + // Liberty Tools section (which disappears when no folder is open). + try { + await wait.forCondition(async () => { + try { + const { SideBarView } = require('vscode-extension-tester'); + const sidebar = new SideBarView(); + const content = sidebar.getContent(); + const sections = await content.getSections(); + for (const sec of sections) { + const title = await sec.getTitle(); + if (title === 'Liberty Tools') { + // Section still present — workspace not fully closed yet + return; + } + } + // Liberty Tools section gone — workspace closed + return true; + } catch { + // Sidebar may throw while the window is transitioning — treat as ready + // (no sidebar content means workspace is closed) + return true; + } + }, { + timeout: 30000, + pollInterval: 1000, + message: 'Workspace did not finish closing within 30 seconds' + }); + } catch { + // If the gate itself fails, fall back to a generous sleep + 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);