Skip to content
Draft
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
6 changes: 5 additions & 1 deletion test/e2e/config/e2e.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"timeouts": {
"defaultCommandTimeout": 20000,
"pageLoadTimeout": 45000
"pageLoadTimeout": 45000,
"webviewLoad": 30000,
"deployment": 60000,
"oauthFlow": 90000,
"apiRequest": 10000
}
}
2 changes: 1 addition & 1 deletion test/e2e/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
42 changes: 39 additions & 3 deletions test/e2e/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down Expand Up @@ -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`,
);
}
});
}
Expand Down
17 changes: 15 additions & 2 deletions test/e2e/support/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() =>
Expand All @@ -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", () => {
Expand Down
22 changes: 15 additions & 7 deletions test/e2e/support/sequences.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
});
Expand Down Expand Up @@ -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);

Expand All @@ -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")
Expand Down Expand Up @@ -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");
}
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/support/workbench.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading