diff --git a/apps/vs-code-designer/src/app/commands/enableDevContainer/__test__/enableDevContainerIntegration.test.ts b/apps/vs-code-designer/src/app/commands/enableDevContainer/__test__/enableDevContainerIntegration.test.ts index c015f5be954..1195d91205a 100644 --- a/apps/vs-code-designer/src/app/commands/enableDevContainer/__test__/enableDevContainerIntegration.test.ts +++ b/apps/vs-code-designer/src/app/commands/enableDevContainer/__test__/enableDevContainerIntegration.test.ts @@ -44,7 +44,9 @@ describe('enableDevContainer - Integration Tests', () => { // Mock ext.outputChannel const { ext } = await import('../../../../extensionVariables'); + ext.designTimeInstances.clear(); ext.outputChannel = { + appendLog: vi.fn(), appendLine: vi.fn(), show: vi.fn(), } as any; @@ -56,6 +58,8 @@ describe('enableDevContainer - Integration Tests', () => { }); afterEach(async () => { + const { ext } = await import('../../../../extensionVariables'); + ext.designTimeInstances.clear(); if (tempDir) { await fse.remove(tempDir); } diff --git a/apps/vs-code-designer/src/test/ui/designerActions.test.ts b/apps/vs-code-designer/src/test/ui/designerActions.test.ts index 39beda1ac80..13be649f877 100644 --- a/apps/vs-code-designer/src/test/ui/designerActions.test.ts +++ b/apps/vs-code-designer/src/test/ui/designerActions.test.ts @@ -1295,6 +1295,24 @@ async function findAddActionElement(driver: WebDriver): Promise { + try { + await driver.executeScript('arguments[0].scrollIntoView({ block: "center", inline: "center" });', element); + await sleep(100); + } catch { + /* ignore */ + } + + try { + await driver.actions().move({ origin: element }).click().perform(); + return; + } catch (e: any) { + console.log(`[clickElementWithFallback] Actions click failed for ${description}: ${e.message}`); + } + + await driver.executeScript('arguments[0].click();', element); +} + /** * Find and click the "Add an action" menu item in the edge contextual menu. * This menu appears after clicking the "+" button on an edge. @@ -1381,7 +1399,7 @@ async function clickAddActionMenuItem(driver: WebDriver): Promise { const text = await el.getText(); if (text.toLowerCase().includes('add an action')) { console.log(`[clickAddActionMenuItem] Found "Add an action" menu item`); - await el.click(); + await clickElementWithFallback(driver, el, 'Add an action menu item'); // Poll for the discovery panel to appear await waitForDiscoveryPanel(driver); return true; @@ -2496,7 +2514,7 @@ async function getLatestRunStatus(driver: WebDriver): Promise { async function waitForRunStatusInList( driver: WebDriver, targetStatus: string, - timeoutMs = 30_000 + timeoutMs = 90_000 ): Promise<{ found: boolean; lastStatus: string }> { const t0 = Date.now(); const deadline = t0 + timeoutMs; @@ -2754,7 +2772,7 @@ describe('Designer Actions Tests', function () { continue; } try { - await addActionEl.click(); + await clickElementWithFallback(driver, addActionEl, 'add action button'); await sleep(300); await clickAddActionMenuItem(driver); actionPanelOpened = await waitForDiscoveryPanel(driver); @@ -2991,7 +3009,7 @@ describe('Designer Actions Tests', function () { try { if (addActionEl) { - await addActionEl.click(); + await clickElementWithFallback(driver, addActionEl, 'add action button'); await sleep(500); await clickAddActionMenuItem(driver); } else { diff --git a/apps/vs-code-designer/src/test/ui/designerHelpers.ts b/apps/vs-code-designer/src/test/ui/designerHelpers.ts index 9efa985224f..7f1d32c93b7 100644 --- a/apps/vs-code-designer/src/test/ui/designerHelpers.ts +++ b/apps/vs-code-designer/src/test/ui/designerHelpers.ts @@ -56,6 +56,7 @@ import { Key, InputBox, } from 'vscode-extension-tester'; +import { execSync } from 'child_process'; import { sleep, captureScreenshot, @@ -89,7 +90,7 @@ export const PROJECT_RECOGNITION_WAIT = 4_000; * the Open Designer command. Covers the dotnet build + func host start that * happens inside openDesigner for CustomCode workspaces. */ -export const DESIGNER_TAB_TIMEOUT = 30_000; +export const DESIGNER_TAB_TIMEOUT = 75_000; /** * Maximum time to wait inside the webview for the designer to finish loading. @@ -112,6 +113,33 @@ export const DEPENDENCY_VALIDATION_TIMEOUT = 300_000; // Helpers from designerActions.test.ts // =========================================================================== +function ensureRuntimeDependencyExecutablePermissions(): void { + if (process.platform !== 'linux' && process.platform !== 'darwin') { + return; + } + + const depsRoot = path.join(os.homedir(), '.azurelogicapps', 'dependencies'); + let fixedAny = false; + + for (const subDir of ['FuncCoreTools', 'NodeJs', 'DotNetSDK']) { + const binDir = path.join(depsRoot, subDir); + if (!fs.existsSync(binDir)) { + continue; + } + + try { + execSync(`chmod -R +x "${binDir}"`, { stdio: 'ignore' }); + fixedAny = true; + } catch { + /* ignore */ + } + } + + if (fixedAny) { + console.log('[depValidation] Fixed execute permissions on runtime binaries'); + } +} + /** * Ensure the local.settings.json for a workspace has WORKFLOWS_SUBSCRIPTION_ID * set to "" so that the Azure connector wizard is skipped when opening the designer. @@ -449,7 +477,7 @@ export async function openFileInEditor(workbench: Workbench, driver: WebDriver, await sleep(1000); const qInput = await driver.findElement(By.css('.quick-input-box input')); // Type parent/filename for uniqueness - await qInput.sendKeys(parentDir + '/' + expectedName); + await qInput.sendKeys(`${parentDir}/${expectedName}`); await sleep(1500); await qInput.sendKeys(Key.ENTER); await sleep(2000); @@ -499,24 +527,10 @@ export async function waitForDependencyValidation(driver: WebDriver, timeoutMs = const VALIDATION_TEXT = 'Validating Runtime Dependency'; const funcBinaryPath = path.join(os.homedir(), '.azurelogicapps', 'dependencies', 'FuncCoreTools', 'func'); - // Fix execute permissions on downloaded runtime binaries. - // The extension's download/extract doesn't set chmod +x on Linux, causing - // "/bin/sh: 1: .../func: Permission denied" when running `func host start`. - if (process.platform === 'linux' || process.platform === 'darwin') { - const depsRoot = path.join(os.homedir(), '.azurelogicapps', 'dependencies'); - for (const subDir of ['FuncCoreTools', 'NodeJs', 'DotNetSDK']) { - const binDir = path.join(depsRoot, subDir); - if (fs.existsSync(binDir)) { - try { - const { execSync } = require('child_process'); - execSync(`chmod -R +x "${binDir}"`, { stdio: 'ignore' }); - } catch { - /* ignore */ - } - } - } - console.log('[depValidation] Fixed execute permissions on runtime binaries'); - } + // The extension's download/extract can leave Linux/macOS binaries without + // execute bits. Apply this before and after validation because validation can + // reinstall FuncCoreTools while tests are running. + ensureRuntimeDependencyExecutablePermissions(); // Diagnostic: log VS Code title to see if a workspace is loaded try { @@ -531,7 +545,14 @@ export async function waitForDependencyValidation(driver: WebDriver, timeoutMs = return ( (await driver.executeScript(` var vt = ${JSON.stringify(VALIDATION_TEXT)}; - var els = document.querySelectorAll('[role="dialog"], .notification-toast, .notifications-toasts .notification-list-item'); + var els = document.querySelectorAll( + '[role="dialog"], ' + + '.notification-toast, ' + + '.notifications-toasts .notification-list-item, ' + + '.statusbar-item, ' + + '.statusbar-entry, ' + + '.part.statusbar' + ); for (var i = 0; i < els.length; i++) { if ((els[i].textContent || '').includes(vt)) return true; } @@ -555,7 +576,14 @@ export async function waitForDependencyValidation(driver: WebDriver, timeoutMs = try { const msg = await driver.executeScript(` var vt = ${JSON.stringify(VALIDATION_TEXT)}; - var els = document.querySelectorAll('[role="dialog"], .notification-toast, .notifications-toasts .notification-list-item'); + var els = document.querySelectorAll( + '[role="dialog"], ' + + '.notification-toast, ' + + '.notifications-toasts .notification-list-item, ' + + '.statusbar-item, ' + + '.statusbar-entry, ' + + '.part.statusbar' + ); for (var i = 0; i < els.length; i++) { var t = (els[i].textContent || ''); if (t.includes(vt)) return t.substring(0, 200); @@ -590,7 +618,13 @@ export async function waitForDependencyValidation(driver: WebDriver, timeoutMs = // Check if func binary already exists (validation may have completed before we started) if (fs.existsSync(funcBinaryPath)) { + if (Date.now() - t0 < 15_000) { + await sleep(2000); + continue; + } + console.log(`[depValidation] func binary already exists at ${funcBinaryPath} (${Date.now() - t0}ms)`); + ensureRuntimeDependencyExecutablePermissions(); return; } @@ -598,7 +632,7 @@ export async function waitForDependencyValidation(driver: WebDriver, timeoutMs = } if (!everAppeared && !fs.existsSync(funcBinaryPath)) { - console.log(`[depValidation] Notification never appeared and func not found — waiting for func binary on disk`); + console.log('[depValidation] Notification never appeared and func not found — waiting for func binary on disk'); } } @@ -612,6 +646,7 @@ export async function waitForDependencyValidation(driver: WebDriver, timeoutMs = // Also wait a moment for the extension to update its internal state await sleep(3000); + ensureRuntimeDependencyExecutablePermissions(); return; } @@ -663,7 +698,10 @@ export async function waitForExtensionValidationComplete(driver: WebDriver, time '.notifications-toasts .notification-list-item, ' + '[role="dialog"], ' + '.notification-toast, ' + - '.monaco-notification-list-item' + '.monaco-notification-list-item, ' + + '.statusbar-item, ' + + '.statusbar-entry, ' + + '.part.statusbar' ); for (var i = 0; i < els.length; i++) { var text = els[i].textContent || ''; @@ -703,6 +741,17 @@ export async function waitForExtensionValidationComplete(driver: WebDriver, time }; console.log('[waitForValidation] Waiting for extension dependency validation to complete...'); + let lastPermissionFixAt = 0; + + const fixPermissionsIfNeeded = (): void => { + const now = Date.now(); + if (now - lastPermissionFixAt < 10_000) { + return; + } + + ensureRuntimeDependencyExecutablePermissions(); + lastPermissionFixAt = now; + }; // Phase 1: Wait up to 15s for the first validation notification to appear. let firstSeen = false; @@ -712,6 +761,7 @@ export async function waitForExtensionValidationComplete(driver: WebDriver, time const msg = await hasValidationNotification(); if (msg) { console.log(`[waitForValidation] Validation active: "${msg}"`); + fixPermissionsIfNeeded(); firstSeen = true; break; } @@ -720,6 +770,7 @@ export async function waitForExtensionValidationComplete(driver: WebDriver, time if (!firstSeen) { console.log(`[waitForValidation] No validation notification in ${Math.round((Date.now() - t0) / 1000)}s — assuming complete`); + ensureRuntimeDependencyExecutablePermissions(); return; } @@ -734,6 +785,7 @@ export async function waitForExtensionValidationComplete(driver: WebDriver, time const msg = await hasValidationNotification(); if (msg) { lastSeenAt = Date.now(); + fixPermissionsIfNeeded(); // Log every ~15 seconds to show progress const elapsed = Math.round((Date.now() - t0) / 1000); if (elapsed % 15 < 2) { @@ -749,6 +801,7 @@ export async function waitForExtensionValidationComplete(driver: WebDriver, time if (Date.now() - t0 >= timeoutMs) { console.log(`[waitForValidation] Timeout after ${Math.round(timeoutMs / 1000)}s — proceeding anyway`); } + ensureRuntimeDependencyExecutablePermissions(); // Phase 3: Wait for design-time API (func host start) to be ready. // After validation completes, the extension starts func which takes time. @@ -1205,6 +1258,24 @@ export async function findLastAddActionElement(driver: WebDriver): Promise { + try { + await driver.executeScript('arguments[0].scrollIntoView({ block: "center", inline: "center" });', element); + await sleep(100); + } catch { + /* ignore */ + } + + try { + await driver.actions().move({ origin: element }).click().perform(); + return; + } catch (e: any) { + console.log(`[clickElementWithFallback] Actions click failed for ${description}: ${e.message}`); + } + + await driver.executeScript('arguments[0].click();', element); +} + /** * Poll until the discovery panel (recommendation panel) is visible. * Returns when the panel root or search box appears, or after timeout. @@ -1290,7 +1361,7 @@ export async function clickAddActionMenuItem(driver: WebDriver): Promise( - 'return !!(document.querySelector("iframe.webview") || document.querySelector("iframe[id*=\\"webview\\"]") || document.querySelector(\'*[id="active-frame"]\'))' - ) - .catch(() => false); - if (found) { - console.log(`[openDesignerViaExplorer] Designer webview detected for "${label}"`); - return true; - } - await sleep(500); + const found = await waitForDesignerWebviewTab(driver); + if (found) { + console.log(`[openDesignerViaExplorer] Designer webview detected for "${label}"`); + return true; } - console.log(`[openDesignerViaExplorer] Webview not detected for "${label}" within 30s`); - return false; + + ensureRuntimeDependencyExecutablePermissions(); + console.log(`[openDesignerViaExplorer] Webview not detected for "${label}" — retrying`); + break; } } catch { /* stale menu item */ diff --git a/apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts b/apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts index 6376d9d1125..3b3131adb31 100644 --- a/apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts +++ b/apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts @@ -36,6 +36,7 @@ import { countCanvasNodes, waitForNodeCountIncrease, clickAddActionMenuItem, + clickElementWithFallback, clickSaveButton, readWorkflowJson, fillCodeEditor, @@ -148,7 +149,7 @@ describe('Inline JavaScript Tests', function () { await sleep(2000); const addAction1 = await findAddActionElement(driver); assert.ok(addAction1, 'Add action element should be visible'); - await addAction1.click(); + await clickElementWithFallback(driver, addAction1, 'first add action button'); await sleep(500); await clickAddActionMenuItem(driver); assert.ok(await waitForDiscoveryPanel(driver), 'Discovery panel should open for action'); @@ -167,7 +168,7 @@ describe('Inline JavaScript Tests', function () { // Add Response action — use findLastAddActionElement to add AFTER the JS action const addAction2 = await findLastAddActionElement(driver); if (addAction2) { - await addAction2.click(); + await clickElementWithFallback(driver, addAction2, 'last add action button'); await sleep(500); await clickAddActionMenuItem(driver); } diff --git a/apps/vs-code-designer/src/test/ui/run-e2e.js b/apps/vs-code-designer/src/test/ui/run-e2e.js index 1f142f3c49b..082639deede 100644 --- a/apps/vs-code-designer/src/test/ui/run-e2e.js +++ b/apps/vs-code-designer/src/test/ui/run-e2e.js @@ -505,6 +505,8 @@ async function main() { const funcBinary = path.join(depsRoot, 'FuncCoreTools', 'func'); const dotnetBinary = path.join(depsRoot, 'DotNetSDK', 'dotnet'); const nodeBinary = path.join(depsRoot, 'NodeJs', 'node'); + const runtimeDependenciesReady = () => [funcBinary, dotnetBinary, nodeBinary].every((binaryPath) => fs.existsSync(binaryPath)); + const shouldValidateRuntimeDependencies = () => !runtimeDependenciesReady(); // Create a VS Code settings file. Called before each phase group so we can // enable dependency validation for Phase 4.1 (first run) and disable it @@ -791,8 +793,7 @@ async function main() { // Ensure VS Code and ChromeDriver are downloaded await extest.downloadCode(VSCODE_VERSION); await extest.downloadChromeDriver(VSCODE_VERSION); - // Keep dependency validation on to ensure extension activates properly - writeTestSettings({ validateDependencies: true, autoStartDesignTime: true }); + writeTestSettings({ validateDependencies: shouldValidateRuntimeDependencies(), autoStartDesignTime: true }); await prepareFreshSession('phase2-only'); const phase2Resources = getPhase2Resources(); @@ -806,8 +807,7 @@ async function main() { // Run only the new tests (phases 4.3–4.6) each in their own session await extest.downloadCode(VSCODE_VERSION); await extest.downloadChromeDriver(VSCODE_VERSION); - // Keep dependency validation on to ensure extension activates properly - writeTestSettings({ validateDependencies: true, autoStartDesignTime: true }); + writeTestSettings({ validateDependencies: shouldValidateRuntimeDependencies(), autoStartDesignTime: true }); const wsResources = getPhase2Resources(); const exits = []; @@ -975,8 +975,11 @@ async function main() { console.log(`\n⚠ Phase 4.1 exited with code ${phase1Exit} — continuing to Phase 4.2 anyway (workspaces may still have been created)`); } - // After Phase 4.1, keep dependency validation ON but the conversion phases - // don't need design-time. That change happens before phase 4.8. + // Phase 4.1 performs the dependency download/validation. Later designer + // phases use the explicit binary paths above and skip revalidation when all + // binaries are present, avoiding CI flakes where validation overwrites func + // without executable bits while the test is opening the designer. + writeTestSettings({ validateDependencies: shouldValidateRuntimeDependencies(), autoStartDesignTime: true }); console.log('\n=== Session boundary: closing createWorkspace session completely ==='); await new Promise((resolve) => setTimeout(resolve, 5000)); diff --git a/apps/vs-code-designer/src/test/ui/runHelpers.ts b/apps/vs-code-designer/src/test/ui/runHelpers.ts index 8979cad68bd..87efa3eedd9 100644 --- a/apps/vs-code-designer/src/test/ui/runHelpers.ts +++ b/apps/vs-code-designer/src/test/ui/runHelpers.ts @@ -458,7 +458,7 @@ export async function getLatestRunStatus(driver: WebDriver): Promise { export async function waitForRunStatusInList( driver: WebDriver, targetStatus: string, - timeoutMs = 30_000 + timeoutMs = 90_000 ): Promise<{ found: boolean; lastStatus: string }> { const t0 = Date.now(); const deadline = t0 + timeoutMs; diff --git a/apps/vs-code-designer/src/test/ui/statelessVariables.test.ts b/apps/vs-code-designer/src/test/ui/statelessVariables.test.ts index f136aa298c6..dbbff38b201 100644 --- a/apps/vs-code-designer/src/test/ui/statelessVariables.test.ts +++ b/apps/vs-code-designer/src/test/ui/statelessVariables.test.ts @@ -30,6 +30,7 @@ import { countCanvasNodes, waitForNodeCountIncrease, clickAddActionMenuItem, + clickElementWithFallback, clickSaveButton, readWorkflowJson, openNodeSettingsPanel, @@ -143,7 +144,7 @@ describe('Stateless Variable Tests', function () { await sleep(2000); const addAction = await findAddActionElement(driver); assert.ok(addAction, 'Add action element should exist'); - await addAction.click(); + await clickElementWithFallback(driver, addAction, 'add action button'); await sleep(500); await clickAddActionMenuItem(driver); assert.ok(await waitForDiscoveryPanel(driver), 'Discovery panel should open'); @@ -299,7 +300,7 @@ describe('Stateless Variable Tests', function () { // Add Response action — use last + button to add AFTER variable action const addAction2 = await findLastAddActionElement(driver); if (addAction2) { - await addAction2.click(); + await clickElementWithFallback(driver, addAction2, 'last add action button'); await sleep(500); await clickAddActionMenuItem(driver); }