diff --git a/test/e2e/support/selectors.js b/test/e2e/support/selectors.js index d68f2d8fe..4bbc5acf4 100644 --- a/test/e2e/support/selectors.js +++ b/test/e2e/support/selectors.js @@ -8,87 +8,86 @@ // publisherWebview // Purpose: Resolve the Publisher extension's nested iframe and return its inner body. -// - Retries, reloads once, and verifies content presence for robustness. +// - Uses waitUntil for cleaner retry logic, reloads once if needed. // When to use: Before any .findByTestId() inside the extension. Cypress.Commands.add("publisherWebview", () => { - // Wait up to 60 seconds (30 retries x 2s) for the publisher iframe, then reload once and try 10 more times. - // If still not found, log a clear error and fail the test. - function findPublisherIframe(retries = 30, hasReloaded = false) { - return cy - .get("iframe.webview.ready", { timeout: 30000 }) - .then(($iframes) => { - if (Cypress.env("DEBUG_CYPRESS") === "true") { - cy.task("print", `Found ${$iframes.length} webview.ready iframes`); - } - const $target = Cypress.$($iframes).filter((i, el) => - (el.src || "").includes("extensionId=posit.publisher"), - ); - if (Cypress.env("DEBUG_CYPRESS") === "true") { - cy.task("print", `Found ${$target.length} publisher iframes`); - } - if ($target.length > 0) { - expect( - $target.length, - "publisher webview iframe present", - ).to.be.greaterThan(0); - - // Verify iframe has actual content - const body = $target[0].contentDocument?.body; - if (body) { - const bodyText = body.innerText || ""; - const hasAutomationElements = - body.querySelector("[data-automation]") !== null; - - if (bodyText.includes("Posit") || hasAutomationElements) { - cy.log( - "Publisher iframe content verified to contain expected content", - ); - } else { - cy.log( - "WARNING: Publisher iframe found but content may not be fully loaded", - ); - cy.log(`Content sample: ${bodyText.substring(0, 100)}`); - } - } + let hasReloaded = false; - return cy.wrap($target[0].contentDocument.body); - } else if (retries > 0) { - // eslint-disable-next-line cypress/no-unnecessary-waiting - return cy - .wait(2000) - .then(() => findPublisherIframe(retries - 1, hasReloaded)); - } else if (!hasReloaded) { - cy.log("Publisher iframe not found after retries, reloading page..."); - return cy.reload().then(() => findPublisherIframe(10, true)); - } else { - cy.log( - "ERROR: Publisher iframe not found after waiting and reloading. UI may not have loaded correctly. If this happens often, check Connect service health or add a backend health check before running tests.", - ); - cy.log("Attempting extreme iframe finder as last resort..."); - // Try extreme finder as absolute last resort - return cy.findPublisherIframeExtreme().then(($iframe) => { - const iframe = $iframe[0]; - if ( - iframe && - iframe.contentDocument && - iframe.contentDocument.body - ) { - return cy.wrap(iframe.contentDocument.body); - } else { - throw new Error( - "Even extreme iframe finder failed - iframe content not accessible", - ); - } - }); + // Helper to find the publisher iframe and return its body + const findPublisherIframeBody = () => { + return cy.get("body").then(($body) => { + const $iframes = $body.find("iframe.webview.ready"); + if (Cypress.env("DEBUG_CYPRESS") === "true") { + cy.task("print", `Found ${$iframes.length} webview.ready iframes`); + } + + // Filter to find the publisher iframe using JavaScript (not CSS selector) + // because the src URL contains encoded characters + const $target = $iframes.filter((i, el) => + (el.src || "").includes("extensionId=posit.publisher"), + ); + + if ($target.length > 0) { + const outerBody = $target[0].contentDocument?.body; + if (outerBody) { + const bodyText = outerBody.innerText || ""; + const hasAutomationElements = + outerBody.querySelector("[data-automation]") !== null; + + if (bodyText.includes("Posit") || hasAutomationElements) { + cy.log("Publisher iframe content verified"); + return outerBody; + } } - }); - } - return findPublisherIframe() - .should("not.be.empty") - .then(cy.wrap) - .find("iframe#active-frame", { timeout: 30000 }) - .its("0.contentDocument.body") - .should("not.be.empty") + } + return null; + }); + }; + + // Wait for publisher iframe using cypress-wait-until + return cy + .waitUntil( + () => + findPublisherIframeBody().then((body) => { + if (body) return body; + + // If not found and haven't reloaded yet, log it + if (!hasReloaded) { + cy.log( + "Publisher iframe not found, will reload page on next attempt...", + ); + } + return false; + }), + { + timeout: 60000, + interval: 2000, + errorMsg: + "Publisher iframe not found. UI may not have loaded correctly.", + }, + ) + .then((outerBody) => { + // If still not found after timeout, try reload once + if (!outerBody && !hasReloaded) { + hasReloaded = true; + cy.log("Reloading page to find publisher iframe..."); + cy.reload(); + return cy.waitUntil(() => findPublisherIframeBody(), { + timeout: 20000, + interval: 2000, + errorMsg: "Publisher iframe not found even after page reload.", + }); + } + return outerBody; + }) + .then((outerBody) => { + // Now find the inner active-frame iframe + return cy + .wrap(outerBody) + .find("iframe#active-frame", { timeout: 30000 }) + .its("0.contentDocument.body") + .should("not.be.empty"); + }) .then((body) => { // We need to wrap in jQuery to use html() and text() const $body = Cypress.$(body); @@ -160,101 +159,49 @@ Cypress.Commands.add("publisherWebviewExtreme", () => { // and ensure the UI is stable before clicking. // When to use: Before opening the Publisher webview from VS Code UI. Cypress.Commands.add("getPublisherSidebarIcon", () => { - // Advanced Publisher icon finder that waits for extension stability - const maxAttempts = 30; - const stabilityChecks = 3; // Number of consecutive checks to confirm stability - - function waitForExtensionStability(attempt = 1) { - cy.log(`Checking extension stability (attempt ${attempt}/${maxAttempts})`); - - return cy.get("body").then(($body) => { - const bodyText = $body.text(); - const isLoading = - bodyText.includes("starting posit publisher") || - bodyText.includes("python extension loading") || - bodyText.includes("please wait") || - bodyText.includes("activating extension"); - - if (isLoading) { - cy.log("Extension still loading, waiting for stability..."); - if (attempt < maxAttempts) { - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1500); - return waitForExtensionStability(attempt + 1); - } - cy.log( - "WARNING: Extension still appears to be loading after max attempts", + const selectors = [ + 'button[aria-label*="Posit Publisher"]', + 'button[title*="Posit Publisher"]', + 'button[aria-label*="Publisher"]', + 'button[title*="Publisher"]', + ".codicon-posit-publisher-publish", + ]; + + const loadingIndicators = [ + "starting posit publisher", + "python extension loading", + "please wait", + "activating extension", + ]; + + // Wait for extension loading indicators to clear using cypress-wait-until + cy.waitUntil( + () => + cy.get("body").then(($body) => { + const bodyText = $body.text().toLowerCase(); + const isLoading = loadingIndicators.some((indicator) => + bodyText.includes(indicator), ); - } else { - cy.log("Extension loading indicators cleared"); - } - - return findAndVerifyIcon(); - }); - } - - function findAndVerifyIcon() { - const selectors = [ - 'button[aria-label*="Posit Publisher"]', - 'button[title*="Posit Publisher"]', - 'button[aria-label*="Publisher"]', - 'button[title*="Publisher"]', - ".codicon-posit-publisher-publish", - ]; - - function trySelectors(selectorIndex = 0, stabilityCount = 0) { - if (selectorIndex >= selectors.length) { - cy.log("No icon found with any selector, retrying..."); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(2000); - return findAndVerifyIcon(); - } - - const selector = selectors[selectorIndex]; - cy.log(`Trying selector: ${selector}`); - - return cy.get("body").then(($body) => { - const elements = $body.find(selector); - - if (elements.length > 0 && elements.is(":visible")) { - cy.log(`Found potentially stable icon with ${selector}`); - - // Wait and verify stability multiple times - // eslint-disable-next-line cypress/no-unnecessary-waiting - return cy.wait(1000).then(() => { - return cy.get("body").then(($bodyAfter) => { - const elementsAfter = $bodyAfter.find(selector); - - if (elementsAfter.length > 0 && elementsAfter.is(":visible")) { - if (stabilityCount >= stabilityChecks - 1) { - cy.log( - `Icon confirmed stable after ${stabilityChecks} checks`, - ); - return cy.get(selector).first().should("be.visible"); - } else { - cy.log( - `Stability check ${stabilityCount + 1}/${stabilityChecks} passed`, - ); - return trySelectors(selectorIndex, stabilityCount + 1); - } - } else { - cy.log( - "Icon disappeared during stability check, trying next selector", - ); - return trySelectors(selectorIndex + 1, 0); - } - }); - }); - } else { - return trySelectors(selectorIndex + 1, 0); + if (isLoading) { + cy.log("Extension still loading, waiting..."); } - }); - } + return !isLoading; + }), + { + timeout: 45000, + interval: 1500, + errorMsg: "Extension loading indicators did not clear in time", + }, + ); - return trySelectors(); - } + // Build a combined selector to find the icon with any of the patterns + const combinedSelector = selectors.join(", "); - return waitForExtensionStability().should("be.visible"); + // Use Cypress's built-in retry to find and verify the icon is visible + return cy + .get(combinedSelector, { timeout: 30000 }) + .first() + .should("be.visible"); }); // toggleCredentialsSection / refreshCredentials / toggleHelpSection diff --git a/test/e2e/support/sequences.js b/test/e2e/support/sequences.js index 0c16db06e..1d07b0a91 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"); } }); @@ -473,44 +471,42 @@ Cypress.Commands.add("startCredentialCreationFlow", (platform = "server") => { cy.waitForPublisherIframe(); cy.debugIframes(); - // Ensure the Credentials section is expanded (visibility-based check with retry) - const ensureCredentialsSectionExpanded = (attempt = 0) => { - if (attempt > 3) { - cy.log("Max attempts reached ensuring credentials section expansion"); - return; - } - cy.publisherWebview() - .findByTestId("publisher-credentials-section") - .then(($section) => { - const $sec = Cypress.$($section); - const isVisibleEmpty = - $sec - .find(':contains("No credentials have been added yet.")') - .filter(":visible").length > 0; - const hasVisibleBody = - $sec.find(".pane-body:visible").length > 0 || - $sec.find(".tree:visible").length > 0; - - const expanded = isVisibleEmpty || hasVisibleBody; - - if (!expanded) { - cy.log( - `Credentials section appears collapsed, expanding (attempt ${ - attempt + 1 - })`, - ); - $sec.find(".title").trigger("click"); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(200).then(() => - ensureCredentialsSectionExpanded(attempt + 1), - ); - } else { - cy.log("Credentials section expanded"); - } - }); - }; + // Ensure the Credentials section is expanded using Cypress retry assertions + cy.publisherWebview() + .findByTestId("publisher-credentials-section") + .then(($section) => { + const $sec = Cypress.$($section); + const isVisibleEmpty = + $sec + .find(':contains("No credentials have been added yet.")') + .filter(":visible").length > 0; + const hasVisibleBody = + $sec.find(".pane-body:visible").length > 0 || + $sec.find(".tree:visible").length > 0; + + const expanded = isVisibleEmpty || hasVisibleBody; + + if (!expanded) { + cy.log("Credentials section appears collapsed, expanding"); + $sec.find(".title").trigger("click"); + } + }); - ensureCredentialsSectionExpanded(); + // Wait for section to be expanded by asserting content is visible + cy.publisherWebview() + .findByTestId("publisher-credentials-section") + .should(($section) => { + const $sec = Cypress.$($section); + const isVisibleEmpty = + $sec + .find(':contains("No credentials have been added yet.")') + .filter(":visible").length > 0; + const hasVisibleBody = + $sec.find(".pane-body:visible").length > 0 || + $sec.find(".tree:visible").length > 0; + expect(isVisibleEmpty || hasVisibleBody, "Credentials section expanded") + .to.be.true; + }); // After ensuring expansion, proceed cy.publisherWebview() diff --git a/test/e2e/support/workbench.js b/test/e2e/support/workbench.js index af059c624..80e35125a 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 network to be idle before clicking to ensure backend is ready + cy.waitForNetworkIdle(500); 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") + .click(); cy.get("button").contains("Positron").click(); cy.get("button").contains("Launch").click(); @@ -401,18 +401,15 @@ Cypress.Commands.add( cy.get('.monaco-menu .actions-container[role="menu"]') .should("be.visible") .within(() => { - // Even a double-click the Posit Publisher menu item sometimes fails to open it for some reason - // TODO try to determine some other way to make this more reliable than a hardcoded wait - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1_000); - - cy.findByLabelText("Posit Publisher").dblclick(); + // Wait for the menu item to be fully rendered and actionable before clicking + cy.findByLabelText("Posit Publisher") + .should("be.visible") + .and("not.have.attr", "aria-disabled", "true") + .dblclick(); }); - // Small wait to allow the UI to settle in CI before proceeding - // TODO try to determine some other way to make this more reliable than a hardcoded wait - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1_000); + // Wait for menu to close before proceeding + cy.get('.monaco-menu .actions-container[role="menu"]').should("not.exist"); cy.debugIframes();