From dbaf3ae0dc7f44fd8c8d145da6f9644348b0d68c Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Thu, 5 Feb 2026 16:03:34 -0600 Subject: [PATCH 1/3] test: add back file and directory interaction test --- test/e2e/tests/common.cy.js | 159 +++++++++++++++++++++++++++--------- 1 file changed, 120 insertions(+), 39 deletions(-) diff --git a/test/e2e/tests/common.cy.js b/test/e2e/tests/common.cy.js index 98c005446e..d5730d4b57 100644 --- a/test/e2e/tests/common.cy.js +++ b/test/e2e/tests/common.cy.js @@ -1,49 +1,130 @@ -// Copyright (C) 2025 by Posit Software, PBC. +// Copyright (C) 2026 by Posit Software, PBC. // Purpose: Smoke-test that the Publisher extension loads, the webview is accessible, // and all primary sections appear (Deployments, Credentials, Help). // This is a fast readiness check used by other specs as a baseline. describe("Common", () => { - beforeEach(() => { - cy.visit("/"); - cy.getPublisherSidebarIcon().click(); - cy.waitForPublisherIframe(); - cy.debugIframes(); + before(() => { + cy.resetConnect(); + cy.clearupDeployments(); + cy.setAdminCredentials(); }); - it("Publisher extension can be selected and initial state", () => { - // Validates basic webview readiness and presence of core sections. - // expectInitialPublisherState ensures the main call-to-action is present. - // The retry checks reduce flakiness on CI cold starts. - cy.expectInitialPublisherState(); - - cy.retryWithBackoff( - () => - cy.findUniqueInPublisherWebview( - '[data-automation="publisher-deployment-section"]', - ), - 5, - 500, - ).should("exist"); - cy.retryWithBackoff( - () => - cy.findUniqueInPublisherWebview( - '[data-automation="publisher-credentials-section"]', - ), - 5, - 500, - ).should("exist"); - cy.debugIframes(); - cy.publisherWebview().then((body) => { - cy.task("print", body.innerHTML); + describe("Initial state", () => { + beforeEach(() => { + cy.visit("/"); + cy.getPublisherSidebarIcon().click(); + cy.waitForPublisherIframe(); + cy.debugIframes(); + }); + + it("Publisher extension can be selected and initial state", () => { + // Validates basic webview readiness and presence of core sections. + // expectInitialPublisherState ensures the main call-to-action is present. + // The retry checks reduce flakiness on CI cold starts. + cy.expectInitialPublisherState(); + + cy.retryWithBackoff( + () => + cy.findUniqueInPublisherWebview( + '[data-automation="publisher-deployment-section"]', + ), + 5, + 500, + ).should("exist"); + cy.retryWithBackoff( + () => + cy.findUniqueInPublisherWebview( + '[data-automation="publisher-credentials-section"]', + ), + 5, + 500, + ).should("exist"); + cy.debugIframes(); + cy.publisherWebview().then((body) => { + cy.task("print", body.innerHTML); + }); + cy.retryWithBackoff( + () => + cy.findUniqueInPublisherWebview( + '[data-automation="publisher-help-section"]', + ), + 5, + 500, + ).should("exist"); + }); + }); + + describe("File and directory interaction", () => { + beforeEach(() => { + cy.visit("/"); + cy.getPublisherSidebarIcon().click(); + cy.waitForPublisherIframe(); + cy.debugIframes(); + }); + + afterEach(() => { + cy.clearupDeployments(); + }); + + it("Clicking on a file name opens the file or opens directory", () => { + cy.expectInitialPublisherState(); + cy.createPCSDeployment( + "examples-shiny-python", + "app.py", + "file-click-test", + () => {}, + ); + + cy.publisherWebview() + .find('[data-automation="project-files"]') + .should("be.visible"); + + cy.publisherWebview() + .find('[data-automation="project-files"]') + .contains(".tree-item-title", "README.md") + .click(); + + cy.get(".tabs-container", { timeout: 10000 }) + .find('[aria-label="README.md"]') + .should("be.visible"); + + cy.publisherWebview() + .find('[data-automation="project-files"]') + .contains(".tree-item-title", "README.md") + .parents(".tree-item") + .first() + .find('input[type="checkbox"]') + .should("not.be.checked"); + + cy.publisherWebview() + .find('[data-automation="project-files"]') + .contains(".tree-item-title", "data") + .parents(".tree-item") + .first() + .find(".codicon") + .should("have.class", "codicon-chevron-right"); + + cy.publisherWebview() + .find('[data-automation="project-files"]') + .contains(".tree-item-title", "data") + .click(); + + cy.publisherWebview() + .find('[data-automation="project-files"]') + .contains(".tree-item-title", "data") + .parents(".tree-item") + .first() + .find(".codicon") + .should("have.class", "codicon-chevron-down"); + + cy.publisherWebview() + .find('[data-automation="project-files"]') + .contains(".tree-item-title", "data") + .parents(".tree-item") + .first() + .find('input[type="checkbox"]') + .should("not.be.checked"); }); - cy.retryWithBackoff( - () => - cy.findUniqueInPublisherWebview( - '[data-automation="publisher-help-section"]', - ), - 5, - 500, - ).should("exist"); }); }); From 79e663ecf89cb1ad10d3d06c260c425ea6084dfa Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Fri, 6 Feb 2026 10:59:01 -0600 Subject: [PATCH 2/3] ci: ensure deployments are cleaned up --- test/e2e/support/commands.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/e2e/support/commands.js b/test/e2e/support/commands.js index d277b187a4..61a791bea8 100644 --- a/test/e2e/support/commands.js +++ b/test/e2e/support/commands.js @@ -1,4 +1,5 @@ -// Copyright (C) 2025 by Posit Software, PBC. +/* eslint-disable cypress/unsafe-to-chain-command */ +// Copyright (C) 2026 by Posit Software, PBC. import "@testing-library/cypress/add-commands"; import "cypress-wait-until"; @@ -180,6 +181,7 @@ EOF`, // clearupDeployments // Purpose: Remove .posit metadata to reset deployments per test or per subdir, with exclusions. +// - Container-safe: runs deletion inside the Docker container to avoid permission issues in CqI. Cypress.Commands.add( "clearupDeployments", (subdir, excludeDirs = ["config-errors"]) => { @@ -188,14 +190,24 @@ Cypress.Commands.add( // If subdir is in the exclude list, skip deletion if (excludeDirs.includes(subdir)) return; const target = `content-workspace/${subdir}/.posit`; - cy.exec(`rm -rf ${target}`, { failOnNonZeroExit: false }); + const dockerPath = target.replace( + "content-workspace/", + "/home/coder/workspace/", + ); + cy.exec( + `docker exec publisher-e2e.code-server bash -c "rm -rf '${dockerPath}'"`, + { failOnNonZeroExit: false }, + ); } else { // Build a list of all .posit directories except excluded ones const excludePatterns = excludeDirs .map((dir) => `-not -path "*/${dir}/*"`) .join(" "); - const findCmd = `find content-workspace -type d -name ".posit" ${excludePatterns}`; - cy.exec(`${findCmd} -exec rm -rf {} +`, { failOnNonZeroExit: false }); + const findCmd = `find /home/coder/workspace -type d -name ".posit" ${excludePatterns}`; + cy.exec( + `docker exec publisher-e2e.code-server bash -c "${findCmd} -exec rm -rf {} +"`, + { failOnNonZeroExit: false }, + ); } }, ); @@ -862,3 +874,5 @@ Cypress.on("uncaught:exception", () => { // Prevent CI from failing on harmless errors return false; }); + +/* eslint-enable cypress/unsafe-to-chain-command */ From 020bcd842b9ad0b0b666a2d7ec49a0075c02cd07 Mon Sep 17 00:00:00 2001 From: Byonca Honaker Date: Tue, 10 Feb 2026 16:03:48 -0600 Subject: [PATCH 3/3] test: test to check `clearupDeployments` for CI --- test/e2e/support/commands.js | 16 +- test/e2e/tests/clearup-deployments.cy.js | 326 +++++++++++++++++++++++ 2 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 test/e2e/tests/clearup-deployments.cy.js diff --git a/test/e2e/support/commands.js b/test/e2e/support/commands.js index 61a791bea8..6b4b293092 100644 --- a/test/e2e/support/commands.js +++ b/test/e2e/support/commands.js @@ -190,24 +190,14 @@ Cypress.Commands.add( // If subdir is in the exclude list, skip deletion if (excludeDirs.includes(subdir)) return; const target = `content-workspace/${subdir}/.posit`; - const dockerPath = target.replace( - "content-workspace/", - "/home/coder/workspace/", - ); - cy.exec( - `docker exec publisher-e2e.code-server bash -c "rm -rf '${dockerPath}'"`, - { failOnNonZeroExit: false }, - ); + cy.exec(`rm -rf ${target}`, { failOnNonZeroExit: false }); } else { // Build a list of all .posit directories except excluded ones const excludePatterns = excludeDirs .map((dir) => `-not -path "*/${dir}/*"`) .join(" "); - const findCmd = `find /home/coder/workspace -type d -name ".posit" ${excludePatterns}`; - cy.exec( - `docker exec publisher-e2e.code-server bash -c "${findCmd} -exec rm -rf {} +"`, - { failOnNonZeroExit: false }, - ); + const findCmd = `find content-workspace -type d -name ".posit" ${excludePatterns}`; + cy.exec(`${findCmd} -exec rm -rf {} +`, { failOnNonZeroExit: false }); } }, ); diff --git a/test/e2e/tests/clearup-deployments.cy.js b/test/e2e/tests/clearup-deployments.cy.js new file mode 100644 index 0000000000..ecab04a20f --- /dev/null +++ b/test/e2e/tests/clearup-deployments.cy.js @@ -0,0 +1,326 @@ +// Copyright (C) 2026 by Posit Software, PBC. + +describe("clearupDeployments functionality", () => { + const testProjectDir = "fastapi-simple"; + const excludedDir = "config-errors"; + + before(() => { + cy.resetConnect(); + cy.setAdminCredentials(); + }); + + beforeEach(() => { + cy.visit("/"); + cy.getPublisherSidebarIcon().click(); + cy.waitForPublisherIframe(); + }); + + it("should cleanup deployments for a specific subdirectory", () => { + // Create a deployment in the test project directory + cy.log("Creating test deployment"); + cy.createPCSDeployment( + testProjectDir, + "fastapi-main.py", + "Test Deployment", + () => {}, + ); + + // Verify .posit directory exists before cleanup + cy.exec(`test -d content-workspace/${testProjectDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect(result.code, "Deployment .posit directory should exist").to.equal( + 0, + ); + }); + + // Run clearupDeployments for specific subdirectory + cy.log("Running clearupDeployments for specific subdirectory"); + cy.clearupDeployments(testProjectDir); + + // Verify .posit directory was removed + cy.exec(`test -d content-workspace/${testProjectDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect( + result.code, + "Deployment .posit directory should be removed", + ).to.not.equal(0); + }); + + // Verify excluded directories are still intact + cy.exec(`test -d content-workspace/${excludedDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect( + result.code, + "Excluded directory .posit should still exist", + ).to.equal(0); + }); + + // Verify in UI - deployments should be cleared + cy.log("Verifying UI shows no deployments for test project"); + cy.visit("/"); + cy.getPublisherSidebarIcon().click(); + cy.waitForPublisherIframe(); + cy.expectInitialPublisherState(); + + // Try to select deployment - should show only excluded directories + cy.publisherWebview() + .findByTestId("select-deployment") + .then((dplyPicker) => { + Cypress.$(dplyPicker).trigger("click"); + }); + + cy.get(".quick-input-widget").should("be.visible"); + cy.get(".quick-input-titlebar").should("have.text", "Select Deployment"); + + // Verify the cleaned-up deployment is not in the list + cy.get(".quick-input-widget") + .find(".quick-input-list-row") + .should(($rows) => { + const text = $rows.text(); + expect(text).to.not.include(testProjectDir); + }); + + // Close the picker + cy.get("body").type("{esc}"); + }); + + it("should cleanup all deployments except excluded directories", () => { + // Create multiple deployments + cy.log("Creating test deployments in multiple directories"); + cy.createPCSDeployment( + testProjectDir, + "fastapi-main.py", + "Test Deployment 1", + () => {}, + ); + cy.createPCSDeployment( + "static", + "index.html", + "Test Deployment 2", + () => {}, + ); + + // Verify .posit directories exist before cleanup + cy.exec(`test -d content-workspace/${testProjectDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect(result.code, "First deployment should exist").to.equal(0); + }); + + cy.exec(`test -d content-workspace/static/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect(result.code, "Second deployment should exist").to.equal(0); + }); + + // Run clearupDeployments without subdirectory (cleanup all) + cy.log("Running clearupDeployments for all directories"); + cy.clearupDeployments(); + + // Verify .posit directories were removed + cy.exec(`test -d content-workspace/${testProjectDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect(result.code, "First deployment should be removed").to.not.equal(0); + }); + + cy.exec(`test -d content-workspace/static/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect(result.code, "Second deployment should be removed").to.not.equal( + 0, + ); + }); + + // Verify excluded directories are still intact + cy.exec(`test -d content-workspace/${excludedDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect(result.code, "Excluded directory should still exist").to.equal(0); + }); + + // Verify in UI + cy.log("Verifying UI shows only excluded deployments"); + cy.visit("/"); + cy.getPublisherSidebarIcon().click(); + cy.waitForPublisherIframe(); + cy.expectInitialPublisherState(); + + cy.publisherWebview() + .findByTestId("select-deployment") + .then((dplyPicker) => { + Cypress.$(dplyPicker).trigger("click"); + }); + + cy.get(".quick-input-widget").should("be.visible"); + cy.get(".quick-input-titlebar").should("have.text", "Select Deployment"); + + // Should only see excluded directory deployments + cy.get(".quick-input-widget") + .find(".quick-input-list-row") + .should(($rows) => { + const text = $rows.text(); + expect(text).to.not.include(testProjectDir); + expect(text).to.not.include("static"); + // Should still have the error cases from config-errors + expect(text).to.include("Error"); + }); + + cy.get("body").type("{esc}"); + }); + + it("should verify find command works correctly in CI environment", () => { + // This test specifically checks if the find command used in clearupDeployments + // works correctly. In CI, there might be permission or path issues. + + // Create a test deployment + cy.log("Creating test deployment for find command verification"); + cy.createPCSDeployment( + testProjectDir, + "fastapi-main.py", + "Find Test Deployment", + () => {}, + ); + + // Manually run the find command that clearupDeployments uses + const excludePatterns = [excludedDir] + .map((dir) => `-not -path "*/${dir}/*"`) + .join(" "); + const findCmd = `find content-workspace -type d -name ".posit" ${excludePatterns}`; + + cy.log(`Testing find command: ${findCmd}`); + cy.exec(findCmd, { failOnNonZeroExit: false }).then((result) => { + cy.log(`Find command result code: ${result.code}`); + cy.log(`Find command stdout: ${result.stdout}`); + cy.log(`Find command stderr: ${result.stderr}`); + + expect(result.code, "Find command should succeed").to.equal(0); + expect(result.stdout, "Find should locate .posit directories").to.include( + ".posit", + ); + expect( + result.stdout, + "Find should not include excluded directories", + ).to.not.include(excludedDir); + + // Count how many .posit directories were found + const positDirs = result.stdout + .split("\n") + .filter((line) => line.includes(".posit")); + cy.log(`Found ${positDirs.length} .posit directories`); + expect( + positDirs.length, + "Should find at least one .posit directory", + ).to.be.greaterThan(0); + }); + + // Now run the delete command + cy.log("Testing delete command"); + cy.exec(`${findCmd} -exec rm -rf {} +`, { failOnNonZeroExit: false }).then( + (result) => { + cy.log(`Delete command result code: ${result.code}`); + cy.log(`Delete command stderr: ${result.stderr}`); + + expect(result.code, "Delete command should succeed").to.equal(0); + }, + ); + + // Verify deletion worked + cy.exec(`test -d content-workspace/${testProjectDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect( + result.code, + "Deployment should be removed after find+delete", + ).to.not.equal(0); + }); + + // Verify excluded directory still exists + cy.exec(`test -d content-workspace/${excludedDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect( + result.code, + "Excluded directory should still exist after find+delete", + ).to.equal(0); + }); + }); + + it("should handle case when no deployments exist", () => { + // First cleanup everything + cy.log("Cleaning up all deployments"); + cy.clearupDeployments(); + + // Run cleanup again - should not fail + cy.log("Running clearupDeployments when no deployments exist"); + cy.clearupDeployments(); + + // Verify we can still navigate UI normally + cy.visit("/"); + cy.getPublisherSidebarIcon().click(); + cy.waitForPublisherIframe(); + cy.expectInitialPublisherState(); + }); + + it("CI-specific: verify deployment cleanup in containerized environment", () => { + // This test verifies cleanup works when running inside Docker (CI scenario) + // Check if we're running in CI by looking for Docker + cy.exec("docker ps", { failOnNonZeroExit: false }).then((dockerResult) => { + if (dockerResult.code !== 0) { + cy.log("Not running in CI environment, skipping CI-specific test"); + return; + } + + cy.log("Running in CI environment, testing Docker-based cleanup"); + + // Create a deployment + cy.createPCSDeployment( + testProjectDir, + "fastapi-main.py", + "CI Test Deployment", + () => {}, + ); + + // Verify deployment exists from inside container + cy.exec( + `docker exec publisher-e2e.code-server bash -c "test -d /home/coder/workspace/${testProjectDir}/.posit"`, + { failOnNonZeroExit: false }, + ).then((result) => { + expect(result.code, "Deployment should exist in container").to.equal(0); + }); + + // Run cleanup + cy.clearupDeployments(testProjectDir); + + // Verify deployment was removed from inside container + cy.exec( + `docker exec publisher-e2e.code-server bash -c "test -d /home/coder/workspace/${testProjectDir}/.posit"`, + { failOnNonZeroExit: false }, + ).then((result) => { + expect( + result.code, + "Deployment should be removed from container", + ).to.not.equal(0); + }); + + // Verify from host filesystem as well + cy.exec(`test -d content-workspace/${testProjectDir}/.posit`, { + failOnNonZeroExit: false, + }).then((result) => { + expect( + result.code, + "Deployment should be removed from host", + ).to.not.equal(0); + }); + }); + }); + + afterEach(() => { + // Clean up test deployments after each test + cy.clearupDeployments(testProjectDir, [excludedDir]); + cy.clearupDeployments("static", [excludedDir]); + }); +});