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
2 changes: 2 additions & 0 deletions test/e2e/content-workspace/quarto-doc/_quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
project:
title: "quarto-doc"
16 changes: 16 additions & 0 deletions test/e2e/content-workspace/quarto-doc/quarto-doc.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: "Publisher E2E Test - Quarto Document"
format: html
---

## Quarto Document

This is a simple Quarto document used for E2E testing of Posit Publisher.

The document uses **markdown only** (no Python or R engine) to keep dependencies minimal.

### Sample Content

- Item one
- Item two
- Item three
31 changes: 31 additions & 0 deletions test/e2e/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import "./workbench";
// from the with-connect GitHub Action, which handles Connect startup and bootstrapping.
Cypress.Commands.add("initializeConnect", () => {
cy.clearupDeployments();
cy.resetCredentials();
cy.setAdminCredentials();
});

Expand Down Expand Up @@ -849,5 +850,35 @@ Cypress.Commands.add("deletePCCContent", () => {
});
});

// modifyFileInContainer
// Purpose: Write file content inside the Docker container via `docker exec`.
// When to use: Tests that need to modify workspace files (e.g., redeployment after content change).
Cypress.Commands.add("modifyFileInContainer", (containerPath, content) => {
const escaped = content.replace(/\\/g, "\\\\").replace(/'/g, "'\"'\"'");
return cy
.exec(
`docker exec publisher-e2e.code-server bash -c "cat <<'MODEOF' > '${containerPath}'\n${escaped}\nMODEOF"`,
)
.then((result) => {
if (result.exitCode !== 0) {
throw new Error(`Failed to modify file in container: ${result.stderr}`);
}
});
});

// countDeploymentRecordFiles
// Purpose: Count *.toml files in .posit/publish/deployments/ for a project directory.
// When to use: Verifying redeployment reuses the same record (count stays at 1).
Cypress.Commands.add("countDeploymentRecordFiles", (projectDir) => {
const targetDir = `content-workspace/${projectDir}/.posit/publish/deployments`;
return cy
.exec(`ls -1 ${targetDir}/*.toml 2>/dev/null | wc -l`, {
failOnNonZeroExit: false,
})
.then((result) => {
return parseInt(result.stdout.trim(), 10) || 0;
});
});

// NOTE: Specific exception handling is done in support/index.js
// Do not add a catch-all here as it masks real errors.
70 changes: 70 additions & 0 deletions test/e2e/tests/deployment-logs.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (C) 2026 by Posit Software, PBC.

// Purpose: Verify deployment status and post-deployment UI elements.
// - Deploys static content.
// - Asserts deploy-status shows "Last Deployment Successful".
// - Verifies "View Content" button is visible.
// - Validates deployment record contains expected fields (dashboard_url, direct_url, deployed_at, bundle_id).
describe("Deployment Status Section", () => {
const projectDir = "static";

before(() => {
cy.initializeConnect();
});

beforeEach(() => {
cy.visit("/");
cy.getPublisherSidebarIcon().click();
cy.waitForPublisherIframe();
cy.debugIframes();
});

afterEach(() => {
cy.clearupDeployments(projectDir);
});

it("Shows deployment status and View Content button after successful deploy", () => {
cy.expectInitialPublisherState();

// Deploy static content
cy.createPCSDeployment(
projectDir,
"index.html",
"static-status-test",
(tomlFiles) => {
const { contents: config } = tomlFiles.config;
expect(config.title).to.equal("static-status-test");
expect(config.type).to.equal("html");
return tomlFiles;
},
).deployCurrentlySelected();

// Verify deploy-status shows success
cy.findInPublisherWebview('[data-automation="deploy-status"]').should(
"contain.text",
"Last Deployment Successful",
);

// Verify "View Content" button is visible
cy.publisherWebview().findByText("View Content").should("be.visible");

// Verify deployment record has expected fields
cy.getPublisherTomlFilePaths(projectDir).then((filePaths) => {
cy.loadTomlFile(filePaths.contentRecord.path).then((contentRecord) => {
expect(contentRecord.dashboard_url).to.exist;
expect(contentRecord.dashboard_url).to.be.a("string");
expect(contentRecord.dashboard_url).to.not.be.empty;

expect(contentRecord.direct_url).to.exist;
expect(contentRecord.direct_url).to.be.a("string");
expect(contentRecord.direct_url).to.not.be.empty;

expect(contentRecord.deployed_at).to.exist;
expect(contentRecord.deployed_at).to.be.a("string");
expect(contentRecord.deployed_at).to.not.be.empty;

expect(contentRecord.bundle_id).to.exist;
});
});
});
});
106 changes: 106 additions & 0 deletions test/e2e/tests/file-include-exclude.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (C) 2026 by Posit Software, PBC.

// Purpose: Verify file include/exclude toggling in the project files tree.
// - Creates a PCS deployment for fastapi-simple.
// - Unchecks requirements.txt in the project files tree.
// - Verifies the TOML config no longer includes /requirements.txt.
// - Re-checks requirements.txt.
// - Verifies the TOML config includes /requirements.txt again.
describe("File Include/Exclude Section", () => {
const projectDir = "fastapi-simple";

before(() => {
cy.initializeConnect();
});

beforeEach(() => {
cy.visit("/");
cy.getPublisherSidebarIcon().click();
cy.waitForPublisherIframe();
cy.debugIframes();
});

afterEach(() => {
cy.clearupDeployments(projectDir);
});

it("Toggle file inclusion via checkbox updates TOML config", () => {
cy.expectInitialPublisherState();

cy.createPCSDeployment(
projectDir,
"fastapi-main.py",
"fastapi-file-toggle",
(tomlFiles) => {
const { contents: config } = tomlFiles.config;
expect(config.title).to.equal("fastapi-file-toggle");
expect(config.files).to.include("/requirements.txt");
return tomlFiles;
},
);

// Step 1: Find requirements.txt in the project files tree and uncheck it
cy.retryWithBackoff(
() =>
cy
.publisherWebview()
.find('[data-automation="project-files"]')
.contains(".tree-item-title", "requirements.txt")
.closest(".tree-item")
.find('.vscode-checkbox input[type="checkbox"]'),
10,
1000,
)
.should("exist")
.should("be.visible")
.then(($checkbox) => {
// Should be checked initially
expect($checkbox.prop("checked")).to.be.true;
cy.wrap($checkbox).click({ force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.wrap($checkbox).should("not.be.checked");
});

// Step 2: Verify TOML no longer includes requirements.txt
// Wait for the async TOML update to propagate
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.getPublisherTomlFilePaths(projectDir).then((filePaths) => {
cy.loadTomlFile(filePaths.config.path).then((config) => {
expect(config.files).to.not.include("/requirements.txt");
});
});

// Step 3: Re-check requirements.txt
cy.retryWithBackoff(
() =>
cy
.publisherWebview()
.find('[data-automation="project-files"]')
.contains(".tree-item-title", "requirements.txt")
.closest(".tree-item")
.find('.vscode-checkbox input[type="checkbox"]'),
10,
1000,
)
.should("exist")
.should("be.visible")
.then(($checkbox) => {
expect($checkbox.prop("checked")).to.be.false;
cy.wrap($checkbox).click({ force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.wrap($checkbox).should("be.checked");
});

// Step 4: Verify TOML includes requirements.txt again
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.getPublisherTomlFilePaths(projectDir).then((filePaths) => {
cy.loadTomlFile(filePaths.config.path).then((config) => {
expect(config.files).to.include("/requirements.txt");
});
});
});
});
144 changes: 144 additions & 0 deletions test/e2e/tests/multi-deployment-switching.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (C) 2026 by Posit Software, PBC.

// Purpose: Verify switching between multiple deployments.
// - Creates a deployment for static content.
// - Creates a second deployment for fastapi-simple.
// - Switches back to the first deployment via the deployment picker.
// - Asserts the entrypoint-label text changes to match the selected deployment.
describe("Multi-Deployment Switching Section", () => {
before(() => {
cy.initializeConnect();
});

beforeEach(() => {
cy.visit("/");
cy.getPublisherSidebarIcon().click();
cy.waitForPublisherIframe();
cy.debugIframes();
});

afterEach(() => {
cy.clearupDeployments("static");
cy.clearupDeployments("fastapi-simple");
});

it("Switch between two deployments via picker", () => {
cy.expectInitialPublisherState();

// Step 1: Create first deployment (static)
cy.createPCSDeployment(
"static",
"index.html",
"static-multi-test",
(tomlFiles) => {
const { contents: config } = tomlFiles.config;
expect(config.title).to.equal("static-multi-test");
expect(config.type).to.equal("html");
return tomlFiles;
},
);

// Step 2: Verify first deployment is shown in entrypoint-label
cy.findInPublisherWebview('[data-automation="entrypoint-label"]').should(
"contain.text",
"static-multi-test",
);

// Step 3: Create second deployment (fastapi-simple) via the deployment picker
cy.publisherWebview()
.find(".deployment-control")
.first()
.then((control) => {
Cypress.$(control).trigger("click");
});

cy.get(".quick-input-widget").should("be.visible");
cy.get(".quick-input-titlebar").should("have.text", "Select Deployment");
cy.get(".quick-input-list")
.find('[aria-label*="Create a New Deployment"]')
.should("be.visible")
.click();

// Select entrypoint for second deployment
cy.retryWithBackoff(
() =>
cy
.get(".quick-input-widget")
.find(
'[aria-label="fastapi-simple/fastapi-main.py, Open Files"], [aria-label="fastapi-main.py, Open Files"]',
),
10,
700,
).then(($el) => {
cy.wrap($el).scrollIntoView();
cy.wrap($el).click({ force: true });
});

// Wait for title step and enter title
cy.retryWithBackoff(
() =>
cy
.get(".quick-input-widget")
.find(".quick-input-message")
.then(($m) => {
const txt = ($m.text() || "").toLowerCase();
return /title|name/.test(txt) ? $m : Cypress.$();
}),
10,
700,
);

cy.get(".quick-input-widget")
.find(".quick-input-filter input")
.then(($input) => {
const el = $input[0];
el.value = "";
el.dispatchEvent(new Event("input", { bubbles: true }));
el.value = "fastapi-multi-test";
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
});

cy.get(".quick-input-widget")
.find(".quick-input-filter input")
.should("have.value", "fastapi-multi-test");

cy.get(".quick-input-widget").type("{enter}");

// Select credential
cy.get(
'.quick-input-widget .monaco-list-row[aria-label*="admin-code-server"]',
)
.first()
.click({ force: true });

// Step 4: Verify second deployment is now selected
cy.findInPublisherWebview('[data-automation="entrypoint-label"]').should(
"contain.text",
"fastapi-multi-test",
);

// Step 5: Switch back to first deployment via picker
cy.publisherWebview()
.find(".deployment-control")
.first()
.then((control) => {
Cypress.$(control).trigger("click");
});

cy.get(".quick-input-widget").should("be.visible");
cy.get(".quick-input-titlebar").should("have.text", "Select Deployment");

// Select the first deployment (static-multi-test)
cy.get(".quick-input-list")
.find('[aria-label*="static-multi-test"]')
.should("be.visible")
.click();

// Step 6: Verify first deployment is shown again
cy.findInPublisherWebview('[data-automation="entrypoint-label"]').should(
"contain.text",
"static-multi-test",
);
});
});
Loading
Loading