diff --git a/test/e2e/config/e2e.json b/test/e2e/config/e2e.json index ac19a1a09..453ca92a6 100644 --- a/test/e2e/config/e2e.json +++ b/test/e2e/config/e2e.json @@ -1,6 +1,10 @@ { "timeouts": { "defaultCommandTimeout": 20000, - "pageLoadTimeout": 45000 + "pageLoadTimeout": 45000, + "webviewLoad": 30000, + "deployment": 60000, + "oauthFlow": 90000, + "apiRequest": 10000 } } diff --git a/test/e2e/cypress.config.js b/test/e2e/cypress.config.js index 2a41b6eeb..71756d047 100644 --- a/test/e2e/cypress.config.js +++ b/test/e2e/cypress.config.js @@ -32,7 +32,7 @@ module.exports = defineConfig({ supportFile: "support/index.js", specPattern: "tests/**/*.cy.{js,jsx,ts,tsx}", retries: { - runMode: 2, // Retry failed tests in run mode (CI) + runMode: 3, // Retry failed tests in run mode (CI) - increased from 2 for stability openMode: 0, }, defaultCommandTimeout: e2eConfig.timeouts.defaultCommandTimeout, diff --git a/test/e2e/support/commands.js b/test/e2e/support/commands.js index 11b77fb76..9d632d6ba 100644 --- a/test/e2e/support/commands.js +++ b/test/e2e/support/commands.js @@ -8,10 +8,43 @@ import "./selectors"; import "./sequences"; import "./workbench"; +// verifyServicesReady: Check that all required services are responding before tests +// This prevents flaky failures due to services not being fully ready +Cypress.Commands.add("verifyServicesReady", () => { + const connectUrl = + Cypress.env("CONNECT_SERVER_URL") || "http://localhost:3939"; + const baseUrl = Cypress.config("baseUrl"); + + // Verify Connect server is responding + cy.request({ + url: `${connectUrl}/__ping__`, + retryOnStatusCodeFailure: true, + timeout: 30000, + failOnStatusCode: false, + }).then((response) => { + if (response.status !== 200) { + cy.log(`WARNING: Connect server returned status ${response.status}`); + } + }); + + // Verify code-server is responding + cy.request({ + url: baseUrl, + retryOnStatusCodeFailure: true, + timeout: 30000, + failOnStatusCode: false, + }).then((response) => { + if (response.status !== 200) { + cy.log(`WARNING: code-server returned status ${response.status}`); + } + }); +}); + // initializeConnect: Simple initialization for use with with-connect action // The API key is passed via CYPRESS_BOOTSTRAP_ADMIN_API_KEY environment variable // from the with-connect GitHub Action, which handles Connect startup and bootstrapping. Cypress.Commands.add("initializeConnect", () => { + cy.verifyServicesReady(); cy.clearupDeployments(); cy.setAdminCredentials(); }); @@ -456,12 +489,15 @@ Cypress.Commands.add( if (result && result.length) { return result; } else if (attempt < maxAttempts) { - // Cap max delay at 5 seconds to prevent exponential backoff from causing very long waits - const delay = Math.min(initialDelay * Math.pow(2, attempt - 1), 5000); + // Cap max delay at 3 seconds to prevent exponential backoff from causing very long waits + const delay = Math.min(initialDelay * Math.pow(2, attempt - 1), 3000); + cy.wait(delay); return tryFn(); } else { - throw new Error("Element not found after retries with backoff"); + throw new Error( + `Element not found after ${maxAttempts} retries with backoff`, + ); } }); } diff --git a/test/e2e/support/selectors.js b/test/e2e/support/selectors.js index d68f2d8fe..c93b72bb8 100644 --- a/test/e2e/support/selectors.js +++ b/test/e2e/support/selectors.js @@ -274,6 +274,10 @@ Cypress.Commands.add("toggleCredentialsSection", () => { }); Cypress.Commands.add("refreshCredentials", () => { + // Intercept the credentials API call before triggering refresh + // Match both /api/credentials and /credentials endpoints + cy.intercept("GET", "**/credentials**").as("credentialsRefresh"); + // Robustly locate the credentials section inside the webview before interacting cy.retryWithBackoff( () => @@ -297,8 +301,17 @@ Cypress.Commands.add("refreshCredentials", () => { } }); - // Wait for credential refresh API call to complete - cy.waitForNetworkIdle(500); + // Wait for credential refresh API call to complete, with additional network idle as backup + cy.wait("@credentialsRefresh", { timeout: 10000 }).then((interception) => { + if (interception.response?.statusCode !== 200) { + cy.log( + `Credentials refresh returned status: ${interception.response?.statusCode}`, + ); + } + }); + + // Additional brief wait for UI to update after API response + cy.waitForNetworkIdle(200); }); Cypress.Commands.add("toggleHelpSection", () => { diff --git a/test/e2e/support/sequences.js b/test/e2e/support/sequences.js index 0c16db06e..171ff98d4 100644 --- a/test/e2e/support/sequences.js +++ b/test/e2e/support/sequences.js @@ -323,9 +323,7 @@ Cypress.Commands.add( const isChecked = $checkbox.prop("checked"); if (!isChecked) { cy.wrap($checkbox).click({ force: true }); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(500); // Small wait after click - // Verify the click worked + // Verify the click worked - Cypress will retry until checked cy.wrap($checkbox).should("be.checked"); } }); @@ -372,6 +370,10 @@ Cypress.Commands.add( // Purpose: Click the Deploy button, wait for toasts to clear, and confirm success. // When to use: Immediately after createPCSDeployment/createPCCDeployment when deployment should succeed. Cypress.Commands.add("deployCurrentlySelected", () => { + // Intercept deploy API calls for better synchronization + cy.intercept("POST", "**/api/publish/**").as("publishRequest"); + cy.intercept("GET", "**/api/deployments/**").as("deploymentsCheck"); + // Wait for any pending network activity to settle before deploying cy.waitForNetworkIdle(500); @@ -381,6 +383,10 @@ Cypress.Commands.add("deployCurrentlySelected", () => { .then((dplyBtn) => { Cypress.$(dplyBtn).trigger("click"); }); + + // Wait for the publish request to start + cy.wait("@publishRequest", { timeout: 30000 }); + // Wait for deploying message to finish cy.get(".notifications-toasts", { timeout: 30_000 }) .should("be.visible") @@ -500,10 +506,12 @@ Cypress.Commands.add("startCredentialCreationFlow", (platform = "server") => { })`, ); $sec.find(".title").trigger("click"); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(200).then(() => - ensureCredentialsSectionExpanded(attempt + 1), - ); + // Wait for the section to expand by checking for visible content + cy.publisherWebview() + .findByTestId("publisher-credentials-section") + .find(".pane-body, .tree, :contains('No credentials')") + .should("be.visible") + .then(() => ensureCredentialsSectionExpanded(attempt + 1)); } else { cy.log("Credentials section expanded"); } diff --git a/test/e2e/support/workbench.js b/test/e2e/support/workbench.js index af059c624..9bc8ea2ae 100644 --- a/test/e2e/support/workbench.js +++ b/test/e2e/support/workbench.js @@ -243,13 +243,13 @@ Cypress.Commands.add("startPositronSession", () => { // Start a Positron session // TODO remove this workaround for "All types of sessions are disabled" error after Workbench 2025.12.0 is released + // Wait for the button to be fully interactive before clicking cy.get("button") .contains("New Session") .should("be.visible") - .and("be.enabled"); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(500); - cy.get("button").contains("New Session").click(); + .and("be.enabled") + .and("not.have.attr", "aria-busy", "true") + .click(); cy.get("button").contains("Positron").click(); cy.get("button").contains("Launch").click();