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
283 changes: 115 additions & 168 deletions test/e2e/support/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
76 changes: 36 additions & 40 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 @@ -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()
Expand Down
Loading
Loading