Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -56,6 +58,8 @@ describe('enableDevContainer - Integration Tests', () => {
});

afterEach(async () => {
const { ext } = await import('../../../../extensionVariables');
ext.designTimeInstances.clear();
if (tempDir) {
await fse.remove(tempDir);
}
Expand Down
26 changes: 22 additions & 4 deletions apps/vs-code-designer/src/test/ui/designerActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,24 @@ async function findAddActionElement(driver: WebDriver): Promise<WebElement | nul
return null;
}

async function clickElementWithFallback(driver: WebDriver, element: WebElement, description: string): Promise<void> {
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.
Expand Down Expand Up @@ -1381,7 +1399,7 @@ async function clickAddActionMenuItem(driver: WebDriver): Promise<boolean> {
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;
Expand Down Expand Up @@ -2496,7 +2514,7 @@ async function getLatestRunStatus(driver: WebDriver): Promise<string> {
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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
151 changes: 105 additions & 46 deletions apps/vs-code-designer/src/test/ui/designerHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
Key,
InputBox,
} from 'vscode-extension-tester';
import { execSync } from 'child_process';
import {
sleep,
captureScreenshot,
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -531,7 +545,14 @@ export async function waitForDependencyValidation(driver: WebDriver, timeoutMs =
return (
(await driver.executeScript<boolean>(`
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;
}
Expand All @@ -555,7 +576,14 @@ export async function waitForDependencyValidation(driver: WebDriver, timeoutMs =
try {
const msg = await driver.executeScript<string>(`
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);
Expand Down Expand Up @@ -590,15 +618,21 @@ 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;
}

await sleep(2000);
}

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');
}
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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 || '';
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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) {
Expand All @@ -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.
Expand Down Expand Up @@ -1205,6 +1258,24 @@ export async function findLastAddActionElement(driver: WebDriver): Promise<WebEl
return null;
}

export async function clickElementWithFallback(driver: WebDriver, element: WebElement, description: string): Promise<void> {
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.
Expand Down Expand Up @@ -1290,7 +1361,7 @@ export async function clickAddActionMenuItem(driver: WebDriver): Promise<boolean
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;
Expand Down Expand Up @@ -1747,7 +1818,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);
Expand Down Expand Up @@ -1843,27 +1914,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<boolean>(
'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 */
Expand Down
5 changes: 3 additions & 2 deletions apps/vs-code-designer/src/test/ui/inlineJavascript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
countCanvasNodes,
waitForNodeCountIncrease,
clickAddActionMenuItem,
clickElementWithFallback,
clickSaveButton,
readWorkflowJson,
fillCodeEditor,
Expand Down Expand Up @@ -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');
Expand All @@ -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);
}
Expand Down
Loading
Loading