From fdc9c0f12cff7549f56573261eb9fcab78df4acf Mon Sep 17 00:00:00 2001 From: Brian Lam Date: Wed, 29 Apr 2026 13:00:44 -0700 Subject: [PATCH 1/3] Fix enableDevContainer output channel mock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../__test__/enableDevContainerIntegration.test.ts | 4 ++++ 1 file changed, 4 insertions(+) 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); } From d2b6cfa43be8655450a3c8bfb570dba9faf85f75 Mon Sep 17 00:00:00 2001 From: Brian Lam Date: Mon, 4 May 2026 22:54:06 -0700 Subject: [PATCH 2/3] Stabilize VS Code E2E dependency validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/test/ui/designerHelpers.ts | 131 ++++++++++++------ apps/vs-code-designer/src/test/ui/run-e2e.js | 15 +- 2 files changed, 95 insertions(+), 51 deletions(-) diff --git a/apps/vs-code-designer/src/test/ui/designerHelpers.ts b/apps/vs-code-designer/src/test/ui/designerHelpers.ts index 9efa985224f..ff0af08d3c7 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. @@ -1747,7 +1800,7 @@ export async function openDesignerViaExplorer(driver: WebDriver, workflowJsonPat await sleep(1000); const quickInput = await driver.findElement(By.css('.quick-input-box input')); // Type workflow folder + filename for uniqueness in multi-workflow workspaces - await quickInput.sendKeys(label + '/workflow.json'); + await quickInput.sendKeys(`${label}/workflow.json`); await sleep(1500); await quickInput.sendKeys(Key.ENTER); await sleep(2000); @@ -1843,27 +1896,15 @@ export async function openDesignerViaExplorer(driver: WebDriver, workflowJsonPat await menuItem.click(); await sleep(3000); - // Wait for webview iframe to appear - const deadline = Date.now() + 30_000; - while (Date.now() < deadline) { - try { - await dismissAllDialogs(driver); - } catch { - /* ignore */ - } - const found = await driver - .executeScript( - '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/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)); From 9c5f6bd6d5fb0cab6d2b355bd8ddf807b47b922f Mon Sep 17 00:00:00 2001 From: Brian Lam Date: Mon, 4 May 2026 23:37:28 -0700 Subject: [PATCH 3/3] Stabilize VS Code E2E action clicks and run waits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/test/ui/designerActions.test.ts | 26 ++++++++++++++++--- .../src/test/ui/designerHelpers.ts | 20 +++++++++++++- .../src/test/ui/inlineJavascript.test.ts | 5 ++-- .../src/test/ui/runHelpers.ts | 2 +- .../src/test/ui/statelessVariables.test.ts | 5 ++-- 5 files changed, 48 insertions(+), 10 deletions(-) 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 ff0af08d3c7..7f1d32c93b7 100644 --- a/apps/vs-code-designer/src/test/ui/designerHelpers.ts +++ b/apps/vs-code-designer/src/test/ui/designerHelpers.ts @@ -1258,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. @@ -1343,7 +1361,7 @@ export async function clickAddActionMenuItem(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); }