diff --git a/.evergreen/scripts/constants.js b/.evergreen/scripts/constants.js index 5f2aa23190..5d4c28feb5 100644 --- a/.evergreen/scripts/constants.js +++ b/.evergreen/scripts/constants.js @@ -54,8 +54,8 @@ const MARKDOWN_EXT = ".md"; const IGNORED_FILE_EXTENSIONS = new Set([MARKDOWN_EXT]); -const CYPRESS_PARALLEL_COUNT = 2; -const PLAYWRIGHT_PARALLEL_COUNT = 2; +const CYPRESS_PARALLEL_COUNT = 1; +const PLAYWRIGHT_PARALLEL_COUNT = 3; export { Tasks, diff --git a/.evergreen/scripts/generate-parallel-e2e-tasks.js b/.evergreen/scripts/generate-parallel-e2e-tasks.js index 355a27d397..d7ee162a88 100644 --- a/.evergreen/scripts/generate-parallel-e2e-tasks.js +++ b/.evergreen/scripts/generate-parallel-e2e-tasks.js @@ -57,14 +57,21 @@ const getPlaywrightDirSizes = (dirPath) => { const makeRelative = (d) => d.substring(d.indexOf("playwright")); const dirSizeMap = {}; - const dirs = getDirs(dirPath); - dirs.forEach((dir) => { - const size = getDirSize(dir); - dirSizeMap[`${makeRelative(dir)}/*.spec.ts`] = size; - }); - // Specifically add root tests with no wildcard directory regex - dirSizeMap[`${makeRelative(dirPath)}/*.spec.ts`] = getDirSize(dirPath, false); + const collectDirSizes = (currentPath, isRoot) => { + const dirs = getDirs(currentPath); + const dirSize = getDirSize(currentPath, false); + // Add an entry for specs directly in this directory (always for root, only when non-empty for subdirs) + if (isRoot || dirSize > 0) { + dirSizeMap[`${makeRelative(currentPath)}/*.spec.ts`] = dirSize; + } + // Recurse into subdirectories. + dirs.forEach((dir) => { + collectDirSizes(dir, false); + }); + }; + + collectDirSizes(dirPath, true); return dirSizeMap; }; diff --git a/apps/spruce/cypress/integration/projectSettings/access.ts b/apps/spruce/cypress/integration/projectSettings/access.ts deleted file mode 100644 index edb32df24a..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/access.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - getProjectSettingsRoute, - project, - ProjectSettingsTabRoutes, - projectUseRepoEnabled, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Access page", () => { - const origin = getProjectSettingsRoute( - projectUseRepoEnabled, - ProjectSettingsTabRoutes.Access, - ); - beforeEach(() => { - cy.visit(origin); - saveButtonEnabled(false); - cy.dataCy("default-to-repo-button") - .should("be.visible") - .should("be.enabled") - .should("not.have.attr", "aria-disabled", "true"); - }); - - it("Changing settings and clicking the save button produces a success toast and the changes are persisted", () => { - cy.contains("label", "Unrestricted").click(); - cy.getInputByLabel("Unrestricted").should("be.checked"); - // Input and save username - cy.contains("Add Username").click(); - cy.getInputByLabel("Username").as("usernameInput"); - cy.get("@usernameInput").type("admin"); - cy.get("@usernameInput").should("have.value", "admin").should("be.visible"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - // Assert persistence - cy.reload(); - cy.getInputByLabel("Username").as("usernameInput"); - cy.get("@usernameInput").should("have.value", "admin").should("be.visible"); - // Delete a username - cy.dataCy("delete-item-button").should("be.visible").click(); - cy.get("@usernameInput").should("not.exist"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - // Assert persistence - cy.reload(); - saveButtonEnabled(false); - cy.get("@usernameInput").should("not.exist"); - }); - - it("Clicking on 'Default to Repo on Page' selects the 'Default to repo (unrestricted)' radio box and produces a success banner", () => { - cy.dataCy("default-to-repo-button").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("default-to-repo-button").click(); - cy.getInputByLabel('Type "confirm" to confirm your action').type("confirm"); - cy.dataCy("default-to-repo-modal").contains("Confirm").click(); - cy.validateToast("success", "Successfully defaulted page to repo"); - cy.getInputByLabel("Default to repo (unrestricted)").should("be.checked"); - }); - - it("Submitting an invalid admin username produces an error toast", () => { - cy.visit(getProjectSettingsRoute(project, ProjectSettingsTabRoutes.Access)); - cy.contains("Add Username").click(); - cy.getInputByLabel("Username").type("mongodb_user"); - clickSaveAndConfirmDiff(); - cy.validateToast("error", "There was an error saving the project"); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/admin_actions.ts b/apps/spruce/cypress/integration/projectSettings/admin_actions.ts deleted file mode 100644 index 03389ff8c9..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/admin_actions.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { getProjectSettingsRoute, project } from "./constants"; - -describe("projectSettings/admin_actions", () => { - describe("Duplicating a project", () => { - const destination = getProjectSettingsRoute(project); - - it("Successfully duplicates a project with warnings", () => { - cy.visit(destination); - cy.dataCy("new-project-button").click(); - cy.dataCy("new-project-menu").should("be.visible"); - cy.dataCy("copy-project-button").click(); - cy.dataCy("copy-project-modal").should("be.visible"); - cy.dataCy("performance-tooling-banner").should("be.visible"); - - cy.dataCy("project-name-input").type("copied-project"); - - cy.contains("button", "Duplicate").click(); - cy.validateToast( - "warning", - "The project was duplicated but may not be fully enabled", - ); - - cy.url().should("include", "copied-project"); - }); - }); - - describe("Creating a new project and deleting it", () => { - it("Successfully creates a new project and then deletes it", () => { - // Create project - cy.visit(getProjectSettingsRoute(project)); - cy.dataCy("new-project-button").click(); - cy.dataCy("new-project-menu").should("be.visible"); - cy.dataCy("create-project-button").click(); - cy.dataCy("create-project-modal").should("be.visible"); - cy.dataCy("performance-tooling-banner").should("be.visible"); - - cy.dataCy("project-name-input").type("my-new-project"); - cy.dataCy("new-owner-select").contains("evergreen-ci"); - cy.dataCy("new-repo-input").should("have.value", "spruce"); - cy.dataCy("new-repo-input").clear(); - cy.dataCy("new-repo-input").type("new-repo"); - - cy.contains("button", "Create project").click(); - cy.validateToast( - "success", - "Successfully created the project “my-new-project”", - ); - - cy.url().should("include", "my-new-project"); - - // Delete project - cy.visit(getProjectSettingsRoute("my-new-project")); - cy.dataCy("attach-repo-button").click(); - cy.dataCy("attach-repo-modal") - .find("button") - .contains("Attach") - .parent() - .click(); - cy.validateToast("success", "Successfully attached to repo"); - - cy.dataCy("delete-project-button").scrollIntoView(); - cy.dataCy("delete-project-button").click(); - cy.dataCy("delete-project-modal") - .find("button") - .contains("Delete") - .parent() - .click(); - cy.validateToast("success", "The project “my-new-project” was deleted."); - - cy.reload(); - cy.validateToast( - "error", - "There was an error loading the project my-new-project", - ); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/attaching_to_repo.ts b/apps/spruce/cypress/integration/projectSettings/attaching_to_repo.ts deleted file mode 100644 index 8722cb5aaa..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/attaching_to_repo.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getProjectSettingsRoute, project } from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Attaching Spruce to a repo", () => { - const origin = getProjectSettingsRoute(project); - - beforeEach(() => { - cy.visit(origin); - }); - - it("Saves and attaches new repo and shows warnings on the Github page", () => { - cy.dataCy("repo-input").as("repoInput").clear(); - cy.get("@repoInput").type("evergreen"); - cy.dataCy("attach-repo-button").should( - "have.attr", - "aria-disabled", - "true", - ); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("attach-repo-button").click(); - cy.dataCy("attach-repo-modal").contains("button", "Attach").click(); - cy.validateToast("success", "Successfully attached to repo"); - cy.dataCy("navitem-github-commitqueue").click(); - cy.dataCy("pr-testing-enabled-radio-box") - .prev() - .dataCy("warning-banner") - .should("exist"); - cy.dataCy("manual-pr-testing-enabled-radio-box") - .prev() - .dataCy("warning-banner") - .should("exist"); - cy.dataCy("github-checks-enabled-radio-box").prev().should("not.exist"); - cy.dataCy("cq-card").dataCy("warning-banner").should("exist"); - cy.dataCy("cq-enabled-radio-box").within(($el) => { - cy.wrap($el).getInputByLabel("Enabled").parent().click(); - }); - cy.dataCy("cq-card").dataCy("error-banner").should("exist"); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/defaulting_to_repo.ts b/apps/spruce/cypress/integration/projectSettings/defaulting_to_repo.ts deleted file mode 100644 index 932112c9e7..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/defaulting_to_repo.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { - getProjectSettingsRoute, - getRepoSettingsRoute, - project, - ProjectSettingsTabRoutes, - projectUseRepoEnabled, - repo, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Project Settings when defaulting to repo", () => { - const origin = getProjectSettingsRoute(projectUseRepoEnabled); - - beforeEach(() => { - cy.visit(origin); - }); - - describe("General Settings page", () => { - it("Save button is disabled on load and shows a link to the repo", () => { - saveButtonEnabled(false); - cy.dataCy("attached-repo-link") - .should("have.attr", "href") - .and("eq", `/${getRepoSettingsRoute(repo)}`); - }); - - it("Preserves edits to the form when navigating between settings tabs and does not show a warning modal", () => { - cy.dataCy("spawn-host-input").should("have.value", "/path"); - cy.dataCy("spawn-host-input").type("/test"); - saveButtonEnabled(); - cy.dataCy("navitem-access").click(); - cy.dataCy("navigation-warning-modal").should("not.exist"); - cy.dataCy("navitem-general").click(); - cy.dataCy("spawn-host-input").should("have.value", "/path/test"); - saveButtonEnabled(); - }); - - it("Shows a 'Default to Repo' button on page", () => { - cy.dataCy("default-to-repo-button").should("exist"); - }); - - it("Shows only two radio boxes even when rendering a project that inherits from repo", () => { - cy.dataCy("enabled-radio-box").children().should("have.length", 2); - }); - - it("Does not default to repo value for display name", () => { - cy.dataCy("display-name-input").should("not.have.attr", "placeholder"); - }); - - it("Shows a navigation warning modal that lists the general page when navigating away from project settings", () => { - cy.dataCy("spawn-host-input").type("/test"); - saveButtonEnabled(); - cy.contains("My Patches").click(); - cy.dataCy("navigation-warning-modal").should("be.visible"); - cy.dataCy("unsaved-pages").within(() => { - cy.get("li").should("have.length", 1); - }); - cy.get("body").type("{esc}"); - }); - - it("Shows the repo value for Batch Time", () => { - cy.dataCy("batch-time-input").should("have.attr", "placeholder"); - }); - - it("Clicking on save button should show a success toast", () => { - cy.dataCy("spawn-host-input").type("/test"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); - - it("Saves when batch time is updated", () => { - cy.dataCy("batch-time-input").clear(); - cy.dataCy("batch-time-input").type("12"); - clickSaveAndConfirmDiff(); - cy.dataCy("batch-time-input").should("have.value", 12); - cy.validateToast("success", "Successfully updated project"); - // Check if clearing attached project defaults batchtime to repo value - cy.dataCy("batch-time-input").clear(); - clickSaveAndConfirmDiff(); - cy.dataCy("batch-time-input") - .invoke("attr", "placeholder") - .should("equal", "60 (Default from repo)"); - cy.validateToast("success", "Successfully updated project"); - // Update repo batch time and check if project batch time placeholder is updated - cy.dataCy("attached-repo-link").click(); - cy.dataCy("batch-time-input").should("have.value", 60); - cy.dataCy("batch-time-input").clear(); - clickSaveAndConfirmDiff(); - cy.dataCy("batch-time-input").should("have.value", 0); - cy.validateToast("success", "Successfully updated repo"); - cy.visit(origin); - cy.dataCy("batch-time-input") - .invoke("attr", "placeholder") - .should("equal", "0 (Default from repo)"); - // Check if clearing project batch time saves as 0 instead of null - cy.visit(getProjectSettingsRoute(project)); - cy.dataCy("batch-time-input").should("have.value", 60); - cy.dataCy("batch-time-input").clear(); - clickSaveAndConfirmDiff(); - cy.dataCy("batch-time-input").should("have.value", 0); - cy.validateToast("success", "Successfully updated project"); - }); - }); - - describe("Variables page", () => { - beforeEach(() => { - cy.dataCy("navitem-variables").click(); - saveButtonEnabled(false); - }); - - it("Successfully saves variables and then promotes them using the promote variables modal", () => { - // Save variables - cy.dataCy("add-button").should("be.visible").click(); - cy.dataCy("var-name-input").type("a"); - cy.dataCy("var-value-input").type("1"); - cy.dataCy("var-description-input").type("Description for variable a"); - cy.contains("label", "Private").click(); - - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").first().type("b"); - cy.dataCy("var-value-input").first().type("2"); - cy.dataCy("var-description-input") - .first() - .type("Description for variable b"); - - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").first().type("c"); - cy.dataCy("var-value-input").first().type("3"); - cy.dataCy("var-description-input") - .first() - .type("Description for variable c"); - - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - // Promote variables - cy.dataCy("promote-vars-modal").should("not.exist"); - cy.dataCy("promote-vars-button").click(); - cy.dataCy("promote-vars-modal").should("be.visible"); - cy.dataCy("promote-var-checkbox").first().check({ force: true }); - cy.contains("button", "Move 1 variable").click(); - cy.validateToast("success", "Successfully moved variables to repo"); - }); - }); - - describe("GitHub page", () => { - beforeEach(() => { - cy.dataCy("navitem-github-commitqueue").click(); - }); - - it("Should not have the save button enabled on load", () => { - saveButtonEnabled(false); - }); - - it("Allows overriding repo patch definitions", () => { - cy.dataCy("pr-testing-enabled-radio-box") - .find("label") - .should("have.length", 3); - cy.contains("label", "Override Repo Patch Definition").click(); - cy.dataCy("error-banner") - .contains( - "A GitHub Patch Definition must be specified for this feature to run.", - ) - .should("exist"); - cy.contains("button", "Add Patch Definition").click(); - - cy.dataCy("variant-input-control") - .find("button") - .contains("Regex") - .click(); - cy.dataCy("variant-input").first().type(".*"); - saveButtonEnabled(false); - // Persist input value when toggling inputs - cy.contains("button", "Tags").first().click(); - cy.contains("button", "Regex").first().click(); - cy.dataCy("variant-input").should("have.value", ".*"); - cy.dataCy("task-input-control").find("button").contains("Regex").click(); - cy.dataCy("task-input").first().type(".*"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); - - it("Shows a warning banner when a commit check definition does not exist", () => { - cy.contains("Default to repo (disabled)").should("be.visible"); - cy.dataCy("github-checks-enabled-radio-box").scrollIntoView(); - cy.dataCy("github-checks-enabled-radio-box") - .contains("label", "Enabled") - .click(); - - cy.dataCy("warning-banner") - .contains( - "This feature will only run if a Commit Check Definition is defined in the project or repo.", - ) - .should("exist"); - }); - - it("Disables Authorized Users section based on repo settings", () => { - cy.contains("Authorized Users").should("not.exist"); - cy.contains("Authorized Teams").should("not.exist"); - }); - - it("Defaults to overriding repo since a patch definition is defined", () => { - cy.dataCy("cq-override-radio-box") - .find("input") - .first() - .should("be.checked"); - }); - - it("Shows the existing patch definition", () => { - cy.dataCy("variant-input").last().should("have.value", "^ubuntu1604$"); - cy.dataCy("task-input") - .last() - .should("have.value", "^smoke-test-endpoints$"); - }); - - it("Returns an error on save because no commit check definitions are defined", () => { - // Ensure page has loaded - cy.dataCy("pr-testing-enabled-radio-box") - .contains("label", "Default to repo (enabled)") - .should("be.visible"); - cy.dataCy("pr-testing-enabled-radio-box") - .contains("label", "Disabled") - .click(); - cy.dataCy("manual-pr-testing-enabled-radio-box") - .contains("label", "Disabled") - .click(); - cy.dataCy("github-checks-enabled-radio-box") - .contains("label", "Enabled") - .click(); - clickSaveAndConfirmDiff(); - cy.validateToast("error", "There was an error saving the project"); - }); - - it("Defaults to repo and shows the repo's disabled patch definition", () => { - cy.dataCy("accordion-toggle") - .contains("Repo Patch Definition 1") - .should("not.exist"); - // Save a repo patch definition - cy.visit(getRepoSettingsRoute(repo)); - cy.dataCy("navitem-github-commitqueue").click(); - cy.contains("button", "Add Patch Definition").click(); - cy.dataCy("variant-tags-input").first().type("vtag"); - cy.dataCy("task-tags-input").first().type("ttag"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - cy.visit(origin); - cy.dataCy("navitem-github-commitqueue").click(); - cy.dataCy("default-to-repo-button").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("default-to-repo-button").click(); - cy.dataCy("default-to-repo-modal").should("be.visible"); - cy.getInputByLabel('Type "confirm" to confirm your action').type( - "confirm", - ); - cy.dataCy("default-to-repo-modal").contains("Confirm").click(); - cy.validateToast("success", "Successfully defaulted page to repo"); - cy.dataCy("accordion-toggle").scrollIntoView(); - cy.dataCy("accordion-toggle") - .should("be.visible") - .contains("Repo Patch Definition 1"); - }); - }); - - describe("Patch Aliases page", () => { - beforeEach(() => { - cy.dataCy("navitem-patch-aliases").click(); - saveButtonEnabled(false); - }); - - it("Defaults to repo patch aliases", () => { - cy.getInputByLabel("Default to Repo Patch Aliases").should( - "have.attr", - "checked", - ); - }); - - it("Patch aliases added before defaulting to repo patch aliases are cleared", () => { - // Override repo patch alias and add a patch alias. - cy.contains("label", "Override Repo Patch Aliases") - .should("be.visible") - .click(); - cy.getInputByLabel("Override Repo Patch Aliases").should( - "have.attr", - "aria-checked", - "true", - ); - saveButtonEnabled(false); - cy.dataCy("add-button") - .contains("Add Patch Alias") - .parent() - .click({ force: true }); - saveButtonEnabled(false); - cy.dataCy("alias-input").type("my overriden alias name"); - cy.dataCy("variant-tags-input").first().type("alias variant tag 2"); - cy.dataCy("task-tags-input").first().type("alias task tag 2"); - cy.dataCy("add-button").contains("Add Task Tag").parent().click(); - cy.dataCy("task-tags-input").first().type("alias task tag 3"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - // Default to repo patch alias - cy.contains("label", "Default to Repo Patch Aliases").click(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - saveButtonEnabled(false); - // Aliases are cleared - cy.contains("label", "Override Repo Patch Aliases").click(); - cy.dataCy("alias-row").should("have.length", 0); - }); - }); - - describe("Virtual Workstation page", () => { - beforeEach(() => { - cy.dataCy("navitem-virtual-workstation").click(); - }); - - it("Enable git clone", () => { - cy.contains("label", "Enabled").click(); - cy.getInputByLabel("Enabled").should("be.checked"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); - it("Add commands", () => { - // Repo commands should be visible on project page based on button selection - cy.getInputByLabel("Default to repo (disabled)").should("be.checked"); - cy.dataCy("command-row").should("not.exist"); - cy.dataCy("attached-repo-link").click(); - cy.location("pathname").should( - "equal", - `/${getRepoSettingsRoute(repo, ProjectSettingsTabRoutes.VirtualWorkstation)}`, - ); - cy.contains("button", "Add Command").click(); - cy.dataCy("command-input").type("a repo command"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - // Go to project page - cy.visit(origin); - cy.dataCy("navitem-virtual-workstation").click(); - cy.dataCy("command-row") - .contains("textarea", "a repo command") - .should("have.attr", "aria-disabled", "true"); - // Override commands, add a command, default to repo then show override commands are cleared - cy.contains("label", "Override Repo Commands") - .as("overrideRepoCommandsButton") - .click(); - cy.dataCy("command-row").should("not.exist"); - cy.contains("button", "Add Command").click(); - cy.dataCy("command-input").type("a project command"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("command-row") - .contains("textarea", "a project command") - .should("have.attr", "aria-disabled", "false"); - cy.contains("label", "Default to Repo Commands").click(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("command-row") - .contains("textarea", "a repo command") - .should("have.attr", "aria-disabled", "true"); - cy.get("@overrideRepoCommandsButton").click(); - cy.dataCy("command-row").should("not.exist"); - }); - - it("Allows overriding without adding a command", () => { - cy.contains("label", "Override Repo Commands").click(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.getInputByLabel("Override Repo Commands").should("be.checked"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/github_app_settings.ts b/apps/spruce/cypress/integration/projectSettings/github_app_settings.ts deleted file mode 100644 index 7abf3b80dc..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/github_app_settings.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("GitHub app settings", () => { - const destination = getProjectSettingsRoute( - "spruce", - ProjectSettingsTabRoutes.GithubAppSettings, - ); - const selectMenu = "[role='listbox']"; - const permissionGroups = { - all: "All app permissions", - readPRs: "Read Pull Requests", - writeIssues: "Write Issues", - }; - - beforeEach(() => { - cy.visit(destination); - // Wait for page content to finish loading. - cy.contains("Token Permission Restrictions"); - }); - - it("save button should be disabled by default", () => { - saveButtonEnabled(false); - }); - - it("should be able to replace app credentials", () => { - // Replace button should be visible when app is defined. - cy.dataCy("replace-app-credentials-button").should("be.visible"); - cy.dataCy("replace-app-credentials-button").click(); - cy.dataCy("replace-github-credentials-modal").should("be.visible"); - - // Replace button in modal should be disabled without input. - cy.dataCy("replace-github-credentials-modal") - .find("button") - .contains("Replace") - .parent() - .should("have.attr", "aria-disabled", "true"); - - // Fill in new credentials. - cy.dataCy("replace-app-id-input").type("99999"); - cy.dataCy("replace-private-key-input").type("new-private-key"); - - // Replace button should now be enabled. - cy.dataCy("replace-github-credentials-modal") - .find("button") - .contains("Replace") - .parent() - .should("not.have.attr", "aria-disabled", "true"); - - cy.dataCy("replace-github-credentials-modal") - .find("button") - .contains("Replace") - .parent() - .click(); - cy.validateToast( - "success", - "GitHub app credentials were successfully replaced.", - ); - }); - - it("should be able to save different permission groups for requesters, then return to defaults", () => { - cy.dataCy("permission-group-input").should("have.length", 8); - cy.dataCy("permission-group-input").eq(0).as("permission-group-input-0"); - cy.dataCy("permission-group-input").eq(4).as("permission-group-input-4"); - - // Save different permission groups. - cy.get("@permission-group-input-0").click(); - cy.get(selectMenu) - .first() - .within(() => { - cy.contains(permissionGroups.readPRs).click(); - }); - cy.get("@permission-group-input-4").click(); - cy.get(selectMenu) - .first() - .within(() => { - cy.contains(permissionGroups.writeIssues).click(); - }); - cy.dataCy("save-settings-button").scrollIntoView(); - saveButtonEnabled(true); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - - // Changes should persist on the page. - cy.reload(); - cy.get("@permission-group-input-0").contains(permissionGroups.readPRs); - cy.get("@permission-group-input-4").contains(permissionGroups.writeIssues); - - // Return to and save defaults. - cy.get("@permission-group-input-0").click(); - cy.get(selectMenu) - .first() - .within(() => { - cy.contains(permissionGroups.all).click(); - }); - cy.get("@permission-group-input-4").click(); - cy.get(selectMenu) - .first() - .within(() => { - cy.contains(permissionGroups.all).click(); - }); - cy.dataCy("save-settings-button").scrollIntoView(); - saveButtonEnabled(true); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/github_permission_groups.ts b/apps/spruce/cypress/integration/projectSettings/github_permission_groups.ts deleted file mode 100644 index af9ca254d1..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/github_permission_groups.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("GitHub permission groups", () => { - const destination = getProjectSettingsRoute( - "logkeeper", - ProjectSettingsTabRoutes.GithubPermissionGroups, - ); - beforeEach(() => { - cy.visit(destination); - // Wait for page content to finish loading. - cy.contains("Token Permission Groups"); - }); - - it("should not have any permission groups defined", () => { - cy.dataCy("permission-group-list").children().should("have.length", 0); - saveButtonEnabled(false); - }); - - it("should throw an error if permission group definitions are invalid", () => { - cy.contains("button", /^Add permission group$/).should("be.visible"); - cy.contains("button", /^Add permission group$/).click(); - cy.dataCy("permission-group-list").children().should("have.length", 1); - - const invalidGithubPermission = "invalid_github_permission"; - cy.dataCy("permission-group-title-input").type("test permission group"); - cy.dataCy("add-permission-button").should("be.visible"); - cy.dataCy("add-permission-button").click(); - cy.dataCy("permission-type-input").type(invalidGithubPermission); - cy.dataCy("permission-value-input").click(); - cy.contains("Write").click({ force: true }); - saveButtonEnabled(true); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSaveAndConfirmDiff(); - cy.validateToast("error", "There was an error saving the project"); - }); - - it("should be able to save permission group, then delete it", () => { - // Add permission group. - cy.contains("button", /^Add permission group$/).should("be.visible"); - cy.contains("button", /^Add permission group$/).click(); - cy.dataCy("permission-group-list").children().should("have.length", 1); - - cy.dataCy("permission-group-title-input").type("test permission group"); - cy.dataCy("add-permission-button").should("be.visible"); - cy.dataCy("add-permission-button").click(); - cy.dataCy("permission-type-input").type("actions"); - cy.dataCy("permission-value-input").click(); - cy.contains("Read").click(); - saveButtonEnabled(true); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - - // Delete permission group. - cy.reload(); - cy.dataCy("permission-group-list").children().should("have.length", 1); - cy.dataCy("delete-item-button").click(); - cy.dataCy("permission-group-list").children().should("have.length", 0); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/not_defaulting_to_repo.ts b/apps/spruce/cypress/integration/projectSettings/not_defaulting_to_repo.ts deleted file mode 100644 index e4023d08cc..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/not_defaulting_to_repo.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { - getProjectSettingsRoute, - project, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Project Settings when not defaulting to repo", () => { - const origin = getProjectSettingsRoute(project); - - beforeEach(() => { - cy.visit(origin); - saveButtonEnabled(false); - }); - - it("Does not show a 'Default to Repo' button on page", () => { - cy.dataCy("default-to-repo-button").should("not.exist"); - }); - - it("Shows two radio boxes", () => { - cy.dataCy("enabled-radio-box").children().should("have.length", 2); - }); - - it("Successfully attaches to and detaches from a repo that does not yet exist and shows 'Default to Repo' options", () => { - cy.dataCy("attach-repo-button").click(); - cy.dataCy("attach-repo-modal") - .find("button") - .contains("Attach") - .parent() - .click(); - cy.validateToast("success", "Successfully attached to repo"); - cy.dataCy("attach-repo-button").click(); - cy.dataCy("attach-repo-modal") - .find("button") - .contains("Detach") - .parent() - .click(); - cy.validateToast("success", "Successfully detached from repo"); - }); - - it("Allows enabling Run Every Mainline Commit", () => { - cy.dataCy("navitem-general").click(); - cy.dataCy("run-every-mainline-commit-radio-box").children().first().click(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("run-every-mainline-commit-radio-box") - .children() - .first() - .children() - .first() - .should("have.attr", "aria-checked", "true"); - }); - - describe("Variables page", () => { - beforeEach(() => { - cy.dataCy("navitem-variables").click(); - }); - - it("Should not have the save button enabled on load", () => { - saveButtonEnabled(false); - }); - - it("Should not show the move variables button", () => { - cy.dataCy("promote-vars-button").should("not.exist"); - }); - - it("Should redact and disable private variables on saving", () => { - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").type("sample_name"); - saveButtonEnabled(false); - cy.dataCy("var-value-input").type("sample_value"); - cy.dataCy("var-description-input").type("Sample description"); - cy.dataCy("var-private-input").should("be.checked"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("var-value-input").should("have.value", "{REDACTED}"); - cy.dataCy("var-name-input").should("have.attr", "aria-disabled", "true"); - cy.dataCy("var-value-input").should("have.attr", "aria-disabled", "true"); - cy.dataCy("var-private-input").should( - "have.attr", - "aria-disabled", - "true", - ); - // Admin checkbox should not be disabled. - cy.dataCy("var-admin-input").should( - "have.attr", - "aria-disabled", - "false", - ); - // Description input should not be disabled. - cy.dataCy("var-description-input").should( - "have.attr", - "aria-disabled", - "false", - ); - }); - - it("Typing a duplicate variable name will disable saving and show an error message", () => { - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").type("sample_name"); - cy.dataCy("var-value-input").type("sample_value"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").first().type("sample_name"); - cy.dataCy("var-value-input").first().type("sample_value_2"); - cy.contains("Value already appears in project variables.").as( - "errorMessage", - ); - saveButtonEnabled(false); - // Undo variable duplication - cy.dataCy("var-name-input").first().type("_2"); - saveButtonEnabled(); - cy.get("@errorMessage").should("not.exist"); - }); - - it("Should correctly save an admin only variable", () => { - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").first().type("admin_var"); - cy.dataCy("var-value-input").first().type("admin_value"); - cy.contains("label", "Admin Only").click(); - cy.dataCy("var-admin-input").should("be.checked"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); - - it("Should persist saved variables and allow deletion", () => { - // Add variables - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").type("sample_name"); - cy.dataCy("var-value-input").type("sample_value"); - cy.dataCy("var-description-input").type("Description for sample_name"); - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").first().type("sample_name_2"); - cy.dataCy("var-value-input").first().type("sample_value"); - cy.dataCy("var-description-input") - .first() - .type("Description for sample_name_2"); - cy.dataCy("add-button").click(); - cy.dataCy("var-name-input").first().type("admin_var"); - cy.dataCy("var-value-input").first().type("admin_value"); - cy.dataCy("var-description-input") - .first() - .type("Description for admin_var"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - // Verify persistence - cy.reload(); - cy.dataCy("var-name-input").eq(0).should("have.value", "admin_var"); - cy.dataCy("var-description-input") - .eq(0) - .should("have.value", "Description for admin_var"); - cy.dataCy("var-name-input").eq(1).should("have.value", "sample_name"); - cy.dataCy("var-description-input") - .eq(1) - .should("have.value", "Description for sample_name"); - cy.dataCy("var-name-input").eq(2).should("have.value", "sample_name_2"); - cy.dataCy("var-description-input") - .eq(2) - .should("have.value", "Description for sample_name_2"); - // Verify deletion - cy.dataCy("delete-item-button").first().click(); - cy.dataCy("delete-item-button").first().click(); - cy.dataCy("delete-item-button").first().click(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("var-name-input").should("not.exist"); - // Verify persistence - cy.reload(); - saveButtonEnabled(false); - cy.dataCy("var-name-input").should("not.exist"); - }); - }); - - describe("GitHub page", () => { - beforeEach(() => { - cy.dataCy("navitem-github-commitqueue").click(); - }); - - it("Allows adding a git tag alias", () => { - cy.dataCy("git-tag-enabled-radio-box").children().first().click(); - cy.dataCy("add-button").contains("Add git tag").parent().click(); - cy.dataCy("git-tag-input").type("myGitTag"); - cy.dataCy("remote-path-input").type("./evergreen.yml"); - - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("remote-path-input").should("have.value", "./evergreen.yml"); - }); - }); - - describe("Periodic Builds page", () => { - beforeEach(() => { - cy.dataCy("navitem-periodic-builds").click(); - }); - - it("allows a user to schedule the next build on the current day", () => { - const now = new Date(2025, 8, 16); // month is 0-indexed - - cy.clock(now, ["Date"]); - // Reload to apply clock changes - cy.reload(); - cy.dataCy("add-button").click(); - cy.validateDatePickerDate("date-picker", { - year: "2025", - month: "09", - day: "16", - }); - - cy.get("input[id='year']").as("startOfDateInput"); - - cy.clearDatePickerInput(); - - cy.get("@startOfDateInput").type("20250101"); - // Check for error text - cy.contains("Date must be after").should("exist"); - cy.validateDatePickerDate("date-picker", { - year: "2025", - month: "01", - day: "01", - }); - - cy.clearDatePickerInput(); - - cy.get("@startOfDateInput").type("20250920"); - // No error text - cy.contains("Date must be after").should("not.exist"); - cy.validateDatePickerDate("date-picker", { - year: "2025", - month: "09", - day: "20", - }); - - cy.clearDatePickerInput(); - - cy.get("@startOfDateInput").type("20250916"); - // No error text - cy.contains("Date must be after").should("not.exist"); - cy.validateDatePickerDate("date-picker", { - year: "2025", - month: "09", - day: "16", - }); - }); - - it("Disables save button when interval is NaN or below minimum and allows saving a number in range", () => { - cy.dataCy("add-button").click(); - cy.dataCy("interval-input").as("intervalInput").type("NaN"); - cy.dataCy("config-file-input").type("config.yml"); - saveButtonEnabled(false); - cy.contains("Value should be a number."); - cy.get("@intervalInput").clear(); - cy.get("@intervalInput").type("0"); - saveButtonEnabled(false); - cy.get("@intervalInput").clear(); - cy.get("@intervalInput").type("12"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); - }); - - describe("Project Triggers page", () => { - beforeEach(() => { - cy.dataCy("navitem-project-triggers").click(); - }); - - it("Saves a project trigger", () => { - cy.dataCy("add-button").click(); - cy.dataCy("project-input").should("be.visible").should("not.be.disabled"); - cy.dataCy("project-input").type("spruce"); - cy.dataCy("config-file-input").type(".evergreen.yml"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/notifications.ts b/apps/spruce/cypress/integration/projectSettings/notifications.ts deleted file mode 100644 index 883162b94d..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/notifications.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Notifications", () => { - const origin = getProjectSettingsRoute( - "evergreen", - ProjectSettingsTabRoutes.Notifications, - ); - beforeEach(() => { - cy.visit(origin); - }); - it("shows correct intitial state", () => { - cy.dataCy("default-to-repo-button").should("not.exist"); - cy.contains("No subscriptions are defined.").should("be.visible"); - saveButtonEnabled(false); - }); - it("should be able to add a subscription, save it and delete it", () => { - cy.dataCy("expandable-card").should("not.exist"); - cy.dataCy("add-button").contains("Add Subscription").should("be.visible"); - cy.dataCy("add-button").click(); - cy.dataCy("expandable-card").should("contain.text", "New Subscription"); - cy.selectLGOption("Event", "Any version finishes"); - cy.selectLGOption("Notification Method", "Email"); - cy.getInputByLabel("Email").type("mohamed.khelif@mongodb.com"); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - - saveButtonEnabled(false); - cy.dataCy("expandable-card").as("subscriptionItem").scrollIntoView(); - cy.get("@subscriptionItem") - .should("be.visible") - .should("contain.text", "Version outcome - mohamed.khelif@mongodb.com"); - cy.dataCy("delete-item-button").should("not.be.disabled").click(); - cy.get("@subscriptionItem").should("not.exist"); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); - - it("should not be able to combine a jira comment subscription with a task event", () => { - cy.dataCy("expandable-card").should("not.exist"); - cy.dataCy("add-button").contains("Add Subscription").should("be.visible"); - cy.dataCy("add-button").click(); - cy.dataCy("expandable-card").should("exist").scrollIntoView(); - cy.dataCy("expandable-card") - .should("be.visible") - .should("contain.text", "New Subscription"); - cy.selectLGOption("Event", "Any task finishes"); - cy.selectLGOption("Notification Method", "Comment on a JIRA issue"); - cy.getInputByLabel("JIRA Issue").type("JIRA-123"); - cy.contains("Subscription type not allowed for tasks in a project.").should( - "be.visible", - ); - cy.dataCy("save-settings-button").scrollIntoView(); - saveButtonEnabled(false); - }); - it("should not be able to save a subscription if an input is invalid", () => { - cy.dataCy("add-button").click(); - cy.dataCy("expandable-card").scrollIntoView(); - cy.dataCy("expandable-card") - .should("be.visible") - .should("contain.text", "New Subscription"); - cy.selectLGOption("Event", "Any version finishes"); - cy.selectLGOption("Notification Method", "Email"); - cy.getInputByLabel("Email").type("Not a real email"); - cy.contains("Value should be a valid email.").should("be.visible"); - cy.dataCy("save-settings-button").scrollIntoView(); - saveButtonEnabled(false); - }); - it("Setting a project banner displays the banner on the correct pages and unsetting is removes it", () => { - const bannerText = "This is a project banner!"; - - // set banner - cy.dataCy("banner-text").clear(); - cy.dataCy("banner-text").type(bannerText); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - - // ensure banner is displayed - cy.contains(bannerText).should("be.visible"); - - const taskRoute = - "task/evergreen_ubuntu1604_test_model_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48"; - cy.visit(taskRoute); - cy.contains(bannerText).should("be.visible"); - - const configureRoute = "patch/5e6bb9e23066155a993e0f1b/configure/tasks"; - cy.visit(configureRoute); - cy.contains(bannerText).should("be.visible"); - - const versionRoute = "version/5e4ff3abe3c3317e352062e4"; - cy.visit(versionRoute); - cy.contains(bannerText).should("be.visible"); - - const waterfallRoute = "project/evergreen/waterfall"; - cy.visit(waterfallRoute); - cy.contains(bannerText).should("be.visible"); - - const variantHistoryRoute = "/variant-history/evergreen/ubuntu1604"; - cy.visit(variantHistoryRoute); - cy.contains(bannerText).should("be.visible"); - - // clear banner - cy.visit(origin); - cy.dataCy("banner-text").clear(); - clickSaveAndConfirmDiff(); - - // ensure banner is not displayed - cy.contains(bannerText).should("not.exist"); - - cy.visit(taskRoute); - cy.contains(bannerText).should("not.exist"); - - cy.visit(configureRoute); - cy.contains(bannerText).should("not.exist"); - - cy.visit(versionRoute); - cy.contains(bannerText).should("not.exist"); - - cy.visit(waterfallRoute); - cy.contains(bannerText).should("not.exist"); - - cy.visit(variantHistoryRoute); - cy.contains(bannerText).should("not.exist"); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/permissions.ts b/apps/spruce/cypress/integration/projectSettings/permissions.ts deleted file mode 100644 index 9c19dcd410..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/permissions.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { users } from "../../constants"; -import { - projectUseRepoEnabled, - getRepoSettingsRoute, - getProjectSettingsRoute, - repo, -} from "./constants"; - -describe("project/repo permissions", () => { - beforeEach(() => { - cy.logout(); - }); - - describe("projects", () => { - it("disables fields when user lacks edit permissions", () => { - cy.login(users.privileged); - cy.visit(getProjectSettingsRoute(projectUseRepoEnabled)); - cy.dataCy("project-settings-page").within(() => { - cy.get('input[type="radio"]').should( - "have.attr", - "aria-disabled", - "true", - ); - }); - }); - - it("enables fields if user has edit permissions", () => { - cy.login(users.admin); - cy.visit(getProjectSettingsRoute(projectUseRepoEnabled)); - cy.dataCy("project-settings-page").within(() => { - cy.get('input[type="radio"]').should( - "have.attr", - "aria-disabled", - "false", - ); - }); - }); - }); - - describe("repos", () => { - it("disables fields when user lacks edit permissions", () => { - cy.login(users.privileged); - cy.visit(getRepoSettingsRoute(repo)); - cy.dataCy("repo-settings-page").within(() => { - cy.get('input[type="radio"]').should( - "have.attr", - "aria-disabled", - "true", - ); - }); - }); - - it("enables fields if user has edit permissions", () => { - cy.login(users.admin); - cy.visit(getRepoSettingsRoute(repo)); - cy.dataCy("repo-settings-page").within(() => { - cy.get('input[type="radio"]').should( - "have.attr", - "aria-disabled", - "false", - ); - }); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/plugins.ts b/apps/spruce/cypress/integration/projectSettings/plugins.ts deleted file mode 100644 index df0833dc51..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/plugins.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - projectUseRepoEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Plugins", () => { - const patchPage = "version/5ecedafb562343215a7ff297"; - - const addMetadataLink = (metadataLink: { - displayName: string; - url: string; - }) => { - cy.contains("button", "Add metadata link").scrollIntoView(); - cy.contains("button", "Add metadata link").click(); - cy.dataCy("requesters-input").first().click(); - cy.getInputByLabel("Patches").check({ force: true }); - cy.dataCy("requesters-input").first().click(); - cy.dataCy("display-name-input").first().type(metadataLink.displayName); - cy.dataCy("url-template-input").first().type(metadataLink.url, { - parseSpecialCharSequences: false, - }); - }; - - it("Should be able to set external links to render on patch metadata panel", () => { - // Add external links. - cy.visit( - getProjectSettingsRoute( - projectUseRepoEnabled, - ProjectSettingsTabRoutes.Plugins, - ), - ); - addMetadataLink({ - displayName: "An external link 1", - url: "https://example-1.com/{version_id}", - }); - addMetadataLink({ - displayName: "An external link 2", - url: "https://example-2.com/{version_id}", - }); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSaveAndConfirmDiff(); - - cy.visit(patchPage); - cy.dataCy("external-link").should("have.length", 2); - cy.dataCy("external-link").last().contains("An external link 1"); - cy.dataCy("external-link") - .last() - .should( - "have.attr", - "href", - "https://example-1.com/5ecedafb562343215a7ff297", - ); - cy.dataCy("external-link").first().contains("An external link 2"); - cy.dataCy("external-link") - .first() - .should( - "have.attr", - "href", - "https://example-2.com/5ecedafb562343215a7ff297", - ); - - // Remove external links. - cy.visit( - getProjectSettingsRoute( - projectUseRepoEnabled, - ProjectSettingsTabRoutes.Plugins, - ), - ); - cy.dataCy("delete-item-button").first().click(); - cy.dataCy("delete-item-button").first().click(); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSaveAndConfirmDiff(); - - cy.visit(patchPage); - cy.dataCy("external-link").should("not.exist"); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/project_select.ts b/apps/spruce/cypress/integration/projectSettings/project_select.ts deleted file mode 100644 index b80066f4c5..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/project_select.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getProjectSettingsRoute, project } from "./constants"; - -describe("Clicking on The Project Select Dropdown", () => { - const origin = getProjectSettingsRoute(project); - - beforeEach(() => { - cy.visit(origin); - }); - - it("Headers are clickable", () => { - cy.dataCy("project-select").should("be.visible"); - cy.dataCy("project-select").click(); - cy.dataCy("project-select-options").should("be.visible"); - cy.dataCy("project-select-options") - .find("div") - .contains("evergreen-ci/evergreen") - .click(); - cy.location().should((loc) => expect(loc.pathname).to.not.eq(origin)); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/project_settings.ts b/apps/spruce/cypress/integration/projectSettings/project_settings.ts deleted file mode 100644 index 19ac03f3bd..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/project_settings.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - getProjectSettingsRoute, - project, - ProjectSettingsTabRoutes, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("projectSettings/project_settings", () => { - describe("Renaming the identifier", () => { - const origin = getProjectSettingsRoute(project); - - beforeEach(() => { - cy.visit(origin); - }); - - it("Update identifier", () => { - const warningText = - "Updates made to the project identifier will change the identifier used for the CLI, inter-project dependencies, etc. Project users should be made aware of this change, as the old identifier will no longer work."; - - cy.dataCy("input-warning").should("not.exist"); - cy.dataCy("identifier-input").clear(); - cy.dataCy("identifier-input").type("new-identifier"); - cy.dataCy("input-warning").should("contain", warningText); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.url().should("include", "new-identifier"); - }); - }); - - describe("A project that has GitHub webhooks disabled", () => { - const origin = getProjectSettingsRoute( - "logkeeper", - ProjectSettingsTabRoutes.GithubCommitQueue, - ); - - beforeEach(() => { - cy.visit(origin); - }); - - it("Disables all interactive elements on the page", () => { - cy.dataCy("project-settings-page") - .find("button") - .should("have.attr", "aria-disabled", "true"); - cy.get("input").should("have.attr", "aria-disabled", "true"); - }); - }); - - describe("A project id should redirect to the project identifier", () => { - const projectId = "602d70a2b2373672ee493189"; - const origin = getProjectSettingsRoute(projectId); - - beforeEach(() => { - cy.visit(origin); - }); - - it("Redirects to the project identifier", () => { - cy.url().should("include", getProjectSettingsRoute("parsley")); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/repo_settings.ts b/apps/spruce/cypress/integration/projectSettings/repo_settings.ts deleted file mode 100644 index c0b59458d7..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/repo_settings.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { - getProjectSettingsRoute, - getRepoSettingsRoute, - ProjectSettingsTabRoutes, - projectUseRepoEnabled, - repo, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Repo Settings", () => { - const origin = getRepoSettingsRoute(repo); - - beforeEach(() => { - cy.visit(origin); - }); - - describe("General settings page", () => { - it("Should have the save button disabled on load", () => { - saveButtonEnabled(false); - }); - - it("Does not show a 'Default to Repo' button on page", () => { - cy.dataCy("default-to-repo-button").should("not.exist"); - }); - - it("Does not show a 'Move to New Repo' button on page", () => { - cy.dataCy("move-repo-button").should("not.exist"); - }); - - it("Does not show an Attach/Detach to Repo button on page", () => { - cy.dataCy("attach-repo-button").should("not.exist"); - }); - - it("Does not show a 'Go to repo settings' link on page", () => { - cy.dataCy("attached-repo-link").should("not.exist"); - }); - it("Inputting a display name then clicking save shows a success toast", () => { - cy.dataCy("display-name-input").type("evg"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - }); - }); - - describe("GitHub page", () => { - beforeEach(() => { - cy.dataCy("navitem-github-commitqueue").click(); - saveButtonEnabled(false); - }); - describe("GitHub section", () => { - it("Shows an error banner when Commit Checks are enabled and hides it when Commit Checks are disabled", () => { - cy.dataCy("github-checks-enabled-radio-box") - .contains("label", "Enabled") - .click(); - cy.dataCy("error-banner") - .contains( - "A Commit Check Definition must be specified for this feature to run.", - ) - .as("errorBanner"); - cy.get("@errorBanner").should("be.visible"); - cy.dataCy("github-checks-enabled-radio-box") - .contains("label", "Disabled") - .click(); - cy.get("@errorBanner").should("not.exist"); - }); - - it("Allows enabling manual PR testing", () => { - cy.dataCy("manual-pr-testing-enabled-radio-box") - .children() - .first() - .click(); - }); - it("Saving a patch definition should hide the error banner, success toast and displays disable patch definitions for the repo", () => { - cy.contains( - "A GitHub Patch Definition must be specified for this feature to run.", - ).as("errorBanner"); - cy.get("@errorBanner").should("be.visible"); - cy.contains("button", "Add Patch Definition").click(); - cy.get("@errorBanner").should("not.exist"); - saveButtonEnabled(false); - cy.dataCy("variant-tags-input").first().type("vtag"); - cy.dataCy("task-tags-input").first().type("ttag"); - saveButtonEnabled(true); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - cy.visit(getProjectSettingsRoute(projectUseRepoEnabled)); - cy.dataCy("navitem-github-commitqueue").click(); - cy.contains("Repo Patch Definition 1") - .as("patchDefAccordion") - .scrollIntoView(); - cy.get("@patchDefAccordion").click(); - cy.dataCy("variant-tags-input").should("have.value", "vtag"); - cy.dataCy("variant-tags-input").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("task-tags-input").should("have.value", "ttag"); - cy.dataCy("task-tags-input").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.contains( - "A GitHub Patch Definition must be specified for this feature to run.", - ).should("not.exist"); - }); - }); - - describe("Merge Queue section", () => { - beforeEach(() => { - cy.dataCy("cq-enabled-radio-box") - .contains("label", "Enabled") - .as("enableCQButton") - .scrollIntoView(); - }); - it("Enabling merge queue shows hidden inputs and error banner", () => { - cy.dataCy("cq-card") - .children() - .as("cqCardFields") - .should("have.length", 2); - - cy.get("@enableCQButton").click(); - cy.get("@cqCardFields").should("have.length", 3); - cy.contains("Merge Queue Patch Definitions").scrollIntoView(); - cy.dataCy("error-banner") - .contains( - "A Merge Queue Patch Definition must be specified for this feature to run.", - ) - .should("be.visible"); - }); - - it("Does not show override buttons for merge queue patch definitions", () => { - cy.get("@enableCQButton").click(); - cy.dataCy("cq-override-radio-box").should("not.exist"); - }); - - it("Saves a merge queue definition", () => { - cy.get("@enableCQButton").click(); - cy.contains("button", "Add Patch Definition").click(); - cy.dataCy("variant-tags-input").first().type("vtag"); - cy.dataCy("task-tags-input").first().type("ttag"); - saveButtonEnabled(false); - cy.contains("button", "Add merge queue patch definition").click(); - cy.dataCy("variant-tags-input").last().type("cqvtag"); - cy.dataCy("task-tags-input").last().type("cqttag"); - cy.dataCy("warning-banner").should("not.exist"); - cy.dataCy("error-banner").should("not.exist"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - }); - }); - }); - - describe("Patch Aliases page", () => { - beforeEach(() => { - cy.dataCy("navitem-patch-aliases").click(); - saveButtonEnabled(false); - cy.dataCy("patch-aliases-override-radio-box").should("not.exist"); - }); - - it("Saving a patch alias shows a success toast, the alias name in the card title and in the repo defaulted project", () => { - cy.dataCy("add-button").contains("Add Patch Alias").parent().click(); - cy.dataCy("expandable-card-title").contains("New Patch Alias"); - cy.dataCy("alias-input").type("my alias name"); - saveButtonEnabled(false); - cy.dataCy("variant-tags-input").first().type("alias variant tag"); - cy.dataCy("task-tags-input").first().type("alias task tag"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - cy.dataCy("expandable-card-title").contains("my alias name"); - // Verify persistence - cy.reload(); - cy.dataCy("expandable-card-title").contains("my alias name"); - cy.visit( - getProjectSettingsRoute( - projectUseRepoEnabled, - ProjectSettingsTabRoutes.Access, - ), - ); - cy.dataCy("default-to-repo-button").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("default-to-repo-button").click(); - cy.dataCy("default-to-repo-modal").should("be.visible"); - cy.getInputByLabel('Type "confirm" to confirm your action').type( - "confirm", - ); - cy.dataCy("default-to-repo-modal").contains("Confirm").click(); - cy.validateToast("success", "Successfully defaulted page to repo"); - cy.dataCy("navitem-patch-aliases").click(); - cy.dataCy("expandable-card-title").contains("my alias name"); - cy.dataCy("expandable-card-title") - .parentsUntil("div") - .first() - .click({ force: true }); - cy.dataCy("expandable-card") - .find("input") - .should("have.attr", "aria-disabled", "true"); - cy.dataCy("expandable-card").find("button").should("be.disabled"); - }); - - it("Saving a Patch Trigger Alias shows a success toast and updates the Github page", () => { - cy.dataCy("add-button") - .contains("Add Patch Trigger Alias") - .parent() - .click(); - cy.dataCy("pta-alias-input").type("my-alias"); - cy.dataCy("project-input").type("spruce"); - cy.dataCy("module-input").type("module_name"); - cy.contains("button", "Variant/Task").click(); - cy.dataCy("variant-regex-input").type(".*"); - cy.dataCy("task-regex-input").type(".*"); - cy.getInputByLabel("Schedule in GitHub Pull Requests").as( - "pullRequestCheckbox", - ); - cy.get("@pullRequestCheckbox").should("not.be.checked"); - cy.get("@pullRequestCheckbox").check({ force: true }); - cy.get("@pullRequestCheckbox").should("be.checked"); - cy.getInputByLabel("Schedule in GitHub Merge Queue").as( - "mergeQueueCheckbox", - ); - cy.get("@mergeQueueCheckbox").should("not.be.checked"); - cy.get("@mergeQueueCheckbox").check({ force: true }); - cy.get("@mergeQueueCheckbox").should("be.checked"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - saveButtonEnabled(false); - // Demonstrate Wait on field is optional - cy.selectLGOption("Wait on", "Success"); - cy.getInputByLabel("Wait on").should( - "have.attr", - "aria-invalid", - "false", - ); - saveButtonEnabled(true); - cy.selectLGOption("Wait on", "Select event…"); - cy.getInputByLabel("Wait on").should( - "have.attr", - "aria-invalid", - "false", - ); - saveButtonEnabled(false); - // Verify information on Github page - cy.dataCy("navitem-github-commitqueue").click(); - - cy.contains("Pull Request Trigger Aliases").scrollIntoView(); - cy.dataCy("github-pr-trigger-aliases").within(() => { - cy.dataCy("pta-item").should("have.length", 1); - cy.contains("my-alias").should("be.visible"); - cy.dataCy("pta-item").trigger("mouseover"); - }); - // The tooltip is rendered in a different part of the DOM so we can't chain the 'within' command. - cy.dataCy("pta-tooltip").should("be.visible"); - cy.dataCy("pta-tooltip").contains("spruce"); - cy.dataCy("pta-tooltip").contains("module_name"); - cy.dataCy("pta-tooltip").contains("Variant/Task Regex Pairs"); - cy.dataCy("github-pr-trigger-aliases").within(() => { - cy.dataCy("pta-item").trigger("mouseout"); - }); - - cy.contains("Merge Queue Trigger Aliases").scrollIntoView(); - cy.dataCy("github-mq-trigger-aliases").within(() => { - cy.dataCy("pta-item").should("have.length", 1); - cy.contains("my-alias").should("be.visible"); - cy.dataCy("pta-item").trigger("mouseover"); - }); - cy.dataCy("pta-tooltip").should("be.visible"); - cy.dataCy("pta-tooltip").contains("spruce"); - cy.dataCy("pta-tooltip").contains("module_name"); - cy.dataCy("pta-tooltip").contains("Variant/Task Regex Pairs"); - }); - }); - - describe("Virtual Workstation page", () => { - beforeEach(() => { - cy.dataCy("navitem-virtual-workstation").click(); - }); - - it("Adds two commands and then reorders them", () => { - saveButtonEnabled(false); - cy.dataCy("add-button").click(); - cy.dataCy("command-input").type("command 1"); - cy.dataCy("directory-input").type("mongodb.user.directory"); - - cy.dataCy("add-button").click(); - cy.dataCy("command-input").eq(1).type("command 2"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - cy.dataCy("array-down-button").click(); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated repo"); - cy.dataCy("command-input").first().should("have.value", "command 2"); - cy.dataCy("command-input").eq(1).should("have.value", "command 1"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/stepback_bisect.ts b/apps/spruce/cypress/integration/projectSettings/stepback_bisect.ts deleted file mode 100644 index eeab7187d7..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/stepback_bisect.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - getProjectSettingsRoute, - project, - projectUseRepoEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Stepback bisect setting", () => { - describe("Repo project present", () => { - const destination = getProjectSettingsRoute(projectUseRepoEnabled); - - beforeEach(() => { - cy.visit(destination); - }); - - it("Starts as default to repo", () => { - cy.dataCy("stepback-bisect-group") - .contains("Default to repo") - .find("input") - .should("have.attr", "aria-checked", "true"); - }); - - it("Clicking on enabled and then save shows a success toast", () => { - cy.dataCy("stepback-bisect-group").contains("Enable").click(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - - cy.reload(); - - cy.dataCy("stepback-bisect-group") - .contains("Enable") - .find("input") - .should("have.attr", "aria-checked", "true"); - }); - }); - - describe("Repo project not present", () => { - const destination = getProjectSettingsRoute(project); - - beforeEach(() => { - cy.visit(destination); - }); - - it("Starts as disabled", () => { - cy.dataCy("stepback-bisect-group") - .contains("Disable") - .find("input") - .should("have.attr", "aria-checked", "true"); - }); - - it("Clicking on enabled and then save shows a success toast", () => { - cy.dataCy("stepback-bisect-group").contains("Enable").click(); - - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - - cy.reload(); - - cy.dataCy("stepback-bisect-group") - .contains("Enable") - .find("input") - .should("have.attr", "aria-checked", "true"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/projectSettings/utils.ts b/apps/spruce/cypress/integration/projectSettings/utils.ts deleted file mode 100644 index bfe7857f98..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { clickSave } from "../../utils"; - -/** - * Save a project or repo settings page. Clicking the save button opens a diff - * confirmation modal which must be accepted to persist the change. - */ -export const clickSaveAndConfirmDiff = () => { - clickSave(); - cy.dataCy("save-changes-modal").should("be.visible"); - cy.dataCy("save-changes-modal").contains("button", "Save changes").click(); - cy.dataCy("save-changes-modal").should("not.exist"); -}; diff --git a/apps/spruce/cypress/integration/projectSettings/views_and_filters.ts b/apps/spruce/cypress/integration/projectSettings/views_and_filters.ts deleted file mode 100644 index 7342464c9f..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/views_and_filters.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("Views & filters page", () => { - const destination = getProjectSettingsRoute( - "sys-perf", - ProjectSettingsTabRoutes.ViewsAndFilters, - ); - - beforeEach(() => { - cy.visit(destination); - // Wait for page content to finish loading. - cy.dataCy("parsley-filter-list").children().should("have.length", 2); - saveButtonEnabled(false); - }); - - describe("parsley filters", () => { - it("does not allow saving with invalid regular expression or empty expression", () => { - cy.contains("button", "Add filter").should("be.visible").click(); - cy.dataCy("parsley-filter-expression").first().type("*"); - saveButtonEnabled(false); - cy.contains("Value should be a valid regex expression."); - cy.dataCy("parsley-filter-expression").first().clear(); - saveButtonEnabled(false); - }); - - it("does not allow saving with duplicate filter expressions", () => { - cy.contains("button", "Add filter").should("be.visible").click(); - cy.dataCy("parsley-filter-expression").first().type("filter_1"); - saveButtonEnabled(false); - cy.contains("Filter expression already appears in this project."); - }); - - it("can successfully save and delete filter", () => { - cy.contains("button", "Add filter").should("be.visible").click(); - cy.dataCy("parsley-filter-expression").first().type("my_filter"); - saveButtonEnabled(true); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("parsley-filter-list").children().should("have.length", 3); - - cy.dataCy("delete-item-button").first().scrollIntoView(); - cy.dataCy("delete-item-button").first().should("be.visible").click(); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("parsley-filter-list").children().should("have.length", 2); - }); - }); -}); diff --git a/apps/spruce/playwright/fixtures.ts b/apps/spruce/playwright/fixtures.ts index a1b7d19688..9f29b0961a 100644 --- a/apps/spruce/playwright/fixtures.ts +++ b/apps/spruce/playwright/fixtures.ts @@ -6,6 +6,7 @@ import { SEEN_TASK_HISTORY_ONBOARDING_TUTORIAL, SEEN_TASK_REVIEW_TOOLTIP, SEEN_TEST_SELECTION_GUIDE_CUE, + SEEN_GITHUB_NAV_GUIDE_CUE, } from "constants/cookies"; import * as helpers from "./helpers"; @@ -84,6 +85,12 @@ export const test = base.extend({ domain: "localhost", path: "/", }, + { + name: SEEN_GITHUB_NAV_GUIDE_CUE, + value: "true", + domain: "localhost", + path: "/", + }, ]); await use(page); diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 69e9b7860e..1db35d9c6e 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -15,6 +15,7 @@ export const selectOption = async ( options?: { exact: boolean }, ): Promise => { const button = page.getByRole("button", { name: label, exact: true }); + await expect(button).toHaveCount(1); await expect(button).toBeEnabled(); await button.click(); const listbox = page.locator('[role="listbox"]'); @@ -123,6 +124,7 @@ export { login, logout, clickCheckbox, + clickRadio, mockGraphQLResponse, hasOperationName, } from "@evg-ui/playwright-config/helpers"; diff --git a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts index ea44f4c650..5bfd2449c5 100644 --- a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts @@ -119,7 +119,6 @@ test.describe("admin settings save properly", () => { await validateToast(page, "success", "Settings saved successfully"); await page.reload(); - await page.getByLabel("Total Project Limit").scrollIntoViewIfNeeded(); await expect(page.getByLabel("Total Project Limit")).toHaveValue("200"); await expect(newProjectCreationException.getByLabel("Owner")).toHaveValue( "owner", diff --git a/apps/spruce/playwright/tests/adminSettings/web.spec.ts b/apps/spruce/playwright/tests/adminSettings/web.spec.ts index 4f7608233e..3f996de68f 100644 --- a/apps/spruce/playwright/tests/adminSettings/web.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/web.spec.ts @@ -24,9 +24,6 @@ test.describe("web", () => { // Disabled GraphQL Queries section. const disabledGQLQueriesSection = page.getByTestId("disabled-gql-queries"); - await disabledGQLQueriesSection - .getByLabel("Disabled GraphQL Queries") - .scrollIntoViewIfNeeded(); await disabledGQLQueriesSection .getByLabel("Disabled GraphQL Queries") .clear(); diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts index c589422132..31f2b0a663 100644 --- a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts @@ -50,15 +50,22 @@ test.describe("provider section", () => { test("shows pool mapping information based on container pool id", async ({ authenticatedPage: page, }) => { - await expect(page.getByLabel("Container Pool ID")).toContainText( - "test-pool-1", - ); - await expect(page.getByLabel("Pool Mapping Information")).toHaveAttribute( + const containerPoolSelect = page.getByRole("button", { + name: "Container Pool ID", + }); + await expect(containerPoolSelect).toHaveCount(1); + await expect(containerPoolSelect).toContainText("test-pool-1"); + + const containerPoolMapping = page.getByRole("textbox", { + name: "Pool Mapping Information", + }); + await expect(containerPoolMapping).toHaveCount(1); + await expect(containerPoolMapping).toHaveAttribute( "placeholder", /test-pool-1/, ); await selectOption(page, "Container Pool ID", "test-pool-2"); - await expect(page.getByLabel("Pool Mapping Information")).toHaveAttribute( + await expect(containerPoolMapping).toHaveAttribute( "placeholder", /test-pool-2/, ); diff --git a/apps/spruce/cypress/integration/projectSettings/constants.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/constants.ts similarity index 68% rename from apps/spruce/cypress/integration/projectSettings/constants.ts rename to apps/spruce/playwright/tests/projectAndRepoSettings/constants.ts index c8a98e9efc..639b6de88a 100644 --- a/apps/spruce/cypress/integration/projectSettings/constants.ts +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/constants.ts @@ -1,4 +1,5 @@ export enum ProjectSettingsTabRoutes { + // Evergreen sections General = "general", Access = "access", Variables = "variables", @@ -11,36 +12,26 @@ export enum ProjectSettingsTabRoutes { Plugins = "plugins", EventLog = "event-log", ViewsAndFilters = "views-and-filters", + + // GitHub sections. GithubAppSettings = "github-app-settings", GithubPermissionGroups = "github-permission-groups", - MergeQueue = "merge-queue", - PullRequests = "pull-requests", CommitChecks = "commit-checks", + PullRequests = "pull-requests", + MergeQueue = "merge-queue", GitTags = "git-tags", } export const getProjectSettingsRoute = ( identifier: string, tab: ProjectSettingsTabRoutes = ProjectSettingsTabRoutes.General, -) => `project/${identifier}/settings/${tab}`; +) => `/project/${identifier}/settings/${tab}`; export const getRepoSettingsRoute = ( repoId: string, tab: ProjectSettingsTabRoutes = ProjectSettingsTabRoutes.General, -) => `repo/${repoId}/settings/${tab}`; +) => `/repo/${repoId}/settings/${tab}`; export const project = "spruce"; export const projectUseRepoEnabled = "evergreen"; export const repo = "602d70a2b2373672ee493184"; - -/** - * `saveButtonEnabled` checks if the save button is enabled or disabled. - * @param isEnabled - if true, the save button should be enabled. If false, the save button should be disabled. - */ -export const saveButtonEnabled = (isEnabled: boolean = true) => { - cy.dataCy("save-settings-button").should( - isEnabled ? "not.have.attr" : "have.attr", - "aria-disabled", - "true", - ); -}; diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/access_section.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/access_section.spec.ts new file mode 100644 index 0000000000..51edda5df7 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/access_section.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from "../../../fixtures"; +import { clickRadio, validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + project, + ProjectSettingsTabRoutes, + projectUseRepoEnabled, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("Access page", () => { + const origin = getProjectSettingsRoute( + projectUseRepoEnabled, + ProjectSettingsTabRoutes.Access, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await expectSaveButtonEnabled(page, false); + const defaultToRepoButton = page.getByRole("button", { + name: "Default to repo on page", + }); + await expect(defaultToRepoButton).toBeVisible(); + await expect(defaultToRepoButton).toBeEnabled(); + }); + + test("Changing settings and clicking the save button produces a success toast and the changes are persisted", async ({ + authenticatedPage: page, + }) => { + const unrestrictedRadio = page.getByRole("radio", { + name: "Unrestricted", + exact: true, + }); + await clickRadio(unrestrictedRadio); + await expect(unrestrictedRadio).toBeChecked(); + + await page.getByText("Add Username").click(); + const usernameInput = page.getByLabel("Username"); + await usernameInput.fill("admin"); + await expect(usernameInput).toHaveValue("admin"); + await expect(usernameInput).toBeVisible(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await page.reload(); + await expect(page.getByLabel("Username")).toHaveValue("admin"); + await expect(page.getByLabel("Username")).toBeVisible(); + + await page.getByTestId("delete-item-button").click(); + await expect(page.getByLabel("Username")).toHaveCount(0); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await page.reload(); + await expectSaveButtonEnabled(page, false); + await expect(page.getByLabel("Username")).toHaveCount(0); + }); + + test("Clicking on 'Default to Repo on Page' selects the 'Default to repo (unrestricted)' radio box and produces a success banner", async ({ + authenticatedPage: page, + }) => { + const defaultToRepoButton = page.getByRole("button", { + name: "Default to repo on page", + }); + await expect(defaultToRepoButton).toBeEnabled(); + await defaultToRepoButton.click(); + await page + .getByLabel('Type "confirm" to confirm your action') + .fill("confirm"); + await page + .getByTestId("default-to-repo-modal") + .getByRole("button", { name: "Confirm" }) + .click(); + await validateToast(page, "success", "Successfully defaulted page to repo"); + + const defaultToRepoRadio = page.getByRole("radio", { + name: "Default to repo (unrestricted)", + }); + await expect(defaultToRepoRadio).toBeChecked(); + }); + + test("Submitting an invalid admin username produces an error toast", async ({ + authenticatedPage: page, + }) => { + await page.goto( + getProjectSettingsRoute(project, ProjectSettingsTabRoutes.Access), + ); + await page.getByText("Add Username").click(); + const newUsernameInput = page.getByLabel("Username").first(); + await newUsernameInput.fill("mongodb_user"); + await save(page); + await validateToast(page, "error", "There was an error saving the project"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/admin_actions.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/admin_actions.spec.ts new file mode 100644 index 0000000000..e421326484 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/admin_actions.spec.ts @@ -0,0 +1,98 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast } from "../../../helpers"; +import { getProjectSettingsRoute, project } from "../constants"; + +test.describe("projectSettings/admin_actions", () => { + test.describe("Duplicating a project", () => { + const destination = getProjectSettingsRoute(project); + + test("Successfully duplicates a project with warnings", async ({ + authenticatedPage: page, + }) => { + await page.goto(destination); + await page.getByTestId("new-project-button").click(); + await expect(page.getByTestId("new-project-menu")).toBeVisible(); + await page.getByTestId("copy-project-button").click(); + await expect(page.getByTestId("copy-project-modal")).toBeVisible(); + await expect( + page.getByTestId("performance-tooling-banner"), + ).toBeVisible(); + + await page.getByTestId("project-name-input").fill("copied-project"); + + await page.getByRole("button", { name: "Duplicate" }).click(); + await validateToast( + page, + "warning", + "The project was duplicated but may not be fully enabled", + ); + + await expect(page).toHaveURL(/copied-project/); + }); + }); + + test.describe("Creating a new project and deleting it", () => { + test("Successfully creates a new project and then deletes it", async ({ + authenticatedPage: page, + }) => { + await page.goto(getProjectSettingsRoute(project)); + await page.getByTestId("new-project-button").click(); + await expect(page.getByTestId("new-project-menu")).toBeVisible(); + await page.getByTestId("create-project-button").click(); + await expect(page.getByTestId("create-project-modal")).toBeVisible(); + await expect( + page.getByTestId("performance-tooling-banner"), + ).toBeVisible(); + + await page.getByTestId("project-name-input").fill("my-new-project"); + await expect(page.getByTestId("new-owner-select")).toContainText( + "evergreen-ci", + ); + await expect(page.getByTestId("new-repo-input")).toHaveValue("spruce"); + await page.getByTestId("new-repo-input").clear(); + await page.getByTestId("new-repo-input").fill("new-repo"); + + await page.getByRole("button", { name: "Create project" }).click(); + await validateToast( + page, + "success", + "Successfully created the project “my-new-project”", + true, + ); + + await expect(page).toHaveURL(/my-new-project/); + + await page.goto(getProjectSettingsRoute("my-new-project")); + await page.getByTestId("attach-repo-button").click(); + await page + .getByTestId("attach-repo-modal") + .getByRole("button", { name: "Attach" }) + .click(); + await validateToast( + page, + "success", + "Successfully attached to repo", + true, + ); + + await page.getByRole("button", { name: "Delete project" }).click(); + await page + .getByTestId("delete-project-modal") + .getByRole("button", { name: "Delete" }) + .click(); + await validateToast( + page, + "success", + "The project “my-new-project” was deleted.", + true, + ); + + await page.reload(); + await validateToast( + page, + "error", + "There was an error loading the project my-new-project", + ); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/commit_checks.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/commit_checks.spec.ts new file mode 100644 index 0000000000..b14bee8181 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/commit_checks.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from "../../../fixtures"; +import { clickRadio, validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + project, + ProjectSettingsTabRoutes, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("A project that has GitHub webhooks disabled", () => { + const destination = getProjectSettingsRoute( + "logkeeper", + ProjectSettingsTabRoutes.CommitChecks, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + await expectSaveButtonEnabled(page, false); + }); + + test("Commit Checks page shows a disabled webhooks banner when webhooks are disabled", async ({ + authenticatedPage: page, + }) => { + const banner = page.getByTestId("disabled-webhook-banner"); + await expect(banner).toBeVisible(); + await expect(banner).toContainText( + "GitHub features are disabled because the Evergreen GitHub App is not", + ); + }); + + test("Disables all interactive elements on the page", async ({ + authenticatedPage: page, + }) => { + const settingsPage = page.getByTestId("project-settings-page"); + const buttons = settingsPage.getByRole("button"); + for (const button of await buttons.all()) { + await expect(button).toBeDisabled(); + } + const inputs = page.locator("input"); + for (const input of await inputs.all()) { + await expect(input).toBeDisabled(); + } + }); +}); + +test.describe("A project that has GitHub webhooks enabled", () => { + const destination = getProjectSettingsRoute( + project, + ProjectSettingsTabRoutes.CommitChecks, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + await expectSaveButtonEnabled(page, false); + }); + + test("Shows an error banner when Commit Checks are enabled and hides it when Commit Checks are disabled", async ({ + authenticatedPage: page, + }) => { + const radioBox = page.getByTestId("github-checks-enabled-radio-box"); + const githubChecksEnabledRadio = radioBox.getByRole("radio", { + name: "Enabled", + }); + const githubChecksDisabledRadio = radioBox.getByRole("radio", { + name: "Disabled", + }); + + await clickRadio(githubChecksEnabledRadio); + const errorBanner = page.getByTestId("error-banner").filter({ + hasText: + "A Commit Check Definition must be specified for this feature to run.", + }); + await expect(errorBanner).toBeVisible(); + await clickRadio(githubChecksDisabledRadio); + await expect(errorBanner).toBeHidden(); + }); + + test("Saves successfully when Commit Checks are enabled and a Commit Check Definition is provided", async ({ + authenticatedPage: page, + }) => { + const radioBox = page.getByTestId("github-checks-enabled-radio-box"); + const githubChecksEnabledRadio = radioBox.getByRole("radio", { + name: "Enabled", + }); + await clickRadio(githubChecksEnabledRadio); + + await page.getByRole("button", { name: "Add Definition" }).click(); + await page.getByTestId("variant-tags-input").first().fill("vtag"); + await page.getByTestId("task-tags-input").first().fill("ttag"); + + await expect(page.getByTestId("error-banner")).toBeHidden(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/general_section.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/general_section.spec.ts new file mode 100644 index 0000000000..47d4cadbf5 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/general_section.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from "../../../fixtures"; +import { clickRadio, validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + project, + projectUseRepoEnabled, +} from "../constants"; +import { save } from "../utils"; + +const origin = getProjectSettingsRoute(project); + +test.describe("general section", () => { + test.describe("Renaming the identifier", () => { + test("Update identifier", async ({ authenticatedPage: page }) => { + await page.goto(origin); + const warningText = + "Updates made to the project identifier will change the identifier used for the CLI, inter-project dependencies, etc. Project users should be made aware of this change, as the old identifier will no longer work."; + + await expect(page.getByTestId("input-warning")).toHaveCount(0); + await page.getByTestId("identifier-input").clear(); + await page.getByTestId("identifier-input").fill("new-identifier"); + await expect(page.getByTestId("input-warning")).toContainText( + warningText, + ); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect(page).toHaveURL(/new-identifier/); + }); + }); + + test("Allows enabling Run Every Mainline Commit", async ({ + authenticatedPage: page, + }) => { + await page.goto(origin); + await page.getByTestId("navitem-general").click(); + const enableRadio = page + .getByTestId("run-every-mainline-commit-radio-box") + .getByRole("radio", { name: "Enabled" }); + clickRadio(enableRadio); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect(enableRadio).toBeChecked(); + }); + + test.describe("Stepback bisect setting", () => { + test.describe("Repo project present", () => { + const destination = getProjectSettingsRoute(projectUseRepoEnabled); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + }); + + test("Starts as default to repo", async ({ authenticatedPage: page }) => { + await expect( + page + .getByTestId("stepback-bisect-group") + .getByRole("radio", { name: "Default to repo" }), + ).toHaveAttribute("aria-checked", "true"); + }); + + test("Clicking on enabled and then save shows a success toast", async ({ + authenticatedPage: page, + }) => { + const enableRadio = page + .getByTestId("stepback-bisect-group") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(enableRadio); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await page.reload(); + await expect(enableRadio).toHaveAttribute("aria-checked", "true"); + }); + }); + + test.describe("Repo project not present", () => { + const destination = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + }); + + test("Starts as disabled", async ({ authenticatedPage: page }) => { + await expect( + page + .getByTestId("stepback-bisect-group") + .getByRole("radio", { name: "Disabled", exact: true }), + ).toHaveAttribute("aria-checked", "true"); + }); + + test("Clicking on enabled and then save shows a success toast", async ({ + authenticatedPage: page, + }) => { + const enableRadio = page + .getByTestId("stepback-bisect-group") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(enableRadio); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await page.reload(); + await expect(enableRadio).toHaveAttribute("aria-checked", "true"); + }); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/git_tags.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/git_tags.spec.ts new file mode 100644 index 0000000000..e338b65b9f --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/git_tags.spec.ts @@ -0,0 +1,68 @@ +import { clickRadio } from "@evg-ui/playwright-config/helpers"; +import { test, expect } from "../../../fixtures"; +import { validateToast } from "../../../helpers"; +import { save, expectSaveButtonEnabled } from "../utils"; + +test.describe("Git Tags project settings when GitHub webhooks are disabled", () => { + const origin = "/project/logkeeper/settings/git-tags"; + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await expectSaveButtonEnabled(page, false); + }); + + test("Git tags page shows a disabled webhooks banner when webhooks are disabled", async ({ + authenticatedPage: page, + }) => { + const banner = page.getByTestId("disabled-webhook-banner"); + await expect(banner).toBeVisible(); + await expect(banner).toContainText( + "GitHub features are disabled because the Evergreen GitHub App is not", + ); + }); + + test("Disables all interactive elements on the page", async ({ + authenticatedPage: page, + }) => { + const settingsPage = page.getByTestId("project-settings-page"); + const buttons = settingsPage.getByRole("button"); + for (const button of await buttons.all()) { + await expect(button).toBeDisabled(); + } + const inputs = page.locator("input"); + for (const input of await inputs.all()) { + await expect(input).toBeDisabled(); + } + }); +}); + +test.describe("Git Tags project settings when GitHub webhooks are enabled", () => { + const origin = "/repo/602d70a2b2373672ee493184/settings/git-tags"; + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await expectSaveButtonEnabled(page, false); + }); + + test("Saves successfully when Git Tags are enabled and a Git Tag Definition is provided", async ({ + authenticatedPage: page, + }) => { + const gitTagRadioBox = page.getByTestId("git-tag-enabled-radio-box"); + const enabledRadio = gitTagRadioBox.getByRole("radio", { name: "Enabled" }); + await clickRadio(enabledRadio); + + const errorBanner = page.getByTestId("error-banner").filter({ + hasText: + "A Git Tag Version Definition must be specified for this feature to run.", + }); + await expect(errorBanner).toBeVisible(); + + await page.getByRole("button", { name: "Add git tag" }).click(); + await page.getByTestId("git-tag-input").fill("v*"); + await page.getByTestId("remote-path-input").fill("./evergreen.yml"); + + await expect(page.getByTestId("error-banner")).toBeHidden(); + save(page); + await validateToast(page, "success", "Successfully updated repo"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_app_settings.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_app_settings.spec.ts new file mode 100644 index 0000000000..8fa224d3fb --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_app_settings.spec.ts @@ -0,0 +1,96 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + ProjectSettingsTabRoutes, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("GitHub app settings", () => { + const destination = getProjectSettingsRoute( + "spruce", + ProjectSettingsTabRoutes.GithubAppSettings, + ); + const permissionGroups = { + all: "All app permissions", + readPRs: "Read Pull Requests", + writeIssues: "Write Issues", + }; + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + await expect(page.getByText("Token Permission Restrictions")).toBeVisible(); + }); + + test("save button should be disabled by default", async ({ + authenticatedPage: page, + }) => { + await expectSaveButtonEnabled(page, false); + }); + + test("should be able to replace app credentials", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("replace-app-credentials-button"), + ).toBeVisible(); + await page.getByTestId("replace-app-credentials-button").click(); + + const modal = page.getByTestId("replace-github-credentials-modal"); + await expect(modal).toBeVisible(); + + const confirmButton = modal.getByRole("button", { name: "Replace" }); + await expect(confirmButton).toBeDisabled(); + + await page.getByTestId("replace-app-id-input").fill("99999"); + await page.getByTestId("replace-private-key-input").fill("new-private-key"); + + await expect(confirmButton).toBeEnabled(); + + await confirmButton.click(); + await validateToast( + page, + "success", + "GitHub app credentials were successfully replaced.", + ); + }); + + test("should be able to save different permission groups for requesters, then return to defaults", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("permission-group-input")).toHaveCount(8); + const permissionGroupInput0 = page + .getByTestId("permission-group-input") + .nth(0); + const permissionGroupInput4 = page + .getByTestId("permission-group-input") + .nth(4); + + await permissionGroupInput0.click(); + const options = page.locator('[role="listbox"]'); + await expect(options).toHaveCount(1); + await options.getByText(permissionGroups.readPRs).click(); + await permissionGroupInput4.click(); + await expect(options).toHaveCount(1); + await options.getByText(permissionGroups.writeIssues).click(); + await expectSaveButtonEnabled(page, true); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await page.reload(); + await expect(permissionGroupInput0).toContainText(permissionGroups.readPRs); + await expect(permissionGroupInput4).toContainText( + permissionGroups.writeIssues, + ); + + await permissionGroupInput0.click(); + await expect(options).toHaveCount(1); + await options.getByText(permissionGroups.all).click(); + await permissionGroupInput4.click(); + await expect(options).toHaveCount(1); + await options.getByText(permissionGroups.all).click(); + await expectSaveButtonEnabled(page, true); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_permission_groups.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_permission_groups.spec.ts new file mode 100644 index 0000000000..42772abf2b --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_permission_groups.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + ProjectSettingsTabRoutes, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("GitHub permission groups", () => { + const destination = getProjectSettingsRoute( + "logkeeper", + ProjectSettingsTabRoutes.GithubPermissionGroups, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + await expect(page.getByText("Token Permission Groups")).toBeVisible(); + }); + + test("should not have any permission groups defined", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("permission-group")).toHaveCount(0); + await expectSaveButtonEnabled(page, false); + }); + + test("should throw an error if permission group definitions are invalid", async ({ + authenticatedPage: page, + }) => { + const addPermissionGroupButton = page.getByRole("button", { + name: /^Add permission group$/, + }); + await expect(addPermissionGroupButton).toBeVisible(); + await addPermissionGroupButton.click(); + await expect(page.getByTestId("permission-group")).toHaveCount(1); + + const invalidGithubPermission = "invalid_github_permission"; + await page + .getByTestId("permission-group-title-input") + .fill("test permission group"); + await expect(page.getByTestId("add-permission-button")).toBeVisible(); + await page.getByTestId("add-permission-button").click(); + await page + .getByTestId("permission-type-input") + .fill(invalidGithubPermission); + await page.getByTestId("permission-value-input").click(); + await page.getByText("Write").click(); + await expectSaveButtonEnabled(page, true); + await save(page); + await validateToast(page, "error", "There was an error saving the project"); + }); + + test("should be able to save permission group, then delete it", async ({ + authenticatedPage: page, + }) => { + const addPermissionGroupButton = page.getByRole("button", { + name: /^Add permission group$/, + }); + await expect(addPermissionGroupButton).toBeVisible(); + await addPermissionGroupButton.click(); + await expect(page.getByTestId("permission-group")).toHaveCount(1); + + await page + .getByTestId("permission-group-title-input") + .fill("test permission group"); + await expect(page.getByTestId("add-permission-button")).toBeVisible(); + await page.getByTestId("add-permission-button").click(); + await page.getByTestId("permission-type-input").fill("actions"); + await page.getByTestId("permission-value-input").click(); + await page.getByText("Read").click(); + await expectSaveButtonEnabled(page, true); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await page.reload(); + await expect(page.getByTestId("permission-group")).toHaveCount(1); + await page.getByTestId("delete-item-button").click(); + await expect(page.getByTestId("permission-group")).toHaveCount(0); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_section.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_section.spec.ts new file mode 100644 index 0000000000..481313b154 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/github_section.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast, clickRadio } from "../../../helpers"; +import { getProjectSettingsRoute, project } from "../constants"; +import { save } from "../utils"; + +test.describe("GitHub page", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await page.getByTestId("navitem-github-commitqueue").click(); + }); + + test("Allows adding a git tag alias", async ({ authenticatedPage: page }) => { + const enabledRadio = page + .getByTestId("git-tag-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(enabledRadio); + await page.getByRole("button", { name: "Add Git Tag" }).click(); + await page.getByTestId("git-tag-input").fill("myGitTag"); + await page.getByTestId("remote-path-input").fill("./evergreen.yml"); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect(page.getByTestId("remote-path-input")).toHaveValue( + "./evergreen.yml", + ); + }); +}); diff --git a/apps/spruce/playwright/tests/projectSettings/merge_queue.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/merge_queue.spec.ts similarity index 50% rename from apps/spruce/playwright/tests/projectSettings/merge_queue.spec.ts rename to apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/merge_queue.spec.ts index 9dbe93942d..316d39967b 100644 --- a/apps/spruce/playwright/tests/projectSettings/merge_queue.spec.ts +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/merge_queue.spec.ts @@ -1,22 +1,20 @@ -import { SEEN_GITHUB_NAV_GUIDE_CUE } from "../../../src/constants/cookies"; -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { test, expect } from "../../../fixtures"; +import { clickRadio, validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + ProjectSettingsTabRoutes, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; test.describe("Merge Queue project settings when GitHub webhooks are disabled", () => { + const origin = getProjectSettingsRoute( + "logkeeper", + ProjectSettingsTabRoutes.MergeQueue, + ); + test.beforeEach(async ({ authenticatedPage: page }) => { - await page.context().addCookies([ - { - name: SEEN_GITHUB_NAV_GUIDE_CUE, - value: "true", - domain: "localhost", - path: "/", - }, - ]); - await page.goto("/project/logkeeper/settings/merge-queue"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); + await page.goto(origin); + await expectSaveButtonEnabled(page, false); }); test("Merge Queue page shows a disabled webhooks banner when webhooks are disabled", async ({ @@ -33,39 +31,36 @@ test.describe("Merge Queue project settings when GitHub webhooks are disabled", authenticatedPage: page, }) => { const settingsPage = page.getByTestId("project-settings-page"); - await expect( - settingsPage.locator('button:not([aria-disabled="true"])'), - ).toHaveCount(0); - await expect(page.locator('input:not([aria-disabled="true"])')).toHaveCount( - 0, - ); + const buttons = settingsPage.getByRole("button"); + for (const button of await buttons.all()) { + await expect(button).toBeDisabled(); + } + const inputs = page.locator("input"); + for (const input of await inputs.all()) { + await expect(input).toBeDisabled(); + } }); }); test.describe("Merge Queue project settings when GitHub webhooks are enabled", () => { + const origin = getProjectSettingsRoute( + "spruce", + ProjectSettingsTabRoutes.MergeQueue, + ); + test.beforeEach(async ({ authenticatedPage: page }) => { - await page.context().addCookies([ - { - name: SEEN_GITHUB_NAV_GUIDE_CUE, - value: "true", - domain: "localhost", - path: "/", - }, - ]); - await page.goto("/project/spruce/settings/merge-queue"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); + await page.goto(origin); + await expectSaveButtonEnabled(page, false); }); test("Enabling merge queue shows hidden inputs and error banner", async ({ authenticatedPage: page, }) => { - await page - .getByTestId("mq-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .click(); + const radioBox = page.getByTestId("mq-enabled-radio-box"); + const mergeQueueEnabledRadio = radioBox.getByRole("radio", { + name: "Enabled", + }); + await clickRadio(mergeQueueEnabledRadio); await expect(page.getByText("Merge Queue Patch Definitions")).toBeVisible(); const errorBanner = page.getByTestId("error-banner"); @@ -78,23 +73,20 @@ test.describe("Merge Queue project settings when GitHub webhooks are enabled", ( test("Saves a merge queue definition", async ({ authenticatedPage: page, }) => { - await page - .getByTestId("mq-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .click(); + const radioBox = page.getByTestId("mq-enabled-radio-box"); + const mergeQueueEnabledRadio = radioBox.getByRole("radio", { + name: "Enabled", + }); + await clickRadio(mergeQueueEnabledRadio); + await page .getByRole("button", { name: "Add merge queue patch definition" }) .click(); await page.getByTestId("variant-tags-input").first().fill("vtag"); await page.getByTestId("task-tags-input").first().fill("ttag"); - const saveButton = page.getByTestId("save-settings-button"); - await expect(saveButton).toBeEnabled(); - await saveButton.click(); - const modal = page.getByTestId("save-changes-modal"); - await expect(modal).toBeVisible(); - await modal.getByRole("button", { name: "Save changes" }).click(); - await expect(modal).toBeHidden(); + await expect(page.getByTestId("error-banner")).toBeHidden(); + await save(page); await validateToast(page, "success", "Successfully updated project"); }); }); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/navigation.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/navigation.spec.ts new file mode 100644 index 0000000000..d8d4eed398 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/navigation.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from "../../../fixtures"; +import { getProjectSettingsRoute, project } from "../constants"; + +test.describe("navigation", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test("headers (repos) are clickable in project select dropdown", async ({ + authenticatedPage: page, + }) => { + const projectSelect = page.getByTestId("project-select"); + await expect(projectSelect).toBeVisible(); + await projectSelect.click(); + const projectSelectOptions = page.getByTestId("project-select-options"); + await expect(projectSelectOptions).toBeVisible(); + await projectSelectOptions.getByText("evergreen-ci/evergreen").click(); + await expect(page).not.toHaveURL(new RegExp(origin)); + }); + + test.describe("project ID should redirect to the project identifier", () => { + const projectId = "602d70a2b2373672ee493189"; + const origin = getProjectSettingsRoute(projectId); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test("Redirects to the project identifier", async ({ + authenticatedPage: page, + }) => { + await expect(page).toHaveURL( + new RegExp(getProjectSettingsRoute("parsley")), + ); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/not_defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/not_defaulting_to_repo.spec.ts new file mode 100644 index 0000000000..328d7c9155 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/not_defaulting_to_repo.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast } from "../../../helpers"; +import { getProjectSettingsRoute, project } from "../constants"; +import { expectSaveButtonEnabled } from "../utils"; + +test.describe("Project Settings when not defaulting to repo", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await expectSaveButtonEnabled(page, false); + }); + + test("Does not show a 'Default to Repo' button on page", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("default-to-repo-button")).toHaveCount(0); + }); + + test("Shows two radio boxes", async ({ authenticatedPage: page }) => { + await expect( + page.getByTestId("enabled-radio-box").locator("> *"), + ).toHaveCount(2); + }); + + test("Successfully attaches to and detaches from a repo that does not yet exist and shows 'Default to Repo' options", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("attach-repo-button").click(); + await page + .getByTestId("attach-repo-modal") + .getByRole("button", { name: "Attach" }) + .click(); + await validateToast(page, "success", "Successfully attached to repo", true); + await page.getByTestId("attach-repo-button").click(); + await page + .getByTestId("attach-repo-modal") + .getByRole("button", { name: "Detach" }) + .click(); + await validateToast(page, "success", "Successfully detached from repo"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/notifications.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/notifications.spec.ts new file mode 100644 index 0000000000..d701932c51 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/notifications.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast, selectOption } from "../../../helpers"; +import { + getProjectSettingsRoute, + ProjectSettingsTabRoutes, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("Notifications", () => { + const origin = getProjectSettingsRoute( + "evergreen", + ProjectSettingsTabRoutes.Notifications, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test("shows correct initial state", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("default-to-repo-button")).toHaveCount(0); + await expect(page.getByText("No subscriptions are defined.")).toBeVisible(); + await expectSaveButtonEnabled(page, false); + }); + + test("should be able to add a subscription, save it and delete it", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("expandable-card")).toHaveCount(0); + const addSubscriptionButton = page.getByRole("button", { + name: "Add Subscription", + }); + await expect(addSubscriptionButton).toBeVisible(); + await addSubscriptionButton.click(); + await expect(page.getByTestId("expandable-card")).toContainText( + "New Subscription", + ); + await selectOption(page, "Event", "Any version finishes"); + await selectOption(page, "Notification Method", "Email"); + await page.getByTestId("email-input").fill("mohamed.khelif@mongodb.com"); + await save(page); + await validateToast(page, "success", "Successfully updated project", true); + + await expectSaveButtonEnabled(page, false); + const subscriptionItem = page.getByTestId("expandable-card"); + await expect(subscriptionItem).toBeVisible(); + await expect(subscriptionItem).toContainText( + "Version outcome - mohamed.khelif@mongodb.com", + ); + await page.getByTestId("delete-item-button").click(); + await expect(subscriptionItem).toHaveCount(0); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); + + test("should not be able to combine a jira comment subscription with a task event", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("expandable-card")).toHaveCount(0); + const addSubscriptionButton = page.getByRole("button", { + name: "Add Subscription", + }); + await expect(addSubscriptionButton).toBeVisible(); + await addSubscriptionButton.click(); + const expandableCard = page.getByTestId("expandable-card"); + await expect(expandableCard).toBeVisible(); + await expect(expandableCard).toContainText("New Subscription"); + await selectOption(page, "Event", "Any task finishes"); + await selectOption(page, "Notification Method", "Comment on a JIRA issue"); + await page.getByTestId("jira-comment-input").fill("JIRA-123"); + await expect( + page.getByText("Subscription type not allowed for tasks in a project."), + ).toBeVisible(); + await expectSaveButtonEnabled(page, false); + }); + + test("should not be able to save a subscription if an input is invalid", async ({ + authenticatedPage: page, + }) => { + const addSubscriptionButton = page.getByRole("button", { + name: "Add Subscription", + }); + await expect(addSubscriptionButton).toBeVisible(); + await addSubscriptionButton.click(); + const expandableCard = page.getByTestId("expandable-card"); + await expect(expandableCard).toBeVisible(); + await expect(expandableCard).toContainText("New Subscription"); + await selectOption(page, "Event", "Any version finishes"); + await selectOption(page, "Notification Method", "Email"); + await page.getByTestId("email-input").fill("Not a real email"); + await expect( + page.getByText("Value should be a valid email."), + ).toBeVisible(); + await expectSaveButtonEnabled(page, false); + }); + + test("Setting a project banner displays the banner on the correct pages and unsetting it removes it", async ({ + authenticatedPage: page, + }) => { + const bannerText = "This is a project banner!"; + + await page.getByTestId("banner-text").clear(); + await page.getByTestId("banner-text").fill(bannerText); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await expect(page.getByText(bannerText)).toBeVisible(); + + const taskRoute = + "task/evergreen_ubuntu1604_test_model_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48"; + await page.goto(taskRoute); + await expect(page.getByText(bannerText)).toBeVisible(); + + await page.goto("patch/5e6bb9e23066155a993e0f1b/configure/tasks"); + await expect(page.getByText(bannerText)).toBeVisible(); + + await page.goto("version/5e4ff3abe3c3317e352062e4"); + await expect(page.getByText(bannerText)).toBeVisible(); + + await page.goto("project/evergreen/waterfall"); + await expect(page.getByText(bannerText)).toBeVisible(); + + await page.goto("variant-history/evergreen/ubuntu1604"); + await expect(page.getByText(bannerText)).toBeVisible(); + + await page.goto(origin); + await page.getByTestId("banner-text").clear(); + await save(page); + + await expect(page.getByText(bannerText)).toHaveCount(0); + + await page.goto(taskRoute); + await expect(page.getByText(bannerText)).toHaveCount(0); + + await page.goto("patch/5e6bb9e23066155a993e0f1b/configure/tasks"); + await expect(page.getByText(bannerText)).toHaveCount(0); + + await page.goto("version/5e4ff3abe3c3317e352062e4"); + await expect(page.getByText(bannerText)).toHaveCount(0); + + await page.goto("project/evergreen/waterfall"); + await expect(page.getByText(bannerText)).toHaveCount(0); + + await page.goto("variant-history/evergreen/ubuntu1604"); + await expect(page.getByText(bannerText)).toHaveCount(0); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/periodic_builds.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/periodic_builds.spec.ts new file mode 100644 index 0000000000..303dca3fd8 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/periodic_builds.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from "../../../fixtures"; +import { + validateToast, + validateDatePickerDate, + clearDatePickerInput, + typeDatePickerDate, +} from "../../../helpers"; +import { getProjectSettingsRoute, project } from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("Periodic Builds page", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await page.getByTestId("navitem-periodic-builds").click(); + }); + + test("allows a user to schedule the next build on the current day", async ({ + authenticatedPage: page, + }) => { + await page.clock.setFixedTime(new Date(2025, 8, 16)); + await page.reload(); + await page.getByTestId("navitem-periodic-builds").click(); + await page.getByRole("button", { name: "Add periodic build" }).click(); + await validateDatePickerDate(page, "date-picker", { + year: "2025", + month: "09", + day: "16", + }); + await clearDatePickerInput(page); + + await typeDatePickerDate(page, { year: "2025", month: "01", day: "01" }); + await validateDatePickerDate(page, "date-picker", { + year: "2025", + month: "01", + day: "01", + }); + await expect(page.getByText("Date must be after")).toBeVisible(); + await clearDatePickerInput(page); + + await typeDatePickerDate(page, { year: "2025", month: "09", day: "20" }); + await validateDatePickerDate(page, "date-picker", { + year: "2025", + month: "09", + day: "20", + }); + await expect(page.getByText("Date must be after")).toHaveCount(0); + await clearDatePickerInput(page); + + await typeDatePickerDate(page, { year: "2025", month: "09", day: "16" }); + await validateDatePickerDate(page, "date-picker", { + year: "2025", + month: "09", + day: "16", + }); + await expect(page.getByText("Date must be after")).toHaveCount(0); + }); + + test("Disables save button when interval is NaN or below minimum and allows saving a number in range", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add periodic build" }).click(); + const intervalInput = page.getByTestId("interval-input"); + await intervalInput.fill("NaN"); + await page.getByTestId("config-file-input").fill("config.yml"); + await expectSaveButtonEnabled(page, false); + await expect(page.getByText("Value should be a number.")).toBeVisible(); + await intervalInput.clear(); + await intervalInput.fill("0"); + await expectSaveButtonEnabled(page, false); + await intervalInput.clear(); + await intervalInput.fill("12"); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/permissions.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/permissions.spec.ts new file mode 100644 index 0000000000..a566909b05 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/permissions.spec.ts @@ -0,0 +1,34 @@ +import { users } from "@evg-ui/playwright-config/constants"; +import { test, expect } from "../../../fixtures"; +import { login, logout } from "../../../helpers"; +import { getProjectSettingsRoute, projectUseRepoEnabled } from "../constants"; + +test.describe("permissions", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await logout(page); + }); + + test.describe("projects", () => { + test("disables fields when user lacks edit permissions", async ({ + authenticatedPage: page, + }) => { + await login(page, users.privileged); + await page.goto(getProjectSettingsRoute(projectUseRepoEnabled)); + const settingsPage = page.getByTestId("project-settings-page"); + await expect( + settingsPage.locator('input[type="radio"]').first(), + ).toBeDisabled(); + }); + + test("enables fields if user has edit permissions", async ({ + authenticatedPage: page, + }) => { + await login(page, users.admin); + await page.goto(getProjectSettingsRoute(projectUseRepoEnabled)); + const settingsPage = page.getByTestId("project-settings-page"); + await expect( + settingsPage.locator('input[type="radio"]').first(), + ).toBeEnabled(); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/plugins.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/plugins.spec.ts new file mode 100644 index 0000000000..2f6b48f521 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/plugins.spec.ts @@ -0,0 +1,90 @@ +import { Page } from "@playwright/test"; +import { test, expect } from "../../../fixtures"; +import { + getProjectSettingsRoute, + ProjectSettingsTabRoutes, + projectUseRepoEnabled, +} from "../constants"; +import { save } from "../utils"; + +test.describe("Plugins", () => { + const patchPage = "version/5ecedafb562343215a7ff297"; + + const addMetadataLink = async ( + page: Page, + metadataLink: { displayName: string; url: string }, + ) => { + await page.getByRole("button", { name: "Add metadata link" }).click(); + + const mostRecentMetadataLink = page.getByTestId("metadata-link").first(); + await mostRecentMetadataLink.getByTestId("requesters-input").click(); + + const options = mostRecentMetadataLink.getByTestId("tree-select-options"); + await expect(options).toBeVisible(); + await options.getByText("Patches").click(); + + await mostRecentMetadataLink.getByTestId("requesters-input").click(); + await mostRecentMetadataLink + .getByTestId("display-name-input") + + .fill(metadataLink.displayName); + await mostRecentMetadataLink + .getByTestId("url-template-input") + .fill(metadataLink.url); + }; + + test("Should be able to set external links to render on patch metadata panel", async ({ + authenticatedPage: page, + }) => { + await page.goto( + getProjectSettingsRoute( + projectUseRepoEnabled, + ProjectSettingsTabRoutes.Plugins, + ), + ); + await addMetadataLink(page, { + displayName: "An external link 1", + url: "https://example-1.com/{version_id}", + }); + await addMetadataLink(page, { + displayName: "An external link 2", + url: "https://example-2.com/{version_id}", + }); + await expect(page.getByTestId("metadata-link")).toHaveCount(2); + await save(page); + + await page.goto(patchPage); + await expect(page.getByText("Patch Metadata")).toBeVisible(); + await expect(page.getByTestId("user-patches-link")).toBeVisible(); + await expect(page.getByTestId("external-link")).toHaveCount(2); + await expect(page.getByTestId("external-link").last()).toContainText( + "An external link 1", + ); + await expect(page.getByTestId("external-link").last()).toHaveAttribute( + "href", + "https://example-1.com/5ecedafb562343215a7ff297", + ); + await expect(page.getByTestId("external-link").first()).toContainText( + "An external link 2", + ); + await expect(page.getByTestId("external-link").first()).toHaveAttribute( + "href", + "https://example-2.com/5ecedafb562343215a7ff297", + ); + + await page.goto( + getProjectSettingsRoute( + projectUseRepoEnabled, + ProjectSettingsTabRoutes.Plugins, + ), + ); + await page.getByTestId("delete-item-button").first().click(); + await page.getByTestId("delete-item-button").first().click(); + await expect(page.getByTestId("metadata-link")).toHaveCount(0); + await save(page); + + await page.goto(patchPage); + await expect(page.getByText("Patch Metadata")).toBeVisible(); + await expect(page.getByTestId("external-link")).toHaveCount(0); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/project_triggers.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/project_triggers.spec.ts new file mode 100644 index 0000000000..782b6c8a67 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/project_triggers.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from "../../../fixtures"; +import { getProjectSettingsRoute, project } from "../constants"; + +test.describe("Project Triggers page", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await page.getByTestId("navitem-project-triggers").click(); + }); + + test("Saves a project trigger", async ({ authenticatedPage: page }) => { + await page.getByRole("button", { name: "Add project trigger" }).click(); + await expect(page.getByTestId("project-input")).toBeVisible(); + await expect(page.getByTestId("project-input")).toBeEnabled(); + await page.getByTestId("project-input").fill("spruce"); + await page.getByTestId("config-file-input").fill(".evergreen.yml"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/pull_requests.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/pull_requests.spec.ts new file mode 100644 index 0000000000..2d2fbb395a --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/pull_requests.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from "../../../fixtures"; +import { clickRadio, validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + getRepoSettingsRoute, + projectUseRepoEnabled, + ProjectSettingsTabRoutes, + repo, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("A project that has GitHub webhooks disabled", () => { + const destination = getProjectSettingsRoute( + "logkeeper", + ProjectSettingsTabRoutes.PullRequests, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + await expectSaveButtonEnabled(page, false); + }); + + test("Pull Requests page shows a disabled webhooks banner when webhooks are disabled", async ({ + authenticatedPage: page, + }) => { + const banner = page.getByTestId("disabled-webhook-banner"); + await expect(banner).toBeVisible(); + await expect(banner).toContainText( + "GitHub features are disabled because the Evergreen GitHub App is not", + ); + }); + + test("Disables all interactive elements on the page", async ({ + authenticatedPage: page, + }) => { + const settingsPage = page.getByTestId("project-settings-page"); + const buttons = settingsPage.getByRole("button"); + for (const button of await buttons.all()) { + await expect(button).toBeDisabled(); + } + const inputs = page.locator("input"); + for (const input of await inputs.all()) { + await expect(input).toBeDisabled(); + } + }); +}); + +test.describe("A project that has GitHub webhooks enabled", () => { + const destination = getRepoSettingsRoute( + repo, + ProjectSettingsTabRoutes.PullRequests, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + await expectSaveButtonEnabled(page, false); + }); + + test("Allows enabling manual PR testing", async ({ + authenticatedPage: page, + }) => { + const radioBox = page.getByTestId("manual-pr-testing-enabled-radio-box"); + const enabledRadio = radioBox.getByRole("radio", { name: "Enabled" }); + await clickRadio(enabledRadio); + await expect(enabledRadio).toBeChecked(); + }); + + test("Saving a patch definition should hide the error banner, show a success toast and display disabled patch definitions for the repo on the project page", async ({ + authenticatedPage: page, + }) => { + const errorBanner = page.getByTestId("error-banner").filter({ + hasText: + "A GitHub Patch Definition must be specified for this feature to run.", + }); + await expect(errorBanner).toBeVisible(); + + await page.getByRole("button", { name: "Add patch definition" }).click(); + await expect(errorBanner).toBeHidden(); + await expectSaveButtonEnabled(page, false); + + await page.getByTestId("variant-tags-input").first().fill("vtag"); + await page.getByTestId("task-tags-input").first().fill("ttag"); + await expectSaveButtonEnabled(page, true); + + await save(page); + await validateToast(page, "success", "Successfully updated repo"); + + await page.goto( + getProjectSettingsRoute( + projectUseRepoEnabled, + ProjectSettingsTabRoutes.PullRequests, + ), + ); + + const patchDefAccordion = page.getByText("Repo Patch Definition 1"); + await patchDefAccordion.click(); + + const variantTagsInput = page.getByTestId("variant-tags-input"); + await expect(variantTagsInput).toHaveValue("vtag"); + await expect(variantTagsInput).toBeDisabled(); + + const taskTagsInput = page.getByTestId("task-tags-input"); + await expect(taskTagsInput).toHaveValue("ttag"); + await expect(taskTagsInput).toBeDisabled(); + + await expect(errorBanner).toBeHidden(); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/variables.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/variables.spec.ts new file mode 100644 index 0000000000..93730d61ba --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/variables.spec.ts @@ -0,0 +1,145 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast, clickCheckbox } from "../../../helpers"; +import { getProjectSettingsRoute, project } from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("Variables page", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await page.getByTestId("navitem-variables").click(); + }); + + test("Should not have the save button enabled on load", async ({ + authenticatedPage: page, + }) => { + await expectSaveButtonEnabled(page, false); + }); + + test("Should not show the move variables button", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("promote-vars-button")).toHaveCount(0); + }); + + test("Should redact and disable private variables on saving", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").fill("sample_name"); + await expectSaveButtonEnabled(page, false); + await page.getByTestId("var-value-input").fill("sample_value"); + await page.getByTestId("var-description-input").fill("Sample description"); + const privateCheckbox = page.getByRole("checkbox", { + name: "Private", + }); + const adminOnlyCheckbox = page.getByRole("checkbox", { + name: "Admin Only", + }); + await expect(privateCheckbox).toBeChecked(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect(page.getByTestId("var-value-input")).toHaveValue("{REDACTED}"); + await expect(page.getByTestId("var-name-input")).toBeDisabled(); + await expect(page.getByTestId("var-value-input")).toBeDisabled(); + await expect(privateCheckbox).toBeDisabled(); + await expect(adminOnlyCheckbox).toBeEnabled(); + await expect(page.getByTestId("var-description-input")).toBeEnabled(); + }); + + test("Typing a duplicate variable name will disable saving and show an error message", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").fill("sample_name"); + await page.getByTestId("var-value-input").fill("sample_value"); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").first().fill("sample_name"); + await page.getByTestId("var-value-input").first().fill("sample_value_2"); + const errorMessage = page.getByText( + "Value already appears in project variables.", + ); + await expect(errorMessage).toBeVisible(); + await expectSaveButtonEnabled(page, false); + + await page.getByTestId("var-name-input").first().fill("sample_name_2"); + await expectSaveButtonEnabled(page, true); + await expect(errorMessage).toHaveCount(0); + }); + + test("Should correctly save an admin only variable", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").first().fill("admin_var"); + await page.getByTestId("var-value-input").first().fill("admin_value"); + const adminOnlyCheckbox = page.getByRole("checkbox", { + name: "Admin Only", + }); + await clickCheckbox(adminOnlyCheckbox); + await expect(adminOnlyCheckbox).toBeChecked(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); + + test("Should persist saved variables and allow deletion", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").fill("sample_name"); + await page.getByTestId("var-value-input").fill("sample_value"); + await page + .getByTestId("var-description-input") + .fill("Description for sample_name"); + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").first().fill("sample_name_2"); + await page.getByTestId("var-value-input").first().fill("sample_value"); + await page + .getByTestId("var-description-input") + .first() + .fill("Description for sample_name_2"); + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").first().fill("admin_var"); + await page.getByTestId("var-value-input").first().fill("admin_value"); + await page + .getByTestId("var-description-input") + .first() + .fill("Description for admin_var"); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await page.reload(); + await expect(page.getByTestId("var-name-input").nth(0)).toHaveValue( + "admin_var", + ); + await expect(page.getByTestId("var-description-input").nth(0)).toHaveValue( + "Description for admin_var", + ); + await expect(page.getByTestId("var-name-input").nth(1)).toHaveValue( + "sample_name", + ); + await expect(page.getByTestId("var-description-input").nth(1)).toHaveValue( + "Description for sample_name", + ); + await expect(page.getByTestId("var-name-input").nth(2)).toHaveValue( + "sample_name_2", + ); + await expect(page.getByTestId("var-description-input").nth(2)).toHaveValue( + "Description for sample_name_2", + ); + + await page.getByTestId("delete-item-button").first().click(); + await page.getByTestId("delete-item-button").first().click(); + await page.getByTestId("delete-item-button").first().click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect(page.getByTestId("var-name-input")).toHaveCount(0); + + await page.reload(); + await expectSaveButtonEnabled(page, false); + await expect(page.getByTestId("var-name-input")).toHaveCount(0); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/views_and_filters.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/views_and_filters.spec.ts new file mode 100644 index 0000000000..af7c11a8ea --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/projectSettings/views_and_filters.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + ProjectSettingsTabRoutes, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("Views & filters page", () => { + const destination = getProjectSettingsRoute( + "sys-perf", + ProjectSettingsTabRoutes.ViewsAndFilters, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(destination); + await expect(page.getByTestId("parsley-filter")).toHaveCount(2); + await expectSaveButtonEnabled(page, false); + }); + + test.describe("parsley filters", () => { + test("does not allow saving with invalid regular expression or empty expression", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add filter" }).click(); + await page.getByTestId("parsley-filter-expression").first().fill("*"); + await expectSaveButtonEnabled(page, false); + await expect( + page.getByText("Value should be a valid regex expression."), + ).toBeVisible(); + await page.getByTestId("parsley-filter-expression").first().clear(); + await expectSaveButtonEnabled(page, false); + }); + + test("does not allow saving with duplicate filter expressions", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add filter" }).click(); + await page + .getByTestId("parsley-filter-expression") + .first() + .fill("filter_1"); + await expectSaveButtonEnabled(page, false); + await expect( + page.getByText("Filter expression already appears in this project."), + ).toBeVisible(); + }); + + test("can successfully save and delete filter", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add filter" }).click(); + await page + .getByTestId("parsley-filter-expression") + .first() + .fill("my_filter"); + await expectSaveButtonEnabled(page, true); + await save(page); + await validateToast( + page, + "success", + "Successfully updated project", + true, + ); + await expect(page.getByTestId("parsley-filter")).toHaveCount(3); + + await page.getByTestId("delete-item-button").first().click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect(page.getByTestId("parsley-filter")).toHaveCount(2); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/attaching_to_repo.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/attaching_to_repo.spec.ts new file mode 100644 index 0000000000..1d1bce2ba8 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/attaching_to_repo.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from "../../../fixtures"; +import { clickRadio, validateToast } from "../../../helpers"; +import { getProjectSettingsRoute, project } from "../constants"; +import { save } from "../utils"; + +test.describe("Attaching Spruce to a repo", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test("Saves and attaches new repo and shows warnings on the Github page", async ({ + authenticatedPage: page, + }) => { + const repoInput = page.getByTestId("repo-input"); + await repoInput.clear(); + await repoInput.fill("evergreen"); + await expect(page.getByTestId("attach-repo-button")).toBeDisabled(); + await save(page); + await validateToast(page, "success", "Successfully updated project", true); + + await page.getByRole("button", { name: "Attach to current repo" }).click(); + await page + .getByTestId("attach-repo-modal") + .getByRole("button", { name: "Attach" }) + .click(); + await validateToast(page, "success", "Successfully attached to repo"); + + await page.getByTestId("navitem-github-commitqueue").click(); + await expect( + page + .getByTestId("pr-testing-enabled-radio-box") + .locator("..") + .getByTestId("warning-banner"), + ).toBeVisible(); + await expect( + page + .getByTestId("manual-pr-testing-enabled-radio-box") + .locator("..") + .getByTestId("warning-banner"), + ).toBeVisible(); + await expect( + page + .getByTestId("github-checks-enabled-radio-box") + .locator("..") + .getByTestId("warning-banner"), + ).toHaveCount(0); + await expect( + page.getByTestId("cq-card").getByTestId("warning-banner"), + ).toBeVisible(); + + const mergeQueueEnabledRadio = page + .getByTestId("cq-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(mergeQueueEnabledRadio); + await expect( + page.getByTestId("cq-card").getByTestId("error-banner"), + ).toBeVisible(); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/defaulting_to_repo.spec.ts new file mode 100644 index 0000000000..791193b4d7 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/defaulting_to_repo.spec.ts @@ -0,0 +1,529 @@ +import { test, expect } from "../../../fixtures"; +import { clickCheckbox, clickRadio, validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + getRepoSettingsRoute, + project, + ProjectSettingsTabRoutes, + projectUseRepoEnabled, + repo, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("Project Settings when defaulting to repo", () => { + const origin = getProjectSettingsRoute(projectUseRepoEnabled); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test.describe("General Settings page", () => { + test("Save button is disabled on load and shows a link to the repo", async ({ + authenticatedPage: page, + }) => { + await expectSaveButtonEnabled(page, false); + await expect(page.getByTestId("attached-repo-link")).toHaveAttribute( + "href", + `${getRepoSettingsRoute(repo)}`, + ); + }); + + test("Preserves edits to the form when navigating between settings tabs and does not show a warning modal", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("spawn-host-input")).toHaveValue("/path"); + await page.getByTestId("spawn-host-input").fill("/path/test"); + await expectSaveButtonEnabled(page, true); + await page.getByTestId("navitem-access").click(); + await expect(page.getByTestId("navigation-warning-modal")).toHaveCount(0); + await page.getByTestId("navitem-general").click(); + await expect(page.getByTestId("spawn-host-input")).toHaveValue( + "/path/test", + ); + await expectSaveButtonEnabled(page, true); + }); + + test("Shows a 'Default to Repo' button on page", async ({ + authenticatedPage: page, + }) => { + const defaultToRepoButton = page.getByRole("button", { + name: "Default to repo on page", + }); + await expect(defaultToRepoButton).toBeVisible(); + }); + + test("Shows only two radio boxes even when rendering a project that inherits from repo", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("enabled-radio-box").locator("> *"), + ).toHaveCount(2); + }); + + test("Does not default to repo value for display name", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("display-name-input")).not.toHaveAttribute( + "placeholder", + ); + }); + + test("Shows a navigation warning modal that lists the general page when navigating away from project settings", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("spawn-host-input").fill("/path/test"); + await expectSaveButtonEnabled(page, true); + await page.getByText("My Patches").click(); + await expect(page.getByTestId("navigation-warning-modal")).toBeVisible(); + await expect(page.getByTestId("unsaved-pages").locator("li")).toHaveCount( + 1, + ); + await page.keyboard.press("Escape"); + }); + + test("Shows the repo value for Batch Time", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("batch-time-input")).toHaveAttribute( + "placeholder", + /.+/, + ); + }); + + test("Clicking on save button should show a success toast", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("spawn-host-input").fill("/path/test"); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); + + test("Saves when batch time is updated", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("batch-time-input").clear(); + await page.getByTestId("batch-time-input").fill("12"); + await save(page); + await expect(page.getByTestId("batch-time-input")).toHaveValue("12"); + await validateToast( + page, + "success", + "Successfully updated project", + true, + ); + + await page.getByTestId("batch-time-input").clear(); + await save(page); + await expect(page.getByTestId("batch-time-input")).toHaveAttribute( + "placeholder", + "60 (Default from repo)", + ); + await validateToast( + page, + "success", + "Successfully updated project", + true, + ); + + await page.getByTestId("attached-repo-link").click(); + await expect(page.getByTestId("batch-time-input")).toHaveValue("60"); + await page.getByTestId("batch-time-input").clear(); + await save(page); + await expect(page.getByTestId("batch-time-input")).toHaveValue("0"); + await validateToast(page, "success", "Successfully updated repo", true); + await page.goto(origin); + await expect(page.getByTestId("batch-time-input")).toHaveAttribute( + "placeholder", + "0 (Default from repo)", + ); + + await page.goto(getProjectSettingsRoute(project)); + await expect(page.getByTestId("batch-time-input")).toHaveValue("60"); + await page.getByTestId("batch-time-input").clear(); + await save(page); + await expect(page.getByTestId("batch-time-input")).toHaveValue("0"); + await validateToast(page, "success", "Successfully updated project"); + }); + }); + + test.describe("Variables page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.getByTestId("navitem-variables").click(); + await expectSaveButtonEnabled(page, false); + }); + + test("Successfully saves variables and then promotes them using the promote variables modal", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").fill("a"); + await page.getByTestId("var-value-input").fill("1"); + await page + .getByTestId("var-description-input") + .fill("Description for variable a"); + const privateCheckbox = page.getByRole("checkbox", { name: "Private" }); + await clickCheckbox(privateCheckbox); + + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").first().fill("b"); + await page.getByTestId("var-value-input").first().fill("2"); + await page + .getByTestId("var-description-input") + .first() + .fill("Description for variable b"); + + await page.getByRole("button", { name: "Add variables" }).click(); + await page.getByTestId("var-name-input").first().fill("c"); + await page.getByTestId("var-value-input").first().fill("3"); + await page + .getByTestId("var-description-input") + .first() + .fill("Description for variable c"); + + await save(page); + await validateToast( + page, + "success", + "Successfully updated project", + true, + ); + + await page.getByTestId("promote-vars-button").click(); + await expect(page.getByTestId("promote-vars-modal")).toBeVisible(); + + await expect(page.getByTestId("promote-var-checkbox")).toHaveCount(3); + const variableToPromoteCheckbox = page + .getByTestId("promote-var-checkbox") + .first(); + await clickCheckbox(variableToPromoteCheckbox); + + await page.getByRole("button", { name: "Move 1 variable" }).click(); + await validateToast( + page, + "success", + "Successfully moved variables to repo", + ); + }); + }); + + test.describe("GitHub page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.getByTestId("navitem-github-commitqueue").click(); + }); + + test("Should not have the save button enabled on load", async ({ + authenticatedPage: page, + }) => { + await expectSaveButtonEnabled(page, false); + }); + + test("Allows overriding repo patch definitions", async ({ + authenticatedPage: page, + }) => { + const githubSection = page.getByTestId("github-card"); + const overrideRepoPatchDefinitionRadio = githubSection.getByRole( + "radio", + { name: "Override Repo Patch Definition", exact: true }, + ); + await clickRadio(overrideRepoPatchDefinitionRadio); + await expect(overrideRepoPatchDefinitionRadio).toHaveAttribute( + "aria-checked", + "true", + ); + + await expect( + githubSection.getByTestId("error-banner").filter({ + hasText: + "A GitHub Patch Definition must be specified for this feature to run.", + }), + ).toBeVisible(); + + await githubSection + .getByRole("button", { name: "Add patch definition" }) + .click(); + await githubSection.getByText("Variant Regex").click(); + await githubSection.getByTestId("variant-input").fill(".*"); + await expectSaveButtonEnabled(page, false); + + await githubSection.getByText("Variant Tags").click(); + await githubSection.getByText("Variant Regex").click(); + await expect(githubSection.getByTestId("variant-input")).toHaveValue( + ".*", + ); + await githubSection.getByText("Task Regex").click(); + await githubSection.getByTestId("task-input").fill(".*"); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); + + test("Shows a warning banner when a commit check definition does not exist", async ({ + authenticatedPage: page, + }) => { + const enabledRadio = page + .getByTestId("github-checks-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(enabledRadio); + await expect( + page.getByTestId("warning-banner").filter({ + hasText: + "This feature will only run if a Commit Check Definition is defined in the project or repo.", + }), + ).toBeVisible(); + }); + + test("Disables Authorized Users section based on repo settings", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByText("Authorized Users")).toHaveCount(0); + await expect(page.getByText("Authorized Teams")).toHaveCount(0); + }); + + test("Defaults to overriding repo since a patch definition is defined", async ({ + authenticatedPage: page, + }) => { + const overrideRepoPatchDefinitionRadio = page + .getByTestId("cq-override-radio-box") + .getByRole("radio", { + name: "Override Repo Patch Definition", + exact: true, + }); + await expect(overrideRepoPatchDefinitionRadio).toHaveAttribute( + "aria-checked", + "true", + ); + }); + + test("Shows the existing patch definition", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("variant-input").last()).toHaveValue( + "^ubuntu1604$", + ); + await expect(page.getByTestId("task-input").last()).toHaveValue( + "^smoke-test-endpoints$", + ); + }); + + test("Returns an error on save because no commit check definitions are defined", async ({ + authenticatedPage: page, + }) => { + const prDisabledRadio = page + .getByTestId("pr-testing-enabled-radio-box") + .getByRole("radio", { name: "Disabled", exact: true }); + await clickRadio(prDisabledRadio); + const manualDisabledRadio = page + .getByTestId("manual-pr-testing-enabled-radio-box") + .getByRole("radio", { name: "Disabled", exact: true }); + await clickRadio(manualDisabledRadio); + const githubEnabledRadio = page + .getByTestId("github-checks-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(githubEnabledRadio); + await save(page); + await validateToast( + page, + "error", + "There was an error saving the project", + ); + }); + + test("Defaults to repo and shows the repo's disabled patch definition", async ({ + authenticatedPage: page, + }) => { + await expect( + page + .getByTestId("accordion-toggle") + .filter({ hasText: "Repo Patch Definition 1" }), + ).toHaveCount(0); + + await page.goto(getRepoSettingsRoute(repo)); + await page.getByTestId("navitem-github-commitqueue").click(); + await page.getByRole("button", { name: "Add Patch Definition" }).click(); + await page.getByTestId("variant-tags-input").first().fill("vtag"); + await page.getByTestId("task-tags-input").first().fill("ttag"); + await save(page); + await validateToast(page, "success", "Successfully updated repo", true); + + await page.goto(origin); + await page.getByTestId("navitem-github-commitqueue").click(); + await expect(page.getByTestId("default-to-repo-button")).toHaveAttribute( + "aria-disabled", + "false", + ); + await page.getByTestId("default-to-repo-button").click(); + await expect(page.getByTestId("default-to-repo-modal")).toBeVisible(); + await page + .getByLabel('Type "confirm" to confirm your action') + .fill("confirm"); + await page + .getByTestId("default-to-repo-modal") + .getByRole("button", { name: "Confirm" }) + .click(); + await validateToast( + page, + "success", + "Successfully defaulted page to repo", + ); + await expect( + page + .getByTestId("accordion-toggle") + .filter({ hasText: "Repo Patch Definition 1" }), + ).toBeVisible(); + }); + }); + + test.describe("Patch Aliases page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.getByTestId("navitem-patch-aliases").click(); + await expectSaveButtonEnabled(page, false); + }); + + test("Defaults to repo patch aliases", async ({ + authenticatedPage: page, + }) => { + const defaultToRepoRadio = page.getByRole("radio", { + name: "Default to Repo Patch Aliases", + }); + await expect(defaultToRepoRadio).toHaveAttribute("checked"); + }); + + test("Patch aliases added before defaulting to repo patch aliases are cleared", async ({ + authenticatedPage: page, + }) => { + const overrideRepoPatchAliasesRadio = page.getByRole("radio", { + name: "Override Repo Patch Aliases", + }); + await clickRadio(overrideRepoPatchAliasesRadio); + await expect(overrideRepoPatchAliasesRadio).toHaveAttribute( + "aria-checked", + "true", + ); + await expectSaveButtonEnabled(page, false); + + await page.getByRole("button", { name: "Add patch alias" }).click(); + await expectSaveButtonEnabled(page, false); + await page.getByTestId("alias-input").fill("my overriden alias name"); + await page + .getByTestId("variant-tags-input") + .first() + .fill("alias variant tag 2"); + await page + .getByTestId("task-tags-input") + .first() + .fill("alias task tag 2"); + await page.getByRole("button", { name: "Add task tag" }).click(); + await page + .getByTestId("task-tags-input") + .first() + .fill("alias task tag 3"); + await save(page); + await validateToast( + page, + "success", + "Successfully updated project", + true, + ); + + const defaultToRepoRadio = page.getByRole("radio", { + name: "Default to Repo Patch Aliases", + }); + await clickRadio(defaultToRepoRadio); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expectSaveButtonEnabled(page, false); + + await clickRadio(overrideRepoPatchAliasesRadio); + await expect(page.getByTestId("alias-row")).toHaveCount(0); + }); + }); + + test.describe("Virtual Workstation page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.getByTestId("navitem-virtual-workstation").click(); + }); + + test("Enable git clone", async ({ authenticatedPage: page }) => { + const githubEnabledRadio = page.getByRole("radio", { name: "Enabled" }); + await clickRadio(githubEnabledRadio); + await expect(githubEnabledRadio).toBeChecked(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); + + test("Add commands", async ({ authenticatedPage: page }) => { + const defaultToRepoRadio = page.getByRole("radio", { + name: "Default to repo (disabled)", + }); + await expect(defaultToRepoRadio).toBeChecked(); + await expect(page.getByTestId("command-row")).toHaveCount(0); + + await page.getByTestId("attached-repo-link").click(); + await expect(page).toHaveURL( + new RegExp( + getRepoSettingsRoute( + repo, + ProjectSettingsTabRoutes.VirtualWorkstation, + ), + ), + ); + await page.getByRole("button", { name: "Add Command" }).click(); + await page.getByTestId("command-input").fill("a repo command"); + await save(page); + await validateToast(page, "success", "Successfully updated repo", true); + + await page.goto(origin); + await page.getByTestId("navitem-virtual-workstation").click(); + + const commandRow = page.getByTestId("command-row"); + + await expect(commandRow).toHaveCount(1); + const commandInput = commandRow.getByRole("textbox", { name: "Command" }); + await expect(commandInput).toHaveValue("a repo command"); + await expect(commandInput).toBeDisabled(); + + const overrideRepoCommandsRadio = page.getByRole("radio", { + name: "Override Repo Commands", + }); + await clickRadio(overrideRepoCommandsRadio); + + await expect(commandRow).toHaveCount(0); + await page.getByRole("button", { name: "Add Command" }).click(); + await commandInput.fill("a project command"); + await save(page); + await validateToast( + page, + "success", + "Successfully updated project", + true, + ); + await expect(commandInput).toHaveValue("a project command"); + await expect(commandInput).toBeEnabled(); + + const defaultToRepoCommandsRadio = page.getByRole("radio", { + name: "Default to Repo Commands", + }); + await clickRadio(defaultToRepoCommandsRadio); + await expect(commandRow).toHaveCount(1); + await expect(commandInput).toHaveValue("a repo command"); + await expect(commandInput).toBeDisabled(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await clickRadio(overrideRepoCommandsRadio); + await expect(commandRow).toHaveCount(0); + }); + + test("Allows overriding without adding a command", async ({ + authenticatedPage: page, + }) => { + const overrideRepoCommandsRadio = page.getByRole("radio", { + name: "Override Repo Commands", + }); + await clickRadio(overrideRepoCommandsRadio); + await expect(overrideRepoCommandsRadio).toBeChecked(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect(overrideRepoCommandsRadio).toBeChecked(); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/general_section.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/general_section.spec.ts new file mode 100644 index 0000000000..dad75f613d --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/general_section.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast } from "../../../helpers"; +import { getRepoSettingsRoute, repo } from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("General settings page", () => { + const origin = getRepoSettingsRoute(repo); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test("Should have the save button disabled on load", async ({ + authenticatedPage: page, + }) => { + await expectSaveButtonEnabled(page, false); + }); + + test("Does not show a 'Default to Repo' button on page", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("default-to-repo-button")).toHaveCount(0); + }); + + test("Does not show a 'Move to New Repo' button on page", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("move-repo-button")).toHaveCount(0); + }); + + test("Does not show an Attach/Detach to Repo button on page", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("attach-repo-button")).toHaveCount(0); + }); + + test("Does not show a 'Go to repo settings' link on page", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("attached-repo-link")).toHaveCount(0); + }); + + test("Inputting a display name then clicking save shows a success toast", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("display-name-input").fill("evg"); + await save(page); + await validateToast(page, "success", "Successfully updated repo"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/github_section.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/github_section.spec.ts new file mode 100644 index 0000000000..cdd1725572 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/github_section.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from "../../../fixtures"; +import { clickRadio, validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + getRepoSettingsRoute, + projectUseRepoEnabled, + repo, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("GitHub page", () => { + const origin = getRepoSettingsRoute(repo); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await page.getByTestId("navitem-github-commitqueue").click(); + await expectSaveButtonEnabled(page, false); + }); + + test.describe("GitHub section", () => { + test("Shows an error banner when Commit Checks are enabled and hides it when Commit Checks are disabled", async ({ + authenticatedPage: page, + }) => { + const githubChecksEnabledRadio = page + .getByTestId("github-checks-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(githubChecksEnabledRadio); + const errorBanner = page.getByTestId("error-banner").filter({ + hasText: + "A Commit Check Definition must be specified for this feature to run.", + }); + await expect(errorBanner).toBeVisible(); + const githubChecksDisabledRadio = page + .getByTestId("github-checks-enabled-radio-box") + .getByRole("radio", { name: "Disabled", exact: true }); + await clickRadio(githubChecksDisabledRadio); + await expect(errorBanner).toHaveCount(0); + }); + + test("Allows enabling manual PR testing", async ({ + authenticatedPage: page, + }) => { + const enabledRadio = page + .getByTestId("manual-pr-testing-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(enabledRadio); + await expect(enabledRadio).toBeChecked(); + }); + + test("Saving a patch definition should hide the error banner, success toast and displays disabled patch definitions for the repo", async ({ + authenticatedPage: page, + }) => { + const errorBanner = page.getByText( + "A GitHub Patch Definition must be specified for this feature to run.", + ); + await expect(errorBanner).toBeVisible(); + await page.getByRole("button", { name: "Add patch definition" }).click(); + await expect(errorBanner).toHaveCount(0); + await expectSaveButtonEnabled(page, false); + await page.getByTestId("variant-tags-input").first().fill("vtag"); + await page.getByTestId("task-tags-input").first().fill("ttag"); + await expectSaveButtonEnabled(page, true); + await save(page); + await validateToast(page, "success", "Successfully updated repo"); + + await page.goto(getProjectSettingsRoute(projectUseRepoEnabled)); + await page.getByTestId("navitem-github-commitqueue").click(); + const patchDefAccordion = page + .getByTestId("accordion-toggle") + .filter({ hasText: "Repo Patch Definition 1" }); + await patchDefAccordion.click(); + await expect(page.getByTestId("variant-tags-input")).toHaveValue("vtag"); + await expect(page.getByTestId("variant-tags-input")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("task-tags-input")).toHaveValue("ttag"); + await expect(page.getByTestId("task-tags-input")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect( + page.getByText( + "A GitHub Patch Definition must be specified for this feature to run.", + ), + ).toHaveCount(0); + }); + }); + + test.describe("Merge Queue section", () => { + test("Enabling merge queue shows hidden inputs and error banner", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByText("Merge Queue Patch Definitions"), + ).toBeHidden(); + + const mergeQueueEnabledRadio = page + .getByTestId("cq-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(mergeQueueEnabledRadio); + + await expect( + page.getByText("Merge Queue Patch Definitions"), + ).toBeVisible(); + await expect( + page.getByTestId("error-banner").filter({ + hasText: + "A Merge Queue Patch Definition must be specified for this feature to run.", + }), + ).toBeVisible(); + }); + + test("Does not show override buttons for merge queue patch definitions", async ({ + authenticatedPage: page, + }) => { + const mergeQueueEnabledRadio = page + .getByTestId("cq-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(mergeQueueEnabledRadio); + await expect(page.getByTestId("cq-override-radio-box")).toHaveCount(0); + }); + + test("Saves a merge queue definition", async ({ + authenticatedPage: page, + }) => { + const mergeQueueEnabledRadio = page + .getByTestId("cq-enabled-radio-box") + .getByRole("radio", { name: "Enabled" }); + await clickRadio(mergeQueueEnabledRadio); + await page.getByRole("button", { name: "Add patch definition" }).click(); + await page.getByTestId("variant-tags-input").first().fill("vtag"); + await page.getByTestId("task-tags-input").first().fill("ttag"); + await expectSaveButtonEnabled(page, false); + await page + .getByRole("button", { name: "Add merge queue patch definition" }) + .click(); + await page.getByTestId("variant-tags-input").last().fill("cqvtag"); + await page.getByTestId("task-tags-input").last().fill("cqttag"); + await expect(page.getByTestId("warning-banner")).toHaveCount(0); + await expect(page.getByTestId("error-banner")).toHaveCount(0); + await save(page); + await validateToast(page, "success", "Successfully updated repo"); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/patch_aliases.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/patch_aliases.spec.ts new file mode 100644 index 0000000000..30a7c1279d --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/patch_aliases.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from "../../../fixtures"; +import { clickRadio, validateToast } from "../../../helpers"; +import { + getProjectSettingsRoute, + getRepoSettingsRoute, + ProjectSettingsTabRoutes, + projectUseRepoEnabled, + repo, +} from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("Patch Aliases page", () => { + const origin = getRepoSettingsRoute(repo); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await page.getByTestId("navitem-patch-aliases").click(); + await expectSaveButtonEnabled(page, false); + await expect( + page.getByTestId("patch-aliases-override-radio-box"), + ).toHaveCount(0); + }); + + test("Saving a patch alias shows a success toast, the alias name in the card title and in the repo defaulted project", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add patch alias" }).click(); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "New Patch Alias" }), + ).toBeVisible(); + await page.getByTestId("alias-input").fill("my alias name"); + await expectSaveButtonEnabled(page, false); + await page + .getByTestId("variant-tags-input") + .first() + .fill("alias variant tag"); + await page.getByTestId("task-tags-input").first().fill("alias task tag"); + await save(page); + await validateToast(page, "success", "Successfully updated repo", true); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "my alias name" }), + ).toBeVisible(); + + await page.reload(); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "my alias name" }), + ).toBeVisible(); + + await page.goto( + getProjectSettingsRoute( + projectUseRepoEnabled, + ProjectSettingsTabRoutes.Access, + ), + ); + await expect(page.getByTestId("default-to-repo-button")).toHaveAttribute( + "aria-disabled", + "false", + ); + await page.getByTestId("default-to-repo-button").click(); + await expect(page.getByTestId("default-to-repo-modal")).toBeVisible(); + await page + .getByLabel('Type "confirm" to confirm your action') + .fill("confirm"); + await page + .getByTestId("default-to-repo-modal") + .getByRole("button", { name: "Confirm" }) + .click(); + await validateToast(page, "success", "Successfully defaulted page to repo"); + await page.getByTestId("navitem-patch-aliases").click(); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "my alias name" }), + ).toBeVisible(); + + const cardTitle = page + .getByTestId("expandable-card-title") + .filter({ hasText: "my alias name" }); + await cardTitle.click(); + await expect( + page.getByTestId("expandable-card").locator("input").first(), + ).toHaveAttribute("aria-disabled", "true"); + }); + + test("Saving a Patch Trigger Alias shows a success toast and updates the Github page", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add patch trigger alias" }).click(); + await page.getByTestId("pta-alias-input").fill("my-alias"); + await page.getByTestId("project-input").fill("spruce"); + await page.getByTestId("module-input").fill("module_name"); + await page.getByText("Variant/Task", { exact: true }).click(); + await page.getByTestId("variant-regex-input").fill(".*"); + await page.getByTestId("task-regex-input").fill(".*"); + + const pullRequestCheckbox = page.getByRole("checkbox", { + name: "Schedule in GitHub Pull Requests", + }); + await expect(pullRequestCheckbox).not.toBeChecked(); + await clickRadio(pullRequestCheckbox); + await expect(pullRequestCheckbox).toBeChecked(); + + const mergeQueueCheckbox = page.getByRole("checkbox", { + name: "Schedule in GitHub Merge Queue", + }); + await expect(mergeQueueCheckbox).not.toBeChecked(); + await clickRadio(mergeQueueCheckbox); + await expect(mergeQueueCheckbox).toBeChecked(); + + await save(page); + await validateToast(page, "success", "Successfully updated repo"); + await expectSaveButtonEnabled(page, false); + + await page.getByTestId("navitem-github-commitqueue").click(); + + const prTriggerAliases = page.getByTestId("github-pr-trigger-aliases"); + await expect(prTriggerAliases.getByTestId("pta-item")).toHaveCount(1); + await expect(prTriggerAliases.getByText("my-alias")).toBeVisible(); + + await prTriggerAliases.getByTestId("pta-item").hover(); + const prTooltip = prTriggerAliases.getByTestId("pta-tooltip"); + await expect(prTooltip).toHaveCount(1); + await expect(prTooltip).toBeVisible(); + await expect(prTooltip).toContainText("spruce"); + await expect(prTooltip).toContainText("module_name"); + await expect(prTooltip).toContainText("Variant/Task Regex Pairs"); + + const mqTriggerAliases = page.getByTestId("github-mq-trigger-aliases"); + await expect(mqTriggerAliases.getByTestId("pta-item")).toHaveCount(1); + await expect(mqTriggerAliases.getByText("my-alias")).toBeVisible(); + + await mqTriggerAliases.getByTestId("pta-item").hover(); + const mqTooltip = mqTriggerAliases.getByTestId("pta-tooltip"); + await expect(mqTooltip).toHaveCount(1); + await expect(mqTooltip).toBeVisible(); + await expect(mqTooltip).toContainText("spruce"); + await expect(mqTooltip).toContainText("module_name"); + await expect(mqTooltip).toContainText("Variant/Task Regex Pairs"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/permissions.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/permissions.spec.ts new file mode 100644 index 0000000000..867b8b49c2 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/permissions.spec.ts @@ -0,0 +1,32 @@ +import { users } from "@evg-ui/playwright-config/constants"; +import { test, expect } from "../../../fixtures"; +import { login, logout } from "../../../helpers"; +import { getRepoSettingsRoute, repo } from "../constants"; + +test.describe("permissions", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await logout(page); + }); + + test("disables fields when user lacks edit permissions", async ({ + authenticatedPage: page, + }) => { + await login(page, users.privileged); + await page.goto(getRepoSettingsRoute(repo)); + const settingsPage = page.getByTestId("repo-settings-page"); + await expect( + settingsPage.locator('input[type="radio"]').first(), + ).toBeDisabled(); + }); + + test("enables fields if user has edit permissions", async ({ + authenticatedPage: page, + }) => { + await login(page, users.admin); + await page.goto(getRepoSettingsRoute(repo)); + const settingsPage = page.getByTestId("repo-settings-page"); + await expect( + settingsPage.locator('input[type="radio"]').first(), + ).toBeEnabled(); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/virtual_workstation.spec.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/virtual_workstation.spec.ts new file mode 100644 index 0000000000..7ab025f083 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/repoSettings/virtual_workstation.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from "../../../fixtures"; +import { validateToast } from "../../../helpers"; +import { getRepoSettingsRoute, repo } from "../constants"; +import { expectSaveButtonEnabled, save } from "../utils"; + +test.describe("Virtual Workstation page", () => { + const origin = getRepoSettingsRoute(repo); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + await page.getByTestId("navitem-virtual-workstation").click(); + }); + + test("Adds two commands and then reorders them", async ({ + authenticatedPage: page, + }) => { + await expectSaveButtonEnabled(page, false); + + const addCommandButton = page.getByRole("button", { name: "Add Command" }); + await addCommandButton.click(); + await page.getByTestId("command-input").fill("command 1"); + await page.getByTestId("directory-input").fill("mongodb.user.directory"); + + await addCommandButton.click(); + await page.getByTestId("command-input").nth(1).fill("command 2"); + await save(page); + await validateToast(page, "success", "Successfully updated repo", true); + + await page.getByTestId("array-down-button").click(); + await save(page); + await validateToast(page, "success", "Successfully updated repo"); + await expect(page.getByTestId("command-input").first()).toHaveValue( + "command 2", + ); + await expect(page.getByTestId("command-input").nth(1)).toHaveValue( + "command 1", + ); + }); +}); diff --git a/apps/spruce/playwright/tests/projectAndRepoSettings/utils.ts b/apps/spruce/playwright/tests/projectAndRepoSettings/utils.ts new file mode 100644 index 0000000000..3f321fffb0 --- /dev/null +++ b/apps/spruce/playwright/tests/projectAndRepoSettings/utils.ts @@ -0,0 +1,25 @@ +import { Page } from "@playwright/test"; +import { expect } from "../../fixtures"; + +export const save = async (page: Page) => { + const saveButton = page.getByTestId("save-settings-button"); + await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + await saveButton.click(); + + const saveChangesModal = page.getByTestId("save-changes-modal"); + await expect(saveChangesModal).toBeVisible(); + await saveChangesModal.getByRole("button", { name: "Save changes" }).click(); + await expect(saveChangesModal).toBeHidden(); +}; + +export const expectSaveButtonEnabled = async ( + page: Page, + isEnabled: boolean = true, +) => { + const saveButton = page.getByTestId("save-settings-button"); + if (isEnabled) { + await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + } else { + await expect(saveButton).toHaveAttribute("aria-disabled", "true"); + } +}; diff --git a/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts b/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts deleted file mode 100644 index 6240bdadbd..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { SEEN_GITHUB_NAV_GUIDE_CUE } from "../../../src/constants/cookies"; -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; - -test.describe("Commit Checks project settings when GitHub webhooks are disabled", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.context().addCookies([ - { - name: SEEN_GITHUB_NAV_GUIDE_CUE, - value: "true", - domain: "localhost", - path: "/", - }, - ]); - await page.goto("/project/logkeeper/settings/commit-checks"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - - test("Commit Checks page shows a disabled webhooks banner when webhooks are disabled", async ({ - authenticatedPage: page, - }) => { - const banner = page.getByTestId("disabled-webhook-banner"); - await expect(banner).toBeVisible(); - await expect(banner).toContainText( - "GitHub features are disabled because the Evergreen GitHub App is not", - ); - }); - - test("Disables all interactive elements on the page", async ({ - authenticatedPage: page, - }) => { - const settingsPage = page.getByTestId("project-settings-page"); - await expect( - settingsPage.locator('button:not([aria-disabled="true"])'), - ).toHaveCount(0); - await expect(page.locator('input:not([aria-disabled="true"])')).toHaveCount( - 0, - ); - }); -}); - -test.describe("Commit Checks project settings when GitHub webhooks are enabled", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.context().addCookies([ - { - name: SEEN_GITHUB_NAV_GUIDE_CUE, - value: "true", - domain: "localhost", - path: "/", - }, - ]); - await page.goto("/project/spruce/settings/commit-checks"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - - test("Shows an error banner when Commit Checks are enabled and hides it when Commit Checks are disabled", async ({ - authenticatedPage: page, - }) => { - await page - .getByTestId("github-checks-enabled-radio-box") - .locator("label") - .first() - .click(); - - const errorBanner = page.getByTestId("error-banner"); - await expect(errorBanner).toBeVisible(); - await expect(errorBanner).toContainText( - "A Commit Check Definition must be specified for this feature to run.", - ); - await page - .getByTestId("github-checks-enabled-radio-box") - .locator("label") - .last() - .click(); - await expect(page.getByTestId("error-banner")).toHaveCount(0); - }); - - test("Saves successfully when Commit Checks are enabled and a Commit Check Definition is provided", async ({ - authenticatedPage: page, - }) => { - await page - .getByTestId("github-checks-enabled-radio-box") - .locator("label") - .first() - .click(); - await page.getByRole("button", { name: "Add definition" }).click(); - await page.getByTestId("variant-tags-input").first().fill("vtag"); - await page.getByTestId("task-tags-input").first().fill("ttag"); - await expect(page.getByTestId("error-banner")).toHaveCount(0); - - const saveButton = page.getByTestId("save-settings-button"); - await expect(saveButton).toBeEnabled(); - await saveButton.click(); - const modal = page.getByTestId("save-changes-modal"); - await expect(modal).toBeVisible(); - await modal.getByRole("button", { name: "Save changes" }).click(); - await expect(modal).toBeHidden(); - await validateToast(page, "success", "Successfully updated project"); - }); -}); diff --git a/apps/spruce/playwright/tests/projectSettings/git_tags.spec.ts b/apps/spruce/playwright/tests/projectSettings/git_tags.spec.ts deleted file mode 100644 index 30d949030b..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/git_tags.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { clickRadio } from "@evg-ui/playwright-config/helpers"; -import { SEEN_GITHUB_NAV_GUIDE_CUE } from "../../../src/constants/cookies"; -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; - -test.describe("Git Tags project settings when GitHub webhooks are disabled", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.context().addCookies([ - { - name: SEEN_GITHUB_NAV_GUIDE_CUE, - value: "true", - domain: "localhost", - path: "/", - }, - ]); - await page.goto("/project/logkeeper/settings/git-tags"); - await expect(page.getByTestId("save-settings-button")).toBeDisabled(); - }); - - test("Git tags page shows a disabled webhooks banner when webhooks are disabled", async ({ - authenticatedPage: page, - }) => { - const banner = page.getByTestId("disabled-webhook-banner"); - await expect(banner).toBeVisible(); - await expect(banner).toContainText( - "GitHub features are disabled because the Evergreen GitHub App is not", - ); - }); - - test("Disables all interactive elements on the page", async ({ - authenticatedPage: page, - }) => { - const settingsPage = page.getByTestId("project-settings-page"); - await expect( - settingsPage.locator('button:not([aria-disabled="true"])'), - ).toHaveCount(0); - await expect(page.locator('input:not([aria-disabled="true"])')).toHaveCount( - 0, - ); - }); -}); - -test.describe("Git Tags project settings when GitHub webhooks are enabled", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.context().addCookies([ - { - name: SEEN_GITHUB_NAV_GUIDE_CUE, - value: "true", - domain: "localhost", - path: "/", - }, - ]); - await page.goto("/repo/602d70a2b2373672ee493184/settings/git-tags"); - await expect(page.getByTestId("save-settings-button")).toBeDisabled(); - }); - - test("Saves successfully when Git Tags are enabled and a Git Tag Definition is provided", async ({ - authenticatedPage: page, - }) => { - const gitTagRadioBox = page.getByTestId("git-tag-enabled-radio-box"); - const enabledRadio = gitTagRadioBox.getByRole("radio", { name: "Enabled" }); - await clickRadio(enabledRadio); - const errorText = - "A Git Tag Version Definition must be specified for this feature to run."; - const errorBanner = page.getByTestId("error-banner"); - await expect(errorBanner).toBeVisible(); - await expect(errorBanner).toContainText(errorText); - - await page - .getByTestId("add-button") - .filter({ hasText: "Add git tag" }) - .click(); - await page.getByTestId("git-tag-input").fill("v*"); - await page.getByTestId("remote-path-input").fill("./evergreen.yml"); - await expect(page.getByTestId("error-banner")).toHaveCount(0); - - const saveButton = page.getByTestId("save-settings-button"); - await expect(saveButton).toBeEnabled(); - await saveButton.click(); - const modal = page.getByTestId("save-changes-modal"); - await expect(modal).toBeVisible(); - await modal.getByRole("button", { name: "Save changes" }).click(); - await expect(modal).toBeHidden(); - await validateToast(page, "success", "Successfully updated repo"); - }); -}); diff --git a/apps/spruce/playwright/tests/projectSettings/pull_requests.spec.ts b/apps/spruce/playwright/tests/projectSettings/pull_requests.spec.ts deleted file mode 100644 index 2b2dfbfeb5..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/pull_requests.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SEEN_GITHUB_NAV_GUIDE_CUE } from "../../../src/constants/cookies"; -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; - -test.describe("Pull Requests project settings when GitHub webhooks are disabled", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.context().addCookies([ - { - name: SEEN_GITHUB_NAV_GUIDE_CUE, - value: "true", - domain: "localhost", - path: "/", - }, - ]); - await page.goto("/project/logkeeper/settings/pull-requests"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - - test("shows a disabled webhooks banner when webhooks are disabled", async ({ - authenticatedPage: page, - }) => { - const banner = page.getByTestId("disabled-webhook-banner"); - await expect(banner).toBeVisible(); - await expect(banner).toContainText( - "GitHub features are disabled because the Evergreen GitHub App is not", - ); - }); - - test("disables all interactive elements on the page", async ({ - authenticatedPage: page, - }) => { - const settingsPage = page.getByTestId("project-settings-page"); - await expect( - settingsPage.locator('button:not([aria-disabled="true"])'), - ).toHaveCount(0); - await expect(page.locator('input:not([aria-disabled="true"])')).toHaveCount( - 0, - ); - }); -}); - -test.describe("Pull Requests project settings when GitHub webhooks are enabled", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.context().addCookies([ - { - name: SEEN_GITHUB_NAV_GUIDE_CUE, - value: "true", - domain: "localhost", - path: "/", - }, - ]); - await page.goto("/repo/602d70a2b2373672ee493184/settings/pull-requests"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - - test("allows enabling manual PR testing", async ({ - authenticatedPage: page, - }) => { - const manualEnabledLabel = page - .getByTestId("manual-pr-testing-enabled-radio-box") - .locator("label", { hasText: "Enabled" }); - await manualEnabledLabel.scrollIntoViewIfNeeded(); - await manualEnabledLabel.click(); - const manualEnabledRadio = page - .getByTestId("manual-pr-testing-enabled-radio-box") - .getByRole("radio", { name: "Enabled" }); - await expect(manualEnabledRadio).toBeChecked(); - }); - - test("saving a patch definition hides the error banner, shows success toast, and disables repo patch definitions", async ({ - authenticatedPage: page, - }) => { - const errorText = - "A GitHub Patch Definition must be specified for this feature to run."; - const errorBanner = page.getByText(errorText); - await expect(errorBanner).toBeVisible(); - - await page.getByRole("button", { name: "Add patch definition" }).click(); - await expect(page.getByText(errorText)).toHaveCount(0); - await page.getByTestId("variant-tags-input").first().fill("vtag"); - await page.getByTestId("task-tags-input").first().fill("ttag"); - const saveButton = page.getByTestId("save-settings-button"); - await expect(saveButton).toBeEnabled(); - await saveButton.click(); - - const modal = page.getByTestId("save-changes-modal"); - await expect(modal).toBeVisible(); - await modal.getByRole("button", { name: "Save changes" }).click(); - await expect(modal).toBeHidden(); - await validateToast(page, "success", "Successfully updated repo"); - - await page.goto("/project/evergreen/settings/pull-requests"); - const patchDefAccordion = page.getByText("Repo Patch Definition 1"); - await patchDefAccordion.scrollIntoViewIfNeeded(); - await patchDefAccordion.click(); - const variantInput = page.getByTestId("variant-tags-input").first(); - const taskInput = page.getByTestId("task-tags-input").first(); - - await expect(variantInput).toHaveValue("vtag"); - await expect(variantInput).toHaveAttribute("aria-disabled", "true"); - await expect(taskInput).toHaveValue("ttag"); - await expect(taskInput).toHaveAttribute("aria-disabled", "true"); - await expect(page.getByText(errorText)).toHaveCount(0); - }); -}); diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GeneralTab/Fields/RepoConfigField.tsx b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GeneralTab/Fields/RepoConfigField.tsx index 87dffe77d0..de40261ecc 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GeneralTab/Fields/RepoConfigField.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GeneralTab/Fields/RepoConfigField.tsx @@ -62,7 +62,7 @@ export const RepoConfigField: Field = ({ onClick={() => setMoveModalOpen(true)} size="small" > - Move to New Repo + Move to new repo {isAttachedProject - ? "Detach from Current Repo" - : "Attach to Current Repo"} + ? "Detach from current repo" + : "Attach to current repo"} } triggerEvent="hover" diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubCommitQueueTab/getFormSchema.tsx b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubCommitQueueTab/getFormSchema.tsx index 1b68a8e699..910a19465a 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubCommitQueueTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubCommitQueueTab/getFormSchema.tsx @@ -243,6 +243,7 @@ export const getFormSchema = ( uiSchema: { github: { "ui:ObjectFieldTemplate": CardFieldTemplate, + "ui:data-cy": "github-card", prTestingEnabledTitle: { "ui:sectionTitle": true, }, @@ -319,7 +320,7 @@ export const getFormSchema = ( "ui:description": PRAliasesDescription, githubPrAliases: { ...aliasRowUiSchema({ - addButtonText: "Add Patch Definition", + addButtonText: "Add patch definition", numberedTitle: "Patch Definition", }), }, diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubPermissionGroupsTab/getFormSchema.tsx b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubPermissionGroupsTab/getFormSchema.tsx index c76a3fcd0e..5a481e0a8b 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubPermissionGroupsTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubPermissionGroupsTab/getFormSchema.tsx @@ -168,6 +168,7 @@ const permissionCss = css` `; const itemsUISchema = { + "ui:data-cy": "permission-group", "ui:displayTitle": "New Permission Group", name: { "ui:ariaLabelledBy": "Permission Group Name", diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/NotificationsTab/getFormSchema.tsx b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/NotificationsTab/getFormSchema.tsx index 21dfa43492..83a31dddb6 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/NotificationsTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/NotificationsTab/getFormSchema.tsx @@ -138,7 +138,7 @@ export const getFormSchema = ( subscriptions: { "ui:placeholder": "No subscriptions are defined.", "ui:descriptionNode": , - "ui:addButtonText": "Add Subscription", + "ui:addButtonText": "Add subscription", "ui:orderable": false, "ui:useExpandableCard": true, items: { diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PatchAliasesTab/getFormSchema.tsx b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PatchAliasesTab/getFormSchema.tsx index 966f180bab..35c684120a 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PatchAliasesTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PatchAliasesTab/getFormSchema.tsx @@ -197,7 +197,7 @@ export const getFormSchema = ( }); const aliasesUiSchema = { - "ui:addButtonText": "Add Patch Trigger Alias", + "ui:addButtonText": "Add patch trigger alias", "ui:orderable": false, "ui:showLabel": false, "ui:useExpandableCard": true, @@ -220,7 +220,7 @@ const aliasesUiSchema = { "ui:allowDeselect": false, }, taskSpecifiers: { - "ui:addButtonText": "Add Task Regex Pair", + "ui:addButtonText": "Add task regex pair", "ui:orderable": false, "ui:showLabel": false, "ui:topAlignDelete": true, diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PeriodicBuildsTab/getFormSchema.ts b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PeriodicBuildsTab/getFormSchema.ts index 644adaf440..62a73ae72b 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PeriodicBuildsTab/getFormSchema.ts +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PeriodicBuildsTab/getFormSchema.ts @@ -133,7 +133,7 @@ export const getFormSchema = ( "ui:showLabel": false, }, periodicBuilds: { - "ui:addButtonText": "Add Periodic Build", + "ui:addButtonText": "Add periodic build", "ui:orderable": false, "ui:showLabel": false, "ui:useExpandableCard": true, diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PluginsTab/getFormSchema.tsx b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PluginsTab/getFormSchema.tsx index 25238d8caf..e8e0d55872 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PluginsTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PluginsTab/getFormSchema.tsx @@ -213,7 +213,7 @@ export const getFormSchema = ( ticketSearchProjects: { "ui:description": "Specify an existing JIRA project to search for tickets related to a failing task.", - "ui:addButtonText": "Add Search Project", + "ui:addButtonText": "Add search project", "ui:orderable": false, items: { "ui:label": false, @@ -264,6 +264,7 @@ export const getFormSchema = ( "ui:useExpandableCard": true, items: { "ui:displayTitle": "New Metadata Link", + "ui:data-cy": "metadata-link", requesters: { "ui:widget": widgets.MultiSelectWidget, "ui:data-cy": "requesters-input", diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ProjectTriggersTab/getFormSchema.tsx b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ProjectTriggersTab/getFormSchema.tsx index f96bb33e9f..53d92196f8 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ProjectTriggersTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ProjectTriggersTab/getFormSchema.tsx @@ -119,7 +119,7 @@ export const getFormSchema = ( "ui:showLabel": false, }, triggers: { - "ui:addButtonText": "Add Project Trigger", + "ui:addButtonText": "Add project trigger", "ui:orderable": false, "ui:showLabel": false, "ui:useExpandableCard": true, diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ViewsAndFiltersTab/getFormSchema.ts b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ViewsAndFiltersTab/getFormSchema.ts index 75ee912954..d96cc38d39 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ViewsAndFiltersTab/getFormSchema.ts +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ViewsAndFiltersTab/getFormSchema.ts @@ -95,6 +95,7 @@ export const getFormSchema = ( "ui:useExpandableCard": true, "ui:data-cy": "parsley-filter-list", items: { + "ui:data-cy": "parsley-filter", "ui:displayTitle": "New Parsley Filter", "ui:label": false, expression: { diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/VirtualWorkstationTab/getFormSchema.ts b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/VirtualWorkstationTab/getFormSchema.ts index 1ae0cc83b9..b5cf100cd7 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/VirtualWorkstationTab/getFormSchema.ts +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/VirtualWorkstationTab/getFormSchema.ts @@ -68,7 +68,7 @@ export const getFormSchema = ( "ui:showLabel": false, }, setupCommands: { - "ui:addButtonText": "Add Command", + "ui:addButtonText": "Add command", "ui:addToEnd": true, "ui:border": true, "ui:fullWidth": true, diff --git a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/utils/alias.ts b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/utils/alias.ts index 439b3dc3b5..3c1542c97e 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/utils/alias.ts +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/utils/alias.ts @@ -308,7 +308,7 @@ export const baseProps = { }, uiSchema: { "ui:addButtonSize": "xsmall", - "ui:addButtonText": "Add Task Tag", + "ui:addButtonText": "Add task tag", "ui:orderable": false, "ui:sectionId": "task-tags-field", "ui:showLabel": false, @@ -349,7 +349,7 @@ export const baseProps = { }, uiSchema: { "ui:addButtonSize": "xsmall", - "ui:addButtonText": "Add Variant Tag", + "ui:addButtonText": "Add variant tag", "ui:orderable": false, "ui:sectionId": "variant-tags-field", "ui:showLabel": false, @@ -664,7 +664,7 @@ export const patchAliasArray = { }, }, uiSchema: aliasRowUiSchema({ - addButtonText: "Add Patch Alias", + addButtonText: "Add patch alias", displayTitle: "New Patch Alias", aliasHidden: false, useExpandableCard: true, diff --git a/packages/playwright-config/playwright.config.ts b/packages/playwright-config/playwright.config.ts index 2e80c65c8c..e1367bfa97 100644 --- a/packages/playwright-config/playwright.config.ts +++ b/packages/playwright-config/playwright.config.ts @@ -23,9 +23,9 @@ export const createPlaywrightConfig = ({ use: { baseURL, viewport, - video: process.env.CI ? "on-first-retry" : "off", + video: process.env.CI ? "retain-on-failure" : "off", screenshot: process.env.CI ? "only-on-failure" : "off", - trace: process.env.CI ? "on-first-retry" : "off", + trace: process.env.CI ? "retain-on-failure-and-retries" : "off", permissions: ["clipboard-read", "clipboard-write"], testIdAttribute: "data-cy", },