From 37880ac217a35c41010b0da53ff035899a2c6114 Mon Sep 17 00:00:00 2001 From: minnakt Date: Thu, 23 Apr 2026 10:50:48 -0400 Subject: [PATCH 01/32] migrate `hosts` tests --- .../integration/hosts/host_filtering.ts | 146 ------------------ .../integration/hosts/hosts_page_default.ts | 47 ------ .../integration/hosts/hosts_pagination.ts | 34 ---- .../cypress/integration/hosts/select_hosts.ts | 80 ---------- .../integration/hosts/update_status_modal.ts | 31 ---- apps/spruce/playwright/helpers/index.ts | 47 +----- .../tests/hosts/hosts_filtering.spec.ts | 137 ++++++++++++++++ .../playwright/tests/hosts/hosts_page.spec.ts | 91 +++++++++++ .../tests/hosts/hosts_select.spec.ts | 103 ++++++++++++ .../tests/hosts/hosts_sorting.spec.ts} | 136 +++++++++------- 10 files changed, 408 insertions(+), 444 deletions(-) delete mode 100644 apps/spruce/cypress/integration/hosts/host_filtering.ts delete mode 100644 apps/spruce/cypress/integration/hosts/hosts_page_default.ts delete mode 100644 apps/spruce/cypress/integration/hosts/hosts_pagination.ts delete mode 100644 apps/spruce/cypress/integration/hosts/select_hosts.ts delete mode 100644 apps/spruce/cypress/integration/hosts/update_status_modal.ts create mode 100644 apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts create mode 100644 apps/spruce/playwright/tests/hosts/hosts_page.spec.ts create mode 100644 apps/spruce/playwright/tests/hosts/hosts_select.spec.ts rename apps/spruce/{cypress/integration/hosts/hosts_sorting.ts => playwright/tests/hosts/hosts_sorting.spec.ts} (63%) diff --git a/apps/spruce/cypress/integration/hosts/host_filtering.ts b/apps/spruce/cypress/integration/hosts/host_filtering.ts deleted file mode 100644 index 45b1aa0845..0000000000 --- a/apps/spruce/cypress/integration/hosts/host_filtering.ts +++ /dev/null @@ -1,146 +0,0 @@ -const hostsRoute = "/hosts"; - -const idParam = "hostId"; -const distroParam = "distroId"; -const statusesParam = "statuses"; -const currentTaskIdParam = "currentTaskId"; -const ownerParam = "startedBy"; - -const idFilter = "i-0d0ae8b83366d22"; -const dnsFilter = "ec2-34-207-222-84.compute-1.amazonaws.com"; -const distroFilter = "macos-1014"; -const statusesFilter = "running"; -const currentTaskIdFilter = - "mongodb_mongo_v3.6_debian92_sharding_auth_bc405c72dce4714da604810cdc90c132bd5fbaa1_20_07_20_17_39_20"; -const ownerFilter = "mci"; - -const distroFilterIconDataCy = "distro-id-filter"; - -const filterTests = [ - { - param: idParam, - filterIconDataCy: "host-id-filter", - filterValue: idFilter, - filterUrlParam: `${idParam}=${idFilter}`, - expectedIds: ["i-0d0ae8b83366d22"], - }, - { - param: idParam, - filterIconDataCy: "host-id-filter", - filterValue: dnsFilter, - filterUrlParam: `${idParam}=${dnsFilter}`, - expectedIds: ["i-06f80fa6e28f93b7d"], - }, - { - param: distroParam, - filterIconDataCy: distroFilterIconDataCy, - filterValue: distroFilter, - filterUrlParam: `${distroParam}=${distroFilter}`, - expectedIds: [ - "macos-1014-68.macstadium.build.10gen", - "macos-1014-68.macstadium.build.10gen.c", - "macos-1014-68.macstadium.build.10gen.cc", - ], - }, - { - param: statusesParam, - filterIconDataCy: "statuses-filter", - filterValue: statusesFilter, - filterUrlParam: `${statusesParam}=${statusesFilter}`, - expectedIds: [ - "16326bd716fd4ad5845710c479c79e86c66b61bcef8ebbe7fc38dfc36fab512e", - "1694cfe1eac28b3316339f6276021afcb2a07bcd21a266405835fd039557ea2d", - "4b332e12790a585a0c7cbaf1650674f408117cf6134679c9e5f2e96cadd07923", - "6e331e02aaaebba422d1f1d2dbd3e64f01776b84c68c672ea680e4b81b0719bb", - "7f909d47566126bd39a05c1a5bd5d111c2e68de3830a8be414c18c231a47f4fc", - "a99b50cd37b012c53db7207e4ba8b52989aefab551176c07962cea979abcc479", - "b700d10f21a5386c827251a029dd931b5ea910377e0bb93f3393b17fb9bdbd08", - "build10.ny.cbi.10gen", - "build10.ny.cbi.10gen.c", - "build10.ny.cbi.10gen.cc", - ], - }, - { - param: currentTaskIdParam, - filterIconDataCy: "current-task-id-filter", - filterValue: currentTaskIdFilter, - filterUrlParam: `${currentTaskIdParam}=${currentTaskIdFilter}`, - expectedIds: [ - "i-0fb9fe0592ea381", - "i-0fb9fe0592ea3815", - "i-0fb9fe0592ea38150", - ], - }, - { - param: ownerParam, - filterIconDataCy: "owner-filter", - filterValue: ownerFilter, - filterUrlParam: `${ownerParam}=${ownerFilter}`, - expectedIds: [ - "i-06f80fa6e28f93b", - "i-06f80fa6e28f93b7", - "i-06f80fa6e28f93b7d", - "i-0fb9fe0592ea381", - "i-0fb9fe0592ea3815", - "i-0fb9fe0592ea38150", - "macos-1014-68.macstadium.build.10gen", - "macos-1014-68.macstadium.build.10gen.c", - "macos-1014-68.macstadium.build.10gen.cc", - "ubuntu1804-ppc-3.pic.build.10gen", - ], - }, -]; - -describe("Hosts page filtering from table filters", () => { - beforeEach(() => { - cy.visit(`${hostsRoute}?page=0`); - cy.dataCy("hosts-table").should("be.visible"); - cy.dataCy("hosts-table").should("have.attr", "data-loading", "false"); - }); - - filterTests.forEach( - ({ expectedIds, filterIconDataCy, filterUrlParam, filterValue, param }) => { - it(`Filters hosts using table filter dropdowns for ${param}`, () => { - cy.dataCy(filterIconDataCy).should("be.visible"); - cy.dataCy(filterIconDataCy).click(); - - cy.dataCy(`${filterIconDataCy}-wrapper`).as("filterWrapper"); - cy.get("@filterWrapper").should("be.visible"); - if (param === statusesParam) { - cy.get("@filterWrapper").within(() => { - cy.getInputByLabel("Running").check({ force: true }); - }); - cy.dataCy(filterIconDataCy).click(); - } else { - cy.get("input[type='search'").as("searchInput"); - cy.get("@searchInput").should("be.visible"); - cy.get("@searchInput").should("be.focused"); - cy.get("@searchInput").type(`${filterValue}{enter}`, { - scrollBehavior: false, - }); - } - cy.get("@filterWrapper").should("not.exist"); - cy.location("search").should("contain", filterUrlParam); - cy.dataCy("hosts-table").should("have.attr", "data-loading", "false"); - - expectedIds.forEach((id) => { - cy.dataCy("leafygreen-table-row").contains(id).should("be.visible"); - }); - - cy.dataCy(filterIconDataCy).should("be.visible"); - cy.dataCy(filterIconDataCy).click(); - cy.get("@filterWrapper").should("be.visible"); - if (param === statusesParam) { - cy.get("@filterWrapper").within(() => { - cy.getInputByLabel("Running").uncheck({ force: true }); - }); - } else { - cy.get("@searchInput").should("be.visible"); - cy.get("@searchInput").should("be.focused").focus(); - cy.get("@searchInput").clear(); - cy.get("@searchInput").type("{enter}"); - } - }); - }, - ); -}); diff --git a/apps/spruce/cypress/integration/hosts/hosts_page_default.ts b/apps/spruce/cypress/integration/hosts/hosts_page_default.ts deleted file mode 100644 index 5cbc803c39..0000000000 --- a/apps/spruce/cypress/integration/hosts/hosts_page_default.ts +++ /dev/null @@ -1,47 +0,0 @@ -describe("Hosts Page Default", () => { - const taskId = - "mongo_tools_ubuntu1604_qa_dump_restore_with_archiving_current_patch_b7227e1b7aeaaa6283d53b32fc03968a46b19c2d_5f15ad3c3627e07772ab2d01_20_07_20_14_42_05"; - - beforeEach(() => { - cy.visit("/hosts?limit=10"); - }); - - it("Renders hosts table with hosts sorted by status by default", () => { - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.wrap($el).contains(defaultHostsFirstPage[index]), - ); - }); - - it("ID column value links to host page", () => { - cy.dataCy("leafygreen-table-row") - .first() - .within(() => { - cy.dataCy("host-id-link") - .should("have.attr", "href") - .and("eq", "/host/i-06f80fa6e28f93b"); - }); - }); - - it("Current Task column value links to task page", () => { - cy.dataCy("leafygreen-table-row") - .first() - .within(() => { - cy.dataCy("current-task-link") - .should("have.attr", "href") - .and("eq", `/task/${taskId}`); - }); - }); -}); - -export const defaultHostsFirstPage = [ - "i-06f80fa6e28f93b", - "i-06f80fa6e28f93b7", - "i-06f80fa6e28f93b7d", - "i-0fb9fe0592ea381", - "i-0fb9fe0592ea3815", - "i-0fb9fe0592ea38150", - "macos-1014-68.macstadium.build.10gen", - "macos-1014-68.macstadium.build.10gen.c", - "macos-1014-68.macstadium.build.10gen.cc", - "ubuntu1804-ppc-3.pic.build.10gen", -]; diff --git a/apps/spruce/cypress/integration/hosts/hosts_pagination.ts b/apps/spruce/cypress/integration/hosts/hosts_pagination.ts deleted file mode 100644 index c99de9c254..0000000000 --- a/apps/spruce/cypress/integration/hosts/hosts_pagination.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defaultHostsFirstPage } from "./hosts_page_default"; - -describe("Hosts Page", () => { - it("URL query parameters determine pagination values", () => { - cy.visit("/hosts?limit=10&page=1"); - - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.wrap($el).contains(hostsSecondPageWithLimitOfTen[index]), - ); - - cy.visit("/hosts?limit=20&page=0"); - - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy - .wrap($el) - .contains( - [...defaultHostsFirstPage, ...hostsSecondPageWithLimitOfTen][index], - ), - ); - }); -}); - -const hostsSecondPageWithLimitOfTen = [ - "ubuntu1804-ppc-3.pic.build.10gen.c", - "ubuntu1804-ppc-3.pic.build.10gen.cc", - "16326bd716fd4ad5845710c479c79e86c66b61bcef8ebbe7fc38dfc36fab512e", - "1694cfe1eac28b3316339f6276021afcb2a07bcd21a266405835fd039557ea2d", - "4b332e12790a585a0c7cbaf1650674f408117cf6134679c9e5f2e96cadd07923", - "6e331e02aaaebba422d1f1d2dbd3e64f01776b84c68c672ea680e4b81b0719bb", - "7f909d47566126bd39a05c1a5bd5d111c2e68de3830a8be414c18c231a47f4fc", - "a99b50cd37b012c53db7207e4ba8b52989aefab551176c07962cea979abcc479", - "b700d10f21a5386c827251a029dd931b5ea910377e0bb93f3393b17fb9bdbd08", - "build10.ny.cbi.10gen", -]; diff --git a/apps/spruce/cypress/integration/hosts/select_hosts.ts b/apps/spruce/cypress/integration/hosts/select_hosts.ts deleted file mode 100644 index 47e301ce7c..0000000000 --- a/apps/spruce/cypress/integration/hosts/select_hosts.ts +++ /dev/null @@ -1,80 +0,0 @@ -const selectHosts = () => { - cy.get("thead").within(() => { - cy.get("input[type=checkbox]").check({ force: true }); - }); - cy.get("tbody").within(() => { - cy.get("input[type=checkbox]") - .should("have.length", 3) - .and("have.attr", "aria-checked", "true"); - }); -}; - -describe("Select hosts in hosts page table", () => { - const hostsRoute = "/hosts"; - - beforeEach(() => { - cy.visit(`${hostsRoute}?distroId=ubuntu1604-large&page=0&statuses=running`); - cy.dataCy("hosts-table").should("exist"); - cy.dataCy("hosts-table").should("not.have.attr", "data-loading", "true"); - cy.dataCy("update-status-button").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("restart-jasper-button").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("reprovision-button").should( - "have.attr", - "aria-disabled", - "true", - ); - }); - - it("Selecting hosts shows hosts selection data", () => { - selectHosts(); - cy.dataCy("update-status-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("restart-jasper-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("reprovision-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - }); - - it("Can restart jasper for selected hosts", () => { - selectHosts(); - cy.dataCy("restart-jasper-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("restart-jasper-button").should("be.visible").click(); - cy.dataCy("restart-jasper-button-popover").should("be.visible"); - cy.contains("button", "Yes").click(); - cy.validateToast("success", "Marked Jasper as restarting"); - }); - - it("Can reprovision for selected hosts", () => { - selectHosts(); - cy.dataCy("reprovision-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("reprovision-button").should("be.visible").click(); - cy.dataCy("reprovision-button-popover").should("be.visible"); - cy.contains("button", "Yes").click(); - cy.validateToast("success", "Marked hosts to reprovision"); - }); -}); diff --git a/apps/spruce/cypress/integration/hosts/update_status_modal.ts b/apps/spruce/cypress/integration/hosts/update_status_modal.ts deleted file mode 100644 index 20a6ba28f4..0000000000 --- a/apps/spruce/cypress/integration/hosts/update_status_modal.ts +++ /dev/null @@ -1,31 +0,0 @@ -describe("Update Status Modal", () => { - const hostsRoute = "/hosts"; - - beforeEach(() => { - cy.visit(`${hostsRoute}?limit=100&page=0`); - cy.dataCy("hosts-table").should("exist"); - cy.dataCy("hosts-table").should("not.have.attr", "data-loading", "true"); - }); - - it("Update status for selected hosts", () => { - cy.get("thead").within(() => { - cy.get("input[type=checkbox]").should("not.be.disabled"); - cy.get("input[type=checkbox]").check({ force: true }); - }); - - cy.dataCy("update-status-button").click(); - - cy.dataCy("host-status-select").click(); - - cy.dataCy("terminated-option").click(); - - cy.dataCy("host-status-notes").type("notes"); - - cy.dataCy("update-host-status-modal").should("be.visible"); - cy.dataCy("update-host-status-modal").within(() => { - cy.contains("button", "Update").click({ force: true }); - }); - cy.dataCy("update-host-status-modal").should("not.be.visible"); - cy.validateToast("success", "Status was changed to terminated"); - }); -}); diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 9c397851c2..5a53b4cba6 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -1,49 +1,4 @@ -import { Page, Locator, expect } from "@playwright/test"; - -type LocatorOptions = Parameters[1]; - -/** - * Gets a locator for an element with the specified data-row-key attribute - * @param page - The Playwright page object - * @param value - The data-row-key attribute value - * @param options - Optional locator options - * @returns A Playwright locator - */ -export function dataRowKeyLocator( - page: Page, - value: string, - options?: LocatorOptions, -): Locator { - return page.locator(`[data-row-key=${value}]`, options); -} - -/** - * Validates table sort direction by checking for the appropriate sort icon - * @param page - The Playwright page object - * @param direction - The expected sort direction (asc, desc, or none) - */ -export async function validateTableSort( - page: Page, - direction?: "asc" | "desc" | "none", -): Promise { - switch (direction) { - case "asc": - await expect( - page.locator("svg[aria-label='Sort Ascending Icon']"), - ).toBeVisible(); - return; - case "desc": - await expect( - page.locator("svg[aria-label='Sort Descending Icon']"), - ).toBeVisible(); - return; - case "none": - default: - await expect( - page.locator("svg[aria-label='Unsorted Icon']"), - ).toBeVisible(); - } -} +import { Page, expect } from "@playwright/test"; /** * Selects an option from a LeafyGreen select component diff --git a/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts b/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts new file mode 100644 index 0000000000..f6104b1345 --- /dev/null +++ b/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts @@ -0,0 +1,137 @@ +import { test, expect } from "../../fixtures"; +import { clickCheckboxByLabel } from "../../helpers"; + +const hostsRoute = "/hosts"; + +const textFilterTests = [ + { + filterIconDataCy: "host-id-filter", + filterValue: "i-0d0ae8b83366d22", + filterUrlParam: "hostId=i-0d0ae8b83366d22", + expectedIds: ["i-0d0ae8b83366d22"], + }, + { + filterIconDataCy: "host-id-filter", + filterValue: "ec2-34-207-222-84.compute-1.amazonaws.com", + filterUrlParam: "hostId=ec2-34-207-222-84.compute-1.amazonaws.com", + expectedIds: ["i-06f80fa6e28f93b7d"], + }, + { + filterIconDataCy: "distro-id-filter", + filterValue: "macos-1014", + filterUrlParam: "distroId=macos-1014", + expectedIds: [ + "macos-1014-68.macstadium.build.10gen", + "macos-1014-68.macstadium.build.10gen.c", + "macos-1014-68.macstadium.build.10gen.cc", + ], + }, + { + filterIconDataCy: "current-task-id-filter", + filterValue: + "mongodb_mongo_v3.6_debian92_sharding_auth_bc405c72dce4714da604810cdc90c132bd5fbaa1_20_07_20_17_39_20", + filterUrlParam: + "currentTaskId=mongodb_mongo_v3.6_debian92_sharding_auth_bc405c72dce4714da604810cdc90c132bd5fbaa1_20_07_20_17_39_20", + expectedIds: [ + "i-0fb9fe0592ea381", + "i-0fb9fe0592ea3815", + "i-0fb9fe0592ea38150", + ], + }, + { + filterIconDataCy: "owner-filter", + filterValue: "mci", + filterUrlParam: "startedBy=mci", + expectedIds: [ + "i-06f80fa6e28f93b", + "i-06f80fa6e28f93b7", + "i-06f80fa6e28f93b7d", + "i-0fb9fe0592ea381", + "i-0fb9fe0592ea3815", + "i-0fb9fe0592ea38150", + "macos-1014-68.macstadium.build.10gen", + "macos-1014-68.macstadium.build.10gen.c", + "macos-1014-68.macstadium.build.10gen.cc", + "ubuntu1804-ppc-3.pic.build.10gen", + ], + }, +]; + +test.describe("Hosts page filtering from table filters", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(`${hostsRoute}?page=0`); + const hostsTable = page.getByTestId("hosts-table"); + await expect(hostsTable).toBeVisible(); + await expect(hostsTable).toHaveAttribute("data-loading", "false"); + }); + + textFilterTests.forEach( + ({ expectedIds, filterIconDataCy, filterUrlParam, filterValue }) => { + test(`Filters hosts using table filter for ${filterIconDataCy}`, async ({ + authenticatedPage: page, + }) => { + const filterIcon = page.getByTestId(filterIconDataCy); + await expect(filterIcon).toBeVisible(); + await filterIcon.click(); + + const filterWrapper = page.getByTestId(`${filterIconDataCy}-wrapper`); + await expect(filterWrapper).toBeVisible(); + + const searchInput = page.locator("input[type='search']"); + await expect(searchInput).toBeVisible(); + await expect(searchInput).toBeFocused(); + await searchInput.fill(filterValue); + await searchInput.press("Enter"); + + await expect(filterWrapper).toBeHidden(); + await expect(page).toHaveURL(new RegExp(filterUrlParam)); + await expect(page.getByTestId("hosts-table")).toHaveAttribute( + "data-loading", + "false", + ); + + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < expectedIds.length; i++) { + await expect(rows.nth(i)).toContainText(expectedIds[i]); + } + }); + }, + ); + + test("Filters hosts using table filter for statuses", async ({ + authenticatedPage: page, + }) => { + const filterIcon = page.getByTestId("statuses-filter"); + await expect(filterIcon).toBeVisible(); + await filterIcon.click(); + + const filterWrapper = page.getByTestId("statuses-filter-wrapper"); + await expect(filterWrapper).toBeVisible(); + + await clickCheckboxByLabel(page, "Running"); + await filterIcon.click(); + + await expect(page).toHaveURL(/statuses=running/); + await expect(page.getByTestId("hosts-table")).toHaveAttribute( + "data-loading", + "false", + ); + + const expectedIds = [ + "16326bd716fd4ad5845710c479c79e86c66b61bcef8ebbe7fc38dfc36fab512e", + "1694cfe1eac28b3316339f6276021afcb2a07bcd21a266405835fd039557ea2d", + "4b332e12790a585a0c7cbaf1650674f408117cf6134679c9e5f2e96cadd07923", + "6e331e02aaaebba422d1f1d2dbd3e64f01776b84c68c672ea680e4b81b0719bb", + "7f909d47566126bd39a05c1a5bd5d111c2e68de3830a8be414c18c231a47f4fc", + "a99b50cd37b012c53db7207e4ba8b52989aefab551176c07962cea979abcc479", + "b700d10f21a5386c827251a029dd931b5ea910377e0bb93f3393b17fb9bdbd08", + "build10.ny.cbi.10gen", + "build10.ny.cbi.10gen.c", + "build10.ny.cbi.10gen.cc", + ]; + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < expectedIds.length; i++) { + await expect(rows.nth(i)).toContainText(expectedIds[i]); + } + }); +}); diff --git a/apps/spruce/playwright/tests/hosts/hosts_page.spec.ts b/apps/spruce/playwright/tests/hosts/hosts_page.spec.ts new file mode 100644 index 0000000000..773d1ab78a --- /dev/null +++ b/apps/spruce/playwright/tests/hosts/hosts_page.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from "../../fixtures"; + +const hostsRoute = "/hosts"; + +const taskId = + "mongo_tools_ubuntu1604_qa_dump_restore_with_archiving_current_patch_b7227e1b7aeaaa6283d53b32fc03968a46b19c2d_5f15ad3c3627e07772ab2d01_20_07_20_14_42_05"; + +const defaultHostsFirstPage = [ + "i-06f80fa6e28f93b", + "i-06f80fa6e28f93b7", + "i-06f80fa6e28f93b7d", + "i-0fb9fe0592ea381", + "i-0fb9fe0592ea3815", + "i-0fb9fe0592ea38150", + "macos-1014-68.macstadium.build.10gen", + "macos-1014-68.macstadium.build.10gen.c", + "macos-1014-68.macstadium.build.10gen.cc", + "ubuntu1804-ppc-3.pic.build.10gen", +]; + +const hostsSecondPageWithLimitOfTen = [ + "ubuntu1804-ppc-3.pic.build.10gen.c", + "ubuntu1804-ppc-3.pic.build.10gen.cc", + "16326bd716fd4ad5845710c479c79e86c66b61bcef8ebbe7fc38dfc36fab512e", + "1694cfe1eac28b3316339f6276021afcb2a07bcd21a266405835fd039557ea2d", + "4b332e12790a585a0c7cbaf1650674f408117cf6134679c9e5f2e96cadd07923", + "6e331e02aaaebba422d1f1d2dbd3e64f01776b84c68c672ea680e4b81b0719bb", + "7f909d47566126bd39a05c1a5bd5d111c2e68de3830a8be414c18c231a47f4fc", + "a99b50cd37b012c53db7207e4ba8b52989aefab551176c07962cea979abcc479", + "b700d10f21a5386c827251a029dd931b5ea910377e0bb93f3393b17fb9bdbd08", + "build10.ny.cbi.10gen", +]; + +test.describe("Hosts page default", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(`${hostsRoute}?limit=10`); + }); + + test("Renders hosts table with hosts sorted by status by default", async ({ + authenticatedPage: page, + }) => { + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < defaultHostsFirstPage.length; i++) { + await expect(rows.nth(i)).toContainText(defaultHostsFirstPage[i]); + } + }); + + test("ID column value links to host page", async ({ + authenticatedPage: page, + }) => { + await expect( + page + .getByTestId("leafygreen-table-row") + .first() + .getByTestId("host-id-link"), + ).toHaveAttribute("href", "/host/i-06f80fa6e28f93b"); + }); + + test("Current Task column value links to task page", async ({ + authenticatedPage: page, + }) => { + await expect( + page + .getByTestId("leafygreen-table-row") + .first() + .getByTestId("current-task-link"), + ).toHaveAttribute("href", `/task/${taskId}`); + }); +}); + +test.describe("Hosts page pagination", () => { + test("URL query parameters determine pagination values", async ({ + authenticatedPage: page, + }) => { + await page.goto(`${hostsRoute}?limit=10&page=1`); + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < hostsSecondPageWithLimitOfTen.length; i++) { + await expect(rows.nth(i)).toContainText(hostsSecondPageWithLimitOfTen[i]); + } + + await page.goto(`${hostsRoute}?limit=20&page=0`); + const allHosts = [ + ...defaultHostsFirstPage, + ...hostsSecondPageWithLimitOfTen, + ]; + const rows2 = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < allHosts.length; i++) { + await expect(rows2.nth(i)).toContainText(allHosts[i]); + } + }); +}); diff --git a/apps/spruce/playwright/tests/hosts/hosts_select.spec.ts b/apps/spruce/playwright/tests/hosts/hosts_select.spec.ts new file mode 100644 index 0000000000..007829b46f --- /dev/null +++ b/apps/spruce/playwright/tests/hosts/hosts_select.spec.ts @@ -0,0 +1,103 @@ +import { Page } from "@playwright/test"; +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; + +const hostsRoute = "/hosts"; + +const selectAllHosts = async (page: Page) => { + const headerCheckbox = page.locator("thead").locator("input[type=checkbox]"); + const id = await headerCheckbox.getAttribute("id"); + await page.locator(`label[for="${id}"]`).click(); + + const bodyCheckboxes = page.locator("tbody").locator("input[type=checkbox]"); + await expect(bodyCheckboxes).toHaveCount(3); + for (let i = 0; i < 3; i++) { + await expect(bodyCheckboxes.nth(i)).toBeChecked(); + } +}; + +test.describe("Select hosts in hosts page table", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto( + `${hostsRoute}?distroId=ubuntu1604-large&page=0&statuses=running`, + ); + const table = page.getByTestId("hosts-table"); + await expect(table).toBeVisible(); + await expect(table).toHaveAttribute("data-loading", "false"); + await expect(page.getByTestId("update-status-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("restart-jasper-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("reprovision-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + + test("Selecting hosts enables action buttons", async ({ + authenticatedPage: page, + }) => { + await selectAllHosts(page); + await expect(page.getByTestId("update-status-button")).toHaveAttribute( + "aria-disabled", + "false", + ); + await expect(page.getByTestId("restart-jasper-button")).toHaveAttribute( + "aria-disabled", + "false", + ); + await expect(page.getByTestId("reprovision-button")).toHaveAttribute( + "aria-disabled", + "false", + ); + }); + + test("Can restart jasper for selected hosts", async ({ + authenticatedPage: page, + }) => { + await selectAllHosts(page); + const restartJasperButton = page.getByTestId("restart-jasper-button"); + await expect(restartJasperButton).toBeEnabled(); + await restartJasperButton.click(); + await expect( + page.getByTestId("restart-jasper-button-popover"), + ).toBeVisible(); + await page.getByRole("button", { name: "Yes" }).click(); + await validateToast(page, "success", "Marked Jasper as restarting"); + }); + + test("Can reprovision selected hosts", async ({ + authenticatedPage: page, + }) => { + await selectAllHosts(page); + const reprovisionButton = page.getByTestId("reprovision-button"); + await expect(reprovisionButton).toBeEnabled(); + await reprovisionButton.click(); + await expect(page.getByTestId("reprovision-button-popover")).toBeVisible(); + await page.getByRole("button", { name: "Yes" }).click(); + await validateToast(page, "success", "Marked hosts to reprovision"); + }); + + test("Can update status for selected hosts", async ({ + authenticatedPage: page, + }) => { + await selectAllHosts(page); + const updateStatusButton = page.getByTestId("update-status-button"); + await expect(updateStatusButton).toBeEnabled(); + await updateStatusButton.click(); + + await page.getByTestId("host-status-select").click(); + await page.getByTestId("terminated-option").click(); + await page.getByTestId("host-status-notes").fill("notes"); + + const modal = page.getByTestId("update-host-status-modal"); + await expect(modal).toBeVisible(); + await modal.getByRole("button", { name: "Update" }).click(); + await expect(modal).toBeHidden(); + await validateToast(page, "success", "Status was changed to terminated"); + }); +}); diff --git a/apps/spruce/cypress/integration/hosts/hosts_sorting.ts b/apps/spruce/playwright/tests/hosts/hosts_sorting.spec.ts similarity index 63% rename from apps/spruce/cypress/integration/hosts/hosts_sorting.ts rename to apps/spruce/playwright/tests/hosts/hosts_sorting.spec.ts index 15ee495463..1d0da3cb7f 100644 --- a/apps/spruce/cypress/integration/hosts/hosts_sorting.ts +++ b/apps/spruce/playwright/tests/hosts/hosts_sorting.spec.ts @@ -1,3 +1,7 @@ +import { test, expect } from "../../fixtures"; + +const hostsRoute = "/hosts"; + const sortByTests = [ { sorterName: "ID", @@ -45,7 +49,6 @@ const sortByTests = [ "macos-1014-68.macstadium.build.10gen.cc", "ubuntu1604-ppc-1.pic.build.10gen", "ubuntu1604-ppc-1.pic.build.10gen.c", - "ubuntu1604-ppc-1.pic.build.10gen.cc", ], }, { @@ -78,7 +81,6 @@ const sortByTests = [ "macos-1014-68.macstadium.build.10gen.cc", "ubuntu1604-ppc-1.pic.build.10gen", "ubuntu1604-ppc-1.pic.build.10gen.c", - "ubuntu1604-ppc-1.pic.build.10gen.cc", ], }, { @@ -130,7 +132,6 @@ const sortDirectionTests = [ "macos-1014-68.macstadium.build.10gen.cc", "ubuntu1604-ppc-1.pic.build.10gen", "ubuntu1604-ppc-1.pic.build.10gen.c", - "ubuntu1604-ppc-1.pic.build.10gen.cc", ], }, { @@ -151,80 +152,95 @@ const sortDirectionTests = [ }, ]; -describe("Hosts page sorting", () => { - const hostsRoute = "/hosts"; +test.describe("Hosts page sorting", () => { const distroSortControl = "button[aria-label='Sort by Distro']"; - it("Clicking the sort direction filter will set the page query param to 0", () => { - cy.visit(`${hostsRoute}?page=5`); - cy.dataCy("hosts-table").should("be.visible"); - cy.dataCy("hosts-table").should("not.have.attr", "data-loading", "true"); - cy.get(distroSortControl).click(); - cy.location("search").should("equal", "?page=0&sorts=DISTRO%3AASC"); + test("Clicking the sort direction filter will set the page query param to 0", async ({ + authenticatedPage: page, + }) => { + await page.goto(`${hostsRoute}?page=5`); + const hostsTable = page.getByTestId("hosts-table"); + await expect(hostsTable).toBeVisible(); + await expect(hostsTable).toHaveAttribute("data-loading", "false"); + await page.locator(distroSortControl).click(); + await expect(page).toHaveURL("/hosts?page=0&sorts=DISTRO%3AASC"); }); - it("Clicking a sort direction 3 times will set the page query param to 0, clear the direction & sortBy query param, and preserve the rest", () => { - cy.visit(hostsRoute); - cy.get(distroSortControl).click(); - cy.location("search").should("equal", "?page=0&sorts=DISTRO%3AASC"); - cy.get(distroSortControl).click(); - cy.location("search").should("equal", "?page=0&sorts=DISTRO%3ADESC"); - cy.get(distroSortControl).click(); - cy.location("search").should("equal", "?page=0"); + test("Clicking a sort direction 3 times clears the sort params and sets page to 0", async ({ + authenticatedPage: page, + }) => { + await page.goto(hostsRoute); + await page.locator(distroSortControl).click(); + await expect(page).toHaveURL("/hosts?page=0&sorts=DISTRO%3AASC"); + await page.locator(distroSortControl).click(); + await expect(page).toHaveURL("/hosts?page=0&sorts=DISTRO%3ADESC"); + await page.locator(distroSortControl).click(); + await expect(page).toHaveURL("/hosts?page=0"); }); - it("Status sorter is selected by default if no sort params in url", () => { - cy.visit(hostsRoute); - cy.contains("th", "Status") - .first() - .within(() => { - cy.validateTableSort("asc"); - }); + + test("Status sorter is selected by default if no sort params in url", async ({ + authenticatedPage: page, + }) => { + await page.goto(hostsRoute); + await expect( + page.locator("svg[aria-label='Sort Ascending Icon']"), + ).toBeVisible(); }); - it("Status sorter has initial value of sort param from url", () => { - cy.visit(`${hostsRoute}?page=0&sorts=DISTRO%3ADESC`); - cy.contains("th", "Distro").within(() => { - cy.validateTableSort("desc"); - }); + test("Status sorter has initial value of sort param from url", async ({ + authenticatedPage: page, + }) => { + await page.goto(`${hostsRoute}?page=0&sorts=DISTRO%3ADESC`); + await expect( + page.locator("svg[aria-label='Sort Descending Icon']"), + ).toBeVisible(); }); sortByTests.forEach(({ expectedIds, sortBy, sorterName }) => { - it(`Sorts by ${sorterName} when sorts = ${sortBy}`, () => { - cy.visit(`${hostsRoute}?sorts=${sortBy}%3AASC&limit=10`); - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.wrap($el).contains(expectedIds[index]), - ); + test(`Sorts by ${sorterName} when sorts = ${sortBy}`, async ({ + authenticatedPage: page, + }) => { + await page.goto(`${hostsRoute}?sorts=${sortBy}%3AASC&limit=10`); + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < expectedIds.length; i++) { + await expect(rows.nth(i)).toContainText(expectedIds[i]); + } }); }); sortDirectionTests.forEach(({ expectedIds, order, sortDir }) => { - it(`Sorts in ${order} order when sorts = CURRENT_TASK:${sortDir}`, () => { - cy.visit(`${hostsRoute}?page=0&sorts=CURRENT_TASK%3A${sortDir}&limit=10`); - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.wrap($el).contains(expectedIds[index]), + test(`Sorts in ${order} order when sorts = CURRENT_TASK:${sortDir}`, async ({ + authenticatedPage: page, + }) => { + await page.goto( + `${hostsRoute}?page=0&sorts=CURRENT_TASK%3A${sortDir}&limit=10`, ); + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < expectedIds.length; i++) { + await expect(rows.nth(i)).toContainText(expectedIds[i]); + } }); }); - it("Uses default sorts if sorts param is invalid", () => { - cy.visit(`${hostsRoute}?sorts=INVALID%3AINVALID&limit=10`); - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy - .wrap($el) - .contains( - [ - "i-06f80fa6e28f93b", - "i-06f80fa6e28f93b7", - "i-06f80fa6e28f93b7d", - "i-0fb9fe0592ea381", - "i-0fb9fe0592ea3815", - "i-0fb9fe0592ea38150", - "macos-1014-68.macstadium.build.10gen", - "macos-1014-68.macstadium.build.10gen.c", - "macos-1014-68.macstadium.build.10gen.cc", - "ubuntu1804-ppc-3.pic.build.10gen", - ][index], - ), - ); + test("Uses default sorts if sorts param is invalid", async ({ + authenticatedPage: page, + }) => { + await page.goto(`${hostsRoute}?sorts=INVALID%3AINVALID&limit=10`); + const expectedIds = [ + "i-06f80fa6e28f93b", + "i-06f80fa6e28f93b7", + "i-06f80fa6e28f93b7d", + "i-0fb9fe0592ea381", + "i-0fb9fe0592ea3815", + "i-0fb9fe0592ea38150", + "macos-1014-68.macstadium.build.10gen", + "macos-1014-68.macstadium.build.10gen.c", + "macos-1014-68.macstadium.build.10gen.cc", + "ubuntu1804-ppc-3.pic.build.10gen", + ]; + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < expectedIds.length; i++) { + await expect(rows.nth(i)).toContainText(expectedIds[i]); + } }); }); From 815caddb4b4b466ac6f891b571fba0d41150fa77 Mon Sep 17 00:00:00 2001 From: minnakt Date: Thu, 23 Apr 2026 12:56:53 -0400 Subject: [PATCH 02/32] migrate `version` and `spawn` tests --- apps/spruce/cypress/integration/spawn/host.ts | 326 -------------- .../spruce/cypress/integration/spawn/route.ts | 23 - .../cypress/integration/spawn/volume.ts | 329 -------------- .../integration/version/action_buttons.ts | 137 ------ .../cypress/integration/version/banners.ts | 43 -- .../version/downstream_projects.ts | 46 -- .../integration/version/name_change_modal.ts | 51 --- .../version/patch_with_aborted_task.ts | 10 - .../integration/version/restart_modal.ts | 106 ----- .../cypress/integration/version/routes.ts | 185 -------- .../integration/version/schedule_modal.ts | 25 -- .../integration/version/subscription_modal.ts | 113 ----- .../integration/version/task_duration.ts | 124 ------ .../integration/version/task_filters.ts | 301 ------------- .../cypress/integration/version/task_table.ts | 236 ---------- .../integration/version/test_analysis.ts | 53 --- .../integration/version/version_timing.ts | 174 -------- .../tests/hosts/hosts_filtering.spec.ts | 15 +- .../playwright/tests/spawn/host.spec.ts | 358 +++++++++++++++ .../playwright/tests/spawn/route.spec.ts | 33 ++ .../playwright/tests/spawn/volume.spec.ts | 406 ++++++++++++++++++ .../tests/version/action_buttons.spec.ts | 174 ++++++++ .../playwright/tests/version/banners.spec.ts | 69 +++ .../tests/version/downstream_projects.spec.ts | 49 +++ .../tests/version/name_change_modal.spec.ts | 56 +++ .../tests/version/restart_modal.spec.ts | 142 ++++++ .../playwright/tests/version/routes.spec.ts | 208 +++++++++ .../tests/version/schedule_modal.spec.ts | 27 ++ .../tests/version/subscription_modal.spec.ts | 91 ++++ .../tests/version/task_duration.spec.ts | 145 +++++++ .../tests/version/task_filters.spec.ts | 224 ++++++++++ .../tests/version/task_table.spec.ts | 250 +++++++++++ .../tests/version/test_analysis.spec.ts | 89 ++++ .../tests/version/version_timing.spec.ts | 192 +++++++++ .../spawnVolumeTableActions/UnmountButton.tsx | 1 + .../TaskDuration/TaskDurationTable/index.tsx | 2 + 36 files changed, 2529 insertions(+), 2284 deletions(-) delete mode 100644 apps/spruce/cypress/integration/spawn/host.ts delete mode 100644 apps/spruce/cypress/integration/spawn/route.ts delete mode 100644 apps/spruce/cypress/integration/spawn/volume.ts delete mode 100644 apps/spruce/cypress/integration/version/action_buttons.ts delete mode 100644 apps/spruce/cypress/integration/version/banners.ts delete mode 100644 apps/spruce/cypress/integration/version/downstream_projects.ts delete mode 100644 apps/spruce/cypress/integration/version/name_change_modal.ts delete mode 100644 apps/spruce/cypress/integration/version/patch_with_aborted_task.ts delete mode 100644 apps/spruce/cypress/integration/version/restart_modal.ts delete mode 100644 apps/spruce/cypress/integration/version/routes.ts delete mode 100644 apps/spruce/cypress/integration/version/schedule_modal.ts delete mode 100644 apps/spruce/cypress/integration/version/subscription_modal.ts delete mode 100644 apps/spruce/cypress/integration/version/task_duration.ts delete mode 100644 apps/spruce/cypress/integration/version/task_filters.ts delete mode 100644 apps/spruce/cypress/integration/version/task_table.ts delete mode 100644 apps/spruce/cypress/integration/version/test_analysis.ts delete mode 100644 apps/spruce/cypress/integration/version/version_timing.ts create mode 100644 apps/spruce/playwright/tests/spawn/host.spec.ts create mode 100644 apps/spruce/playwright/tests/spawn/route.spec.ts create mode 100644 apps/spruce/playwright/tests/spawn/volume.spec.ts create mode 100644 apps/spruce/playwright/tests/version/action_buttons.spec.ts create mode 100644 apps/spruce/playwright/tests/version/banners.spec.ts create mode 100644 apps/spruce/playwright/tests/version/downstream_projects.spec.ts create mode 100644 apps/spruce/playwright/tests/version/name_change_modal.spec.ts create mode 100644 apps/spruce/playwright/tests/version/restart_modal.spec.ts create mode 100644 apps/spruce/playwright/tests/version/routes.spec.ts create mode 100644 apps/spruce/playwright/tests/version/schedule_modal.spec.ts create mode 100644 apps/spruce/playwright/tests/version/task_duration.spec.ts create mode 100644 apps/spruce/playwright/tests/version/task_filters.spec.ts create mode 100644 apps/spruce/playwright/tests/version/task_table.spec.ts create mode 100644 apps/spruce/playwright/tests/version/test_analysis.spec.ts create mode 100644 apps/spruce/playwright/tests/version/version_timing.spec.ts diff --git a/apps/spruce/cypress/integration/spawn/host.ts b/apps/spruce/cypress/integration/spawn/host.ts deleted file mode 100644 index 6deca276ef..0000000000 --- a/apps/spruce/cypress/integration/spawn/host.ts +++ /dev/null @@ -1,326 +0,0 @@ -const ascendingSortSpawnHostOrderByHostId = [ - "i-04ade558e1e26b0ad", - "i-07669e7a3cd2c238c", - "i-092593689871a50dc", -]; -const descendingSortSpawnHostOrderByHostId = [ - "i-092593689871a50dc", - "i-07669e7a3cd2c238c", - "i-04ade558e1e26b0ad", -]; - -const descendingSortSpawnHostOrderByExpiration = [ - "i-092593689871a50dc", - "i-07669e7a3cd2c238c", - "i-04ade558e1e26b0ad", -]; -const ascendingSortSpawnHostOrderByExpiration = [ - "i-04ade558e1e26b0ad", - "i-07669e7a3cd2c238c", - "i-092593689871a50dc", -]; - -const hostTaskId = - "evergreen_ubuntu1604_dist_patch_33016573166a36bd5f46b4111151899d5c4e95b1_5ecedafb562343215a7ff297_20_05_27_21_39_46"; -const distroId = "windows-64-vs2015-small"; - -describe("Navigating to Spawn Host page", () => { - beforeEach(() => { - cy.visit("/spawn/host"); - }); - it("Visiting the spawn host page should display all of your spawned hosts", () => { - cy.dataCy("leafygreen-table-row").should("have.length", 3); - }); - it("Visiting the spawn host page should not have any cards expanded by default", () => { - cy.dataCy("spawn-host-card").should("not.exist"); - }); - it("Clicking on a spawn host row should toggle the host card", () => { - cy.dataCy("spawn-host-card").should("not.exist"); - cy.get("button[aria-label='Expand row']").first().click(); - cy.dataCy("spawn-host-card").should("be.visible"); - cy.get("button[aria-label='Collapse row']").first().click(); - cy.dataCy("spawn-host-card").should("not.exist"); - }); - it("Visiting the spawn host page with an id in the url should open the page with the row expanded", () => { - cy.visit("/spawn/host?host=i-092593689871a50dc"); - cy.dataCy("spawn-host-card").first().should("be.visible"); - cy.dataCy("spawn-host-card").eq(1).should("not.exist"); - }); - it("Clicking on the Event Log link should redirect to /host/:hostId", () => { - cy.contains('[data-cy="leafygreen-table-row"]', "i-092593689871a50dc") - .find("button[aria-label='Expand row']") - .click(); - cy.contains("Event Log").click(); - cy.location("pathname").should("eq", "/host/i-092593689871a50dc"); - }); - - describe("Spawn host card sorting", () => { - beforeEach(() => { - cy.visit("/spawn/host"); - }); - - it("Visiting the spawn host page should display all of your spawned hosts not sorted by default", () => { - cy.dataCy("leafygreen-table-row").should("have.length", 3); - }); - - it("Clicking on the host column header should sort spawn hosts by ascending order, then descending, then remove sort", () => { - cy.get("button[aria-label='Sort by Host']").as("hostSortControl").click(); - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.wrap($el).contains(ascendingSortSpawnHostOrderByHostId[index]), - ); - cy.get("@hostSortControl").click(); - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.wrap($el).contains(descendingSortSpawnHostOrderByHostId[index]), - ); - cy.get("@hostSortControl").click(); - cy.dataCy("leafygreen-table-row").should("have.length", 3); - }); - - it("Clicking on the expiration column header should sort the hosts by ascending order, then descending, then remove sort", () => { - cy.get("button[aria-label='Sort by Expires In']") - .as("expiresInSortControl") - .click(); - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.wrap($el).contains(ascendingSortSpawnHostOrderByExpiration[index]), - ); - cy.get("@expiresInSortControl").click(); - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.wrap($el).contains(descendingSortSpawnHostOrderByExpiration[index]), - ); - cy.get("@expiresInSortControl").click(); - cy.dataCy("leafygreen-table-row").should("have.length", 3); - }); - }); - - describe("Spawn host modal", () => { - it("Should disable 'Unexpirable Host' radio box when max number of unexpirable hosts is met (2)", () => { - cy.visit("/spawn/host"); - cy.contains("Spawn a host").click(); - cy.dataCy("distro-input").click(); - cy.dataCy("distro-option-ubuntu1804-workstation") - .should("be.visible") - .click(); - cy.dataCy("expirable-radio-box").children().should("have.length", 2); - cy.getInputByLabel("Expirable Host").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.getInputByLabel("Unexpirable Host").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.contains( - "You have reached the max number of unexpirable hosts", - ).should("be.visible"); - }); - - it("Clicking on the spawn host button should open a spawn host modal.", () => { - cy.visit("/spawn/host"); - cy.dataCy("spawn-host-modal").should("not.exist"); - cy.dataCy("spawn-host-button").click(); - cy.dataCy("spawn-host-modal").should("be.visible"); - }); - it("Visiting the spawn host page with the proper url param should open the spawn host modal by default", () => { - cy.visit("/spawn/host?spawnHost=True "); - cy.dataCy("spawn-host-modal").should("be.visible"); - }); - it("Closing the spawn host modal removes the 'spawnHost' query param from the url and hides the modal", () => { - cy.visit("/spawn/host?spawnHost=True "); - cy.dataCy("spawn-host-modal").should("be.visible"); - cy.location().should(({ search }) => { - expect(search).to.include("spawnHost=True"); - }); - cy.dataCy("spawn-host-modal").contains("Cancel").click(); - cy.location().should(({ search }) => { - expect(search).to.not.include("spawnHost"); - }); - cy.dataCy("spawn-host-modal").should("not.exist"); - }); - it("Visiting the spawn host page with a taskId url param should render additional options at the bottom of the modal.", () => { - cy.visit( - `spawn/host?spawnHost=True&distroId=rhel71-power8-large&taskId=${hostTaskId}`, - ); - cy.dataCy("spawn-host-modal").should("contain.text", label1); - cy.dataCy("spawn-host-modal").should( - "contain.text", - "Load data for dist on ubuntu1604", - ); - cy.dataCy("spawn-host-modal").should("contain.text", label2); - }); - - it("Unchecking 'Load data for dist' hides nested checkbox selections and checking shows them.", () => { - cy.visit( - `spawn/host?spawnHost=True&distroId=rhel71-power8-large&taskId=${hostTaskId}`, - ); - cy.dataCy("spawn-host-modal").should("be.visible"); - cy.dataCy("load-data-checkbox").should("be.checked"); - cy.contains(label1).should("be.visible"); - cy.contains(label2).should("be.visible"); - - cy.dataCy("load-data-checkbox").click({ force: true }); - cy.dataCy("load-data-checkbox").should("not.be.checked"); - cy.contains(label1).should("not.exist"); - cy.contains(label2).should("not.exist"); - }); - - it("Visiting the spawn host page with a task and distro supplied in the url should populate the distro input", () => { - cy.visit( - `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, - ); - cy.dataCy("spawn-host-modal").should("be.visible"); - cy.dataCy("distro-input").dataCy("dropdown-value").contains(distroId); - }); - it("The virtual workstation dropdown should filter any volumes that aren't a home volume", () => { - cy.visit( - `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, - ); - cy.dataCy("distro-input").click(); - cy.contains("Admin-only distros").should("not.exist"); - cy.dataCy("distro-option-ubuntu1804-workstation") - .should("be.visible") - .click(); - cy.dataCy("volume-select").should("have.attr", "aria-disabled", "true"); - }); - - it("Clicking 'Add new key' hides the key name dropdown and shows the key value text area", () => { - cy.visit( - `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, - ); - cy.dataCy("key-select").should("be.visible"); - cy.dataCy("key-value-text-area").should("not.exist"); - cy.contains("Add new key").click(); - cy.dataCy("key-select").should("not.exist"); - cy.dataCy("key-value-text-area").should("be.visible"); - }); - - it("Checking 'Run Userdata script on start' shows the user data script text area", () => { - cy.visit( - `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, - ); - cy.dataCy("run-user-data-script-text-area").should("not.exist"); - cy.contains("Run Userdata script on start").click(); - cy.dataCy("user-data-script-text-area").should("be.visible"); - }); - - it("Checking 'Define setup script...' shows the setup script text area", () => { - cy.visit( - `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, - ); - cy.dataCy("setup-script-text-area").should("not.exist"); - cy.dataCy("project-setup-script-checkbox").uncheck({ force: true }); - cy.dataCy("setup-script-checkbox").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("setup-script-checkbox").check({ force: true }); - cy.dataCy("setup-script-text-area").should("be.visible"); - }); - - it("Conditionally disables setup script and project setup script checkboxes based on the other's value", () => { - cy.visit( - `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, - ); - // Project setup script should be checked by default, which should disable setup script. - cy.dataCy("project-setup-script-checkbox").should( - "have.attr", - "aria-checked", - "true", - ); - cy.dataCy("project-setup-script-checkbox").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("setup-script-checkbox").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Unchecking project setup script should reenable setup script. - cy.dataCy("project-setup-script-checkbox").uncheck({ force: true }); - cy.dataCy("project-setup-script-checkbox").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("setup-script-checkbox").should( - "have.attr", - "aria-disabled", - "false", - ); - - // Checking setup script should disable project setup script. - cy.dataCy("setup-script-checkbox").check({ force: true }); - cy.dataCy("project-setup-script-checkbox").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("setup-script-checkbox").should( - "have.attr", - "aria-disabled", - "false", - ); - }); - const label1 = "Use project-specific setup script defined at /path"; - const label2 = "Also start any hosts this task started (if applicable)"; - }); - - it("Allows editing a modal with sleep schedule enabled and validates dates", () => { - cy.dataCy("edit-host-button").eq(2).click(); - cy.dataCy("edit-spawn-host-modal").should("be.visible"); - - cy.get("input[id='year']").as("startOfDateInput"); - - cy.get("@startOfDateInput").click(); - cy.get("td[aria-current=true]").as("currentDateCell"); - - // Select a date in the future either this month or next month - // if the current date is the last day of the month select the first day of the next month - // if it is not select the next day - // We can determine if the current date is the last day of the month if the next cell is disabled - cy.get("@currentDateCell").then(($currentDateCell) => { - // Get all sibling elements of the current date cell - cy.wrap($currentDateCell) - .siblings() - .then(($siblings) => { - const currentDay = Number($currentDateCell.text()); - // Get the next date cell based on the next index - const nextIndex = $siblings - .toArray() - .findIndex((el) => Number(el.textContent) === currentDay + 1); - - const $nextCell = $siblings.eq(nextIndex); - // If the next cell exists and is not disabled, click on it - if ($nextCell && $nextCell.attr("aria-disabled") === "false") { - cy.wrap($nextCell).click(); - } else { - cy.log("Current date is the last day of the month"); - cy.get('button[aria-label^="Next month"]').click(); - cy.get('td[data-testid="lg-date_picker-calendar_cell"]') - .first() - .click(); - } - }); - }); - - cy.contains("button", "Save").should("have.attr", "aria-disabled", "false"); - - cy.clearDatePickerInput(); - - // Select a date in the past - cy.get("@startOfDateInput").type("20250101"); - cy.get("body").click(); - cy.contains("button", "Save").should("have.attr", "aria-disabled", "true"); - - cy.clearDatePickerInput(); - - // Select a date too far in the future - cy.get("@startOfDateInput").type("20600115"); - cy.contains("button", "Save").should("have.attr", "aria-disabled", "true"); - }); -}); diff --git a/apps/spruce/cypress/integration/spawn/route.ts b/apps/spruce/cypress/integration/spawn/route.ts deleted file mode 100644 index 83c7c6e7ed..0000000000 --- a/apps/spruce/cypress/integration/spawn/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -describe("Navigating to Spawn Host and Spawn Volume pages", () => { - it("Navigating to /spawn will redirect to /spawn/host", () => { - cy.visit("/spawn"); - cy.location("pathname").should("eq", "/spawn/host"); - }); - - it("Navigating to /spawn/not-a-route will redirect to /spawn/host", () => { - cy.visit("/spawn/not-a-route"); - cy.location("pathname").should("eq", "/spawn/host"); - }); - - it("Clicking on the Volume side nav item will redirect to /spawn/volume", () => { - cy.visit("/spawn/host"); - cy.dataCy("volume-nav-tab").click(); - cy.location("pathname").should("eq", "/spawn/volume"); - }); - - it("Clicking on the Host side nav item will redirect to /spawn/host", () => { - cy.visit("/spawn/volume"); - cy.dataCy("host-nav-tab").click(); - cy.location("pathname").should("eq", "/spawn/host"); - }); -}); diff --git a/apps/spruce/cypress/integration/spawn/volume.ts b/apps/spruce/cypress/integration/spawn/volume.ts deleted file mode 100644 index c65b196fad..0000000000 --- a/apps/spruce/cypress/integration/spawn/volume.ts +++ /dev/null @@ -1,329 +0,0 @@ -describe("Spawn volume page", () => { - beforeEach(() => { - cy.visit("/spawn/volume"); - }); - it("Visiting the spawn volume page should display the number of free and mounted volumes.", () => { - cy.dataCy("mounted-badge").contains("9 Mounted"); - cy.dataCy("free-badge").contains("4 Free"); - }); - - it("The table initially displays volumes with status ascending.", () => { - cy.dataCy("leafygreen-table-row").each(($el, index) => - cy.contains(expectedVolNames[index]), - ); - }); - - it("Table should have no cards visible by default.", () => { - expectedVolNames.forEach((cardName) => { - cy.dataCy(cardName).should("not.exist"); - }); - }); - - it("Should render migrating volumes with a different badge and disable action buttons", () => { - cy.contains( - '[data-cy="leafygreen-table-row"]', - "vol-0ae8720b445b771b6", - ).within(() => { - cy.dataCy("volume-status-badge").contains("Migrating"); - cy.get("button[aria-label!='Expand row']").should( - "have.attr", - "aria-disabled", - "true", - ); - }); - }); - - it("Should have a volume card visible initially when the 'volume' query param is provided.", () => { - cy.visit("/spawn/volume?volume=vol-0ea662ac92f611ed4"); - cy.dataCy("spawn-volume-card-vol-0ea662ac92f611ed4").should("exist"); - cy.dataCy("spawn-volume-card-vol-0ea662ac92f611ed4").scrollIntoView(); - cy.dataCy("spawn-volume-card-vol-0ea662ac92f611ed4").should("be.visible"); - }); - - it("Clicking on the row should toggle the volume card open and closed", () => { - cy.visit("/spawn/volume?volume=vol-0ea662ac92f611ed4"); - cy.dataCy("spawn-volume-card-vol-0ea662ac92f611ed4").should("be.visible"); - cy.contains('[data-cy="leafygreen-table-row"]', "vol-0ea662ac92f611ed4") - .find("button[aria-label='Collapse row']") - .click(); - cy.dataCy("spawn-volume-card-vol-0ea662ac92f611ed4").should("not.exist"); - cy.contains('[data-cy="leafygreen-table-row"]', "vol-0ea662ac92f611ed4") - .find("button[aria-label='Expand row']") - .click(); - cy.dataCy("spawn-volume-card-vol-0ea662ac92f611ed4").should("be.visible"); - }); - - it("Clicking the trash can should remove the volume from the table and update free/mounted volumes badges.", () => { - cy.contains( - '[data-cy="leafygreen-table-row"]', - "vol-0c66e16459646704d", - ).should("exist"); - cy.dataCy("trash-vol-0c66e16459646704d").click(); - cy.dataCy("delete-volume-popconfirm").should("be.visible"); - cy.dataCy("delete-volume-popconfirm").within(($el) => { - cy.wrap($el) - .contains("Yes") - .should("be.visible") - .should("not.have.attr", "aria-disabled", "true"); - cy.wrap($el).contains("Yes").click(); - }); - cy.contains( - '[data-cy="leafygreen-table-row"]', - "vol-0c66e16459646704d", - ).should("not.exist"); - cy.dataCy("mounted-badge").contains("9 Mounted"); - cy.dataCy("free-badge").contains("3 Free"); - }); - - it("Clicking the trash can for a mounted volume should show an additional confirmation checkbox which enables the submit button when checked.", () => { - cy.dataCy( - "trash-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - ).click(); - cy.dataCy("delete-volume-popconfirm").should("be.visible"); - cy.dataCy("delete-volume-popconfirm").within(($el) => { - cy.wrap($el) - .getInputByLabel( - "I understand this volume is currently mounted to a host.", - ) - .should("not.be.checked"); - cy.wrap($el).contains("Yes"); - cy.wrap($el).contains("Yes").should("have.attr", "aria-disabled", "true"); - cy.wrap($el) - .getInputByLabel( - "I understand this volume is currently mounted to a host.", - ) - .check({ force: true }); - cy.wrap($el) - .contains("Yes") - .should("not.have.attr", "aria-disabled", "true") - .click(); - }); - cy.dataCy("mounted-badge").contains("8 Mounted"); - cy.dataCy("free-badge").contains("4 Free"); - }); - - it("Clicking on unmount should result in a success toast appearing.", () => { - cy.dataCy( - "detach-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b857", - ).click(); - cy.contains("button", "Yes").click(); - cy.validateToast("success", "Successfully unmounted the volume."); - }); - - it("Clicking on 'Spawn Volume' should open the Spawn Volume Modal", () => { - cy.dataCy("spawn-volume-btn").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("spawn-volume-btn").click(); - cy.dataCy("spawn-volume-modal").should("be.visible"); - }); - - it("Reopening the Spawn Volume modal clears previous input changes.", () => { - cy.dataCy("spawn-volume-btn").click(); - cy.dataCy("spawn-volume-modal").should("be.visible"); - cy.selectLGOption("Type", "sc1"); - cy.dataCy("spawn-volume-modal").within(() => { - cy.contains("button", "Cancel").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.contains("button", "Cancel").click({ force: true }); - }); - - cy.dataCy("spawn-volume-btn").click(); - cy.dataCy("spawn-volume-modal").should("be.visible"); - cy.dataCy("type-select").contains("gp3"); - }); - - describe("Edit volume modal", () => { - beforeEach(() => { - cy.visit("/spawn/volume"); - }); - it("Clicking on 'Edit' should open the Edit Volume Modal", () => { - cy.dataCy( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("update-volume-modal").should("be.visible"); - }); - - it("name, size, expiration inputs should be populated on initial render", () => { - cy.dataCy( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("update-volume-modal").should("be.visible"); - cy.dataCy("volume-name-input").should( - "have.value", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ); - cy.dataCy("volume-size-input").should("have.value", "100"); - cy.validateDatePickerDate("date-picker", { - year: "2020", - month: "06", - day: "06", - }); - cy.dataCy("hour-input").should("have.value", "15"); // Defaults to UTC - cy.dataCy("minute-input").should("have.value", "48"); - }); - - it("Reopening the edit volume modal should reset form input fields.", () => { - cy.dataCy( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("update-volume-modal").should("be.visible"); - cy.dataCy("volume-name-input").type("Hello, World"); - cy.dataCy("update-volume-modal").within(() => { - cy.contains("button", "Cancel").click(); - }); - cy.dataCy( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("volume-name-input").should( - "have.value", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ); - }); - - it("size field is validated correctly", () => { - cy.dataCy( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("update-volume-modal").should("be.visible"); - - // Exceeding max volume limit should disable 'Save' button. - cy.dataCy("volume-size-input").clear(); - cy.dataCy("volume-size-input").type("10000"); - cy.contains("button", "Save").should( - "have.attr", - "aria-disabled", - "true", - ); - // Decreasing volume size should disable 'Save' button. - cy.dataCy("volume-size-input").clear(); - cy.dataCy("volume-size-input").type("2"); - cy.contains("button", "Save").should( - "have.attr", - "aria-disabled", - "true", - ); - }); - - it("Submit button should be enabled when the volume details input value differs from what already exists.", () => { - cy.dataCy( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("update-volume-modal").should("be.visible"); - cy.contains("button", "Save").should( - "have.attr", - "aria-disabled", - "true", - ); - // type a new name - cy.dataCy("volume-name-input").type("Hello, World"); - cy.contains("button", "Save").should( - "not.have.attr", - "aria-disabled", - "true", - ); - - // type original name - cy.dataCy("volume-name-input").clear(); - cy.dataCy("volume-name-input").type( - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ); - cy.contains("button", "Save").should( - "have.attr", - "aria-disabled", - "true", - ); - - cy.contains("Never").click(); - cy.contains("button", "Save").should( - "not.have.attr", - "aria-disabled", - "true", - ); - }); - - it("Clicking on save button should close the modal and show a success toast", () => { - cy.dataCy( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("update-volume-modal").should("be.visible"); - cy.dataCy("volume-name-input").type("Hello, World"); - cy.contains("button", "Save").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.contains("button", "Save").click(); - cy.validateToast("success", "Successfully updated volume"); - cy.dataCy("update-volume-modal").should("not.exist"); - }); - }); - - describe("Migrate Modal", () => { - beforeEach(() => { - cy.visit("/spawn/volume"); - }); - it("migrate button is disabled for volumes with the migrating status", () => { - cy.contains('[data-cy="leafygreen-table-row"]', "vol-0ae8720b445b771b6") - .find("[data-cy=volume-status-badge]") - .contains("Migrating"); - cy.dataCy("migrate-btn-vol-0ae8720b445b771b6").should( - "have.attr", - "aria-disabled", - "true", - ); - }); - // TODO the availability zone defined in Evergreen's test data for this volume is invalid, making it impossible to submit the form. Re-enable these tests when the data is fixed. - it.skip("clicking cancel during confirmation renders the Migrate modal form", () => { - cy.dataCy( - "migrate-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("distro-input").should("be.visible").click(); - cy.dataCy("distro-option-ubuntu1804-workstation").click(); - cy.dataCy("migrate-modal").contains("Next").click({ force: true }); - cy.dataCy("migrate-modal").contains( - "Are you sure you want to migrate this home volume?", - ); - cy.dataCy("distro-input").should("not.exist"); - cy.dataCy("migrate-modal").contains("Cancel").click({ force: true }); - cy.dataCy("distro-input").should("be.visible"); - }); - it.skip("open the Migrate modal and spawn a host", () => { - cy.dataCy( - "migrate-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ).click(); - cy.dataCy("distro-input").click(); - cy.dataCy("distro-option-ubuntu1804-workstation").click(); - cy.dataCy("region-select").should("have.attr", "aria-disabled", "true"); - cy.dataCy("migrate-modal").contains("Next").click({ force: true }); - cy.dataCy("migrate-modal") - .contains("Migrate Volume") - .click({ force: true }); - cy.validateToast( - "success", - "Volume migration has been scheduled. A new host will be spawned and accessible on your Hosts page.", - ); - }); - }); - - const expectedVolNames = [ - "1da0e996608e6871b60a92f6564bbc9cdf66ce90be1178dfb653920542a0d0f0", - "vol-0c66e16459646704d", - "vol-0583d66433a69f136", - "vol-0ea662ac92f611ed4", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b815", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b859", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b825", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b857", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b856", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b835", - "vol-0ae8720b445b771b6", - ]; -}); diff --git a/apps/spruce/cypress/integration/version/action_buttons.ts b/apps/spruce/cypress/integration/version/action_buttons.ts deleted file mode 100644 index 93c91a19b7..0000000000 --- a/apps/spruce/cypress/integration/version/action_buttons.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { INCLUDE_NEVER_ACTIVATED_TASKS } from "constants/cookies"; -import { mockErrorResponse } from "../../utils/mockErrorResponse"; - -describe("Action Buttons", () => { - const patch = "5ecedafb562343215a7ff297"; - const mainlineCommit = "5e4ff3abe3c3317e352062e4"; - const versionPath = (id: string) => `/version/${id}`; - - describe("When viewing a patch build", () => { - beforeEach(() => { - cy.visit(versionPath(patch)); - }); - it("Clicking 'Schedule' button shows modal and clicking on 'Cancel' closes it", () => { - cy.dataCy("schedule-patch").click(); - cy.dataCy("schedule-tasks-modal").should("be.visible"); - cy.contains("Cancel").click(); - cy.dataCy("schedule-tasks-modal").should("not.be.visible"); - }); - - it("Clicking ellipses dropdown shows ellipses options", () => { - cy.dataCy("ellipses-btn").should("not.exist"); - cy.dataCy("ellipsis-btn").click(); - cy.dataCy("card-dropdown").should("be.visible"); - - cy.dataCy("ellipsis-btn").click(); - cy.dataCy("card-dropdown").should("not.exist"); - }); - }); - - describe("Version dropdown options", () => { - beforeEach(() => { - cy.visit(versionPath(patch)); - cy.dataCy("ellipsis-btn").click(); - cy.dataCy("card-dropdown").should("be.visible"); - }); - - it("Error unscheduling a version shows error toast", () => { - cy.dataCy("unschedule-patch").click(); - mockErrorResponse({ - errorMessage: "There was an error unscheduling tasks", - }); - cy.contains("button", "Yes").click({ force: true }); - cy.validateToast("error", "Error unscheduling tasks"); - }); - - it("Clicking 'Unschedule' button show popconfirm with abort checkbox and a toast on success", () => { - cy.dataCy("unschedule-patch").click(); - cy.contains("button", "Yes").click({ force: true }); - cy.validateToast( - "success", - "All tasks were unscheduled and tasks that already started were aborted", - ); - }); - - it("Clicking 'Set Priority' button shows popconfirm with input and toast on success", () => { - const priority = "99"; - cy.dataCy("set-priority-menu-item").click(); - cy.dataCy("patch-priority-input").type(`${priority}{enter}`, { - force: true, - }); - cy.validateToast("success", priority); - }); - - it("Error setting priority shows error toast", () => { - cy.dataCy("set-priority-menu-item").click(); - cy.dataCy("patch-priority-input").type("88", { force: true }); - mockErrorResponse({ - errorMessage: "There was an error setting priority", - }); - cy.dataCy("patch-priority-input").type("{enter}", { force: true }); - cy.validateToast("error", "Error updating priority for patch"); - }); - - it("Sets priority for multiple tasks when version page table is filtered", () => { - const priority = 10; - cy.visit( - `${versionPath(mainlineCommit)}/tasks?statuses=failed-umbrella,failed,known-issue`, - ); - cy.dataCy("ellipsis-btn").click(); - cy.dataCy("card-dropdown").should("be.visible"); - cy.dataCy("set-priority-menu-item").should( - "contain.text", - "Set task priority (2)", - ); - cy.dataCy("set-priority-menu-item").click(); - cy.dataCy("task-priority-input").type(`${priority}{enter}`, { - force: true, - }); - cy.validateToast("success", "Priority updated for 2 tasks."); - }); - - it("Should be able to reconfigure the patch", () => { - cy.dataCy("reconfigure-link").should("not.be.disabled"); - cy.dataCy("reconfigure-link").click(); - cy.location("pathname").should("include", "configure"); - cy.visit(versionPath(patch)); - }); - }); - - describe("When viewing a mainline commit", () => { - describe("Version dropdown options", () => { - beforeEach(() => { - cy.visit(versionPath(mainlineCommit)); - cy.dataCy("ellipsis-btn").click(); - cy.dataCy("card-dropdown").should("be.visible"); - }); - it("Reconfigure link is disabled for mainline commits", () => { - cy.dataCy("reconfigure-link").should("be.visible"); - cy.dataCy("reconfigure-link").should( - "have.attr", - "aria-disabled", - "true", - ); - }); - }); - }); - - describe("Include Never-activated Tasks toggle", () => { - it("sets URL and cookie when toggled on", () => { - cy.visit(versionPath(patch)); - cy.dataCy("ellipsis-btn").click(); - cy.dataCy("card-dropdown").should("be.visible"); - cy.dataCy("card-dropdown") - .contains("Include never-activated tasks") - .click(); - cy.location("search").should( - "contain", - "includeNeverActivatedTasks=true", - ); - cy.getCookie(INCLUDE_NEVER_ACTIVATED_TASKS).should( - "have.property", - "value", - "true", - ); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/version/banners.ts b/apps/spruce/cypress/integration/version/banners.ts deleted file mode 100644 index 43058e4cf9..0000000000 --- a/apps/spruce/cypress/integration/version/banners.ts +++ /dev/null @@ -1,43 +0,0 @@ -describe("banners", () => { - const versionWithBanners = - "/version/logkeeper_e864cf934194c161aa044e4599c8c81cee7b6237/tasks?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC"; - - describe("errors", () => { - beforeEach(() => { - cy.visit(versionWithBanners); - }); - - it("should display the number of configuration errors", () => { - cy.dataCy("configuration-errors-banner").should("be.visible"); - cy.contains("4 errors in configuration file").should("be.visible"); - }); - it("should be able to open the modal and see all errors", () => { - cy.dataCy("configuration-errors-modal-trigger").click(); - cy.dataCy("configuration-errors-modal").should("be.visible"); - cy.get("ol").find("li").should("have.length", 4); - }); - }); - - describe("warnings", () => { - beforeEach(() => { - cy.visit(versionWithBanners); - }); - - it("should display the number of configuration warnings", () => { - cy.dataCy("configuration-warnings-banner").should("be.visible"); - cy.contains("3 warnings in configuration file").should("be.visible"); - }); - it("should be able to open the modal and see all warnings", () => { - cy.dataCy("configuration-warnings-modal-trigger").click(); - cy.dataCy("configuration-warnings-modal").should("be.visible"); - cy.get("ol").find("li").should("have.length", 3); - }); - }); - - describe("ignored", () => { - it("should display a banner", () => { - cy.visit("/version/spruce_e695f654c8b4b959d3e12e71696c3e318bcd4c33"); - cy.dataCy("ignored-banner").should("be.visible"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/version/downstream_projects.ts b/apps/spruce/cypress/integration/version/downstream_projects.ts deleted file mode 100644 index 5bc1631bc4..0000000000 --- a/apps/spruce/cypress/integration/version/downstream_projects.ts +++ /dev/null @@ -1,46 +0,0 @@ -describe("Downstream Projects Tab", () => { - const DOWNSTREAM_ROUTE = `/version/5f74d99ab2373627c047c5e5/downstream-projects`; - - beforeEach(() => { - cy.visit(DOWNSTREAM_ROUTE); - }); - - it("shows number of failed patches in the Downstream tab label", () => { - cy.dataCy("downstream-tab-badge").should("exist"); - cy.dataCy("downstream-tab-badge").should("contain.text", "1"); - }); - - it("renders child patches", () => { - cy.dataCy("project-accordion").should("have.length", 3); - cy.dataCy("project-title").should("have.length", 3); - cy.dataCy("downstream-tasks-table").should("have.length", 3); - }); - - it("links to base commit", () => { - cy.dataCy("accordion-toggle").first().click(); - cy.dataCy("downstream-base-commit") - .should("have.attr", "href") - .and( - "includes", - "/version/logkeeper_e3579537e848d14f0c3e5c25ef745fd0f10702d4", - ); - }); - - it("filters by test name", () => { - cy.dataCy("task-name-filter").eq(1).click(); - cy.dataCy("task-name-filter-wrapper") - .find("input") - .as("testnameInputWrapper"); - cy.get("@testnameInputWrapper").focus(); - cy.get("@testnameInputWrapper").type("generate-lint"); - cy.get("@testnameInputWrapper").type("{enter}"); - cy.location("search").should("not.contain", "generate-lint"); // Should not update the URL. - cy.contains("generate-lint").should("be.visible"); - }); - - it("does not push query params to the URL", () => { - cy.location().should((loc) => { - expect(loc.pathname).to.equal(DOWNSTREAM_ROUTE); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/version/name_change_modal.ts b/apps/spruce/cypress/integration/version/name_change_modal.ts deleted file mode 100644 index ecb6c0b2b4..0000000000 --- a/apps/spruce/cypress/integration/version/name_change_modal.ts +++ /dev/null @@ -1,51 +0,0 @@ -describe("Name change modal", () => { - beforeEach(() => { - cy.visit("version/5f74d99ab2373627c047c5e5"); - }); - - it("Use the name change modal to change the name of a patch", () => { - const originalName = "main: EVG-7823 add a commit queue message (#4048)"; - cy.contains(originalName); - cy.dataCy("name-change-modal-trigger").click(); - const newName = "a different name"; - cy.get("textarea").clear(); - cy.get("textarea").type(newName); - cy.contains("Confirm").click(); - cy.get("textarea").should("not.exist"); - cy.contains(newName); - cy.validateToast("success", "Patch name was successfully updated.", true); - // revert name change - cy.dataCy("name-change-modal-trigger").click(); - cy.get("textarea").clear(); - cy.get("textarea").type(originalName); - cy.contains("Confirm").click(); - cy.get("textarea").should("not.exist"); - cy.validateToast("success", "Patch name was successfully updated.", true); - cy.contains(originalName); - }); - - it("The confirm button is disabled when the text area value is empty or greater than 300 characters", () => { - cy.dataCy("name-change-modal-trigger").click(); - cy.get("textarea").clear(); - cy.contains("button", "Confirm").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.get("textarea").type("lol"); - cy.contains("button", "Confirm").should( - "have.attr", - "aria-disabled", - "false", - ); - const over300Chars = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - cy.get("textarea").type(over300Chars); - cy.contains("button", "Confirm").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.contains("Value cannot exceed 300 characters"); - }); -}); diff --git a/apps/spruce/cypress/integration/version/patch_with_aborted_task.ts b/apps/spruce/cypress/integration/version/patch_with_aborted_task.ts deleted file mode 100644 index 105f223858..0000000000 --- a/apps/spruce/cypress/integration/version/patch_with_aborted_task.ts +++ /dev/null @@ -1,10 +0,0 @@ -describe("Patch with aborted task", () => { - it("Shows status `aborted` in task table for tasks that were aborted", () => { - cy.visit("version/5e94c2dfe3c3312519b59480"); - cy.dataCy("tasks-table-row") - .eq(1) - .within(() => { - cy.dataCy("task-status-badge").should("have.text", "Aborted"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/version/restart_modal.ts b/apps/spruce/cypress/integration/version/restart_modal.ts deleted file mode 100644 index 8f882d062b..0000000000 --- a/apps/spruce/cypress/integration/version/restart_modal.ts +++ /dev/null @@ -1,106 +0,0 @@ -describe("version/restart_modal", () => { - describe("Restarting a patch with Downstream Tasks", () => { - it("Clicking on the Select Downstream Tasks should show the downstream projects", () => { - const versionWithDownstream = `/version/5f74d99ab2373627c047c5e5`; - cy.visit(versionWithDownstream); - cy.dataCy("restart-version").click(); - cy.dataCy("select-downstream").first().click(); - cy.dataCy("select-downstream").first().contains("evergreen").click(); - }); - }); - - describe("Restarting a patch", () => { - beforeEach(() => { - cy.visit(path); - cy.dataCy("version-restart-modal").should("not.be.visible"); - cy.dataCy("restart-version").click(); - cy.dataCy("version-restart-modal").should("be.visible"); - }); - it("Clicking on a variant should toggle an accordion drop down of tasks", () => { - cy.dataCy("variant-accordion").first().click(); - cy.dataCy("task-status-checkbox").should("exist"); - }); - it("Clicking on a variant checkbox should toggle its textbox and all the associated tasks", () => { - cy.dataCy("variant-accordion").first().click(); - cy.dataCy("task-status-badge").should("contain.text", "0 of 1 Selected"); - cy.dataCy("variant-checkbox-select-all").first().click({ force: true }); - cy.dataCy("task-status-badge").should("contain.text", "1 of 1 Selected"); - cy.dataCy("variant-checkbox-select-all").first().click({ force: true }); - cy.dataCy("task-status-badge").should("contain.text", "0 of 1 Selected"); - }); - it("Clicking on a task should toggle its check box and select the task", () => { - cy.dataCy("variant-accordion").first().click(); - cy.dataCy("task-status-checkbox").first().click({ force: true }); - cy.dataCy("task-status-badge").should("contain.text", "1 of 1 Selected"); - }); - - it("Selecting on the task status filter should toggle the tasks that have matching statuses to it", () => { - cy.dataCy("task-status-filter").click(); - cy.getInputByLabel("All").check({ force: true }); - cy.dataCy("task-status-filter").click(); - - cy.dataCy("version-restart-modal").should( - "contain.text", - "Are you sure you want to restart the 1 selected tasks?", - ); - cy.dataCy("task-status-filter").click(); - cy.getInputByLabel("All").check({ force: true }); - cy.dataCy("task-status-filter").click(); - }); - - it("Selecting on the base status filter should toggle the tasks that have matching statuses to it", () => { - cy.dataCy("version-restart-modal").within(() => { - cy.dataCy("base-task-status-filter").click(); - cy.getInputByLabel("Succeeded").check({ force: true }); - cy.dataCy("base-task-status-filter").click(); - - // ideally this would target the text field itself but leafygreen Body tags dont - // support cy-data elements currently - cy.dataCy("confirmation-message").should( - "contain.text", - "Are you sure you want to restart the 1 selected tasks?", - ); - cy.dataCy("base-task-status-filter").click(); - - cy.getInputByLabel("Succeeded").check({ force: true }); - cy.dataCy("base-task-status-filter").click(); - }); - }); - - it("Restarting a task should close the modal and display a success message if it occurs successfully.", () => { - cy.dataCy("version-restart-modal").within(() => { - cy.dataCy("variant-accordion").first().click(); - cy.dataCy("task-status-checkbox").first().click({ force: true }); - cy.contains("button", "Restart").click(); - }); - cy.dataCy("version-restart-modal").should("not.be.visible"); - cy.validateToast("success", "Successfully restarted tasks!"); - }); - }); - - describe("Restarting mainline commits", () => { - it("should be able to restart scheduled mainline commit tasks", () => { - cy.visit("/version/spruce_ab494436448fbb1d244833046ea6f6af1544e86d"); - cy.dataCy("restart-version").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("restart-version").click(); - cy.dataCy("version-restart-modal").should("be.visible"); - cy.dataCy("version-restart-modal").within(() => { - cy.dataCy("accordion-toggle").click(); - cy.getInputByLabel("check_codegen").should("exist"); - cy.getInputByLabel("check_codegen").check({ force: true }); - cy.contains("button", "Restart").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.contains("button", "Restart").click(); - }); - cy.validateToast("success", "Successfully restarted tasks!"); - }); - }); - const path = `/version/5ecedafb562343215a7ff297`; -}); diff --git a/apps/spruce/cypress/integration/version/routes.ts b/apps/spruce/cypress/integration/version/routes.ts deleted file mode 100644 index f4c51c0aeb..0000000000 --- a/apps/spruce/cypress/integration/version/routes.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { waitForTaskTable } from "../../utils"; - -const versions = { - 0: "5ecedafb562343215a7ff297", // normal patch - 1: "i-dont-exist", // non existent patch - 2: "52a630633ff1227909000021", // patch 2 - 3: "5e6bb9e23066155a993e0f1a", // unconfigured patch - 4: "evergreen_33016573166a36bd5f46b4111151899d5c4e95b1", // basecommit for versions[0] - 5: "5e4ff3abe3c3317e352062e4", -}; - -const versionRoute = (id: string) => `/version/${id}`; - -describe("Version route", () => { - describe("Metadata", () => { - it("Shows patch parameters if they exist", () => { - cy.visit(versionRoute(versions[0])); - cy.dataCy("parameters-modal").should("not.exist"); - cy.dataCy("parameters-link").click(); - cy.dataCy("parameters-modal").should("be.visible"); - cy.get('button[aria-label="Close modal"]').click(); - cy.dataCy("parameters-modal").should("not.be.visible"); - }); - it("'Base commit' link in metadata links to version page", () => { - cy.visit(versionRoute(versions[0])); - cy.dataCy("patch-base-commit") - .should("have.attr", "href") - .and("include", `/version/${versions[4]}`); - }); - it("Doesn't show patch parameters if they don't exist", () => { - cy.visit(versionRoute(versions[2])); - cy.dataCy("parameters-link").should("not.exist"); - cy.dataCy("parameters-modal").should("not.exist"); - }); - }); - - describe("Build Variants", () => { - beforeEach(() => { - cy.visit(versionRoute(versions[0])); - waitForTaskTable(); - }); - - it("Lists the patch's build variants", () => { - cy.dataCy("build-variants").within(() => { - cy.dataCy("patch-build-variant").within( - ($variants) => Array.from($variants).length > 0, - ); - }); - }); - - describe("Grouped Task Status Badge", () => { - it("Shows tooltip with task's name on hover", () => { - cy.dataCy("build-variants").within(() => { - cy.dataCy("grouped-task-status-badge").first().as("statusBadge"); - cy.get("@statusBadge").trigger("mouseover"); - cy.get("@statusBadge").within(($el) => { - expect($el.text()).to.contain("1Succeeded"); - }); - }); - }); - - it("Navigates to task tab and applies filters when clicking on grouped task status badge", () => { - // click on a different tab first, so that we aren't on the task tab initially - cy.dataCy("changes-tab").first().click(); - cy.dataCy("task-tab") - .should("have.attr", "aria-selected") - .and("equal", "false"); - - // clicking on task status badge should move to the task tab - cy.dataCy("build-variants").within(() => { - cy.dataCy("grouped-task-status-badge").first().click(); - }); - cy.dataCy("task-tab") - .should("have.attr", "aria-selected") - .and("equal", "true"); - cy.location("search").should( - "include", - "sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&statuses=success&variant=%5Eubuntu1604%24", - ); - - // Check that filter values have updated. - cy.dataCy("status-filter").click(); - cy.dataCy("status-filter-wrapper").should("be.visible"); - cy.getInputByLabel("Succeeded") - .should("have.attr", "aria-checked") - .and("equal", "true"); - - cy.dataCy("variant-filter").click(); - cy.dataCy("variant-filter-wrapper").find("input").as("variantInput"); - cy.get("@variantInput").should("have.value", "^ubuntu1604$"); - }); - - it("Keeps sorts but not other filters when clicking on grouped task status badge", () => { - // Clear filters persisting from last test - cy.dataCy("clear-all-filters").click(); - - // Apply name filter - cy.dataCy("task-name-filter").click(); - cy.dataCy("task-name-filter-wrapper").find("input").as("taskNameInput"); - cy.get("@taskNameInput").focus(); - cy.get("@taskNameInput").type("a-task-name{enter}"); - - // name filter shouldn't be applied after clicking task status badge - cy.dataCy("build-variants").within(() => { - cy.dataCy("grouped-task-status-badge").first().click(); - }); - cy.location("search").should( - "include", - "sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&statuses=success&variant=%5Eubuntu1604%24", - ); - }); - }); - - describe("Build Variant Name", () => { - it("Navigates to task tab and applies filters when clicking on build variant name", () => { - // Clear filters persisting from last test - cy.dataCy("clear-all-filters").click(); - - // click on a different tab first, so that we aren't on the task tab initially - cy.dataCy("changes-tab").first().click(); - cy.dataCy("task-tab") - .should("have.attr", "aria-selected") - .and("equal", "false"); - - // clicking on build variant name should move to the task tab - cy.dataCy("build-variant-display-name").first().click(); - cy.dataCy("task-tab") - .should("have.attr", "aria-selected") - .and("equal", "true"); - cy.location("search").should( - "include", - "sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&variant=%5Eubuntu1604%24", - ); - - // Check that filter values have updated. - cy.dataCy("variant-filter").click(); - cy.dataCy("variant-filter-wrapper").find("input").as("variantInput"); - cy.get("@variantInput").should("have.value", "^ubuntu1604$"); - }); - - it("Keeps sorts but not other filters when clicking on build variant name", () => { - // Clear filters persisting from last test - cy.dataCy("clear-all-filters").click(); - - // Apply name filter - cy.dataCy("task-name-filter").click(); - cy.dataCy("task-name-filter-wrapper").find("input").as("taskNameInput"); - cy.get("@taskNameInput").focus(); - cy.get("@taskNameInput").type("a-task-name{enter}"); - - // name filter shouldn't be applied after clicking build variant name - cy.dataCy("build-variant-display-name").first().click(); - cy.location("search").should( - "include", - "sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&variant=%5Eubuntu1604%24", - ); - }); - }); - }); - - describe("Page title", () => { - beforeEach(() => { - cy.visit(versionRoute(versions[5])); - }); - it("Should include a link to Jira", () => { - cy.dataCy("page-title") - .contains("a", "EVG-7425") - .should( - "have.attr", - "href", - "https://jira.example.com/browse/EVG-7425", - ); - }); - - it("Should include a link to GitHub", () => { - cy.dataCy("page-title") - .contains("a", "https://github.com/evergreen-ci/evergreen/pull/3186") - .should( - "have.attr", - "href", - "https://github.com/evergreen-ci/evergreen/pull/3186", - ); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/version/schedule_modal.ts b/apps/spruce/cypress/integration/version/schedule_modal.ts deleted file mode 100644 index 431682096c..0000000000 --- a/apps/spruce/cypress/integration/version/schedule_modal.ts +++ /dev/null @@ -1,25 +0,0 @@ -describe("Restarting and scheduling mainline commits", () => { - it("should be able to schedule inactive mainline commit tasks", () => { - cy.visit("/version/spruce_e695f654c8b4b959d3e12e71696c3e318bcd4c33"); - cy.dataCy("schedule-patch").should("exist"); - cy.dataCy("schedule-patch").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("schedule-patch").click(); - cy.dataCy("schedule-tasks-modal").should("be.visible"); - cy.dataCy("schedule-tasks-modal").within(() => { - cy.dataCy("accordion-toggle").click(); - cy.getInputByLabel("check_codegen").should("exist"); - cy.getInputByLabel("check_codegen").click({ force: true }); - cy.contains("button", "Schedule").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.contains("button", "Schedule").click({ force: true }); - }); - cy.validateToast("success", "Successfully scheduled tasks!"); - }); -}); diff --git a/apps/spruce/cypress/integration/version/subscription_modal.ts b/apps/spruce/cypress/integration/version/subscription_modal.ts deleted file mode 100644 index 7133d88f30..0000000000 --- a/apps/spruce/cypress/integration/version/subscription_modal.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { openSubscriptionModal } from "../../utils/subscriptionModal"; - -describe("Version Subscription Modal", () => { - const dataCyToggleModalButton = "notify-patch"; - const route = "/version/5e4ff3abe3c3317e352062e4/tasks"; - const regexSelectorRow = "regex-selector-row"; - - describe("Regex selector inputs", () => { - it("Can add and remove regex selectors", () => { - openSubscriptionModal(route, dataCyToggleModalButton); - cy.selectLGOption("Event", "A build-variant in this version finishes"); - cy.dataCy(regexSelectorRow).should("have.length", 0); - cy.contains("Add Additional Criteria").click(); - cy.dataCy(regexSelectorRow).should("have.length", 1); - cy.dataCy("delete-item-button").first().click(); - cy.dataCy(regexSelectorRow).should("have.length", 0); - }); - - // Skip because of complications with SpruceForm - it.skip("'Regex' input should be disabled when the 'Field name' is empty and enabled otherwise", () => { - cy.contains("Add Additional Criteria").click(); - cy.dataCy(regexSelectorRow).should("be.disabled"); - cy.selectLGOption("regex-select", "Build Variant Name"); - cy.dataCy(regexSelectorRow).should("not.be.disabled"); - }); - - it("Selecting a regex selector type will disable that option in other regex selector type dropdowns", () => { - openSubscriptionModal(route, dataCyToggleModalButton); - cy.selectLGOption("Event", "A build-variant in this version finishes"); - cy.contains("Add Additional Criteria").click(); - cy.contains("Build Variant ID").should("be.visible"); - cy.contains("Add Additional Criteria").click(); - cy.contains("Build Variant Name").should("be.visible"); - cy.dataCy("regex-select").last().click(); - cy.get("#regex-select-menu").within(() => { - cy.contains("Build Variant ID").should( - "have.css", - "cursor", - "not-allowed", - ); - }); - }); - - it("Regex selectors are optional for triggers that offer them", () => { - openSubscriptionModal(route, dataCyToggleModalButton); - cy.selectLGOption("Event", "A build-variant in this version finishes"); - cy.dataCy("jira-comment-input").type("EVG-2000"); - cy.contains("button", "Save").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.contains("button", "Save").click(); - cy.validateToast("success", "Your subscription has been added"); - }); - - // Skip because of complications with SpruceForm - it.skip("Switching between Event types should either hide or reset regex selector inputs", () => { - openSubscriptionModal(route, dataCyToggleModalButton); - cy.selectLGOption("Event", "A build-variant in this version finishes"); - cy.contains("Add Additional Criteria").click(); - cy.dataCy("regex-select").click(); - cy.contains("Build Variant Name").click(); - cy.dataCy("regex-input").type("stuff"); - cy.dataCy("regex-input").should("have.value", "stuff"); - cy.selectLGOption("Event", "A build-variant in this version fails"); - cy.dataCy("regex-input").should("have.value", ""); - }); - - // Skip because of complications with SpruceForm - it.skip("Changing the regex selector dropdown should reset the regex selector input", () => { - openSubscriptionModal(route, dataCyToggleModalButton); - cy.selectLGOption("Event", "A build-variant in this version finishes"); - cy.contains("Add Additional Criteria").click(); - cy.selectLGOption("Field name", "Build Variant Name"); - - cy.dataCy("regex-input").type("stuff"); - cy.dataCy("regex-input").should("have.value", "stuff"); - cy.dataCy("regex-select").click(); - cy.contains("Build Variant ID").click(); - cy.dataCy("regex-input").should("have.value", ""); - }); - - it("Display success toast after submitting a valid form with regex selectors inputs and request succeeds", () => { - openSubscriptionModal(route, dataCyToggleModalButton); - cy.selectLGOption("Event", "A build-variant in this version finishes"); - cy.contains("Add Additional Criteria").click(); - cy.selectLGOption("Field name", "Build Variant Name"); - cy.dataCy("regex-input").type("stuff"); - cy.dataCy("jira-comment-input").type("EVG-2000"); - cy.contains("button", "Save").click(); - cy.validateToast("success", "Your subscription has been added"); - }); - - it("'Add Additional Criteria' button should not appear when there are enough 'Field name' dropdowns to represent all possible regex selector types for a trigger", () => { - openSubscriptionModal(route, dataCyToggleModalButton); - cy.selectLGOption("Event", "A build-variant in this version finishes"); - cy.contains("Add Additional Criteria").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.contains("Add Additional Criteria").click(); - cy.contains("Add Additional Criteria").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.contains("Add Additional Criteria").click(); - cy.contains("Add Additional Criteria").should("not.exist"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/version/task_duration.ts b/apps/spruce/cypress/integration/version/task_duration.ts deleted file mode 100644 index 0aea7347b0..0000000000 --- a/apps/spruce/cypress/integration/version/task_duration.ts +++ /dev/null @@ -1,124 +0,0 @@ -describe("Task Duration Tab", () => { - beforeEach(() => { - cy.visit("/version/5e4ff3abe3c3317e352062e4/task-duration"); - }); - describe("when interacting with the filters on the page", () => { - it("updates URL appropriately when task name filter is applied", () => { - const filterText = "test-annotation"; - // Apply text filter. - cy.dataCy("task-name-filter-popover").click(); - cy.dataCy("task-name-filter-popover-input-filter").type( - `${filterText}{enter}`, - ); - cy.dataCy("task-duration-table-row").should("have.length", 1); - cy.location("search").should( - "include", - `page=0&sorts=DURATION%3ADESC&taskName=${filterText}`, - ); - // Clear text filter. - cy.dataCy("task-name-filter-popover").click(); - cy.dataCy("task-name-filter-popover-input-filter").clear(); - cy.dataCy("task-name-filter-popover-input-filter").type("{enter}"); - cy.location("search").should("include", `page=0`); - }); - - it("updates URL appropriately when status filter is applied", () => { - // Apply status filter. - cy.dataCy("status-filter-popover").click(); - cy.dataCy("tree-select-options").within(() => - cy.contains("Running").click({ force: true }), - ); - cy.dataCy("task-duration-table-row").should("have.length", 3); - cy.location("search").should( - "include", - "page=0&sorts=DURATION%3ADESC&statuses=running-umbrella,started,dispatched", - ); - // Clear status filter. - cy.dataCy("status-filter-popover").click(); - cy.dataCy("tree-select-options").within(() => - cy.contains("Succeeded").click({ force: true }), - ); - cy.location("search").should("include", "page=0&sorts=DURATION%3ADESC"); - }); - - it("updates URL appropriately when build variant filter is applied", () => { - const filterText = "Lint"; - // Apply text filter. - cy.dataCy("build-variant-filter-popover").click(); - cy.dataCy("build-variant-filter-popover-input-filter").type( - `${filterText}{enter}`, - ); - cy.dataCy("task-duration-table-row").should("have.length", 2); - cy.location("search").should( - "include", - `page=0&sorts=DURATION%3ADESC&variant=${filterText}`, - ); - // Clear text filter. - cy.dataCy("build-variant-filter-popover").click(); - cy.dataCy("build-variant-filter-popover-input-filter").clear(); - cy.dataCy("build-variant-filter-popover-input-filter").type("{enter}"); - cy.location("search").should("include", `page=0`); - }); - - it("updates URL appropriately when sort is changing", () => { - const durationSortControl = "button[aria-label='Sort by Task Duration']"; - const statusSortControl = "button[aria-label='Sort by Status']"; - const variantSortControl = "button[aria-label='Sort by Build Variant']"; - // The default sort (DURATION DESC) should be applied - cy.location("search").should("include", "sorts=DURATION%3ADESC"); - const longestTask = "test-thirdparty"; - cy.contains(longestTask).should("be.visible"); - cy.dataCy("task-duration-table-row") - .first() - .should("contain", longestTask); - cy.get(durationSortControl).click(); - cy.location("search").should("not.include", "duration"); - cy.get(durationSortControl).click(); - cy.location("search").should("include", "sorts=DURATION%3AASC"); - const shortestTask = "clone_test-model"; - cy.contains(shortestTask).should("be.visible"); - cy.dataCy("task-duration-table-row") - .first() - .should("contain", shortestTask); - cy.get(statusSortControl).click(); - cy.location("search").should( - "include", - "page=0&sorts=DURATION%3AASC%3BSTATUS%3AASC", - ); - cy.get(statusSortControl).click(); - cy.location("search").should( - "include", - "page=0&sorts=DURATION%3AASC%3BSTATUS%3ADESC", - ); - cy.get(variantSortControl).click(); - cy.location("search").should( - "include", - "page=0&sorts=DURATION%3AASC%3BSTATUS%3ADESC", - ); - }); - - it("clearing all filters resets to the default sort", () => { - const durationSortControl = "button[aria-label='Sort by Task Duration']"; - cy.get(durationSortControl).click(); - cy.location("search").should("not.include", "duration"); - cy.get(durationSortControl).click(); - cy.location("search").should("include", "sorts=DURATION%3AASC"); - cy.contains("Clear all filters").click(); - cy.location("search").should("include", "sorts=DURATION%3ADESC"); - }); - - it("shows message when no test results are found", () => { - const filterText = "this_does_not_exist"; - - cy.dataCy("task-name-filter-popover").click(); - cy.dataCy("task-name-filter-popover-input-filter").type( - `${filterText}{enter}`, - ); - cy.dataCy("task-name-filter-popover-task-duration-table-row").should( - "have.length", - 0, - ); - cy.contains("No tasks found.").should("exist"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/version/task_filters.ts b/apps/spruce/cypress/integration/version/task_filters.ts deleted file mode 100644 index 6a82f7c3a3..0000000000 --- a/apps/spruce/cypress/integration/version/task_filters.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { urlSearchParamsAreUpdated, waitForTaskTable } from "../../utils"; - -const patch = { - id: "5e4ff3abe3c3317e352062e4", -}; -const pathTasks = `/version/${patch.id}/tasks`; -const pathURLWithFilters = `${pathTasks}?page=0&sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&statuses=failed,success,running-umbrella,dispatched,started&taskName=test-thirdparty&variant=ubuntu`; -const defaultPath = `${pathTasks}?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC`; - -describe("Tasks filters", () => { - it("Should clear any filters with the Clear All Filters button and reset the table to its default state", () => { - cy.visit(pathURLWithFilters); - waitForTaskTable(); - cy.dataCy("tasks-table-row").should("be.visible"); - - cy.dataCy("clear-all-filters").click(); - cy.location().should((loc) => { - expect(loc.href).to.equal(loc.origin + defaultPath); - }); - cy.dataCy("task-name-filter").click(); - cy.dataCy("task-name-filter-wrapper") - .find("input") - .invoke("val") - .should("be.empty"); - cy.dataCy("status-filter").click(); - cy.dataCy("status-filter-wrapper") - .get('input[type="checkbox"]') - .should("not.be.checked"); - cy.dataCy("base-status-filter").click(); - cy.dataCy("base-status-filter-wrapper") - .get('input[type="checkbox"]') - .should("not.be.checked"); - cy.dataCy("variant-filter").click(); - cy.dataCy("variant-filter-wrapper") - .find("input") - .invoke("val") - .should("be.empty"); - }); - - describe("Variant input field", () => { - const variantInputValue = "lint"; - const urlParam = "variant"; - it("Updates url with input value and fetches tasks filtered by variant", () => { - cy.visit(defaultPath); - waitForTaskTable(); - cy.dataCy("variant-filter").click(); - - cy.dataCy("variant-filter-wrapper").find("input").as("variant-input"); - cy.get("@variant-input").focus(); - cy.get("@variant-input").type(variantInputValue); - cy.get("@variant-input").type("{enter}", { scrollBehavior: false }); - cy.dataCy("variant-filter-wrapper").should("not.exist"); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: variantInputValue, - }); - waitForTaskTable(); - cy.dataCy("filtered-count").should("contain.text", 2); - - cy.dataCy("variant-filter").click(); - cy.get("@variant-input").focus(); - cy.get("@variant-input").clear(); - cy.get("@variant-input").type("{enter}", { scrollBehavior: false }); - cy.dataCy("variant-filter-wrapper").should("not.exist"); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: null, - }); - waitForTaskTable(); - cy.dataCy("filtered-count").should("contain.text", 47); - }); - }); - - describe("Task name input field", () => { - const taskNameInputValue = "test-cloud"; - const urlParam = "taskName"; - it("Updates url with input value and fetches tasks filtered by task name", () => { - cy.visit(defaultPath); - waitForTaskTable(); - cy.dataCy("task-name-filter").click(); - - cy.dataCy("task-name-filter-wrapper").find("input").as("taskname-input"); - cy.get("@taskname-input").focus(); - cy.get("@taskname-input").type(taskNameInputValue); - cy.get("@taskname-input").type("{enter}", { scrollBehavior: false }); - cy.dataCy("task-name-filter-wrapper").should("not.exist"); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: taskNameInputValue, - }); - waitForTaskTable(); - cy.dataCy("filtered-count").should("contain.text", 1); - - cy.dataCy("task-name-filter").click(); - cy.get("@taskname-input").focus(); - cy.get("@taskname-input").clear(); - cy.get("@taskname-input").type("{enter}", { scrollBehavior: false }); - cy.dataCy("task-name-filter-wrapper").should("not.exist"); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: null, - }); - waitForTaskTable(); - cy.dataCy("filtered-count").should("contain.text", 47); - }); - }); - - describe("Task Statuses select", () => { - const dataCyStatusDropdown = "status-filter-wrapper"; - const urlParam = "statuses"; - - beforeEach(() => { - cy.visit(defaultPath); - waitForTaskTable(); - cy.dataCy("status-filter").click(); - cy.dataCy("status-filter-wrapper").should("be.visible"); - }); - - it("Clicking on a status filter filters the tasks to only those statuses", () => { - cy.dataCy("filtered-count") - .invoke("text") - .then((preFilterCount) => { - selectCheckboxOption(dataCyStatusDropdown, "Failed", true); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: "failed", - }); - waitForTaskTable(); - cy.dataCy("filtered-count").should("have.text", 2); - - cy.dataCy("filtered-count") - .invoke("text") - .then((postFilterCount) => { - cy.dataCy("filtered-count").should( - "not.have.text", - preFilterCount, - ); - selectCheckboxOption(dataCyStatusDropdown, "Succeeded", true); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: "failed-umbrella,failed,known-issue,success", - }); - waitForTaskTable(); - cy.dataCy("filtered-count").should( - "not.have.text", - postFilterCount, - ); - }); - }); - }); - - it("Clicking on 'All' checkbox adds all the statuses and clicking again removes them", () => { - const taskStatuses = [ - "All", - "Failed / Timed Out", - "Failed", - "Known Issue", - "Succeeded", - "Running", - "Will Run", - "Dispatched", - "Undispatched", - "Aborted", - "Blocked", - ]; - selectCheckboxOption(dataCyStatusDropdown, "All", true); - assertChecked(dataCyStatusDropdown, taskStatuses, true); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: "all", - }); - waitForTaskTable(); - - selectCheckboxOption(dataCyStatusDropdown, "All", false); - assertChecked(dataCyStatusDropdown, taskStatuses, false); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: null, - }); - }); - }); - - describe("Task Base Statuses select", () => { - const dataCyBaseStatusDropdown = "base-status-filter-wrapper"; - const urlParam = "baseStatuses"; - - beforeEach(() => { - cy.visit(defaultPath); - waitForTaskTable(); - cy.dataCy("base-status-filter").click(); - cy.dataCy("base-status-filter-wrapper").should("be.visible"); - }); - - it("Clicking on a base status filter filters the tasks to only those base statuses", () => { - // All tasks have a base status of succeeded for this version. - cy.dataCy("filtered-count") - .invoke("text") - .then((preFilterCount) => { - selectCheckboxOption(dataCyBaseStatusDropdown, "Succeeded", true); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: "success", - }); - waitForTaskTable(); - - cy.dataCy("filtered-count").should("have.text", 44); - selectCheckboxOption(dataCyBaseStatusDropdown, "Succeeded", false); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: null, - }); - waitForTaskTable(); - cy.dataCy("filtered-count").should("have.text", preFilterCount); - }); - }); - - it("Clicking on 'All' checkbox adds all the base statuses and clicking again removes them", () => { - const taskStatuses = ["All", "Succeeded", "Running"]; - selectCheckboxOption(dataCyBaseStatusDropdown, "All", true); - assertChecked(dataCyBaseStatusDropdown, taskStatuses, true); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: "all", - }); - waitForTaskTable(); - - selectCheckboxOption(dataCyBaseStatusDropdown, "All", false); - assertChecked(dataCyBaseStatusDropdown, taskStatuses, false); - urlSearchParamsAreUpdated({ - pathname: pathTasks, - paramName: urlParam, - search: null, - }); - }); - }); -}); - -/** - * Function used to assert if checkboxes with certain labels are checked or unchecked. - * @param dataCy - data-cy of the container - * @param statuses list of labels to assert on - * @param checked true if should be checked, false if should be unchecked - */ -const assertChecked = ( - dataCy: string, - statuses: string[], - checked: boolean, -) => { - cy.dataCy(dataCy) - .find(".cy-checkbox") - .each((el) => { - expect(statuses).to.include(el.text()); - if (checked) { - cy.wrap(el).find('input[type="checkbox"]').should("be.checked"); - } else { - cy.wrap(el).find('input[type="checkbox"]').should("not.be.checked"); - } - }); -}; - -/** - * Function used to select a checkbox option from the table filter dropdown. - * Only the first checkbox whose label is a match (i.e. the umbrella group name) will be checked. - * @param dataCy - data-cy of the container - * @param label label of the checkbox option to click on - * @param checked true if should be checked, false if should be unchecked - */ -const selectCheckboxOption = ( - dataCy: string, - label: string, - checked: boolean, -) => { - cy.dataCy(dataCy) - .find(".cy-checkbox") - .should("not.be.disabled") - .each((el) => { - if (el.text() === label) { - if (checked) { - cy.wrap(el) - .find('input[type="checkbox"]') - .check({ force: true, scrollBehavior: false }); - } else { - cy.wrap(el) - .find('input[type="checkbox"]') - .uncheck({ force: true, scrollBehavior: false }); - } - return false; - } - }); -}; diff --git a/apps/spruce/cypress/integration/version/task_table.ts b/apps/spruce/cypress/integration/version/task_table.ts deleted file mode 100644 index 14f7666305..0000000000 --- a/apps/spruce/cypress/integration/version/task_table.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { SEEN_TASK_REVIEW_TOOLTIP } from "constants/cookies"; -import { - clickOnPageSizeBtnAndAssertURLandTableSize, - waitForTaskTable, -} from "../../utils"; - -const pathTasks = "/version/5e4ff3abe3c3317e352062e4/tasks"; -const patchDescriptionTasksExist = "dist"; - -describe("Task table", () => { - it("Loading skeleton does not persist when you navigate to Patch page from My Patches and adjust a filter", () => { - cy.visit("user/patches"); - cy.dataCy("patch-card-patch-link") - .filter(`:contains(${patchDescriptionTasksExist})`) - .click(); - cy.dataCy("tasks-table").should("exist"); - }); - - it("Updates sorting in the url when column headers are clicked", () => { - cy.visit(pathTasks); - waitForTaskTable(); - cy.dataCy("tasks-table-row").should("be.visible"); - - cy.location("search").should( - "contain", - "sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC", - ); - - const nameSortControl = "button[aria-label='Sort by Name']"; - const statusSortControl = "button[aria-label='Sort by Task Status']"; - const baseStatusSortControl = - "button[aria-label='Sort by Previous Status']"; - const variantSortControl = "button[aria-label='Sort by Variant']"; - - cy.get(nameSortControl).click(); - cy.location("search").should("contain", "BASE_STATUS%3ADESC%3BNAME%3AASC"); - - cy.get(variantSortControl).click(); - cy.location("search").should("contain", "sorts=NAME%3AASC%3BVARIANT%3AASC"); - - cy.get(statusSortControl).click(); - cy.location("search").should( - "contain", - "sorts=VARIANT%3AASC%3BSTATUS%3AASC", - ); - - cy.get(baseStatusSortControl).click(); - cy.location("search").should( - "contain", - "sorts=STATUS%3AASC%3BBASE_STATUS%3AASC", - ); - - cy.get(baseStatusSortControl).click(); - cy.location("search").should( - "contain", - "sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC", - ); - - cy.get(baseStatusSortControl).click(); - cy.location("search").should("contain", "sorts=STATUS%3AASC"); - }); - - it("Clicking task name goes to task page for that task", () => { - cy.visit(pathTasks); - cy.dataCy("tasks-table-row") - .eq(0) - .within(() => { - cy.get("a").should("have.attr", "href").and("include", "/task"); - }); - }); - - it("Task count displays total tasks", () => { - cy.visit(pathTasks); - cy.dataCy("total-count").first().contains("49"); - }); - - describe("Changing page number", () => { - // Instead of checking the entire table rows lets just check if the elements on the table have changed - it("Displays the next page of results and updates URL when right arrow is clicked and next page exists", () => { - cy.visit(`${pathTasks}?page=0`); - cy.dataCy(dataCyTableRows).should("be.visible"); - - const firstPageRows = tableRowToText(dataCyTableRows); - cy.dataCy(dataCyNextPage).click(); - cy.dataCy(dataCyTableRows).should("be.visible"); - - const secondPageRows = tableRowToText(dataCyTableRows); - - expect(firstPageRows).to.not.eq(secondPageRows); - }); - - it("Displays the previous page of results and updates URL when the left arrow is clicked and previous page exists", () => { - cy.visit(`${pathTasks}?page=1`); - cy.dataCy(dataCyTableRows).should("be.visible"); - const secondPageRows = tableRowToText(dataCyTableRows); - cy.dataCy(dataCyPrevPage).click(); - cy.dataCy(dataCyTableRows).should("be.visible"); - const firstPageRows = tableRowToText(dataCyTableRows); - expect(firstPageRows).to.not.eq(secondPageRows); - }); - - it("Does not update results or URL when left arrow is clicked and previous page does not exist", () => { - cy.visit(`${pathTasks}?page=0`); - cy.dataCy(dataCyTableRows).should("be.visible"); - cy.dataCy(dataCyPrevPage).should("have.attr", "aria-disabled", "true"); - }); - - it("Does not update results or URL when right arrow is clicked and next page does not exist", () => { - cy.visit(`${pathTasks}?page=4`); - cy.dataCy(dataCyTableRows).should("be.visible"); - cy.dataCy(dataCyNextPage).should("have.attr", "aria-disabled", "true"); - }); - }); - - describe("Changing page limit", () => { - it("Changing page size updates URL and renders less than or equal to that many rows", () => { - [20, 50, 100].forEach((pageSize) => { - it(`when the page size is set to ${pageSize}`, () => { - cy.visit(pathTasks); - cy.dataCy("tasks-table").should("exist"); - cy.dataCy(dataCyTableRows).should("be.visible"); - clickOnPageSizeBtnAndAssertURLandTableSize( - pageSize, - dataCyTableDataRows, - ); - }); - }); - }); - }); - - describe("blocked tasks", () => { - beforeEach(() => { - cy.visit(`${pathTasks}?limit=100`); - waitForTaskTable(); - }); - - it("shows the blocking tasks when hovering over status badge", () => { - cy.dataCy("depends-on-tooltip").should("not.exist"); - cy.dataCy("task-status-badge").contains("Blocked").trigger("mouseover"); - cy.dataCy("depends-on-tooltip").should("be.visible"); - cy.dataCy("depends-on-tooltip").contains( - "Depends on tasks: “test-migrations”, “test-graphql”", - ); - }); - }); - - describe("task review", () => { - it("marks tasks as viewed and preserves their state on reload", () => { - cy.visit(pathTasks); - cy.dataCy(`reviewed-${firstTaskId}`).check({ force: true }); - cy.dataCy(`reviewed-${firstTaskId}`).should("be.checked"); - - cy.get(`button[aria-label='Expand row']`).click(); - cy.dataCy(`reviewed-${executionTaskId1}`).should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy(`reviewed-${executionTaskId2}`).check({ force: true }); - cy.dataCy(`reviewed-${displayTaskId}`).should("be.checked"); - cy.dataCy(`reviewed-${displayTaskId}`).uncheck({ force: true }); - cy.dataCy(`reviewed-${displayTaskId}`).should("not.be.checked"); - cy.dataCy(`reviewed-${executionTaskId2}`).should("not.be.checked"); - cy.dataCy(`reviewed-${displayTaskId}`).check({ force: true }); - cy.dataCy(`reviewed-${displayTaskId}`).should("be.checked"); - cy.dataCy(`reviewed-${executionTaskId2}`).should("be.checked"); - - cy.reload(); - - cy.dataCy(`reviewed-${firstTaskId}`).should("be.checked"); - cy.dataCy(`reviewed-${displayTaskId}`).should("be.checked"); - cy.get(`button[aria-label='Expand row']`).click(); - cy.dataCy(`reviewed-${executionTaskId2}`).should("be.checked"); - }); - - describe("announcement tooltip", () => { - const resetDate = (d: Date) => { - cy.clock().then((clock) => { - // Need to restore to set a new system date - clock.restore(); - }); - cy.clock(d, ["Date"]); - // Reload to apply clock changes - cy.reload(); - }; - - beforeEach(() => { - cy.clearCookie(SEEN_TASK_REVIEW_TOOLTIP); - cy.visit(pathTasks); - waitForTaskTable(); - }); - - it("shows the announcement tooltip open on the first viewing", () => { - cy.contains("New feature: Task Review").should("be.visible"); - }); - - it("shows the info icon one day after the initial close and hides it after one week", () => { - const now = new Date(2025, 1, 1); // month is 0-indexed - resetDate(now); - cy.contains("New feature: Task Review").should("be.visible"); - cy.contains("button", "Got it").click(); - - const tomorrow = new Date(2025, 1, 2); - resetDate(tomorrow); - cy.contains("Reviewed").should("be.visible"); - cy.contains("New feature: Task Review").should("not.exist"); - cy.dataCy("announcement-tooltip-trigger").click({ force: true }); - - const nextWeek = new Date(2025, 1, 9); - resetDate(nextWeek); - cy.contains("Reviewed").should("be.visible"); - cy.contains("New feature: Task Review").should("not.exist"); - cy.dataCy("announcement-tooltip-trigger").should("not.exist"); - }); - }); - }); -}); - -const firstTaskId = - "evergreen_ubuntu1604_test_service_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48"; -const displayTaskId = "evergreen_ubuntu1604_89"; -const executionTaskId1 = "exec1"; -const executionTaskId2 = "exec2"; - -const dataCyTableDataRows = "[data-cy=tasks-table-row]"; -const dataCyTableRows = "tasks-table-row"; - -const dataCyNextPage = "next-page-button"; -const dataCyPrevPage = "prev-page-button"; - -const tableRowToText = (selector: string) => - new Cypress.Promise((resolve) => { - cy.dataCy(selector) - .invoke("text") - .then((txt) => resolve(txt.toString())); - }); diff --git a/apps/spruce/cypress/integration/version/test_analysis.ts b/apps/spruce/cypress/integration/version/test_analysis.ts deleted file mode 100644 index b87245f541..0000000000 --- a/apps/spruce/cypress/integration/version/test_analysis.ts +++ /dev/null @@ -1,53 +0,0 @@ -describe("Test Analysis", () => { - beforeEach(() => { - cy.visit("/version/5e4ff3abe3c3317e352062e4/test-analysis"); - }); - - it("should group together all matching failing tests in a version and present a stat", () => { - cy.contains("1 test failed across more than one task").should("be.visible"); - cy.contains("JustAFakeTestInALonelyWorld").should("be.visible"); - cy.contains("JustAnotherFakeFailingTestInALonelyWorld").should( - "be.visible", - ); - }); - it("clicking on a test should show the test details", () => { - cy.contains("JustAFakeTestInALonelyWorld").click(); - cy.dataCy("failed-test-grouped-table").should("be.visible"); - }); - it("filtering by test name should only show matching tests", () => { - cy.getInputByLabel("Search Test Failures").type( - "JustAFakeTestInALonelyWorld{enter}", - ); - cy.contains("1 test failed across more than one task").should("be.visible"); - cy.contains("JustAFakeTestInALonelyWorld").should("be.visible"); - cy.contains("JustAnotherFakeFailingTestInALonelyWorld").should("not.exist"); - }); - it("filtering by task status should only show matching tests", () => { - cy.getInputByLabel("Failure Type").click(); - cy.dataCy("task-status-known-issue-option").should("be.visible"); - cy.dataCy("task-status-known-issue-option").click(); - - cy.contains("0 tests failed across more than one task").should( - "be.visible", - ); - cy.contains("JustAFakeTestInALonelyWorld").should("be.visible"); - cy.contains("JustAnotherFakeFailingTestInALonelyWorld").should("not.exist"); - }); - it("clearing the filters should reset the view", () => { - cy.getInputByLabel("Search Test Failures").type( - "JustAFakeTestInALonelyWorld{enter}", - ); - cy.getInputByLabel("Failure Type").click(); - cy.dataCy("task-status-known-issue-option").should("be.visible"); - cy.dataCy("task-status-known-issue-option").click(); - cy.get("body").type("{esc}"); - - cy.contains("0 tests failed across more than one task").should( - "be.visible", - ); - cy.dataCy("clear-filter-button").should("not.be.disabled"); - cy.dataCy("clear-filter-button").click({ force: true }); - cy.getInputByLabel("Search Test Failures").should("have.value", ""); - cy.contains("1 test failed across more than one task").should("be.visible"); - }); -}); diff --git a/apps/spruce/cypress/integration/version/version_timing.ts b/apps/spruce/cypress/integration/version/version_timing.ts deleted file mode 100644 index c9b766f5dc..0000000000 --- a/apps/spruce/cypress/integration/version/version_timing.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { clickOnPageSizeBtnAndAssertURLandTableSize } from "../../utils"; - -describe("Version Timing Tab without a variant selected", () => { - beforeEach(() => { - cy.visit("/version/5e4ff3abe3c3317e352062e4/version-timing"); - }); - it("shows a chart of all variants in the version", () => { - const expectedVariants = ["Ubuntu 16.04", "Lint", "Race Detector"]; - cy.get("svg > g > text").then(($items) => { - const textFound = Array.from($items, (item) => item.innerHTML); - expectedVariants.forEach((variant) => { - expect(textFound).to.include(variant); - }); - }); - }); - it("allows the user to select a variant and navigate to the variant timing view", () => { - cy.get("[id^=reactgooglegraph]").within(() => { - cy.contains("Ubuntu 16.04").click(); - }); - cy.url().should( - "equal", - "http://localhost:3000/version/5e4ff3abe3c3317e352062e4/version-timing?variant=%5Eubuntu1604%24", - ); - }); - - it("has disabled pagination functionality", () => { - cy.get("button[aria-labelledby='page-size-select']").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("next-page-button").should("have.attr", "aria-disabled", "true"); - cy.dataCy("prev-page-button").should("have.attr", "aria-disabled", "true"); - cy.dataCy("clear-all-filters").should("have.attr", "aria-disabled", "true"); - }); -}); - -describe("Version Timing Tab with a variant selected", () => { - beforeEach(() => { - cy.visit( - "/version/5e4ff3abe3c3317e352062e4/version-timing?variant=^ubuntu1604%24", - ); - }); - - const expectedTasks = [ - [ - "test-agent", - "test-cloud", - "test-operations", - "test-scheduler", - "js-test", - "test-units", - "test-command", - "test-model", - "test-model-2", - "test-model-host", - ], - [ - "test-validator", - "test-thirdparty", - "test-service", - "test-rest-model", - "test-rest-client", - "test-graphql", - "test-repotracker", - "test-trigger", - "test-rest-data", - "test-rest-route", - ], - [ - "test-model-grid", - "test-model-user", - "test-model-testresult", - "test-model-manifest", - "test-model-notification", - "test-model-commitqueue", - "test-model-patch", - "test-model-event", - "test-monitor", - "test-model-task", - ], - [ - "test-migrations", - "test-model-alertrecord", - "test-thirdparty-docker", - "test-model-stats", - "test-evergreen", - "test-model-distro", - "test-util", - "test-model-artifact", - "test-model-build", - "test-plugin", - ], - ["test-db", "test-auth"], - ]; - - it("shows a paginated chart of all tasks in the variant", () => { - // Iterate through each page of results and check the expected tasks are present - expectedTasks.forEach((page) => { - cy.get("svg > g > text").then(($items) => { - const textFound = Array.from($items, (item) => item.innerHTML); - page.forEach((task) => { - expect(textFound).to.include(task); - }); - cy.dataCy("next-page-button").click(); - }); - }); - - // Reverse and iterate through each page of results and check the expected tasks are present - expectedTasks.reverse().forEach((page) => { - cy.get("svg > g > text").then(($items) => { - const textFound = Array.from($items, (item) => item.innerHTML); - page.forEach((task) => { - expect(textFound).to.include(task); - }); - cy.dataCy("prev-page-button").click(); - }); - }); - }); - - it("respects the task filter", () => { - cy.visit( - "/version/5e4ff3abe3c3317e352062e4/version-timing?taskName=agent&variant=^ubuntu1604%24", - ); - cy.dataCy("next-page-button").should("have.attr", "aria-disabled", "true"); - cy.get("svg > g > text").then(($items) => { - const textFound = Array.from($items, (item) => item.innerHTML); - expectedTasks.flat().forEach((task) => { - if (task.includes("agent")) { - expect(textFound).to.include(task); - } else { - expect(textFound).to.not.include(task); - } - }); - }); - }); - - it("allows the user to clear all filters", () => { - cy.dataCy("clear-all-filters").click(); - - cy.url().should( - "equal", - "http://localhost:3000/version/5e4ff3abe3c3317e352062e4/version-timing?sorts=DURATION%3ADESC", - ); - - const expectedVariants = ["Ubuntu 16.04", "Lint", "Race Detector"]; - cy.get("svg > g > text").then(($items) => { - const textFound = Array.from($items, (item) => item.innerHTML); - expectedVariants.forEach((variant) => { - expect(textFound).to.include(variant); - }); - }); - }); - - it("allows the user to change the page size", () => { - clickOnPageSizeBtnAndAssertURLandTableSize(50, ""); - cy.get("svg > g > text").then(($items) => { - const textFound = Array.from($items, (item) => item.innerHTML); - expectedTasks.flat().forEach((task) => { - expect(textFound).to.include(task); - }); - }); - }); - - it("allows the user to select a task and navigate to it", () => { - cy.get("[id^=reactgooglegraph]").within(() => { - cy.contains("test-agent").click(); - }); - cy.location("pathname").should( - "equal", - "/task/evergreen_ubuntu1604_test_agent_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48/logs", - ); - }); -}); diff --git a/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts b/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts index f6104b1345..a4aeb7a60d 100644 --- a/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts +++ b/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts @@ -5,18 +5,21 @@ const hostsRoute = "/hosts"; const textFilterTests = [ { + testName: "host ID filter", filterIconDataCy: "host-id-filter", filterValue: "i-0d0ae8b83366d22", filterUrlParam: "hostId=i-0d0ae8b83366d22", expectedIds: ["i-0d0ae8b83366d22"], }, { + testName: "host ID (EC2) filter", filterIconDataCy: "host-id-filter", filterValue: "ec2-34-207-222-84.compute-1.amazonaws.com", filterUrlParam: "hostId=ec2-34-207-222-84.compute-1.amazonaws.com", expectedIds: ["i-06f80fa6e28f93b7d"], }, { + testName: "distro ID filter", filterIconDataCy: "distro-id-filter", filterValue: "macos-1014", filterUrlParam: "distroId=macos-1014", @@ -27,6 +30,7 @@ const textFilterTests = [ ], }, { + testName: "current task ID filter", filterIconDataCy: "current-task-id-filter", filterValue: "mongodb_mongo_v3.6_debian92_sharding_auth_bc405c72dce4714da604810cdc90c132bd5fbaa1_20_07_20_17_39_20", @@ -39,6 +43,7 @@ const textFilterTests = [ ], }, { + testName: "owner filter", filterIconDataCy: "owner-filter", filterValue: "mci", filterUrlParam: "startedBy=mci", @@ -66,8 +71,14 @@ test.describe("Hosts page filtering from table filters", () => { }); textFilterTests.forEach( - ({ expectedIds, filterIconDataCy, filterUrlParam, filterValue }) => { - test(`Filters hosts using table filter for ${filterIconDataCy}`, async ({ + ({ + expectedIds, + filterIconDataCy, + filterUrlParam, + filterValue, + testName, + }) => { + test(`Filters hosts using table filter for ${testName}`, async ({ authenticatedPage: page, }) => { const filterIcon = page.getByTestId(filterIconDataCy); diff --git a/apps/spruce/playwright/tests/spawn/host.spec.ts b/apps/spruce/playwright/tests/spawn/host.spec.ts new file mode 100644 index 0000000000..444e7a8f95 --- /dev/null +++ b/apps/spruce/playwright/tests/spawn/host.spec.ts @@ -0,0 +1,358 @@ +import { test, expect } from "../../fixtures"; +import { clearDatePickerInput, selectDatePickerDate } from "../../helpers"; + +const ascendingSortSpawnHostOrderByHostId = [ + "i-04ade558e1e26b0ad", + "i-07669e7a3cd2c238c", + "i-092593689871a50dc", +]; +const descendingSortSpawnHostOrderByHostId = [ + "i-092593689871a50dc", + "i-07669e7a3cd2c238c", + "i-04ade558e1e26b0ad", +]; +const descendingSortSpawnHostOrderByExpiration = [ + "i-092593689871a50dc", + "i-07669e7a3cd2c238c", + "i-04ade558e1e26b0ad", +]; +const ascendingSortSpawnHostOrderByExpiration = [ + "i-04ade558e1e26b0ad", + "i-07669e7a3cd2c238c", + "i-092593689871a50dc", +]; + +const hostTaskId = + "evergreen_ubuntu1604_dist_patch_33016573166a36bd5f46b4111151899d5c4e95b1_5ecedafb562343215a7ff297_20_05_27_21_39_46"; +const distroId = "windows-64-vs2015-small"; +const projectSetupCheckbox = + "Use project-specific setup script defined at /path"; +const startHostsCheckbox = + "Also start any hosts this task started (if applicable)"; + +test.describe("Spawn Host page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/spawn/host"); + }); + + test("Visiting the spawn host page should display all of your spawned hosts", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("leafygreen-table-row")).toHaveCount(3); + }); + + test("Visiting the spawn host page should not have any cards expanded by default", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("spawn-host-card")).toHaveCount(0); + }); + + test("Clicking on a spawn host row should toggle the host card", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("spawn-host-card")).toHaveCount(0); + + const firstExpandButton = page + .getByRole("button", { name: "Expand row" }) + .first(); + await firstExpandButton.click(); + await expect(page.getByTestId("spawn-host-card")).toBeVisible(); + + const firstCollapseButton = page + .getByRole("button", { name: "Collapse row" }) + .first(); + await firstCollapseButton.click(); + await expect(page.getByTestId("spawn-host-card")).toHaveCount(0); + }); + + test("Visiting the spawn host page with an id in the url should open the page with the row expanded", async ({ + authenticatedPage: page, + }) => { + await page.goto("/spawn/host?host=i-092593689871a50dc"); + await expect(page.getByTestId("spawn-host-card").first()).toBeVisible(); + await expect(page.getByTestId("spawn-host-card")).toHaveCount(1); + }); + + test("Clicking on the Event Log link should redirect to /host/:hostId", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId("leafygreen-table-row") + .filter({ hasText: "i-092593689871a50dc" }) + .getByRole("button", { name: "Expand row" }) + .click(); + await page.getByText("Event Log").click(); + await expect(page).toHaveURL("/host/i-092593689871a50dc"); + }); + + test.describe("Spawn host card sorting", () => { + test("Clicking on the host column header should sort spawn hosts by ascending order, then descending, then remove sort", async ({ + authenticatedPage: page, + }) => { + const hostSortControl = page.getByRole("button", { + name: "Sort by Host", + }); + await hostSortControl.click(); + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < ascendingSortSpawnHostOrderByHostId.length; i++) { + await expect(rows.nth(i)).toContainText( + ascendingSortSpawnHostOrderByHostId[i], + ); + } + await hostSortControl.click(); + for (let i = 0; i < descendingSortSpawnHostOrderByHostId.length; i++) { + await expect(rows.nth(i)).toContainText( + descendingSortSpawnHostOrderByHostId[i], + ); + } + await hostSortControl.click(); + await expect(rows).toHaveCount(3); + }); + + test("Clicking on the expiration column header should sort the hosts by ascending order, then descending, then remove sort", async ({ + authenticatedPage: page, + }) => { + const expiresInSortControl = page.getByRole("button", { + name: "Sort by Expires In", + }); + await expiresInSortControl.click(); + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < ascendingSortSpawnHostOrderByExpiration.length; i++) { + await expect(rows.nth(i)).toContainText( + ascendingSortSpawnHostOrderByExpiration[i], + ); + } + await expiresInSortControl.click(); + for ( + let i = 0; + i < descendingSortSpawnHostOrderByExpiration.length; + i++ + ) { + await expect(rows.nth(i)).toContainText( + descendingSortSpawnHostOrderByExpiration[i], + ); + } + await expiresInSortControl.click(); + await expect(rows).toHaveCount(3); + }); + }); + + test.describe("Spawn host modal", () => { + test("Should disable 'Unexpirable Host' radio box when max number of unexpirable hosts is met (2)", async ({ + authenticatedPage: page, + }) => { + await page.getByText("Spawn a host").click(); + await page.getByTestId("distro-input").click(); + await page.getByTestId("distro-option-ubuntu1804-workstation").click(); + + const expirableHostButton = page.getByRole("radio", { + name: "Expirable Host", + exact: true, + }); + await expect(expirableHostButton).toBeEnabled(); + + const unexpirableHostButton = page.getByRole("radio", { + name: "Unexpirable Host", + exact: true, + }); + await expect(unexpirableHostButton).toBeDisabled(); + + await expect( + page.getByText("You have reached the max number of unexpirable hosts"), + ).toBeVisible(); + }); + + test("Clicking on the spawn host button should open a spawn host modal", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("spawn-host-modal")).toHaveCount(0); + await page.getByTestId("spawn-host-button").click(); + await expect(page.getByTestId("spawn-host-modal")).toBeVisible(); + }); + + test("Visiting the spawn host page with the proper url param should open the spawn host modal by default", async ({ + authenticatedPage: page, + }) => { + await page.goto("/spawn/host?spawnHost=True"); + await expect(page.getByTestId("spawn-host-modal")).toBeVisible(); + }); + + test("Closing the spawn host modal removes the 'spawnHost' query param from the url and hides the modal", async ({ + authenticatedPage: page, + }) => { + await page.goto("/spawn/host?spawnHost=True"); + await expect(page.getByTestId("spawn-host-modal")).toBeVisible(); + await expect(page).toHaveURL("/spawn/host?spawnHost=True"); + await page + .getByTestId("spawn-host-modal") + .getByRole("button", { name: "Cancel" }) + .click(); + await expect(page).not.toHaveURL("/spawn/host?spawnHost=True"); + await expect(page.getByTestId("spawn-host-modal")).toHaveCount(0); + }); + + test("Visiting the spawn host page with a taskId url param should render additional options at the bottom of the modal", async ({ + authenticatedPage: page, + }) => { + await page.goto( + `/spawn/host?spawnHost=True&distroId=rhel71-power8-large&taskId=${hostTaskId}`, + ); + await expect(page.getByTestId("spawn-host-modal")).toContainText( + projectSetupCheckbox, + ); + await expect(page.getByTestId("spawn-host-modal")).toContainText( + "Load data for dist on ubuntu1604", + ); + await expect(page.getByTestId("spawn-host-modal")).toContainText( + startHostsCheckbox, + ); + }); + + test("Unchecking 'Load data for dist' hides nested checkbox selections and checking shows them", async ({ + authenticatedPage: page, + }) => { + await page.goto( + `/spawn/host?spawnHost=True&distroId=rhel71-power8-large&taskId=${hostTaskId}`, + ); + await expect(page.getByTestId("spawn-host-modal")).toBeVisible(); + await expect(page.getByTestId("load-data-checkbox")).toBeChecked(); + await expect(page.getByText(projectSetupCheckbox)).toBeVisible(); + await expect(page.getByText(startHostsCheckbox)).toBeVisible(); + + const loadDataCheckbox = page.getByTestId("load-data-checkbox"); + const loadDataId = await loadDataCheckbox.getAttribute("id"); + await page.locator(`label[for="${loadDataId}"]`).click(); + await expect(loadDataCheckbox).not.toBeChecked(); + await expect(page.getByText(projectSetupCheckbox)).toHaveCount(0); + await expect(page.getByText(startHostsCheckbox)).toHaveCount(0); + }); + + test("Visiting the spawn host page with a task and distro supplied in the url should populate the distro input", async ({ + authenticatedPage: page, + }) => { + await page.goto( + `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, + ); + await expect(page.getByTestId("spawn-host-modal")).toBeVisible(); + await expect( + page.getByTestId("distro-input").getByTestId("dropdown-value"), + ).toContainText(distroId); + }); + + test("The virtual workstation dropdown should filter any volumes that aren't a home volume", async ({ + authenticatedPage: page, + }) => { + await page.goto( + `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, + ); + await page.getByTestId("distro-input").click(); + await expect(page.getByText("Admin-only distros")).toHaveCount(0); + await page.getByTestId("distro-option-ubuntu1804-workstation").click(); + await expect(page.getByTestId("volume-select")).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + + test("Clicking 'Add new key' hides the key name dropdown and shows the key value text area", async ({ + authenticatedPage: page, + }) => { + await page.goto( + `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, + ); + await expect(page.getByTestId("key-select")).toBeVisible(); + await expect(page.getByTestId("key-value-text-area")).toHaveCount(0); + await page.getByText("Add new key").click(); + await expect(page.getByTestId("key-select")).toHaveCount(0); + await expect(page.getByTestId("key-value-text-area")).toBeVisible(); + }); + + test("Checking 'Run Userdata script on start' shows the user data script text area", async ({ + authenticatedPage: page, + }) => { + await page.goto( + `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, + ); + await expect( + page.getByTestId("run-user-data-script-text-area"), + ).toHaveCount(0); + await page.getByText("Run Userdata script on start").click(); + await expect( + page.getByTestId("user-data-script-text-area"), + ).toBeVisible(); + }); + + test("Checking 'Define setup script...' shows the setup script text area", async ({ + authenticatedPage: page, + }) => { + await page.goto( + `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, + ); + await expect(page.getByTestId("setup-script-text-area")).toBeHidden(); + await page.getByText("Use project-specific setup script").click(); + await expect(page.getByTestId("setup-script-checkbox")).toHaveAttribute( + "aria-disabled", + "false", + ); + await page.getByText("Define setup script to run after host").click(); + await expect(page.getByTestId("setup-script-text-area")).toBeVisible(); + }); + + test("Conditionally disables setup script and project setup script checkboxes based on the other's value", async ({ + authenticatedPage: page, + }) => { + await page.goto( + `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, + ); + const projectCheckbox = page.getByTestId("project-setup-script-checkbox"); + const setupCheckbox = page.getByTestId("setup-script-checkbox"); + + await expect(projectCheckbox).toHaveAttribute("aria-checked", "true"); + await expect(projectCheckbox).toHaveAttribute("aria-disabled", "false"); + await expect(setupCheckbox).toHaveAttribute("aria-disabled", "true"); + + const projectId = await projectCheckbox.getAttribute("id"); + await page.locator(`label[for="${projectId}"]`).click(); + await expect(projectCheckbox).toHaveAttribute("aria-disabled", "false"); + await expect(setupCheckbox).toHaveAttribute("aria-disabled", "false"); + + const setupId = await setupCheckbox.getAttribute("id"); + await page.locator(`label[for="${setupId}"]`).click(); + await expect(projectCheckbox).toHaveAttribute("aria-disabled", "true"); + await expect(setupCheckbox).toHaveAttribute("aria-disabled", "false"); + }); + }); + + test("Allows editing a modal with sleep schedule enabled and validates dates", async ({ + authenticatedPage: page, + }) => { + await page.clock.setFixedTime("2026-05-26T00:00:00Z"); + await page.getByTestId("edit-host-button").nth(2).click(); + + const modal = page.getByTestId("edit-spawn-host-modal").nth(2); + await expect(modal).toBeVisible(); + + const yearInput = page.locator("input[id='year']"); + const monthInput = page.locator("input[id='month']"); + const dayInput = page.locator("input[id='day']"); + const saveButton = page.getByRole("button", { name: "Save" }); + + // Set a valid near-future date + await selectDatePickerDate(page, "2026", "Jun", "2026-06-01"); + await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + + // Set a date in the past + await clearDatePickerInput(page); + await yearInput.fill("2025"); + await monthInput.fill("01"); + await dayInput.fill("01"); + await expect(saveButton).toHaveAttribute("aria-disabled", "true"); + + // Set a date too far in the future + await clearDatePickerInput(page); + await yearInput.fill("2060"); + await monthInput.fill("01"); + await dayInput.fill("15"); + await expect(saveButton).toHaveAttribute("aria-disabled", "true"); + }); +}); diff --git a/apps/spruce/playwright/tests/spawn/route.spec.ts b/apps/spruce/playwright/tests/spawn/route.spec.ts new file mode 100644 index 0000000000..ae3d3f6ba6 --- /dev/null +++ b/apps/spruce/playwright/tests/spawn/route.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "../../fixtures"; + +test.describe("Navigating to Spawn Host and Spawn Volume pages", () => { + test("Navigating to /spawn will redirect to /spawn/host", async ({ + authenticatedPage: page, + }) => { + await page.goto("/spawn"); + await expect(page).toHaveURL("/spawn/host"); + }); + + test("Navigating to /spawn/not-a-route will redirect to /spawn/host", async ({ + authenticatedPage: page, + }) => { + await page.goto("/spawn/not-a-route"); + await expect(page).toHaveURL("/spawn/host"); + }); + + test("Clicking on the Volume side nav item will redirect to /spawn/volume", async ({ + authenticatedPage: page, + }) => { + await page.goto("/spawn/host"); + await page.getByTestId("volume-nav-tab").click(); + await expect(page).toHaveURL("/spawn/volume"); + }); + + test("Clicking on the Host side nav item will redirect to /spawn/host", async ({ + authenticatedPage: page, + }) => { + await page.goto("/spawn/volume"); + await page.getByTestId("host-nav-tab").click(); + await expect(page).toHaveURL("/spawn/host"); + }); +}); diff --git a/apps/spruce/playwright/tests/spawn/volume.spec.ts b/apps/spruce/playwright/tests/spawn/volume.spec.ts new file mode 100644 index 0000000000..5924b4cf0b --- /dev/null +++ b/apps/spruce/playwright/tests/spawn/volume.spec.ts @@ -0,0 +1,406 @@ +import { test, expect } from "../../fixtures"; +import { + clickCheckboxByLabel, + mockGraphQLResponse, + selectOption, + validateDatePickerDate, + validateToast, +} from "../../helpers"; + +const expectedVolNames = [ + "vol-0ae8720b445b771b6", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b856", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b857", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b859", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b815", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b825", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b835", + "1da0e996608e6871b60a92f6564bbc9cdf66ce90be1178dfb653920542a0d0f0", + "vol-0c66e16459646704d", + "vol-0583d66433a69f136", + "vol-0ea662ac92f611ed4", +]; + +test.describe("Spawn volume page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/spawn/volume"); + }); + + test("Visiting the spawn volume page should display the number of free and mounted volumes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("mounted-badge")).toContainText("9 Mounted"); + await expect(page.getByTestId("free-badge")).toContainText("4 Free"); + }); + + test("The table initially displays volumes with status ascending", async ({ + authenticatedPage: page, + }) => { + const rows = page.getByTestId("leafygreen-table-row"); + for (let i = 0; i < expectedVolNames.length; i++) { + await expect(rows.nth(i)).toContainText(expectedVolNames[i]); + } + }); + + test("Table should have no cards visible by default", async ({ + authenticatedPage: page, + }) => { + for (const name of expectedVolNames) { + await expect(page.getByTestId(name)).toHaveCount(0); + } + }); + + test("Should render migrating volumes with a different badge and disable action buttons", async ({ + authenticatedPage: page, + }) => { + const targetVolume = "vol-0ae8720b445b771b6"; + const migratingRow = page + .getByTestId("leafygreen-table-row") + .filter({ hasText: targetVolume }); + await expect(migratingRow.getByTestId("volume-status-badge")).toContainText( + "Migrating", + ); + const trashButton = page.getByTestId(`trash-vol-${targetVolume}`); + await expect(trashButton).toBeDisabled(); + const migrateButton = page.getByTestId(`migrate-btn-vol-${targetVolume}`); + await expect(migrateButton).toBeDisabled(); + const editButton = page.getByTestId(`edit-btn-vol-${targetVolume}`); + await expect(editButton).toBeDisabled(); + }); + + test("Should have a volume card visible initially when the 'volume' query param is provided", async ({ + authenticatedPage: page, + }) => { + const targetVolume = "vol-0ea662ac92f611ed4"; + await page.goto(`/spawn/volume?volume=${targetVolume}`); + const card = page.getByTestId(`spawn-volume-card-${targetVolume}`); + await card.scrollIntoViewIfNeeded(); + await expect(card).toBeVisible(); + }); + + test("Clicking on the row should toggle the volume card open and closed", async ({ + authenticatedPage: page, + }) => { + const targetVolume = "vol-0ea662ac92f611ed4"; + await page.goto(`/spawn/volume?volume=${targetVolume}`); + const card = page.getByTestId(`spawn-volume-card-${targetVolume}`); + await expect(card).toBeVisible(); + + const row = page + .getByTestId("leafygreen-table-row") + .filter({ hasText: targetVolume }); + await row.getByRole("button", { name: "Collapse row" }).click(); + await expect(card).toHaveCount(0); + + await row.getByRole("button", { name: "Expand row" }).click(); + await expect(card).toBeVisible(); + }); + + test("Clicking the trash can should remove the volume from the table and update free/mounted volumes badges", async ({ + authenticatedPage: page, + }) => { + await mockGraphQLResponse(page, "RemoveVolume", { + data: { removeVolume: true }, + errors: null, + }); + const targetVolume = "vol-0c66e16459646704d"; + await expect( + page + .getByTestId("leafygreen-table-row") + .filter({ hasText: targetVolume }), + ).toBeVisible(); + + await page.getByTestId(`trash-vol-${targetVolume}`).click(); + const popconfirm = page.getByTestId("delete-volume-popconfirm"); + await expect(popconfirm).toBeVisible(); + const yesButton = popconfirm.getByRole("button", { name: "Yes" }); + await expect(yesButton).toBeVisible(); + await expect(yesButton).toBeEnabled(); + await yesButton.click(); + await validateToast(page, "success", "Successfully deleted the volume."); + }); + + test("Clicking the trash can for a mounted volume should show an additional confirmation checkbox which enables the submit button when checked", async ({ + authenticatedPage: page, + }) => { + await mockGraphQLResponse(page, "RemoveVolume", { + data: { removeVolume: true }, + errors: null, + }); + await page + .getByTestId( + "trash-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) + .click(); + const popconfirm = page.getByTestId("delete-volume-popconfirm"); + await expect(popconfirm).toBeVisible(); + + const confirmCheckbox = popconfirm.getByRole("checkbox", { + name: "I understand this volume is currently mounted to a host.", + }); + await expect(confirmCheckbox).not.toBeChecked(); + + const yesButton = popconfirm.getByRole("button", { name: "Yes" }); + await expect(yesButton).toHaveAttribute("aria-disabled", "true"); + + const checkboxId = await confirmCheckbox.getAttribute("id"); + await page.locator(`label[for="${checkboxId}"]`).click(); + + await expect(yesButton).not.toHaveAttribute("aria-disabled", "true"); + await yesButton.click(); + + await validateToast(page, "success", "Successfully deleted the volume."); + }); + + test("Clicking on unmount should result in a success toast appearing", async ({ + authenticatedPage: page, + }) => { + await mockGraphQLResponse(page, "DetachVolumeFromHost", { + data: { detachVolumeFromHost: true }, + errors: null, + }); + await page + .getByTestId( + "detach-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b857", + ) + .click(); + const popconfirm = page.getByTestId("unmount-volume-popconfirm"); + await expect(popconfirm).toBeVisible(); + await popconfirm.getByRole("button", { name: "Yes" }).click(); + await validateToast(page, "success", "Successfully unmounted the volume."); + }); + + test("Clicking on 'Spawn Volume' should open the Spawn Volume Modal", async ({ + authenticatedPage: page, + }) => { + const spawnButton = page.getByTestId("spawn-volume-btn"); + await expect(spawnButton).toBeEnabled(); + await spawnButton.click(); + await expect(page.getByTestId("spawn-volume-modal")).toBeVisible(); + }); + + test("Reopening the Spawn Volume modal clears previous input changes", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("spawn-volume-btn").click(); + await expect(page.getByTestId("spawn-volume-modal")).toBeVisible(); + await selectOption(page, "Type", "sc1"); + + const modal = page.getByTestId("spawn-volume-modal"); + const cancelButton = modal.getByRole("button", { name: "Cancel" }); + await expect(cancelButton).toBeEnabled(); + await cancelButton.click(); + + await page.getByTestId("spawn-volume-btn").click(); + await expect(page.getByTestId("spawn-volume-modal")).toBeVisible(); + await expect(page.getByTestId("type-select")).toContainText("gp3"); + }); + + test.describe("Edit volume modal", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/spawn/volume"); + }); + + test("Clicking on 'Edit' should open the Edit Volume Modal", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId( + "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await expect(page.getByTestId("update-volume-modal")).toBeVisible(); + }); + + test("name, size, expiration inputs should be populated on initial render", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId( + "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await expect(page.getByTestId("update-volume-modal")).toBeVisible(); + await expect(page.getByTestId("volume-name-input")).toHaveValue( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ); + await expect(page.getByTestId("volume-size-input")).toHaveValue("100"); + await validateDatePickerDate(page, "date-picker", { + year: "2020", + month: "06", + day: "06", + }); + await expect(page.getByTestId("hour-input")).toHaveValue("15"); + await expect(page.getByTestId("minute-input")).toHaveValue("48"); + }); + + test("Reopening the edit volume modal should reset form input fields", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId( + "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await expect(page.getByTestId("update-volume-modal")).toBeVisible(); + await page.getByTestId("volume-name-input").type("Hello, World"); + + await page + .getByTestId("update-volume-modal") + .getByRole("button", { name: "Cancel" }) + .click(); + + await page + .getByTestId( + "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await expect(page.getByTestId("volume-name-input")).toHaveValue( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ); + }); + + test("size field is validated correctly", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId( + "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await expect(page.getByTestId("update-volume-modal")).toBeVisible(); + + await page.getByTestId("volume-size-input").clear(); + await page.getByTestId("volume-size-input").fill("10000"); + await expect(page.getByRole("button", { name: "Save" })).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByTestId("volume-size-input").clear(); + await page.getByTestId("volume-size-input").fill("2"); + await expect(page.getByRole("button", { name: "Save" })).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + + test("Submit button should be enabled when the volume details input value differs from what already exists", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId( + "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await expect(page.getByTestId("update-volume-modal")).toBeVisible(); + + const saveButton = page.getByRole("button", { name: "Save" }); + const volumeInput = page.getByTestId("volume-name-input"); + + await expect(saveButton).toHaveAttribute("aria-disabled", "true"); + await volumeInput.fill("Hello, World"); + await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + await volumeInput.clear(); + await volumeInput.fill( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ); + await expect(saveButton).toHaveAttribute("aria-disabled", "true"); + await clickCheckboxByLabel(page, "Never expire"); + await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + }); + + test("Clicking on save button should close the modal and show a success toast", async ({ + authenticatedPage: page, + }) => { + mockGraphQLResponse(page, "UpdateVolume", { + data: { + updateVolume: { + id: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + name: "Hello, World", + }, + }, + errors: null, + }); + await page + .getByTestId( + "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await expect(page.getByTestId("update-volume-modal")).toBeVisible(); + await page.getByTestId("volume-name-input").fill("Hello, World"); + const saveButton = page.getByRole("button", { name: "Save" }); + await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + await saveButton.click(); + await validateToast(page, "success", "Successfully updated volume"); + await expect(page.getByTestId("update-volume-modal")).toHaveCount(0); + }); + }); + + test.describe("Migrate Modal", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/spawn/volume"); + }); + + test("migrate button is disabled for volumes with the migrating status", async ({ + authenticatedPage: page, + }) => { + const migratingRow = page + .getByTestId("leafygreen-table-row") + .filter({ hasText: "vol-0ae8720b445b771b6" }); + await expect( + migratingRow.getByTestId("volume-status-badge"), + ).toContainText("Migrating"); + await expect( + page.getByTestId("migrate-btn-vol-0ae8720b445b771b6"), + ).toHaveAttribute("aria-disabled", "true"); + }); + + test("clicking cancel during confirmation renders the Migrate modal form", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId( + "migrate-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await page.getByTestId("distro-input").click(); + await page.getByTestId("distro-option-ubuntu1804-workstation").click(); + await page.getByTestId("migrate-modal").getByText("Next").click(); + await expect(page.getByTestId("migrate-modal")).toContainText( + "Are you sure you want to migrate this home volume?", + ); + await expect(page.getByTestId("distro-input")).toHaveCount(0); + await page.getByTestId("migrate-modal").getByText("Cancel").click(); + await expect(page.getByTestId("distro-input")).toBeVisible(); + }); + + test("open the Migrate modal and spawn a host", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId( + "migrate-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + ) + .click(); + await page.getByTestId("distro-input").click(); + await page.getByTestId("distro-option-ubuntu1804-workstation").click(); + await expect(page.getByTestId("region-select")).toHaveAttribute( + "aria-disabled", + "true", + ); + await page.getByTestId("migrate-modal").getByText("Next").click(); + await page + .getByTestId("migrate-modal") + .getByText("Migrate Volume") + .click(); + await validateToast( + page, + "success", + "Volume migration has been scheduled. A new host will be spawned and accessible on your Hosts page.", + ); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/version/action_buttons.spec.ts b/apps/spruce/playwright/tests/version/action_buttons.spec.ts new file mode 100644 index 0000000000..1e54187007 --- /dev/null +++ b/apps/spruce/playwright/tests/version/action_buttons.spec.ts @@ -0,0 +1,174 @@ +import { test, expect } from "../../fixtures"; +import { validateToast, mockGraphQLResponse } from "../../helpers"; + +const patch = "5ecedafb562343215a7ff297"; +const mainlineCommit = "5e4ff3abe3c3317e352062e4"; +const versionPath = (id: string) => `/version/${id}`; + +test.describe("Action Buttons", () => { + test.describe("When viewing a patch build", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(versionPath(patch)); + }); + + test("Clicking 'Schedule' button shows modal and clicking on 'Cancel' closes it", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("schedule-patch").click(); + await expect(page.getByTestId("schedule-tasks-modal")).toBeVisible(); + await page.getByRole("button", { name: "Cancel" }).click(); + await expect(page.getByTestId("schedule-tasks-modal")).toBeHidden(); + }); + + test("Clicking ellipses dropdown shows ellipses options", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("ellipses-btn")).toHaveCount(0); + await page.getByTestId("ellipsis-btn").click(); + await expect(page.getByTestId("card-dropdown")).toBeVisible(); + + await page.getByTestId("ellipsis-btn").click(); + await expect(page.getByTestId("card-dropdown")).toHaveCount(0); + }); + }); + + test.describe("Version dropdown options", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(versionPath(patch)); + await page.getByTestId("ellipsis-btn").click(); + await expect(page.getByTestId("card-dropdown")).toBeVisible(); + }); + + test("Error unscheduling a version shows error toast", async ({ + authenticatedPage: page, + }) => { + await mockGraphQLResponse(page, "UnscheduleVersionTasks", { + data: null, + errors: [ + { + message: "There was an error unscheduling tasks", + path: ["UnscheduleVersionTasks"], + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }, + ], + }); + await page.getByTestId("unschedule-patch").click(); + await page.getByRole("button", { name: "Yes" }).click(); + await validateToast( + page, + "error", + "There was an error unscheduling tasks", + ); + }); + + test("Clicking 'Unschedule' button show popconfirm with abort checkbox and a toast on success", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("unschedule-patch").click(); + await page.getByRole("button", { name: "Yes" }).click(); + await validateToast( + page, + "success", + "All tasks were unscheduled and tasks that already started were aborted", + ); + }); + + test("Clicking 'Set Priority' button shows popconfirm with input and toast on success", async ({ + authenticatedPage: page, + }) => { + const priority = "99"; + await page.getByTestId("set-priority-menu-item").click(); + await page.getByTestId("patch-priority-input").fill(priority); + await page.getByTestId("patch-priority-input").press("Enter"); + await validateToast(page, "success", priority); + }); + + test("Error setting priority shows error toast", async ({ + authenticatedPage: page, + }) => { + await mockGraphQLResponse(page, "SetVersionPriority", { + data: null, + errors: [ + { + message: "There was an error setting priority", + path: ["SetVersionPriority"], + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }, + ], + }); + await page.getByTestId("set-priority-menu-item").click(); + await page.getByTestId("patch-priority-input").fill("80"); + await page.getByTestId("patch-priority-input").press("Enter"); + await validateToast(page, "error", "Error updating priority for patch"); + }); + + test("Sets priority for multiple tasks when version page table is filtered", async ({ + authenticatedPage: page, + }) => { + const priority = 10; + await page.goto( + `${versionPath(mainlineCommit)}/tasks?statuses=failed-umbrella,failed,known-issue`, + ); + await page.getByTestId("ellipsis-btn").click(); + await expect(page.getByTestId("card-dropdown")).toBeVisible(); + await expect(page.getByTestId("set-priority-menu-item")).toContainText( + "Set task priority (2)", + ); + await page.getByTestId("set-priority-menu-item").click(); + await page.getByTestId("task-priority-input").fill(`${priority}`); + await page.getByTestId("task-priority-input").press("Enter"); + await validateToast(page, "success", "Priority updated for 2 tasks."); + }); + + test("Should be able to reconfigure the patch", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("reconfigure-link")).not.toHaveAttribute( + "disabled", + ); + await page.getByTestId("reconfigure-link").click(); + await expect(page).toHaveURL(/configure/); + }); + }); + + test.describe("When viewing a mainline commit", () => { + test.describe("Version dropdown options", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(versionPath(mainlineCommit)); + await page.getByTestId("ellipsis-btn").click(); + await expect(page.getByTestId("card-dropdown")).toBeVisible(); + }); + + test("Reconfigure link is disabled for mainline commits", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("reconfigure-link")).toBeVisible(); + await expect(page.getByTestId("reconfigure-link")).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + }); + }); + + test.describe("Include Never-activated Tasks toggle", () => { + test("sets URL and cookie when toggled on", async ({ + authenticatedPage: page, + }) => { + await page.goto(versionPath(patch)); + await page.getByTestId("ellipsis-btn").click(); + await expect(page.getByTestId("card-dropdown")).toBeVisible(); + await page + .getByTestId("card-dropdown") + .getByText("Include never-activated tasks") + .click(); + await expect(page).toHaveURL(/includeNeverActivatedTasks=true/); + + const cookies = await page.context().cookies(); + const cookie = cookies.find( + (c) => c.name === "include-never-activated-tasks", + ) ?? { value: "false" }; + expect(cookie.value).toBe("true"); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/version/banners.spec.ts b/apps/spruce/playwright/tests/version/banners.spec.ts new file mode 100644 index 0000000000..464a963f86 --- /dev/null +++ b/apps/spruce/playwright/tests/version/banners.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from "../../fixtures"; + +const versionWithBanners = + "/version/logkeeper_e864cf934194c161aa044e4599c8c81cee7b6237/tasks?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC"; + +test.describe("banners", () => { + test.describe("errors", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(versionWithBanners); + }); + + test("should display the number of configuration errors", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("configuration-errors-banner"), + ).toBeVisible(); + await expect( + page.getByText("4 errors in configuration file"), + ).toBeVisible(); + }); + + test("should be able to open the modal and see all errors", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("configuration-errors-modal-trigger").click(); + await expect( + page.getByTestId("configuration-errors-modal"), + ).toBeVisible(); + await expect(page.locator("li")).toHaveCount(4); + }); + }); + + test.describe("warnings", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(versionWithBanners); + }); + + test("should display the number of configuration warnings", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("configuration-warnings-banner"), + ).toBeVisible(); + await expect( + page.getByText("3 warnings in configuration file"), + ).toBeVisible(); + }); + + test("should be able to open the modal and see all warnings", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("configuration-warnings-modal-trigger").click(); + await expect( + page.getByTestId("configuration-warnings-modal"), + ).toBeVisible(); + await expect(page.locator("li")).toHaveCount(3); + }); + }); + + test.describe("ignored", () => { + test("should display a banner", async ({ authenticatedPage: page }) => { + await page.goto( + "/version/spruce_e695f654c8b4b959d3e12e71696c3e318bcd4c33", + ); + await expect(page.getByTestId("ignored-banner")).toBeVisible(); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/version/downstream_projects.spec.ts b/apps/spruce/playwright/tests/version/downstream_projects.spec.ts new file mode 100644 index 0000000000..38a075a24f --- /dev/null +++ b/apps/spruce/playwright/tests/version/downstream_projects.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from "../../fixtures"; + +const DOWNSTREAM_ROUTE = + "/version/5f74d99ab2373627c047c5e5/downstream-projects"; + +test.describe("Downstream Projects Tab", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(DOWNSTREAM_ROUTE); + }); + + test("shows number of failed patches in the Downstream tab label", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("downstream-tab-badge")).toBeVisible(); + await expect(page.getByTestId("downstream-tab-badge")).toContainText("1"); + }); + + test("renders child patches", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("project-accordion")).toHaveCount(3); + await expect(page.getByTestId("project-title")).toHaveCount(3); + await expect(page.getByTestId("downstream-tasks-table")).toHaveCount(3); + }); + + test("links to base commit", async ({ authenticatedPage: page }) => { + await page.getByTestId("accordion-toggle").first().click(); + await expect( + page.getByTestId("downstream-base-commit").first(), + ).toHaveAttribute( + "href", + /\/version\/logkeeper_e3579537e848d14f0c3e5c25ef745fd0f10702d4/, + ); + }); + + test("filters by test name", async ({ authenticatedPage: page }) => { + await page.getByTestId("task-name-filter").nth(1).click(); + const input = page.getByTestId("task-name-filter-wrapper").locator("input"); + await input.focus(); + await input.fill("generate-lint"); + await input.press("Enter"); + await expect(page).not.toHaveURL(/generate-lint/); + await expect(page.getByText("generate-lint")).toBeVisible(); + }); + + test("does not push query params to the URL", async ({ + authenticatedPage: page, + }) => { + expect(new URL(page.url()).pathname).toBe(DOWNSTREAM_ROUTE); + }); +}); diff --git a/apps/spruce/playwright/tests/version/name_change_modal.spec.ts b/apps/spruce/playwright/tests/version/name_change_modal.spec.ts new file mode 100644 index 0000000000..9b8637411a --- /dev/null +++ b/apps/spruce/playwright/tests/version/name_change_modal.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; + +test.describe("Name change modal", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/version/5f74d99ab2373627c047c5e5"); + }); + + test("Use the name change modal to change the name of a patch", async ({ + authenticatedPage: page, + }) => { + const originalName = "main: EVG-7823 add a commit queue message (#4048)"; + await expect(page.getByText(originalName)).toBeVisible(); + + await page.getByTestId("name-change-modal-trigger").click(); + const newName = "a different name"; + await page.locator("textarea").clear(); + await page.locator("textarea").fill(newName); + await page.getByRole("button", { name: "Confirm" }).click(); + await expect(page.locator("textarea")).toBeHidden(); + await expect(page.getByText(newName)).toBeVisible(); + await validateToast( + page, + "success", + "Patch name was successfully updated.", + ); + }); + + test("The confirm button is disabled when the text area value is empty or greater than 300 characters", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("name-change-modal-trigger").click(); + await page.locator("textarea").clear(); + await expect(page.getByRole("button", { name: "Confirm" })).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.locator("textarea").fill("lol"); + await expect(page.getByRole("button", { name: "Confirm" })).toHaveAttribute( + "aria-disabled", + "false", + ); + + const over300Chars = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + await page.locator("textarea").fill(over300Chars); + await expect(page.getByRole("button", { name: "Confirm" })).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect( + page.getByText("Value cannot exceed 300 characters"), + ).toBeVisible(); + }); +}); diff --git a/apps/spruce/playwright/tests/version/restart_modal.spec.ts b/apps/spruce/playwright/tests/version/restart_modal.spec.ts new file mode 100644 index 0000000000..d8d4d8d5f3 --- /dev/null +++ b/apps/spruce/playwright/tests/version/restart_modal.spec.ts @@ -0,0 +1,142 @@ +import { test, expect } from "../../fixtures"; +import { clickCheckboxByLabel, validateToast } from "../../helpers"; + +const path = "/version/5ecedafb562343215a7ff297"; + +test.describe("version/restart_modal", () => { + test.describe("Restarting a patch with Downstream Tasks", () => { + test("Clicking on the Select Downstream Tasks should show the downstream projects", async ({ + authenticatedPage: page, + }) => { + await page.goto("/version/5f74d99ab2373627c047c5e5"); + await page.getByTestId("restart-version").click(); + await page + .getByTestId("select-downstream") + .getByText("evergreen") + .first() + .click(); + }); + }); + + test.describe("Restarting a patch", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(path); + await expect(page.getByTestId("version-restart-modal")).toBeHidden(); + await page.getByTestId("restart-version").click(); + await expect(page.getByTestId("version-restart-modal")).toBeVisible(); + }); + + test("Clicking on a variant should toggle an accordion dropdown of tasks", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("variant-accordion").first().click(); + await expect( + page.getByTestId("version-restart-modal").getByText("dist"), + ).toBeVisible(); + }); + + test("Clicking on a variant checkbox should toggle its textbox and all the associated tasks", async ({ + authenticatedPage: page, + }) => { + const taskStatusBadge = page + .getByTestId("version-restart-modal") + .getByTestId("task-status-badge"); + await page.getByTestId("variant-accordion").first().click(); + await expect(taskStatusBadge).toContainText("0 of 1 Selected"); + const selectUbuntuCheckbox = page + .getByTestId("version-restart-modal") + .getByText("Ubuntu 16.04"); + await selectUbuntuCheckbox.click(); + await expect(taskStatusBadge).toContainText("1 of 1 Selected"); + await selectUbuntuCheckbox.click(); + await expect(taskStatusBadge).toContainText("0 of 1 Selected"); + }); + + test("Clicking on a task should toggle its check box and select the task", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("variant-accordion").first().click(); + const taskCheckbox = page + .getByTestId("version-restart-modal") + .getByText("dist"); + await taskCheckbox.click(); + await expect( + page + .getByTestId("version-restart-modal") + .getByTestId("task-status-badge"), + ).toContainText("1 of 1 Selected"); + }); + + test("Selecting on the task status filter should toggle the tasks that have matching statuses to it", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("task-status-filter").click(); + await clickCheckboxByLabel(page, "All"); + await page.getByTestId("task-status-filter").click(); + + await expect(page.getByTestId("version-restart-modal")).toContainText( + "Are you sure you want to restart the 1 selected tasks?", + ); + await page.getByTestId("task-status-filter").click(); + await clickCheckboxByLabel(page, "All"); + await page.getByTestId("task-status-filter").click(); + }); + + test("Selecting on the base status filter should toggle the tasks that have matching statuses to it", async ({ + authenticatedPage: page, + }) => { + const modal = page.getByTestId("version-restart-modal"); + await modal.getByTestId("base-task-status-filter").click(); + await clickCheckboxByLabel(page, "Succeeded"); + await modal.getByTestId("base-task-status-filter").click(); + + await expect(modal.getByTestId("confirmation-message")).toContainText( + "Are you sure you want to restart the 1 selected tasks?", + ); + await modal.getByTestId("base-task-status-filter").click(); + await clickCheckboxByLabel(page, "Succeeded"); + await modal.getByTestId("base-task-status-filter").click(); + }); + + test("Restarting a task should close the modal and display a success message if it occurs successfully.", async ({ + authenticatedPage: page, + }) => { + const modal = page.getByTestId("version-restart-modal"); + await modal.getByTestId("variant-accordion").first().click(); + const taskCheckbox = page + .getByTestId("version-restart-modal") + .getByText("dist"); + await taskCheckbox.click(); + await modal.getByRole("button", { name: "Restart" }).click(); + await expect(page.getByTestId("version-restart-modal")).toBeHidden(); + await validateToast(page, "success", "Successfully restarted tasks!"); + }); + }); + + test.describe("Restarting mainline commits", () => { + test("should be able to restart scheduled mainline commit tasks", async ({ + authenticatedPage: page, + }) => { + await page.goto( + "/version/spruce_ab494436448fbb1d244833046ea6f6af1544e86d", + ); + await expect(page.getByTestId("restart-version")).toHaveAttribute( + "aria-disabled", + "false", + ); + await page.getByTestId("restart-version").click(); + await expect(page.getByTestId("version-restart-modal")).toBeVisible(); + + const modal = page.getByTestId("version-restart-modal"); + await modal.getByTestId("accordion-toggle").click(); + const taskCheckbox = modal.getByText("check_codegen"); + + await taskCheckbox.click(); + await expect( + modal.getByRole("button", { name: "Restart" }), + ).toHaveAttribute("aria-disabled", "false"); + await modal.getByRole("button", { name: "Restart" }).click(); + await validateToast(page, "success", "Successfully restarted tasks!"); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/version/routes.spec.ts b/apps/spruce/playwright/tests/version/routes.spec.ts new file mode 100644 index 0000000000..77eea3b7bd --- /dev/null +++ b/apps/spruce/playwright/tests/version/routes.spec.ts @@ -0,0 +1,208 @@ +import { test, expect } from "../../fixtures"; + +const versions = { + 0: "5ecedafb562343215a7ff297", + 2: "52a630633ff1227909000021", + 4: "evergreen_33016573166a36bd5f46b4111151899d5c4e95b1", + 5: "5e4ff3abe3c3317e352062e4", +}; + +const versionRoute = (id: string) => `/version/${id}`; + +test.describe("Version route", () => { + test.describe("Metadata", () => { + test("Shows patch parameters if they exist", async ({ + authenticatedPage: page, + }) => { + await page.goto(versionRoute(versions[0])); + await expect(page.getByTestId("parameters-modal")).toHaveCount(0); + await page.getByTestId("parameters-link").click(); + await expect(page.getByTestId("parameters-modal")).toBeVisible(); + await page.getByRole("button", { name: "Close modal" }).click(); + await expect(page.getByTestId("parameters-modal")).toBeHidden(); + }); + + test("'Base commit' link in metadata links to version page", async ({ + authenticatedPage: page, + }) => { + await page.goto(versionRoute(versions[0])); + await expect(page.getByTestId("patch-base-commit")).toHaveAttribute( + "href", + new RegExp(`/version/${versions[4]}`), + ); + }); + + test("Doesn't show patch parameters if they don't exist", async ({ + authenticatedPage: page, + }) => { + await page.goto(versionRoute(versions[2])); + await expect(page.getByTestId("parameters-link")).toHaveCount(0); + await expect(page.getByTestId("parameters-modal")).toHaveCount(0); + }); + }); + + test.describe("Build Variants", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(versionRoute(versions[0])); + const table = page.getByTestId("tasks-table"); + await expect(table).toBeVisible(); + await expect(table).not.toHaveAttribute("data-loading", "true"); + }); + + test("Lists the patch's build variants", async ({ + authenticatedPage: page, + }) => { + await expect( + page + .getByTestId("build-variants") + .getByTestId("patch-build-variant") + .first(), + ).toBeVisible(); + }); + + test.describe("Grouped Task Status Badge", () => { + test("Shows tooltip with task's name on hover", async ({ + authenticatedPage: page, + }) => { + const statusBadge = page + .getByTestId("build-variants") + .getByTestId("grouped-task-status-badge") + .first(); + await statusBadge.hover(); + await expect(statusBadge).toContainText("1Succeeded"); + }); + + test("Navigates to task tab and applies filters when clicking on grouped task status badge", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("changes-tab").first().click(); + await expect(page.getByTestId("task-tab")).toHaveAttribute( + "aria-selected", + "false", + ); + + await page + .getByTestId("build-variants") + .getByTestId("grouped-task-status-badge") + .first() + .click(); + await expect(page.getByTestId("task-tab")).toHaveAttribute( + "aria-selected", + "true", + ); + await expect(page).toHaveURL( + /sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&statuses=success&variant=%5Eubuntu1604%24/, + ); + + await page.getByTestId("status-filter").click(); + await expect(page.getByTestId("status-filter-wrapper")).toBeVisible(); + await expect(page.getByLabel("Succeeded")).toHaveAttribute( + "aria-checked", + "true", + ); + + await page.getByTestId("variant-filter").click(); + await expect( + page.getByTestId("variant-filter-wrapper").locator("input"), + ).toHaveValue("^ubuntu1604$"); + }); + + test("Keeps sorts but not other filters when clicking on grouped task status badge", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("clear-all-filters").click(); + + await page.getByTestId("task-name-filter").click(); + const taskNameInput = page + .getByTestId("task-name-filter-wrapper") + .locator("input"); + await taskNameInput.focus(); + await taskNameInput.fill("a-task-name"); + await taskNameInput.press("Enter"); + + await page + .getByTestId("build-variants") + .getByTestId("grouped-task-status-badge") + .first() + .click(); + await expect(page).toHaveURL( + /sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&statuses=success&variant=%5Eubuntu1604%24/, + ); + }); + }); + + test.describe("Build Variant Name", () => { + test("Navigates to task tab and applies filters when clicking on build variant name", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("clear-all-filters").click(); + + await page.getByTestId("changes-tab").first().click(); + await expect(page.getByTestId("task-tab")).toHaveAttribute( + "aria-selected", + "false", + ); + + await page.getByTestId("build-variant-display-name").first().click(); + await expect(page.getByTestId("task-tab")).toHaveAttribute( + "aria-selected", + "true", + ); + await expect(page).toHaveURL( + /sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&variant=%5Eubuntu1604%24/, + ); + + await page.getByTestId("variant-filter").click(); + await expect( + page.getByTestId("variant-filter-wrapper").locator("input"), + ).toHaveValue("^ubuntu1604$"); + }); + + test("Keeps sorts but not other filters when clicking on build variant name", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("clear-all-filters").click(); + + await page.getByTestId("task-name-filter").click(); + const taskNameInput = page + .getByTestId("task-name-filter-wrapper") + .locator("input"); + await taskNameInput.focus(); + await taskNameInput.fill("a-task-name"); + await taskNameInput.press("Enter"); + + await page.getByTestId("build-variant-display-name").first().click(); + await expect(page).toHaveURL( + /sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&variant=%5Eubuntu1604%24/, + ); + }); + }); + }); + + test.describe("Page title", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(versionRoute(versions[5])); + }); + + test("Should include a link to Jira", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("page-title").getByRole("link", { name: "EVG-7425" }), + ).toHaveAttribute("href", "https://jira.example.com/browse/EVG-7425"); + }); + + test("Should include a link to GitHub", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("page-title").getByRole("link", { + name: "https://github.com/evergreen-ci/evergreen/pull/3186", + }), + ).toHaveAttribute( + "href", + "https://github.com/evergreen-ci/evergreen/pull/3186", + ); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/version/schedule_modal.spec.ts b/apps/spruce/playwright/tests/version/schedule_modal.spec.ts new file mode 100644 index 0000000000..7c43f1abcd --- /dev/null +++ b/apps/spruce/playwright/tests/version/schedule_modal.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; + +test.describe("Restarting and scheduling mainline commits", () => { + test("should be able to schedule inactive mainline commit tasks", async ({ + authenticatedPage: page, + }) => { + await page.goto("/version/spruce_e695f654c8b4b959d3e12e71696c3e318bcd4c33"); + await expect(page.getByTestId("schedule-patch")).toBeVisible(); + await expect(page.getByTestId("schedule-patch")).toHaveAttribute( + "aria-disabled", + "false", + ); + await page.getByTestId("schedule-patch").click(); + await expect(page.getByTestId("schedule-tasks-modal")).toBeVisible(); + + const modal = page.getByTestId("schedule-tasks-modal"); + await modal.getByTestId("accordion-toggle").click(); + const taskCheckbox = modal.getByText("check_codegen"); + await taskCheckbox.click(); + await expect( + modal.getByRole("button", { name: "Schedule" }), + ).toHaveAttribute("aria-disabled", "false"); + await modal.getByRole("button", { name: "Schedule" }).click(); + await validateToast(page, "success", "Successfully scheduled tasks!"); + }); +}); diff --git a/apps/spruce/playwright/tests/version/subscription_modal.spec.ts b/apps/spruce/playwright/tests/version/subscription_modal.spec.ts index 649d847235..134a974a03 100644 --- a/apps/spruce/playwright/tests/version/subscription_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/subscription_modal.spec.ts @@ -171,4 +171,95 @@ test.describe("Version Subscription Modal", () => { await context.clearCookies(); }); + + test.describe("Regex selector inputs", () => { + test("Can add and remove regex selectors", async ({ + authenticatedPage: page, + }) => { + await openSubscriptionModal(page); + await selectOption( + page, + "Event", + "A build-variant in this version finishes", + ); + await expect(page.getByTestId("regex-selector-row")).toHaveCount(0); + await page.getByText("Add Additional Criteria").click(); + await expect(page.getByTestId("regex-selector-row")).toHaveCount(1); + await page.getByTestId("delete-item-button").first().click(); + await expect(page.getByTestId("regex-selector-row")).toHaveCount(0); + }); + + test("Selecting a regex selector type will disable that option in other regex selector type dropdowns", async ({ + authenticatedPage: page, + }) => { + await openSubscriptionModal(page); + await selectOption( + page, + "Event", + "A build-variant in this version finishes", + ); + await page.getByText("Add Additional Criteria").click(); + await expect(page.getByText("Build Variant ID")).toBeVisible(); + await page.getByText("Add Additional Criteria").click(); + await expect(page.getByText("Build Variant Name")).toBeVisible(); + await page.getByTestId("regex-select").last().click(); + await expect( + page.locator("#regex-select-menu").getByText("Build Variant ID"), + ).toHaveCSS("cursor", "not-allowed"); + }); + + test("Regex selectors are optional for triggers that offer them", async ({ + authenticatedPage: page, + }) => { + await openSubscriptionModal(page); + await selectOption( + page, + "Event", + "A build-variant in this version finishes", + ); + await page.getByTestId("jira-comment-input").fill("EVG-2000"); + await expect(page.getByRole("button", { name: "Save" })).toHaveAttribute( + "aria-disabled", + "false", + ); + await page.getByRole("button", { name: "Save" }).click(); + await validateToast(page, "success", "Your subscription has been added"); + }); + + test("Display success toast after submitting a valid form with regex selectors inputs and request succeeds", async ({ + authenticatedPage: page, + }) => { + await openSubscriptionModal(page); + await selectOption( + page, + "Event", + "A build-variant in this version finishes", + ); + await page.getByText("Add Additional Criteria").click(); + await selectOption(page, "Field name", "Build Variant Name"); + await page.getByTestId("regex-input").fill("stuff"); + await page.getByTestId("jira-comment-input").fill("EVG-2000"); + await page.getByRole("button", { name: "Save" }).click(); + await validateToast(page, "success", "Your subscription has been added"); + }); + + test("'Add Additional Criteria' button should not appear when there are enough 'Field name' dropdowns to represent all possible regex selector types for a trigger", async ({ + authenticatedPage: page, + }) => { + await openSubscriptionModal(page); + await selectOption( + page, + "Event", + "A build-variant in this version finishes", + ); + const addCriteriaButton = page.getByRole("button", { + name: "Add Additional Criteria", + }); + await expect(addCriteriaButton).toHaveAttribute("aria-disabled", "false"); + await addCriteriaButton.click(); + await expect(addCriteriaButton).toHaveAttribute("aria-disabled", "false"); + await addCriteriaButton.click(); + await expect(addCriteriaButton).toBeHidden(); + }); + }); }); diff --git a/apps/spruce/playwright/tests/version/task_duration.spec.ts b/apps/spruce/playwright/tests/version/task_duration.spec.ts new file mode 100644 index 0000000000..48ba4e594d --- /dev/null +++ b/apps/spruce/playwright/tests/version/task_duration.spec.ts @@ -0,0 +1,145 @@ +import { test, expect } from "../../fixtures"; + +test.describe("Task Duration Tab", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/version/5e4ff3abe3c3317e352062e4/task-duration"); + }); + + test.describe("when interacting with the filters on the page", () => { + test("updates URL appropriately when task name filter is applied", async ({ + authenticatedPage: page, + }) => { + const filterText = "test-annotation"; + await page.getByTestId("task-name-filter-popover").click(); + const filterInput = page.getByPlaceholder("Task name regex"); + await filterInput.fill(filterText); + await filterInput.press("Enter"); + await expect(page.getByTestId("task-duration-table-row")).toHaveCount(1); + await expect(page).toHaveURL( + /page=0&sorts=DURATION%3ADESC&taskName=test-annotation/, + ); + + await page.getByTestId("task-name-filter-popover").click(); + await filterInput.clear(); + await filterInput.press("Enter"); + await expect(page).toHaveURL( + "/version/5e4ff3abe3c3317e352062e4/task-duration?page=0&sorts=DURATION%3ADESC", + ); + }); + + test("updates URL appropriately when status filter is applied", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("status-filter-popover").click(); + const options = page.getByTestId("tree-select-options"); + await expect(options).toBeVisible(); + await options.getByText("Running").first().click(); + await expect(page.getByTestId("task-duration-table-row")).toHaveCount(3); + await expect(page).toHaveURL( + /statuses=running-umbrella,started,dispatched/, + ); + await expect(options).toBeVisible(); + await options.getByText("Succeeded").click(); + await expect(page).toHaveURL( + /statuses=running-umbrella,started,dispatched,success/, + ); + }); + + test("updates URL appropriately when build variant filter is applied", async ({ + authenticatedPage: page, + }) => { + const filterText = "Lint"; + await page.getByTestId("build-variant-filter-popover").click(); + const filterInput = page.getByPlaceholder("Build variant regex"); + await filterInput.fill(filterText); + await filterInput.press("Enter"); + await expect(page.getByTestId("task-duration-table-row")).toHaveCount(2); + await expect(page).toHaveURL(/page=0&sorts=DURATION%3ADESC&variant=Lint/); + + await page.getByTestId("build-variant-filter-popover").click(); + await filterInput.clear(); + await filterInput.press("Enter"); + await expect(page).toHaveURL( + "/version/5e4ff3abe3c3317e352062e4/task-duration?page=0&sorts=DURATION%3ADESC", + ); + }); + + test("updates URL appropriately when sort is changing", async ({ + authenticatedPage: page, + }) => { + const durationSortControl = page.getByRole("button", { + name: "Sort by Task Duration", + }); + const statusSortControl = page.getByRole("button", { + name: "Sort by Status", + }); + const variantSortControl = page.getByRole("button", { + name: "Sort by Build Variant", + }); + + await expect(page).toHaveURL( + "/version/5e4ff3abe3c3317e352062e4/task-duration?sorts=DURATION%3ADESC", + ); + const longestTask = "test-thirdparty"; + await expect(page.getByText(longestTask)).toBeVisible(); + await expect( + page.getByTestId("task-duration-table-row").first(), + ).toContainText(longestTask); + + await durationSortControl.click(); + await expect(page).toHaveURL( + "/version/5e4ff3abe3c3317e352062e4/task-duration?page=0", + ); + await durationSortControl.click(); + await expect(page).toHaveURL(/sorts=DURATION%3AASC/); + const shortestTask = "clone_test-model"; + await expect(page.getByText(shortestTask)).toBeVisible(); + await expect( + page.getByTestId("task-duration-table-row").first(), + ).toContainText(shortestTask); + + await statusSortControl.click(); + await expect(page).toHaveURL( + /page=0&sorts=DURATION%3AASC%3BSTATUS%3AASC/, + ); + await statusSortControl.click(); + await expect(page).toHaveURL( + /page=0&sorts=DURATION%3AASC%3BSTATUS%3ADESC/, + ); + await variantSortControl.click(); + await expect(page).toHaveURL( + /page=0&sorts=DURATION%3AASC%3BSTATUS%3ADESC/, + ); + }); + + test("clearing all filters resets to the default sort", async ({ + authenticatedPage: page, + }) => { + const durationSortControl = page.getByRole("button", { + name: "Sort by Task Duration", + }); + await durationSortControl.click(); + await expect(page).toHaveURL( + "/version/5e4ff3abe3c3317e352062e4/task-duration?page=0", + ); + await durationSortControl.click(); + await expect(page).toHaveURL(/sorts=DURATION%3AASC/); + await page.getByText("Clear all filters").click(); + await expect(page).toHaveURL(/sorts=DURATION%3ADESC/); + }); + + test("shows message when no test results are found", async ({ + authenticatedPage: page, + }) => { + const filterText = "this_does_not_exist"; + await page.getByTestId("task-name-filter-popover").click(); + const filterInput = page.getByPlaceholder("Task name regex"); + await filterInput.fill(filterText); + await filterInput.press("Enter"); + await expect( + page.getByTestId("task-name-filter-popover-task-duration-table-row"), + ).toHaveCount(0); + await expect(page.getByText("No tasks found.")).toBeVisible(); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/version/task_filters.spec.ts b/apps/spruce/playwright/tests/version/task_filters.spec.ts new file mode 100644 index 0000000000..b4e01244c0 --- /dev/null +++ b/apps/spruce/playwright/tests/version/task_filters.spec.ts @@ -0,0 +1,224 @@ +import { Page } from "@playwright/test"; +import { test, expect } from "../../fixtures"; +import { clickCheckboxByLabel } from "../../helpers"; + +const patch = { id: "5e4ff3abe3c3317e352062e4" }; +const pathTasks = `/version/${patch.id}/tasks`; +const pathURLWithFilters = `${pathTasks}?page=0&sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&statuses=failed,success,running-umbrella,dispatched,started&taskName=test-thirdparty&variant=ubuntu`; +const defaultPath = `${pathTasks}?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC`; + +const waitForTaskTable = async (page: Page) => { + const table = page.getByTestId("tasks-table"); + await expect(table).toBeVisible(); + await expect(table).not.toHaveAttribute("data-loading", "true"); +}; + +test.describe("Tasks filters", () => { + test("Should clear any filters with the Clear All Filters button and reset the table to its default state", async ({ + authenticatedPage: page, + }) => { + await page.goto(pathURLWithFilters); + await waitForTaskTable(page); + await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); + + await page.getByTestId("clear-all-filters").click(); + await expect(page).toHaveURL(`${defaultPath}`); + + await page.getByTestId("task-name-filter").click(); + await expect( + page.getByTestId("task-name-filter-wrapper").locator("input"), + ).toHaveValue(""); + + await page.getByTestId("status-filter").click(); + const statusCheckboxes = page + .getByTestId("status-filter-wrapper") + .locator('input[type="checkbox"]'); + + for (const checkbox of await statusCheckboxes.all()) { + await expect(checkbox).not.toBeChecked(); + } + + await page.getByTestId("base-status-filter").click(); + const baseStatusCheckboxes = page + .getByTestId("base-status-filter-wrapper") + .locator('input[type="checkbox"]'); + + for (const checkbox of await baseStatusCheckboxes.all()) { + await expect(checkbox).not.toBeChecked(); + } + + await page.getByTestId("variant-filter").click(); + await expect( + page.getByTestId("variant-filter-wrapper").locator("input"), + ).toHaveValue(""); + }); + + test.describe("Variant input field", () => { + const variantInputValue = "lint"; + + test("Updates url with input value and fetches tasks filtered by variant", async ({ + authenticatedPage: page, + }) => { + await page.goto(defaultPath); + await waitForTaskTable(page); + await page.getByTestId("variant-filter").click(); + + const variantInput = page + .getByTestId("variant-filter-wrapper") + .locator("input"); + await variantInput.focus(); + await variantInput.fill(variantInputValue); + await variantInput.press("Enter"); + await expect(page.getByTestId("variant-filter-wrapper")).toHaveCount(0); + await expect(page).toHaveURL(new RegExp(variantInputValue)); + await waitForTaskTable(page); + await expect(page.getByTestId("filtered-count")).toContainText("2"); + + await page.getByTestId("variant-filter").click(); + await variantInput.focus(); + await variantInput.clear(); + await variantInput.press("Enter"); + await expect(page.getByTestId("variant-filter-wrapper")).toHaveCount(0); + await expect(page).not.toHaveURL(/variant=/); + await waitForTaskTable(page); + await expect(page.getByTestId("filtered-count")).toContainText("47"); + }); + }); + + test.describe("Task name input field", () => { + const taskNameInputValue = "test-cloud"; + + test("Updates url with input value and fetches tasks filtered by task name", async ({ + authenticatedPage: page, + }) => { + await page.goto(defaultPath); + await waitForTaskTable(page); + await page.getByTestId("task-name-filter").click(); + + const taskNameInput = page + .getByTestId("task-name-filter-wrapper") + .locator("input"); + await taskNameInput.focus(); + await taskNameInput.fill(taskNameInputValue); + await taskNameInput.press("Enter"); + await expect(page.getByTestId("task-name-filter-wrapper")).toHaveCount(0); + await expect(page).toHaveURL(new RegExp(taskNameInputValue)); + await waitForTaskTable(page); + await expect(page.getByTestId("filtered-count")).toContainText("1"); + + await page.getByTestId("task-name-filter").click(); + await taskNameInput.focus(); + await taskNameInput.clear(); + await taskNameInput.press("Enter"); + await expect(page.getByTestId("task-name-filter-wrapper")).toHaveCount(0); + await expect(page).not.toHaveURL(/taskName=/); + await waitForTaskTable(page); + await expect(page.getByTestId("filtered-count")).toContainText("47"); + }); + }); + + test.describe("Task Statuses select", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(defaultPath); + await waitForTaskTable(page); + await page.getByTestId("status-filter").click(); + await expect(page.getByTestId("tree-select-options")).toBeVisible(); + }); + + test("Clicking on a status filter filters the tasks to only those statuses", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId("tree-select-options") + .getByText("Failed") + .first() + .click(); + await expect(page).toHaveURL(/statuses=failed/); + await waitForTaskTable(page); + await expect(page.getByTestId("filtered-count")).toHaveText("2"); + + await page + .getByTestId("tree-select-options") + .getByText("Succeeded") + .click(); + await expect(page).toHaveURL( + /statuses=failed-umbrella,failed,known-issue,success/, + ); + await waitForTaskTable(page); + await expect(page.getByTestId("filtered-count")).not.toHaveText("2"); + }); + + test("Clicking on 'All' checkbox adds all the statuses and clicking again removes them", async ({ + authenticatedPage: page, + }) => { + const taskStatuses = [ + "All", + "Failed", + "Known Issue", + "Succeeded", + "Running", + "Dispatched", + "Blocked", + ]; + await clickCheckboxByLabel(page, "All"); + for (const label of taskStatuses) { + const checkbox = page.getByRole("checkbox", { name: label }).first(); + await expect(checkbox).toBeChecked(); + } + await expect(page).toHaveURL(/statuses=all/); + await waitForTaskTable(page); + + await clickCheckboxByLabel(page, "All"); + for (const label of taskStatuses) { + const checkbox = page.getByRole("checkbox", { name: label }).first(); + await expect(checkbox).not.toBeChecked(); + } + await expect(page).not.toHaveURL(/statuses/); + }); + }); + + test.describe("Task Base Statuses select", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(defaultPath); + await waitForTaskTable(page); + await page.getByTestId("base-status-filter").click(); + await expect( + page.getByTestId("base-status-filter-wrapper"), + ).toBeVisible(); + }); + + test("Clicking on a base status filter filters the tasks to only those base statuses", async ({ + authenticatedPage: page, + }) => { + await clickCheckboxByLabel(page, "Succeeded"); + await expect(page).toHaveURL(/baseStatuses=success/); + await waitForTaskTable(page); + await expect(page.getByTestId("filtered-count")).toHaveText("44"); + + await clickCheckboxByLabel(page, "Succeeded"); + await expect(page).not.toHaveURL(/baseStatuses/); + await waitForTaskTable(page); + await expect(page.getByTestId("filtered-count")).toHaveText("47"); + }); + + test("Clicking on 'All' checkbox adds all the base statuses and clicking again removes them", async ({ + authenticatedPage: page, + }) => { + const taskStatuses = ["All", "Succeeded", "Running"]; + await clickCheckboxByLabel(page, "All"); + for (const label of taskStatuses) { + await expect(page.getByRole("checkbox", { name: label })).toBeChecked(); + } + await expect(page).toHaveURL(/baseStatuses=all/); + await waitForTaskTable(page); + + await clickCheckboxByLabel(page, "All"); + for (const label of taskStatuses) { + await expect( + page.getByRole("checkbox", { name: label }), + ).not.toBeChecked(); + } + await expect(page).not.toHaveURL(/baseStatuses/); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/version/task_table.spec.ts b/apps/spruce/playwright/tests/version/task_table.spec.ts new file mode 100644 index 0000000000..994455fd51 --- /dev/null +++ b/apps/spruce/playwright/tests/version/task_table.spec.ts @@ -0,0 +1,250 @@ +import { Page } from "@playwright/test"; +import { SEEN_TASK_REVIEW_TOOLTIP } from "constants/cookies"; +import { test, expect } from "../../fixtures"; + +const pathTasks = "/version/5e4ff3abe3c3317e352062e4/tasks"; +const patchDescriptionTasksExist = "dist"; + +const firstTaskId = + "evergreen_ubuntu1604_test_service_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48"; +const displayTaskId = "evergreen_ubuntu1604_89"; +const executionTaskId1 = "exec1"; +const executionTaskId2 = "exec2"; + +const waitForTaskTable = async (page: Page) => { + const table = page.getByTestId("tasks-table"); + await expect(table).toBeVisible(); + await expect(table).not.toHaveAttribute("data-loading", "true"); +}; + +test.describe("Task table", () => { + test("Loading skeleton does not persist when you navigate to Patch page from My Patches and adjust a filter", async ({ + authenticatedPage: page, + }) => { + await page.goto("/user/patches"); + await page + .getByTestId("patch-card-patch-link") + .filter({ hasText: patchDescriptionTasksExist }) + .click(); + await expect(page.getByTestId("tasks-table")).toBeVisible(); + }); + + test("Updates sorting in the url when column headers are clicked", async ({ + authenticatedPage: page, + }) => { + await page.goto(pathTasks); + await waitForTaskTable(page); + await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); + await expect(page).toHaveURL(/sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC/); + + const nameSortControl = page.getByRole("button", { name: "Sort by Name" }); + const statusSortControl = page.getByRole("button", { + name: "Sort by Task Status", + }); + const baseStatusSortControl = page.getByRole("button", { + name: "Sort by Previous Status", + }); + const variantSortControl = page.getByRole("button", { + name: "Sort by Variant", + }); + + await nameSortControl.click(); + await expect(page).toHaveURL(/BASE_STATUS%3ADESC%3BNAME%3AASC/); + + await variantSortControl.click(); + await expect(page).toHaveURL(/sorts=NAME%3AASC%3BVARIANT%3AASC/); + + await statusSortControl.click(); + await expect(page).toHaveURL(/sorts=VARIANT%3AASC%3BSTATUS%3AASC/); + + await baseStatusSortControl.click(); + await expect(page).toHaveURL(/sorts=STATUS%3AASC%3BBASE_STATUS%3AASC/); + + await baseStatusSortControl.click(); + await expect(page).toHaveURL(/sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC/); + + await baseStatusSortControl.click(); + await expect(page).toHaveURL(/sorts=STATUS%3AASC/); + }); + + test("Clicking task name goes to task page for that task", async ({ + authenticatedPage: page, + }) => { + await page.goto(pathTasks); + await expect( + page.getByTestId("tasks-table-row").first().locator("a").first(), + ).toHaveAttribute("href", /\/task/); + }); + + test("Task count displays total tasks", async ({ + authenticatedPage: page, + }) => { + await page.goto(pathTasks); + await expect(page.getByTestId("total-count").first()).toContainText("49"); + }); + + test.describe("Changing page number", () => { + test("Displays the next page of results and updates URL when right arrow is clicked and next page exists", async ({ + authenticatedPage: page, + }) => { + await page.goto(`${pathTasks}?page=0`); + await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); + + const firstPageText = await page + .getByTestId("tasks-table-row") + .first() + .textContent(); + await page.getByTestId("next-page-button").click(); + await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); + const secondPageText = page.getByTestId("tasks-table-row").first(); + await expect(secondPageText).not.toHaveText(firstPageText!); + }); + + test("Displays the previous page of results and updates URL when the left arrow is clicked and previous page exists", async ({ + authenticatedPage: page, + }) => { + await page.goto(`${pathTasks}?page=1`); + await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); + const secondPageText = await page + .getByTestId("tasks-table-row") + .first() + .textContent(); + await page.getByTestId("prev-page-button").click(); + await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); + const firstPageText = page.getByTestId("tasks-table-row").first(); + await expect(firstPageText).not.toHaveText(secondPageText!); + }); + + test("Does not update results or URL when left arrow is clicked and previous page does not exist", async ({ + authenticatedPage: page, + }) => { + await page.goto(`${pathTasks}?page=0`); + await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); + await expect(page.getByTestId("prev-page-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + + test("Does not update results or URL when right arrow is clicked and next page does not exist", async ({ + authenticatedPage: page, + }) => { + await page.goto(`${pathTasks}?page=4`); + await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); + await expect(page.getByTestId("next-page-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + }); + + test.describe("blocked tasks", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(`${pathTasks}?limit=100`); + await waitForTaskTable(page); + }); + + test("shows the blocking tasks when hovering over status badge", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("depends-on-tooltip")).toHaveCount(0); + await page.getByTestId("task-status-badge").getByText("Blocked").hover(); + await expect(page.getByTestId("depends-on-tooltip")).toBeVisible(); + await expect(page.getByTestId("depends-on-tooltip")).toContainText( + `Depends on tasks: “test-migrations”, “test-graphql”`, + ); + }); + }); + + test.describe("task review", () => { + // Clicks the label associated with a reviewed checkbox (the input itself is hidden). + const clickReviewed = async (page: Page, testId: string) => { + const id = await page.getByTestId(testId).getAttribute("id"); + await page.locator(`label[for="${id}"]`).click(); + }; + + test("marks tasks as viewed and preserves their state on reload", async ({ + authenticatedPage: page, + }) => { + await page.goto(pathTasks); + await clickReviewed(page, `reviewed-${firstTaskId}`); + await expect(page.getByTestId(`reviewed-${firstTaskId}`)).toBeChecked(); + + await page.getByRole("button", { name: "Expand row" }).click(); + await expect( + page.getByTestId(`reviewed-${executionTaskId1}`), + ).toHaveAttribute("aria-disabled", "true"); + await clickReviewed(page, `reviewed-${executionTaskId2}`); + await expect(page.getByTestId(`reviewed-${displayTaskId}`)).toBeChecked(); + await clickReviewed(page, `reviewed-${displayTaskId}`); + await expect( + page.getByTestId(`reviewed-${displayTaskId}`), + ).not.toBeChecked(); + await expect( + page.getByTestId(`reviewed-${executionTaskId2}`), + ).not.toBeChecked(); + await clickReviewed(page, `reviewed-${displayTaskId}`); + await expect(page.getByTestId(`reviewed-${displayTaskId}`)).toBeChecked(); + await expect( + page.getByTestId(`reviewed-${executionTaskId2}`), + ).toBeChecked(); + + await page.reload(); + + await expect(page.getByTestId(`reviewed-${firstTaskId}`)).toBeChecked(); + await expect(page.getByTestId(`reviewed-${displayTaskId}`)).toBeChecked(); + await page.getByRole("button", { name: "Expand row" }).click(); + await expect( + page.getByTestId(`reviewed-${executionTaskId2}`), + ).toBeChecked(); + }); + + test.describe("announcement tooltip", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.context().clearCookies({ name: SEEN_TASK_REVIEW_TOOLTIP }); + await page.goto(pathTasks); + await waitForTaskTable(page); + }); + + test("shows the announcement tooltip open on the first viewing", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByText("New feature: Task Review")).toBeVisible(); + }); + + test("shows the info icon one day after the initial close and hides it after one week", async ({ + authenticatedPage: page, + }) => { + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + await page.context().addCookies([ + { + name: SEEN_TASK_REVIEW_TOOLTIP, + value: oneDayAgo.toString(), + domain: "localhost", + path: "/", + }, + ]); + await page.goto(pathTasks); + await expect(page.getByText("Reviewed")).toBeVisible(); + await expect(page.getByText("New feature: Task Review")).toBeHidden(); + await page.getByTestId("announcement-tooltip-trigger").click(); + + const eightDaysAgo = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000); + await page.context().addCookies([ + { + name: SEEN_TASK_REVIEW_TOOLTIP, + value: eightDaysAgo.toString(), + domain: "localhost", + path: "/", + }, + ]); + await page.goto(pathTasks); + await expect(page.getByText("Reviewed")).toBeVisible(); + await expect(page.getByText("New feature: Task Review")).toBeHidden(); + await expect( + page.getByTestId("announcement-tooltip-trigger"), + ).toBeHidden(); + }); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/version/test_analysis.spec.ts b/apps/spruce/playwright/tests/version/test_analysis.spec.ts new file mode 100644 index 0000000000..d380e4c525 --- /dev/null +++ b/apps/spruce/playwright/tests/version/test_analysis.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from "../../fixtures"; + +test.describe("Test Analysis", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/version/5e4ff3abe3c3317e352062e4/test-analysis"); + }); + + test("should group together all matching failing tests in a version and present a stat", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByText("1 test failed across more than one task"), + ).toBeVisible(); + await expect(page.getByText("JustAFakeTestInALonelyWorld")).toBeVisible(); + await expect( + page.getByText("JustAnotherFakeFailingTestInALonelyWorld"), + ).toBeVisible(); + }); + + test("clicking on a test should show the test details", async ({ + authenticatedPage: page, + }) => { + await page.getByText("JustAFakeTestInALonelyWorld").click(); + await expect( + page.getByTestId("failed-test-grouped-table").first(), + ).toBeVisible(); + }); + + test("filtering by test name should only show matching tests", async ({ + authenticatedPage: page, + }) => { + await page + .getByLabel("Search Test Failures") + .fill("JustAFakeTestInALonelyWorld"); + await page.getByLabel("Search Test Failures").press("Enter"); + await expect( + page.getByText("1 test failed across more than one task"), + ).toBeVisible(); + await expect(page.getByText("JustAFakeTestInALonelyWorld")).toBeVisible(); + await expect( + page.getByText("JustAnotherFakeFailingTestInALonelyWorld"), + ).toBeHidden(); + }); + + test("filtering by task status should only show matching tests", async ({ + authenticatedPage: page, + }) => { + await page.getByLabel("Failure Type").click(); + await expect( + page.getByTestId("task-status-known-issue-option"), + ).toBeVisible(); + await page.getByTestId("task-status-known-issue-option").click(); + + await expect( + page.getByText("0 tests failed across more than one task"), + ).toBeVisible(); + await expect(page.getByText("JustAFakeTestInALonelyWorld")).toBeVisible(); + await expect( + page.getByText("JustAnotherFakeFailingTestInALonelyWorld"), + ).toBeHidden(); + }); + + test("clearing the filters should reset the view", async ({ + authenticatedPage: page, + }) => { + await page + .getByLabel("Search Test Failures") + .fill("JustAFakeTestInALonelyWorld"); + await page.getByLabel("Search Test Failures").press("Enter"); + await page.getByLabel("Failure Type").click(); + await expect( + page.getByTestId("task-status-known-issue-option"), + ).toBeVisible(); + await page.getByTestId("task-status-known-issue-option").click(); + await page.keyboard.press("Escape"); + + await expect( + page.getByText("0 tests failed across more than one task"), + ).toBeVisible(); + await expect(page.getByTestId("clear-filter-button")).not.toHaveAttribute( + "disabled", + ); + await page.getByTestId("clear-filter-button").click(); + await expect(page.getByLabel("Search Test Failures")).toHaveValue(""); + await expect( + page.getByText("1 test failed across more than one task"), + ).toBeVisible(); + }); +}); diff --git a/apps/spruce/playwright/tests/version/version_timing.spec.ts b/apps/spruce/playwright/tests/version/version_timing.spec.ts new file mode 100644 index 0000000000..43b706b409 --- /dev/null +++ b/apps/spruce/playwright/tests/version/version_timing.spec.ts @@ -0,0 +1,192 @@ +import { test, expect } from "../../fixtures"; + +const chart = "[id^=reactgooglegraph]"; + +test.describe("Version Timing Tab without a variant selected", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/version/5e4ff3abe3c3317e352062e4/version-timing"); + }); + + test("shows a chart of all variants in the version", async ({ + authenticatedPage: page, + }) => { + await expect(page.locator(chart).getByText("Ubuntu 16.04")).toBeVisible(); + await expect(page.locator(chart).getByText("Race Detector")).toBeVisible(); + await expect(page.locator(chart).getByText("Lint")).toBeVisible(); + }); + + test("allows the user to select a variant and navigate to the variant timing view", async ({ + authenticatedPage: page, + }) => { + await page.locator(chart).getByText("Ubuntu 16.04").click(); + await expect(page).toHaveURL( + "/version/5e4ff3abe3c3317e352062e4/version-timing?variant=%5Eubuntu1604%24", + ); + }); + + test("has disabled pagination functionality", async ({ + authenticatedPage: page, + }) => { + await expect( + page.locator("button[aria-labelledby='page-size-select']"), + ).toHaveAttribute("aria-disabled", "true"); + await expect(page.getByTestId("next-page-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("prev-page-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("clear-all-filters")).toHaveAttribute( + "aria-disabled", + "true", + ); + }); +}); + +test.describe("Version Timing Tab with a variant selected", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto( + "/version/5e4ff3abe3c3317e352062e4/version-timing?variant=^ubuntu1604%24", + ); + }); + + const expectedTasks = [ + [ + "test-agent", + "test-cloud", + "test-operations", + "test-scheduler", + "js-test", + "test-units", + "test-command", + "test-model", + "test-model-2", + "test-model-host", + ], + [ + "test-validator", + "test-thirdparty", + "test-service", + "test-rest-model", + "test-rest-client", + "test-graphql", + "test-repotracker", + "test-trigger", + "test-rest-data", + "test-rest-route", + ], + [ + "test-model-grid", + "test-model-user", + "test-model-testresult", + "test-model-manifest", + "test-model-notification", + "test-model-commitqueue", + "test-model-patch", + "test-model-event", + "test-monitor", + "test-model-task", + ], + [ + "test-migrations", + "test-model-alertrecord", + "test-thirdparty-docker", + "test-model-stats", + "test-evergreen", + "test-model-distro", + "test-util", + "test-model-artifact", + "test-model-build", + "test-plugin", + ], + ["test-db", "test-auth"], + ]; + + test("shows a paginated chart of all tasks in the variant", async ({ + authenticatedPage: page, + }) => { + for (const pageTasks of expectedTasks.slice(0, -1)) { + for (const task of pageTasks) { + await expect( + page.locator(chart).getByText(task, { exact: true }), + ).toBeVisible(); + } + await page.getByTestId("next-page-button").click(); + } + for (const task of expectedTasks.at(-1)!) { + await expect( + page.locator(chart).getByText(task, { exact: true }), + ).toBeVisible(); + } + + const reversedTasks = [...expectedTasks].reverse(); + for (const pageTasks of reversedTasks.slice(0, -1)) { + for (const task of pageTasks) { + await expect( + page.locator(chart).getByText(task, { exact: true }), + ).toBeVisible(); + } + await page.getByTestId("prev-page-button").click(); + } + for (const task of reversedTasks.at(-1)!) { + await expect( + page.locator(chart).getByText(task, { exact: true }), + ).toBeVisible(); + } + }); + + test("respects the task filter", async ({ authenticatedPage: page }) => { + await page.goto( + "/version/5e4ff3abe3c3317e352062e4/version-timing?taskName=agent&variant=^ubuntu1604%24", + ); + await expect(page.getByTestId("next-page-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect( + page.locator(chart).getByText("test-agent", { exact: true }), + ).toBeVisible(); + const otherTasks = expectedTasks.flat().filter((t) => t !== "test-agent"); + for (const task of otherTasks) { + await expect( + page.locator(chart).getByText(task, { exact: true }), + ).toBeHidden(); + } + }); + + test("allows the user to clear all filters", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("clear-all-filters").click(); + await expect(page).toHaveURL( + "/version/5e4ff3abe3c3317e352062e4/version-timing?sorts=DURATION%3ADESC", + ); + await expect(page.locator(chart).getByText("Ubuntu 16.04")).toBeVisible(); + await expect(page.locator(chart).getByText("Race Detector")).toBeVisible(); + await expect(page.locator(chart).getByText("Lint")).toBeVisible(); + }); + + test("allows the user to change the page size", async ({ + authenticatedPage: page, + }) => { + await page.locator("button[aria-labelledby='page-size-select']").click(); + await page.getByText("50 / page").first().click(); + await expect(page).toHaveURL(/limit=50/); + for (const task of expectedTasks.flat()) { + await expect( + page.locator(chart).getByText(task, { exact: true }), + ).toBeVisible(); + } + }); + + test("allows the user to select a task and navigate to it", async ({ + authenticatedPage: page, + }) => { + await page.locator(chart).getByText("test-agent").click(); + await expect(page).toHaveURL( + "/task/evergreen_ubuntu1604_test_agent_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48/logs?execution=0", + ); + }); +}); diff --git a/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/UnmountButton.tsx b/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/UnmountButton.tsx index a4a1e27e71..15055587ff 100644 --- a/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/UnmountButton.tsx +++ b/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/UnmountButton.tsx @@ -68,6 +68,7 @@ export const UnmountButton: React.FC = ({ volume }) => { ) : ( { spawnAnalytics.sendEvent({ name: "Changed unmounted volume on host", diff --git a/apps/spruce/src/pages/version/Tabs/TaskDuration/TaskDurationTable/index.tsx b/apps/spruce/src/pages/version/Tabs/TaskDuration/TaskDurationTable/index.tsx index 4ae7aba8b3..c9df84a9a4 100644 --- a/apps/spruce/src/pages/version/Tabs/TaskDuration/TaskDurationTable/index.tsx +++ b/apps/spruce/src/pages/version/Tabs/TaskDuration/TaskDurationTable/index.tsx @@ -185,6 +185,7 @@ const getColumns = ( meta: { search: { "data-cy": "task-name-filter-popover", + placeholder: "Task name regex", }, }, }, @@ -215,6 +216,7 @@ const getColumns = ( meta: { search: { "data-cy": "build-variant-filter-popover", + placeholder: "Build variant regex", }, }, }, From 88581ed6745c8b80ea46de70254415cce7335f84 Mon Sep 17 00:00:00 2001 From: minnakt Date: Thu, 23 Apr 2026 13:35:14 -0400 Subject: [PATCH 03/32] clean up a bit --- .../tests/version/downstream_projects.spec.ts | 14 +++++++------ .../tests/version/page_tabs.spec.ts | 18 ++++++++-------- .../tests/version/restart_modal.spec.ts | 7 +++---- .../tests/version/schedule_modal.spec.ts | 21 +++++++++---------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/spruce/playwright/tests/version/downstream_projects.spec.ts b/apps/spruce/playwright/tests/version/downstream_projects.spec.ts index 38a075a24f..39bda0641e 100644 --- a/apps/spruce/playwright/tests/version/downstream_projects.spec.ts +++ b/apps/spruce/playwright/tests/version/downstream_projects.spec.ts @@ -31,12 +31,14 @@ test.describe("Downstream Projects Tab", () => { ); }); - test("filters by test name", async ({ authenticatedPage: page }) => { + test("filters by task name", async ({ authenticatedPage: page }) => { await page.getByTestId("task-name-filter").nth(1).click(); - const input = page.getByTestId("task-name-filter-wrapper").locator("input"); - await input.focus(); - await input.fill("generate-lint"); - await input.press("Enter"); + const taskInput = page + .getByTestId("task-name-filter-wrapper") + .locator("input"); + await taskInput.focus(); + await taskInput.fill("generate-lint"); + await taskInput.press("Enter"); await expect(page).not.toHaveURL(/generate-lint/); await expect(page.getByText("generate-lint")).toBeVisible(); }); @@ -44,6 +46,6 @@ test.describe("Downstream Projects Tab", () => { test("does not push query params to the URL", async ({ authenticatedPage: page, }) => { - expect(new URL(page.url()).pathname).toBe(DOWNSTREAM_ROUTE); + await expect(page).toHaveURL(new RegExp(DOWNSTREAM_ROUTE)); }); }); diff --git a/apps/spruce/playwright/tests/version/page_tabs.spec.ts b/apps/spruce/playwright/tests/version/page_tabs.spec.ts index f63d582fae..46534f2a22 100644 --- a/apps/spruce/playwright/tests/version/page_tabs.spec.ts +++ b/apps/spruce/playwright/tests/version/page_tabs.spec.ts @@ -3,9 +3,9 @@ import { test, expect } from "../../fixtures"; const versionId = "5ecedafb562343215a7ff297"; const versionRoute = `/version/${versionId}`; const versions = { - changes: { route: `${versionRoute}/changes`, btn: "changes-tab" }, - tasks: { route: `${versionRoute}/tasks`, btn: "task-tab" }, - duration: { route: `${versionRoute}/task-duration`, btn: "duration-tab" }, + changes: { route: `${versionRoute}/changes`, button: "changes-tab" }, + tasks: { route: `${versionRoute}/tasks`, button: "task-tab" }, + duration: { route: `${versionRoute}/task-duration`, button: "duration-tab" }, }; test.describe("page tabs", () => { @@ -13,7 +13,7 @@ test.describe("page tabs", () => { authenticatedPage: page, }) => { await page.goto(versionRoute); - await expect(page.getByTestId(versions.tasks.btn)).toHaveAttribute( + await expect(page.getByTestId(versions.tasks.button)).toHaveAttribute( "aria-selected", "true", ); @@ -26,7 +26,7 @@ test.describe("page tabs", () => { authenticatedPage: page, }) => { await page.goto(`${versionRoute}/task-duration`); - await expect(page.getByTestId(versions.duration.btn)).toHaveAttribute( + await expect(page.getByTestId(versions.duration.button)).toHaveAttribute( "aria-selected", "true", ); @@ -39,7 +39,7 @@ test.describe("page tabs", () => { authenticatedPage: page, }) => { await page.goto(`${versionRoute}/changes`); - await expect(page.getByTestId(versions.changes.btn)).toHaveAttribute( + await expect(page.getByTestId(versions.changes.button)).toHaveAttribute( "aria-selected", "true", ); @@ -57,7 +57,7 @@ test.describe("page tabs", () => { await page.goto(versionRoute); await expect(page).toHaveURL(/sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC/); - await page.getByTestId(versions.changes.btn).click(); + await page.getByTestId(versions.changes.button).click(); await expect(page).toHaveURL( `${versions.changes.route}?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC`, ); @@ -76,9 +76,9 @@ test.describe("page tabs", () => { authenticatedPage: page, }) => { await page.goto(versionRoute); - await page.getByTestId(versions.changes.btn).click(); + await page.getByTestId(versions.changes.button).click(); await expect(page.getByTestId("code-changes")).toBeVisible(); - await page.getByTestId(versions.tasks.btn).click(); + await page.getByTestId(versions.tasks.button).click(); await expect(page.getByTestId("total-count")).toBeVisible(); }); }); diff --git a/apps/spruce/playwright/tests/version/restart_modal.spec.ts b/apps/spruce/playwright/tests/version/restart_modal.spec.ts index d8d4d8d5f3..d644e005ee 100644 --- a/apps/spruce/playwright/tests/version/restart_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/restart_modal.spec.ts @@ -132,10 +132,9 @@ test.describe("version/restart_modal", () => { const taskCheckbox = modal.getByText("check_codegen"); await taskCheckbox.click(); - await expect( - modal.getByRole("button", { name: "Restart" }), - ).toHaveAttribute("aria-disabled", "false"); - await modal.getByRole("button", { name: "Restart" }).click(); + const restartButton = modal.getByRole("button", { name: "Restart" }); + await expect(restartButton).toHaveAttribute("aria-disabled", "false"); + await restartButton.click(); await validateToast(page, "success", "Successfully restarted tasks!"); }); }); diff --git a/apps/spruce/playwright/tests/version/schedule_modal.spec.ts b/apps/spruce/playwright/tests/version/schedule_modal.spec.ts index 7c43f1abcd..0024ac824f 100644 --- a/apps/spruce/playwright/tests/version/schedule_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/schedule_modal.spec.ts @@ -6,22 +6,21 @@ test.describe("Restarting and scheduling mainline commits", () => { authenticatedPage: page, }) => { await page.goto("/version/spruce_e695f654c8b4b959d3e12e71696c3e318bcd4c33"); - await expect(page.getByTestId("schedule-patch")).toBeVisible(); - await expect(page.getByTestId("schedule-patch")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByTestId("schedule-patch").click(); - await expect(page.getByTestId("schedule-tasks-modal")).toBeVisible(); + + const schedulePatchButton = page.getByTestId("schedule-patch"); + await expect(schedulePatchButton).toBeVisible(); + await expect(schedulePatchButton).toBeEnabled(); + await schedulePatchButton.click(); const modal = page.getByTestId("schedule-tasks-modal"); + await expect(modal).toBeVisible(); await modal.getByTestId("accordion-toggle").click(); const taskCheckbox = modal.getByText("check_codegen"); await taskCheckbox.click(); - await expect( - modal.getByRole("button", { name: "Schedule" }), - ).toHaveAttribute("aria-disabled", "false"); - await modal.getByRole("button", { name: "Schedule" }).click(); + + const scheduleButton = modal.getByRole("button", { name: "Schedule" }); + await expect(scheduleButton).toHaveAttribute("aria-disabled", "false"); + await scheduleButton.click(); await validateToast(page, "success", "Successfully scheduled tasks!"); }); }); From af0ee2781bffd78811a620b3c5ccdee5c1dd4c03 Mon Sep 17 00:00:00 2001 From: minnakt Date: Thu, 23 Apr 2026 14:47:06 -0400 Subject: [PATCH 04/32] migrate `preferences` tests --- .../integration/preferences/notifications.ts | 92 --------- .../preferences/public_key_management.ts | 140 -------------- .../preferences/sage_bot_settings.ts | 103 ---------- .../preferences/user_preferences.ts | 64 ------- .../tests/patch/configure_patch.spec.ts | 22 ++- .../tests/preferences/notifications.spec.ts | 104 +++++++++++ .../preferences/public_key_management.spec.ts | 176 ++++++++++++++++++ .../preferences/sage_bot_settings.spec.ts | 117 ++++++++++++ .../preferences/user_preferences.spec.ts | 62 ++++++ .../src/components/Header/UserDropdown.tsx | 1 + packages/playwright-config/helpers/index.ts | 12 +- 11 files changed, 479 insertions(+), 414 deletions(-) delete mode 100644 apps/spruce/cypress/integration/preferences/notifications.ts delete mode 100644 apps/spruce/cypress/integration/preferences/public_key_management.ts delete mode 100644 apps/spruce/cypress/integration/preferences/sage_bot_settings.ts delete mode 100644 apps/spruce/cypress/integration/preferences/user_preferences.ts create mode 100644 apps/spruce/playwright/tests/preferences/notifications.spec.ts create mode 100644 apps/spruce/playwright/tests/preferences/public_key_management.spec.ts create mode 100644 apps/spruce/playwright/tests/preferences/sage_bot_settings.spec.ts create mode 100644 apps/spruce/playwright/tests/preferences/user_preferences.spec.ts diff --git a/apps/spruce/cypress/integration/preferences/notifications.ts b/apps/spruce/cypress/integration/preferences/notifications.ts deleted file mode 100644 index e52e1679a0..0000000000 --- a/apps/spruce/cypress/integration/preferences/notifications.ts +++ /dev/null @@ -1,92 +0,0 @@ -const pageRoute = "/preferences/notifications"; -describe("preferences/notifications", () => { - describe("global subscription settings", () => { - it("updating a field should enable the submit button", () => { - cy.visit(pageRoute); - cy.dataCy("save-profile-changes-button").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("slack-member-id-field").clear(); - cy.dataCy("slack-member-id-field").type("12345"); - cy.dataCy("save-profile-changes-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - }); - it("saving changes to a field should work", () => { - cy.visit(pageRoute); - cy.dataCy("slack-username-field").clear(); - cy.dataCy("slack-username-field").type("slack.user"); - cy.dataCy("save-profile-changes-button").click(); - cy.validateToast("success", "Your changes have been saved."); - }); - }); - - describe("user subscriptions table", () => { - beforeEach(() => { - cy.visit(pageRoute); - }); - - it("shows all of a user's subscriptions and expands with details", () => { - cy.dataCy("subscription-row").should("have.length", 3); - - cy.dataCy("regex-selectors").should("not.exist"); - cy.dataCy("trigger-data").should("not.exist"); - - cy.dataCy("subscription-row") - .eq(0) - .within(() => { - cy.get("button[aria-label='Expand row']").click(); - }); - cy.dataCy("regex-selectors").should("be.visible"); - cy.dataCy("trigger-data").should("not.exist"); - cy.dataCy("subscription-row") - .eq(2) - .within(() => { - cy.get("button[aria-label='Expand row']").click(); - }); - cy.dataCy("regex-selectors").should("be.visible"); - cy.dataCy("trigger-data").should("be.visible"); - }); - - it("Shows the selected count in the 'Delete' button", () => { - cy.dataCy("subscription-row") - .eq(0) - .within(() => { - cy.get("input[type=checkbox]").check({ force: true }); - }); - cy.dataCy("delete-some-button").contains("Delete (1)"); - - cy.get("thead").within(() => { - cy.get("input[type=checkbox]").check({ force: true }); - }); - cy.dataCy("delete-some-button").contains("Delete (3)"); - - cy.get("thead").within(() => { - cy.get("input[type=checkbox]").uncheck({ force: true }); - }); - cy.dataCy("delete-some-button").contains("Delete"); - cy.dataCy("delete-some-button").should( - "have.attr", - "aria-disabled", - "true", - ); - }); - - describe("Deleting subscriptions", () => { - it("Deletes a single subscription", () => { - cy.dataCy("subscription-row") - .eq(0) - .within(() => { - cy.get("input[type=checkbox]").check({ force: true }); - }); - cy.dataCy("delete-some-button").click(); - cy.validateToast("success", "Deleted 1 subscription."); - cy.dataCy("subscription-row").should("have.length", 2); - }); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/preferences/public_key_management.ts b/apps/spruce/cypress/integration/preferences/public_key_management.ts deleted file mode 100644 index bcc06cf345..0000000000 --- a/apps/spruce/cypress/integration/preferences/public_key_management.ts +++ /dev/null @@ -1,140 +0,0 @@ -describe("Public Key Management Page", () => { - const route = "/preferences/publickeys"; - beforeEach(() => { - cy.visit(route); - }); - describe("Public keys list", () => { - it("Displays the user's public keys", () => { - cy.dataCy("table-key-name").each(($el, index) => - cy.wrap($el).contains([keyName1, keyName2][index]), - ); - }); - it("Removes a public key from the table after deletion", () => { - cy.dataCy("delete-btn").first().click(); - cy.contains("button", "Yes").click(); - cy.dataCy("table-key-name").should("have.length", 1); - cy.dataCy("table-key-name").first().should("not.contain", keyName1); - cy.dataCy("table-key-name").first().contains(keyName2); - }); - it("Displays empty message", () => { - cy.dataCy("delete-btn").first().click(); - cy.contains("button", "Yes").click(); - cy.dataCy("table-key-name").should("have.length", 1); - cy.dataCy("delete-btn").first().click(); - cy.contains("button", "Yes").click(); - cy.contains("No keys saved."); - }); - }); - - describe("Add New Key Modal", () => { - beforeEach(() => { - cy.contains(keyName2).should("be.visible"); - cy.dataCy("add-key-button").click(); - }); - - it("Displays errors when the modal opens", () => { - cy.contains(invalidSSHPublicKeyError).should("be.visible"); - }); - - it("Error messages go away after typing valid input", () => { - cy.dataCy("key-name-input").type(keyName3); - cy.dataCy("key-value-input").type("ssh-dss someHash", { delay: 0 }); - cy.contains(invalidSSHPublicKeyError).should("not.exist"); - }); - - it("Should include the public in the public key list after adding", () => { - cy.dataCy("key-name-input").clear(); - cy.dataCy("key-name-input").type(keyName3); - cy.dataCy("key-value-input").clear(); - cy.dataCy("key-value-input").type(pubKey, { delay: 0 }); - cy.contains("button", "Save").click(); - cy.dataCy("table-key-name").eq(1).contains(keyName3); - }); - - it("Should show an error if the key name already exists", () => { - cy.dataCy("key-name-input").clear(); - cy.dataCy("key-name-input").type(keyName2, { delay: 0 }); - cy.contains(duplicateKeyError); - }); - - it("Modal has correct title", () => { - cy.contains("Add Public Key").should("be.visible"); - }); - }); - - describe("Edit Key Modal", () => { - beforeEach(() => { - cy.visit(route); - cy.dataCy("edit-btn").first().click(); - cy.dataCy("key-edit-modal").should("be.visible"); - }); - it("Should not have any errors when the modal opens", () => { - cy.dataCy("error-message").should("have.length", 0); - }); - - it("After submitting, the key name and key value are updated", () => { - cy.dataCy("key-name-input").clear(); - cy.dataCy("key-name-input").type(keyName4); - - cy.dataCy("key-value-input").clear(); - cy.dataCy("key-value-input").type(pubKey2, { delay: 0 }); - cy.contains("button", "Save").click(); - cy.dataCy("key-edit-modal").should("not.be.visible"); - cy.validateToast("success", "Updated public key."); - cy.dataCy("table-key-name").eq(1).contains(keyName4); - cy.dataCy("edit-btn").eq(1).click(); - cy.dataCy("key-name-input").should("have.value", keyName4); - cy.dataCy("key-value-input").should("have.value", pubKey2); - cy.dataCy("key-value-input").clear(); - cy.dataCy("key-value-input").type(pubKey3, { delay: 0 }); - cy.contains("button", "Save").click(); - cy.dataCy("key-edit-modal").should("not.be.visible"); - cy.validateToast("success", "Updated public key."); - cy.dataCy("table-key-name").eq(1).contains(keyName4); - cy.dataCy("edit-btn").eq(1).click(); - cy.dataCy("key-name-input").should("have.value", keyName4); - cy.dataCy("key-value-input").should("have.value", pubKey3); - cy.dataCy("key-value-input").clear(); - cy.dataCy("key-value-input").type(pubKey4, { delay: 0 }); - cy.contains("button", "Save").click(); - cy.dataCy("key-edit-modal").should("not.be.visible"); - cy.validateToast("success", "Updated public key."); - cy.dataCy("table-key-name").eq(1).contains(keyName4); - cy.dataCy("edit-btn").eq(1).click(); - cy.dataCy("key-name-input").should("have.value", keyName4); - cy.dataCy("key-value-input").should("have.value", pubKey4); - }); - - it("Modal has correct title", () => { - cy.contains("Update Public Key"); - }); - }); - - describe("Error State", () => { - it("should show an error toast after submitting an invalid public key", () => { - cy.visit(route); - cy.dataCy("add-key-button").click(); - cy.dataCy("key-name-input").type("rsioeantarsn"); - cy.dataCy("key-value-input").type("ssh-rsa ", { delay: 0 }); - cy.contains("button", "Save").click(); - cy.validateToast("error", "There was an error creating the public key"); - }); - }); -}); - -const keyName1 = - "a loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong name"; -const keyName2 = "bKey"; -const keyName3 = "a unique key name"; -const keyName4 = "stuff!"; - -const invalidSSHPublicKeyError = "Value should be a valid SSH public key."; -const duplicateKeyError = "Duplicate key names are not allowed."; -const pubKey = - "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local"; -const pubKey2 = - "ssh-dss AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local"; -const pubKey3 = - "ssh-ed25519 AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local"; -const pubKey4 = - "ecdsa-sha2-nistp256 AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local"; diff --git a/apps/spruce/cypress/integration/preferences/sage_bot_settings.ts b/apps/spruce/cypress/integration/preferences/sage_bot_settings.ts deleted file mode 100644 index ebb7215b58..0000000000 --- a/apps/spruce/cypress/integration/preferences/sage_bot_settings.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { hasOperationName } from "../../utils/graphql-test-utils"; - -const GQL_PATH = "**/graphql/query"; - -interface MockState { - keyConfigured: boolean; - keyLastFour: string; -} - -const setupSageMocks = (initialState: MockState) => { - const state = { ...initialState }; - - cy.intercept("POST", GQL_PATH, (req) => { - if (hasOperationName(req, "CursorSettings")) { - req.reply({ - data: { - cursorSettings: { - __typename: "CursorSettings", - keyConfigured: state.keyConfigured, - keyLastFour: state.keyConfigured ? state.keyLastFour : "", - }, - }, - }); - } else if (hasOperationName(req, "SetCursorAPIKey")) { - state.keyConfigured = true; - state.keyLastFour = "2345"; - req.reply({ - data: { - setCursorAPIKey: { - __typename: "SetCursorAPIKeyPayload", - keyLastFour: state.keyLastFour, - success: true, - }, - }, - }); - } else if (hasOperationName(req, "DeleteCursorAPIKey")) { - state.keyConfigured = false; - state.keyLastFour = ""; - req.reply({ - data: { - deleteCursorAPIKey: { - __typename: "DeleteCursorAPIKeyPayload", - success: true, - }, - }, - }); - } - }); -}; - -describe("Sage Bot Settings", () => { - const route = "/preferences/sage-bot-settings"; - - it("should navigate to Sage Bot Settings from sidebar and display the tab", () => { - setupSageMocks({ keyConfigured: false, keyLastFour: "" }); - cy.visit("/preferences/profile"); - cy.dataCy("sage-bot-settings-nav-tab").click(); - cy.url().should("include", "/preferences/sage-bot-settings"); - cy.dataCy("cursor-api-key-card").should("be.visible"); - cy.contains("Cursor API Key").should("be.visible"); - }); - - it("should have a disabled save button when input is empty", () => { - setupSageMocks({ keyConfigured: false, keyLastFour: "" }); - cy.visit(route); - cy.dataCy("save-cursor-api-key-button").should( - "have.attr", - "aria-disabled", - "true", - ); - }); - - it("should enable save button when API key is entered", () => { - setupSageMocks({ keyConfigured: false, keyLastFour: "" }); - cy.visit(route); - cy.dataCy("cursor-api-key-input").type("test-api-key-12345"); - cy.dataCy("save-cursor-api-key-button").should( - "have.attr", - "aria-disabled", - "false", - ); - }); - - it("should save cursor API key and show success toast", () => { - setupSageMocks({ keyConfigured: false, keyLastFour: "" }); - cy.visit(route); - cy.dataCy("cursor-api-key-input").type("test-api-key-12345"); - cy.dataCy("save-cursor-api-key-button").click(); - cy.validateToast("success", "Cursor API key saved successfully"); - - // After saving, the key status should show the last 4 characters - cy.dataCy("cursor-key-status").should("be.visible"); - cy.dataCy("delete-cursor-api-key-button").should("be.visible"); - }); - - it("should delete cursor API key and show success toast", () => { - setupSageMocks({ keyConfigured: true, keyLastFour: "2345" }); - cy.visit(route); - cy.dataCy("delete-cursor-api-key-button").click(); - cy.validateToast("success", "Cursor API key deleted successfully"); - cy.dataCy("delete-cursor-api-key-button").should("not.exist"); - }); -}); diff --git a/apps/spruce/cypress/integration/preferences/user_preferences.ts b/apps/spruce/cypress/integration/preferences/user_preferences.ts deleted file mode 100644 index 0707424ea3..0000000000 --- a/apps/spruce/cypress/integration/preferences/user_preferences.ts +++ /dev/null @@ -1,64 +0,0 @@ -const baseRoute = "/preferences"; -const tabNames = { - profile: "/profile", - cli: "/cli", - uiSettings: "/ui-settings", -}; -describe("user preferences pages", () => { - it("visiting /preferences should redirect to the profile tab", () => { - cy.visit(baseRoute); - cy.url().should("include", `${baseRoute}${tabNames.profile}`); - }); - it("should be able to navigate between tabs using the side nav", () => { - cy.visit(baseRoute); - cy.dataCy("preferences-tab-title").should("have.text", "Profile"); - cy.dataCy("notifications-nav-tab").click(); - cy.dataCy("preferences-tab-title").should("have.text", "Notifications"); - }); - it("should be able to reset Evergreen API key", () => { - const defaultApiKey = "abb623665fdbf368a1db980dde6ee0f0"; - cy.visit(`${baseRoute}${tabNames.cli}`); - cy.contains("Download the authentication file.").scrollIntoView(); - cy.contains(defaultApiKey).should("be.visible"); - cy.contains("button", "Reset key").click(); - cy.contains(defaultApiKey).should("not.exist"); - }); - - describe("ui settings", () => { - describe("beta features", () => { - it("should be able to edit beta features", () => {}); - }); - - describe("task review", () => { - beforeEach(async () => { - // Clear IndexedDB data to reset reviewed status between runs - if (window.indexedDB.databases) { - const dbs = await window.indexedDB.databases(); - dbs.forEach((db) => { - window.indexedDB.deleteDatabase(db.name); - }); - } - }); - - it("disabling task review should hide review button on a task page", () => { - const failedTaskRoute = - "/task/evergreen_ubuntu1604_test_service_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48"; - - cy.visit(failedTaskRoute); - cy.contains("button", "Mark reviewed").click(); - cy.contains("button", "Mark unreviewed").should("be.visible"); - - cy.dataCy("user-dropdown-link").click(); - cy.contains("a", "UI Settings").click(); - cy.getInputByLabel("Task review").click(); - cy.getInputByLabel("Task review").should( - "have.attr", - "aria-checked", - "false", - ); - cy.go("back"); - cy.contains("button", "Mark unreviewed").should("not.exist"); - }); - }); - }); -}); diff --git a/apps/spruce/playwright/tests/patch/configure_patch.spec.ts b/apps/spruce/playwright/tests/patch/configure_patch.spec.ts index 6ef7c76188..d8b9134312 100644 --- a/apps/spruce/playwright/tests/patch/configure_patch.spec.ts +++ b/apps/spruce/playwright/tests/patch/configure_patch.spec.ts @@ -39,10 +39,9 @@ test.describe("Configure Patch Page", () => { authenticatedPage: page, }) => { await page.goto(`/patch/${unactivatedPatchId}/configure`); - await expect( - page.locator('button[data-cy="tasks-tab"]'), - ).toHaveAttribute("aria-selected", "true"); - await expect(page.getByTestId("tasks-tab")).toBeVisible(); + const tasksTab = page.getByTestId("tasks-tab"); + await expect(tasksTab).toHaveAttribute("aria-selected", "true"); + await expect(tasksTab).toBeVisible(); }); }); @@ -74,15 +73,18 @@ test.describe("Configure Patch Page", () => { test("Should be able to switch between tabs", async ({ authenticatedPage: page, }) => { - await page.locator('button[data-cy="changes-tab"]').click(); + const changesTab = page.getByTestId("changes-tab"); + await changesTab.click(); await expect(page).toHaveURL( `/patch/${unactivatedPatchId}/configure/changes`, ); - await page.locator('button[data-cy="parameters-tab"]').click(); + const parametersTab = page.getByTestId("parameters-tab"); + await parametersTab.click(); await expect(page).toHaveURL( `/patch/${unactivatedPatchId}/configure/parameters`, ); - await page.locator('button[data-cy="tasks-tab"]').click(); + const tasksTab = page.getByTestId("tasks-tab"); + await tasksTab.click(); await expect(page).toHaveURL( `/patch/${unactivatedPatchId}/configure/tasks`, ); @@ -91,7 +93,7 @@ test.describe("Configure Patch Page", () => { test("Navigating away from the configure tab should disable the build variant selector", async ({ authenticatedPage: page, }) => { - await page.locator('button[data-cy="changes-tab"]').click(); + await page.getByTestId("changes-tab").click(); await expect( page.getByTestId("build-variant-select-wrapper"), ).toHaveAttribute("disabled", ""); @@ -108,7 +110,7 @@ test.describe("Configure Patch Page", () => { authenticatedPage: page, }) => { await page.goto(`patch/${unactivatedPatchId}/configure/tasks`); - await page.locator('button[data-cy="parameters-tab"]').click(); + await page.getByTestId("parameters-tab").click(); await page.getByTestId("add-tag-button").click(); await page.getByTestId("user-tag-key-field").fill("testKey"); await page.getByTestId("user-tag-value-field").fill("testValue"); @@ -122,7 +124,7 @@ test.describe("Configure Patch Page", () => { authenticatedPage: page, }) => { await page.goto("patch/5ecedafb562343215a7ff297/configure/tasks"); - await page.locator('button[data-cy="parameters-tab"]').click(); + await page.getByTestId("parameters-tab").click(); await expect(page.getByTestId("add-tag-button")).toBeHidden(); await expect(page.getByTestId("parameters-disclaimer")).toBeVisible(); await expect( diff --git a/apps/spruce/playwright/tests/preferences/notifications.spec.ts b/apps/spruce/playwright/tests/preferences/notifications.spec.ts new file mode 100644 index 0000000000..eddb682365 --- /dev/null +++ b/apps/spruce/playwright/tests/preferences/notifications.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; + +const pageRoute = "/preferences/notifications"; + +test.describe("preferences/notifications", () => { + test.describe("global subscription settings", () => { + test("updating a field should enable the submit button", async ({ + authenticatedPage: page, + }) => { + await page.goto(pageRoute); + await expect( + page.getByTestId("save-profile-changes-button"), + ).toHaveAttribute("aria-disabled", "true"); + await page.getByTestId("slack-member-id-field").clear(); + await page.getByTestId("slack-member-id-field").fill("12345"); + await expect( + page.getByTestId("save-profile-changes-button"), + ).not.toHaveAttribute("aria-disabled", "true"); + }); + + test("saving changes to a field should work", async ({ + authenticatedPage: page, + }) => { + await page.goto(pageRoute); + await page.getByTestId("slack-username-field").clear(); + await page.getByTestId("slack-username-field").fill("slack.user"); + await page.getByTestId("save-profile-changes-button").click(); + await validateToast(page, "success", "Your changes have been saved."); + }); + }); + + test.describe("user subscriptions table", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(pageRoute); + }); + + test("shows all of a user's subscriptions and expands with details", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("subscription-row")).toHaveCount(3); + + await expect(page.getByTestId("regex-selectors")).toHaveCount(0); + await expect(page.getByTestId("trigger-data")).toHaveCount(0); + + await page + .getByTestId("subscription-row") + .nth(0) + .getByRole("button", { name: "Expand row" }) + .click(); + await expect(page.getByTestId("regex-selectors")).toBeVisible(); + await expect(page.getByTestId("trigger-data")).toHaveCount(0); + + await page + .getByTestId("subscription-row") + .nth(2) + .getByRole("button", { name: "Expand row" }) + .click(); + await expect(page.getByTestId("regex-selectors")).toBeVisible(); + await expect(page.getByTestId("trigger-data")).toBeVisible(); + }); + + test("shows the selected count in the 'Delete' button", async ({ + authenticatedPage: page, + }) => { + const rowCheckbox = page + .getByTestId("subscription-row") + .nth(0) + .locator("input[type=checkbox]"); + const rowCheckboxId = await rowCheckbox.getAttribute("id"); + await page.locator(`label[for="${rowCheckboxId}"]`).click(); + + const deleteButton = page.getByTestId("delete-some-button"); + await expect(deleteButton).toContainText("Delete (1)"); + + const headerCheckbox = page + .locator("thead") + .locator("input[type=checkbox]"); + const headerCheckboxId = await headerCheckbox.getAttribute("id"); + await page.locator(`label[for="${headerCheckboxId}"]`).click(); + await expect(deleteButton).toContainText("Delete (3)"); + + await page.locator(`label[for="${headerCheckboxId}"]`).click(); + await expect(deleteButton).toContainText("Delete"); + await expect(deleteButton).toHaveAttribute("aria-disabled", "true"); + }); + + test.describe("Deleting subscriptions", () => { + test("deletes a single subscription", async ({ + authenticatedPage: page, + }) => { + const rowCheckbox = page + .getByTestId("subscription-row") + .nth(0) + .locator("input[type=checkbox]"); + const rowCheckboxId = await rowCheckbox.getAttribute("id"); + await page.locator(`label[for="${rowCheckboxId}"]`).click(); + await page.getByTestId("delete-some-button").click(); + await validateToast(page, "success", "Deleted 1 subscription."); + await expect(page.getByTestId("subscription-row")).toHaveCount(2); + }); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/preferences/public_key_management.spec.ts b/apps/spruce/playwright/tests/preferences/public_key_management.spec.ts new file mode 100644 index 0000000000..c391d934fc --- /dev/null +++ b/apps/spruce/playwright/tests/preferences/public_key_management.spec.ts @@ -0,0 +1,176 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; + +const route = "/preferences/publickeys"; + +const keyName1 = + "a loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong name"; +const keyName2 = "bKey"; +const keyName3 = "a unique key name"; +const keyName4 = "stuff!"; + +const invalidSSHPublicKeyError = "Value should be a valid SSH public key."; +const duplicateKeyError = "Duplicate key names are not allowed."; +const pubKey = + "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local"; +const pubKey2 = + "ssh-dss AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local"; +const pubKey3 = + "ssh-ed25519 AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local"; +const pubKey4 = + "ecdsa-sha2-nistp256 AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local"; + +test.describe("Public Key Management Page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(route); + }); + + test.describe("Public keys list", () => { + test("displays the user's public keys", async ({ + authenticatedPage: page, + }) => { + const keyNames = page.getByTestId("table-key-name"); + await expect(keyNames.nth(0)).toContainText(keyName1); + await expect(keyNames.nth(1)).toContainText(keyName2); + }); + + test("removes a public key from the table after deletion", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("delete-btn").first().click(); + await page.getByTestId("popconfirm-confirm-button").click(); + await expect(page.getByTestId("table-key-name")).toHaveCount(1); + await expect( + page.getByTestId("table-key-name").first(), + ).not.toContainText(keyName1); + await expect(page.getByTestId("table-key-name").first()).toContainText( + keyName2, + ); + }); + + test("displays empty message", async ({ authenticatedPage: page }) => { + await page.getByTestId("delete-btn").first().click(); + await page.getByTestId("popconfirm-confirm-button").click(); + await expect(page.getByTestId("table-key-name")).toHaveCount(1); + await page.getByTestId("delete-btn").first().click(); + await page.getByTestId("popconfirm-confirm-button").click(); + await expect(page.getByText("No keys saved.")).toBeVisible(); + }); + }); + + test.describe("Add New Key Modal", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await expect(page.getByText(keyName2)).toBeVisible(); + await page.getByTestId("add-key-button").click(); + }); + + test("displays errors when the modal opens", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByText(invalidSSHPublicKeyError)).toBeVisible(); + }); + + test("error messages go away after typing valid input", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("key-name-input").fill(keyName3); + await page.getByTestId("key-value-input").fill("ssh-dss someHash"); + await expect(page.getByText(invalidSSHPublicKeyError)).toHaveCount(0); + }); + + test("should include the public key in the list after adding", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("key-name-input").clear(); + await page.getByTestId("key-name-input").fill(keyName3); + await page.getByTestId("key-value-input").clear(); + await page.getByTestId("key-value-input").fill(pubKey); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByTestId("table-key-name").nth(1)).toContainText( + keyName3, + ); + }); + + test("should show an error if the key name already exists", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("key-name-input").clear(); + await page.getByTestId("key-name-input").fill(keyName2); + await expect(page.getByText(duplicateKeyError)).toBeVisible(); + }); + + test("modal has correct title", async ({ authenticatedPage: page }) => { + await expect(page.getByText("Add Public Key")).toBeVisible(); + }); + }); + + test.describe("Edit Key Modal", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(route); + await page.getByTestId("edit-btn").first().click(); + await expect(page.getByText("Update Public Key")).toBeVisible(); + }); + + test("should not have any errors when the modal opens", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("error-message")).toHaveCount(0); + }); + + test("after submitting, the key name and key value are updated", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("key-name-input").clear(); + await page.getByTestId("key-name-input").fill(keyName4); + await page.getByTestId("key-value-input").clear(); + await page.getByTestId("key-value-input").fill(pubKey2); + await page.getByRole("button", { name: "Save" }).click(); + await validateToast(page, "success", "Updated public key.", true); + await expect(page.getByTestId("table-key-name").nth(1)).toContainText( + keyName4, + ); + + await page.getByTestId("edit-btn").nth(1).click(); + await expect(page.getByTestId("key-name-input")).toHaveValue(keyName4); + await expect(page.getByTestId("key-value-input")).toHaveValue(pubKey2); + await page.getByTestId("key-value-input").clear(); + await page.getByTestId("key-value-input").fill(pubKey3); + await page.getByRole("button", { name: "Save" }).click(); + await validateToast(page, "success", "Updated public key.", true); + await expect(page.getByTestId("table-key-name").nth(1)).toContainText( + keyName4, + ); + + await page.getByTestId("edit-btn").nth(1).click(); + await expect(page.getByTestId("key-name-input")).toHaveValue(keyName4); + await expect(page.getByTestId("key-value-input")).toHaveValue(pubKey3); + await page.getByTestId("key-value-input").clear(); + await page.getByTestId("key-value-input").fill(pubKey4); + await page.getByRole("button", { name: "Save" }).click(); + await validateToast(page, "success", "Updated public key.", true); + await expect(page.getByTestId("table-key-name").nth(1)).toContainText( + keyName4, + ); + + await page.getByTestId("edit-btn").nth(1).click(); + await expect(page.getByTestId("key-name-input")).toHaveValue(keyName4); + await expect(page.getByTestId("key-value-input")).toHaveValue(pubKey4); + }); + }); + + test.describe("Error State", () => { + test("should show an error toast after submitting an invalid public key", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("add-key-button").click(); + await page.getByTestId("key-name-input").fill("rsioeantarsn"); + await page.getByTestId("key-value-input").fill("ssh-rsa "); + await page.getByRole("button", { name: "Save" }).click(); + await validateToast( + page, + "error", + "There was an error creating the public key", + ); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/preferences/sage_bot_settings.spec.ts b/apps/spruce/playwright/tests/preferences/sage_bot_settings.spec.ts new file mode 100644 index 0000000000..2e0aa6ab10 --- /dev/null +++ b/apps/spruce/playwright/tests/preferences/sage_bot_settings.spec.ts @@ -0,0 +1,117 @@ +import { Page } from "@playwright/test"; +import { test, expect } from "../../fixtures"; +import { mockGraphQLResponse, validateToast } from "../../helpers"; + +const route = "/preferences/sage-bot-settings"; + +const setupSageMocks = async ( + page: Page, + initialState: { keyConfigured: boolean; keyLastFour: string }, +) => { + const state = { ...initialState }; + + await mockGraphQLResponse(page, "CursorSettings", () => ({ + data: { + cursorSettings: { + __typename: "CursorSettings", + keyConfigured: state.keyConfigured, + keyLastFour: state.keyConfigured ? state.keyLastFour : "", + }, + }, + errors: null, + })); + + await mockGraphQLResponse(page, "SetCursorAPIKey", () => { + state.keyConfigured = true; + state.keyLastFour = "2345"; + return { + data: { + setCursorAPIKey: { + __typename: "SetCursorAPIKeyPayload", + keyLastFour: state.keyLastFour, + success: true, + }, + }, + errors: null, + }; + }); + + await mockGraphQLResponse(page, "DeleteCursorAPIKey", () => { + state.keyConfigured = false; + state.keyLastFour = ""; + return { + data: { + deleteCursorAPIKey: { + __typename: "DeleteCursorAPIKeyPayload", + success: true, + }, + }, + errors: null, + }; + }); +}; + +test.describe("Sage Bot Settings", () => { + test("should navigate to Sage Bot Settings from sidebar and display the tab", async ({ + authenticatedPage: page, + }) => { + await setupSageMocks(page, { keyConfigured: false, keyLastFour: "" }); + await page.goto("/preferences/profile"); + await page.getByTestId("sage-bot-settings-nav-tab").click(); + await expect(page).toHaveURL(/\/preferences\/sage-bot-settings/); + await expect(page.getByTestId("cursor-api-key-card")).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Cursor API Key" }), + ).toBeVisible(); + }); + + test("should have a disabled save button when input is empty", async ({ + authenticatedPage: page, + }) => { + await setupSageMocks(page, { keyConfigured: false, keyLastFour: "" }); + await page.goto(route); + await expect( + page.getByTestId("save-cursor-api-key-button"), + ).toHaveAttribute("aria-disabled", "true"); + }); + + test("should enable save button when API key is entered", async ({ + authenticatedPage: page, + }) => { + await setupSageMocks(page, { keyConfigured: false, keyLastFour: "" }); + await page.goto(route); + await page.getByTestId("cursor-api-key-input").fill("test-api-key-12345"); + await expect( + page.getByTestId("save-cursor-api-key-button"), + ).toHaveAttribute("aria-disabled", "false"); + }); + + test("should save cursor API key and show success toast", async ({ + authenticatedPage: page, + }) => { + await setupSageMocks(page, { keyConfigured: false, keyLastFour: "" }); + await page.goto(route); + await page.getByTestId("cursor-api-key-input").fill("test-api-key-12345"); + await page.getByTestId("save-cursor-api-key-button").click(); + await validateToast(page, "success", "Cursor API key saved successfully"); + await expect(page.getByTestId("cursor-key-status")).toBeVisible(); + await expect( + page.getByTestId("delete-cursor-api-key-button"), + ).toBeVisible(); + }); + + test("should delete cursor API key and show success toast", async ({ + authenticatedPage: page, + }) => { + await setupSageMocks(page, { keyConfigured: true, keyLastFour: "2345" }); + await page.goto(route); + await expect( + page.getByTestId("delete-cursor-api-key-button"), + ).toBeVisible(); + await page.getByTestId("delete-cursor-api-key-button").click(); + await validateToast(page, "success", "Cursor API key deleted successfully"); + await expect(page.getByTestId("delete-cursor-api-key-button")).toHaveCount( + 0, + ); + }); +}); diff --git a/apps/spruce/playwright/tests/preferences/user_preferences.spec.ts b/apps/spruce/playwright/tests/preferences/user_preferences.spec.ts new file mode 100644 index 0000000000..213dbb9f24 --- /dev/null +++ b/apps/spruce/playwright/tests/preferences/user_preferences.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from "../../fixtures"; + +const baseRoute = "/preferences"; + +test.describe("user preferences pages", () => { + test("visiting /preferences should redirect to the profile tab", async ({ + authenticatedPage: page, + }) => { + await page.goto(baseRoute); + await expect(page).toHaveURL(`${baseRoute}/profile`); + }); + + test("should be able to navigate between tabs using the side nav", async ({ + authenticatedPage: page, + }) => { + await page.goto(baseRoute); + await expect(page.getByTestId("preferences-tab-title")).toHaveText( + "Profile", + ); + await page.getByTestId("notifications-nav-tab").click(); + await expect(page.getByTestId("preferences-tab-title")).toHaveText( + "Notifications", + ); + }); + + test("should be able to reset Evergreen API key", async ({ + authenticatedPage: page, + }) => { + const defaultApiKey = "abb623665fdbf368a1db980dde6ee0f0"; + await page.goto(`${baseRoute}/cli`); + await expect(page.getByText(defaultApiKey)).toBeVisible(); + await page.getByRole("button", { name: "Reset key" }).click(); + await expect(page.getByText(defaultApiKey)).toHaveCount(0); + }); + + test("disabling task review should hide review button on a task page", async ({ + authenticatedPage: page, + }) => { + const failedTaskRoute = + "/task/evergreen_ubuntu1604_test_service_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48"; + + await page.goto(failedTaskRoute); + + await page.getByRole("button", { name: "Mark reviewed" }).click(); + await expect( + page.getByRole("button", { name: "Mark unreviewed" }), + ).toBeVisible(); + + await page.getByTestId("user-dropdown-link").click(); + await page.getByTestId("ui-settings-link").click(); + + const toggle = page.getByLabel("Task review"); + await expect(toggle).toHaveAttribute("aria-checked", "true"); + await toggle.click(); + await expect(toggle).toHaveAttribute("aria-checked", "false"); + + await page.goBack(); + await expect( + page.getByRole("button", { name: "Mark unreviewed" }), + ).toHaveCount(0); + }); +}); diff --git a/apps/spruce/src/components/Header/UserDropdown.tsx b/apps/spruce/src/components/Header/UserDropdown.tsx index 560d6a15f5..5d4ea68859 100644 --- a/apps/spruce/src/components/Header/UserDropdown.tsx +++ b/apps/spruce/src/components/Header/UserDropdown.tsx @@ -30,6 +30,7 @@ export const UserDropdown = () => { onClick: () => sendEvent({ name: "Clicked notifications link" }), }, { + "data-cy": "ui-settings-link", text: "UI Settings", to: getPreferencesRoute(PreferencesTabRoutes.UISettings), onClick: () => sendEvent({ name: "Clicked UI settings link" }), diff --git a/packages/playwright-config/helpers/index.ts b/packages/playwright-config/helpers/index.ts index d70bb458f7..8bb81f4a37 100644 --- a/packages/playwright-config/helpers/index.ts +++ b/packages/playwright-config/helpers/index.ts @@ -85,27 +85,29 @@ type ResponseData = { * Helper function to mock GraphQL response. * @param page - The Playwright page object * @param operationName - name of the operation to mock - * @param responseData - The mock response data + * @param responseData - The mock response data, or a function returning it (useful for stateful mocks) */ export async function mockGraphQLResponse( page: Page, operationName: string, - responseData: ResponseData, + responseData: ResponseData | (() => ResponseData), ) { await page.route("**/graphql/query", async (route) => { const request = route.request(); const postData = request.postDataJSON(); if (postData?.operationName === operationName) { + const data = + typeof responseData === "function" ? responseData() : responseData; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ - errors: responseData.errors, - data: responseData.data, + errors: data.errors, + data: data.data, }), }); } else { - await route.continue(); + await route.fallback(); } }); } From 73c71a9787b325836569a639c3cecaff26c9fb01 Mon Sep 17 00:00:00 2001 From: minnakt Date: Thu, 23 Apr 2026 16:21:15 -0400 Subject: [PATCH 05/32] migrate `distroSettings` tests --- .../distroSettings/general_section.ts | 113 ------ .../distroSettings/host_section.ts | 125 ------- .../integration/distroSettings/navigation.ts | 102 ------ .../integration/distroSettings/new_distro.ts | 71 ---- .../distroSettings/on_save_operations.ts | 26 -- .../integration/distroSettings/permissions.ts | 55 --- .../distroSettings/project_section.ts | 42 --- .../distroSettings/provider_section.ts | 283 --------------- .../distroSettings/single_task_distro.ts | 49 --- .../distroSettings/task_section.ts | 76 ----- .../integration/distroSettings/utils.ts | 15 - apps/spruce/playwright/helpers/index.ts | 34 +- .../distroSettings/general_section.spec.ts | 114 +++++++ .../tests/distroSettings/host_section.spec.ts | 140 ++++++++ .../tests/distroSettings/navigation.spec.ts | 116 +++++++ .../tests/distroSettings/new_distro.spec.ts | 90 +++++ .../distroSettings/on_save_operations.spec.ts | 29 ++ .../tests/distroSettings/permissions.spec.ts | 65 ++++ .../distroSettings/project_section.spec.ts | 40 +++ .../distroSettings/provider_section.spec.ts | 323 ++++++++++++++++++ .../distroSettings/single_task_distro.spec.ts | 31 ++ .../tests/distroSettings/task_section.spec.ts | 90 +++++ .../playwright/tests/distroSettings/utils.ts | 21 ++ .../tabs/HostTab/schemaFields.tsx | 1 + .../tabs/ProviderTab/schemaFields.ts | 1 + 25 files changed, 1069 insertions(+), 983 deletions(-) delete mode 100644 apps/spruce/cypress/integration/distroSettings/general_section.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/host_section.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/navigation.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/new_distro.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/on_save_operations.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/permissions.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/project_section.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/provider_section.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/single_task_distro.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/task_section.ts delete mode 100644 apps/spruce/cypress/integration/distroSettings/utils.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/general_section.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/host_section.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/navigation.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/on_save_operations.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/permissions.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/project_section.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/single_task_distro.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/task_section.spec.ts create mode 100644 apps/spruce/playwright/tests/distroSettings/utils.ts diff --git a/apps/spruce/cypress/integration/distroSettings/general_section.ts b/apps/spruce/cypress/integration/distroSettings/general_section.ts deleted file mode 100644 index 63c400eb76..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/general_section.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { save } from "./utils"; - -describe("general section", () => { - beforeEach(() => { - cy.visit("/distro/localhost/settings/general"); - cy.dataCy("distro-settings-page").should("exist"); - }); - - it("can update fields and those changes will persist", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Update fields. - cy.contains("button", "Add alias").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.contains("button", "Add alias").click(); - cy.getInputByLabel("Alias").type("localhost-alias"); - cy.getInputByLabel("Notes").type("this is a note"); - cy.getInputByLabel("Warnings").type("this is a warning"); - cy.getInputByLabel("Disable shallow clone for this distro").check({ - force: true, - }); - cy.getInputByLabel("Admin only").check({ force: true }); - save(); - cy.validateToast("success", "Updated distro."); - - // Changes should persist. - cy.reload(); - cy.dataCy("distro-settings-page").should("exist"); - cy.getInputByLabel("Alias").should("have.value", "localhost-alias"); - cy.getInputByLabel("Notes").should("have.value", "this is a note"); - cy.getInputByLabel("Warnings").should("have.value", "this is a warning"); - cy.getInputByLabel("Disable shallow clone for this distro").should( - "be.checked", - ); - cy.getInputByLabel("Admin only").should("be.checked"); - - // Undo changes. - cy.dataCy("delete-item-button").click(); - cy.getInputByLabel("Notes").clear(); - cy.getInputByLabel("Warnings").clear(); - cy.getInputByLabel("Disable shallow clone for this distro").uncheck({ - force: true, - }); - cy.getInputByLabel("Admin only").uncheck({ force: true }); - save(); - cy.validateToast("success", "Updated distro."); - }); - - describe("container pool distro", () => { - beforeEach(() => { - cy.visit("/distro/ubuntu1604-parent/settings/general"); - }); - - it("warns users that the distro will not be spawned for tasks", () => { - cy.contains( - "Distro is a container pool, so it cannot be spawned for tasks.", - ).should("be.visible"); - }); - }); - - describe("single task distro", () => { - beforeEach(() => { - cy.visit("/distro/localhost/settings/general"); - cy.dataCy("distro-settings-page").should("exist"); - }); - - it("can toggle a distro as single task distro and shows a warning banner that dismisses on save", () => { - cy.dataCy("single-task-banner").should("not.exist"); - cy.getInputByLabel("Set distro as Single Task Distro").scrollIntoView(); - cy.getInputByLabel("Set distro as Single Task Distro").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.getInputByLabel("Set distro as Single Task Distro").check({ - force: true, - }); - cy.dataCy("single-task-banner").contains( - "This Distro will be converted to a Single Task Distro once saved. Please review before confirming.", - ); - save(); - cy.dataCy("single-task-banner").should("not.exist"); - cy.validateToast("success", "Updated distro."); - cy.reload(); - cy.getInputByLabel("Set distro as Single Task Distro").should( - "be.checked", - ); - cy.dataCy("single-task-banner").should("not.exist"); - cy.getInputByLabel("Set distro as Single Task Distro").scrollIntoView(); - cy.getInputByLabel("Set distro as Single Task Distro").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.getInputByLabel("Set distro as Single Task Distro").uncheck({ - force: true, - }); - cy.dataCy("single-task-banner").contains( - "This Distro will no longer be a Single Task Distro once saved. Please review before confirming.", - ); - save(); - cy.dataCy("single-task-banner").should("not.exist"); - cy.validateToast("success", "Updated distro."); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/host_section.ts b/apps/spruce/cypress/integration/distroSettings/host_section.ts deleted file mode 100644 index 6dc0684d2e..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/host_section.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { save } from "./utils"; - -describe("host section", () => { - describe("using legacy ssh", () => { - beforeEach(() => { - cy.visit("/distro/localhost/settings/host"); - }); - - it("shows the correct fields when distro has static provider", () => { - cy.dataCy("authorized-keys-input").should("exist"); - cy.dataCy("minimum-hosts-input").should("not.exist"); - cy.dataCy("maximum-hosts-input").should("not.exist"); - cy.dataCy("idle-time-input").should("not.exist"); - cy.dataCy("future-fraction-input").should("not.exist"); - cy.dataCy("rounding-rule-select").should("not.exist"); - cy.dataCy("feedback-rule-select").should("not.exist"); - }); - - it("shows an error when selecting an incompatible host communication method", () => { - cy.selectLGOption("Host Communication Method", "RPC"); - cy.contains( - "Legacy and non-legacy bootstrapping and communication are incompatible.", - ); - }); - - it("updates host fields", () => { - cy.selectLGOption("Agent Architecture", "Linux ARM 64-bit"); - cy.getInputByLabel("Working Directory").clear(); - cy.getInputByLabel("Working Directory").type("/usr/local/bin"); - cy.getInputByLabel("SSH User").clear(); - cy.getInputByLabel("SSH User").type("sudo"); - cy.contains("button", "Add SSH option").click(); - cy.getInputByLabel("SSH Option").type("BatchMode=yes"); - cy.selectLGOption( - "Host Overallocation Rule", - "Terminate hosts when overallocated", - ); - - save(); - cy.validateToast("success", "Updated distro."); - - // Reset fields - cy.selectLGOption("Agent Architecture", "Linux 64-bit"); - cy.getInputByLabel("Working Directory").clear(); - cy.getInputByLabel("Working Directory").type("/home/ubuntu/smoke"); - cy.getInputByLabel("SSH User").clear(); - cy.getInputByLabel("SSH User").type("ubuntu"); - cy.dataCy("delete-item-button").click(); - cy.selectLGOption("Host Overallocation Rule", "Default"); - - save(); - cy.validateToast("success", "Updated distro."); - }); - - it("updates mountpoints", { retries: { runMode: 2 } }, () => { - cy.contains("button", "Add mountpoint").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.contains("button", "Add mountpoint").click(); - cy.getInputByLabel("Mountpoint").type("/data"); - - save(); - cy.validateToast("success", "Updated distro."); - - // Reset fields - cy.dataCy("delete-item-button").click(); - - save(); - cy.validateToast("success", "Updated distro."); - }); - }); - - describe("using User Data bootstrap method", () => { - beforeEach(() => { - cy.visit("/distro/ubuntu1604-parent/settings/host"); - cy.selectLGOption("Host Bootstrap Method", "User Data"); - cy.selectLGOption("Host Communication Method", "RPC"); - }); - - it("shows Windows-only fields when the architecture is updated", () => { - cy.contains("label", "Root Directory").should("not.exist"); - cy.contains("label", "Service User").should("not.exist"); - - cy.selectLGOption("Agent Architecture", "Windows 64-bit"); - - cy.getInputByLabel("Root Directory").should("exist"); - cy.getInputByLabel("Service User").should("exist"); - }); - - it("hides resource limit fields when the architecture is not Linux", () => { - cy.contains("Resource Limits").should("exist"); - cy.selectLGOption("Agent Architecture", "Windows 64-bit"); - cy.contains("Resource Limits").should("not.exist"); - }); - - it("saves bootstrap settings", () => { - cy.getInputByLabel("Jasper Binary Directory").type("/jasper/binary"); - cy.getInputByLabel("Jasper Credentials Path").type("/jasper/credentials"); - cy.getInputByLabel("Client Directory").type("/client/dir"); - cy.getInputByLabel("Shell Path").type("/shell/path"); - cy.getInputByLabel("Home Volume Format Command").type( - "echo 'Hello World'", - ); - cy.getInputByLabel("Number of Files").type("10"); - cy.getInputByLabel("Number of CGroup Tasks").type("20"); - cy.getInputByLabel("Number of Processes").type("30"); - cy.getInputByLabel("Locked Memory").type("128"); - cy.getInputByLabel("Virtual Memory").type("256"); - - cy.contains("button", "Add variable").click(); - cy.getInputByLabel("Key").type("my-key"); - cy.getInputByLabel("Value").type("my-value"); - - cy.contains("button", "Add script").click(); - cy.getInputByLabel(/^Path$/).type("/path/to/precondition/script"); - cy.getInputByLabel(/^Script$/).type("script contents here"); - - cy.dataCy("save-settings-button").scrollIntoView(); - save(); - cy.validateToast("success", "Updated distro."); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/navigation.ts b/apps/spruce/cypress/integration/distroSettings/navigation.ts deleted file mode 100644 index dd55cd0d4e..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/navigation.ts +++ /dev/null @@ -1,102 +0,0 @@ -describe("distroSettings/navigation", () => { - describe("using the distro dropdown", () => { - beforeEach(() => { - cy.visit("/distro/localhost/settings"); - }); - - it("navigates to distro when clicked", () => { - cy.dataCy("distro-select").should("be.visible").click(); - cy.contains("Admin-only").should("exist"); - cy.get("[role='listbox']").within(() => { - cy.get("li").last().contains("localhost2"); - cy.contains("rhel71-power8-large").click(); - }); - cy.location("pathname").should("not.contain", "localhost"); - cy.location("pathname").should("contain", "rhel71-power8-large"); - }); - - it("can navigate to the task queue for the selected distro", () => { - cy.dataCy("navitem-task-queue-link").should( - "have.attr", - "href", - "/task-queue/localhost", - ); - }); - - it("can navigate to the image build information for the selected distro", () => { - cy.dataCy("navitem-image-build-information-link").should( - "have.attr", - "href", - "/image/ubuntu2204/build-information", - ); - }); - - it("can navigate to the image event log for the selected distro", () => { - cy.dataCy("navitem-image-event-log-link").should( - "have.attr", - "href", - "/image/ubuntu2204/event-log", - ); - }); - - describe("warning modal", () => { - it("warns when navigating away from distro settings with unsaved changes and allows returning to distro settings", () => { - cy.getInputByLabel("Notes").type("my note"); - cy.dataCy("save-settings-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("waterfall-link").click(); - cy.dataCy("navigation-warning-modal").should("be.visible"); - cy.contains("button", "Cancel").click(); - cy.dataCy("navigation-warning-modal").should("not.exist"); - cy.location("pathname").should( - "eq", - "/distro/localhost/settings/general", - ); - }); - - describe("modifying the distro provider", () => { - beforeEach(() => { - cy.visit("/distro/ubuntu1604-container-test/settings/provider"); - }); - - it("warns when navigating to another distro settings tab after the provider has changed and allows save", () => { - cy.selectLGOption("Provider", "Static"); - cy.dataCy("save-settings-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.contains("a", "Task Settings").click(); - cy.dataCy("save-modal").should("be.visible"); - cy.dataCy("provider-warning-banner").should("be.visible"); - }); - - it("shows the standard save warning modal when non-provider fields have changed", () => { - cy.getInputByLabel("User Data").type("test user data"); - cy.dataCy("save-settings-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("waterfall-link").click(); - cy.dataCy("navigation-warning-modal").should("be.visible"); - cy.dataCy("provider-warning-banner").should("not.exist"); - }); - }); - }); - }); - - describe("/distros redirect route", () => { - it("should redirect to the first distro available", () => { - cy.visit("/distros"); - cy.location("pathname").should("not.contain", "/distros"); - cy.location("pathname").should( - "eq", - "/distro/archlinux-test/settings/general", - ); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/new_distro.ts b/apps/spruce/cypress/integration/distroSettings/new_distro.ts deleted file mode 100644 index bc532202e3..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/new_distro.ts +++ /dev/null @@ -1,71 +0,0 @@ -const distroSettingPage = "/distro/rhel71-power8-large/settings/general"; - -describe("Creating a new distro", () => { - beforeEach(() => { - cy.visit(distroSettingPage); - }); - - it("Allows a user to create a new distro", () => { - const newDistroId = "new-distro"; - - cy.dataCy("new-distro-button").click(); - cy.dataCy("new-distro-menu").should("be.visible"); - cy.dataCy("create-distro-button").click(); - cy.dataCy("create-distro-modal").should("be.visible"); - cy.dataCy("distro-id-input").type(newDistroId); - cy.contains("button", "Create").click(); - cy.validateToast("success", `Created distro “${newDistroId}”`); - cy.location("pathname").should( - "eq", - `/distro/${newDistroId}/settings/general`, - ); - - cy.dataCy("delete-distro-button").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("delete-distro-button").click(); - cy.dataCy("delete-distro-modal").should("be.visible"); - cy.dataCy("delete-distro-modal").within(() => { - cy.get("input").type(newDistroId); - }); - cy.contains("button", /^Delete$/).click(); - cy.validateToast("success", `The distro “${newDistroId}” was deleted.`); - }); -}); - -describe("Copying a distro", () => { - beforeEach(() => { - cy.visit(distroSettingPage); - }); - - it("Allows a user to copy an existing distro", () => { - const copyDistroId = "copy-distro"; - - cy.dataCy("new-distro-button").click(); - cy.dataCy("new-distro-menu").should("be.visible"); - cy.dataCy("copy-distro-button").click(); - cy.dataCy("copy-distro-modal").should("be.visible"); - cy.dataCy("distro-id-input").type(copyDistroId); - cy.contains("button", "Duplicate").click(); - cy.validateToast("success", `Created distro “${copyDistroId}”`); - cy.location("pathname").should( - "eq", - `/distro/${copyDistroId}/settings/general`, - ); - - cy.dataCy("delete-distro-button").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("delete-distro-button").click(); - cy.dataCy("delete-distro-modal").should("be.visible"); - cy.dataCy("delete-distro-modal").within(() => { - cy.get("input").type(copyDistroId); - }); - cy.contains("button", /^Delete$/).click(); - cy.validateToast("success", `The distro “${copyDistroId}” was deleted.`); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/on_save_operations.ts b/apps/spruce/cypress/integration/distroSettings/on_save_operations.ts deleted file mode 100644 index a14017ecf5..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/on_save_operations.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DistroOnSaveOperation } from "gql/generated/types"; -import { save } from "./utils"; - -describe("using an on-save operation", () => { - beforeEach(() => { - cy.visit("/distro/localhost/settings/general"); - }); - - it("notes how many hosts were updated in the resulting toast", () => { - cy.getInputByLabel("Notes").type("My note"); - - save(DistroOnSaveOperation.Decommission); - cy.validateToast( - "success", - "Updated distro and scheduled 0 hosts to update.", - ); - - // Reset field - cy.getInputByLabel("Notes").clear(); - save(DistroOnSaveOperation.RestartJasper); - cy.validateToast( - "success", - "Updated distro and scheduled 0 hosts to update.", - ); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/permissions.ts b/apps/spruce/cypress/integration/distroSettings/permissions.ts deleted file mode 100644 index 7bc1ae5caf..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/permissions.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { users } from "../../constants"; - -describe("distro permissions", () => { - beforeEach(() => { - cy.logout(); - }); - - it("hides the new distro button when a user cannot create distros", () => { - cy.login(users.privileged); - cy.visit("/distro/rhel71-power8-large/settings/general"); - cy.dataCy("new-distro-button").should("not.exist"); - cy.dataCy("delete-distro-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - cy.get("textarea").should("not.be.disabled"); - }); - - it("disables the delete button when user lacks admin permissions", () => { - cy.login(users.regular); - cy.visit("/distro/rhel71-power8-large/settings/general"); - cy.dataCy("delete-distro-button").should( - "have.attr", - "aria-disabled", - "true", - ); - }); - - it("disables fields when user lacks edit permissions", () => { - cy.login(users.regular); - cy.visit("/distro/rhel71-power8-large/settings/general"); - cy.dataCy("distro-settings-page").within(() => { - cy.get('input[type="checkbox"]').should( - "have.attr", - "aria-disabled", - "true", - ); - cy.get("textarea").should("have.attr", "aria-disabled", "true"); - }); - }); - - it("enables fields if user has edit permissions for a particular distro", () => { - cy.login(users.regular); - cy.visit("/distro/localhost/settings/general"); - cy.dataCy("distro-settings-page").within(() => { - cy.get('input[type="checkbox"]').should( - "have.attr", - "aria-disabled", - "false", - ); - cy.get("textarea").should("have.attr", "aria-disabled", "false"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/project_section.ts b/apps/spruce/cypress/integration/distroSettings/project_section.ts deleted file mode 100644 index 3e09096301..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/project_section.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { save } from "./utils"; - -describe("project section", () => { - beforeEach(() => { - cy.visit("/distro/localhost/settings/project"); - }); - - it("can update fields and those changes will persist", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Update fields. - cy.contains("button", "Add expansion").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.contains("button", "Add expansion").click(); - cy.getInputByLabel("Key").type("key-name"); - cy.getInputByLabel("Value").type("my-value"); - cy.contains("button", "Add project").click(); - cy.getInputByLabel("Project ID").type("spruce"); - - save(); - cy.validateToast("success", "Updated distro."); - - // Changes should persist. - cy.reload(); - cy.getInputByLabel("Key").should("have.value", "key-name"); - cy.getInputByLabel("Value").should("have.value", "my-value"); - cy.getInputByLabel("Project ID").should("have.value", "spruce"); - - // Undo changes. - cy.dataCy("delete-item-button").first().click(); - cy.dataCy("delete-item-button").first().click(); - save(); - cy.validateToast("success", "Updated distro."); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/provider_section.ts b/apps/spruce/cypress/integration/distroSettings/provider_section.ts deleted file mode 100644 index 2b7fdbd3f3..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/provider_section.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { save } from "./utils"; - -describe("provider section", () => { - describe("static", () => { - beforeEach(() => { - cy.visit("/distro/localhost/settings/provider"); - }); - - it("successfully updates static provider fields", () => { - cy.dataCy("provider-select").contains("Static IP/VM"); - - // Correct section is displayed. - cy.dataCy("static-provider-settings").should("exist"); - - // Change field values. - cy.getInputByLabel("User Data").type("my user data"); - cy.getInputByLabel("Merge with existing user data").check({ - force: true, - }); - cy.contains("button", "Add security group").click(); - cy.getInputByLabel("Security Group ID").type("sg-1234"); - cy.contains("button", "Add host").click(); - cy.getInputByLabel("Name").type("host-1234"); - save(); - cy.validateToast("success", "Updated distro."); - - // Revert fields to original values. - cy.getInputByLabel("User Data").clear(); - cy.getInputByLabel("Merge with existing user data").uncheck({ - force: true, - }); - cy.dataCy("delete-item-button").first().click(); - cy.dataCy("delete-item-button").first().click(); - save(); - cy.validateToast("success", "Updated distro."); - }); - }); - - describe("docker", () => { - beforeEach(() => { - cy.visit("/distro/ubuntu1604-container-test/settings/provider"); - }); - - it("shows pool mapping information based on container pool id", () => { - cy.getInputByLabel("Container Pool ID").should( - "contain.text", - "test-pool-1", - ); - cy.getInputByLabel("Pool Mapping Information") - .should("have.attr", "placeholder") - .and("match", /test-pool-1/); - cy.selectLGOption("Container Pool ID", "test-pool-2"); - cy.getInputByLabel("Pool Mapping Information") - .should("have.attr", "placeholder") - .and("match", /test-pool-2/); - }); - - it("successfully updates docker provider fields", () => { - cy.dataCy("provider-select").contains("Docker"); - - // Correct section is displayed. - cy.dataCy("docker-provider-settings").should("exist"); - - // Change field values. - cy.selectLGOption("Image Build Method", "Pull"); - cy.selectLGOption("Container Pool ID", "test-pool-2"); - cy.getInputByLabel("Username for Registries").type("username"); - cy.getInputByLabel("Password for Registries").type("password"); - cy.getInputByLabel("User Data").type("my user data"); - cy.getInputByLabel("Merge with existing user data").check({ - force: true, - }); - save(); - cy.validateToast("success", "Updated distro."); - - // Revert fields to original values. - cy.selectLGOption("Image Build Method", "Import"); - cy.selectLGOption("Container Pool ID", "test-pool-1"); - cy.getInputByLabel("Username for Registries").clear(); - cy.getInputByLabel("Password for Registries").clear(); - cy.getInputByLabel("User Data").clear(); - cy.getInputByLabel("Merge with existing user data").uncheck({ - force: true, - }); - save(); - cy.validateToast("success", "Updated distro."); - }); - }); - - describe("ec2 fleet", () => { - beforeEach(() => { - cy.visit("/distro/ubuntu1804-workstation/settings/provider"); - cy.dataCy("ec2-fleet-provider-settings").should("exist"); - cy.dataCy("expandable-card-title").contains("us-east-1").should("exist"); - }); - - it("shows and hides fields correctly", () => { - // VPC options. - cy.dataCy("use-vpc").should("be.checked"); - cy.contains("Default VPC Subnet ID").should("exist"); - cy.contains("VPC Subnet Prefix").should("exist"); - - cy.dataCy("use-vpc").should("have.attr", "aria-disabled", "false"); - cy.dataCy("use-vpc").uncheck({ force: true }); - cy.dataCy("use-vpc").should("not.be.checked"); - cy.contains("Default VPC Subnet ID").should("not.exist"); - cy.contains("VPC Subnet Prefix").should("not.exist"); - }); - - it("successfully updates ec2 fleet provider fields", () => { - cy.openExpandableCard("us-east-1"); - cy.dataCy("provider-select").contains("EC2 Fleet"); - - // Correct section is displayed. - cy.dataCy("ec2-fleet-provider-settings").should("be.visible"); - cy.dataCy("region-select").contains("us-east-1").should("be.visible"); - - // Change field values. - cy.selectLGOption("Region", "us-west-1"); - cy.getInputByLabel("SSH Key Name").as("keyNameInput"); - cy.get("@keyNameInput").clear(); - cy.get("@keyNameInput").type("my ssh key"); - - cy.contains("button", "Add mount point").click(); - cy.getInputByLabel("Device Name").type("device name"); - cy.getInputByLabel("Size").type("200"); - save(); - cy.validateToast("success", "Updated distro."); - - // Revert fields to original values. - cy.selectLGOption("Region", "us-east-1"); - cy.getInputByLabel("SSH Key Name").as("keyNameInput"); - cy.get("@keyNameInput").clear(); - cy.get("@keyNameInput").type("mci"); - - cy.dataCy("mount-points").within(() => { - cy.dataCy("delete-item-button").click(); - }); - save(); - cy.validateToast("success", "Updated distro."); - }); - - it("can add and delete region settings", () => { - cy.dataCy("expandable-card-title").contains("us-east-1").should("exist"); - cy.openExpandableCard("us-east-1"); - - // Add item for new region. - cy.contains("button", "Add region settings").click(); - cy.contains("button", "Add region settings").should("not.exist"); - cy.openExpandableCard("New AWS Region"); - - // Save new region. - cy.selectLGOption("Region", "us-west-1"); - cy.getInputByLabel("EC2 AMI ID").type("ami-1234"); - cy.getInputByLabel("Instance Type").type("m5.xlarge"); - cy.contains("button", "Add security group").click(); - cy.getInputByLabel("Security Group ID").type("sg-5678"); - save(); - cy.validateToast("success", "Updated distro."); - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("expandable-card-title").contains("us-west-1").should("exist"); - - // Revert to original state by deleting the new region. - cy.dataCy("expandable-card-title") - .contains("us-west-1") - .closest('[data-cy="expandable-card"]') - .within(() => { - cy.dataCy("delete-item-button") - .first() - .should("have.attr", "aria-disabled", "false") - .click(); - }); - cy.dataCy("expandable-card").should("have.length", 1); - cy.dataCy("expandable-card-title") - .contains("us-west-1") - .should("not.exist"); - save(); - cy.validateToast("success", "Updated distro."); - cy.contains("button", "Add region settings").should("exist"); - }); - }); - - describe("ec2 on-demand", () => { - beforeEach(() => { - cy.visit("/distro/ubuntu1604-parent/settings/provider"); - cy.dataCy("ec2-on-demand-provider-settings").should("exist"); - }); - - it("shows and hides fields correctly", () => { - // VPC options. - cy.dataCy("use-vpc").should("be.checked"); - cy.contains("Default VPC Subnet ID").should("exist"); - cy.contains("VPC Subnet Prefix").should("exist"); - - cy.dataCy("use-vpc").should("have.attr", "aria-disabled", "false"); - cy.dataCy("use-vpc").uncheck({ force: true }); - cy.contains("Default VPC Subnet ID").should("not.exist"); - cy.contains("VPC Subnet Prefix").should("not.exist"); - }); - it("successfully updates ec2 on-demand provider fields", () => { - cy.openExpandableCard("us-east-1"); - cy.dataCy("provider-select").contains("EC2 On-Demand"); - // Correct section is displayed. - cy.dataCy("ec2-on-demand-provider-settings").should("exist"); - cy.dataCy("region-select").contains("us-east-1"); - - // Change field values. - cy.selectLGOption("Region", "us-west-1"); - cy.getInputByLabel("EC2 AMI ID").as("amiInput"); - cy.get("@amiInput").clear(); - cy.get("@amiInput").type("ami-1234560"); - cy.getInputByLabel("SSH Key Name").as("keyNameInput"); - cy.get("@keyNameInput").should("have.attr", "aria-disabled", "false"); - cy.get("@keyNameInput").clear(); - cy.get("@keyNameInput").type("my ssh key"); - cy.getInputByLabel("User Data").type(""); - cy.getInputByLabel("Merge with existing user data").check({ - force: true, - }); - save(); - cy.validateToast("success", "Updated distro."); - - // Revert fields to original values. - cy.selectLGOption("Region", "us-east-1"); - cy.getInputByLabel("EC2 AMI ID").as("amiInput"); - cy.get("@amiInput").clear(); - cy.get("@amiInput").type("ami-0000"); - cy.getInputByLabel("SSH Key Name").as("keyNameInput"); - cy.get("@keyNameInput").clear(); - cy.get("@keyNameInput").type("mci"); - cy.getInputByLabel("User Data").clear(); - cy.getInputByLabel("Merge with existing user data").uncheck({ - force: true, - }); - save(); - cy.validateToast("success", "Updated distro."); - }); - - it("can add and delete region settings", () => { - // Add item for new region. - cy.contains("button", "Add region settings").click(); - cy.contains("button", "Add region settings").should("not.exist"); - cy.openExpandableCard("New AWS Region"); - - // Save new region. - cy.selectLGOption("Region", "us-west-1"); - cy.getInputByLabel("EC2 AMI ID").type("ami-1234"); - cy.getInputByLabel("Instance Type").type("m5.xlarge"); - cy.contains("button", "Add security group").click(); - cy.getInputByLabel("Security Group ID").type("sg-0000"); - save(); - cy.validateToast("success", "Updated distro."); - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - cy.dataCy("expandable-card-title").contains("us-west-1").should("exist"); - - // Revert to original state by deleting the new region. - cy.dataCy("expandable-card-title") - .contains("us-west-1") - .closest('[data-cy="expandable-card"]') - .within(() => { - cy.dataCy("delete-item-button") - .first() - .should("have.attr", "aria-disabled", "false") - .click(); - }); - cy.dataCy("expandable-card").should("have.length", 1); - cy.dataCy("expandable-card-title") - .contains("us-west-1") - .should("not.exist"); - save(); - cy.validateToast("success", "Updated distro."); - cy.contains("button", "Add region settings").should("exist"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/single_task_distro.ts b/apps/spruce/cypress/integration/distroSettings/single_task_distro.ts deleted file mode 100644 index 9e8d59658d..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/single_task_distro.ts +++ /dev/null @@ -1,49 +0,0 @@ -describe("single task distro", () => { - it("should render allowed project and tasks", () => { - cy.visit("/distro/archlinux-test/settings"); - cy.dataCy("navitem-single-task-distros").click(); - cy.location("pathname").should( - "eq", - "/distro/archlinux-test/settings/single-task-distros", - ); - cy.dataCy("expandable-card-title").should("have.length", 2); - cy.dataCy("expandable-card-title").eq(0).should("have.text", "evergreen"); - cy.dataCy("expandable-card-title").eq(1).should("have.text", "spruce"); - - cy.dataCy("expandable-card-title").eq(0).click(); - cy.dataCy("expandable-card") - .find("input") - .eq(0) - .should("have.value", "evergreen"); - cy.dataCy("expandable-card") - .find("input") - .eq(1) - .should("have.value", "compile"); - cy.dataCy("expandable-card") - .find("input") - .eq(2) - .should("have.value", "test"); - cy.dataCy("expandable-card") - .find("input") - .eq(3) - .should("have.value", "ubuntu1604"); - cy.dataCy("expandable-card") - .find("input") - .eq(4) - .should("have.value", "windows"); - - cy.dataCy("expandable-card-title").eq(1).click(); - cy.dataCy("expandable-card") - .find("input") - .eq(5) - .should("have.value", "spruce"); - cy.dataCy("expandable-card") - .find("input") - .eq(6) - .should("have.value", "lint"); - cy.dataCy("expandable-card") - .find("input") - .eq(7) - .should("have.value", "storybook"); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/task_section.ts b/apps/spruce/cypress/integration/distroSettings/task_section.ts deleted file mode 100644 index 55c4b0b7c8..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/task_section.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { save } from "./utils"; - -describe("task section", () => { - describe("static provider", () => { - it("should not show tunable options", () => { - cy.visit("/distro/localhost/settings/task"); - cy.selectLGOption("Task Planner Version", "Tunable"); - cy.dataCy("tunable-options").should("not.exist"); - }); - }); - - describe("docker provider", () => { - it("should not show tunable options", () => { - cy.visit("/distro/ubuntu1604-container-test/settings/task"); - cy.selectLGOption("Task Planner Version", "Tunable"); - cy.dataCy("tunable-options").should("not.exist"); - }); - }); - - describe("ec2 provider", () => { - beforeEach(() => { - cy.visit("/distro/ubuntu1804-workstation/settings/task"); - }); - it("should only show tunable options if planner version is tunable", () => { - cy.selectLGOption("Task Planner Version", "Tunable"); - cy.dataCy("tunable-options").should("be.visible"); - }); - it("should surface warnings for invalid number inputs", () => { - const inputLabel = "Patch Factor"; - cy.selectLGOption("Task Planner Version", "Tunable"); - cy.getInputByLabel(inputLabel).clear(); - cy.getInputByLabel(inputLabel).type("500"); - cy.contains("Value should be <= 100.").should("be.visible"); - cy.getInputByLabel(inputLabel).clear(); - cy.getInputByLabel(inputLabel).type("-500"); - cy.contains("Value should be >= 0.").should("be.visible"); - }); - - it("can update fields and those changes will persist", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Update fields. - cy.selectLGOption("Task Finder Version", "Parallel"); - cy.selectLGOption("Task Planner Version", "Tunable"); - cy.selectLGOption("Task Dispatcher Version", "Revised with dependencies"); - save(); - cy.validateToast("success", "Updated distro."); - - // Changes should persist. - cy.reload(); - cy.getInputByLabel("Task Finder Version").should( - "contain.text", - "Parallel", - ); - cy.getInputByLabel("Task Planner Version").should( - "contain.text", - "Tunable", - ); - cy.getInputByLabel("Task Dispatcher Version").should( - "contain.text", - "Revised with dependencies", - ); - - // Undo changes. - cy.selectLGOption("Task Finder Version", "Legacy"); - cy.selectLGOption("Task Planner Version", "Tunable"); - cy.selectLGOption("Task Dispatcher Version", "Revised with dependencies"); - save(); - cy.validateToast("success", "Updated distro."); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/distroSettings/utils.ts b/apps/spruce/cypress/integration/distroSettings/utils.ts deleted file mode 100644 index 9f16e2f028..0000000000 --- a/apps/spruce/cypress/integration/distroSettings/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DistroOnSaveOperation } from "gql/generated/types"; -import { clickSave } from "../../utils"; - -export const save = (onSave?: DistroOnSaveOperation) => { - clickSave(); - - if (onSave) { - cy.get(`input[value=${onSave}]`).check({ force: true }); - } - - cy.dataCy("save-modal").within(() => { - cy.contains("button", "Save").should("have.attr", "aria-disabled", "false"); - cy.contains("button", "Save").click(); - }); -}; diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 5a53b4cba6..657f3933f3 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -1,20 +1,23 @@ -import { Page, expect } from "@playwright/test"; +import { Page, Locator, expect } from "@playwright/test"; /** * Selects an option from a LeafyGreen select component * @param page - The Playwright page object - * @param label - The label of the select input + * @param label - The label text of the select input, or `{ testId: string }` to target by data-cy * @param option - The option text or regex pattern to select * @param options - Additional options for selecting the option * @param options.exact - Whether to match the option text exactly (default: false) */ export async function selectOption( - page: Page, - label: string, + page: Page | Locator, + label: string | { testId: string }, option: string | RegExp, options?: { exact: boolean }, ): Promise { - const input = page.getByLabel(label); + const input = + typeof label === "string" + ? page.getByLabel(label) + : page.getByTestId(label.testId); await input.scrollIntoViewIfNeeded(); await expect(input).toBeEnabled(); await input.click(); @@ -24,27 +27,6 @@ export async function selectOption( await listbox.getByText(option, options).click(); } -/** - * Opens an expandable card if it's not already open - * @param page - The Playwright page object - * @param cardTitle - The title of the expandable card - */ -export async function openExpandableCard( - page: Page, - cardTitle: string, -): Promise { - const cardButton = page - .getByTestId("expandable-card-title") - .filter({ hasText: cardTitle }) - .locator('[role="button"]') - .first(); - - const isExpanded = await cardButton.getAttribute("aria-expanded"); - if (isExpanded !== "true") { - await cardButton.click(); - } -} - /** * Clears a date picker input by pressing backspace multiple times * LG Date Picker does not respond well to .clear() diff --git a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts new file mode 100644 index 0000000000..2f28479e44 --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts @@ -0,0 +1,114 @@ +import { test, expect } from "../../fixtures"; +import { clickCheckboxByLabel, validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("general section", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/localhost/settings/general"); + await expect(page.getByTestId("distro-settings-page")).toBeVisible(); + }); + + test("can update fields and those changes will persist", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await expect( + page.getByRole("button", { name: "Add alias" }), + ).not.toHaveAttribute("aria-disabled", "true"); + await page.getByRole("button", { name: "Add alias" }).click(); + await page.getByLabel("Alias").fill("localhost-alias"); + await page.getByLabel("Notes").fill("this is a note"); + await page.getByLabel("Warnings").fill("this is a warning"); + await clickCheckboxByLabel(page, "Disable shallow clone for this distro"); + await clickCheckboxByLabel(page, "Admin only"); + await save(page); + await validateToast(page, "success", "Updated distro."); + + await page.reload(); + await expect(page.getByTestId("distro-settings-page")).toBeVisible(); + await expect(page.getByLabel("Alias")).toHaveValue("localhost-alias"); + await expect(page.getByLabel("Notes")).toHaveValue("this is a note"); + await expect(page.getByLabel("Warnings")).toHaveValue("this is a warning"); + await expect( + page.getByRole("checkbox", { + name: "Disable shallow clone for this distro", + }), + ).toBeChecked(); + await expect( + page.getByRole("checkbox", { name: "Admin only" }), + ).toBeChecked(); + + await page.getByTestId("delete-item-button").click(); + await page.getByLabel("Notes").clear(); + await page.getByLabel("Warnings").clear(); + await clickCheckboxByLabel(page, "Disable shallow clone for this distro"); + await clickCheckboxByLabel(page, "Admin only"); + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + + test.describe("container pool distro", () => { + test("warns users that the distro will not be spawned for tasks", async ({ + authenticatedPage: page, + }) => { + await page.goto("/distro/ubuntu1604-parent/settings/general"); + await expect( + page.getByText( + "Distro is a container pool, so it cannot be spawned for tasks.", + ), + ).toBeVisible(); + }); + }); + + test.describe("single task distro", () => { + test("can toggle a distro as single task distro and shows a warning banner that dismisses on save", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("single-task-banner")).toHaveCount(0); + + await page + .getByLabel("Set distro as Single Task Distro") + .scrollIntoViewIfNeeded(); + await expect( + page.getByRole("checkbox", { + name: "Set distro as Single Task Distro", + }), + ).not.toHaveAttribute("aria-disabled", "true"); + await clickCheckboxByLabel(page, "Set distro as Single Task Distro"); + await expect(page.getByTestId("single-task-banner")).toContainText( + "This Distro will be converted to a Single Task Distro once saved. Please review before confirming.", + ); + await save(page); + await expect(page.getByTestId("single-task-banner")).toHaveCount(0); + await validateToast(page, "success", "Updated distro."); + + await page.reload(); + await expect( + page.getByRole("checkbox", { + name: "Set distro as Single Task Distro", + }), + ).toBeChecked(); + await expect(page.getByTestId("single-task-banner")).toHaveCount(0); + + await page + .getByLabel("Set distro as Single Task Distro") + .scrollIntoViewIfNeeded(); + await expect( + page.getByRole("checkbox", { + name: "Set distro as Single Task Distro", + }), + ).not.toHaveAttribute("aria-disabled", "true"); + await clickCheckboxByLabel(page, "Set distro as Single Task Distro"); + await expect(page.getByTestId("single-task-banner")).toContainText( + "This Distro will no longer be a Single Task Distro once saved. Please review before confirming.", + ); + await save(page); + await expect(page.getByTestId("single-task-banner")).toHaveCount(0); + await validateToast(page, "success", "Updated distro."); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts new file mode 100644 index 0000000000..be983ab31e --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts @@ -0,0 +1,140 @@ +import { test, expect } from "../../fixtures"; +import { selectOption, validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("host section", () => { + test.describe("using legacy ssh", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/localhost/settings/host"); + }); + + test("shows the correct fields when distro has static provider", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("authorized-keys-input")).toBeVisible(); + await expect(page.getByTestId("minimum-hosts-input")).toHaveCount(0); + await expect(page.getByTestId("maximum-hosts-input")).toHaveCount(0); + await expect(page.getByTestId("idle-time-input")).toHaveCount(0); + await expect(page.getByTestId("future-fraction-input")).toHaveCount(0); + await expect(page.getByTestId("rounding-rule-select")).toHaveCount(0); + await expect(page.getByTestId("feedback-rule-select")).toHaveCount(0); + }); + + test("shows an error when selecting an incompatible host communication method", async ({ + authenticatedPage: page, + }) => { + await selectOption(page, "Host Communication Method", "RPC"); + await expect( + page.getByText( + "Legacy and non-legacy bootstrapping and communication are incompatible.", + ), + ).toBeVisible(); + }); + + test("updates host fields", async ({ authenticatedPage: page }) => { + await selectOption(page, "Agent Architecture", "Linux ARM 64-bit"); + await page.getByLabel("Working Directory").clear(); + await page.getByLabel("Working Directory").fill("/usr/local/bin"); + await page.getByLabel("SSH User").clear(); + await page.getByLabel("SSH User").fill("sudo"); + await page.getByRole("button", { name: "Add SSH option" }).click(); + await page.getByLabel("SSH Option").fill("BatchMode=yes"); + await selectOption( + page, + "Host Overallocation Rule", + "Terminate hosts when overallocated", + ); + + await save(page); + await validateToast(page, "success", "Updated distro."); + + await selectOption(page, "Agent Architecture", "Linux 64-bit"); + await page.getByLabel("Working Directory").clear(); + await page.getByLabel("Working Directory").fill("/home/ubuntu/smoke"); + await page.getByLabel("SSH User").clear(); + await page.getByLabel("SSH User").fill("ubuntu"); + await page.getByTestId("delete-item-button").click(); + await selectOption(page, "Host Overallocation Rule", "Default"); + + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + + test("updates mountpoints", async ({ authenticatedPage: page }) => { + await expect( + page.getByRole("button", { name: "Add mountpoint" }), + ).not.toHaveAttribute("aria-disabled", "true"); + await page.getByRole("button", { name: "Add mountpoint" }).click(); + await page.getByLabel("Mountpoint").fill("/data"); + + await save(page); + await validateToast(page, "success", "Updated distro."); + + await page.getByTestId("delete-item-button").click(); + + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + }); + + test.describe("using User Data bootstrap method", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/ubuntu1604-parent/settings/host"); + await selectOption( + page, + { testId: "bootstrap-method-select" }, + "User Data", + ); + await selectOption(page, "Host Communication Method", "RPC"); + }); + + test("shows Windows-only fields when the architecture is updated", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByLabel("Root Directory")).toHaveCount(0); + await expect(page.getByLabel("Service User")).toHaveCount(0); + + await selectOption(page, "Agent Architecture", "Windows 64-bit"); + + await expect(page.getByLabel("Root Directory")).toBeVisible(); + await expect(page.getByLabel("Service User")).toBeVisible(); + }); + + test("hides resource limit fields when the architecture is not Linux", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByText("Resource Limits")).toBeVisible(); + await selectOption(page, "Agent Architecture", "Windows 64-bit"); + await expect(page.getByText("Resource Limits")).toHaveCount(0); + }); + + test("saves bootstrap settings", async ({ authenticatedPage: page }) => { + await page.getByLabel("Jasper Binary Directory").fill("/jasper/binary"); + await page + .getByLabel("Jasper Credentials Path") + .fill("/jasper/credentials"); + await page.getByLabel("Client Directory").fill("/client/dir"); + await page.getByLabel("Shell Path").fill("/shell/path"); + await page + .getByLabel("Home Volume Format Command") + .fill("echo 'Hello World'"); + await page.getByLabel("Number of Files").fill("10"); + await page.getByLabel("Number of CGroup Tasks").fill("20"); + await page.getByLabel("Number of Processes").fill("30"); + await page.getByLabel("Locked Memory").fill("128"); + await page.getByLabel("Virtual Memory").fill("256"); + + await page.getByRole("button", { name: "Add variable" }).click(); + await page.getByLabel("Key").fill("my-key"); + await page.getByLabel("Value").fill("my-value"); + + await page.getByRole("button", { name: "Add script" }).click(); + await page.getByLabel(/^Path$/).fill("/path/to/precondition/script"); + await page.getByLabel(/^Script$/).fill("script contents here"); + + await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts b/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts new file mode 100644 index 0000000000..1fb326b623 --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts @@ -0,0 +1,116 @@ +import { test, expect } from "../../fixtures"; +import { selectOption } from "../../helpers"; + +test.describe("distroSettings/navigation", () => { + test.describe("using the distro dropdown", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/localhost/settings"); + }); + + test("navigates to distro when clicked", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("distro-select").click(); + const listbox = page.getByRole("listbox"); + await expect(listbox.getByText("Admin-only")).toBeVisible(); + await expect(listbox.getByRole("option").last()).toContainText( + "localhost2", + ); + await listbox.getByText("rhel71-power8-large").click(); + await expect(page).not.toHaveURL(/\/distro\/localhost/); + await expect(page).toHaveURL(/\/distro\/rhel71-power8-large/); + }); + + test("can navigate to the task queue for the selected distro", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("navitem-task-queue-link")).toHaveAttribute( + "href", + "/task-queue/localhost", + ); + }); + + test("can navigate to the image build information for the selected distro", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("navitem-image-build-information-link"), + ).toHaveAttribute("href", "/image/ubuntu2204/build-information"); + }); + + test("can navigate to the image event log for the selected distro", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("navitem-image-event-log-link"), + ).toHaveAttribute("href", "/image/ubuntu2204/event-log"); + }); + + test.describe("warning modal", () => { + test("warns when navigating away with unsaved changes and allows returning", async ({ + authenticatedPage: page, + }) => { + await page.getByLabel("Notes").fill("my note"); + await expect( + page.getByTestId("save-settings-button"), + ).not.toHaveAttribute("aria-disabled", "true"); + await page.getByTestId("waterfall-link").click(); + await expect( + page.getByTestId("navigation-warning-modal"), + ).toBeVisible(); + await page.getByRole("button", { name: "Cancel" }).click(); + await expect(page.getByTestId("navigation-warning-modal")).toHaveCount( + 0, + ); + await expect(page).toHaveURL("/distro/localhost/settings/general"); + }); + + test.describe("modifying the distro provider", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto( + "/distro/ubuntu1604-container-test/settings/provider", + ); + }); + + test("warns when navigating to another tab after provider changed and allows save", async ({ + authenticatedPage: page, + }) => { + await selectOption(page, "Provider", "Static"); + await expect( + page.getByTestId("save-settings-button"), + ).toHaveAttribute("aria-disabled", "false"); + await page.getByRole("link", { name: "Task Settings" }).click(); + await expect( + page.getByTestId("provider-warning-banner"), + ).toBeVisible(); + }); + + test("shows the standard save warning modal when non-provider fields have changed", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("user-data-input").fill("test user data"); + await expect( + page.getByTestId("save-settings-button"), + ).toHaveAttribute("aria-disabled", "false"); + await page.getByTestId("waterfall-link").click(); + await expect( + page.getByTestId("navigation-warning-modal"), + ).toBeVisible(); + await expect(page.getByTestId("provider-warning-banner")).toHaveCount( + 0, + ); + }); + }); + }); + }); + + test.describe("/distros redirect route", () => { + test("should redirect to the first distro available", async ({ + authenticatedPage: page, + }) => { + await page.goto("/distros"); + await expect(page).not.toHaveURL(/\/distros$/); + await expect(page).toHaveURL("/distro/archlinux-test/settings/general"); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts b/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts new file mode 100644 index 0000000000..d3bd468c59 --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; + +const distroSettingPage = "/distro/rhel71-power8-large/settings/general"; + +test.describe("Creating a new distro", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(distroSettingPage); + }); + + test("allows a user to create a new distro", async ({ + authenticatedPage: page, + }) => { + const newDistroId = "new-distro"; + + await page.getByTestId("new-distro-button").click(); + await expect(page.getByTestId("new-distro-menu")).toBeVisible(); + await page.getByTestId("create-distro-button").click(); + await expect(page.getByTestId("create-distro-modal")).toBeVisible(); + await page.getByTestId("distro-id-input").fill(newDistroId); + await page.getByRole("button", { name: "Create" }).click(); + await validateToast( + page, + "success", + `Created distro “${newDistroId}”`, + true, + ); + await expect(page).toHaveURL(`/distro/${newDistroId}/settings/general`); + + await expect(page.getByTestId("delete-distro-button")).not.toHaveAttribute( + "aria-disabled", + "true", + ); + await page.getByTestId("delete-distro-button").click(); + await expect(page.getByTestId("delete-distro-modal")).toBeVisible(); + await page + .getByTestId("delete-distro-modal") + .locator("input") + .fill(newDistroId); + await page.getByRole("button", { name: "Delete", exact: true }).click(); + await validateToast( + page, + "success", + `The distro “${newDistroId}” was deleted.`, + ); + }); +}); + +test.describe("Copying a distro", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(distroSettingPage); + }); + + test("allows a user to copy an existing distro", async ({ + authenticatedPage: page, + }) => { + const copyDistroId = "copy-distro"; + + await page.getByTestId("new-distro-button").click(); + await expect(page.getByTestId("new-distro-menu")).toBeVisible(); + await page.getByTestId("copy-distro-button").click(); + await expect(page.getByTestId("copy-distro-modal")).toBeVisible(); + await page.getByTestId("distro-id-input").fill(copyDistroId); + await page.getByRole("button", { name: "Duplicate" }).click(); + await validateToast( + page, + "success", + `Created distro “${copyDistroId}”`, + true, + ); + await expect(page).toHaveURL(`/distro/${copyDistroId}/settings/general`); + + await expect(page.getByTestId("delete-distro-button")).not.toHaveAttribute( + "aria-disabled", + "true", + ); + await page.getByTestId("delete-distro-button").click(); + await expect(page.getByTestId("delete-distro-modal")).toBeVisible(); + await page + .getByTestId("delete-distro-modal") + .locator("input") + .fill(copyDistroId); + await page.getByRole("button", { name: "Delete", exact: true }).click(); + await validateToast( + page, + "success", + `The distro “${copyDistroId}” was deleted.`, + ); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/on_save_operations.spec.ts b/apps/spruce/playwright/tests/distroSettings/on_save_operations.spec.ts new file mode 100644 index 0000000000..4e503ad454 --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/on_save_operations.spec.ts @@ -0,0 +1,29 @@ +import { test } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("using an on-save operation", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/localhost/settings/general"); + }); + + test("notes how many hosts were updated in the resulting toast", async ({ + authenticatedPage: page, + }) => { + await page.getByLabel("Notes").fill("My note"); + await save(page, "DECOMMISSION"); + await validateToast( + page, + "success", + "Updated distro and scheduled 0 hosts to update.", + true, + ); + await page.getByLabel("Notes").clear(); + await save(page, "RESTART_JASPER"); + await validateToast( + page, + "success", + "Updated distro and scheduled 0 hosts to update.", + ); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/permissions.spec.ts b/apps/spruce/playwright/tests/distroSettings/permissions.spec.ts new file mode 100644 index 0000000000..bbcd74f9fa --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/permissions.spec.ts @@ -0,0 +1,65 @@ +import { users } from "@evg-ui/playwright-config/constants"; +import { test, expect } from "../../fixtures"; +import { login, logout } from "../../helpers"; + +const distroRoute = "/distro/rhel71-power8-large/settings/general"; + +test.describe("distro permissions", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await logout(page); + }); + + test("hides the new distro button when a user cannot create distros", async ({ + authenticatedPage: page, + }) => { + await login(page, users.privileged); + await page.goto(distroRoute); + await expect(page.getByTestId("new-distro-button")).toHaveCount(0); + await expect(page.getByTestId("delete-distro-button")).toHaveAttribute( + "aria-disabled", + "false", + ); + await expect(page.locator("textarea").first()).toBeEnabled(); + }); + + test("disables the delete button when user lacks admin permissions", async ({ + authenticatedPage: page, + }) => { + await login(page, users.regular); + await page.goto(distroRoute); + await expect(page.getByTestId("delete-distro-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + + test("disables fields when user lacks edit permissions", async ({ + authenticatedPage: page, + }) => { + await login(page, users.regular); + await page.goto(distroRoute); + const settingsPage = page.getByTestId("distro-settings-page"); + await expect( + settingsPage.locator('input[type="checkbox"]').first(), + ).toHaveAttribute("aria-disabled", "true"); + await expect(settingsPage.locator("textarea").first()).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + + test("enables fields if user has edit permissions for a particular distro", async ({ + authenticatedPage: page, + }) => { + await login(page, users.regular); + await page.goto("/distro/localhost/settings/general"); + const settingsPage = page.getByTestId("distro-settings-page"); + await expect( + settingsPage.locator('input[type="checkbox"]').first(), + ).toHaveAttribute("aria-disabled", "false"); + await expect(settingsPage.locator("textarea").first()).toHaveAttribute( + "aria-disabled", + "false", + ); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts new file mode 100644 index 0000000000..00fd0c1f9a --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("project section", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/localhost/settings/project"); + }); + + test("can update fields and those changes will persist", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await expect( + page.getByRole("button", { name: "Add expansion" }), + ).not.toHaveAttribute("aria-disabled", "true"); + await page.getByRole("button", { name: "Add expansion" }).click(); + await page.getByLabel("Key").fill("key-name"); + await page.getByLabel("Value").fill("my-value"); + await page.getByRole("button", { name: "Add project" }).click(); + await page.getByLabel("Project ID").fill("spruce"); + + await save(page); + await validateToast(page, "success", "Updated distro."); + + await page.reload(); + await expect(page.getByLabel("Key")).toHaveValue("key-name"); + await expect(page.getByLabel("Value")).toHaveValue("my-value"); + await expect(page.getByLabel("Project ID")).toHaveValue("spruce"); + + await page.getByTestId("delete-item-button").first().click(); + await page.getByTestId("delete-item-button").first().click(); + await save(page); + await validateToast(page, "success", "Updated distro."); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts new file mode 100644 index 0000000000..4803a62d6a --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts @@ -0,0 +1,323 @@ +import { test, expect } from "../../fixtures"; +import { + clickCheckboxByLabel, + selectOption, + validateToast, +} from "../../helpers"; +import { save } from "./utils"; + +test.describe("provider section", () => { + test.describe("static", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/localhost/settings/provider"); + }); + + test("successfully updates static provider fields", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("provider-select")).toContainText( + "Static IP/VM", + ); + await expect(page.getByTestId("static-provider-settings")).toBeVisible(); + + await page.getByTestId("user-data-input").fill("my user data"); + await clickCheckboxByLabel(page, "Merge with existing user data"); + await page.getByRole("button", { name: "Add security group" }).click(); + await page.getByLabel("Security Group ID").fill("sg-1234"); + await page.getByRole("button", { name: "Add host" }).click(); + await page.getByLabel("Name").fill("host-1234"); + await save(page); + await validateToast(page, "success", "Updated distro."); + + await page.getByTestId("user-data-input").clear(); + await clickCheckboxByLabel(page, "Merge with existing user data"); + await page.getByTestId("delete-item-button").first().click(); + await page.getByTestId("delete-item-button").first().click(); + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + }); + + test.describe("docker", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/ubuntu1604-container-test/settings/provider"); + }); + + 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( + "placeholder", + /test-pool-1/, + ); + await selectOption(page, "Container Pool ID", "test-pool-2"); + await expect(page.getByLabel("Pool Mapping Information")).toHaveAttribute( + "placeholder", + /test-pool-2/, + ); + }); + + test("successfully updates docker provider fields", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("provider-select")).toContainText("Docker"); + await expect(page.getByTestId("docker-provider-settings")).toBeVisible(); + + await selectOption(page, "Image Build Method", "Pull"); + await selectOption(page, "Container Pool ID", "test-pool-2"); + await page.getByLabel("Username for Registries").fill("username"); + await page.getByLabel("Password for Registries").fill("password"); + await page.getByTestId("user-data-input").fill("my user data"); + await clickCheckboxByLabel(page, "Merge with existing user data"); + await save(page); + await validateToast(page, "success", "Updated distro."); + + await selectOption(page, "Image Build Method", "Import"); + await selectOption(page, "Container Pool ID", "test-pool-1"); + await page.getByLabel("Username for Registries").clear(); + await page.getByLabel("Password for Registries").clear(); + await page.getByTestId("user-data-input").clear(); + await clickCheckboxByLabel(page, "Merge with existing user data"); + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + }); + + test.describe("ec2 fleet", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/ubuntu1804-workstation/settings/provider"); + await expect( + page.getByTestId("ec2-fleet-provider-settings"), + ).toBeVisible(); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "us-east-1" }), + ).toBeVisible(); + }); + + test("shows and hides fields correctly", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("use-vpc")).toBeChecked(); + await expect(page.getByText("Default VPC Subnet ID")).toBeVisible(); + await expect(page.getByText("VPC Subnet Prefix")).toBeVisible(); + + await clickCheckboxByLabel(page, "Use security groups in an EC2 VPC"); + await expect(page.getByTestId("use-vpc")).not.toBeChecked(); + await expect(page.getByText("Default VPC Subnet ID")).toHaveCount(0); + await expect(page.getByText("VPC Subnet Prefix")).toHaveCount(0); + }); + + test("successfully updates ec2 fleet provider fields", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("provider-select")).toContainText( + "EC2 Fleet", + ); + await expect( + page.getByTestId("ec2-fleet-provider-settings"), + ).toBeVisible(); + await expect(page.getByTestId("region-select")).toContainText( + "us-east-1", + ); + + await selectOption(page, "Region", "us-west-1"); + await page.getByLabel("SSH Key Name").clear(); + await page.getByLabel("SSH Key Name").fill("my ssh key"); + await page.getByRole("button", { name: "Add mount point" }).click(); + await page.getByLabel("Device Name").fill("device name"); + await page.getByLabel("Size").fill("200"); + await save(page); + await validateToast(page, "success", "Updated distro."); + + await selectOption(page, "Region", "us-east-1"); + await page.getByLabel("SSH Key Name").clear(); + await page.getByLabel("SSH Key Name").fill("mci"); + await page + .getByTestId("mount-points") + .getByTestId("delete-item-button") + .click(); + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + + test("can add and delete region settings", async ({ + authenticatedPage: page, + }) => { + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "us-east-1" }), + ).toBeVisible(); + + await page.getByRole("button", { name: "Add region settings" }).click(); + await expect( + page.getByRole("button", { name: "Add region settings", exact: true }), + ).toHaveCount(0); + + const newExpandableCard = page.getByTestId("expandable-card").first(); + await expect( + newExpandableCard + .getByTestId("expandable-card-title") + .filter({ hasText: "New AWS Region" }), + ).toBeVisible(); + + await selectOption(page, { testId: "region-select" }, "us-west-1"); + await newExpandableCard.getByLabel("EC2 AMI ID").fill("ami-1234"); + await newExpandableCard.getByLabel("Instance Type").fill("m5.xlarge"); + await newExpandableCard + .getByRole("button", { name: "Add security group" }) + .click(); + await newExpandableCard.getByLabel("Security Group ID").fill("sg-5678"); + await save(page); + await validateToast(page, "success", "Updated distro."); + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "us-west-1" }), + ).toBeVisible(); + + await page + .getByTestId("expandable-card-title") + .filter({ hasText: "us-west-1" }) + .locator("..") + .getByTestId("delete-item-button") + .first() + .click(); + await expect(page.getByTestId("expandable-card")).toHaveCount(1); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "us-west-1" }), + ).toHaveCount(0); + await save(page); + await validateToast(page, "success", "Updated distro."); + await expect( + page.getByRole("button", { name: "Add region settings" }), + ).toBeVisible(); + }); + }); + + test.describe("ec2 on-demand", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/ubuntu1604-parent/settings/provider"); + await expect( + page.getByTestId("ec2-on-demand-provider-settings"), + ).toBeVisible(); + }); + + test("shows and hides fields correctly", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("use-vpc")).toBeChecked(); + await expect(page.getByText("Default VPC Subnet ID")).toBeVisible(); + await expect(page.getByText("VPC Subnet Prefix")).toBeVisible(); + + await clickCheckboxByLabel(page, "Use security groups in an EC2 VPC"); + await expect(page.getByText("Default VPC Subnet ID")).toHaveCount(0); + await expect(page.getByText("VPC Subnet Prefix")).toHaveCount(0); + }); + + test("successfully updates ec2 on-demand provider fields", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("provider-select")).toContainText( + "EC2 On-Demand", + ); + await expect( + page.getByTestId("ec2-on-demand-provider-settings"), + ).toBeVisible(); + await expect(page.getByTestId("region-select")).toContainText( + "us-east-1", + ); + + await selectOption(page, "Region", "us-west-1"); + await page.getByLabel("EC2 AMI ID").clear(); + await page.getByLabel("EC2 AMI ID").fill("ami-1234560"); + await expect(page.getByLabel("SSH Key Name")).not.toHaveAttribute( + "aria-disabled", + "true", + ); + await page.getByLabel("SSH Key Name").clear(); + await page.getByLabel("SSH Key Name").fill("my ssh key"); + await page + .getByTestId("user-data-input") + .fill(""); + await clickCheckboxByLabel(page, "Merge with existing user data"); + await save(page); + await validateToast(page, "success", "Updated distro."); + + await selectOption(page, "Region", "us-east-1"); + await page.getByLabel("EC2 AMI ID").clear(); + await page.getByLabel("EC2 AMI ID").fill("ami-0000"); + await page.getByLabel("SSH Key Name").clear(); + await page.getByLabel("SSH Key Name").fill("mci"); + await page.getByTestId("user-data-input").clear(); + await clickCheckboxByLabel(page, "Merge with existing user data"); + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + + test("can add and delete region settings", async ({ + authenticatedPage: page, + }) => { + await page.getByRole("button", { name: "Add region settings" }).click(); + await expect( + page.getByRole("button", { name: "Add region settings", exact: true }), + ).toHaveCount(0); + + const newExpandableCard = page.getByTestId("expandable-card").first(); + await expect( + newExpandableCard + .getByTestId("expandable-card-title") + .filter({ hasText: "New AWS Region" }), + ).toBeVisible(); + + await selectOption(page, { testId: "region-select" }, "us-west-1"); + await newExpandableCard.getByLabel("EC2 AMI ID").fill("ami-1234"); + await newExpandableCard.getByLabel("Instance Type").fill("m5.xlarge"); + await newExpandableCard + .getByRole("button", { name: "Add security group" }) + .click(); + await newExpandableCard.getByLabel("Security Group ID").fill("sg-0000"); + await save(page); + await validateToast(page, "success", "Updated distro."); + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "us-west-1" }), + ).toBeVisible(); + + await page + .getByTestId("expandable-card-title") + .filter({ hasText: "us-west-1" }) + .locator("..") + .getByTestId("delete-item-button") + .first() + .click(); + await expect(page.getByTestId("expandable-card")).toHaveCount(1); + await expect( + page + .getByTestId("expandable-card-title") + .filter({ hasText: "us-west-1" }), + ).toHaveCount(0); + await save(page); + await validateToast(page, "success", "Updated distro."); + await expect( + page.getByRole("button", { name: "Add region settings" }), + ).toBeVisible(); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/single_task_distro.spec.ts b/apps/spruce/playwright/tests/distroSettings/single_task_distro.spec.ts new file mode 100644 index 0000000000..b0f2074ffb --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/single_task_distro.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "../../fixtures"; + +test.describe("single task distro", () => { + test("should render allowed projects and tasks", async ({ + authenticatedPage: page, + }) => { + await page.goto("/distro/archlinux-test/settings"); + await page.getByTestId("navitem-single-task-distros").click(); + await expect(page).toHaveURL( + "/distro/archlinux-test/settings/single-task-distros", + ); + + const cards = page.getByTestId("expandable-card-title"); + await expect(cards).toHaveCount(2); + await expect(cards.nth(0)).toHaveText("evergreen"); + await expect(cards.nth(1)).toHaveText("spruce"); + + await cards.nth(0).click(); + const inputs = page.getByTestId("expandable-card").locator("input"); + await expect(inputs.nth(0)).toHaveValue("evergreen"); + await expect(inputs.nth(1)).toHaveValue("compile"); + await expect(inputs.nth(2)).toHaveValue("test"); + await expect(inputs.nth(3)).toHaveValue("ubuntu1604"); + await expect(inputs.nth(4)).toHaveValue("windows"); + + await cards.nth(1).click(); + await expect(inputs.nth(5)).toHaveValue("spruce"); + await expect(inputs.nth(6)).toHaveValue("lint"); + await expect(inputs.nth(7)).toHaveValue("storybook"); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts new file mode 100644 index 0000000000..9211fb5de4 --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from "../../fixtures"; +import { selectOption, validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("task section", () => { + test.describe("static provider", () => { + test("should not show tunable options", async ({ + authenticatedPage: page, + }) => { + await page.goto("/distro/localhost/settings/task"); + await selectOption(page, "Task Planner Version", "Tunable"); + await expect(page.getByTestId("tunable-options")).toHaveCount(0); + }); + }); + + test.describe("docker provider", () => { + test("should not show tunable options", async ({ + authenticatedPage: page, + }) => { + await page.goto("/distro/ubuntu1604-container-test/settings/task"); + await selectOption(page, "Task Planner Version", "Tunable"); + await expect(page.getByTestId("tunable-options")).toHaveCount(0); + }); + }); + + test.describe("ec2 provider", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/distro/ubuntu1804-workstation/settings/task"); + }); + + test("should only show tunable options if planner version is tunable", async ({ + authenticatedPage: page, + }) => { + await selectOption(page, "Task Planner Version", "Tunable"); + await expect(page.getByTestId("tunable-options")).toBeVisible(); + }); + + test("should surface warnings for invalid number inputs", async ({ + authenticatedPage: page, + }) => { + await selectOption(page, "Task Planner Version", "Tunable"); + await page.getByLabel("Patch Factor").clear(); + await page.getByLabel("Patch Factor").fill("500"); + await expect(page.getByText("Value should be <= 100.")).toBeVisible(); + await page.getByLabel("Patch Factor").clear(); + await page.getByLabel("Patch Factor").fill("-500"); + await expect(page.getByText("Value should be >= 0.")).toBeVisible(); + }); + + test("can update fields and those changes will persist", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await selectOption(page, "Task Finder Version", "Parallel"); + await selectOption(page, "Task Planner Version", "Tunable"); + await selectOption( + page, + "Task Dispatcher Version", + "Revised with dependencies", + ); + await save(page); + await validateToast(page, "success", "Updated distro."); + + await page.reload(); + await expect(page.getByLabel("Task Finder Version")).toContainText( + "Parallel", + ); + await expect(page.getByLabel("Task Planner Version")).toContainText( + "Tunable", + ); + await expect(page.getByLabel("Task Dispatcher Version")).toContainText( + "Revised with dependencies", + ); + + await selectOption(page, "Task Finder Version", "Legacy"); + await selectOption(page, "Task Planner Version", "Tunable"); + await selectOption( + page, + "Task Dispatcher Version", + "Revised with dependencies", + ); + await save(page); + await validateToast(page, "success", "Updated distro."); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/distroSettings/utils.ts b/apps/spruce/playwright/tests/distroSettings/utils.ts new file mode 100644 index 0000000000..3331ff8ea2 --- /dev/null +++ b/apps/spruce/playwright/tests/distroSettings/utils.ts @@ -0,0 +1,21 @@ +import { Page } from "@playwright/test"; +import { expect } from "../../fixtures"; + +type onSaveOptions = "NONE" | "DECOMMISSION" | "RESTART_JASPER" | "REPROVISION"; + +export const save = async (page: Page, onSaveValue?: onSaveOptions) => { + const saveButton = page.getByTestId("save-settings-button"); + await expect(saveButton).not.toHaveAttribute("aria-disabled", "true"); + await saveButton.click(); + + if (onSaveValue) { + const radio = page.locator(`input[value="${onSaveValue}"]`); + const radioId = await radio.getAttribute("id"); + await page.locator(`label[for="${radioId}"]`).click(); + } + + const modal = page.getByTestId("save-modal"); + const confirmButton = modal.getByRole("button", { name: "Save" }); + await expect(confirmButton).toHaveAttribute("aria-disabled", "false"); + await confirmButton.click(); +}; diff --git a/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx b/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx index 82ddf44204..b660ddba77 100644 --- a/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx +++ b/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx @@ -38,6 +38,7 @@ const bootstrapMethod = { oneOf: enumSelect(bootstrapMethodToCopy), }, uiSchema: { + "ui:data-cy": "bootstrap-method-select", "ui:allowDeselect": false, }, }; diff --git a/apps/spruce/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts b/apps/spruce/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts index d0b7472176..fcf2f66405 100644 --- a/apps/spruce/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts +++ b/apps/spruce/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts @@ -8,6 +8,7 @@ const userData = { title: "User Data", }, uiSchema: { + "ui:data-cy": "user-data-input", "ui:widget": "textarea", "ui:elementWrapperCSS": textAreaCSS, "ui:rows": 6, From 85b4927d66fbc8d85bd185ded4cea72434e8bac4 Mon Sep 17 00:00:00 2001 From: minnakt Date: Thu, 23 Apr 2026 16:22:41 -0400 Subject: [PATCH 06/32] fix: mistake --- .../tests/distroSettings/provider_section.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts index 4803a62d6a..fd147672e9 100644 --- a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts @@ -166,7 +166,11 @@ test.describe("provider section", () => { .filter({ hasText: "New AWS Region" }), ).toBeVisible(); - await selectOption(page, { testId: "region-select" }, "us-west-1"); + await selectOption( + newExpandableCard, + { testId: "region-select" }, + "us-west-1", + ); await newExpandableCard.getByLabel("EC2 AMI ID").fill("ami-1234"); await newExpandableCard.getByLabel("Instance Type").fill("m5.xlarge"); await newExpandableCard @@ -281,7 +285,11 @@ test.describe("provider section", () => { .filter({ hasText: "New AWS Region" }), ).toBeVisible(); - await selectOption(page, { testId: "region-select" }, "us-west-1"); + await selectOption( + newExpandableCard, + { testId: "region-select" }, + "us-west-1", + ); await newExpandableCard.getByLabel("EC2 AMI ID").fill("ami-1234"); await newExpandableCard.getByLabel("Instance Type").fill("m5.xlarge"); await newExpandableCard From c4cf02ffd1a3d365e5e37d6aa6d0b5520e2cb6d2 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 11:52:03 -0400 Subject: [PATCH 07/32] migrate `adminSettings` tests --- .../adminSettings/announcements.ts | 32 - .../adminSettings/authentication.ts | 291 -------- .../adminSettings/background_processing.ts | 84 --- .../adminSettings/external_communications.ts | 192 ------ .../integration/adminSettings/navigation.ts | 13 - .../integration/adminSettings/other.ts | 459 ------------- .../adminSettings/restart_tasks.ts | 50 -- .../integration/adminSettings/runners.ts | 58 -- .../adminSettings/save_function.ts | 620 ------------------ .../adminSettings/service_flags.ts | 63 -- .../cypress/integration/adminSettings/web.ts | 40 -- apps/spruce/playwright/helpers/index.ts | 15 +- .../tests/adminSettings/announcements.spec.ts | 33 + .../adminSettings/authentication.spec.ts | 216 ++++++ .../background_processing.spec.ts | 79 +++ .../external_communications.spec.ts | 136 ++++ .../tests/adminSettings/navigation.spec.ts | 19 + .../tests/adminSettings/other.spec.ts | 415 ++++++++++++ .../tests/adminSettings/restart_tasks.spec.ts | 53 ++ .../tests/adminSettings/runners.spec.ts | 62 ++ .../tests/adminSettings/save_function.spec.ts | 514 +++++++++++++++ .../tests/adminSettings/service_flags.spec.ts | 66 ++ .../playwright/tests/adminSettings/utils.ts | 8 + .../tests/adminSettings/web.spec.ts | 59 ++ .../playwright/tests/spawn/host.spec.ts | 6 +- .../tests/task/task_history.spec.ts | 18 +- .../tests/waterfall/filters.spec.ts | 6 +- .../ObjectFieldTemplates/index.tsx | 7 +- .../AnnouncementsTab/getFormSchema.ts | 1 + .../tabs/GeneralTab/OtherTab/schemaFields.ts | 6 + .../tabs/GeneralTab/WebTab/schemaFields.ts | 4 +- .../RestartTasksTab/RestartTasksButton.tsx | 2 +- 32 files changed, 1709 insertions(+), 1918 deletions(-) delete mode 100644 apps/spruce/cypress/integration/adminSettings/announcements.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/authentication.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/background_processing.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/external_communications.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/navigation.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/other.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/restart_tasks.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/runners.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/save_function.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/service_flags.ts delete mode 100644 apps/spruce/cypress/integration/adminSettings/web.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/announcements.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/authentication.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/external_communications.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/navigation.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/other.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/runners.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/save_function.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/utils.ts create mode 100644 apps/spruce/playwright/tests/adminSettings/web.spec.ts diff --git a/apps/spruce/cypress/integration/adminSettings/announcements.ts b/apps/spruce/cypress/integration/adminSettings/announcements.ts deleted file mode 100644 index 12fd721cc1..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/announcements.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("announcements", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("can save after making changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type("some more banner text"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - - cy.reload(); - cy.getInputByLabel("Banner Text").should( - "have.value", - "some more banner text", - ); - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/authentication.ts b/apps/spruce/cypress/integration/adminSettings/authentication.ts deleted file mode 100644 index 6c04c0611f..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/authentication.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("authentication", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("can save after making changes to authentication settings", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Navigate to Authentication tab if needed - cy.contains("Authentication").click(); - - // Global Config section - const allowServiceUsers = "Allow Service Users"; - cy.getInputByLabel(allowServiceUsers).as("allowServiceUsersCheckbox"); - cy.get("@allowServiceUsersCheckbox").check({ force: true }); - - const backgroundReauth = "Background Reauthentication (Mins)"; - cy.getInputByLabel(backgroundReauth).as("backgroundReauthInput"); - cy.get("@backgroundReauthInput").clear(); - cy.get("@backgroundReauthInput").type("120"); - - const preferredAuthType = "Preferred Authentication Type"; - cy.getInputByLabel(preferredAuthType).as("preferredAuthRadio"); - cy.get("@preferredAuthRadio").within(() => { - cy.contains("Github").click(); - }); - - // Okta section - const oktaClientId = "Client ID"; - cy.dataCy("okta").within(() => { - cy.getInputByLabel(oktaClientId).as("oktaClientIdInput"); - cy.get("@oktaClientIdInput").clear(); - cy.get("@oktaClientIdInput").type("test-okta-client-id"); - - const oktaClientSecret = "Client Secret"; - cy.getInputByLabel(oktaClientSecret).as("oktaClientSecretInput"); - cy.get("@oktaClientSecretInput").clear(); - cy.get("@oktaClientSecretInput").type("test-okta-client-secret"); - - const oktaIssuer = "Issuer"; - cy.getInputByLabel(oktaIssuer).as("oktaIssuerInput"); - cy.get("@oktaIssuerInput").clear(); - cy.get("@oktaIssuerInput").type("https://test.okta.com"); - - const oktaUserGroup = "User Group"; - cy.getInputByLabel(oktaUserGroup).as("oktaUserGroupInput"); - cy.get("@oktaUserGroupInput").clear(); - cy.get("@oktaUserGroupInput").type("test-users"); - - const oktaExpireAfter = "Expire After (Mins)"; - cy.getInputByLabel(oktaExpireAfter).as("oktaExpireAfterInput"); - cy.get("@oktaExpireAfterInput").clear(); - cy.get("@oktaExpireAfterInput").type("480"); - - const oktaScopes = "Scopes"; - cy.getInputByLabel(oktaScopes).as("oktaScopesInput"); - cy.get("@oktaScopesInput").type("openid{enter}"); - cy.get("@oktaScopesInput").type("profile{enter}"); - cy.get("@oktaScopesInput").type("email{enter}"); - }); - - // GitHub section - cy.dataCy("github").within(() => { - const githubAppId = "App ID"; - cy.getInputByLabel(githubAppId).as("githubAppIdInput"); - cy.get("@githubAppIdInput").clear(); - cy.get("@githubAppIdInput").type("54321"); - - const githubClientId = "Client ID"; - cy.getInputByLabel(githubClientId).as("githubClientIdInput"); - cy.get("@githubClientIdInput").clear(); - cy.get("@githubClientIdInput").type("test-github-client-id"); - - const githubClientSecret = "Client Secret"; - cy.getInputByLabel(githubClientSecret).as("githubClientSecretInput"); - cy.get("@githubClientSecretInput").clear(); - cy.get("@githubClientSecretInput").type("test-github-client-secret"); - - const githubDefaultOwner = "Default Owner"; - cy.getInputByLabel(githubDefaultOwner).as("githubDefaultOwnerInput"); - cy.get("@githubDefaultOwnerInput").clear(); - cy.get("@githubDefaultOwnerInput").type("test-owner"); - - const githubDefaultRepo = "Default Repository"; - cy.getInputByLabel(githubDefaultRepo).as("githubDefaultRepoInput"); - cy.get("@githubDefaultRepoInput").clear(); - cy.get("@githubDefaultRepoInput").type("test-repo"); - - const githubOrganization = "Organization"; - cy.getInputByLabel(githubOrganization).as("githubOrganizationInput"); - cy.get("@githubOrganizationInput").clear(); - cy.get("@githubOrganizationInput").type("test-org"); - - const githubUsers = "Users"; - cy.getInputByLabel(githubUsers).as("githubUsersInput"); - cy.get("@githubUsersInput").type("testuser1{enter}"); - cy.get("@githubUsersInput").type("testuser2{enter}"); - }); - - // Naive section - cy.dataCy("naive").within(() => { - cy.get('[data-cy="add-button"]').as("naiveUsersSection"); - cy.contains("Add").click(); - cy.get('[id="root_authentication_naive_users"]') - .children() - .first() - .within(() => { - cy.getInputByLabel("Display Name").type("Test User 1"); - cy.getInputByLabel("Email").type("test1@example.com"); - cy.getInputByLabel("Password").type("password123"); - cy.getInputByLabel("Username").type("testuser1"); - }); - }); - - // Multi section - cy.dataCy("multi-read-write").click(); - cy.dataCy("multi-read-write-options").within(() => { - cy.getInputByLabel("Okta").check({ force: true }); - }); - cy.dataCy("multi-read-write").click({ force: true }); - - cy.dataCy("multi-read-only").click(); - cy.dataCy("multi-read-only-options").within(() => { - cy.getInputByLabel("Naive").check({ force: true }); - }); - cy.dataCy("multi-read-only").click({ force: true }); - - // Kanopy section - cy.dataCy("kanopy").within(() => { - const kanopyHeaderName = "Header Name"; - cy.getInputByLabel(kanopyHeaderName).as("kanopyHeaderNameInput"); - cy.get("@kanopyHeaderNameInput").clear(); - cy.get("@kanopyHeaderNameInput").type("X-Test-Auth-Token"); - - const kanopyIssuer = "Issuer"; - cy.getInputByLabel(kanopyIssuer).as("kanopyIssuerInput"); - cy.get("@kanopyIssuerInput").clear(); - cy.get("@kanopyIssuerInput").type("https://test-kanopy.example.com"); - - const kanopyKeysetURL = "Keyset URL"; - cy.getInputByLabel(kanopyKeysetURL).as("kanopyKeysetURLInput"); - cy.get("@kanopyKeysetURLInput").clear(); - cy.get("@kanopyKeysetURLInput").type( - "https://test-kanopy.example.com/.well-known/jwks.json", - ); - }); - - // OAuth section - cy.dataCy("oauth").within(() => { - const oauthClientId = "Client ID"; - cy.getInputByLabel(oauthClientId).as("oauthClientIdInput"); - cy.get("@oauthClientIdInput").clear(); - cy.get("@oauthClientIdInput").type("oauth-client-id"); - - const oauthIssuer = "Issuer"; - cy.getInputByLabel(oauthIssuer).as("oauthIssuerInput"); - cy.get("@oauthIssuerInput").clear(); - cy.get("@oauthIssuerInput").type("https://test-oauth.example.com"); - - const oauthConnectorId = "Connector ID"; - cy.getInputByLabel(oauthConnectorId).as("oauthConnectorIdInput"); - cy.get("@oauthConnectorIdInput").clear(); - cy.get("@oauthConnectorIdInput").type("oauth-connector-id"); - }); - - // Save the changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify all changes were saved correctly - // Navigate to Authentication tab again after reload - cy.contains("Authentication").click(); - - // Verify Global Config - cy.getInputByLabel(allowServiceUsers).as("allowServiceUsersCheckbox"); - cy.get("@allowServiceUsersCheckbox").should("be.checked"); - - cy.getInputByLabel(backgroundReauth).as("backgroundReauthInput"); - cy.get("@backgroundReauthInput").should("have.value", "120"); - - // Verify Okta settings - cy.dataCy("okta").within(() => { - cy.getInputByLabel("Client ID").should( - "have.value", - "test-okta-client-id", - ); - cy.getInputByLabel("Client Secret").should( - "have.value", - "test-okta-client-secret", - ); - cy.getInputByLabel("Issuer").should( - "have.value", - "https://test.okta.com", - ); - cy.getInputByLabel("User Group").should("have.value", "test-users"); - cy.getInputByLabel("Expire After (Mins)").should("have.value", "480"); - - // Verify scopes chips - cy.dataCy("filter-chip").should("have.length", 3); - cy.contains("openid").should("exist"); - cy.contains("profile").should("exist"); - cy.contains("email").should("exist"); - }); - - // Verify GitHub settings - cy.dataCy("github").within(() => { - cy.getInputByLabel("App ID").should("have.value", "54321"); - cy.getInputByLabel("Client ID").should( - "have.value", - "test-github-client-id", - ); - cy.getInputByLabel("Client Secret").should( - "have.value", - "test-github-client-secret", - ); - cy.getInputByLabel("Default Owner").should("have.value", "test-owner"); - cy.getInputByLabel("Default Repository").should( - "have.value", - "test-repo", - ); - cy.getInputByLabel("Organization").should("have.value", "test-org"); - - // Verify users chips - cy.dataCy("filter-chip").should("have.length", 2); - cy.get("span[data-cy=filter-chip]").contains("testuser1"); - cy.get("span[data-cy=filter-chip]").contains("testuser2"); - }); - - // Verify Naive user was added - cy.dataCy("naive").within(() => { - cy.getInputByLabel("Display Name") - .first() - .should("have.value", "Test User 1"); - cy.getInputByLabel("Email") - .first() - .should("have.value", "test1@example.com"); - cy.getInputByLabel("Password") - .first() - .should("have.value", "password123"); - cy.getInputByLabel("Username").first().should("have.value", "testuser1"); - }); - - // Verify Multi settings - cy.dataCy("multi-read-write").click(); - cy.dataCy("multi-read-write-options").within(() => { - cy.getInputByLabel("Okta").should("be.checked"); - }); - cy.dataCy("multi-read-write").click({ force: true }); - cy.dataCy("multi-read-only").click(); - cy.dataCy("multi-read-only-options").within(() => { - cy.getInputByLabel("Naive").should("be.checked"); - }); - cy.dataCy("multi-read-only").click({ force: true }); - - // Verify Kanopy settings - cy.dataCy("kanopy").within(() => { - cy.getInputByLabel("Header Name").should( - "have.value", - "X-Test-Auth-Token", - ); - cy.getInputByLabel("Issuer").should( - "have.value", - "https://test-kanopy.example.com", - ); - cy.getInputByLabel("Keyset URL").should( - "have.value", - "https://test-kanopy.example.com/.well-known/jwks.json", - ); - }); - - // Verify OAuth settings - cy.dataCy("oauth").within(() => { - cy.getInputByLabel("Client ID").should("have.value", "oauth-client-id"); - cy.getInputByLabel("Issuer").should( - "have.value", - "https://test-oauth.example.com", - ); - cy.getInputByLabel("Connector ID").should( - "have.value", - "oauth-connector-id", - ); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/background_processing.ts b/apps/spruce/cypress/integration/adminSettings/background_processing.ts deleted file mode 100644 index beeb8d264f..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/background_processing.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("background processing", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("can save after making changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Amboy section. - const singleWorker = "Single Worker Name"; - cy.getInputByLabel(singleWorker).as("amboyInput"); - cy.get("@amboyInput").clear(); - cy.get("@amboyInput").type("new single worker name"); - - const namedQueueList = "named-queue-list"; - cy.dataCy(namedQueueList).as("amboyList"); - cy.get("@amboyList") - .children() - .eq(0) - .within(() => { - cy.dataCy("delete-item-button").click(); - }); - - // Logger section. - const defaultLevel = "Default Level"; - cy.getInputByLabel(defaultLevel).as("loggerSelect"); - cy.selectLGOption(defaultLevel, "Alert"); - - cy.getInputByLabel("Redact Keys").as("loggerChipInput"); - cy.get("@loggerChipInput").type("aNewRedactedKey{enter}"); - - const asyncBuffer = "Use asynchronous buffered logger"; - cy.getInputByLabel(asyncBuffer).as("loggerCheckbox"); - cy.get("@loggerCheckbox").uncheck({ force: true }); - - // Notification Rate Limits section. - const timeInterval = "Time Interval (secs)"; - cy.getInputByLabel(timeInterval).as("notificationRateLimitsInput"); - cy.get("@notificationRateLimitsInput").clear(); - cy.get("@notificationRateLimitsInput").type("1"); - - // Triggers section. - const generateTasksDistro = "Distro for Generated Tasks"; - cy.getInputByLabel(generateTasksDistro).as("triggersSelect"); - cy.get("@triggersSelect").clear(); - cy.get("@triggersSelect").type("localhost"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Need to redefine these aliases as the addition / deletion of elements from the - // form has caused the mapping of these items to change. - cy.getInputByLabel(singleWorker).as("amboyInput"); - cy.dataCy(namedQueueList).as("amboyList"); - cy.getInputByLabel(defaultLevel).as("loggerSelect"); - cy.getInputByLabel(asyncBuffer).as("loggerCheckbox"); - cy.getInputByLabel(timeInterval).as("notificationRateLimitsInput"); - cy.getInputByLabel(generateTasksDistro).as("triggersSelect"); - - cy.get("@amboyInput").should("have.value", "new single worker name"); - cy.get("@amboyList").should("have.length", 1); - cy.get("@loggerSelect").should("have.value", "ALERT"); - cy.dataCy("logger").within(() => { - cy.dataCy("filter-chip").should("have.length", 4); - cy.contains("aNewRedactedKey").should("exist"); - }); - cy.get("@loggerCheckbox").should("not.be.checked"); - cy.get("@notificationRateLimitsInput").should("have.value", "1"); - cy.get("@triggersSelect").should("have.value", "localhost"); - - // Ensure that Notify SES Input has not changed. - cy.getInputByLabel("SES Email").should( - "have.value", - "evg-sender@email.com", - ); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/external_communications.ts b/apps/spruce/cypress/integration/adminSettings/external_communications.ts deleted file mode 100644 index 1ba0541421..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/external_communications.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("external communications", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("can save after making changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Navigate to External Communications tab if it exists - cy.contains("External Communications").click(); - - // JIRA section - cy.dataCy("jira").within(() => { - cy.getInputByLabel("Email").as("jiraEmailInput"); - cy.get("@jiraEmailInput").clear(); - cy.get("@jiraEmailInput").type("test@example.com"); - - const jiraHost = "Host"; - cy.getInputByLabel(jiraHost).as("jiraHostInput"); - cy.get("@jiraHostInput").clear(); - cy.get("@jiraHostInput").type("jira.test.com"); - - const jiraPat = "Personal Access Token"; - cy.getInputByLabel(jiraPat).as("jiraPat"); - cy.get("@jiraPat").clear(); - cy.get("@jiraPat").type("password"); - }); - - // Slack section. - cy.dataCy("slack").within(() => { - const slackToken = "Token"; - cy.getInputByLabel(slackToken).as("slackTokenInput"); - cy.get("@slackTokenInput").clear(); - cy.get("@slackTokenInput").type("xoxb-test-token"); - - const slackAppName = "App Name"; - cy.getInputByLabel(slackAppName).as("slackAppNameInput"); - cy.get("@slackAppNameInput").clear(); - cy.get("@slackAppNameInput").type("test-app"); - - const slackChannel = "Channel"; - cy.getInputByLabel(slackChannel).as("slackChannelInput"); - cy.get("@slackChannelInput").clear(); - cy.get("@slackChannelInput").type("#test-channel"); - - const slackFieldsSet = "Fields To Set"; - cy.getInputByLabel(slackFieldsSet).as("slackFieldsSetInput"); - cy.get("@slackFieldsSetInput").type("field1{enter}"); - cy.get("@slackFieldsSetInput").type("field2{enter}"); - }); - - // Splunk section. - cy.dataCy("splunk").within(() => { - const splunkServerUrl = "Server URL"; - cy.getInputByLabel(splunkServerUrl).as("splunkServerUrlInput"); - cy.get("@splunkServerUrlInput").clear(); - cy.get("@splunkServerUrlInput").type("splunk.test.com"); - - const splunkChannel = "Channel"; - cy.getInputByLabel(splunkChannel).as("splunkChannelInput"); - cy.get("@splunkChannelInput").clear(); - cy.get("@splunkChannelInput").type("test-logs"); - }); - - // // Runtime Environments section. - cy.dataCy("runtime-environments").within(() => { - const runtimeBaseUrl = "Base URL"; - cy.getInputByLabel(runtimeBaseUrl).as("runtimeBaseUrlInput"); - cy.get("@runtimeBaseUrlInput").clear(); - cy.get("@runtimeBaseUrlInput").type("runtime.test.com"); - }); - // // Test Selection section. - cy.dataCy("test-selection").within(() => { - const testSelectionUrl = "URL"; - cy.getInputByLabel(testSelectionUrl).as("testSelectionUrlInput"); - cy.get("@testSelectionUrlInput").clear(); - cy.get("@testSelectionUrlInput").type("testselection.test.com"); - }); - - // // FWS section. - cy.dataCy("fws").within(() => { - const fwsUrl = "URL"; - cy.getInputByLabel(fwsUrl).as("fwsUrlInput"); - cy.get("@fwsUrlInput").clear(); - cy.get("@fwsUrlInput").type("fws.test.com"); - }); - - // // Cedar section. - cy.dataCy("cedar").within(() => { - const cedarDbUrl = "Database URL"; - cy.getInputByLabel(cedarDbUrl).as("cedarDbUrlInput"); - cy.get("@cedarDbUrlInput").clear(); - cy.get("@cedarDbUrlInput").type("cedar-db.test.com"); - - const cedarDbName = "Database Name"; - cy.getInputByLabel(cedarDbName).as("cedarDbNameInput"); - cy.get("@cedarDbNameInput").clear(); - cy.get("@cedarDbNameInput").type("test-cedar-db"); - - const cedarSpsUrl = "SPS URL (Vanity, for hosts only)"; - cy.getInputByLabel(cedarSpsUrl).as("cedarSpsUrlInput"); - cy.get("@cedarSpsUrlInput").clear(); - cy.get("@cedarSpsUrlInput").type("sps.test.com"); - - const cedarSpsKanopyUrl = "SPS Kanopy URL"; - cy.getInputByLabel(cedarSpsKanopyUrl).as("cedarSpsKanopyUrlInput"); - cy.get("@cedarSpsKanopyUrlInput").clear(); - cy.get("@cedarSpsKanopyUrlInput").type("sps-kanopy.test.com"); - }); - - // Sage section. - cy.dataCy("sage").within(() => { - const sageBaseUrl = "Base URL"; - cy.getInputByLabel(sageBaseUrl).as("sageBaseUrlInput"); - cy.get("@sageBaseUrlInput").clear(); - cy.get("@sageBaseUrlInput").type("sage.test.com"); - }); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - - cy.reload(); - - // Verify all changes were saved. - // JIRA section. - cy.dataCy("jira").within(() => { - cy.getInputByLabel("Email").should("have.value", "test@example.com"); - cy.getInputByLabel("Host").should("have.value", "jira.test.com"); - }); - - // Slack section. - cy.dataCy("slack").within(() => { - cy.getInputByLabel("Token").should("have.value", "xoxb-test-token"); - cy.getInputByLabel("App Name").should("have.value", "test-app"); - cy.getInputByLabel("Channel").should("have.value", "#test-channel"); - - // Check chip inputs for fields set. - cy.dataCy("filter-chip").should("have.length", 2); - cy.contains("field1").should("exist"); - cy.contains("field2").should("exist"); - }); - - // Splunk section. - cy.dataCy("splunk").within(() => { - cy.getInputByLabel("Server URL").should("have.value", "splunk.test.com"); - cy.getInputByLabel("Channel").should("have.value", "test-logs"); - }); - - // Runtime Environments section. - cy.dataCy("runtime-environments").within(() => { - cy.getInputByLabel("Base URL").should("have.value", "runtime.test.com"); - }); - - // Test Selection section. - cy.dataCy("test-selection").within(() => { - cy.getInputByLabel("URL").should("have.value", "testselection.test.com"); - }); - - // FWS section. - cy.dataCy("fws").within(() => { - cy.getInputByLabel("URL").should("have.value", "fws.test.com"); - }); - - // Cedar section. - cy.dataCy("cedar").within(() => { - cy.getInputByLabel("Database URL").should( - "have.value", - "cedar-db.test.com", - ); - cy.getInputByLabel("Database Name").should("have.value", "test-cedar-db"); - cy.getInputByLabel("SPS URL (Vanity, for hosts only)").should( - "have.value", - "sps.test.com", - ); - cy.getInputByLabel("SPS Kanopy URL").should( - "have.value", - "sps-kanopy.test.com", - ); - }); - - // Sage section. - cy.dataCy("sage").within(() => { - cy.getInputByLabel("Base URL").should("have.value", "sage.test.com"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/navigation.ts b/apps/spruce/cypress/integration/adminSettings/navigation.ts deleted file mode 100644 index 0e51fafd0e..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/navigation.ts +++ /dev/null @@ -1,13 +0,0 @@ -describe("admin settings page", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("can navigate to the admin settings page", () => { - cy.dataCy("admin-settings-page").should("be.visible"); - }); - - it("has a side navigation with the correct items", () => { - cy.get("[id=announcements]").should("be.visible"); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/other.ts b/apps/spruce/cypress/integration/adminSettings/other.ts deleted file mode 100644 index 6fac1aa975..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/other.ts +++ /dev/null @@ -1,459 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("other", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("can save misc settings changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Misc Settings section. - const configDir = "Config Directory"; - cy.getInputByLabel(configDir).as("configDirInput"); - cy.get("@configDirInput").clear(); - cy.get("@configDirInput").type("/new/config/dir"); - - const githubPrCreatorOrg = "GitHub PR Creator Organization"; - cy.getInputByLabel(githubPrCreatorOrg).as("githubPrCreatorOrg"); - cy.get("@githubPrCreatorOrg").clear(); - cy.get("@githubPrCreatorOrg").type("new.example.com"); - - const shutdownWaitSeconds = "Shutdown Wait Time (secs)"; - cy.getInputByLabel(shutdownWaitSeconds).as("shutdownWaitInput"); - cy.get("@shutdownWaitInput").clear(); - cy.get("@shutdownWaitInput").type("45"); - - cy.getInputByLabel("GitHub Organizations").as("githubOrgsInput"); - cy.get("@githubOrgsInput").type("neworg{enter}"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.getInputByLabel(configDir).should("have.value", "/new/config/dir"); - cy.getInputByLabel(githubPrCreatorOrg).should( - "have.value", - "new.example.com", - ); - cy.getInputByLabel(shutdownWaitSeconds).should("have.value", "45"); - - cy.dataCy("misc-settings").within(() => { - cy.dataCy("filter-chip").contains("neworg").should("exist"); - }); - }); - - it("can save cost settings changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Cost Settings section. - const financeFormula = "Finance Formula"; - cy.getInputByLabel(financeFormula).as("financeFormulaInput"); - cy.get("@financeFormulaInput").clear(); - cy.get("@financeFormulaInput").type("0.5"); - - const savingsPlanDiscount = "Savings Plan Discount"; - cy.getInputByLabel(savingsPlanDiscount).as("savingsPlanDiscountInput"); - cy.get("@savingsPlanDiscountInput").clear(); - cy.get("@savingsPlanDiscountInput").type("0.15"); - - const onDemandDiscount = "On-Demand Discount"; - cy.getInputByLabel(onDemandDiscount).as("onDemandDiscountInput"); - cy.get("@onDemandDiscountInput").clear(); - cy.get("@onDemandDiscountInput").type("0.08"); - - const ebsCostDiscount = "EBS Cost Discount"; - cy.getInputByLabel(ebsCostDiscount).as("ebsCostDiscountInput"); - cy.get("@ebsCostDiscountInput").clear(); - cy.get("@ebsCostDiscountInput").type("0.1"); - - // S3 Cost Settings. - const uploadCostDiscount = "Upload Cost Discount"; - cy.getInputByLabel(uploadCostDiscount).as("uploadCostDiscountInput"); - cy.get("@uploadCostDiscountInput").clear(); - cy.get("@uploadCostDiscountInput").type("0.12"); - - const standardStorageCostDiscount = "Standard Storage Cost Discount"; - cy.getInputByLabel(standardStorageCostDiscount).as( - "standardStorageCostDiscountInput", - ); - cy.get("@standardStorageCostDiscountInput").clear(); - cy.get("@standardStorageCostDiscountInput").type("0.18"); - - const iAStorageCostDiscount = "Infrequent Access Storage Cost Discount"; - cy.getInputByLabel(iAStorageCostDiscount).as("iAStorageCostDiscountInput"); - cy.get("@iAStorageCostDiscountInput").clear(); - cy.get("@iAStorageCostDiscountInput").type("0.22"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.getInputByLabel(financeFormula).should("have.value", "0.5"); - cy.getInputByLabel(savingsPlanDiscount).should("have.value", "0.15"); - cy.getInputByLabel(onDemandDiscount).should("have.value", "0.08"); - cy.getInputByLabel(ebsCostDiscount).should("have.value", "0.1"); - cy.getInputByLabel(uploadCostDiscount).should("have.value", "0.12"); - cy.getInputByLabel(standardStorageCostDiscount).should( - "have.value", - "0.18", - ); - cy.getInputByLabel(iAStorageCostDiscount).should("have.value", "0.22"); - }); - - it("can clear S3 cost discount values", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - const uploadCostDiscount = "Upload Cost Discount"; - const standardStorageCostDiscount = "Standard Storage Cost Discount"; - const iAStorageCostDiscount = "Infrequent Access Storage Cost Discount"; - - cy.getInputByLabel(uploadCostDiscount).as("uploadCostDiscountInput"); - cy.get("@uploadCostDiscountInput").clear(); - cy.get("@uploadCostDiscountInput").type("0.12"); - - cy.getInputByLabel(standardStorageCostDiscount).as( - "standardStorageCostDiscountInput", - ); - cy.get("@standardStorageCostDiscountInput").clear(); - cy.get("@standardStorageCostDiscountInput").type("0.18"); - - cy.getInputByLabel(iAStorageCostDiscount).as("iAStorageCostDiscountInput"); - cy.get("@iAStorageCostDiscountInput").clear(); - cy.get("@iAStorageCostDiscountInput").type("0.22"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.getInputByLabel(uploadCostDiscount).should("have.value", "0.12"); - cy.getInputByLabel(standardStorageCostDiscount).should( - "have.value", - "0.18", - ); - cy.getInputByLabel(iAStorageCostDiscount).should("have.value", "0.22"); - - cy.get("@uploadCostDiscountInput").clear(); - cy.get("@standardStorageCostDiscountInput").clear(); - cy.get("@iAStorageCostDiscountInput").clear(); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.getInputByLabel(uploadCostDiscount).should("have.value", "0"); - cy.getInputByLabel(standardStorageCostDiscount).should("have.value", "0"); - cy.getInputByLabel(iAStorageCostDiscount).should("have.value", "0"); - }); - - it("can save single task host changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Single Task Host section. - const projectTasksList = "project-tasks-pairs-list"; - cy.dataCy(projectTasksList).as("projectTasksList"); - cy.contains("Add project tasks pair").click(); - - cy.getInputByLabel("Project ID / Repo").click({ force: true }); - cy.get('[role="listbox"]').should("be.visible"); - cy.get('[role="option"]').last().click(); - - cy.getInputByLabel("Allowed Tasks").type("compile{enter}test{enter}"); - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.dataCy(projectTasksList).children().should("have.length.at.least", 1); - }); - - it("can save bucket config changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Bucket Config section. - cy.dataCy("bucket-config").within(() => { - cy.getInputByLabel("Name").as("logBucketNameInput"); - cy.get("@logBucketNameInput").clear(); - cy.get("@logBucketNameInput").type("new-log-bucket"); - }); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.dataCy("bucket-config").within(() => { - cy.getInputByLabel("Name").should("have.value", "new-log-bucket"); - }); - }); - - it("can save SSH pairs changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // SSH Pairs section. - cy.dataCy("ssh-pairs").within(() => { - cy.contains("Task Host Key"); - cy.getInputByLabel("Name").as("taskHostKeyInput"); - cy.get("@taskHostKeyInput").clear(); - cy.get("@taskHostKeyInput").type("new-task-host-key"); - - cy.getInputByLabel("Secret ARN").as("taskHostSecretARN"); - cy.get("@taskHostSecretARN").clear(); - cy.get("@taskHostSecretARN").type("new-task-host-secret-arn"); - }); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.dataCy("ssh-pairs").within(() => { - cy.getInputByLabel("Name").should("have.value", "new-task-host-key"); - cy.getInputByLabel("Secret ARN").should( - "have.value", - "new-task-host-secret-arn", - ); - }); - }); - - it("can save expansions changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Expansions section. - cy.dataCy("expansions-list").within(() => { - cy.contains("Add").click(); - }); - - cy.dataCy("expansions-list") - .children() - .last() - .within(() => { - cy.getInputByLabel("Key").type("NEW_VAR"); - cy.getInputByLabel("Value").type("new_value"); - }); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.dataCy("expansions-list").children().should("have.length.at.least", 1); - }); - - it("can save host jasper changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Host Jasper section. - const binaryName = "Binary Name"; - cy.dataCy("host-jasper").within(() => { - cy.getInputByLabel(binaryName).as("binaryNameInput"); - cy.get("@binaryNameInput").clear(); - cy.get("@binaryNameInput").type("new-jasper"); - }); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.dataCy("host-jasper").within(() => { - cy.getInputByLabel("Binary Name").should("have.value", "new-jasper"); - }); - }); - - it("can save JIRA notifications changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // JIRA Notifications section. - const jiraNotifications = "jira-notifications"; - cy.dataCy(jiraNotifications).as("jiraNotifications"); - cy.get("@jiraNotifications").within(() => { - cy.contains("Add new Jira project").click(); - }); - - cy.get("@jiraNotifications") - .children() - .last() - .within(() => { - cy.getInputByLabel("Project").type("TEST"); - cy.getInputByLabel("Components").type("backend{enter}"); - cy.getInputByLabel("Labels").type("feature{enter}"); - }); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.dataCy(jiraNotifications).children().should("have.length.at.least", 1); - }); - - it("can save spawn host changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Spawn Host section. - const unexpirableHosts = "Unexpirable Hosts Per User"; - cy.getInputByLabel(unexpirableHosts).as("unexpirableHostsInput"); - cy.get("@unexpirableHostsInput").clear(); - cy.get("@unexpirableHostsInput").type("5"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.getInputByLabel(unexpirableHosts).should("have.value", "5"); - }); - - it("can save sleep schedule changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Sleep Schedule section. - cy.getInputByLabel("Permanently Exempt Hosts").as("exemptHostsInput"); - cy.get("@exemptHostsInput").type("exempt-host-1{enter}"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.dataCy("sleep-schedule").within(() => { - cy.dataCy("filter-chip").contains("exempt-host-1").should("exist"); - }); - }); - - it("can save tracer configuration changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Tracer Configuration section. - const tracerEnabled = "Enable tracer"; - cy.dataCy("tracer-configuration").within(() => { - cy.getInputByLabel(tracerEnabled).as("tracerEnabledInput"); - cy.get("@tracerEnabledInput").check({ force: true }); - }); - - const collectorEndpoint = "Collector Endpoint"; - cy.getInputByLabel(collectorEndpoint).as("collectorEndpointInput"); - cy.get("@collectorEndpointInput").clear(); - cy.get("@collectorEndpointInput").type("https://new-collector.example.com"); - - const traceUrlTemplate = "Trace URL Template"; - cy.getInputByLabel(traceUrlTemplate).as("traceUrlTemplateInput"); - cy.get("@traceUrlTemplateInput").clear(); - cy.get("@traceUrlTemplateInput").type("https://apm.example.com/trace/%s"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.dataCy("tracer-configuration").within(() => { - cy.getInputByLabel(tracerEnabled).should("be.checked"); - }); - cy.getInputByLabel(collectorEndpoint).should( - "have.value", - "https://new-collector.example.com", - ); - cy.getInputByLabel(traceUrlTemplate).should( - "have.value", - "https://apm.example.com/trace/%s", - ); - }); - - it("can save project creation changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Project Creation section. - const totalProjectLimit = "Total Project Limit"; - cy.getInputByLabel(totalProjectLimit).as("totalProjectLimitInput"); - cy.get("@totalProjectLimitInput").clear(); - cy.get("@totalProjectLimitInput").type("150"); - - const projectCreationSettings = "project-creation-settings"; - cy.dataCy(projectCreationSettings).as("projectCreationSettings"); - cy.get("@projectCreationSettings").within(() => { - cy.contains("Add repository exception").click(); - }); - - cy.get("@projectCreationSettings") - .children() - .last() - .within(() => { - cy.getInputByLabel("Owner").type("test-owner"); - cy.getInputByLabel("Repository").type("test-repo"); - }); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - cy.getInputByLabel(totalProjectLimit).should("have.value", "150"); - cy.dataCy(projectCreationSettings) - .children() - .should("have.length.at.least", 1); - }); - - it("can save GitHub check run changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // GitHub Check Run section. - const checkRunLimit = "Check Run Limit"; - cy.getInputByLabel(checkRunLimit).as("checkRunLimitInput"); - cy.get("@checkRunLimitInput").clear(); - cy.get("@checkRunLimitInput").type("25"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify GitHub check run value persisted. - cy.getInputByLabel(checkRunLimit).should("have.value", "25"); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/restart_tasks.ts b/apps/spruce/cypress/integration/adminSettings/restart_tasks.ts deleted file mode 100644 index d43a0fb479..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/restart_tasks.ts +++ /dev/null @@ -1,50 +0,0 @@ -describe("restart tasks", () => { - beforeEach(() => { - cy.visit("/admin-settings/restart-tasks"); - }); - - it("can restart tasks", () => { - cy.dataCy("restart-tasks-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Change Start Date. - cy.dataCy("start-date-picker").click(); - cy.get("[aria-label*='Select year' i]").click(); - cy.contains("li", "2020").click({ force: true }); - cy.get("[aria-label*='Select month' i]").click(); - cy.contains("li", "Feb").click({ force: true }); - cy.get("[data-iso='2020-02-01']").click(); - cy.validateDatePickerDate("start-date-picker", { - year: "2020", - month: "02", - day: "01", - }); - - // Change End Date. - cy.dataCy("end-date-picker").click(); - cy.get("[aria-label*='Select year' i]").click(); - cy.contains("li", "2021").click({ force: true }); - cy.get("[aria-label*='Select month' i]").click(); - cy.contains("li", "Mar").click({ force: true }); - cy.get("[data-iso='2021-03-01']").click(); - cy.validateDatePickerDate("end-date-picker", { - year: "2021", - month: "03", - day: "01", - }); - - cy.dataCy("restart-tasks-button").should( - "have.attr", - "aria-disabled", - "false", - ); - cy.dataCy("restart-tasks-button").click(); - cy.dataCy("restart-tasks-modal").should("be.visible"); - cy.dataCy("restart-tasks-list").children().should("have.length", 4); - cy.contains("button", "Confirm").click(); - cy.validateToast("success", "Created job to restart 4 tasks."); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/runners.ts b/apps/spruce/cypress/integration/adminSettings/runners.ts deleted file mode 100644 index 2c09b43a42..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/runners.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("runners", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("can save after making changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Notify section. - cy.getInputByLabel("SES Email").as("sesInput"); - cy.get("@sesInput").clear(); - cy.get("@sesInput").type("new_email@email.com"); - - // Task Limits section. - cy.getInputByLabel("Max Hourly Patch Tasks Per User").as("taskLimitInput"); - cy.get("@taskLimitInput").clear(); - cy.get("@taskLimitInput").type("9999"); - - // Host Init section. - cy.getInputByLabel("Max Total Dynamic Hosts").as("hostInitInput"); - cy.get("@hostInitInput").clear(); - cy.get("@hostInitInput").type("8888"); - - // Scheduler section. - cy.getInputByLabel("Rounding Rule").as("schedulerSelect"); - cy.selectLGOption("Rounding Rule", "Round up"); - - cy.getInputByLabel("Default Future Host Fraction").as("schedulerInput"); - cy.get("@schedulerInput").clear(); - cy.get("@schedulerInput").type("0.6"); - - cy.getInputByLabel("Group Versions").as("schedulerCheckbox"); - cy.get("@schedulerCheckbox").uncheck({ force: true }); - - // Repotracker section. - cy.getInputByLabel("New Revisions to Fetch").as("repotrackerInput"); - cy.get("@repotrackerInput").clear(); - cy.get("@repotrackerInput").type("5"); - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - - cy.reload(); - cy.get("@sesInput").should("have.value", "new_email@email.com"); - cy.get("@taskLimitInput").should("have.value", "9999"); - cy.get("@hostInitInput").should("have.value", "8888"); - cy.get("@schedulerSelect").should("have.value", "UP"); - cy.get("@schedulerInput").should("have.value", "0.6"); - cy.get("@schedulerCheckbox").should("not.be.checked"); - cy.get("@repotrackerInput").should("have.value", "5"); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/save_function.ts b/apps/spruce/cypress/integration/adminSettings/save_function.ts deleted file mode 100644 index c72ff75eea..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/save_function.ts +++ /dev/null @@ -1,620 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("admin settings save properly", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("saves changes in each section independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial values for all settings sections - const configDir = "Config Directory"; - cy.getInputByLabel(configDir).as("configDirInput"); - cy.get("@configDirInput").clear(); - cy.get("@configDirInput").type("/test/config/dir"); - cy.getInputByLabel("GitHub Organizations").type("testorg{enter}"); - - const financeFormula = "Finance Formula"; - cy.getInputByLabel(financeFormula).as("financeFormulaInput"); - cy.get("@financeFormulaInput").clear(); - cy.get("@financeFormulaInput").type("0.75"); - - cy.dataCy("bucket-config").within(() => { - cy.getInputByLabel("Default Log Bucket").as("logBucketInput"); - cy.get("@logBucketInput").clear(); - cy.get("@logBucketInput").type("test-logs"); - }); - - cy.dataCy("ssh-pairs").within(() => { - cy.getInputByLabel("Name").first().as("taskHostKeyNameInput"); - cy.get("@taskHostKeyNameInput").clear(); - cy.get("@taskHostKeyNameInput").type("test-task-host-key"); - - cy.getInputByLabel("Secret ARN").first().as("taskHostSecretARNInput"); - cy.get("@taskHostSecretARNInput").clear(); - cy.get("@taskHostSecretARNInput").type("test-task-host-secret-arn"); - }); - - cy.dataCy("host-jasper").within(() => { - cy.getInputByLabel("Binary Name").as("binaryNameInput"); - cy.get("@binaryNameInput").clear(); - cy.get("@binaryNameInput").type("test-jasper"); - }); - - cy.getInputByLabel("Total Spawn Hosts Per User").as("spawnHostInput"); - cy.get("@spawnHostInput").clear(); - cy.get("@spawnHostInput").type("10"); - - cy.getInputByLabel("Permanently Exempt Hosts").type("test-host{enter}"); - - cy.dataCy("tracer-configuration").within(() => { - cy.getInputByLabel("Enable tracer").check({ force: true }); - cy.getInputByLabel("Collector Endpoint").as("collectorEndpointInput"); - cy.get("@collectorEndpointInput").clear(); - cy.get("@collectorEndpointInput").type("https://test-collector.com"); - cy.getInputByLabel("Trace URL Template").as("traceUrlTemplateInput"); - cy.get("@traceUrlTemplateInput").clear(); - cy.get("@traceUrlTemplateInput").type("https://apm.test.com/trace/%s"); - }); - - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify initial changes persisted - cy.getInputByLabel(configDir).should("have.value", "/test/config/dir"); - cy.dataCy("misc-settings").within(() => { - cy.dataCy("filter-chip").contains("testorg").should("exist"); - }); - cy.getInputByLabel(financeFormula).should("have.value", "0.75"); - cy.dataCy("bucket-config").within(() => { - cy.getInputByLabel("Default Log Bucket").should( - "have.value", - "test-logs", - ); - }); - cy.dataCy("ssh-pairs").within(() => { - cy.getInputByLabel("Name") - .first() - .should("have.value", "test-task-host-key"); - cy.getInputByLabel("Secret ARN") - .first() - .should("have.value", "test-task-host-secret-arn"); - }); - cy.dataCy("host-jasper").within(() => { - cy.getInputByLabel("Binary Name").should("have.value", "test-jasper"); - }); - cy.getInputByLabel("Total Spawn Hosts Per User").should("have.value", "10"); - cy.dataCy("sleep-schedule").within(() => { - cy.dataCy("filter-chip").contains("test-host").should("exist"); - }); - cy.dataCy("tracer-configuration").within(() => { - cy.getInputByLabel("Enable tracer").should("be.checked"); - cy.getInputByLabel("Collector Endpoint").should( - "have.value", - "https://test-collector.com", - ); - cy.getInputByLabel("Trace URL Template").should( - "have.value", - "https://apm.test.com/trace/%s", - ); - }); - - // Modify single section - cy.getInputByLabel("Total Project Limit").as("projectLimitInput"); - cy.get("@projectLimitInput").clear(); - cy.get("@projectLimitInput").type("200"); - cy.dataCy("project-creation-settings").within(() => { - cy.contains("Add repository exception").click(); - cy.getInputByLabel("Repository").clear(); - cy.getInputByLabel("Repository").type("5"); - }); - - // Save single section change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify changes - cy.getInputByLabel("Total Project Limit").should("have.value", "200"); - cy.dataCy("project-creation-settings").within(() => { - cy.getInputByLabel("Repository").should("have.value", "5"); - }); - - // Verify other sections unchanged - cy.getInputByLabel(configDir).should("have.value", "/test/config/dir"); - cy.dataCy("misc-settings").within(() => { - cy.dataCy("filter-chip").contains("testorg").should("exist"); - }); - cy.getInputByLabel(financeFormula).should("have.value", "0.75"); - cy.dataCy("bucket-config").within(() => { - cy.getInputByLabel("Default Log Bucket").should( - "have.value", - "test-logs", - ); - }); - cy.dataCy("ssh-pairs").within(() => { - cy.getInputByLabel("Name") - .first() - .should("have.value", "test-task-host-key"); - cy.getInputByLabel("Secret ARN") - .first() - .should("have.value", "test-task-host-secret-arn"); - }); - cy.dataCy("host-jasper").within(() => { - cy.getInputByLabel("Binary Name").should("have.value", "test-jasper"); - }); - cy.getInputByLabel("Total Spawn Hosts Per User").should("have.value", "10"); - cy.dataCy("sleep-schedule").within(() => { - cy.dataCy("filter-chip").contains("test-host").should("exist"); - }); - cy.dataCy("tracer-configuration").within(() => { - cy.getInputByLabel("Enable tracer").should("be.checked"); - cy.getInputByLabel("Collector Endpoint").should( - "have.value", - "https://test-collector.com", - ); - cy.getInputByLabel("Trace URL Template").should( - "have.value", - "https://apm.test.com/trace/%s", - ); - }); - }); - - describe("save parameter store values independently", () => { - it("saves Okta Client Secret parameter store value independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial Okta Client Secret value - cy.dataCy("okta").within(() => { - const oktaClientSecret = "Client Secret"; - cy.getInputByLabel(oktaClientSecret).as("oktaClientSecretInput"); - cy.get("@oktaClientSecretInput").clear(); - cy.get("@oktaClientSecretInput").type("test-okta-secret"); - }); - // Set Kanopy Authentication values (for auth validation to work) - cy.dataCy("kanopy").within(() => { - const headerName = "Header Name"; - const issuer = "Issuer"; - const keysetUrl = "Keyset URL"; - cy.getInputByLabel(headerName).as("headerNameInput"); - cy.get("@headerNameInput").clear(); - cy.get("@headerNameInput").type("test-header-name"); - - cy.getInputByLabel(issuer).as("issuerInput"); - cy.get("@issuerInput").clear(); - cy.get("@issuerInput").type("test-issuer"); - - cy.getInputByLabel(keysetUrl).as("keysetUrlInput"); - cy.get("@keysetUrlInput").clear(); - cy.get("@keysetUrlInput").type("test-keyset-url"); - }); - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify parameter store value persisted - cy.dataCy("okta").within(() => { - cy.getInputByLabel("Client Secret").should( - "have.value", - "test-okta-secret", - ); - }); - - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type("Okta param store test"); - - // Save banner change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify banner message changed - cy.getInputByLabel("Banner Text").should( - "have.value", - "Okta param store test", - ); - - // Verify parameter store value remained unchanged - cy.dataCy("okta").within(() => { - cy.getInputByLabel("Client Secret").should( - "have.value", - "test-okta-secret", - ); - }); - }); - - it("saves Jira Personal Access Token parameter store value independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial Jira PAT value - cy.dataCy("jira").within(() => { - const jiraPat = "Personal Access Token"; - cy.getInputByLabel(jiraPat).as("jiraPat"); - cy.get("@jiraPat").clear(); - cy.get("@jiraPat").type("test-jira-pat"); - }); - - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify parameter store value persisted - cy.dataCy("jira").within(() => { - cy.getInputByLabel("Personal Access Token").should( - "have.value", - "test-jira-pat", - ); - }); - - // Change banner message - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type("Jira param store test"); - - // Save banner change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify banner message changed - cy.getInputByLabel("Banner Text").should( - "have.value", - "Jira param store test", - ); - - // Verify parameter store value remained unchanged - cy.dataCy("jira").within(() => { - cy.getInputByLabel("Personal Access Token").should( - "have.value", - "test-jira-pat", - ); - }); - }); - - it("saves Slack and Splunk token parameter store values independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial Slack Token value - cy.dataCy("slack").within(() => { - const slackToken = "Token"; - cy.getInputByLabel(slackToken).as("slackTokenInput"); - cy.get("@slackTokenInput").clear(); - cy.get("@slackTokenInput").type("xoxb-test-slack-token"); - }); - - // Set initial Splunk Token value - cy.dataCy("splunk").within(() => { - const splunkToken = "Token"; - cy.getInputByLabel(splunkToken).as("splunkTokenInput"); - cy.get("@splunkTokenInput").clear(); - cy.get("@splunkTokenInput").type("test-splunk-token"); - }); - - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify parameter store values persisted - cy.dataCy("slack").within(() => { - cy.getInputByLabel("Token").should( - "have.value", - "xoxb-test-slack-token", - ); - }); - - cy.dataCy("splunk").within(() => { - cy.getInputByLabel("Token").should("have.value", "test-splunk-token"); - }); - - // Change banner message - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type("Tokens param store test"); - - // Save banner change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify banner message changed - cy.getInputByLabel("Banner Text").should( - "have.value", - "Tokens param store test", - ); - - // Verify parameter store values remained unchanged - cy.dataCy("slack").within(() => { - cy.getInputByLabel("Token").should( - "have.value", - "xoxb-test-slack-token", - ); - }); - - cy.dataCy("splunk").within(() => { - cy.getInputByLabel("Token").should("have.value", "test-splunk-token"); - }); - }); - - it("saves Runtime Environments API Key parameter store value independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial Runtime Environments API Key value - cy.dataCy("runtime-environments").within(() => { - const apiKey = "API Key"; - cy.getInputByLabel(apiKey).as("runtimeEnvApiKey"); - cy.get("@runtimeEnvApiKey").clear(); - cy.get("@runtimeEnvApiKey").type("test-runtime-env-key"); - }); - - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify parameter store value persisted - cy.dataCy("runtime-environments").within(() => { - cy.getInputByLabel("API Key").should( - "have.value", - "test-runtime-env-key", - ); - }); - - // Change banner message - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type( - "Runtime Environments param store test", - ); - - // Save banner change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify banner message changed - cy.getInputByLabel("Banner Text").should( - "have.value", - "Runtime Environments param store test", - ); - - // Verify parameter store value remained unchanged - cy.dataCy("runtime-environments").within(() => { - cy.getInputByLabel("API Key").should( - "have.value", - "test-runtime-env-key", - ); - }); - }); - - it("saves AWS EC2 Keys parameter store values independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial AWS EC2 Keys values - cy.dataCy("aws-configuration").within(() => { - cy.getInputByLabel("EC2 Key").as("ec2KeyInput"); - cy.get("@ec2KeyInput").clear(); - cy.get("@ec2KeyInput").type("test-ec2-key"); - - cy.getInputByLabel("EC2 Secret").as("ec2SecretInput"); - cy.get("@ec2SecretInput").clear(); - cy.get("@ec2SecretInput").type("test-ec2-secret"); - }); - - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify parameter store values persisted - cy.dataCy("aws-configuration").within(() => { - cy.getInputByLabel("EC2 Key").should("have.value", "test-ec2-key"); - cy.getInputByLabel("EC2 Secret").should( - "have.value", - "test-ec2-secret", - ); - }); - - // Change banner message - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type("AWS EC2 param store test"); - - // Save banner change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify banner message changed - cy.getInputByLabel("Banner Text").should( - "have.value", - "AWS EC2 param store test", - ); - - // Verify parameter store values remained unchanged - cy.dataCy("aws-configuration").within(() => { - cy.getInputByLabel("EC2 Key").should("have.value", "test-ec2-key"); - cy.getInputByLabel("EC2 Secret").should( - "have.value", - "test-ec2-secret", - ); - }); - }); - - it("saves S3 Keys parameter store values independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial S3 Keys values - cy.dataCy("bucket-config").within(() => { - cy.getInputByLabel("S3 Key").as("s3KeyInput"); - cy.get("@s3KeyInput").clear(); - cy.get("@s3KeyInput").type("test-s3-key"); - - cy.getInputByLabel("S3 Secret").as("s3SecretInput"); - cy.get("@s3SecretInput").clear(); - cy.get("@s3SecretInput").type("test-s3-secret"); - }); - - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify parameter store values persisted - cy.dataCy("bucket-config").within(() => { - cy.getInputByLabel("S3 Key").should("have.value", "test-s3-key"); - cy.getInputByLabel("S3 Secret").should("have.value", "test-s3-secret"); - }); - - // Change banner message - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type("S3 Keys param store test"); - - // Save banner change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify banner message changed - cy.getInputByLabel("Banner Text").should( - "have.value", - "S3 Keys param store test", - ); - - // Verify parameter store values remained unchanged - cy.dataCy("bucket-config").within(() => { - cy.getInputByLabel("S3 Key").should("have.value", "test-s3-key"); - cy.getInputByLabel("S3 Secret").should("have.value", "test-s3-secret"); - }); - }); - - it("saves GitHub Webhook Secret parameter store value independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial GitHub Webhook Secret value - cy.dataCy("misc-settings").within(() => { - cy.getInputByLabel("Webhook Secret").as("webhookSecretInput"); - cy.get("@webhookSecretInput").clear(); - cy.get("@webhookSecretInput").type("test-webhook-secret"); - }); - - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify parameter store value persisted - cy.dataCy("misc-settings").within(() => { - cy.getInputByLabel("Webhook Secret").should( - "have.value", - "test-webhook-secret", - ); - }); - - // Change banner message - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type( - "GitHub Webhook Secret param store test", - ); - - // Save banner change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify banner message changed - cy.getInputByLabel("Banner Text").should( - "have.value", - "GitHub Webhook Secret param store test", - ); - - // Verify parameter store value remained unchanged - cy.dataCy("misc-settings").within(() => { - cy.getInputByLabel("Webhook Secret").should( - "have.value", - "test-webhook-secret", - ); - }); - }); - - it("saves Expansions List values independently", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // Set initial Expansions List values - cy.dataCy("expansions-list").within(() => { - cy.contains("Add").click(); - cy.getInputByLabel("Key").type("TEST_KEY"); - cy.getInputByLabel("Value").type("test_value"); - }); - - // Save initial changes - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify expansions list values persisted - cy.dataCy("expansions-list").within(() => { - cy.getInputByLabel("Key").should("have.value", "TEST_KEY"); - cy.getInputByLabel("Value").should("have.value", "test_value"); - }); - - // Change banner message - cy.getInputByLabel("Banner Text").clear(); - cy.getInputByLabel("Banner Text").type("Expansions List test"); - - // Save banner change - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - cy.reload(); - - // Verify banner message changed - cy.getInputByLabel("Banner Text").should( - "have.value", - "Expansions List test", - ); - - // Verify expansions list values remained unchanged - cy.dataCy("expansions-list").within(() => { - cy.getInputByLabel("Key").should("have.value", "TEST_KEY"); - cy.getInputByLabel("Value").should("have.value", "test_value"); - }); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/service_flags.ts b/apps/spruce/cypress/integration/adminSettings/service_flags.ts deleted file mode 100644 index e0227835b5..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/service_flags.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("service flags", () => { - beforeEach(() => { - cy.visit("/admin-settings/service-flags"); - }); - - it("can interact with and save service flags", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - cy.get('input[type="checkbox"]').then(($checkboxes) => { - const initialCheckedCount = $checkboxes.filter( - '[aria-checked="true"]', - ).length; - const $target = $checkboxes.filter(':not([aria-checked="true"])').first(); - const idx = $checkboxes.index($target); - - cy.get('input[type="checkbox"]').eq(idx).scrollIntoView(); - cy.get('input[type="checkbox"]').eq(idx).check({ force: true }); - - cy.get('input[type="checkbox"]') - .filter('[aria-checked="true"]') - .should("have.length", initialCheckedCount + 1); - - cy.dataCy("save-settings-button").should( - "not.have.attr", - "aria-disabled", - "true", - ); - - cy.get('input[type="checkbox"]').eq(idx).uncheck({ force: true }); - cy.get('input[type="checkbox"]') - .filter('[aria-checked="true"]') - .should("have.length", initialCheckedCount); - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - cy.get('input[type="checkbox"]').eq(idx).check({ force: true }); - clickSave(); - cy.validateToast("success", "Service flags saved successfully"); - - cy.reload(); - cy.get('input[type="checkbox"]') - .eq(idx) - .should("have.attr", "aria-checked", "true"); - cy.get('input[type="checkbox"]') - .filter('[aria-checked="true"]') - .should("have.length", initialCheckedCount + 1); - - // Restore original state. - cy.get('input[type="checkbox"]').eq(idx).uncheck({ force: true }); - clickSave(); - cy.validateToast("success", "Service flags saved successfully"); - }); - }); -}); diff --git a/apps/spruce/cypress/integration/adminSettings/web.ts b/apps/spruce/cypress/integration/adminSettings/web.ts deleted file mode 100644 index d4b95fba17..0000000000 --- a/apps/spruce/cypress/integration/adminSettings/web.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { clickSave } from "../../utils"; - -describe("web", () => { - beforeEach(() => { - cy.visit("/admin-settings"); - }); - - it("can save after making changes", () => { - cy.dataCy("save-settings-button").should( - "have.attr", - "aria-disabled", - "true", - ); - - // API section. - cy.getInputByLabel("HTTP listen address").as("apiUrlInput"); - cy.get("@apiUrlInput").clear(); - cy.get("@apiUrlInput").type("http://example.com/api"); - - // UI section. - cy.getInputByLabel("UIv2 URL").as("uiUrlInput"); - cy.get("@uiUrlInput").clear(); - cy.get("@uiUrlInput").type("http://example.com/ui"); - - // Disabled GraphQL Queries section. - cy.getInputByLabel("Disabled GraphQL Queries").as("disabledQueriesInput"); - cy.get("@disabledQueriesInput").scrollIntoView(); - cy.get("@disabledQueriesInput").clear(); - cy.get("@disabledQueriesInput").type("query1"); - cy.get("@disabledQueriesInput").type("{enter}"); // Ensure the input is submitted - - clickSave(); - cy.validateToast("success", "Settings saved successfully"); - - cy.reload(); - cy.get("@apiUrlInput").should("have.value", "http://example.com/api"); - cy.get("@uiUrlInput").should("have.value", "http://example.com/ui"); - cy.get("span[data-cy=filter-chip]").contains("query1"); - }); -}); diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 657f3933f3..b696eee8a6 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -66,17 +66,18 @@ export async function validateDatePickerDate( * Selects a date in a LeafyGreen date picker by navigating the year/month dropdowns * and clicking the target day cell. * @param page - The Playwright page object - * @param year - The year to select (e.g., "2025") - * @param month - The abbreviated month name to select (e.g., "Feb") - * @param isoDate - The ISO date string of the day cell to click (e.g., "2025-02-28") + * @param opts - The expected date values + * @param opts.year - The year to select (e.g., "2025") + * @param opts.month - The abbreviated month name to select (e.g., "Feb") + * @param opts.isoDate - The ISO date string of the day cell to click (e.g., "2025-02-28") + * @param dataCy - The data-cy attribute value of the date picker (default: "date-picker") */ export async function selectDatePickerDate( page: Page, - year: string, - month: string, - isoDate: string, + { year = "", month = "", isoDate = "" } = {}, + dataCy = "date-picker", ): Promise { - await page.getByTestId("date-picker").click(); + await page.getByTestId(dataCy).click(); const options = page.getByRole("listbox").getByRole("option"); diff --git a/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts b/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts new file mode 100644 index 0000000000..d2d4d6ac68 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("announcements", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("can save after making changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("some more banner text"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + + await page.reload(); + await expect(bannerText).toHaveValue("some more banner text"); + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts b/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts new file mode 100644 index 0000000000..4c4aefbe7c --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts @@ -0,0 +1,216 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("authentication", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("can save after making changes to authentication settings", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByTestId("navitem-admin-global-config").click(); + + // Global Config section. + const allowServiceUsersCheckbox = page.getByLabel("Allow Service Users"); + const allowServiceUsersId = + await allowServiceUsersCheckbox.getAttribute("id"); + await page.locator(`label[for="${allowServiceUsersId}"]`).click(); + + await page.getByLabel("Background Reauthentication (Mins)").clear(); + await page.getByLabel("Background Reauthentication (Mins)").fill("120"); + + // Okta section. + const okta = page.getByTestId("okta"); + await okta.getByLabel("Client ID").clear(); + await okta.getByLabel("Client ID").fill("test-okta-client-id"); + await okta.getByLabel("Client Secret").clear(); + await okta.getByLabel("Client Secret").fill("test-okta-client-secret"); + await okta.getByLabel("Issuer").clear(); + await okta.getByLabel("Issuer").fill("https://test.okta.com"); + await okta.getByLabel("User Group").clear(); + await okta.getByLabel("User Group").fill("test-users"); + await okta.getByLabel("Expire After (Mins)").clear(); + await okta.getByLabel("Expire After (Mins)").fill("480"); + await okta.getByLabel("Scopes").fill("openid"); + await okta.getByLabel("Scopes").press("Enter"); + await okta.getByLabel("Scopes").fill("profile"); + await okta.getByLabel("Scopes").press("Enter"); + await okta.getByLabel("Scopes").fill("email"); + await okta.getByLabel("Scopes").press("Enter"); + + // GitHub section. + const github = page.getByTestId("github"); + await github.getByLabel("App ID").clear(); + await github.getByLabel("App ID").fill("54321"); + await github.getByLabel("Client ID").clear(); + await github.getByLabel("Client ID").fill("test-github-client-id"); + await github.getByLabel("Client Secret").clear(); + await github.getByLabel("Client Secret").fill("test-github-client-secret"); + await github.getByLabel("Default Owner").clear(); + await github.getByLabel("Default Owner").fill("test-owner"); + await github.getByLabel("Default Repository").clear(); + await github.getByLabel("Default Repository").fill("test-repo"); + await github.getByLabel("Organization").clear(); + await github.getByLabel("Organization").fill("test-org"); + await github.getByLabel("Users").fill("testuser1"); + await github.getByLabel("Users").press("Enter"); + await github.getByLabel("Users").fill("testuser2"); + await github.getByLabel("Users").press("Enter"); + + // Naive section. + const naive = page.getByTestId("naive"); + await naive.getByRole("button", { name: "Add" }).click(); + await naive.getByLabel("Display Name").first().fill("Test User 1"); + await naive.getByLabel("Email").first().fill("test1@example.com"); + await naive.getByLabel("Password").first().fill("password123"); + await naive.getByLabel("Username").first().fill("testuser1"); + + // Multi read-write section. + await page.getByTestId("multi-read-write").click(); + const multiReadWriteOptions = page.getByTestId("multi-read-write-options"); + const oktaCheckbox = multiReadWriteOptions.getByLabel("Okta"); + const oktaCheckboxId = await oktaCheckbox.getAttribute("id"); + await multiReadWriteOptions + .locator(`label[for="${oktaCheckboxId}"]`) + .click(); + await page.getByTestId("multi-read-write").click(); + + // Multi read-only section. + await page.getByTestId("multi-read-only").click(); + const multiReadOnlyOptions = page.getByTestId("multi-read-only-options"); + const naiveCheckbox = multiReadOnlyOptions.getByLabel("Naive"); + const naiveCheckboxId = await naiveCheckbox.getAttribute("id"); + await multiReadOnlyOptions + .locator(`label[for="${naiveCheckboxId}"]`) + .click(); + await page.getByTestId("multi-read-only").click(); + + // Kanopy section. + const kanopy = page.getByTestId("kanopy"); + await kanopy.getByLabel("Header Name").clear(); + await kanopy.getByLabel("Header Name").fill("X-Test-Auth-Token"); + await kanopy.getByLabel("Issuer").clear(); + await kanopy.getByLabel("Issuer").fill("https://test-kanopy.example.com"); + await kanopy.getByLabel("Keyset URL").clear(); + await kanopy + .getByLabel("Keyset URL") + .fill("https://test-kanopy.example.com/.well-known/jwks.json"); + + // OAuth section. + const oauth = page.getByTestId("oauth"); + await oauth.getByLabel("Client ID").clear(); + await oauth.getByLabel("Client ID").fill("oauth-client-id"); + await oauth.getByLabel("Issuer").clear(); + await oauth.getByLabel("Issuer").fill("https://test-oauth.example.com"); + await oauth.getByLabel("Connector ID").clear(); + await oauth.getByLabel("Connector ID").fill("oauth-connector-id"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await page.getByTestId("navitem-admin-global-config").click(); + + // Verify Global Config. + await expect(page.getByLabel("Allow Service Users")).toBeChecked(); + await expect( + page.getByLabel("Background Reauthentication (Mins)"), + ).toHaveValue("120"); + + // Verify Okta settings. + await expect(okta.getByLabel("Client ID")).toHaveValue( + "test-okta-client-id", + ); + await expect(okta.getByLabel("Client Secret")).toHaveValue( + "test-okta-client-secret", + ); + await expect(okta.getByLabel("Issuer")).toHaveValue( + "https://test.okta.com", + ); + await expect(okta.getByLabel("User Group")).toHaveValue("test-users"); + await expect(okta.getByLabel("Expire After (Mins)")).toHaveValue("480"); + await expect(okta.getByTestId("filter-chip")).toHaveCount(3); + await expect( + okta.getByTestId("filter-chip").filter({ hasText: "openid" }), + ).toBeVisible(); + await expect( + okta.getByTestId("filter-chip").filter({ hasText: "profile" }), + ).toBeVisible(); + await expect( + okta.getByTestId("filter-chip").filter({ hasText: "email" }), + ).toBeVisible(); + + // Verify GitHub settings. + await expect(github.getByLabel("App ID")).toHaveValue("54321"); + await expect(github.getByLabel("Client ID")).toHaveValue( + "test-github-client-id", + ); + await expect(github.getByLabel("Client Secret")).toHaveValue( + "test-github-client-secret", + ); + await expect(github.getByLabel("Default Owner")).toHaveValue("test-owner"); + await expect(github.getByLabel("Default Repository")).toHaveValue( + "test-repo", + ); + await expect(github.getByLabel("Organization")).toHaveValue("test-org"); + await expect(github.getByTestId("filter-chip")).toHaveCount(2); + await expect( + github.getByTestId("filter-chip").filter({ hasText: "testuser1" }), + ).toBeVisible(); + await expect( + github.getByTestId("filter-chip").filter({ hasText: "testuser2" }), + ).toBeVisible(); + + // Verify Naive user was added. + await expect(naive.getByLabel("Display Name").first()).toHaveValue( + "Test User 1", + ); + await expect(naive.getByLabel("Email").first()).toHaveValue( + "test1@example.com", + ); + await expect(naive.getByLabel("Password").first()).toHaveValue( + "password123", + ); + await expect(naive.getByLabel("Username").first()).toHaveValue("testuser1"); + + // Verify Multi settings. + await page.getByTestId("multi-read-write").click(); + await expect( + page.getByTestId("multi-read-write-options").getByLabel("Okta"), + ).toBeChecked(); + await page.getByTestId("multi-read-write").click(); + + await page.getByTestId("multi-read-only").click(); + await expect( + page.getByTestId("multi-read-only-options").getByLabel("Naive"), + ).toBeChecked(); + await page.getByTestId("multi-read-only").click(); + + // Verify Kanopy settings. + await expect(kanopy.getByLabel("Header Name")).toHaveValue( + "X-Test-Auth-Token", + ); + await expect(kanopy.getByLabel("Issuer")).toHaveValue( + "https://test-kanopy.example.com", + ); + await expect(kanopy.getByLabel("Keyset URL")).toHaveValue( + "https://test-kanopy.example.com/.well-known/jwks.json", + ); + + // Verify OAuth settings. + await expect(oauth.getByLabel("Client ID")).toHaveValue("oauth-client-id"); + await expect(oauth.getByLabel("Issuer")).toHaveValue( + "https://test-oauth.example.com", + ); + await expect(oauth.getByLabel("Connector ID")).toHaveValue( + "oauth-connector-id", + ); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts new file mode 100644 index 0000000000..b2ad04939f --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from "../../fixtures"; +import { selectOption, validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("background processing", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("can save after making changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + // Amboy section. + await page.getByLabel("Single Worker Name").clear(); + await page.getByLabel("Single Worker Name").fill("new single worker name"); + + await page + .getByTestId("named-queue-list") + .locator("> *") + .first() + .getByTestId("delete-item-button") + .click(); + + // Logger section. + await selectOption(page, "Default Level", "Alert"); + + await page.getByLabel("Redact Keys").fill("aNewRedactedKey"); + await page.getByLabel("Redact Keys").press("Enter"); + + const asyncBufferCheckbox = page.getByLabel( + "Use asynchronous buffered logger", + ); + const asyncBufferId = await asyncBufferCheckbox.getAttribute("id"); + await page.locator(`label[for="${asyncBufferId}"]`).click(); + + // Notification Rate Limits section. + await page.getByLabel("Time Interval (secs)", { exact: true }).clear(); + await page.getByLabel("Time Interval (secs)", { exact: true }).fill("1"); + + // Triggers section. + await page.getByLabel("Distro for Generated Tasks").clear(); + await page.getByLabel("Distro for Generated Tasks").fill("localhost"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Single Worker Name")).toHaveValue( + "new single worker name", + ); + await expect(page.getByLabel("Default Level")).toContainText("Alert"); + await expect( + page.getByTestId("logger").getByTestId("filter-chip"), + ).toHaveCount(4); + await expect( + page + .getByTestId("logger") + .getByTestId("filter-chip") + .filter({ hasText: "aNewRedactedKey" }), + ).toBeVisible(); + await expect( + page.getByLabel("Use asynchronous buffered logger"), + ).not.toBeChecked(); + await expect( + page.getByLabel("Time Interval (secs)", { exact: true }), + ).toHaveValue("1"); + await expect(page.getByLabel("Distro for Generated Tasks")).toHaveValue( + "localhost", + ); + + // Ensure that Notify SES Input has not changed. + await expect(page.getByLabel("SES Email")).toHaveValue( + "evg-sender@email.com", + ); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/external_communications.spec.ts b/apps/spruce/playwright/tests/adminSettings/external_communications.spec.ts new file mode 100644 index 0000000000..394448eab6 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/external_communications.spec.ts @@ -0,0 +1,136 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("external communications", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("can save after making changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByTestId("navitem-admin-jira").click(); + + // JIRA section. + const jira = page.getByTestId("jira"); + await jira.getByLabel("Email").clear(); + await jira.getByLabel("Email").fill("test@example.com"); + await jira.getByLabel("Host").clear(); + await jira.getByLabel("Host").fill("jira.test.com"); + await jira.getByLabel("Personal Access Token").clear(); + await jira.getByLabel("Personal Access Token").fill("password"); + + // Slack section. + const slack = page.getByTestId("slack"); + await slack.getByLabel("Token").clear(); + await slack.getByLabel("Token").fill("xoxb-test-token"); + await slack.getByLabel("App Name").clear(); + await slack.getByLabel("App Name").fill("test-app"); + await slack.getByLabel("Channel").clear(); + await slack.getByLabel("Channel").fill("#test-channel"); + await slack.getByLabel("Fields To Set").fill("field1"); + await slack.getByLabel("Fields To Set").press("Enter"); + await slack.getByLabel("Fields To Set").fill("field2"); + await slack.getByLabel("Fields To Set").press("Enter"); + + // Splunk section. + const splunk = page.getByTestId("splunk"); + await splunk.getByLabel("Server URL").clear(); + await splunk.getByLabel("Server URL").fill("splunk.test.com"); + await splunk.getByLabel("Channel").clear(); + await splunk.getByLabel("Channel").fill("test-logs"); + + // Runtime Environments section. + const runtimeEnv = page.getByTestId("runtime-environments"); + await runtimeEnv.getByLabel("Base URL").clear(); + await runtimeEnv.getByLabel("Base URL").fill("runtime.test.com"); + + // Test Selection section. + const testSelection = page.getByTestId("test-selection"); + await testSelection.getByLabel("URL").clear(); + await testSelection.getByLabel("URL").fill("testselection.test.com"); + + // FWS section. + const fws = page.getByTestId("fws"); + await fws.getByLabel("URL").clear(); + await fws.getByLabel("URL").fill("fws.test.com"); + + // Cedar section. + const cedar = page.getByTestId("cedar"); + await cedar.getByLabel("Database URL").clear(); + await cedar.getByLabel("Database URL").fill("cedar-db.test.com"); + await cedar.getByLabel("Database Name").clear(); + await cedar.getByLabel("Database Name").fill("test-cedar-db"); + await cedar.getByLabel("SPS URL (Vanity, for hosts only)").clear(); + await cedar + .getByLabel("SPS URL (Vanity, for hosts only)") + .fill("sps.test.com"); + await cedar.getByLabel("SPS Kanopy URL").clear(); + await cedar.getByLabel("SPS Kanopy URL").fill("sps-kanopy.test.com"); + + // Sage section. + const sage = page.getByTestId("sage"); + await sage.getByLabel("Base URL").clear(); + await sage.getByLabel("Base URL").fill("sage.test.com"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + // Verify JIRA section. + await expect(jira.getByLabel("Email")).toHaveValue("test@example.com"); + await expect(jira.getByLabel("Host")).toHaveValue("jira.test.com"); + + // Verify Slack section. + await expect(slack.getByLabel("Token")).toHaveValue("xoxb-test-token"); + await expect(slack.getByLabel("App Name")).toHaveValue("test-app"); + await expect(slack.getByLabel("Channel")).toHaveValue("#test-channel"); + await expect(slack.getByTestId("filter-chip")).toHaveCount(2); + await expect( + slack.getByTestId("filter-chip").filter({ hasText: "field1" }), + ).toBeVisible(); + await expect( + slack.getByTestId("filter-chip").filter({ hasText: "field2" }), + ).toBeVisible(); + + // Verify Splunk section. + await expect(splunk.getByLabel("Server URL")).toHaveValue( + "splunk.test.com", + ); + await expect(splunk.getByLabel("Channel")).toHaveValue("test-logs"); + + // Verify Runtime Environments section. + await expect(runtimeEnv.getByLabel("Base URL")).toHaveValue( + "runtime.test.com", + ); + + // Verify Test Selection section. + await expect(testSelection.getByLabel("URL")).toHaveValue( + "testselection.test.com", + ); + + // Verify FWS section. + await expect(fws.getByLabel("URL")).toHaveValue("fws.test.com"); + + // Verify Cedar section. + await expect(cedar.getByLabel("Database URL")).toHaveValue( + "cedar-db.test.com", + ); + await expect(cedar.getByLabel("Database Name")).toHaveValue( + "test-cedar-db", + ); + await expect( + cedar.getByLabel("SPS URL (Vanity, for hosts only)"), + ).toHaveValue("sps.test.com"); + await expect(cedar.getByLabel("SPS Kanopy URL")).toHaveValue( + "sps-kanopy.test.com", + ); + + // Verify Sage section. + await expect(sage.getByLabel("Base URL")).toHaveValue("sage.test.com"); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/navigation.spec.ts b/apps/spruce/playwright/tests/adminSettings/navigation.spec.ts new file mode 100644 index 0000000000..afaedcdaf5 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/navigation.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from "../../fixtures"; + +test.describe("admin settings page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("can navigate to the admin settings page", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("admin-settings-page")).toBeVisible(); + }); + + test("has a side navigation with the correct items", async ({ + authenticatedPage: page, + }) => { + await expect(page.locator("[id=announcements]")).toBeVisible(); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/other.spec.ts b/apps/spruce/playwright/tests/adminSettings/other.spec.ts new file mode 100644 index 0000000000..8d3cfff719 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/other.spec.ts @@ -0,0 +1,415 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("other", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("can save misc settings changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByLabel("Config Directory").clear(); + await page.getByLabel("Config Directory").fill("/new/config/dir"); + + await page.getByLabel("GitHub PR Creator Organization").clear(); + await page + .getByLabel("GitHub PR Creator Organization") + .fill("new.example.com"); + + await page.getByLabel("Shutdown Wait Time (secs)").clear(); + await page.getByLabel("Shutdown Wait Time (secs)").fill("45"); + + await page.getByLabel("GitHub Organizations").fill("neworg"); + await page.getByLabel("GitHub Organizations").press("Enter"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Config Directory")).toHaveValue( + "/new/config/dir", + ); + await expect(page.getByLabel("GitHub PR Creator Organization")).toHaveValue( + "new.example.com", + ); + await expect(page.getByLabel("Shutdown Wait Time (secs)")).toHaveValue( + "45", + ); + await expect( + page + .getByTestId("misc-settings") + .getByTestId("filter-chip") + .filter({ hasText: "neworg" }), + ).toBeVisible(); + }); + + test("can save cost settings changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByLabel("Finance Formula").clear(); + await page.getByLabel("Finance Formula").fill("0.5"); + await page.getByLabel("Savings Plan Discount").clear(); + await page.getByLabel("Savings Plan Discount").fill("0.15"); + await page.getByLabel("On-Demand Discount").clear(); + await page.getByLabel("On-Demand Discount").fill("0.08"); + await page.getByLabel("EBS Cost Discount").clear(); + await page.getByLabel("EBS Cost Discount").fill("0.1"); + await page.getByLabel("Upload Cost Discount").clear(); + await page.getByLabel("Upload Cost Discount").fill("0.12"); + await page.getByLabel("Standard Storage Cost Discount").clear(); + await page.getByLabel("Standard Storage Cost Discount").fill("0.18"); + await page.getByLabel("Infrequent Access Storage Cost Discount").clear(); + await page + .getByLabel("Infrequent Access Storage Cost Discount") + .fill("0.22"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Finance Formula")).toHaveValue("0.5"); + await expect(page.getByLabel("Savings Plan Discount")).toHaveValue("0.15"); + await expect(page.getByLabel("On-Demand Discount")).toHaveValue("0.08"); + await expect(page.getByLabel("EBS Cost Discount")).toHaveValue("0.1"); + await expect(page.getByLabel("Upload Cost Discount")).toHaveValue("0.12"); + await expect(page.getByLabel("Standard Storage Cost Discount")).toHaveValue( + "0.18", + ); + await expect( + page.getByLabel("Infrequent Access Storage Cost Discount"), + ).toHaveValue("0.22"); + }); + + test("can clear S3 cost discount values", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByLabel("Upload Cost Discount").clear(); + await page.getByLabel("Upload Cost Discount").fill("0.12"); + await page.getByLabel("Standard Storage Cost Discount").clear(); + await page.getByLabel("Standard Storage Cost Discount").fill("0.18"); + await page.getByLabel("Infrequent Access Storage Cost Discount").clear(); + await page + .getByLabel("Infrequent Access Storage Cost Discount") + .fill("0.22"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Upload Cost Discount")).toHaveValue("0.12"); + await expect(page.getByLabel("Standard Storage Cost Discount")).toHaveValue( + "0.18", + ); + await expect( + page.getByLabel("Infrequent Access Storage Cost Discount"), + ).toHaveValue("0.22"); + + await page.getByLabel("Upload Cost Discount").clear(); + await page.getByLabel("Standard Storage Cost Discount").clear(); + await page.getByLabel("Infrequent Access Storage Cost Discount").clear(); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Upload Cost Discount")).toHaveValue("0"); + await expect(page.getByLabel("Standard Storage Cost Discount")).toHaveValue( + "0", + ); + await expect( + page.getByLabel("Infrequent Access Storage Cost Discount"), + ).toHaveValue("0"); + }); + + test("can save single task host changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByRole("button", { name: "Add project tasks pair" }).click(); + await page.getByLabel("Project ID / Repo").first().click(); + await expect(page.locator('[role="listbox"]')).toBeVisible(); + await page.locator('[role="option"]').last().click(); + + await page.getByLabel("Allowed Tasks").first().fill("compile"); + await page.getByLabel("Allowed Tasks").first().press("Enter"); + await page.getByLabel("Allowed Tasks").first().fill("test"); + await page.getByLabel("Allowed Tasks").first().press("Enter"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Project ID / Repo")).toHaveCount(3); + }); + + test("can save bucket config changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const bucketConfig = page.getByTestId("bucket-config"); + await bucketConfig.getByLabel("Name").clear(); + await bucketConfig.getByLabel("Name").fill("new-log-bucket"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect( + page.getByTestId("bucket-config").getByLabel("Name"), + ).toHaveValue("new-log-bucket"); + }); + + test("can save SSH pairs changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const sshPairs = page.getByTestId("ssh-pairs"); + const taskHostKey = sshPairs.getByTestId("task-host-key"); + await taskHostKey.getByLabel("Name").clear(); + await taskHostKey.getByLabel("Name").fill("new-task-host-key"); + await taskHostKey.getByLabel("Secret ARN").clear(); + await taskHostKey.getByLabel("Secret ARN").fill("new-task-host-secret-arn"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(taskHostKey.getByLabel("Name")).toHaveValue( + "new-task-host-key", + ); + await expect(taskHostKey.getByLabel("Secret ARN")).toHaveValue( + "new-task-host-secret-arn", + ); + }); + + test("can save expansions changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page + .getByTestId("expansions-list") + .getByRole("button", { name: "Add" }) + .click(); + const newExpansionsRow = page.getByTestId("expansions-list-item").first(); + await newExpansionsRow.getByLabel("Key").fill("NEW_VAR"); + await newExpansionsRow.getByLabel("Value").fill("new_value"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByTestId("expansions-list-item")).toHaveCount(2); + }); + + test("can save host jasper changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const hostJasper = page.getByTestId("host-jasper"); + await hostJasper.getByLabel("Binary Name").clear(); + await hostJasper.getByLabel("Binary Name").fill("new-jasper"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect( + page.getByTestId("host-jasper").getByLabel("Binary Name"), + ).toHaveValue("new-jasper"); + }); + + test("can save JIRA notifications changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const jiraNotifications = page.getByTestId("jira-notifications"); + await jiraNotifications + .getByRole("button", { name: "Add new Jira project" }) + .click(); + + const newJiraNotification = page + .getByTestId("jira-custom-fields-list-item") + .first(); + await newJiraNotification.getByLabel("Project").fill("TEST"); + await newJiraNotification.getByLabel("Components").fill("backend"); + await newJiraNotification.getByLabel("Components").press("Enter"); + await newJiraNotification.getByLabel("Labels").fill("feature"); + await newJiraNotification.getByLabel("Labels").press("Enter"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByTestId("jira-custom-fields-list-item")).toHaveCount( + 3, + ); + }); + + test("can save spawn host changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByLabel("Unexpirable Hosts Per User").clear(); + await page.getByLabel("Unexpirable Hosts Per User").fill("5"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Unexpirable Hosts Per User")).toHaveValue( + "5", + ); + }); + + test("can save sleep schedule changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByLabel("Permanently Exempt Hosts").fill("exempt-host-1"); + await page.getByLabel("Permanently Exempt Hosts").press("Enter"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect( + page + .getByTestId("sleep-schedule") + .getByTestId("filter-chip") + .filter({ hasText: "exempt-host-1" }), + ).toBeVisible(); + }); + + test("can save tracer configuration changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const tracerConfiguration = page.getByTestId("tracer-configuration"); + const tracerEnabledCheckbox = + tracerConfiguration.getByLabel("Enable tracer"); + const tracerEnabledId = await tracerEnabledCheckbox.getAttribute("id"); + await tracerConfiguration + .locator(`label[for="${tracerEnabledId}"]`) + .click(); + + await page.getByLabel("Collector Endpoint").clear(); + await page + .getByLabel("Collector Endpoint") + .fill("https://new-collector.example.com"); + + await page.getByLabel("Trace URL Template").clear(); + await page + .getByLabel("Trace URL Template") + .fill("https://apm.example.com/trace/%s"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect( + page.getByTestId("tracer-configuration").getByLabel("Enable tracer"), + ).toBeChecked(); + await expect(page.getByLabel("Collector Endpoint")).toHaveValue( + "https://new-collector.example.com", + ); + await expect(page.getByLabel("Trace URL Template")).toHaveValue( + "https://apm.example.com/trace/%s", + ); + }); + + test("can save project creation changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByLabel("Total Project Limit").clear(); + await page.getByLabel("Total Project Limit").fill("150"); + + const projectCreationSettings = page.getByTestId( + "project-creation-settings", + ); + await projectCreationSettings + .getByRole("button", { name: "Add repository exception" }) + .click(); + const newProjectCreationException = page + .getByTestId("repo-exceptions-list-item") + .first(); + await newProjectCreationException.getByLabel("Owner").fill("test-owner"); + await newProjectCreationException + .getByLabel("Repository") + .fill("test-repo"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Total Project Limit")).toHaveValue("150"); + await expect(page.getByTestId("repo-exceptions-list-item")).toHaveCount(1); + }); + + test("can save GitHub check run changes", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByLabel("Check Run Limit").clear(); + await page.getByLabel("Check Run Limit").fill("25"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Check Run Limit")).toHaveValue("25"); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts b/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts new file mode 100644 index 0000000000..e9fc5161d5 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from "../../fixtures"; +import { + selectDatePickerDate, + validateDatePickerDate, + validateToast, +} from "../../helpers"; + +test.describe("restart tasks", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings/restart-tasks"); + }); + + test("can restart tasks", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("restart-tasks-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await selectDatePickerDate( + page, + { year: "2020", month: "Feb", isoDate: "2020-02-01" }, + "start-date-picker", + ); + await validateDatePickerDate(page, "start-date-picker", { + year: "2020", + month: "02", + day: "01", + }); + + await selectDatePickerDate( + page, + { year: "2021", month: "Mar", isoDate: "2021-03-01" }, + "end-date-picker", + ); + await validateDatePickerDate(page, "end-date-picker", { + year: "2021", + month: "03", + day: "01", + }); + + await expect(page.getByTestId("restart-tasks-button")).not.toHaveAttribute( + "aria-disabled", + "true", + ); + await page.getByTestId("restart-tasks-button").click(); + await expect(page.getByTestId("restart-tasks-modal")).toBeVisible(); + await expect( + page.getByTestId("restart-tasks-list").locator("> *"), + ).toHaveCount(4); + await page.getByRole("button", { name: "Confirm" }).click(); + await validateToast(page, "success", "Created job to restart 4 tasks."); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/runners.spec.ts b/apps/spruce/playwright/tests/adminSettings/runners.spec.ts new file mode 100644 index 0000000000..1dec4358f5 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/runners.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from "../../fixtures"; +import { selectOption, validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("runners", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("can save after making changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + // Notify section. + await page.getByLabel("SES Email").clear(); + await page.getByLabel("SES Email").fill("new_email@email.com"); + + // Task Limits section. + await page.getByLabel("Max Hourly Patch Tasks Per User").clear(); + await page.getByLabel("Max Hourly Patch Tasks Per User").fill("9999"); + + // Host Init section. + await page.getByLabel("Max Total Dynamic Hosts").clear(); + await page.getByLabel("Max Total Dynamic Hosts").fill("8888"); + + // Scheduler section. + await selectOption(page, "Rounding Rule", "Round up"); + + await page.getByLabel("Default Future Host Fraction").clear(); + await page.getByLabel("Default Future Host Fraction").fill("0.6"); + + const groupVersionsCheckbox = page.getByLabel("Group Versions"); + const groupVersionsId = await groupVersionsCheckbox.getAttribute("id"); + await page.locator(`label[for="${groupVersionsId}"]`).click(); + + // Repotracker section. + await page.getByLabel("New Revisions to Fetch").clear(); + await page.getByLabel("New Revisions to Fetch").fill("5"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("SES Email")).toHaveValue( + "new_email@email.com", + ); + await expect( + page.getByLabel("Max Hourly Patch Tasks Per User"), + ).toHaveValue("9999"); + await expect(page.getByLabel("Max Total Dynamic Hosts")).toHaveValue( + "8888", + ); + await expect(page.getByLabel("Rounding Rule")).toContainText("Round up"); + await expect(page.getByLabel("Default Future Host Fraction")).toHaveValue( + "0.6", + ); + await expect(page.getByLabel("Group Versions")).not.toBeChecked(); + await expect(page.getByLabel("New Revisions to Fetch")).toHaveValue("5"); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts new file mode 100644 index 0000000000..594cdaede1 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts @@ -0,0 +1,514 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("admin settings save properly", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("saves changes in each section independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + await page.getByLabel("Config Directory").clear(); + await page.getByLabel("Config Directory").fill("/test/config/dir"); + await page.getByLabel("GitHub Organizations").fill("testorg"); + await page.getByLabel("GitHub Organizations").press("Enter"); + + await page.getByLabel("Finance Formula").clear(); + await page.getByLabel("Finance Formula").fill("0.75"); + + const bucketConfig = page.getByTestId("bucket-config"); + await bucketConfig.getByLabel("Default Log Bucket").clear(); + await bucketConfig.getByLabel("Default Log Bucket").fill("test-logs"); + + const sshPairs = page.getByTestId("ssh-pairs"); + await sshPairs.getByLabel("Name").first().clear(); + await sshPairs.getByLabel("Name").first().fill("test-task-host-key"); + await sshPairs.getByLabel("Secret ARN").first().clear(); + await sshPairs + .getByLabel("Secret ARN") + .first() + .fill("test-task-host-secret-arn"); + + const hostJasper = page.getByTestId("host-jasper"); + await hostJasper.getByLabel("Binary Name").clear(); + await hostJasper.getByLabel("Binary Name").fill("test-jasper"); + + await page.getByLabel("Total Spawn Hosts Per User").clear(); + await page.getByLabel("Total Spawn Hosts Per User").fill("10"); + + await page.getByLabel("Permanently Exempt Hosts").fill("test-host"); + await page.getByLabel("Permanently Exempt Hosts").press("Enter"); + + const tracerConfiguration = page.getByTestId("tracer-configuration"); + const tracerEnabledCheckbox = + tracerConfiguration.getByLabel("Enable tracer"); + const tracerEnabledId = await tracerEnabledCheckbox.getAttribute("id"); + await tracerConfiguration + .locator(`label[for="${tracerEnabledId}"]`) + .click(); + await tracerConfiguration.getByLabel("Collector Endpoint").clear(); + await tracerConfiguration + .getByLabel("Collector Endpoint") + .fill("https://test-collector.com"); + await tracerConfiguration.getByLabel("Trace URL Template").clear(); + await tracerConfiguration + .getByLabel("Trace URL Template") + .fill("https://apm.test.com/trace/%s"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(page.getByLabel("Config Directory")).toHaveValue( + "/test/config/dir", + ); + await expect( + page + .getByTestId("misc-settings") + .getByTestId("filter-chip") + .filter({ hasText: "testorg" }), + ).toBeVisible(); + await expect(page.getByLabel("Finance Formula")).toHaveValue("0.75"); + await expect(bucketConfig.getByLabel("Default Log Bucket")).toHaveValue( + "test-logs", + ); + await expect(sshPairs.getByLabel("Name").first()).toHaveValue( + "test-task-host-key", + ); + await expect(sshPairs.getByLabel("Secret ARN").first()).toHaveValue( + "test-task-host-secret-arn", + ); + await expect(hostJasper.getByLabel("Binary Name")).toHaveValue( + "test-jasper", + ); + await expect(page.getByLabel("Total Spawn Hosts Per User")).toHaveValue( + "10", + ); + await expect( + page + .getByTestId("sleep-schedule") + .getByTestId("filter-chip") + .filter({ hasText: "test-host" }), + ).toBeVisible(); + await expect(tracerConfiguration.getByLabel("Enable tracer")).toBeChecked(); + await expect( + tracerConfiguration.getByLabel("Collector Endpoint"), + ).toHaveValue("https://test-collector.com"); + await expect( + tracerConfiguration.getByLabel("Trace URL Template"), + ).toHaveValue("https://apm.test.com/trace/%s"); + + // Modify single section. + await page.getByLabel("Total Project Limit").clear(); + await page.getByLabel("Total Project Limit").fill("200"); + const projectCreationSettings = page.getByTestId( + "project-creation-settings", + ); + await projectCreationSettings + .getByRole("button", { name: "Add repository exception" }) + .click(); + const newProjectCreationException = page + .getByTestId("repo-exceptions-list-item") + .first(); + await newProjectCreationException.getByLabel("Owner").fill("owner"); + await newProjectCreationException.getByLabel("Repository").fill("repo"); + + await save(page); + 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", + ); + await expect( + newProjectCreationException.getByLabel("Repository"), + ).toHaveValue("repo"); + + // Verify other sections unchanged. + await expect(page.getByLabel("Config Directory")).toHaveValue( + "/test/config/dir", + ); + await expect( + page + .getByTestId("misc-settings") + .getByTestId("filter-chip") + .filter({ hasText: "testorg" }), + ).toBeVisible(); + await expect(page.getByLabel("Finance Formula")).toHaveValue("0.75"); + await expect(bucketConfig.getByLabel("Default Log Bucket")).toHaveValue( + "test-logs", + ); + await expect(sshPairs.getByLabel("Name").first()).toHaveValue( + "test-task-host-key", + ); + await expect(sshPairs.getByLabel("Secret ARN").first()).toHaveValue( + "test-task-host-secret-arn", + ); + await expect(hostJasper.getByLabel("Binary Name")).toHaveValue( + "test-jasper", + ); + await expect(page.getByLabel("Total Spawn Hosts Per User")).toHaveValue( + "10", + ); + await expect( + page + .getByTestId("sleep-schedule") + .getByTestId("filter-chip") + .filter({ hasText: "test-host" }), + ).toBeVisible(); + await expect(tracerConfiguration.getByLabel("Enable tracer")).toBeChecked(); + await expect( + tracerConfiguration.getByLabel("Collector Endpoint"), + ).toHaveValue("https://test-collector.com"); + await expect( + tracerConfiguration.getByLabel("Trace URL Template"), + ).toHaveValue("https://apm.test.com/trace/%s"); + }); + + test.describe("save parameter store values independently", () => { + test("saves Okta Client Secret parameter store value independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const okta = page.getByTestId("okta"); + await okta.getByLabel("Client Secret").clear(); + await okta.getByLabel("Client Secret").fill("test-okta-secret"); + + const kanopy = page.getByTestId("kanopy"); + await kanopy.getByLabel("Header Name").clear(); + await kanopy.getByLabel("Header Name").fill("test-header-name"); + await kanopy.getByLabel("Issuer").clear(); + await kanopy.getByLabel("Issuer").fill("test-issuer"); + await kanopy.getByLabel("Keyset URL").clear(); + await kanopy.getByLabel("Keyset URL").fill("test-keyset-url"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(okta.getByLabel("Client Secret")).toHaveValue( + "test-okta-secret", + ); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("Okta param store test"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bannerText).toHaveValue("Okta param store test"); + await expect(okta.getByLabel("Client Secret")).toHaveValue( + "test-okta-secret", + ); + }); + + test("saves Jira Personal Access Token parameter store value independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const jira = page.getByTestId("jira"); + await jira.getByLabel("Personal Access Token").clear(); + await jira.getByLabel("Personal Access Token").fill("test-jira-pat"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(jira.getByLabel("Personal Access Token")).toHaveValue( + "test-jira-pat", + ); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("Jira param store test"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bannerText).toHaveValue("Jira param store test"); + await expect(jira.getByLabel("Personal Access Token")).toHaveValue( + "test-jira-pat", + ); + }); + + test("saves Slack and Splunk token parameter store values independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const slack = page.getByTestId("slack"); + await slack.getByLabel("Token").clear(); + await slack.getByLabel("Token").fill("xoxb-test-slack-token"); + + const splunk = page.getByTestId("splunk"); + await splunk.getByLabel("Token").clear(); + await splunk.getByLabel("Token").fill("test-splunk-token"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(slack.getByLabel("Token")).toHaveValue( + "xoxb-test-slack-token", + ); + await expect(splunk.getByLabel("Token")).toHaveValue("test-splunk-token"); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("Tokens param store test"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bannerText).toHaveValue("Tokens param store test"); + await expect(slack.getByLabel("Token")).toHaveValue( + "xoxb-test-slack-token", + ); + await expect(splunk.getByLabel("Token")).toHaveValue("test-splunk-token"); + }); + + test("saves Runtime Environments API Key parameter store value independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const runtimeEnv = page.getByTestId("runtime-environments"); + await runtimeEnv.getByLabel("API Key").clear(); + await runtimeEnv.getByLabel("API Key").fill("test-runtime-env-key"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(runtimeEnv.getByLabel("API Key")).toHaveValue( + "test-runtime-env-key", + ); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("Runtime Environments param store test"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bannerText).toHaveValue( + "Runtime Environments param store test", + ); + await expect(runtimeEnv.getByLabel("API Key")).toHaveValue( + "test-runtime-env-key", + ); + }); + + test("saves AWS EC2 Keys parameter store values independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const awsConfiguration = page.getByTestId("aws-configuration"); + await awsConfiguration.getByLabel("EC2 Key").clear(); + await awsConfiguration.getByLabel("EC2 Key").fill("test-ec2-key"); + await awsConfiguration.getByLabel("EC2 Secret").clear(); + await awsConfiguration.getByLabel("EC2 Secret").fill("test-ec2-secret"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(awsConfiguration.getByLabel("EC2 Key")).toHaveValue( + "test-ec2-key", + ); + await expect(awsConfiguration.getByLabel("EC2 Secret")).toHaveValue( + "test-ec2-secret", + ); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("AWS EC2 param store test"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bannerText).toHaveValue("AWS EC2 param store test"); + await expect(awsConfiguration.getByLabel("EC2 Key")).toHaveValue( + "test-ec2-key", + ); + await expect(awsConfiguration.getByLabel("EC2 Secret")).toHaveValue( + "test-ec2-secret", + ); + }); + + test("saves S3 Keys parameter store values independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const bucketConfig = page.getByTestId("bucket-config"); + await bucketConfig.getByLabel("S3 Key").clear(); + await bucketConfig.getByLabel("S3 Key").fill("test-s3-key"); + await bucketConfig.getByLabel("S3 Secret").clear(); + await bucketConfig.getByLabel("S3 Secret").fill("test-s3-secret"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bucketConfig.getByLabel("S3 Key")).toHaveValue( + "test-s3-key", + ); + await expect(bucketConfig.getByLabel("S3 Secret")).toHaveValue( + "test-s3-secret", + ); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("S3 Keys param store test"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bannerText).toHaveValue("S3 Keys param store test"); + await expect(bucketConfig.getByLabel("S3 Key")).toHaveValue( + "test-s3-key", + ); + await expect(bucketConfig.getByLabel("S3 Secret")).toHaveValue( + "test-s3-secret", + ); + }); + + test("saves GitHub Webhook Secret parameter store value independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const miscSettings = page.getByTestId("misc-settings"); + await miscSettings.getByLabel("Webhook Secret").clear(); + await miscSettings + .getByLabel("Webhook Secret") + .fill("test-webhook-secret"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect( + page.getByTestId("misc-settings").getByLabel("Webhook Secret"), + ).toHaveValue("test-webhook-secret"); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("GitHub Webhook Secret param store test"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bannerText).toHaveValue( + "GitHub Webhook Secret param store test", + ); + await expect( + page.getByTestId("misc-settings").getByLabel("Webhook Secret"), + ).toHaveValue("test-webhook-secret"); + }); + + test("saves Expansions List values independently", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const expansionsList = page.getByTestId("expansions-list"); + await expansionsList.getByRole("button", { name: "Add" }).click(); + const newExpansionsRow = page.getByTestId("expansions-list-item").first(); + await newExpansionsRow.getByLabel("Key").fill("TEST_KEY"); + await newExpansionsRow.getByLabel("Value").fill("test_value"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(newExpansionsRow.getByLabel("Key")).toHaveValue("TEST_KEY"); + await expect(newExpansionsRow.getByLabel("Value")).toHaveValue( + "test_value", + ); + + const announcements = page.getByTestId("announcements-section"); + const bannerText = announcements.getByRole("textbox", { + name: "Banner Text", + }); + await bannerText.clear(); + await bannerText.fill("Expansions List test"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(bannerText).toHaveValue("Expansions List test"); + await expect(newExpansionsRow.getByLabel("Key")).toHaveValue("TEST_KEY"); + await expect(newExpansionsRow.getByLabel("Value")).toHaveValue( + "test_value", + ); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts b/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts new file mode 100644 index 0000000000..90494dc374 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("service flags", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings/service-flags"); + }); + + test("can interact with and save service flags", async ({ + authenticatedPage: page, + }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + const checkedCheckboxes = page.locator( + 'input[type="checkbox"][aria-checked="true"]', + ); + const initialCheckedCount = await checkedCheckboxes.count(); + + // Find the first unchecked checkbox. + const targetCheckbox = page + .locator('input[type="checkbox"]:not([aria-checked="true"])') + .first(); + await targetCheckbox.scrollIntoViewIfNeeded(); + + // Check it via its label. + const checkboxId = await targetCheckbox.getAttribute("id"); + await page.locator(`label[for="${checkboxId}"]`).click(); + + // Verify checked count increased. + await expect(checkedCheckboxes).toHaveCount(initialCheckedCount + 1); + + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "false", + ); + + // Uncheck it again. + await page.locator(`label[for="${checkboxId}"]`).click(); + await expect(checkedCheckboxes).toHaveCount(initialCheckedCount); + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + // Re-check and save. + await page.locator(`label[for="${checkboxId}"]`).click(); + await save(page); + await validateToast(page, "success", "Service flags saved successfully"); + await page.reload(); + + await expect(page.locator(`#${checkboxId}`)).toHaveAttribute( + "aria-checked", + "true", + ); + await expect(checkedCheckboxes).toHaveCount(initialCheckedCount + 1); + + // Restore original state. + await page.locator(`label[for="${checkboxId}"]`).click(); + await save(page); + await validateToast(page, "success", "Service flags saved successfully"); + }); +}); diff --git a/apps/spruce/playwright/tests/adminSettings/utils.ts b/apps/spruce/playwright/tests/adminSettings/utils.ts new file mode 100644 index 0000000000..87171d7b5f --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/utils.ts @@ -0,0 +1,8 @@ +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(); +}; diff --git a/apps/spruce/playwright/tests/adminSettings/web.spec.ts b/apps/spruce/playwright/tests/adminSettings/web.spec.ts new file mode 100644 index 0000000000..a3b9738c53 --- /dev/null +++ b/apps/spruce/playwright/tests/adminSettings/web.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { save } from "./utils"; + +test.describe("web", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto("/admin-settings"); + }); + + test("can save after making changes", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "true", + ); + + // API section. + const apiSection = page.getByTestId("api-settings"); + await apiSection.getByLabel("HTTP Listen Address").clear(); + await apiSection + .getByLabel("HTTP Listen Address") + .fill("http://example.com/api"); + + // UI section. + const uiSection = page.getByTestId("ui-settings"); + await uiSection.getByLabel("UIv2 URL").clear(); + await uiSection.getByLabel("UIv2 URL").fill("http://example.com/ui"); + + // 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(); + await disabledGQLQueriesSection + .getByLabel("Disabled GraphQL Queries") + .fill("query1"); + await disabledGQLQueriesSection + .getByLabel("Disabled GraphQL Queries") + .press("Enter"); + + await save(page); + await validateToast(page, "success", "Settings saved successfully"); + await page.reload(); + + await expect(apiSection.getByLabel("HTTP Listen Address")).toHaveValue( + "http://example.com/api", + ); + await expect(uiSection.getByLabel("UIv2 URL")).toHaveValue( + "http://example.com/ui", + ); + await expect( + disabledGQLQueriesSection + .getByTestId("filter-chip") + .filter({ hasText: "query1" }), + ).toBeVisible(); + }); +}); diff --git a/apps/spruce/playwright/tests/spawn/host.spec.ts b/apps/spruce/playwright/tests/spawn/host.spec.ts index 444e7a8f95..961b65248c 100644 --- a/apps/spruce/playwright/tests/spawn/host.spec.ts +++ b/apps/spruce/playwright/tests/spawn/host.spec.ts @@ -338,7 +338,11 @@ test.describe("Spawn Host page", () => { const saveButton = page.getByRole("button", { name: "Save" }); // Set a valid near-future date - await selectDatePickerDate(page, "2026", "Jun", "2026-06-01"); + await selectDatePickerDate(page, { + year: "2026", + month: "Jun", + isoDate: "2026-06-01", + }); await expect(saveButton).toHaveAttribute("aria-disabled", "false"); // Set a date in the past diff --git a/apps/spruce/playwright/tests/task/task_history.spec.ts b/apps/spruce/playwright/tests/task/task_history.spec.ts index 6d01ae770f..a00d7dcb63 100644 --- a/apps/spruce/playwright/tests/task/task_history.spec.ts +++ b/apps/spruce/playwright/tests/task/task_history.spec.ts @@ -440,7 +440,11 @@ test.describe("task history", () => { await page.goto(mciTaskHistoryLink); await page.getByTestId("expanded-option").click(); - await selectDatePickerDate(page, "2025", "Feb", "2025-02-28"); + await selectDatePickerDate(page, { + year: "2025", + month: "Feb", + isoDate: "2025-02-28", + }); await expect(page).toHaveURL(/2025-02-28/); await validateDatePickerDate(page, "date-picker", { year: "2025", @@ -468,7 +472,11 @@ test.describe("task history", () => { await page.goto(mciTaskHistoryLink); await page.getByTestId("expanded-option").click(); - await selectDatePickerDate(page, "2025", "Feb", "2025-02-28"); + await selectDatePickerDate(page, { + year: "2025", + month: "Feb", + isoDate: "2025-02-28", + }); await expect(page).toHaveURL(/2025-02-28/); await validateDatePickerDate(page, "date-picker", { year: "2025", @@ -540,7 +548,11 @@ test.describe("task history", () => { const firstTaskCard = page.getByTestId("commit-details-card").nth(0); await expect(firstTaskCard.getByText("Order: 12306")).toBeVisible(); - await selectDatePickerDate(page, "2025", "Feb", "2025-02-28"); + await selectDatePickerDate(page, { + year: "2025", + month: "Feb", + isoDate: "2025-02-28", + }); await expect(firstTaskCard.getByText("Order: 12306")).toBeHidden(); await page.getByTestId("jump-to-this-task-button").click(); diff --git a/apps/spruce/playwright/tests/waterfall/filters.spec.ts b/apps/spruce/playwright/tests/waterfall/filters.spec.ts index f3e594ff62..28e624bc97 100644 --- a/apps/spruce/playwright/tests/waterfall/filters.spec.ts +++ b/apps/spruce/playwright/tests/waterfall/filters.spec.ts @@ -188,7 +188,11 @@ test.describe("date filter", () => { await expect(page.getByTestId("waterfall-skeleton")).toBeHidden(); await expect(page).toHaveURL("/project/spruce/waterfall"); - await selectDatePickerDate(page, "2022", "Feb", "2022-02-28"); + await selectDatePickerDate(page, { + year: "2022", + month: "Feb", + isoDate: "2022-02-28", + }); await expect(page).toHaveURL(/date=2022-02-28/); await validateDatePickerDate(page, "date-picker", { year: "2022", diff --git a/apps/spruce/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/index.tsx b/apps/spruce/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/index.tsx index 3778ad7c9f..c299a8f8ba 100644 --- a/apps/spruce/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/index.tsx +++ b/apps/spruce/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/index.tsx @@ -19,8 +19,13 @@ export const ObjectFieldTemplate = ({ }: ObjectFieldTemplateProps) => { const errors = uiSchema["ui:errors"] ?? []; const warnings = uiSchema["ui:warnings"] ?? []; + const dataCy = uiSchema["ui:data-cy"]; return ( -
+
{(uiSchema["ui:title"] || title) && ( = { }, uiSchema: { announcements: { + "ui:data-cy": "announcements-section", "ui:ObjectFieldTemplate": CardFieldTemplate, banner: { "ui:widget": "textarea", diff --git a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/OtherTab/schemaFields.ts b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/OtherTab/schemaFields.ts index d201d6381a..cd1284a1a2 100644 --- a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/OtherTab/schemaFields.ts +++ b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/OtherTab/schemaFields.ts @@ -438,6 +438,7 @@ export const sshPairs = { "ui:objectFieldCss": objectGridCss, taskHostKey: { "ui:fieldCss": nestedObjectGridCss, + "ui:data-cy": "task-host-key", }, spawnHostKey: { "ui:fieldCss": nestedObjectGridCss, @@ -476,6 +477,7 @@ export const expansions = { "ui:ObjectFieldTemplate": CardFieldTemplate, "ui:arrayItemCSS": arrayItemCSS, items: { + "ui:data-cy": "expansions-list-item", value: { "ui:widget": "textarea", }, @@ -574,6 +576,7 @@ export const jiraNotificationsFields = { "ui:fieldCss": fullWidthCss, "ui:arrayItemCSS": arrayItemCSS, items: { + "ui:data-cy": "jira-custom-fields-list-item", fields: { "ui:addButtonText": "Add custom field", "ui:placeholder": "No custom fields defined.", @@ -733,6 +736,9 @@ export const projectCreationSettings = { "ui:fullWidth": true, "ui:fieldCss": fullWidthCss, "ui:arrayItemCSS": arrayItemCSS, + items: { + "ui:data-cy": "repo-exceptions-list-item", + }, }, }, }; diff --git a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/WebTab/schemaFields.ts b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/WebTab/schemaFields.ts index d08eb60e5a..bdd785a323 100644 --- a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/WebTab/schemaFields.ts +++ b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/WebTab/schemaFields.ts @@ -6,7 +6,7 @@ export const api = { schema: { httpListenAddr: { type: "string" as const, - title: "HTTP listen address", + title: "HTTP Listen Address", }, url: { type: "string" as const, @@ -60,7 +60,7 @@ export const ui = { }, httpListenAddr: { type: "string" as const, - title: "HTTP listen address", + title: "HTTP Listen Address", }, secret: { type: "string" as const, diff --git a/apps/spruce/src/pages/adminSettings/tabs/RestartTasksTab/RestartTasksButton.tsx b/apps/spruce/src/pages/adminSettings/tabs/RestartTasksTab/RestartTasksButton.tsx index db75baa13c..5d638ca879 100644 --- a/apps/spruce/src/pages/adminSettings/tabs/RestartTasksTab/RestartTasksButton.tsx +++ b/apps/spruce/src/pages/adminSettings/tabs/RestartTasksTab/RestartTasksButton.tsx @@ -156,7 +156,7 @@ export const RestartTasksButton: React.FC = ({ onClick={() => setModalOpen(true)} variant={ButtonVariant.Primary} > - Preview Restart Tasks + Preview restart tasks ); From 6f106c329dfab46fb5ef8a0a212ec140bbcac970 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 14:46:34 -0400 Subject: [PATCH 08/32] migrate `projectSettings` tests --- apps/spruce/cypress/integration/dummy_test.ts | 12 + .../integration/projectSettings/access.ts | 69 --- .../projectSettings/admin_actions.ts | 77 --- .../projectSettings/attaching_to_repo.ts | 40 -- .../projectSettings/defaulting_to_repo.ts | 372 ------------- .../projectSettings/github_app_settings.ts | 111 ---- .../github_permission_groups.ts | 68 --- .../projectSettings/not_defaulting_to_repo.ts | 274 ---------- .../projectSettings/notifications.ts | 131 ----- .../projectSettings/permissions.ts | 65 --- .../integration/projectSettings/plugins.ts | 79 --- .../projectSettings/project_select.ts | 20 - .../projectSettings/project_settings.ts | 60 -- .../projectSettings/repo_settings.ts | 300 ---------- .../projectSettings/stepback_bisect.ts | 65 --- .../projectSettings/views_and_filters.ts | 53 -- apps/spruce/playwright/helpers/index.ts | 24 + .../tests/projectSettings/access.spec.ts | 87 +++ .../projectSettings/admin_actions.spec.ts | 99 ++++ .../projectSettings/attaching_to_repo.spec.ts | 63 +++ .../tests}/projectSettings/constants.ts | 16 +- .../defaulting_to_repo.spec.ts | 514 ++++++++++++++++++ .../github_app_settings.spec.ts | 105 ++++ .../github_permission_groups.spec.ts | 90 +++ .../not_defaulting_to_repo.spec.ts | 318 +++++++++++ .../projectSettings/notifications.spec.ts | 144 +++++ .../tests/projectSettings/permissions.spec.ts | 63 +++ .../tests/projectSettings/plugins.spec.ts | 92 ++++ .../projectSettings/project_select.spec.ts | 21 + .../projectSettings/project_settings.spec.ts | 73 +++ .../projectSettings/repo_settings.spec.ts | 386 +++++++++++++ .../projectSettings/stepback_bisect.spec.ts | 80 +++ .../playwright/tests/projectSettings/utils.ts | 20 + .../projectSettings/views_and_filters.spec.ts | 75 +++ .../playwright/tests/spawn/host.spec.ts | 19 +- .../GithubCommitQueueTab/getFormSchema.tsx | 1 + .../shared/tabs/PluginsTab/getFormSchema.tsx | 1 + 37 files changed, 2274 insertions(+), 1813 deletions(-) create mode 100644 apps/spruce/cypress/integration/dummy_test.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/access.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/admin_actions.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/attaching_to_repo.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/defaulting_to_repo.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/github_app_settings.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/github_permission_groups.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/not_defaulting_to_repo.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/notifications.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/permissions.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/plugins.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/project_select.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/project_settings.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/repo_settings.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/stepback_bisect.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/views_and_filters.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/access.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/admin_actions.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/attaching_to_repo.spec.ts rename apps/spruce/{cypress/integration => playwright/tests}/projectSettings/constants.ts (66%) create mode 100644 apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/notifications.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/permissions.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/plugins.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/project_select.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/project_settings.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/utils.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts diff --git a/apps/spruce/cypress/integration/dummy_test.ts b/apps/spruce/cypress/integration/dummy_test.ts new file mode 100644 index 0000000000..a9c3a7c8d5 --- /dev/null +++ b/apps/spruce/cypress/integration/dummy_test.ts @@ -0,0 +1,12 @@ +describe("Downstream Projects Tab", () => { + const DOWNSTREAM_ROUTE = `/version/5f74d99ab2373627c047c5e5/downstream-projects`; + + beforeEach(() => { + cy.visit(DOWNSTREAM_ROUTE); + }); + + it("shows number of failed patches in the Downstream tab label", () => { + cy.dataCy("downstream-tab-badge").should("exist"); + cy.dataCy("downstream-tab-badge").should("contain.text", "1"); + }); +}); diff --git a/apps/spruce/cypress/integration/projectSettings/access.ts b/apps/spruce/cypress/integration/projectSettings/access.ts deleted file mode 100644 index 4c40578c70..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/access.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - project, - ProjectSettingsTabRoutes, - projectUseRepoEnabled, - saveButtonEnabled, -} from "./constants"; - -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"); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - 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 2f21e23159..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/attaching_to_repo.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { clickSave } from "../../utils"; -import { getProjectSettingsRoute, project } from "./constants"; - -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", - ); - clickSave(); - 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 4a65261192..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/defaulting_to_repo.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - getRepoSettingsRoute, - project, - ProjectSettingsTabRoutes, - projectUseRepoEnabled, - repo, - saveButtonEnabled, -} from "./constants"; - -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"); - clickSave(); - 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"); - clickSave(); - 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(); - clickSave(); - 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(); - clickSave(); - 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(); - clickSave(); - 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"); - - clickSave(); - 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(".*"); - clickSave(); - 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(); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - cy.validateToast("success", "Successfully updated project"); - // Default to repo patch alias - cy.contains("label", "Default to Repo Patch Aliases").click(); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - 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(); - clickSave(); - 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(); - clickSave(); - 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 a5a5de034e..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/github_app_settings.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; - -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); - clickSave(); - 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); - clickSave(); - 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 5a987c3ffc..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/github_permission_groups.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; - -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(); - clickSave(); - 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(); - clickSave(); - 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(); - clickSave(); - 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 3a3b930e26..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/not_defaulting_to_repo.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - project, - saveButtonEnabled, -} from "./constants"; - -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(); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - 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(); - clickSave(); - 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"); - - clickSave(); - 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"); - clickSave(); - 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 06c7ac4b28..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/notifications.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; - -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(); - clickSave(); - 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(); - clickSave(); - 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); - clickSave(); - 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(); - clickSave(); - - // 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 df336714e1..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/plugins.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - projectUseRepoEnabled, -} from "./constants"; - -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(); - clickSave(); - - 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(); - clickSave(); - - 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 61de206e19..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/project_settings.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - project, - ProjectSettingsTabRoutes, -} from "./constants"; - -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); - clickSave(); - 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 af6603b19f..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/repo_settings.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - getRepoSettingsRoute, - ProjectSettingsTabRoutes, - projectUseRepoEnabled, - repo, - saveButtonEnabled, -} from "./constants"; - -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"); - clickSave(); - 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 defintion 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); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - 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"); - clickSave(); - cy.validateToast("success", "Successfully updated repo"); - cy.dataCy("array-down-button").click(); - cy.dataCy("save-settings-button").scrollIntoView(); - clickSave(); - 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 3e79f3e942..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/stepback_bisect.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - project, - projectUseRepoEnabled, -} from "./constants"; - -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(); - clickSave(); - 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(); - - clickSave(); - 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/views_and_filters.ts b/apps/spruce/cypress/integration/projectSettings/views_and_filters.ts deleted file mode 100644 index c909da518a..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/views_and_filters.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { clickSave } from "../../utils"; -import { - getProjectSettingsRoute, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; - -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); - clickSave(); - 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(); - clickSave(); - cy.validateToast("success", "Successfully updated project"); - cy.dataCy("parsley-filter-list").children().should("have.length", 2); - }); - }); -}); diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index b696eee8a6..9a5c57d9d8 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -96,6 +96,30 @@ export async function selectDatePickerDate( await page.locator(`[data-iso='${isoDate}']`).click(); } +/** + * Types a date into a LeafyGreen date picker by filling the year, month, and day inputs. + * @param page - The Playwright page object + * @param opts - The expected date values + * @param opts.year - The year to select (e.g., "2025") + * @param opts.month - The numerical month value to select (e.g., "02") + * @param opts.day - The day to select (e.g., "28") + * @param dataCy - The data-cy attribute value of the date picker (default: "date-picker") + */ +export async function typeDatePickerDate( + page: Page, + { year = "", month = "", day = "" } = {}, + dataCy = "date-picker", +): Promise { + const datePicker = page.getByTestId(dataCy); + const yearInput = datePicker.locator("input[id='year']"); + const monthInput = datePicker.locator("input[id='month']"); + const dayInput = datePicker.locator("input[id='day']"); + + await yearInput.fill(year); + await monthInput.fill(month); + await dayInput.fill(day); +} + // Re-export shared helpers from the playwright-config package. export { validateToast, diff --git a/apps/spruce/playwright/tests/projectSettings/access.spec.ts b/apps/spruce/playwright/tests/projectSettings/access.spec.ts new file mode 100644 index 0000000000..a4b3867d86 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/access.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from "../../fixtures"; +import { 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); + await expect(page.getByTestId("default-to-repo-button")).toBeVisible(); + await expect(page.getByTestId("default-to-repo-button")).toBeEnabled(); + }); + + test("Changing settings and clicking the save button produces a success toast and the changes are persisted", async ({ + authenticatedPage: page, + }) => { + await page.getByText("Unrestricted", { exact: true }).click(); + await expect( + page.getByLabel("Unrestricted", { exact: true }), + ).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, + }) => { + await expect(page.getByTestId("default-to-repo-button")).toHaveAttribute( + "aria-disabled", + "false", + ); + await page.getByTestId("default-to-repo-button").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"); + await expect( + page.getByLabel("Default to repo (unrestricted)"), + ).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/projectSettings/admin_actions.spec.ts b/apps/spruce/playwright/tests/projectSettings/admin_actions.spec.ts new file mode 100644 index 0000000000..4a485edcc4 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/admin_actions.spec.ts @@ -0,0 +1,99 @@ +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.getByTestId("delete-project-button").scrollIntoViewIfNeeded(); + await page.getByTestId("delete-project-button").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/projectSettings/attaching_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/attaching_to_repo.spec.ts new file mode 100644 index 0000000000..43e4d7c532 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/attaching_to_repo.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from "../../fixtures"; +import { 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")).toHaveAttribute( + "aria-disabled", + "true", + ); + await save(page); + await validateToast(page, "success", "Successfully updated project", true); + + 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"); + + 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(); + await page + .getByTestId("cq-enabled-radio-box") + .getByText("Enabled", { exact: true }) + .click(); + await expect( + page.getByTestId("cq-card").getByTestId("error-banner"), + ).toBeVisible(); + }); +}); diff --git a/apps/spruce/cypress/integration/projectSettings/constants.ts b/apps/spruce/playwright/tests/projectSettings/constants.ts similarity index 66% rename from apps/spruce/cypress/integration/projectSettings/constants.ts rename to apps/spruce/playwright/tests/projectSettings/constants.ts index bba58cf444..efd3818d1a 100644 --- a/apps/spruce/cypress/integration/projectSettings/constants.ts +++ b/apps/spruce/playwright/tests/projectSettings/constants.ts @@ -18,25 +18,13 @@ export enum ProjectSettingsTabRoutes { 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/projectSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts new file mode 100644 index 0000000000..a03fa84aaa --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts @@ -0,0 +1,514 @@ +import { test, expect } from "../../fixtures"; +import { 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, + }) => { + await expect(page.getByTestId("default-to-repo-button")).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.getByTestId("add-button").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"); + await page.locator("label", { hasText: "Private" }).click(); + + await page.getByTestId("add-button").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.getByTestId("add-button").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 expect(page.getByTestId("promote-vars-modal")).toHaveCount(0); + await page.getByTestId("promote-vars-button").click(); + await expect(page.getByTestId("promote-vars-modal")).toBeVisible(); + + const variableToPromote = page + .getByTestId("promote-var-checkbox") + .first(); + const checkboxId = await variableToPromote.getAttribute("id"); + await page.locator(`label[for="${checkboxId}"]`).click(); + + 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"); + await githubSection + .getByText("Override Repo Patch Definition", { exact: true }) + .click(); + await expect( + githubSection.getByLabel("Override Repo Patch Definition", { + exact: true, + }), + ).toBeChecked(); + + 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, + }) => { + await page + .getByTestId("github-checks-enabled-radio-box") + .scrollIntoViewIfNeeded(); + await page + .getByTestId("github-checks-enabled-radio-box") + .locator("label", { hasText: "Enabled" }) + .click(); + 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, + }) => { + await expect( + page.getByTestId("cq-override-radio-box").locator("input").first(), + ).toBeChecked(); + }); + + 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, + }) => { + await expect( + page + .getByTestId("pr-testing-enabled-radio-box") + .getByText("Default to repo (enabled)"), + ).toBeVisible(); + await page + .getByTestId("pr-testing-enabled-radio-box") + .locator("label", { hasText: /^Disabled$/ }) + .click(); + await page + .getByTestId("manual-pr-testing-enabled-radio-box") + .locator("label", { hasText: /^Disabled$/ }) + .click(); + await page + .getByTestId("github-checks-enabled-radio-box") + .locator("label", { hasText: /^Enabled$/ }) + .click(); + 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 page.getByTestId("accordion-toggle").scrollIntoViewIfNeeded(); + 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, + }) => { + await expect( + page.getByLabel("Default to Repo Patch Aliases"), + ).toHaveAttribute("checked"); + }); + + test("Patch aliases added before defaulting to repo patch aliases are cleared", async ({ + authenticatedPage: page, + }) => { + await page + .locator("label", { hasText: "Override Repo Patch Aliases" }) + .click(); + await expect( + page.getByLabel("Override Repo Patch Aliases"), + ).toHaveAttribute("aria-checked", "true"); + await expectSaveButtonEnabled(page, false); + + await page + .getByTestId("add-button") + .filter({ hasText: "Add Patch Alias" }) + .click({ force: true }); + 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"); + + await page + .locator("label", { hasText: "Default to Repo Patch Aliases" }) + .click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expectSaveButtonEnabled(page, false); + + await page + .locator("label", { hasText: "Override Repo Patch Aliases" }) + .click(); + 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 }) => { + await page.locator("label", { hasText: "Enabled" }).click(); + await expect(page.getByLabel("Enabled")).toBeChecked(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); + + test("Add commands", async ({ authenticatedPage: page }) => { + await expect(page.getByLabel("Default to repo (disabled)")).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"); + + await page.goto(origin); + await page.getByTestId("navitem-virtual-workstation").click(); + await expect( + page + .getByTestId("command-row") + .locator("textarea", { hasText: "a repo command" }), + ).toHaveAttribute("aria-disabled", "true"); + + await page + .locator("label", { hasText: "Override Repo Commands" }) + .click(); + await expect(page.getByTestId("command-row")).toHaveCount(0); + await page.getByRole("button", { name: "Add Command" }).click(); + await page.getByTestId("command-input").fill("a project command"); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect( + page + .getByTestId("command-row") + .locator("textarea", { hasText: "a project command" }), + ).toHaveAttribute("aria-disabled", "false"); + + await page + .locator("label", { hasText: "Default to Repo Commands" }) + .click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect( + page + .getByTestId("command-row") + .locator("textarea", { hasText: "a repo command" }), + ).toHaveAttribute("aria-disabled", "true"); + + await page + .locator("label", { hasText: "Override Repo Commands" }) + .click(); + await expect(page.getByTestId("command-row")).toHaveCount(0); + }); + + test("Allows overriding without adding a command", async ({ + authenticatedPage: page, + }) => { + await page + .locator("label", { hasText: "Override Repo Commands" }) + .click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect(page.getByLabel("Override Repo Commands")).toBeChecked(); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts new file mode 100644 index 0000000000..2bf59f3544 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts @@ -0,0 +1,105 @@ +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(); + await expect( + page.getByTestId("replace-github-credentials-modal"), + ).toBeVisible(); + + await expect( + page + .getByTestId("replace-github-credentials-modal") + .getByRole("button", { name: "Replace" }), + ).toHaveAttribute("aria-disabled", "true"); + + await page.getByTestId("replace-app-id-input").fill("99999"); + await page.getByTestId("replace-private-key-input").fill("new-private-key"); + + await expect( + page + .getByTestId("replace-github-credentials-modal") + .getByRole("button", { name: "Replace" }), + ).not.toHaveAttribute("aria-disabled", "true"); + + await page + .getByTestId("replace-github-credentials-modal") + .getByRole("button", { name: "Replace" }) + .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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await expectSaveButtonEnabled(page, true); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts b/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts new file mode 100644 index 0000000000..d015daa333 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts @@ -0,0 +1,90 @@ +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-list").locator("> *"), + ).toHaveCount(0); + await expectSaveButtonEnabled(page, false); + }); + + test("should throw an error if permission group definitions are invalid", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByRole("button", { name: /^Add permission group$/ }), + ).toBeVisible(); + await page.getByRole("button", { name: /^Add permission group$/ }).click(); + await expect( + page.getByTestId("permission-group-list").locator("> *"), + ).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({ force: true }); + await expectSaveButtonEnabled(page, true); + await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + 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, + }) => { + await expect( + page.getByRole("button", { name: /^Add permission group$/ }), + ).toBeVisible(); + await page.getByRole("button", { name: /^Add permission group$/ }).click(); + await expect( + page.getByTestId("permission-group-list").locator("> *"), + ).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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await page.reload(); + await expect( + page.getByTestId("permission-group-list").locator("> *"), + ).toHaveCount(1); + await page.getByTestId("delete-item-button").click(); + await expect( + page.getByTestId("permission-group-list").locator("> *"), + ).toHaveCount(0); + await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); +}); diff --git a/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts new file mode 100644 index 0000000000..05f2adb572 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts @@ -0,0 +1,318 @@ +import { test, expect } from "../../fixtures"; +import { + validateToast, + validateDatePickerDate, + clearDatePickerInput, + typeDatePickerDate, +} from "../../helpers"; +import { getProjectSettingsRoute, project } from "./constants"; +import { expectSaveButtonEnabled, save } 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"); + }); + + test("Allows enabling Run Every Mainline Commit", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("navitem-general").click(); + await page + .getByTestId("run-every-mainline-commit-radio-box") + .getByText("Enabled") + .click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect( + page + .getByTestId("run-every-mainline-commit-radio-box") + .getByLabel("Enabled"), + ).toHaveAttribute("aria-checked", "true"); + }); + + test.describe("Variables page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + 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.getByTestId("add-button").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"); + await expect(page.getByTestId("var-private-input")).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")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("var-value-input")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("var-private-input")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("var-admin-input")).toHaveAttribute( + "aria-disabled", + "false", + ); + await expect(page.getByTestId("var-description-input")).toHaveAttribute( + "aria-disabled", + "false", + ); + }); + + test("Typing a duplicate variable name will disable saving and show an error message", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("add-button").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.getByTestId("add-button").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.getByTestId("add-button").click(); + await page.getByTestId("var-name-input").first().fill("admin_var"); + await page.getByTestId("var-value-input").first().fill("admin_value"); + await page.locator("label", { hasText: "Admin Only" }).click(); + await expect(page.getByTestId("var-admin-input")).toBeChecked(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + }); + + test("Should persist saved variables and allow deletion", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("add-button").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.getByTestId("add-button").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.getByTestId("add-button").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); + }); + }); + + test.describe("GitHub page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.getByTestId("navitem-github-commitqueue").click(); + }); + + test("Allows adding a git tag alias", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId("git-tag-enabled-radio-box") + .getByText("Enabled") + .click(); + await page + .getByTestId("add-button") + .filter({ hasText: "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", + ); + }); + }); + + test.describe("Periodic Builds page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + 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.getByTestId("add-button").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.getByTestId("add-button").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"); + }); + }); + + test.describe("Project Triggers page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.getByTestId("navitem-project-triggers").click(); + }); + + test("Saves a project trigger", async ({ authenticatedPage: page }) => { + await page.getByTestId("add-button").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/projectSettings/notifications.spec.ts b/apps/spruce/playwright/tests/projectSettings/notifications.spec.ts new file mode 100644 index 0000000000..5016adbff4 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/notifications.spec.ts @@ -0,0 +1,144 @@ +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); + await expect( + page.getByTestId("add-button").getByText("Add Subscription"), + ).toBeVisible(); + await page.getByTestId("add-button").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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await expectSaveButtonEnabled(page, false); + const subscriptionItem = page.getByTestId("expandable-card"); + await subscriptionItem.scrollIntoViewIfNeeded(); + 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + 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); + await expect( + page.getByTestId("add-button").getByText("Add Subscription"), + ).toBeVisible(); + await page.getByTestId("add-button").click(); + const expandableCard = page.getByTestId("expandable-card"); + await expandableCard.scrollIntoViewIfNeeded(); + 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await expectSaveButtonEnabled(page, false); + }); + + test("should not be able to save a subscription if an input is invalid", async ({ + authenticatedPage: page, + }) => { + await page.getByTestId("add-button").click(); + const expandableCard = page.getByTestId("expandable-card"); + await expandableCard.scrollIntoViewIfNeeded(); + 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + 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/projectSettings/permissions.spec.ts b/apps/spruce/playwright/tests/projectSettings/permissions.spec.ts new file mode 100644 index 0000000000..742ada3057 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/permissions.spec.ts @@ -0,0 +1,63 @@ +import { users } from "@evg-ui/playwright-config/constants"; +import { test, expect } from "../../fixtures"; +import { login, logout } from "../../helpers"; +import { + getProjectSettingsRoute, + getRepoSettingsRoute, + projectUseRepoEnabled, + repo, +} from "./constants"; + +test.describe("project/repo 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(), + ).toHaveAttribute("aria-disabled", "true"); + }); + + 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(), + ).toHaveAttribute("aria-disabled", "false"); + }); + }); + + test.describe("repos", () => { + 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(), + ).toHaveAttribute("aria-disabled", "true"); + }); + + 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(), + ).toHaveAttribute("aria-disabled", "false"); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts b/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts new file mode 100644 index 0000000000..1cc8447e65 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts @@ -0,0 +1,92 @@ +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" }) + .scrollIntoViewIfNeeded(); + await page.getByRole("button", { name: "Add metadata link" }).click(); + + const mostRecentMetadataLink = page + .getByTestId("metadata-link-item") + .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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await save(page); + + await page.goto(patchPage); + 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await save(page); + + await page.goto(patchPage); + await expect(page.getByTestId("external-link")).toHaveCount(0); + }); +}); diff --git a/apps/spruce/playwright/tests/projectSettings/project_select.spec.ts b/apps/spruce/playwright/tests/projectSettings/project_select.spec.ts new file mode 100644 index 0000000000..8f1fbe842f --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/project_select.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "../../fixtures"; +import { getProjectSettingsRoute, project } from "./constants"; + +test.describe("Clicking on The Project Select Dropdown", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test("Headers are clickable", async ({ authenticatedPage: page }) => { + await expect(page.getByTestId("project-select")).toBeVisible(); + await page.getByTestId("project-select").click(); + await expect(page.getByTestId("project-select-options")).toBeVisible(); + await page + .getByTestId("project-select-options") + .getByText("evergreen-ci/evergreen") + .click(); + await expect(page).not.toHaveURL(new RegExp(origin)); + }); +}); diff --git a/apps/spruce/playwright/tests/projectSettings/project_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/project_settings.spec.ts new file mode 100644 index 0000000000..dc058e5365 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/project_settings.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { + getProjectSettingsRoute, + project, + ProjectSettingsTabRoutes, +} from "./constants"; +import { save } from "./utils"; + +test.describe("projectSettings/project_settings", () => { + test.describe("Renaming the identifier", () => { + const origin = getProjectSettingsRoute(project); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test("Update identifier", async ({ authenticatedPage: page }) => { + 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.describe("A project that has GitHub webhooks disabled", () => { + const origin = getProjectSettingsRoute( + "logkeeper", + ProjectSettingsTabRoutes.GithubCommitQueue, + ); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test("Disables all interactive elements on the page", async ({ + authenticatedPage: page, + }) => { + await expect( + page.getByTestId("project-settings-page").getByRole("button").first(), + ).toHaveAttribute("aria-disabled", "true"); + await expect(page.locator("input").first()).toHaveAttribute( + "aria-disabled", + "true", + ); + }); + }); + + test.describe("A 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/projectSettings/repo_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts new file mode 100644 index 0000000000..6e296b6a34 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts @@ -0,0 +1,386 @@ +import { test, expect } from "../../fixtures"; +import { clickCheckboxByLabel, validateToast } from "../../helpers"; +import { + getProjectSettingsRoute, + getRepoSettingsRoute, + ProjectSettingsTabRoutes, + projectUseRepoEnabled, + repo, +} from "./constants"; +import { expectSaveButtonEnabled, save } from "./utils"; + +test.describe("Repo Settings", () => { + const origin = getRepoSettingsRoute(repo); + + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.goto(origin); + }); + + test.describe("General settings page", () => { + 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"); + }); + }); + + test.describe("GitHub page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + 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, + }) => { + await page + .getByTestId("github-checks-enabled-radio-box") + .locator("label", { hasText: "Enabled" }) + .click(); + 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 page + .getByTestId("github-checks-enabled-radio-box") + .locator("label", { hasText: "Disabled" }) + .click(); + await expect(errorBanner).toHaveCount(0); + }); + + test("Allows enabling manual PR testing", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId("manual-pr-testing-enabled-radio-box") + .locator("> *") + .first() + .click(); + }); + + 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.scrollIntoViewIfNeeded(); + 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.beforeEach(async ({ authenticatedPage: page }) => { + await page + .getByTestId("cq-enabled-radio-box") + .locator("label", { hasText: "Enabled" }) + .scrollIntoViewIfNeeded(); + }); + + test("Enabling merge queue shows hidden inputs and error banner", async ({ + authenticatedPage: page, + }) => { + const cqCardFields = page.getByTestId("cq-card").locator("> *"); + await expect(cqCardFields).toHaveCount(2); + + await page + .getByTestId("cq-enabled-radio-box") + .locator("label", { hasText: "Enabled" }) + .click(); + await expect(cqCardFields).toHaveCount(3); + await page + .getByText("Merge Queue Patch Definitions") + .scrollIntoViewIfNeeded(); + 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, + }) => { + await page + .getByTestId("cq-enabled-radio-box") + .locator("label", { hasText: "Enabled" }) + .click(); + await expect(page.getByTestId("cq-override-radio-box")).toHaveCount(0); + }); + + test("Saves a merge queue definition", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId("cq-enabled-radio-box") + .locator("label", { hasText: "Enabled" }) + .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 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"); + }); + }); + }); + + test.describe("Patch Aliases page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + 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 + .getByTestId("add-button") + .filter({ hasText: "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 + .getByTestId("add-button") + .filter({ hasText: "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 githubPRLabel = "Schedule in GitHub Pull Requests"; + const pullRequestCheckbox = page.getByLabel(githubPRLabel); + await expect(pullRequestCheckbox).not.toBeChecked(); + await clickCheckboxByLabel(page, githubPRLabel); + await expect(pullRequestCheckbox).toBeChecked(); + + const githubMQLabel = "Schedule in GitHub Merge Queue"; + const mergeQueueCheckbox = page.getByLabel(githubMQLabel); + await expect(mergeQueueCheckbox).not.toBeChecked(); + await clickCheckboxByLabel(page, githubMQLabel); + 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(); + + await page + .getByText("Pull Request Trigger Aliases") + .scrollIntoViewIfNeeded(); + 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(); + await expect(page.getByTestId("pta-tooltip")).toHaveCount(1); + await expect(page.getByTestId("pta-tooltip")).toBeVisible(); + await expect(page.getByTestId("pta-tooltip")).toContainText("spruce"); + await expect(page.getByTestId("pta-tooltip")).toContainText( + "module_name", + ); + await expect(page.getByTestId("pta-tooltip")).toContainText( + "Variant/Task Regex Pairs", + ); + + await page + .getByText("Merge Queue Trigger Aliases") + .scrollIntoViewIfNeeded(); + 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(); + await expect(page.getByTestId("pta-tooltip")).toHaveCount(1); + await expect(page.getByTestId("pta-tooltip")).toBeVisible(); + await expect(page.getByTestId("pta-tooltip")).toContainText("spruce"); + await expect(page.getByTestId("pta-tooltip")).toContainText( + "module_name", + ); + await expect(page.getByTestId("pta-tooltip")).toContainText( + "Variant/Task Regex Pairs", + ); + }); + }); + + test.describe("Virtual Workstation page", () => { + test.beforeEach(async ({ authenticatedPage: page }) => { + await page.getByTestId("navitem-virtual-workstation").click(); + }); + + test("Adds two commands and then reorders them", async ({ + authenticatedPage: page, + }) => { + await expectSaveButtonEnabled(page, false); + await page.getByTestId("add-button").click(); + await page.getByTestId("command-input").fill("command 1"); + await page.getByTestId("directory-input").fill("mongodb.user.directory"); + + await page.getByTestId("add-button").click(); + await page.getByTestId("command-input").nth(1).fill("command 2"); + await save(page); + await validateToast(page, "success", "Successfully updated repo"); + await page.getByTestId("array-down-button").click(); + await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + 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/projectSettings/stepback_bisect.spec.ts b/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts new file mode 100644 index 0000000000..ec1621b197 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from "../../fixtures"; +import { validateToast } from "../../helpers"; +import { + getProjectSettingsRoute, + project, + projectUseRepoEnabled, +} from "./constants"; +import { save } from "./utils"; + +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, + }) => { + await page + .getByTestId("stepback-bisect-group") + .locator("label", { hasText: "Enable" }) + .click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await page.reload(); + + await expect( + page + .getByTestId("stepback-bisect-group") + .getByRole("radio", { name: "Enable" }), + ).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: "Disable" }), + ).toHaveAttribute("aria-checked", "true"); + }); + + test("Clicking on enabled and then save shows a success toast", async ({ + authenticatedPage: page, + }) => { + await page + .getByTestId("stepback-bisect-group") + .locator("label", { hasText: "Enable" }) + .click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + + await page.reload(); + + await expect( + page + .getByTestId("stepback-bisect-group") + .getByRole("radio", { name: "Enable" }), + ).toHaveAttribute("aria-checked", "true"); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/projectSettings/utils.ts b/apps/spruce/playwright/tests/projectSettings/utils.ts new file mode 100644 index 0000000000..8e9ebc72ac --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/utils.ts @@ -0,0 +1,20 @@ +import { Page } from "@playwright/test"; +import { expect } from "../../fixtures"; + +export const save = async (page: Page) => { + const saveButton = page.getByTestId("save-settings-button"); + await saveButton.scrollIntoViewIfNeeded(); + await saveButton.click(); +}; + +export const expectSaveButtonEnabled = async ( + page: Page, + isEnabled: boolean = true, +) => { + const saveButton = page.getByTestId("save-settings-button"); + if (isEnabled) { + await expect(saveButton).not.toHaveAttribute("aria-disabled", "true"); + } else { + await expect(saveButton).toHaveAttribute("aria-disabled", "true"); + } +}; diff --git a/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts b/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts new file mode 100644 index 0000000000..1e88484116 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts @@ -0,0 +1,75 @@ +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-list").locator("> *"), + ).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"); + await expect( + page.getByTestId("parsley-filter-list").locator("> *"), + ).toHaveCount(3); + + await page + .getByTestId("delete-item-button") + .first() + .scrollIntoViewIfNeeded(); + await page.getByTestId("delete-item-button").first().click(); + await save(page); + await validateToast(page, "success", "Successfully updated project"); + await expect( + page.getByTestId("parsley-filter-list").locator("> *"), + ).toHaveCount(2); + }); + }); +}); diff --git a/apps/spruce/playwright/tests/spawn/host.spec.ts b/apps/spruce/playwright/tests/spawn/host.spec.ts index 961b65248c..158da11440 100644 --- a/apps/spruce/playwright/tests/spawn/host.spec.ts +++ b/apps/spruce/playwright/tests/spawn/host.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clearDatePickerInput, selectDatePickerDate } from "../../helpers"; +import { clearDatePickerInput, typeDatePickerDate } from "../../helpers"; const ascendingSortSpawnHostOrderByHostId = [ "i-04ade558e1e26b0ad", @@ -332,31 +332,20 @@ test.describe("Spawn Host page", () => { const modal = page.getByTestId("edit-spawn-host-modal").nth(2); await expect(modal).toBeVisible(); - const yearInput = page.locator("input[id='year']"); - const monthInput = page.locator("input[id='month']"); - const dayInput = page.locator("input[id='day']"); const saveButton = page.getByRole("button", { name: "Save" }); // Set a valid near-future date - await selectDatePickerDate(page, { - year: "2026", - month: "Jun", - isoDate: "2026-06-01", - }); + await typeDatePickerDate(page, { year: "2026", month: "06", day: "01" }); await expect(saveButton).toHaveAttribute("aria-disabled", "false"); // Set a date in the past await clearDatePickerInput(page); - await yearInput.fill("2025"); - await monthInput.fill("01"); - await dayInput.fill("01"); + await typeDatePickerDate(page, { year: "2025", month: "01", day: "01" }); await expect(saveButton).toHaveAttribute("aria-disabled", "true"); // Set a date too far in the future await clearDatePickerInput(page); - await yearInput.fill("2060"); - await monthInput.fill("01"); - await dayInput.fill("15"); + await typeDatePickerDate(page, { year: "2060", month: "01", day: "15" }); await expect(saveButton).toHaveAttribute("aria-disabled", "true"); }); }); 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 24872a6409..a6cae0cb5a 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubCommitQueueTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubCommitQueueTab/getFormSchema.tsx @@ -237,6 +237,7 @@ export const getFormSchema = ( }, uiSchema: { github: { + "ui:data-cy": "github-card", "ui:ObjectFieldTemplate": CardFieldTemplate, prTestingEnabledTitle: { "ui:sectionTitle": 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..5ad01746b0 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PluginsTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/PluginsTab/getFormSchema.tsx @@ -264,6 +264,7 @@ export const getFormSchema = ( "ui:useExpandableCard": true, items: { "ui:displayTitle": "New Metadata Link", + "ui:data-cy": "metadata-link-item", requesters: { "ui:widget": widgets.MultiSelectWidget, "ui:data-cy": "requesters-input", From c9d3fe2d4e40cb8a620bb0e20a00ccf159212945 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 14:48:03 -0400 Subject: [PATCH 09/32] cleanup: no `force` --- .evergreen/scripts/constants.js | 4 ++-- .../tests/projectSettings/defaulting_to_repo.spec.ts | 2 +- .../tests/projectSettings/github_permission_groups.spec.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.evergreen/scripts/constants.js b/.evergreen/scripts/constants.js index 5f2aa23190..a6a9e2487a 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 = 4; export { Tasks, diff --git a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts index a03fa84aaa..464b33fb99 100644 --- a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts @@ -398,7 +398,7 @@ test.describe("Project Settings when defaulting to repo", () => { await page .getByTestId("add-button") .filter({ hasText: "Add Patch Alias" }) - .click({ force: true }); + .click(); await expectSaveButtonEnabled(page, false); await page.getByTestId("alias-input").fill("my overriden alias name"); await page diff --git a/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts b/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts index d015daa333..8439980b76 100644 --- a/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts @@ -44,7 +44,7 @@ test.describe("GitHub permission groups", () => { .getByTestId("permission-type-input") .fill(invalidGithubPermission); await page.getByTestId("permission-value-input").click(); - await page.getByText("Write").click({ force: true }); + await page.getByText("Write").click(); await expectSaveButtonEnabled(page, true); await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); await save(page); From 670d61726e330294a73bafa9e49a57ee511bccd3 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 14:53:23 -0400 Subject: [PATCH 10/32] cleanup: weird usage of locators --- .../background_processing.spec.ts | 3 +-- .../tests/adminSettings/restart_tasks.spec.ts | 2 +- .../github_permission_groups.spec.ts | 20 +++++-------------- .../projectSettings/views_and_filters.spec.ts | 12 +++-------- .../BackgroundProcessingTab/schemaFields.ts | 1 + .../getFormSchema.tsx | 1 + .../tabs/ViewsAndFiltersTab/getFormSchema.ts | 1 + 7 files changed, 13 insertions(+), 27 deletions(-) diff --git a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts index b2ad04939f..7cc85d0d53 100644 --- a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts @@ -18,8 +18,7 @@ test.describe("background processing", () => { await page.getByLabel("Single Worker Name").fill("new single worker name"); await page - .getByTestId("named-queue-list") - .locator("> *") + .getByTestId("named-queue-item") .first() .getByTestId("delete-item-button") .click(); diff --git a/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts b/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts index e9fc5161d5..ccb870127f 100644 --- a/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts @@ -45,7 +45,7 @@ test.describe("restart tasks", () => { await page.getByTestId("restart-tasks-button").click(); await expect(page.getByTestId("restart-tasks-modal")).toBeVisible(); await expect( - page.getByTestId("restart-tasks-list").locator("> *"), + page.getByTestId("restart-tasks-list").locator("li"), ).toHaveCount(4); await page.getByRole("button", { name: "Confirm" }).click(); await validateToast(page, "success", "Created job to restart 4 tasks."); diff --git a/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts b/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts index 8439980b76..d8f4262b80 100644 --- a/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts @@ -17,9 +17,7 @@ test.describe("GitHub permission groups", () => { test("should not have any permission groups defined", async ({ authenticatedPage: page, }) => { - await expect( - page.getByTestId("permission-group-list").locator("> *"), - ).toHaveCount(0); + await expect(page.getByTestId("permission-group-item")).toHaveCount(0); await expectSaveButtonEnabled(page, false); }); @@ -30,9 +28,7 @@ test.describe("GitHub permission groups", () => { page.getByRole("button", { name: /^Add permission group$/ }), ).toBeVisible(); await page.getByRole("button", { name: /^Add permission group$/ }).click(); - await expect( - page.getByTestId("permission-group-list").locator("> *"), - ).toHaveCount(1); + await expect(page.getByTestId("permission-group-item")).toHaveCount(1); const invalidGithubPermission = "invalid_github_permission"; await page @@ -58,9 +54,7 @@ test.describe("GitHub permission groups", () => { page.getByRole("button", { name: /^Add permission group$/ }), ).toBeVisible(); await page.getByRole("button", { name: /^Add permission group$/ }).click(); - await expect( - page.getByTestId("permission-group-list").locator("> *"), - ).toHaveCount(1); + await expect(page.getByTestId("permission-group-item")).toHaveCount(1); await page .getByTestId("permission-group-title-input") @@ -76,13 +70,9 @@ test.describe("GitHub permission groups", () => { await validateToast(page, "success", "Successfully updated project"); await page.reload(); - await expect( - page.getByTestId("permission-group-list").locator("> *"), - ).toHaveCount(1); + await expect(page.getByTestId("permission-group-item")).toHaveCount(1); await page.getByTestId("delete-item-button").click(); - await expect( - page.getByTestId("permission-group-list").locator("> *"), - ).toHaveCount(0); + await expect(page.getByTestId("permission-group-item")).toHaveCount(0); await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); await save(page); await validateToast(page, "success", "Successfully updated project"); diff --git a/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts b/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts index 1e88484116..cf4529886b 100644 --- a/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts @@ -11,9 +11,7 @@ test.describe("Views & filters page", () => { test.beforeEach(async ({ authenticatedPage: page }) => { await page.goto(destination); - await expect( - page.getByTestId("parsley-filter-list").locator("> *"), - ).toHaveCount(2); + await expect(page.getByTestId("parsley-filter-item")).toHaveCount(2); await expectSaveButtonEnabled(page, false); }); @@ -56,9 +54,7 @@ test.describe("Views & filters page", () => { await expectSaveButtonEnabled(page, true); await save(page); await validateToast(page, "success", "Successfully updated project"); - await expect( - page.getByTestId("parsley-filter-list").locator("> *"), - ).toHaveCount(3); + await expect(page.getByTestId("parsley-filter-item")).toHaveCount(3); await page .getByTestId("delete-item-button") @@ -67,9 +63,7 @@ test.describe("Views & filters page", () => { await page.getByTestId("delete-item-button").first().click(); await save(page); await validateToast(page, "success", "Successfully updated project"); - await expect( - page.getByTestId("parsley-filter-list").locator("> *"), - ).toHaveCount(2); + await expect(page.getByTestId("parsley-filter-item")).toHaveCount(2); }); }); }); diff --git a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/BackgroundProcessingTab/schemaFields.ts b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/BackgroundProcessingTab/schemaFields.ts index 8f37241dee..ae94f4fdea 100644 --- a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/BackgroundProcessingTab/schemaFields.ts +++ b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/BackgroundProcessingTab/schemaFields.ts @@ -84,6 +84,7 @@ const namedQueues = { "ui:fieldCss": fullWidthCss, "ui:arrayItemCSS": arrayItemCSS, items: { + "ui:data-cy": "named-queue-item", regexp: { "ui:optional": true, }, 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..ddbe30189a 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubPermissionGroupsTab/getFormSchema.tsx +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/GithubPermissionGroupsTab/getFormSchema.tsx @@ -169,6 +169,7 @@ const permissionCss = css` const itemsUISchema = { "ui:displayTitle": "New Permission Group", + "ui:data-cy": "permission-group-item", name: { "ui:ariaLabelledBy": "Permission Group Name", "ui:data-cy": "permission-group-title-input", 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..a7cc13539a 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ViewsAndFiltersTab/getFormSchema.ts +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/ViewsAndFiltersTab/getFormSchema.ts @@ -96,6 +96,7 @@ export const getFormSchema = ( "ui:data-cy": "parsley-filter-list", items: { "ui:displayTitle": "New Parsley Filter", + "ui:data-cy": "parsley-filter-item", "ui:label": false, expression: { "ui:widget": "textarea", From 9503e759088ef9211648190418e4f59159363513 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 15:19:54 -0400 Subject: [PATCH 11/32] fix failing tests --- .../tests/adminSettings/background_processing.spec.ts | 4 ++-- apps/spruce/playwright/tests/spawn/volume.spec.ts | 8 ++++---- .../GeneralTab/BackgroundProcessingTab/schemaFields.ts | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts index 7cc85d0d53..3689366480 100644 --- a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts @@ -18,9 +18,9 @@ test.describe("background processing", () => { await page.getByLabel("Single Worker Name").fill("new single worker name"); await page - .getByTestId("named-queue-item") - .first() + .getByTestId("named-queue-list") .getByTestId("delete-item-button") + .first() .click(); // Logger section. diff --git a/apps/spruce/playwright/tests/spawn/volume.spec.ts b/apps/spruce/playwright/tests/spawn/volume.spec.ts index 5924b4cf0b..4cb76b4610 100644 --- a/apps/spruce/playwright/tests/spawn/volume.spec.ts +++ b/apps/spruce/playwright/tests/spawn/volume.spec.ts @@ -62,11 +62,11 @@ test.describe("Spawn volume page", () => { await expect(migratingRow.getByTestId("volume-status-badge")).toContainText( "Migrating", ); - const trashButton = page.getByTestId(`trash-vol-${targetVolume}`); + const trashButton = page.getByTestId(`trash-${targetVolume}`); await expect(trashButton).toBeDisabled(); - const migrateButton = page.getByTestId(`migrate-btn-vol-${targetVolume}`); + const migrateButton = page.getByTestId(`migrate-btn-${targetVolume}`); await expect(migrateButton).toBeDisabled(); - const editButton = page.getByTestId(`edit-btn-vol-${targetVolume}`); + const editButton = page.getByTestId(`edit-btn-${targetVolume}`); await expect(editButton).toBeDisabled(); }); @@ -112,7 +112,7 @@ test.describe("Spawn volume page", () => { .filter({ hasText: targetVolume }), ).toBeVisible(); - await page.getByTestId(`trash-vol-${targetVolume}`).click(); + await page.getByTestId(`trash-${targetVolume}`).click(); const popconfirm = page.getByTestId("delete-volume-popconfirm"); await expect(popconfirm).toBeVisible(); const yesButton = popconfirm.getByRole("button", { name: "Yes" }); diff --git a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/BackgroundProcessingTab/schemaFields.ts b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/BackgroundProcessingTab/schemaFields.ts index ae94f4fdea..8f37241dee 100644 --- a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/BackgroundProcessingTab/schemaFields.ts +++ b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/BackgroundProcessingTab/schemaFields.ts @@ -84,7 +84,6 @@ const namedQueues = { "ui:fieldCss": fullWidthCss, "ui:arrayItemCSS": arrayItemCSS, items: { - "ui:data-cy": "named-queue-item", regexp: { "ui:optional": true, }, From 0b09faebdc46708b9e3ac87841a4a97c08cc14f3 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 15:24:58 -0400 Subject: [PATCH 12/32] improvement: `data-cy` for banner --- .../tests/adminSettings/announcements.spec.ts | 5 +-- .../tests/adminSettings/save_function.spec.ts | 40 ++++--------------- .../AnnouncementsTab/getFormSchema.ts | 2 +- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts b/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts index d2d4d6ac68..0e372751d3 100644 --- a/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts @@ -13,10 +13,7 @@ test.describe("announcements", () => { "true", ); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("some more banner text"); diff --git a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts index 594cdaede1..7724eff050 100644 --- a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts @@ -203,10 +203,7 @@ test.describe("admin settings save properly", () => { "test-okta-secret", ); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("Okta param store test"); @@ -240,10 +237,7 @@ test.describe("admin settings save properly", () => { "test-jira-pat", ); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("Jira param store test"); @@ -282,10 +276,7 @@ test.describe("admin settings save properly", () => { ); await expect(splunk.getByLabel("Token")).toHaveValue("test-splunk-token"); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("Tokens param store test"); @@ -320,10 +311,7 @@ test.describe("admin settings save properly", () => { "test-runtime-env-key", ); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("Runtime Environments param store test"); @@ -364,10 +352,7 @@ test.describe("admin settings save properly", () => { "test-ec2-secret", ); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("AWS EC2 param store test"); @@ -409,10 +394,7 @@ test.describe("admin settings save properly", () => { "test-s3-secret", ); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("S3 Keys param store test"); @@ -451,10 +433,7 @@ test.describe("admin settings save properly", () => { page.getByTestId("misc-settings").getByLabel("Webhook Secret"), ).toHaveValue("test-webhook-secret"); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("GitHub Webhook Secret param store test"); @@ -493,10 +472,7 @@ test.describe("admin settings save properly", () => { "test_value", ); - const announcements = page.getByTestId("announcements-section"); - const bannerText = announcements.getByRole("textbox", { - name: "Banner Text", - }); + const bannerText = page.getByTestId("banner-text"); await bannerText.clear(); await bannerText.fill("Expansions List test"); diff --git a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/AnnouncementsTab/getFormSchema.ts b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/AnnouncementsTab/getFormSchema.ts index e4dae50352..c875cab185 100644 --- a/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/AnnouncementsTab/getFormSchema.ts +++ b/apps/spruce/src/pages/adminSettings/tabs/GeneralTab/AnnouncementsTab/getFormSchema.ts @@ -31,9 +31,9 @@ export const formSchema: ReturnType = { }, uiSchema: { announcements: { - "ui:data-cy": "announcements-section", "ui:ObjectFieldTemplate": CardFieldTemplate, banner: { + "ui:data-cy": "banner-text", "ui:widget": "textarea", "ui:rows": 2, }, From cf84570a6e49cb1a9628557ce383dbb693643c95 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 15:58:17 -0400 Subject: [PATCH 13/32] refactor: clicking checkboxes and radios --- apps/parsley/playwright/helpers/index.ts | 2 +- .../playwright/tests/project_filters.spec.ts | 13 +-- apps/spruce/playwright/helpers/index.ts | 2 +- .../adminSettings/authentication.spec.ts | 16 +--- .../background_processing.spec.ts | 9 +- .../tests/adminSettings/other.spec.ts | 7 +- .../tests/adminSettings/runners.spec.ts | 9 +- .../tests/adminSettings/save_function.spec.ts | 7 +- .../tests/adminSettings/service_flags.spec.ts | 24 +++--- .../distroSettings/general_section.spec.ts | 22 +++-- .../distroSettings/provider_section.spec.ts | 34 ++++++-- .../playwright/tests/distroSettings/utils.ts | 4 +- .../playwright/tests/host/host_events.spec.ts | 6 +- .../tests/hosts/hosts_filtering.spec.ts | 4 +- .../tests/hosts/hosts_select.spec.ts | 6 +- .../tests/myPatches/dropdown_menu.spec.ts | 6 +- .../tests/myPatches/my_patches.spec.ts | 10 +-- .../tests/patch/configure_patch.spec.ts | 55 ++++++------- .../tests/preferences/notifications.spec.ts | 13 ++- .../defaulting_to_repo.spec.ts | 82 ++++++++++--------- .../not_defaulting_to_repo.spec.ts | 4 +- .../projectSettings/repo_settings.spec.ts | 43 +++++----- .../projectSettings/stepback_bisect.spec.ts | 14 ++-- .../playwright/tests/spawn/host.spec.ts | 15 ++-- .../playwright/tests/spawn/volume.spec.ts | 7 +- .../playwright/tests/task/test_table.spec.ts | 8 +- .../tests/version/restart_modal.spec.ts | 10 +-- .../tests/version/task_filters.spec.ts | 14 ++-- .../tests/version/task_table.spec.ts | 4 +- .../playwright/tests/waterfall/menu.spec.ts | 10 +-- packages/playwright-config/helpers/index.ts | 21 ++--- 31 files changed, 240 insertions(+), 241 deletions(-) diff --git a/apps/parsley/playwright/helpers/index.ts b/apps/parsley/playwright/helpers/index.ts index dbce07ce88..6403dc3905 100644 --- a/apps/parsley/playwright/helpers/index.ts +++ b/apps/parsley/playwright/helpers/index.ts @@ -199,5 +199,5 @@ export { validateToast, login, logout, - clickCheckboxByLabel, + clickLabelForLocator, } from "@evg-ui/playwright-config/helpers"; diff --git a/apps/parsley/playwright/tests/project_filters.spec.ts b/apps/parsley/playwright/tests/project_filters.spec.ts index 8af678b281..108805707b 100644 --- a/apps/parsley/playwright/tests/project_filters.spec.ts +++ b/apps/parsley/playwright/tests/project_filters.spec.ts @@ -27,7 +27,8 @@ test.describe("project filters", () => { await page.goto(resmokeLogLink); await page.getByText("View project filters").click(); await expect(page.getByTestId("project-filters-modal")).toBeVisible(); - await helpers.clickCheckboxByLabel(page, "Select row 0"); + const row0Checkbox = page.getByRole("checkbox", { name: "Select row 0" }); + await helpers.clickLabelForLocator(row0Checkbox); await page.getByRole("button", { name: "Apply filters" }).click(); await expect(page).toHaveURL( /111%28NETWORK%257CASIO%257CEXECUTOR%257CCONNPOOL%257CREPL_HB%29/, @@ -43,10 +44,9 @@ test.describe("project filters", () => { await page.goto(resmokeLogLink); await page.getByText("View project filters").click(); await expect(page.getByTestId("project-filters-modal")).toBeVisible(); - await helpers.clickCheckboxByLabel(page, "Select row 0"); - await expect( - page.getByRole("checkbox", { name: "Select row 0" }), - ).toBeChecked(); + const row0Checkbox = page.getByRole("checkbox", { name: "Select row 0" }); + await helpers.clickLabelForLocator(row0Checkbox); + await expect(row0Checkbox).toBeChecked(); }); test("properly processes filters with commas", async ({ @@ -55,7 +55,8 @@ test.describe("project filters", () => { await page.goto(resmokeLogLink); await page.getByText("View project filters").click(); await expect(page.getByTestId("project-filters-modal")).toBeVisible(); - await helpers.clickCheckboxByLabel(page, "Select row 3"); + const row3Checkbox = page.getByRole("checkbox", { name: "Select row 3" }); + await helpers.clickLabelForLocator(row3Checkbox); await page.getByRole("button", { name: "Apply filters" }).click(); await expect(page).toHaveURL( /110%2522Connection%2520accepted%2522%252C%2522attr%2522/, diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 9a5c57d9d8..2c73ea1239 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -125,7 +125,7 @@ export { validateToast, login, logout, - clickCheckboxByLabel, + clickLabelForLocator, mockGraphQLResponse, hasOperationName, } from "@evg-ui/playwright-config/helpers"; diff --git a/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts b/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts index 4c4aefbe7c..18df2a84b0 100644 --- a/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("authentication", () => { @@ -19,9 +19,7 @@ test.describe("authentication", () => { // Global Config section. const allowServiceUsersCheckbox = page.getByLabel("Allow Service Users"); - const allowServiceUsersId = - await allowServiceUsersCheckbox.getAttribute("id"); - await page.locator(`label[for="${allowServiceUsersId}"]`).click(); + await clickLabelForLocator(allowServiceUsersCheckbox); await page.getByLabel("Background Reauthentication (Mins)").clear(); await page.getByLabel("Background Reauthentication (Mins)").fill("120"); @@ -76,20 +74,14 @@ test.describe("authentication", () => { await page.getByTestId("multi-read-write").click(); const multiReadWriteOptions = page.getByTestId("multi-read-write-options"); const oktaCheckbox = multiReadWriteOptions.getByLabel("Okta"); - const oktaCheckboxId = await oktaCheckbox.getAttribute("id"); - await multiReadWriteOptions - .locator(`label[for="${oktaCheckboxId}"]`) - .click(); + await clickLabelForLocator(oktaCheckbox); await page.getByTestId("multi-read-write").click(); // Multi read-only section. await page.getByTestId("multi-read-only").click(); const multiReadOnlyOptions = page.getByTestId("multi-read-only-options"); const naiveCheckbox = multiReadOnlyOptions.getByLabel("Naive"); - const naiveCheckboxId = await naiveCheckbox.getAttribute("id"); - await multiReadOnlyOptions - .locator(`label[for="${naiveCheckboxId}"]`) - .click(); + await clickLabelForLocator(naiveCheckbox); await page.getByTestId("multi-read-only").click(); // Kanopy section. diff --git a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts index 3689366480..2fff5b789b 100644 --- a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts @@ -1,5 +1,9 @@ import { test, expect } from "../../fixtures"; -import { selectOption, validateToast } from "../../helpers"; +import { + clickLabelForLocator, + selectOption, + validateToast, +} from "../../helpers"; import { save } from "./utils"; test.describe("background processing", () => { @@ -32,8 +36,7 @@ test.describe("background processing", () => { const asyncBufferCheckbox = page.getByLabel( "Use asynchronous buffered logger", ); - const asyncBufferId = await asyncBufferCheckbox.getAttribute("id"); - await page.locator(`label[for="${asyncBufferId}"]`).click(); + await clickLabelForLocator(asyncBufferCheckbox); // Notification Rate Limits section. await page.getByLabel("Time Interval (secs)", { exact: true }).clear(); diff --git a/apps/spruce/playwright/tests/adminSettings/other.spec.ts b/apps/spruce/playwright/tests/adminSettings/other.spec.ts index 8d3cfff719..7af3f6e0d8 100644 --- a/apps/spruce/playwright/tests/adminSettings/other.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/other.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("other", () => { @@ -332,10 +332,7 @@ test.describe("other", () => { const tracerConfiguration = page.getByTestId("tracer-configuration"); const tracerEnabledCheckbox = tracerConfiguration.getByLabel("Enable tracer"); - const tracerEnabledId = await tracerEnabledCheckbox.getAttribute("id"); - await tracerConfiguration - .locator(`label[for="${tracerEnabledId}"]`) - .click(); + await clickLabelForLocator(tracerEnabledCheckbox); await page.getByLabel("Collector Endpoint").clear(); await page diff --git a/apps/spruce/playwright/tests/adminSettings/runners.spec.ts b/apps/spruce/playwright/tests/adminSettings/runners.spec.ts index 1dec4358f5..9ac5eeee1a 100644 --- a/apps/spruce/playwright/tests/adminSettings/runners.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/runners.spec.ts @@ -1,5 +1,9 @@ import { test, expect } from "../../fixtures"; -import { selectOption, validateToast } from "../../helpers"; +import { + clickLabelForLocator, + selectOption, + validateToast, +} from "../../helpers"; import { save } from "./utils"; test.describe("runners", () => { @@ -32,8 +36,7 @@ test.describe("runners", () => { await page.getByLabel("Default Future Host Fraction").fill("0.6"); const groupVersionsCheckbox = page.getByLabel("Group Versions"); - const groupVersionsId = await groupVersionsCheckbox.getAttribute("id"); - await page.locator(`label[for="${groupVersionsId}"]`).click(); + await clickLabelForLocator(groupVersionsCheckbox); // Repotracker section. await page.getByLabel("New Revisions to Fetch").clear(); diff --git a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts index 7724eff050..a0dbb5bdf9 100644 --- a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("admin settings save properly", () => { @@ -49,10 +49,7 @@ test.describe("admin settings save properly", () => { const tracerConfiguration = page.getByTestId("tracer-configuration"); const tracerEnabledCheckbox = tracerConfiguration.getByLabel("Enable tracer"); - const tracerEnabledId = await tracerEnabledCheckbox.getAttribute("id"); - await tracerConfiguration - .locator(`label[for="${tracerEnabledId}"]`) - .click(); + await clickLabelForLocator(tracerEnabledCheckbox); await tracerConfiguration.getByLabel("Collector Endpoint").clear(); await tracerConfiguration .getByLabel("Collector Endpoint") diff --git a/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts b/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts index 90494dc374..388b403682 100644 --- a/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("service flags", () => { @@ -20,15 +20,14 @@ test.describe("service flags", () => { ); const initialCheckedCount = await checkedCheckboxes.count(); - // Find the first unchecked checkbox. - const targetCheckbox = page + // Find the first unchecked checkbox and capture a stable locator by id. + const uncheckedCheckbox = page .locator('input[type="checkbox"]:not([aria-checked="true"])') .first(); - await targetCheckbox.scrollIntoViewIfNeeded(); + const checkboxId = await uncheckedCheckbox.getAttribute("id"); + const checkbox = page.locator(`#${checkboxId}`); - // Check it via its label. - const checkboxId = await targetCheckbox.getAttribute("id"); - await page.locator(`label[for="${checkboxId}"]`).click(); + await clickLabelForLocator(checkbox); // Verify checked count increased. await expect(checkedCheckboxes).toHaveCount(initialCheckedCount + 1); @@ -39,7 +38,7 @@ test.describe("service flags", () => { ); // Uncheck it again. - await page.locator(`label[for="${checkboxId}"]`).click(); + await clickLabelForLocator(checkbox); await expect(checkedCheckboxes).toHaveCount(initialCheckedCount); await expect(page.getByTestId("save-settings-button")).toHaveAttribute( "aria-disabled", @@ -47,19 +46,16 @@ test.describe("service flags", () => { ); // Re-check and save. - await page.locator(`label[for="${checkboxId}"]`).click(); + await clickLabelForLocator(checkbox); await save(page); await validateToast(page, "success", "Service flags saved successfully"); await page.reload(); - await expect(page.locator(`#${checkboxId}`)).toHaveAttribute( - "aria-checked", - "true", - ); + await expect(checkbox).toHaveAttribute("aria-checked", "true"); await expect(checkedCheckboxes).toHaveCount(initialCheckedCount + 1); // Restore original state. - await page.locator(`label[for="${checkboxId}"]`).click(); + await clickLabelForLocator(checkbox); await save(page); await validateToast(page, "success", "Service flags saved successfully"); }); diff --git a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts index 2f28479e44..c53a799cea 100644 --- a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel, validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("general section", () => { @@ -23,8 +23,10 @@ test.describe("general section", () => { await page.getByLabel("Alias").fill("localhost-alias"); await page.getByLabel("Notes").fill("this is a note"); await page.getByLabel("Warnings").fill("this is a warning"); - await clickCheckboxByLabel(page, "Disable shallow clone for this distro"); - await clickCheckboxByLabel(page, "Admin only"); + await clickLabelForLocator( + page.getByLabel("Disable shallow clone for this distro"), + ); + await clickLabelForLocator(page.getByLabel("Admin only")); await save(page); await validateToast(page, "success", "Updated distro."); @@ -45,8 +47,10 @@ test.describe("general section", () => { await page.getByTestId("delete-item-button").click(); await page.getByLabel("Notes").clear(); await page.getByLabel("Warnings").clear(); - await clickCheckboxByLabel(page, "Disable shallow clone for this distro"); - await clickCheckboxByLabel(page, "Admin only"); + await clickLabelForLocator( + page.getByLabel("Disable shallow clone for this distro"), + ); + await clickLabelForLocator(page.getByLabel("Admin only")); await save(page); await validateToast(page, "success", "Updated distro."); }); @@ -78,7 +82,9 @@ test.describe("general section", () => { name: "Set distro as Single Task Distro", }), ).not.toHaveAttribute("aria-disabled", "true"); - await clickCheckboxByLabel(page, "Set distro as Single Task Distro"); + await clickLabelForLocator( + page.getByLabel("Set distro as Single Task Distro"), + ); await expect(page.getByTestId("single-task-banner")).toContainText( "This Distro will be converted to a Single Task Distro once saved. Please review before confirming.", ); @@ -102,7 +108,9 @@ test.describe("general section", () => { name: "Set distro as Single Task Distro", }), ).not.toHaveAttribute("aria-disabled", "true"); - await clickCheckboxByLabel(page, "Set distro as Single Task Distro"); + await clickLabelForLocator( + page.getByLabel("Set distro as Single Task Distro"), + ); await expect(page.getByTestId("single-task-banner")).toContainText( "This Distro will no longer be a Single Task Distro once saved. Please review before confirming.", ); diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts index fd147672e9..5f6d513a23 100644 --- a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../../fixtures"; import { - clickCheckboxByLabel, + clickLabelForLocator, selectOption, validateToast, } from "../../helpers"; @@ -21,7 +21,9 @@ test.describe("provider section", () => { await expect(page.getByTestId("static-provider-settings")).toBeVisible(); await page.getByTestId("user-data-input").fill("my user data"); - await clickCheckboxByLabel(page, "Merge with existing user data"); + await clickLabelForLocator( + page.getByLabel("Merge with existing user data"), + ); await page.getByRole("button", { name: "Add security group" }).click(); await page.getByLabel("Security Group ID").fill("sg-1234"); await page.getByRole("button", { name: "Add host" }).click(); @@ -30,7 +32,9 @@ test.describe("provider section", () => { await validateToast(page, "success", "Updated distro."); await page.getByTestId("user-data-input").clear(); - await clickCheckboxByLabel(page, "Merge with existing user data"); + await clickLabelForLocator( + page.getByLabel("Merge with existing user data"), + ); await page.getByTestId("delete-item-button").first().click(); await page.getByTestId("delete-item-button").first().click(); await save(page); @@ -71,7 +75,9 @@ test.describe("provider section", () => { await page.getByLabel("Username for Registries").fill("username"); await page.getByLabel("Password for Registries").fill("password"); await page.getByTestId("user-data-input").fill("my user data"); - await clickCheckboxByLabel(page, "Merge with existing user data"); + await clickLabelForLocator( + page.getByLabel("Merge with existing user data"), + ); await save(page); await validateToast(page, "success", "Updated distro."); @@ -80,7 +86,9 @@ test.describe("provider section", () => { await page.getByLabel("Username for Registries").clear(); await page.getByLabel("Password for Registries").clear(); await page.getByTestId("user-data-input").clear(); - await clickCheckboxByLabel(page, "Merge with existing user data"); + await clickLabelForLocator( + page.getByLabel("Merge with existing user data"), + ); await save(page); await validateToast(page, "success", "Updated distro."); }); @@ -106,7 +114,9 @@ test.describe("provider section", () => { await expect(page.getByText("Default VPC Subnet ID")).toBeVisible(); await expect(page.getByText("VPC Subnet Prefix")).toBeVisible(); - await clickCheckboxByLabel(page, "Use security groups in an EC2 VPC"); + await clickLabelForLocator( + page.getByLabel("Use security groups in an EC2 VPC"), + ); await expect(page.getByTestId("use-vpc")).not.toBeChecked(); await expect(page.getByText("Default VPC Subnet ID")).toHaveCount(0); await expect(page.getByText("VPC Subnet Prefix")).toHaveCount(0); @@ -225,7 +235,9 @@ test.describe("provider section", () => { await expect(page.getByText("Default VPC Subnet ID")).toBeVisible(); await expect(page.getByText("VPC Subnet Prefix")).toBeVisible(); - await clickCheckboxByLabel(page, "Use security groups in an EC2 VPC"); + await clickLabelForLocator( + page.getByLabel("Use security groups in an EC2 VPC"), + ); await expect(page.getByText("Default VPC Subnet ID")).toHaveCount(0); await expect(page.getByText("VPC Subnet Prefix")).toHaveCount(0); }); @@ -255,7 +267,9 @@ test.describe("provider section", () => { await page .getByTestId("user-data-input") .fill(""); - await clickCheckboxByLabel(page, "Merge with existing user data"); + await clickLabelForLocator( + page.getByLabel("Merge with existing user data"), + ); await save(page); await validateToast(page, "success", "Updated distro."); @@ -265,7 +279,9 @@ test.describe("provider section", () => { await page.getByLabel("SSH Key Name").clear(); await page.getByLabel("SSH Key Name").fill("mci"); await page.getByTestId("user-data-input").clear(); - await clickCheckboxByLabel(page, "Merge with existing user data"); + await clickLabelForLocator( + page.getByLabel("Merge with existing user data"), + ); await save(page); await validateToast(page, "success", "Updated distro."); }); diff --git a/apps/spruce/playwright/tests/distroSettings/utils.ts b/apps/spruce/playwright/tests/distroSettings/utils.ts index 3331ff8ea2..c66dab7415 100644 --- a/apps/spruce/playwright/tests/distroSettings/utils.ts +++ b/apps/spruce/playwright/tests/distroSettings/utils.ts @@ -1,4 +1,5 @@ import { Page } from "@playwright/test"; +import { clickLabelForLocator } from "@evg-ui/playwright-config/helpers"; import { expect } from "../../fixtures"; type onSaveOptions = "NONE" | "DECOMMISSION" | "RESTART_JASPER" | "REPROVISION"; @@ -10,8 +11,7 @@ export const save = async (page: Page, onSaveValue?: onSaveOptions) => { if (onSaveValue) { const radio = page.locator(`input[value="${onSaveValue}"]`); - const radioId = await radio.getAttribute("id"); - await page.locator(`label[for="${radioId}"]`).click(); + await clickLabelForLocator(radio); } const modal = page.getByTestId("save-modal"); diff --git a/apps/spruce/playwright/tests/host/host_events.spec.ts b/apps/spruce/playwright/tests/host/host_events.spec.ts index af425f0f63..e4ad304669 100644 --- a/apps/spruce/playwright/tests/host/host_events.spec.ts +++ b/apps/spruce/playwright/tests/host/host_events.spec.ts @@ -1,6 +1,6 @@ import { Page } from "@playwright/test"; import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel, selectOption } from "../../helpers"; +import { clickLabelForLocator, selectOption } from "../../helpers"; /** * Helper to select page size and verify URL and table row count @@ -271,7 +271,7 @@ test.describe("Host events", () => { // Apply filter. await page.getByTestId("event-type-filter").click(); await expect(page.getByTestId("event-type-filter-wrapper")).toBeVisible(); - await clickCheckboxByLabel(page, "Agent deployed"); + await clickLabelForLocator(page.getByLabel("Agent deployed")); await expect(page.getByTestId("host-events-table-row")).toHaveCount(2); await expect(page.getByTestId("event-type-filter")).toHaveAttribute( "data-highlighted", @@ -280,7 +280,7 @@ test.describe("Host events", () => { // Remove filter. await expect(page.getByTestId("event-type-filter-wrapper")).toBeVisible(); - await clickCheckboxByLabel(page, "Agent deployed"); + await clickLabelForLocator(page.getByLabel("Agent deployed")); await expect(page.getByTestId("host-events-table-row")).not.toHaveCount( 2, ); diff --git a/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts b/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts index a4aeb7a60d..d07228e091 100644 --- a/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts +++ b/apps/spruce/playwright/tests/hosts/hosts_filtering.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel } from "../../helpers"; +import { clickLabelForLocator } from "../../helpers"; const hostsRoute = "/hosts"; @@ -119,7 +119,7 @@ test.describe("Hosts page filtering from table filters", () => { const filterWrapper = page.getByTestId("statuses-filter-wrapper"); await expect(filterWrapper).toBeVisible(); - await clickCheckboxByLabel(page, "Running"); + await clickLabelForLocator(page.getByLabel("Running")); await filterIcon.click(); await expect(page).toHaveURL(/statuses=running/); diff --git a/apps/spruce/playwright/tests/hosts/hosts_select.spec.ts b/apps/spruce/playwright/tests/hosts/hosts_select.spec.ts index 007829b46f..d258e9d838 100644 --- a/apps/spruce/playwright/tests/hosts/hosts_select.spec.ts +++ b/apps/spruce/playwright/tests/hosts/hosts_select.spec.ts @@ -1,14 +1,12 @@ import { Page } from "@playwright/test"; import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; const hostsRoute = "/hosts"; const selectAllHosts = async (page: Page) => { const headerCheckbox = page.locator("thead").locator("input[type=checkbox]"); - const id = await headerCheckbox.getAttribute("id"); - await page.locator(`label[for="${id}"]`).click(); - + await clickLabelForLocator(headerCheckbox); const bodyCheckboxes = page.locator("tbody").locator("input[type=checkbox]"); await expect(bodyCheckboxes).toHaveCount(3); for (let i = 0; i < 3; i++) { diff --git a/apps/spruce/playwright/tests/myPatches/dropdown_menu.spec.ts b/apps/spruce/playwright/tests/myPatches/dropdown_menu.spec.ts index 857b56fc6f..737443011f 100644 --- a/apps/spruce/playwright/tests/myPatches/dropdown_menu.spec.ts +++ b/apps/spruce/playwright/tests/myPatches/dropdown_menu.spec.ts @@ -1,6 +1,6 @@ import { INCLUDE_HIDDEN_PATCHES } from "constants/cookies"; import { test, expect } from "../../fixtures"; -import { validateToast, clickCheckboxByLabel } from "../../helpers"; +import { validateToast, clickLabelForLocator } from "../../helpers"; const patchWithoutVersion = "test meee"; const patchWithVersion = "main: EVG-7823 add a commit queue message (#4048)"; @@ -119,7 +119,7 @@ test.describe("Dropdown Menu of Patch Actions", () => { await expect(targetPatchCard).toBeHidden(); // Check "Include hidden" checkbox and unhide patch card. - await clickCheckboxByLabel(page, "Include hidden"); + await clickLabelForLocator(page.getByLabel("Include hidden")); const cookies = await page.context().cookies(); const hiddenCookie = cookies.find((c) => c.name === INCLUDE_HIDDEN_PATCHES); expect(hiddenCookie?.value).toBe("true"); @@ -151,7 +151,7 @@ test.describe("Dropdown Menu of Patch Actions", () => { await expect(targetPatchCard.getByTestId("hidden-badge")).toBeHidden(); // Uncheck "Include hidden" and verify patch card is visible. - await clickCheckboxByLabel(page, "Include hidden"); + await clickLabelForLocator(page.getByLabel("Include hidden")); const cookiesAfterUncheck = await page.context().cookies(); const hiddenCookieAfterUncheck = cookiesAfterUncheck.find( (c) => c.name === INCLUDE_HIDDEN_PATCHES, diff --git a/apps/spruce/playwright/tests/myPatches/my_patches.spec.ts b/apps/spruce/playwright/tests/myPatches/my_patches.spec.ts index 7dfe286c8d..38a8269167 100644 --- a/apps/spruce/playwright/tests/myPatches/my_patches.spec.ts +++ b/apps/spruce/playwright/tests/myPatches/my_patches.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel } from "../../helpers"; +import { clickLabelForLocator } from "../../helpers"; const MY_PATCHES_ROUTE = "/user/admin/patches"; const BOB_HICKS_PATCHES_ROUTE = "/user/bob.hicks/patches"; @@ -201,10 +201,10 @@ test.describe("My Patches Page", () => { authenticatedPage: page, }) => { for (const { display, key } of statuses) { - await clickCheckboxByLabel(page, display); // Click to check status checkbox. + await clickLabelForLocator(page.getByLabel(display)); // Click to check status checkbox. await expect(page).toHaveURL(new RegExp(`statuses=${key}`)); - await clickCheckboxByLabel(page, display); // Click to uncheck status checkbox. + await clickLabelForLocator(page.getByLabel(display)); // Click to uncheck status checkbox. await expect(page).not.toHaveURL(/statuses/); } }); @@ -212,12 +212,12 @@ test.describe("My Patches Page", () => { test("Clicking on All status checkbox applies all of the statuses and clicking again removes them", async ({ authenticatedPage: page, }) => { - await clickCheckboxByLabel(page, "All"); // Click to check status checkbox. + await clickLabelForLocator(page.getByLabel("All")); // Click to check status checkbox. await expect(page).toHaveURL( /statuses=all,success,created,started,failed/, ); - await clickCheckboxByLabel(page, "All"); // Click to uncheck status checkbox. + await clickLabelForLocator(page.getByLabel("All")); // Click to uncheck status checkbox. await expect(page).not.toHaveURL(/statuses/); }); }); diff --git a/apps/spruce/playwright/tests/patch/configure_patch.spec.ts b/apps/spruce/playwright/tests/patch/configure_patch.spec.ts index d8b9134312..2f2da83e07 100644 --- a/apps/spruce/playwright/tests/patch/configure_patch.spec.ts +++ b/apps/spruce/playwright/tests/patch/configure_patch.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel, mockGraphQLResponse } from "../../helpers"; +import { clickLabelForLocator, mockGraphQLResponse } from "../../helpers"; const unactivatedPatchId = "5e6bb9e23066155a993e0f1a"; const patchWithDisplayTasks = "5e6bb9e23066155a993e0f1b"; @@ -184,8 +184,7 @@ test.describe("Configure Patch Page", () => { for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).toHaveAttribute("aria-checked", "false"); - const label = await checkbox.getAttribute("aria-label"); - await page.getByText(label!, { exact: true }).click(); + await clickLabelForLocator(checkbox); await expect(checkbox).toHaveAttribute("aria-checked", "true"); count += 1; @@ -210,8 +209,7 @@ test.describe("Configure Patch Page", () => { const taskCheckboxes = page.getByTestId("task-checkbox"); await expect(taskCheckboxes).toHaveCount(7); for (const checkbox of await taskCheckboxes.all()) { - const label = await checkbox.getAttribute("aria-label"); - await page.getByText(label!, { exact: true }).click(); + await clickLabelForLocator(checkbox); } const taskCountBadge = page .getByTestId("build-variant-list-item") @@ -223,8 +221,7 @@ test.describe("Configure Patch Page", () => { ); for (const checkbox of await taskCheckboxes.all()) { - const label = await checkbox.getAttribute("aria-label"); - await page.getByText(label!, { exact: true }).click(); + await clickLabelForLocator(checkbox); } await expect(taskCountBadge).toBeHidden(); @@ -289,12 +286,12 @@ test.describe("Configure Patch Page", () => { const taskCheckboxes = page.getByTestId("task-checkbox"); await expect(taskCheckboxes).toHaveCount(1); - await clickCheckboxByLabel(page, "Select all"); + await clickLabelForLocator(page.getByText("Select all")); for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).toBeChecked(); } - await clickCheckboxByLabel(page, "Select all"); + await clickLabelForLocator(page.getByText("Select all")); for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).not.toBeChecked(); } @@ -307,8 +304,7 @@ test.describe("Configure Patch Page", () => { await expect(taskCheckboxes).toHaveCount(1); await expect(page.getByTestId("select-all-checkbox")).not.toBeChecked(); for (const checkbox of await taskCheckboxes.all()) { - const label = await checkbox.getAttribute("aria-label"); - await page.getByText(label!, { exact: true }).click(); + await clickLabelForLocator(checkbox); } await expect(page.getByTestId("select-all-checkbox")).toBeChecked(); }); @@ -319,14 +315,12 @@ test.describe("Configure Patch Page", () => { const taskCheckboxes = page.getByTestId("task-checkbox"); await expect(taskCheckboxes).toHaveCount(1); for (const checkbox of await taskCheckboxes.all()) { - const label = await checkbox.getAttribute("aria-label"); - await page.getByText(label!, { exact: true }).click(); + await clickLabelForLocator(checkbox); await expect(checkbox).toBeChecked(); } await expect(page.getByTestId("select-all-checkbox")).toBeChecked(); for (const checkbox of await taskCheckboxes.all()) { - const label = await checkbox.getAttribute("aria-label"); - await page.getByText(label!, { exact: true }).click(); + await clickLabelForLocator(checkbox); await expect(checkbox).not.toBeChecked(); } await expect(page.getByTestId("select-all-checkbox")).not.toBeChecked(); @@ -342,8 +336,7 @@ test.describe("Configure Patch Page", () => { const taskCheckboxes = page.getByTestId("task-checkbox"); await expect(taskCheckboxes).toHaveCount(6); const firstCheckbox = taskCheckboxes.first(); - const label = await firstCheckbox.getAttribute("aria-label"); - await page.getByText(label!, { exact: true }).click(); + await clickLabelForLocator(firstCheckbox); await expect(page.getByTestId("select-all-checkbox")).toHaveAttribute( "aria-checked", "mixed", @@ -356,12 +349,12 @@ test.describe("Configure Patch Page", () => { const taskCheckboxes = page.getByTestId("task-checkbox"); await expect(taskCheckboxes).toHaveCount(1); - await clickCheckboxByLabel(page, "Select all"); + await clickLabelForLocator(page.getByText("Select all")); for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).toBeChecked(); } await expect(page.getByTestId("select-all-checkbox")).toBeChecked(); - await clickCheckboxByLabel(page, "Select all"); + await clickLabelForLocator(page.getByText("Select all")); for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).not.toBeChecked(); } @@ -376,14 +369,14 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - await clickCheckboxByLabel(page, "test-agent"); + await clickLabelForLocator(page.getByText("test-agent")); await expect(page.getByTestId("task-count-badge")).toHaveCount(1); await expect(page.getByTestId("task-count-badge")).toContainText("1"); await expect( page.getByTestId("selected-task-disclaimer"), ).toContainText("1 task across 1 build variant"); - await clickCheckboxByLabel(page, "test-agent"); + await clickLabelForLocator(page.getByText("test-agent")); await expect(page.getByTestId("task-count-badge")).toBeHidden(); await expect( page.getByTestId("selected-task-disclaimer"), @@ -447,7 +440,7 @@ test.describe("Configure Patch Page", () => { await expect( page.getByRole("checkbox", { name: "test-agent" }), ).toHaveCount(1); - await clickCheckboxByLabel(page, "test-agent"); + await clickLabelForLocator(page.getByText("test-agent")); const taskCountBadge = page .getByTestId("build-variant-select-wrapper") .getByTestId("task-count-badge"); @@ -471,12 +464,12 @@ test.describe("Configure Patch Page", () => { ).toBeChecked(); // Deselect the buttons and reset - await clickCheckboxByLabel(page, "test-agent"); + await clickLabelForLocator(page.getByText("test-agent")); await page .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - await clickCheckboxByLabel(page, "test-agent"); + await clickLabelForLocator(page.getByText("test-agent")); }); test.describe("Selecting/deselecting all multiple buildvariants", () => { @@ -507,7 +500,7 @@ test.describe("Configure Patch Page", () => { .getByText("RHEL 7.2 zLinux") .click(); - await clickCheckboxByLabel(page, "Select all"); + await clickLabelForLocator(page.getByText("Select all")); const taskCheckboxes = page.getByTestId("task-checkbox"); const count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { @@ -530,7 +523,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - await clickCheckboxByLabel(page, "Select all"); + await clickLabelForLocator(page.getByText("Select all")); const taskCheckboxes = page.getByTestId("task-checkbox"); let count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { @@ -541,7 +534,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.1 POWER8") .click(); - await clickCheckboxByLabel(page, "Select all"); + await clickLabelForLocator(page.getByText("Select all")); count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { @@ -554,7 +547,7 @@ test.describe("Configure Patch Page", () => { .getByText("RHEL 7.2 zLinux") .click(); - await clickCheckboxByLabel(page, "Select all"); + await clickLabelForLocator(page.getByText("Select all")); count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { @@ -617,7 +610,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("task-count-badge"); await expect(taskCountBadge).toBeHidden(); - await clickCheckboxByLabel(page, "Add alias to patch"); + await clickLabelForLocator(page.getByText("Add alias to patch")); await expect( page.getByTestId("selected-task-disclaimer"), @@ -645,13 +638,13 @@ test.describe("Configure Patch Page", () => { test("Updates the badge count when the trigger alias is deselected", async ({ authenticatedPage: page, }) => { - await clickCheckboxByLabel(page, "Add alias to patch"); + await clickLabelForLocator(page.getByText("Add alias to patch")); const taskCountBadge = page .getByTestId("trigger-alias-list-item") .getByTestId("task-count-badge"); await expect(taskCountBadge).toBeVisible(); - await clickCheckboxByLabel(page, "Add alias to patch"); + await clickLabelForLocator(page.getByText("Add alias to patch")); await expect(taskCountBadge).toBeHidden(); }); diff --git a/apps/spruce/playwright/tests/preferences/notifications.spec.ts b/apps/spruce/playwright/tests/preferences/notifications.spec.ts index eddb682365..f263deb627 100644 --- a/apps/spruce/playwright/tests/preferences/notifications.spec.ts +++ b/apps/spruce/playwright/tests/preferences/notifications.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; const pageRoute = "/preferences/notifications"; @@ -67,8 +67,7 @@ test.describe("preferences/notifications", () => { .getByTestId("subscription-row") .nth(0) .locator("input[type=checkbox]"); - const rowCheckboxId = await rowCheckbox.getAttribute("id"); - await page.locator(`label[for="${rowCheckboxId}"]`).click(); + await clickLabelForLocator(rowCheckbox); const deleteButton = page.getByTestId("delete-some-button"); await expect(deleteButton).toContainText("Delete (1)"); @@ -76,11 +75,10 @@ test.describe("preferences/notifications", () => { const headerCheckbox = page .locator("thead") .locator("input[type=checkbox]"); - const headerCheckboxId = await headerCheckbox.getAttribute("id"); - await page.locator(`label[for="${headerCheckboxId}"]`).click(); + await clickLabelForLocator(headerCheckbox); await expect(deleteButton).toContainText("Delete (3)"); - await page.locator(`label[for="${headerCheckboxId}"]`).click(); + await clickLabelForLocator(headerCheckbox); await expect(deleteButton).toContainText("Delete"); await expect(deleteButton).toHaveAttribute("aria-disabled", "true"); }); @@ -93,8 +91,7 @@ test.describe("preferences/notifications", () => { .getByTestId("subscription-row") .nth(0) .locator("input[type=checkbox]"); - const rowCheckboxId = await rowCheckbox.getAttribute("id"); - await page.locator(`label[for="${rowCheckboxId}"]`).click(); + await clickLabelForLocator(rowCheckbox); await page.getByTestId("delete-some-button").click(); await validateToast(page, "success", "Deleted 1 subscription."); await expect(page.getByTestId("subscription-row")).toHaveCount(2); diff --git a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts index 464b33fb99..fbfbce1bb0 100644 --- a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; import { getProjectSettingsRoute, getRepoSettingsRoute, @@ -158,7 +158,8 @@ test.describe("Project Settings when defaulting to repo", () => { await page .getByTestId("var-description-input") .fill("Description for variable a"); - await page.locator("label", { hasText: "Private" }).click(); + const privateCheckbox = page.getByTestId("var-private-input"); + await clickLabelForLocator(privateCheckbox); await page.getByTestId("add-button").click(); await page.getByTestId("var-name-input").first().fill("b"); @@ -188,11 +189,10 @@ test.describe("Project Settings when defaulting to repo", () => { await page.getByTestId("promote-vars-button").click(); await expect(page.getByTestId("promote-vars-modal")).toBeVisible(); - const variableToPromote = page + const variableToPromoteCheckbox = page .getByTestId("promote-var-checkbox") .first(); - const checkboxId = await variableToPromote.getAttribute("id"); - await page.locator(`label[for="${checkboxId}"]`).click(); + await clickLabelForLocator(variableToPromoteCheckbox); await page.getByRole("button", { name: "Move 1 variable" }).click(); await validateToast( @@ -258,10 +258,10 @@ test.describe("Project Settings when defaulting to repo", () => { await page .getByTestId("github-checks-enabled-radio-box") .scrollIntoViewIfNeeded(); - await page + const enabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .click(); + .getByLabel("Enabled"); + await clickLabelForLocator(enabledRadio); await expect( page.getByTestId("warning-banner").filter({ hasText: @@ -304,18 +304,18 @@ test.describe("Project Settings when defaulting to repo", () => { .getByTestId("pr-testing-enabled-radio-box") .getByText("Default to repo (enabled)"), ).toBeVisible(); - await page + const prDisabledRadio = page .getByTestId("pr-testing-enabled-radio-box") - .locator("label", { hasText: /^Disabled$/ }) - .click(); - await page + .getByLabel("Disabled"); + await clickLabelForLocator(prDisabledRadio); + const manualDisabledRadio = page .getByTestId("manual-pr-testing-enabled-radio-box") - .locator("label", { hasText: /^Disabled$/ }) - .click(); - await page + .getByLabel("Disabled"); + await clickLabelForLocator(manualDisabledRadio); + const githubEnabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .locator("label", { hasText: /^Enabled$/ }) - .click(); + .getByLabel("Enabled"); + await clickLabelForLocator(githubEnabledRadio); await save(page); await validateToast( page, @@ -387,9 +387,10 @@ test.describe("Project Settings when defaulting to repo", () => { test("Patch aliases added before defaulting to repo patch aliases are cleared", async ({ authenticatedPage: page, }) => { - await page - .locator("label", { hasText: "Override Repo Patch Aliases" }) - .click(); + const overrideRepoPatchAliasesRadio = page.getByLabel( + "Override Repo Patch Aliases", + ); + await clickLabelForLocator(overrideRepoPatchAliasesRadio); await expect( page.getByLabel("Override Repo Patch Aliases"), ).toHaveAttribute("aria-checked", "true"); @@ -417,16 +418,15 @@ test.describe("Project Settings when defaulting to repo", () => { await save(page); await validateToast(page, "success", "Successfully updated project"); - await page - .locator("label", { hasText: "Default to Repo Patch Aliases" }) - .click(); + const defaultToRepoRadio = page.getByLabel( + "Default to Repo Patch Aliases", + ); + await clickLabelForLocator(defaultToRepoRadio); await save(page); await validateToast(page, "success", "Successfully updated project"); await expectSaveButtonEnabled(page, false); - await page - .locator("label", { hasText: "Override Repo Patch Aliases" }) - .click(); + await clickLabelForLocator(overrideRepoPatchAliasesRadio); await expect(page.getByTestId("alias-row")).toHaveCount(0); }); }); @@ -437,7 +437,8 @@ test.describe("Project Settings when defaulting to repo", () => { }); test("Enable git clone", async ({ authenticatedPage: page }) => { - await page.locator("label", { hasText: "Enabled" }).click(); + const githubEnabledRadio = page.getByLabel("Enabled"); + await clickLabelForLocator(githubEnabledRadio); await expect(page.getByLabel("Enabled")).toBeChecked(); await save(page); await validateToast(page, "success", "Successfully updated project"); @@ -469,9 +470,11 @@ test.describe("Project Settings when defaulting to repo", () => { .locator("textarea", { hasText: "a repo command" }), ).toHaveAttribute("aria-disabled", "true"); - await page - .locator("label", { hasText: "Override Repo Commands" }) - .click(); + const overrideRepoCommandsRadio = page.getByLabel( + "Override Repo Commands", + ); + await clickLabelForLocator(overrideRepoCommandsRadio); + await expect(page.getByTestId("command-row")).toHaveCount(0); await page.getByRole("button", { name: "Add Command" }).click(); await page.getByTestId("command-input").fill("a project command"); @@ -483,9 +486,8 @@ test.describe("Project Settings when defaulting to repo", () => { .locator("textarea", { hasText: "a project command" }), ).toHaveAttribute("aria-disabled", "false"); - await page - .locator("label", { hasText: "Default to Repo Commands" }) - .click(); + const defaultToRepoRadio = page.getByLabel("Default to repo (disabled)"); + await clickLabelForLocator(defaultToRepoRadio); await save(page); await validateToast(page, "success", "Successfully updated project"); await expect( @@ -494,21 +496,21 @@ test.describe("Project Settings when defaulting to repo", () => { .locator("textarea", { hasText: "a repo command" }), ).toHaveAttribute("aria-disabled", "true"); - await page - .locator("label", { hasText: "Override Repo Commands" }) - .click(); + await clickLabelForLocator(overrideRepoCommandsRadio); await expect(page.getByTestId("command-row")).toHaveCount(0); }); test("Allows overriding without adding a command", async ({ authenticatedPage: page, }) => { - await page - .locator("label", { hasText: "Override Repo Commands" }) - .click(); + const overrideRepoCommandsRadio = page.getByLabel( + "Override Repo Commands", + ); + await clickLabelForLocator(overrideRepoCommandsRadio); + await expect(overrideRepoCommandsRadio).toBeChecked(); await save(page); await validateToast(page, "success", "Successfully updated project"); - await expect(page.getByLabel("Override Repo Commands")).toBeChecked(); + await expect(overrideRepoCommandsRadio).toBeChecked(); }); }); }); diff --git a/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts index 05f2adb572..1ad9f9e372 100644 --- a/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts @@ -4,6 +4,7 @@ import { validateDatePickerDate, clearDatePickerInput, typeDatePickerDate, + clickLabelForLocator, } from "../../helpers"; import { getProjectSettingsRoute, project } from "./constants"; import { expectSaveButtonEnabled, save } from "./utils"; @@ -145,7 +146,8 @@ test.describe("Project Settings when not defaulting to repo", () => { await page.getByTestId("add-button").click(); await page.getByTestId("var-name-input").first().fill("admin_var"); await page.getByTestId("var-value-input").first().fill("admin_value"); - await page.locator("label", { hasText: "Admin Only" }).click(); + const adminOnlyCheckbox = page.getByTestId("var-admin-input"); + await clickLabelForLocator(adminOnlyCheckbox); await expect(page.getByTestId("var-admin-input")).toBeChecked(); await save(page); await validateToast(page, "success", "Successfully updated project"); diff --git a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts index 6e296b6a34..71ac8edf4c 100644 --- a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel, validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; import { getProjectSettingsRoute, getRepoSettingsRoute, @@ -66,19 +66,19 @@ test.describe("Repo Settings", () => { test("Shows an error banner when Commit Checks are enabled and hides it when Commit Checks are disabled", async ({ authenticatedPage: page, }) => { - await page + const githubChecksEnabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .click(); + .getByLabel("Enabled"); + await clickLabelForLocator(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 page + const githubChecksDisabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .locator("label", { hasText: "Disabled" }) - .click(); + .getByLabel("Disabled"); + await clickLabelForLocator(githubChecksDisabledRadio); await expect(errorBanner).toHaveCount(0); }); @@ -139,10 +139,11 @@ test.describe("Repo Settings", () => { test.describe("Merge Queue section", () => { test.beforeEach(async ({ authenticatedPage: page }) => { - await page + const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .scrollIntoViewIfNeeded(); + .getByLabel("Enabled"); + await mergeQueueEnabledRadio.scrollIntoViewIfNeeded(); + await clickLabelForLocator(mergeQueueEnabledRadio); }); test("Enabling merge queue shows hidden inputs and error banner", async ({ @@ -151,10 +152,10 @@ test.describe("Repo Settings", () => { const cqCardFields = page.getByTestId("cq-card").locator("> *"); await expect(cqCardFields).toHaveCount(2); - await page + const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .click(); + .getByLabel("Enabled"); + await clickLabelForLocator(mergeQueueEnabledRadio); await expect(cqCardFields).toHaveCount(3); await page .getByText("Merge Queue Patch Definitions") @@ -170,20 +171,20 @@ test.describe("Repo Settings", () => { test("Does not show override buttons for merge queue patch definitions", async ({ authenticatedPage: page, }) => { - await page + const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .click(); + .getByLabel("Enabled"); + await clickLabelForLocator(mergeQueueEnabledRadio); await expect(page.getByTestId("cq-override-radio-box")).toHaveCount(0); }); test("Saves a merge queue definition", async ({ authenticatedPage: page, }) => { - await page + const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .click(); + .getByLabel("Enabled"); + await clickLabelForLocator(mergeQueueEnabledRadio); await page .getByRole("button", { name: "Add Patch Definition" }) .click(); @@ -303,13 +304,13 @@ test.describe("Repo Settings", () => { const githubPRLabel = "Schedule in GitHub Pull Requests"; const pullRequestCheckbox = page.getByLabel(githubPRLabel); await expect(pullRequestCheckbox).not.toBeChecked(); - await clickCheckboxByLabel(page, githubPRLabel); + await clickLabelForLocator(page.getByLabel(githubPRLabel)); await expect(pullRequestCheckbox).toBeChecked(); const githubMQLabel = "Schedule in GitHub Merge Queue"; const mergeQueueCheckbox = page.getByLabel(githubMQLabel); await expect(mergeQueueCheckbox).not.toBeChecked(); - await clickCheckboxByLabel(page, githubMQLabel); + await clickLabelForLocator(page.getByLabel(githubMQLabel)); await expect(mergeQueueCheckbox).toBeChecked(); await save(page); diff --git a/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts b/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts index ec1621b197..d12d62e4e8 100644 --- a/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; import { getProjectSettingsRoute, project, @@ -26,10 +26,10 @@ test.describe("Stepback bisect setting", () => { test("Clicking on enabled and then save shows a success toast", async ({ authenticatedPage: page, }) => { - await page + const enableRadio = page .getByTestId("stepback-bisect-group") - .locator("label", { hasText: "Enable" }) - .click(); + .getByLabel("Enable"); + await clickLabelForLocator(enableRadio); await save(page); await validateToast(page, "success", "Successfully updated project"); @@ -61,10 +61,10 @@ test.describe("Stepback bisect setting", () => { test("Clicking on enabled and then save shows a success toast", async ({ authenticatedPage: page, }) => { - await page + const enableRadio = page .getByTestId("stepback-bisect-group") - .locator("label", { hasText: "Enable" }) - .click(); + .getByLabel("Enabled"); + await clickLabelForLocator(enableRadio); await save(page); await validateToast(page, "success", "Successfully updated project"); diff --git a/apps/spruce/playwright/tests/spawn/host.spec.ts b/apps/spruce/playwright/tests/spawn/host.spec.ts index 158da11440..2bc5248f25 100644 --- a/apps/spruce/playwright/tests/spawn/host.spec.ts +++ b/apps/spruce/playwright/tests/spawn/host.spec.ts @@ -1,5 +1,9 @@ import { test, expect } from "../../fixtures"; -import { clearDatePickerInput, typeDatePickerDate } from "../../helpers"; +import { + clearDatePickerInput, + clickLabelForLocator, + typeDatePickerDate, +} from "../../helpers"; const ascendingSortSpawnHostOrderByHostId = [ "i-04ade558e1e26b0ad", @@ -220,8 +224,7 @@ test.describe("Spawn Host page", () => { await expect(page.getByText(startHostsCheckbox)).toBeVisible(); const loadDataCheckbox = page.getByTestId("load-data-checkbox"); - const loadDataId = await loadDataCheckbox.getAttribute("id"); - await page.locator(`label[for="${loadDataId}"]`).click(); + await clickLabelForLocator(loadDataCheckbox); await expect(loadDataCheckbox).not.toBeChecked(); await expect(page.getByText(projectSetupCheckbox)).toHaveCount(0); await expect(page.getByText(startHostsCheckbox)).toHaveCount(0); @@ -311,13 +314,11 @@ test.describe("Spawn Host page", () => { await expect(projectCheckbox).toHaveAttribute("aria-disabled", "false"); await expect(setupCheckbox).toHaveAttribute("aria-disabled", "true"); - const projectId = await projectCheckbox.getAttribute("id"); - await page.locator(`label[for="${projectId}"]`).click(); + await clickLabelForLocator(projectCheckbox); await expect(projectCheckbox).toHaveAttribute("aria-disabled", "false"); await expect(setupCheckbox).toHaveAttribute("aria-disabled", "false"); - const setupId = await setupCheckbox.getAttribute("id"); - await page.locator(`label[for="${setupId}"]`).click(); + await clickLabelForLocator(setupCheckbox); await expect(projectCheckbox).toHaveAttribute("aria-disabled", "true"); await expect(setupCheckbox).toHaveAttribute("aria-disabled", "false"); }); diff --git a/apps/spruce/playwright/tests/spawn/volume.spec.ts b/apps/spruce/playwright/tests/spawn/volume.spec.ts index 4cb76b4610..8149b3ec11 100644 --- a/apps/spruce/playwright/tests/spawn/volume.spec.ts +++ b/apps/spruce/playwright/tests/spawn/volume.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../../fixtures"; import { - clickCheckboxByLabel, + clickLabelForLocator, mockGraphQLResponse, selectOption, validateDatePickerDate, @@ -145,8 +145,7 @@ test.describe("Spawn volume page", () => { const yesButton = popconfirm.getByRole("button", { name: "Yes" }); await expect(yesButton).toHaveAttribute("aria-disabled", "true"); - const checkboxId = await confirmCheckbox.getAttribute("id"); - await page.locator(`label[for="${checkboxId}"]`).click(); + await clickLabelForLocator(confirmCheckbox); await expect(yesButton).not.toHaveAttribute("aria-disabled", "true"); await yesButton.click(); @@ -308,7 +307,7 @@ test.describe("Spawn volume page", () => { "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", ); await expect(saveButton).toHaveAttribute("aria-disabled", "true"); - await clickCheckboxByLabel(page, "Never expire"); + await clickLabelForLocator(page.getByLabel("Never expire")); await expect(saveButton).toHaveAttribute("aria-disabled", "false"); }); diff --git a/apps/spruce/playwright/tests/task/test_table.spec.ts b/apps/spruce/playwright/tests/task/test_table.spec.ts index b3b7fb8070..915934e9a2 100644 --- a/apps/spruce/playwright/tests/task/test_table.spec.ts +++ b/apps/spruce/playwright/tests/task/test_table.spec.ts @@ -1,6 +1,6 @@ import { Page } from "@playwright/test"; import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel } from "../../helpers"; +import { clickLabelForLocator } from "../../helpers"; const TESTS_ROUTE = "/task/evergreen_ubuntu1604_test_model_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48/tests"; @@ -57,7 +57,7 @@ test.describe("Tests Table", () => { await expect(totalCount.getByText("20")).toBeVisible(); await page.getByTestId("status-treeselect").click(); - await clickCheckboxByLabel(page, "Silent fail"); + await clickLabelForLocator(page.getByText("Silent fail")); await expect(filteredCount.getByText("1")).toBeVisible(); await expect(totalCount.getByText("20")).toBeVisible(); @@ -141,7 +141,7 @@ test.describe("Tests Table", () => { authenticatedPage: page, }) => { await page.getByTestId("status-treeselect").click(); - await clickCheckboxByLabel(page, "All"); + await clickLabelForLocator(page.getByText("All")); await expect(page).toHaveURL(/statuses=all,pass,fail,skip,silentfail/); }); @@ -156,7 +156,7 @@ test.describe("Tests Table", () => { }) => { await page.getByTestId("status-treeselect").click(); for (const { display } of statuses) { - await clickCheckboxByLabel(page, display); + await clickLabelForLocator(page.getByText(display)); } await expect(page).toHaveURL( new RegExp(`statuses=${statuses.map(({ key }) => key).join(",")}`), diff --git a/apps/spruce/playwright/tests/version/restart_modal.spec.ts b/apps/spruce/playwright/tests/version/restart_modal.spec.ts index d644e005ee..c20b1cf1e9 100644 --- a/apps/spruce/playwright/tests/version/restart_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/restart_modal.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel, validateToast } from "../../helpers"; +import { clickLabelForLocator, validateToast } from "../../helpers"; const path = "/version/5ecedafb562343215a7ff297"; @@ -71,14 +71,14 @@ test.describe("version/restart_modal", () => { authenticatedPage: page, }) => { await page.getByTestId("task-status-filter").click(); - await clickCheckboxByLabel(page, "All"); + await clickLabelForLocator(page.getByText("All")); await page.getByTestId("task-status-filter").click(); await expect(page.getByTestId("version-restart-modal")).toContainText( "Are you sure you want to restart the 1 selected tasks?", ); await page.getByTestId("task-status-filter").click(); - await clickCheckboxByLabel(page, "All"); + await clickLabelForLocator(page.getByText("All")); await page.getByTestId("task-status-filter").click(); }); @@ -87,14 +87,14 @@ test.describe("version/restart_modal", () => { }) => { const modal = page.getByTestId("version-restart-modal"); await modal.getByTestId("base-task-status-filter").click(); - await clickCheckboxByLabel(page, "Succeeded"); + await clickLabelForLocator(page.getByText("Succeeded")); await modal.getByTestId("base-task-status-filter").click(); await expect(modal.getByTestId("confirmation-message")).toContainText( "Are you sure you want to restart the 1 selected tasks?", ); await modal.getByTestId("base-task-status-filter").click(); - await clickCheckboxByLabel(page, "Succeeded"); + await clickLabelForLocator(page.getByText("Succeeded")); await modal.getByTestId("base-task-status-filter").click(); }); diff --git a/apps/spruce/playwright/tests/version/task_filters.spec.ts b/apps/spruce/playwright/tests/version/task_filters.spec.ts index b4e01244c0..070407b5e0 100644 --- a/apps/spruce/playwright/tests/version/task_filters.spec.ts +++ b/apps/spruce/playwright/tests/version/task_filters.spec.ts @@ -1,6 +1,6 @@ import { Page } from "@playwright/test"; import { test, expect } from "../../fixtures"; -import { clickCheckboxByLabel } from "../../helpers"; +import { clickLabelForLocator } from "../../helpers"; const patch = { id: "5e4ff3abe3c3317e352062e4" }; const pathTasks = `/version/${patch.id}/tasks`; @@ -160,7 +160,7 @@ test.describe("Tasks filters", () => { "Dispatched", "Blocked", ]; - await clickCheckboxByLabel(page, "All"); + await clickLabelForLocator(page.getByText("All")); for (const label of taskStatuses) { const checkbox = page.getByRole("checkbox", { name: label }).first(); await expect(checkbox).toBeChecked(); @@ -168,7 +168,7 @@ test.describe("Tasks filters", () => { await expect(page).toHaveURL(/statuses=all/); await waitForTaskTable(page); - await clickCheckboxByLabel(page, "All"); + await clickLabelForLocator(page.getByText("All")); for (const label of taskStatuses) { const checkbox = page.getByRole("checkbox", { name: label }).first(); await expect(checkbox).not.toBeChecked(); @@ -190,12 +190,12 @@ test.describe("Tasks filters", () => { test("Clicking on a base status filter filters the tasks to only those base statuses", async ({ authenticatedPage: page, }) => { - await clickCheckboxByLabel(page, "Succeeded"); + await clickLabelForLocator(page.getByText("Succeeded")); await expect(page).toHaveURL(/baseStatuses=success/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("44"); - await clickCheckboxByLabel(page, "Succeeded"); + await clickLabelForLocator(page.getByText("Succeeded")); await expect(page).not.toHaveURL(/baseStatuses/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("47"); @@ -205,14 +205,14 @@ test.describe("Tasks filters", () => { authenticatedPage: page, }) => { const taskStatuses = ["All", "Succeeded", "Running"]; - await clickCheckboxByLabel(page, "All"); + await clickLabelForLocator(page.getByText("All")); for (const label of taskStatuses) { await expect(page.getByRole("checkbox", { name: label })).toBeChecked(); } await expect(page).toHaveURL(/baseStatuses=all/); await waitForTaskTable(page); - await clickCheckboxByLabel(page, "All"); + await clickLabelForLocator(page.getByText("All")); for (const label of taskStatuses) { await expect( page.getByRole("checkbox", { name: label }), diff --git a/apps/spruce/playwright/tests/version/task_table.spec.ts b/apps/spruce/playwright/tests/version/task_table.spec.ts index 994455fd51..467d19c3f8 100644 --- a/apps/spruce/playwright/tests/version/task_table.spec.ts +++ b/apps/spruce/playwright/tests/version/task_table.spec.ts @@ -1,6 +1,7 @@ import { Page } from "@playwright/test"; import { SEEN_TASK_REVIEW_TOOLTIP } from "constants/cookies"; import { test, expect } from "../../fixtures"; +import { clickLabelForLocator } from "../../helpers"; const pathTasks = "/version/5e4ff3abe3c3317e352062e4/tasks"; const patchDescriptionTasksExist = "dist"; @@ -159,8 +160,7 @@ test.describe("Task table", () => { test.describe("task review", () => { // Clicks the label associated with a reviewed checkbox (the input itself is hidden). const clickReviewed = async (page: Page, testId: string) => { - const id = await page.getByTestId(testId).getAttribute("id"); - await page.locator(`label[for="${id}"]`).click(); + await clickLabelForLocator(page.getByTestId(testId)); }; test("marks tasks as viewed and preserves their state on reload", async ({ diff --git a/apps/spruce/playwright/tests/waterfall/menu.spec.ts b/apps/spruce/playwright/tests/waterfall/menu.spec.ts index 0826f63058..a3199e52e3 100644 --- a/apps/spruce/playwright/tests/waterfall/menu.spec.ts +++ b/apps/spruce/playwright/tests/waterfall/menu.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from "../../fixtures"; import { selectOption, validateToast, - clickCheckboxByLabel, + clickLabelForLocator, mockGraphQLResponse, } from "../../helpers"; @@ -18,7 +18,7 @@ test.describe("Waterfall menu settings", () => { await expect( page.getByTestId("omit-inactive-builds-checkbox"), ).not.toBeChecked(); - await clickCheckboxByLabel(page, "Omit inactive builds"); + await clickLabelForLocator(page.getByText("Omit inactive builds")); await expect( page.getByTestId("omit-inactive-builds-checkbox"), ).toBeChecked(); @@ -29,7 +29,7 @@ test.describe("Waterfall menu settings", () => { page.getByTestId("omit-inactive-builds-checkbox"), ).toBeChecked(); - await clickCheckboxByLabel(page, "Omit inactive builds"); + await clickLabelForLocator(page.getByText("Omit inactive builds")); }); test("omits inactive build variants when filter is applied and setting is enabled", async ({ @@ -40,7 +40,7 @@ test.describe("Waterfall menu settings", () => { await expect(page.getByTestId("build-variant-label")).toHaveCount(1); await page.getByTestId("waterfall-menu").click(); - await clickCheckboxByLabel(page, "Omit inactive builds"); + await clickLabelForLocator(page.getByText("Omit inactive builds")); await page.locator("body").click(); await page.getByTestId("build-variant-filter-input").clear(); @@ -51,7 +51,7 @@ test.describe("Waterfall menu settings", () => { expect(count).toBeGreaterThanOrEqual(1); await page.getByTestId("waterfall-menu").click(); - await clickCheckboxByLabel(page, "Omit inactive builds"); + await clickLabelForLocator(page.getByText("Omit inactive builds")); }); }); diff --git a/packages/playwright-config/helpers/index.ts b/packages/playwright-config/helpers/index.ts index 8bb81f4a37..76092adbfa 100644 --- a/packages/playwright-config/helpers/index.ts +++ b/packages/playwright-config/helpers/index.ts @@ -1,4 +1,4 @@ -import { expect, Page } from "@playwright/test"; +import { expect, Locator, Page } from "@playwright/test"; import { evergreenUrl, toastDataCy, users } from "./constants"; /** @@ -56,20 +56,13 @@ export const validateToast = async ( }; /** - * Checkboxes are not visible in LG so they cannot be clicked. - * We need to click the associated label instead. - * @param page - The Playwright page object - * @param name - The name of the checkbox + * Checkboxes and radio options are not visible in LG so they cannot be clicked directly. + * This helper clicks the associated label instead, via the associated id attribute. + * @param locator - A locator pointing to the checkbox or radio element */ -export const clickCheckboxByLabel = async (page: Page, name: string) => { - const checkbox = page.getByRole("checkbox", { name }); - const checkboxId = await checkbox.getAttribute("id"); - if (checkboxId) { - await page.locator(`label[for="${checkboxId}"]`).click(); - } else { - // Fallback: click the checkbox's parent label if it exists. - await checkbox.locator("..").locator("label").click(); - } +export const clickLabelForLocator = async (locator: Locator) => { + const id = await locator.getAttribute("id"); + await locator.page().locator(`label[for="${id}"]`).click(); }; type ResponseData = { From 45d75c1774cdbb55aa112e774ff98c43c09340b1 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 17:12:03 -0400 Subject: [PATCH 14/32] fix: failing tests due to refactor --- .../tests/patch/configure_patch.spec.ts | 67 +++++++++---------- .../defaulting_to_repo.spec.ts | 60 +++++++++-------- .../projectSettings/repo_settings.spec.ts | 18 ++--- .../playwright/tests/task/test_table.spec.ts | 18 +++-- .../tests/version/restart_modal.spec.ts | 13 ++-- .../tests/version/task_filters.spec.ts | 25 +++---- .../ConfigureTasks/index.tsx | 6 +- .../VirtualWorkstationTab/getFormSchema.ts | 1 + .../src/components/TreeSelect/TreeSelect.tsx | 6 +- 9 files changed, 106 insertions(+), 108 deletions(-) diff --git a/apps/spruce/playwright/tests/patch/configure_patch.spec.ts b/apps/spruce/playwright/tests/patch/configure_patch.spec.ts index 2f2da83e07..39637d523e 100644 --- a/apps/spruce/playwright/tests/patch/configure_patch.spec.ts +++ b/apps/spruce/playwright/tests/patch/configure_patch.spec.ts @@ -286,12 +286,12 @@ test.describe("Configure Patch Page", () => { const taskCheckboxes = page.getByTestId("task-checkbox"); await expect(taskCheckboxes).toHaveCount(1); - await clickLabelForLocator(page.getByText("Select all")); + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).toBeChecked(); } - await clickLabelForLocator(page.getByText("Select all")); + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).not.toBeChecked(); } @@ -302,11 +302,12 @@ test.describe("Configure Patch Page", () => { }) => { const taskCheckboxes = page.getByTestId("task-checkbox"); await expect(taskCheckboxes).toHaveCount(1); - await expect(page.getByTestId("select-all-checkbox")).not.toBeChecked(); + const selectAllCheckbox = page.getByTestId("select-all-checkbox"); + await expect(selectAllCheckbox).not.toBeChecked(); for (const checkbox of await taskCheckboxes.all()) { await clickLabelForLocator(checkbox); } - await expect(page.getByTestId("select-all-checkbox")).toBeChecked(); + await expect(selectAllCheckbox).toBeChecked(); }); test("Unchecking all task checkboxes should uncheck the Select All checkbox", async ({ @@ -349,12 +350,13 @@ test.describe("Configure Patch Page", () => { const taskCheckboxes = page.getByTestId("task-checkbox"); await expect(taskCheckboxes).toHaveCount(1); - await clickLabelForLocator(page.getByText("Select all")); + const selectAllCheckbox = page.getByTestId("select-all-checkbox"); + await clickLabelForLocator(selectAllCheckbox); for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).toBeChecked(); } - await expect(page.getByTestId("select-all-checkbox")).toBeChecked(); - await clickLabelForLocator(page.getByText("Select all")); + await expect(selectAllCheckbox).toBeChecked(); + await clickLabelForLocator(selectAllCheckbox); for (const checkbox of await taskCheckboxes.all()) { await expect(checkbox).not.toBeChecked(); } @@ -369,14 +371,15 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - await clickLabelForLocator(page.getByText("test-agent")); + const testAgentCheckbox = page.getByTestId("test-agent-task-checkbox"); + await clickLabelForLocator(testAgentCheckbox); await expect(page.getByTestId("task-count-badge")).toHaveCount(1); await expect(page.getByTestId("task-count-badge")).toContainText("1"); await expect( page.getByTestId("selected-task-disclaimer"), ).toContainText("1 task across 1 build variant"); - await clickLabelForLocator(page.getByText("test-agent")); + await clickLabelForLocator(testAgentCheckbox); await expect(page.getByTestId("task-count-badge")).toBeHidden(); await expect( page.getByTestId("selected-task-disclaimer"), @@ -437,10 +440,9 @@ test.describe("Configure Patch Page", () => { .getByText("RHEL 7.1 POWER8") .click(); await page.keyboard.up("Meta"); - await expect( - page.getByRole("checkbox", { name: "test-agent" }), - ).toHaveCount(1); - await clickLabelForLocator(page.getByText("test-agent")); + const testAgentCheckbox = page.getByTestId("test-agent-task-checkbox"); + await expect(testAgentCheckbox).toHaveCount(1); + await clickLabelForLocator(testAgentCheckbox); const taskCountBadge = page .getByTestId("build-variant-select-wrapper") .getByTestId("task-count-badge"); @@ -451,25 +453,21 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - await expect( - page.getByRole("checkbox", { name: "test-agent" }), - ).toBeChecked(); + await expect(testAgentCheckbox).toBeChecked(); await page .getByTestId("build-variant-list-item") .getByText("RHEL 7.1 POWER8") .click(); - await expect( - page.getByRole("checkbox", { name: "test-agent" }), - ).toBeChecked(); + await expect(testAgentCheckbox).toBeChecked(); // Deselect the buttons and reset - await clickLabelForLocator(page.getByText("test-agent")); + await clickLabelForLocator(testAgentCheckbox); await page .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - await clickLabelForLocator(page.getByText("test-agent")); + await clickLabelForLocator(testAgentCheckbox); }); test.describe("Selecting/deselecting all multiple buildvariants", () => { @@ -482,7 +480,7 @@ test.describe("Configure Patch Page", () => { .click(); const variant1TaskCount = await page - .getByTestId("task-checkbox") + .locator('[data-cy$="task-checkbox"]') .count(); await page @@ -491,7 +489,7 @@ test.describe("Configure Patch Page", () => { .click(); const variant2TaskCount = await page - .getByTestId("task-checkbox") + .locator('[data-cy$="task-checkbox"]') .count(); await page.keyboard.down("Meta"); @@ -500,8 +498,8 @@ test.describe("Configure Patch Page", () => { .getByText("RHEL 7.2 zLinux") .click(); - await clickLabelForLocator(page.getByText("Select all")); - const taskCheckboxes = page.getByTestId("task-checkbox"); + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); + const taskCheckboxes = page.locator('[data-cy$="task-checkbox"]'); const count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { await expect(taskCheckboxes.nth(i)).toBeChecked(); @@ -523,8 +521,9 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - await clickLabelForLocator(page.getByText("Select all")); - const taskCheckboxes = page.getByTestId("task-checkbox"); + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); + + const taskCheckboxes = page.locator('[data-cy$="task-checkbox"]'); let count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { await expect(taskCheckboxes.nth(i)).toBeChecked(); @@ -534,7 +533,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.1 POWER8") .click(); - await clickLabelForLocator(page.getByText("Select all")); + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { @@ -546,8 +545,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - - await clickLabelForLocator(page.getByText("Select all")); + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { @@ -610,7 +608,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("task-count-badge"); await expect(taskCountBadge).toBeHidden(); - await clickLabelForLocator(page.getByText("Add alias to patch")); + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); await expect( page.getByTestId("selected-task-disclaimer"), @@ -631,21 +629,18 @@ test.describe("Configure Patch Page", () => { .getByText("Windows") .click(); await expect(page.getByTestId("alias-checkbox")).toHaveCount(1); - await expect(page.locator("[data-selected=true]")).toHaveCount(2); }); test("Updates the badge count when the trigger alias is deselected", async ({ authenticatedPage: page, }) => { - await clickLabelForLocator(page.getByText("Add alias to patch")); - + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); const taskCountBadge = page .getByTestId("trigger-alias-list-item") .getByTestId("task-count-badge"); await expect(taskCountBadge).toBeVisible(); - await clickLabelForLocator(page.getByText("Add alias to patch")); - + await clickLabelForLocator(page.getByTestId("select-all-checkbox")); await expect(taskCountBadge).toBeHidden(); }); }); diff --git a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts index fbfbce1bb0..f721db5d6b 100644 --- a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts @@ -299,22 +299,17 @@ test.describe("Project Settings when defaulting to repo", () => { test("Returns an error on save because no commit check definitions are defined", async ({ authenticatedPage: page, }) => { - await expect( - page - .getByTestId("pr-testing-enabled-radio-box") - .getByText("Default to repo (enabled)"), - ).toBeVisible(); const prDisabledRadio = page .getByTestId("pr-testing-enabled-radio-box") - .getByLabel("Disabled"); + .getByLabel("Disabled", { exact: true }); await clickLabelForLocator(prDisabledRadio); const manualDisabledRadio = page .getByTestId("manual-pr-testing-enabled-radio-box") - .getByLabel("Disabled"); + .getByLabel("Disabled", { exact: true }); await clickLabelForLocator(manualDisabledRadio); const githubEnabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .getByLabel("Enabled"); + .getByLabel("Enabled", { exact: true }); await clickLabelForLocator(githubEnabledRadio); await save(page); await validateToast( @@ -445,7 +440,8 @@ test.describe("Project Settings when defaulting to repo", () => { }); test("Add commands", async ({ authenticatedPage: page }) => { - await expect(page.getByLabel("Default to repo (disabled)")).toBeChecked(); + const defaultToRepoRadio = page.getByLabel("Default to repo (disabled)"); + await expect(defaultToRepoRadio).toBeChecked(); await expect(page.getByTestId("command-row")).toHaveCount(0); await page.getByTestId("attached-repo-link").click(); @@ -460,15 +456,15 @@ test.describe("Project Settings when defaulting to repo", () => { 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"); + await validateToast(page, "success", "Successfully updated repo", true); await page.goto(origin); await page.getByTestId("navitem-virtual-workstation").click(); - await expect( - page - .getByTestId("command-row") - .locator("textarea", { hasText: "a repo command" }), - ).toHaveAttribute("aria-disabled", "true"); + + await expect(page.getByTestId("command-row")).toHaveCount(1); + const repoCommandInput = page.getByTestId("repo-command-input"); + await expect(repoCommandInput).toHaveValue("a repo command"); + await expect(repoCommandInput).toHaveAttribute("aria-disabled", "true"); const overrideRepoCommandsRadio = page.getByLabel( "Override Repo Commands", @@ -477,24 +473,30 @@ test.describe("Project Settings when defaulting to repo", () => { await expect(page.getByTestId("command-row")).toHaveCount(0); await page.getByRole("button", { name: "Add Command" }).click(); - await page.getByTestId("command-input").fill("a project command"); + const projectCommandInput = page.getByTestId("command-input"); + await projectCommandInput.fill("a project command"); await save(page); - await validateToast(page, "success", "Successfully updated project"); - await expect( - page - .getByTestId("command-row") - .locator("textarea", { hasText: "a project command" }), - ).toHaveAttribute("aria-disabled", "false"); + await validateToast( + page, + "success", + "Successfully updated project", + true, + ); + await expect(projectCommandInput).toHaveValue("a project command"); + await expect(projectCommandInput).toHaveAttribute( + "aria-disabled", + "false", + ); - const defaultToRepoRadio = page.getByLabel("Default to repo (disabled)"); - await clickLabelForLocator(defaultToRepoRadio); + const defaultToRepoCommandsRadio = page.getByLabel( + "Default to repo commands", + ); + await clickLabelForLocator(defaultToRepoCommandsRadio); + await expect(page.getByTestId("command-row")).toHaveCount(1); + await expect(repoCommandInput).toHaveValue("a repo command"); + await expect(repoCommandInput).toHaveAttribute("aria-disabled", "true"); await save(page); await validateToast(page, "success", "Successfully updated project"); - await expect( - page - .getByTestId("command-row") - .locator("textarea", { hasText: "a repo command" }), - ).toHaveAttribute("aria-disabled", "true"); await clickLabelForLocator(overrideRepoCommandsRadio); await expect(page.getByTestId("command-row")).toHaveCount(0); diff --git a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts index 71ac8edf4c..06c57c7869 100644 --- a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts @@ -138,25 +138,21 @@ test.describe("Repo Settings", () => { }); test.describe("Merge Queue section", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - const mergeQueueEnabledRadio = page - .getByTestId("cq-enabled-radio-box") - .getByLabel("Enabled"); - await mergeQueueEnabledRadio.scrollIntoViewIfNeeded(); - await clickLabelForLocator(mergeQueueEnabledRadio); - }); - test("Enabling merge queue shows hidden inputs and error banner", async ({ authenticatedPage: page, }) => { - const cqCardFields = page.getByTestId("cq-card").locator("> *"); - await expect(cqCardFields).toHaveCount(2); + await expect( + page.getByText("Merge Queue Patch Definitions"), + ).toBeHidden(); const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") .getByLabel("Enabled"); await clickLabelForLocator(mergeQueueEnabledRadio); - await expect(cqCardFields).toHaveCount(3); + + await expect( + page.getByText("Merge Queue Patch Definitions"), + ).toBeVisible(); await page .getByText("Merge Queue Patch Definitions") .scrollIntoViewIfNeeded(); diff --git a/apps/spruce/playwright/tests/task/test_table.spec.ts b/apps/spruce/playwright/tests/task/test_table.spec.ts index 915934e9a2..34f9c2730a 100644 --- a/apps/spruce/playwright/tests/task/test_table.spec.ts +++ b/apps/spruce/playwright/tests/task/test_table.spec.ts @@ -57,7 +57,7 @@ test.describe("Tests Table", () => { await expect(totalCount.getByText("20")).toBeVisible(); await page.getByTestId("status-treeselect").click(); - await clickLabelForLocator(page.getByText("Silent fail")); + await clickLabelForLocator(page.getByTestId("silent-fail-checkbox")); await expect(filteredCount.getByText("1")).toBeVisible(); await expect(totalCount.getByText("20")).toBeVisible(); @@ -141,22 +141,26 @@ test.describe("Tests Table", () => { authenticatedPage: page, }) => { await page.getByTestId("status-treeselect").click(); - await clickLabelForLocator(page.getByText("All")); + await clickLabelForLocator(page.getByTestId("all-checkbox")); await expect(page).toHaveURL(/statuses=all,pass,fail,skip,silentfail/); }); const statuses = [ - { display: "Pass", key: "pass" }, - { display: "Silent Fail", key: "silentfail" }, - { display: "Skip", key: "skip" }, + { display: "Pass", key: "pass", dataCy: "pass-checkbox" }, + { + display: "Silent Fail", + key: "silentfail", + dataCy: "silent-fail-checkbox", + }, + { display: "Skip", key: "skip", dataCy: "skip-checkbox" }, ]; test("Checking multiple statuses adds them all to the URL", async ({ authenticatedPage: page, }) => { await page.getByTestId("status-treeselect").click(); - for (const { display } of statuses) { - await clickLabelForLocator(page.getByText(display)); + for (const { dataCy } of statuses) { + await clickLabelForLocator(page.getByTestId(dataCy)); } await expect(page).toHaveURL( new RegExp(`statuses=${statuses.map(({ key }) => key).join(",")}`), diff --git a/apps/spruce/playwright/tests/version/restart_modal.spec.ts b/apps/spruce/playwright/tests/version/restart_modal.spec.ts index c20b1cf1e9..930aaa661a 100644 --- a/apps/spruce/playwright/tests/version/restart_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/restart_modal.spec.ts @@ -71,14 +71,17 @@ test.describe("version/restart_modal", () => { authenticatedPage: page, }) => { await page.getByTestId("task-status-filter").click(); - await clickLabelForLocator(page.getByText("All")); + const options = page.getByTestId("tree-select-options"); + await expect(options).toBeVisible(); + await clickLabelForLocator(options.getByTestId("all-checkbox")); await page.getByTestId("task-status-filter").click(); await expect(page.getByTestId("version-restart-modal")).toContainText( "Are you sure you want to restart the 1 selected tasks?", ); await page.getByTestId("task-status-filter").click(); - await clickLabelForLocator(page.getByText("All")); + await expect(options).toBeVisible(); + await clickLabelForLocator(options.getByTestId("all-checkbox")); await page.getByTestId("task-status-filter").click(); }); @@ -87,14 +90,16 @@ test.describe("version/restart_modal", () => { }) => { const modal = page.getByTestId("version-restart-modal"); await modal.getByTestId("base-task-status-filter").click(); - await clickLabelForLocator(page.getByText("Succeeded")); + const options = page.getByTestId("tree-select-options"); + await expect(options).toBeVisible(); + await clickLabelForLocator(options.getByTestId("succeeded-checkbox")); await modal.getByTestId("base-task-status-filter").click(); await expect(modal.getByTestId("confirmation-message")).toContainText( "Are you sure you want to restart the 1 selected tasks?", ); await modal.getByTestId("base-task-status-filter").click(); - await clickLabelForLocator(page.getByText("Succeeded")); + await clickLabelForLocator(options.getByTestId("succeeded-checkbox")); await modal.getByTestId("base-task-status-filter").click(); }); diff --git a/apps/spruce/playwright/tests/version/task_filters.spec.ts b/apps/spruce/playwright/tests/version/task_filters.spec.ts index 070407b5e0..add01a7b52 100644 --- a/apps/spruce/playwright/tests/version/task_filters.spec.ts +++ b/apps/spruce/playwright/tests/version/task_filters.spec.ts @@ -128,19 +128,12 @@ test.describe("Tasks filters", () => { test("Clicking on a status filter filters the tasks to only those statuses", async ({ authenticatedPage: page, }) => { - await page - .getByTestId("tree-select-options") - .getByText("Failed") - .first() - .click(); + const options = page.getByTestId("tree-select-options"); + await clickLabelForLocator(options.getByTestId("failed-checkbox")); await expect(page).toHaveURL(/statuses=failed/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("2"); - - await page - .getByTestId("tree-select-options") - .getByText("Succeeded") - .click(); + await clickLabelForLocator(options.getByTestId("succeeded-checkbox")); await expect(page).toHaveURL( /statuses=failed-umbrella,failed,known-issue,success/, ); @@ -160,7 +153,7 @@ test.describe("Tasks filters", () => { "Dispatched", "Blocked", ]; - await clickLabelForLocator(page.getByText("All")); + await clickLabelForLocator(page.getByTestId("all-checkbox")); for (const label of taskStatuses) { const checkbox = page.getByRole("checkbox", { name: label }).first(); await expect(checkbox).toBeChecked(); @@ -168,7 +161,7 @@ test.describe("Tasks filters", () => { await expect(page).toHaveURL(/statuses=all/); await waitForTaskTable(page); - await clickLabelForLocator(page.getByText("All")); + await clickLabelForLocator(page.getByTestId("all-checkbox")); for (const label of taskStatuses) { const checkbox = page.getByRole("checkbox", { name: label }).first(); await expect(checkbox).not.toBeChecked(); @@ -190,12 +183,12 @@ test.describe("Tasks filters", () => { test("Clicking on a base status filter filters the tasks to only those base statuses", async ({ authenticatedPage: page, }) => { - await clickLabelForLocator(page.getByText("Succeeded")); + await clickLabelForLocator(page.getByTestId("succeeded-checkbox")); await expect(page).toHaveURL(/baseStatuses=success/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("44"); - await clickLabelForLocator(page.getByText("Succeeded")); + await clickLabelForLocator(page.getByTestId("succeeded-checkbox")); await expect(page).not.toHaveURL(/baseStatuses/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("47"); @@ -205,14 +198,14 @@ test.describe("Tasks filters", () => { authenticatedPage: page, }) => { const taskStatuses = ["All", "Succeeded", "Running"]; - await clickLabelForLocator(page.getByText("All")); + await clickLabelForLocator(page.getByTestId("all-checkbox")); for (const label of taskStatuses) { await expect(page.getByRole("checkbox", { name: label })).toBeChecked(); } await expect(page).toHaveURL(/baseStatuses=all/); await waitForTaskTable(page); - await clickLabelForLocator(page.getByText("All")); + await clickLabelForLocator(page.getByTestId("all-checkbox")); for (const label of taskStatuses) { await expect( page.getByRole("checkbox", { name: label }), diff --git a/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/index.tsx b/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/index.tsx index 9bde86ab28..80a93f09f6 100644 --- a/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/index.tsx +++ b/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/index.tsx @@ -256,7 +256,7 @@ const ConfigureTasks: React.FC = ({ key={name} aria-label={name} checked={isTaskCheckboxChecked(state)} - data-cy="task-checkbox" + data-cy={`${name}-task-checkbox`} indeterminate={isTaskCheckboxIndeterminate(state)} label={name} onChange={onClickCheckbox(name)} @@ -272,7 +272,7 @@ const ConfigureTasks: React.FC = ({ = ({ 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..b388513e9d 100644 --- a/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/VirtualWorkstationTab/getFormSchema.ts +++ b/apps/spruce/src/pages/projectAndRepoSettings/shared/tabs/VirtualWorkstationTab/getFormSchema.ts @@ -96,6 +96,7 @@ export const getFormSchema = ( items: { "ui:ObjectFieldTemplate": CommandRow, command: { + "ui:data-cy": "repo-command-input", "ui:widget": widgets.TextareaWidget, }, }, diff --git a/packages/lib/src/components/TreeSelect/TreeSelect.tsx b/packages/lib/src/components/TreeSelect/TreeSelect.tsx index 56b2dc19d9..b5952dc541 100644 --- a/packages/lib/src/components/TreeSelect/TreeSelect.tsx +++ b/packages/lib/src/components/TreeSelect/TreeSelect.tsx @@ -123,13 +123,14 @@ const renderCheckboxesHelper = ({ // push parent const onChangeFn = (): void => handleOnChange({ state, value: data.value, onChange, tData }); + const parentDataCy = data.title.replace(" ", "-").toLowerCase(); rows.push( @@ -140,6 +141,7 @@ const renderCheckboxesHelper = ({ data.children.forEach((child) => { const onChangeChildFn = (): void => handleOnChange({ state, value: child.value, onChange, tData }); + const childDataCy = child.title.replace(" ", "-").toLowerCase(); rows.push( From b6ac1583b374bba6da05b0ed24581c41f46e7418 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 17:54:47 -0400 Subject: [PATCH 15/32] fix: failing tests --- .../tests/patch/configure_patch.spec.ts | 41 +++++++++---------- .../playwright/tests/waterfall/menu.spec.ts | 28 ++++++------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/apps/spruce/playwright/tests/patch/configure_patch.spec.ts b/apps/spruce/playwright/tests/patch/configure_patch.spec.ts index 39637d523e..b113d01c96 100644 --- a/apps/spruce/playwright/tests/patch/configure_patch.spec.ts +++ b/apps/spruce/playwright/tests/patch/configure_patch.spec.ts @@ -5,6 +5,9 @@ const unactivatedPatchId = "5e6bb9e23066155a993e0f1a"; const patchWithDisplayTasks = "5e6bb9e23066155a993e0f1b"; const activatedPatchId = "5e4ff3abe3c3317e352062e4"; +const allCheckboxes = '[data-cy$="task-checkbox"]'; +const aliasCheckboxes = '[data-cy$="alias-checkbox"]'; + test.describe("Configure Patch Page", () => { test.describe("Initial state reflects patch data", () => { test("First build variant in list is selected by default", async ({ @@ -179,7 +182,7 @@ test.describe("Configure Patch Page", () => { `${count} tasks across 0 build variants`, ); - const taskCheckboxes = page.getByTestId("task-checkbox"); + const taskCheckboxes = page.locator(allCheckboxes); await expect(taskCheckboxes).toHaveCount(7); for (const checkbox of await taskCheckboxes.all()) { @@ -206,7 +209,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("Windows") .click(); - const taskCheckboxes = page.getByTestId("task-checkbox"); + const taskCheckboxes = page.locator(allCheckboxes); await expect(taskCheckboxes).toHaveCount(7); for (const checkbox of await taskCheckboxes.all()) { await clickLabelForLocator(checkbox); @@ -239,12 +242,12 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("Ubuntu 16.04", { exact: true }) .click(); - await expect(page.getByTestId("task-checkbox")).toHaveCount(45); + await expect(page.locator(allCheckboxes)).toHaveCount(45); await expect( page.getByTestId("selected-task-disclaimer"), ).toContainText("0 tasks across 0 build variants"); await page.getByTestId("task-filter-input").fill("^dist"); - await expect(page.getByTestId("task-checkbox")).toHaveCount(2); + await expect(page.locator(allCheckboxes)).toHaveCount(2); await page.getByText("Select all tasks in view").click(); await expect( page.getByTestId("selected-task-disclaimer"), @@ -265,12 +268,12 @@ test.describe("Configure Patch Page", () => { .getByText("Ubuntu 16.04 (Docker)") .click(); await page.keyboard.up("Meta"); - await expect(page.getByTestId("task-checkbox")).toHaveCount(46); + await expect(page.locator(allCheckboxes)).toHaveCount(46); await expect( page.getByTestId("selected-task-disclaimer"), ).toContainText("0 tasks across 0 build variants"); await page.getByTestId("task-filter-input").fill("dist"); - await expect(page.getByTestId("task-checkbox")).toHaveCount(2); + await expect(page.locator(allCheckboxes)).toHaveCount(2); await page.getByText("Select all tasks in view").click(); await expect( page.getByTestId("selected-task-disclaimer"), @@ -283,7 +286,7 @@ test.describe("Configure Patch Page", () => { test("Checking Select All should check/uncheck all task checkboxes", async ({ authenticatedPage: page, }) => { - const taskCheckboxes = page.getByTestId("task-checkbox"); + const taskCheckboxes = page.locator(allCheckboxes); await expect(taskCheckboxes).toHaveCount(1); await clickLabelForLocator(page.getByTestId("select-all-checkbox")); @@ -300,7 +303,7 @@ test.describe("Configure Patch Page", () => { test("Checking all task checkboxes should check the Select All checkbox", async ({ authenticatedPage: page, }) => { - const taskCheckboxes = page.getByTestId("task-checkbox"); + const taskCheckboxes = page.locator(allCheckboxes); await expect(taskCheckboxes).toHaveCount(1); const selectAllCheckbox = page.getByTestId("select-all-checkbox"); await expect(selectAllCheckbox).not.toBeChecked(); @@ -313,7 +316,7 @@ test.describe("Configure Patch Page", () => { test("Unchecking all task checkboxes should uncheck the Select All checkbox", async ({ authenticatedPage: page, }) => { - const taskCheckboxes = page.getByTestId("task-checkbox"); + const taskCheckboxes = page.locator(allCheckboxes); await expect(taskCheckboxes).toHaveCount(1); for (const checkbox of await taskCheckboxes.all()) { await clickLabelForLocator(checkbox); @@ -334,7 +337,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - const taskCheckboxes = page.getByTestId("task-checkbox"); + const taskCheckboxes = page.locator(allCheckboxes); await expect(taskCheckboxes).toHaveCount(6); const firstCheckbox = taskCheckboxes.first(); await clickLabelForLocator(firstCheckbox); @@ -347,7 +350,7 @@ test.describe("Configure Patch Page", () => { test("Selecting all tasks on an an indeterminate state should check all the checkboxes", async ({ authenticatedPage: page, }) => { - const taskCheckboxes = page.getByTestId("task-checkbox"); + const taskCheckboxes = page.locator(allCheckboxes); await expect(taskCheckboxes).toHaveCount(1); const selectAllCheckbox = page.getByTestId("select-all-checkbox"); @@ -424,7 +427,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("RHEL 7.2 zLinux") .click(); - await expect(page.getByTestId("task-checkbox")).toHaveCount(8); + await expect(page.locator(allCheckboxes)).toHaveCount(8); }); test("Checking a deduplicated task between multiple build variants updates the task within each selected build variant", async ({ @@ -479,18 +482,14 @@ test.describe("Configure Patch Page", () => { .getByText("RHEL 7.2 zLinux") .click(); - const variant1TaskCount = await page - .locator('[data-cy$="task-checkbox"]') - .count(); + const variant1TaskCount = await page.locator(allCheckboxes).count(); await page .getByTestId("build-variant-list-item") .getByText("RHEL 7.1 POWER8") .click(); - const variant2TaskCount = await page - .locator('[data-cy$="task-checkbox"]') - .count(); + const variant2TaskCount = await page.locator(allCheckboxes).count(); await page.keyboard.down("Meta"); await page @@ -499,7 +498,7 @@ test.describe("Configure Patch Page", () => { .click(); await clickLabelForLocator(page.getByTestId("select-all-checkbox")); - const taskCheckboxes = page.locator('[data-cy$="task-checkbox"]'); + const taskCheckboxes = page.locator(allCheckboxes); const count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { await expect(taskCheckboxes.nth(i)).toBeChecked(); @@ -523,7 +522,7 @@ test.describe("Configure Patch Page", () => { .click(); await clickLabelForLocator(page.getByTestId("select-all-checkbox")); - const taskCheckboxes = page.locator('[data-cy$="task-checkbox"]'); + const taskCheckboxes = page.locator(allCheckboxes); let count = await taskCheckboxes.count(); for (let i = 0; i < count; i++) { await expect(taskCheckboxes.nth(i)).toBeChecked(); @@ -628,7 +627,7 @@ test.describe("Configure Patch Page", () => { .getByTestId("build-variant-list-item") .getByText("Windows") .click(); - await expect(page.getByTestId("alias-checkbox")).toHaveCount(1); + await expect(page.locator(aliasCheckboxes)).toHaveCount(1); await expect(page.locator("[data-selected=true]")).toHaveCount(2); }); diff --git a/apps/spruce/playwright/tests/waterfall/menu.spec.ts b/apps/spruce/playwright/tests/waterfall/menu.spec.ts index a3199e52e3..d1def6f13a 100644 --- a/apps/spruce/playwright/tests/waterfall/menu.spec.ts +++ b/apps/spruce/playwright/tests/waterfall/menu.spec.ts @@ -15,21 +15,18 @@ test.describe("Waterfall menu settings", () => { authenticatedPage: page, }) => { await page.getByTestId("waterfall-menu").click(); - await expect( - page.getByTestId("omit-inactive-builds-checkbox"), - ).not.toBeChecked(); - await clickLabelForLocator(page.getByText("Omit inactive builds")); - await expect( - page.getByTestId("omit-inactive-builds-checkbox"), - ).toBeChecked(); + + const omitInactiveBuildsCheckbox = page.getByTestId( + "omit-inactive-builds-checkbox", + ); + await expect(omitInactiveBuildsCheckbox).not.toBeChecked(); + await clickLabelForLocator(omitInactiveBuildsCheckbox); + await expect(omitInactiveBuildsCheckbox).toBeChecked(); await page.reload(); await page.getByTestId("waterfall-menu").click(); - await expect( - page.getByTestId("omit-inactive-builds-checkbox"), - ).toBeChecked(); - - await clickLabelForLocator(page.getByText("Omit inactive builds")); + await expect(omitInactiveBuildsCheckbox).toBeChecked(); + await clickLabelForLocator(omitInactiveBuildsCheckbox); }); test("omits inactive build variants when filter is applied and setting is enabled", async ({ @@ -40,7 +37,10 @@ test.describe("Waterfall menu settings", () => { await expect(page.getByTestId("build-variant-label")).toHaveCount(1); await page.getByTestId("waterfall-menu").click(); - await clickLabelForLocator(page.getByText("Omit inactive builds")); + const omitInactiveBuildsCheckbox = page.getByTestId( + "omit-inactive-builds-checkbox", + ); + await clickLabelForLocator(omitInactiveBuildsCheckbox); await page.locator("body").click(); await page.getByTestId("build-variant-filter-input").clear(); @@ -51,7 +51,7 @@ test.describe("Waterfall menu settings", () => { expect(count).toBeGreaterThanOrEqual(1); await page.getByTestId("waterfall-menu").click(); - await clickLabelForLocator(page.getByText("Omit inactive builds")); + await clickLabelForLocator(omitInactiveBuildsCheckbox); }); }); From b2ba9576f1647b5051025ed6a5dba7b9f0a24c29 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 17:57:25 -0400 Subject: [PATCH 16/32] fix: unit tests --- .../ConfigureTasks/ConfigureTasks.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx b/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx index 2f1b3d63a0..856dd1e0e3 100644 --- a/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx +++ b/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx @@ -22,7 +22,7 @@ describe("configureTasks", () => { totalSelectedTaskCount={0} />, ); - expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(2); + expect(screen.queryAllByDataCy(/task-checkbox$/)).toHaveLength(2); expect(screen.getByText("compile")).toBeInTheDocument(); expect(screen.getByText("test")).toBeInTheDocument(); }); @@ -46,7 +46,7 @@ describe("configureTasks", () => { totalSelectedTaskCount={0} />, ); - expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(4); + expect(screen.queryAllByDataCy(/task-checkbox$/)).toHaveLength(4); expect(screen.getByText("compile")).toBeInTheDocument(); expect(screen.getByText("test")).toBeInTheDocument(); expect(screen.getByText("e2e")).toBeInTheDocument(); @@ -72,7 +72,7 @@ describe("configureTasks", () => { totalSelectedTaskCount={3} />, ); - expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(3); + expect(screen.queryAllByDataCy(/task-checkbox$/)).toHaveLength(3); expect(screen.getByText("compile")).toBeInTheDocument(); expect(screen.getByText("test")).toBeInTheDocument(); expect(screen.getByText("lint")).toBeInTheDocument(); @@ -249,7 +249,7 @@ describe("configureTasks", () => { />, ); await user.type(screen.getByDataCy("task-filter-input"), "compile"); - expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(1); + expect(screen.queryAllByDataCy(/task-checkbox$/)).toHaveLength(1); const checkbox = screen.getByLabelText("compile"); expect(checkbox).toBeInTheDocument(); }); From fd010b216f7546bed6bc3cf42ca37174f31d4582 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 20:43:49 -0400 Subject: [PATCH 17/32] fix: reduce flakiness --- .../playwright/tests/distroSettings/task_section.spec.ts | 6 +++--- .../tests/projectSettings/defaulting_to_repo.spec.ts | 2 +- .../spruce/playwright/tests/projectSettings/plugins.spec.ts | 2 +- apps/spruce/playwright/tests/projectSettings/utils.ts | 1 + apps/spruce/playwright/tests/version/restart_modal.spec.ts | 1 + .../src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts | 3 +++ packages/playwright-config/helpers/index.ts | 1 + 7 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts index 9211fb5de4..81e0ad5e6b 100644 --- a/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts @@ -66,13 +66,13 @@ test.describe("task section", () => { await validateToast(page, "success", "Updated distro."); await page.reload(); - await expect(page.getByLabel("Task Finder Version")).toContainText( + await expect(page.getByTestId("finder-version-select")).toContainText( "Parallel", ); - await expect(page.getByLabel("Task Planner Version")).toContainText( + await expect(page.getByTestId("planner-version-select")).toContainText( "Tunable", ); - await expect(page.getByLabel("Task Dispatcher Version")).toContainText( + await expect(page.getByTestId("dispatcher-version-select")).toContainText( "Revised with dependencies", ); diff --git a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts index f721db5d6b..736a5abf97 100644 --- a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts @@ -185,10 +185,10 @@ test.describe("Project Settings when defaulting to repo", () => { true, ); - await expect(page.getByTestId("promote-vars-modal")).toHaveCount(0); 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(); diff --git a/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts b/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts index 1cc8447e65..f78218f781 100644 --- a/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts @@ -55,7 +55,7 @@ test.describe("Plugins", () => { displayName: "An external link 2", url: "https://example-2.com/{version_id}", }); - await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); + await expect(page.getByTestId("metadata-link-item")).toHaveCount(2); await save(page); await page.goto(patchPage); diff --git a/apps/spruce/playwright/tests/projectSettings/utils.ts b/apps/spruce/playwright/tests/projectSettings/utils.ts index 8e9ebc72ac..666c3dcc36 100644 --- a/apps/spruce/playwright/tests/projectSettings/utils.ts +++ b/apps/spruce/playwright/tests/projectSettings/utils.ts @@ -4,6 +4,7 @@ import { expect } from "../../fixtures"; export const save = async (page: Page) => { const saveButton = page.getByTestId("save-settings-button"); await saveButton.scrollIntoViewIfNeeded(); + await expect(saveButton).toHaveAttribute("aria-disabled", "false"); await saveButton.click(); }; diff --git a/apps/spruce/playwright/tests/version/restart_modal.spec.ts b/apps/spruce/playwright/tests/version/restart_modal.spec.ts index 930aaa661a..5262abcbd1 100644 --- a/apps/spruce/playwright/tests/version/restart_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/restart_modal.spec.ts @@ -99,6 +99,7 @@ test.describe("version/restart_modal", () => { "Are you sure you want to restart the 1 selected tasks?", ); await modal.getByTestId("base-task-status-filter").click(); + await expect(options).toBeVisible(); await clickLabelForLocator(options.getByTestId("succeeded-checkbox")); await modal.getByTestId("base-task-status-filter").click(); }); diff --git a/apps/spruce/src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts b/apps/spruce/src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts index a860b66d79..844bf13b22 100644 --- a/apps/spruce/src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts +++ b/apps/spruce/src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts @@ -172,12 +172,14 @@ export const getFormSchema = ({ "ui:ObjectFieldTemplate": CardFieldTemplate, version: { "ui:allowDeselect": false, + "ui:data-cy": "finder-version-select", }, }, plannerSettings: { "ui:ObjectFieldTemplate": CardFieldTemplate, version: { "ui:allowDeselect": false, + "ui:data-cy": "planner-version-select", }, tunableOptions: { "ui:field-data-cy": "tunable-options", @@ -216,6 +218,7 @@ export const getFormSchema = ({ "ui:ObjectFieldTemplate": CardFieldTemplate, version: { "ui:allowDeselect": false, + "ui:data-cy": "dispatcher-version-select", }, }, }, diff --git a/packages/playwright-config/helpers/index.ts b/packages/playwright-config/helpers/index.ts index 76092adbfa..1e16fac6ab 100644 --- a/packages/playwright-config/helpers/index.ts +++ b/packages/playwright-config/helpers/index.ts @@ -61,6 +61,7 @@ export const validateToast = async ( * @param locator - A locator pointing to the checkbox or radio element */ export const clickLabelForLocator = async (locator: Locator) => { + await expect(locator).toHaveAttribute("id", /.+/); // Wait for ID to be defined. const id = await locator.getAttribute("id"); await locator.page().locator(`label[for="${id}"]`).click(); }; From ee7310cc426967338cc260d8b66680904c5f7b19 Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 21:00:29 -0400 Subject: [PATCH 18/32] fix: merge conflicts --- apps/spruce/playwright/tests/projectSettings/utils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/spruce/playwright/tests/projectSettings/utils.ts b/apps/spruce/playwright/tests/projectSettings/utils.ts index 666c3dcc36..04796ebefb 100644 --- a/apps/spruce/playwright/tests/projectSettings/utils.ts +++ b/apps/spruce/playwright/tests/projectSettings/utils.ts @@ -6,6 +6,11 @@ export const save = async (page: Page) => { await saveButton.scrollIntoViewIfNeeded(); 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 ( From a50610310498196a8cbdaa1ce488a5d5e0772f8c Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 21:24:28 -0400 Subject: [PATCH 19/32] fix: reduce flakiness --- .../tests/adminSettings/restart_tasks.spec.ts | 4 ++-- .../tests/distroSettings/general_section.spec.ts | 6 +++--- .../tests/distroSettings/host_section.spec.ts | 14 +++++++++++--- .../tests/distroSettings/navigation.spec.ts | 7 ++++--- .../tests/distroSettings/new_distro.spec.ts | 8 ++++---- .../tests/distroSettings/project_section.spec.ts | 13 ++++++++----- .../tests/distroSettings/provider_section.spec.ts | 4 ++-- .../tests/preferences/notifications.spec.ts | 2 +- .../projectSettings/github_app_settings.spec.ts | 2 +- .../tests/projectSettings/plugins.spec.ts | 1 + .../playwright/tests/projectSettings/utils.ts | 2 +- apps/spruce/playwright/tests/spawn/volume.spec.ts | 2 +- .../distroSettings/tabs/HostTab/schemaFields.tsx | 1 + .../tabs/ProjectTab/getFormSchema.ts | 1 + 14 files changed, 41 insertions(+), 26 deletions(-) diff --git a/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts b/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts index ccb870127f..6083a3a5bd 100644 --- a/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts @@ -38,9 +38,9 @@ test.describe("restart tasks", () => { day: "01", }); - await expect(page.getByTestId("restart-tasks-button")).not.toHaveAttribute( + await expect(page.getByTestId("restart-tasks-button")).toHaveAttribute( "aria-disabled", - "true", + "false", ); await page.getByTestId("restart-tasks-button").click(); await expect(page.getByTestId("restart-tasks-modal")).toBeVisible(); diff --git a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts index c53a799cea..88869aaabd 100644 --- a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts @@ -18,7 +18,7 @@ test.describe("general section", () => { await expect( page.getByRole("button", { name: "Add alias" }), - ).not.toHaveAttribute("aria-disabled", "true"); + ).toHaveAttribute("aria-disabled", "false"); await page.getByRole("button", { name: "Add alias" }).click(); await page.getByLabel("Alias").fill("localhost-alias"); await page.getByLabel("Notes").fill("this is a note"); @@ -81,7 +81,7 @@ test.describe("general section", () => { page.getByRole("checkbox", { name: "Set distro as Single Task Distro", }), - ).not.toHaveAttribute("aria-disabled", "true"); + ).toHaveAttribute("aria-disabled", "false"); await clickLabelForLocator( page.getByLabel("Set distro as Single Task Distro"), ); @@ -107,7 +107,7 @@ test.describe("general section", () => { page.getByRole("checkbox", { name: "Set distro as Single Task Distro", }), - ).not.toHaveAttribute("aria-disabled", "true"); + ).toHaveAttribute("aria-disabled", "false"); await clickLabelForLocator( page.getByLabel("Set distro as Single Task Distro"), ); diff --git a/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts index be983ab31e..168ddb41da 100644 --- a/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts @@ -23,7 +23,11 @@ test.describe("host section", () => { test("shows an error when selecting an incompatible host communication method", async ({ authenticatedPage: page, }) => { - await selectOption(page, "Host Communication Method", "RPC"); + await selectOption( + page, + { testId: "communication-method-select" }, + "RPC", + ); await expect( page.getByText( "Legacy and non-legacy bootstrapping and communication are incompatible.", @@ -63,7 +67,7 @@ test.describe("host section", () => { test("updates mountpoints", async ({ authenticatedPage: page }) => { await expect( page.getByRole("button", { name: "Add mountpoint" }), - ).not.toHaveAttribute("aria-disabled", "true"); + ).toHaveAttribute("aria-disabled", "false"); await page.getByRole("button", { name: "Add mountpoint" }).click(); await page.getByLabel("Mountpoint").fill("/data"); @@ -85,7 +89,11 @@ test.describe("host section", () => { { testId: "bootstrap-method-select" }, "User Data", ); - await selectOption(page, "Host Communication Method", "RPC"); + await selectOption( + page, + { testId: "communication-method-select" }, + "RPC", + ); }); test("shows Windows-only fields when the architecture is updated", async ({ diff --git a/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts b/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts index 1fb326b623..2e8288363f 100644 --- a/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts @@ -51,9 +51,10 @@ test.describe("distroSettings/navigation", () => { authenticatedPage: page, }) => { await page.getByLabel("Notes").fill("my note"); - await expect( - page.getByTestId("save-settings-button"), - ).not.toHaveAttribute("aria-disabled", "true"); + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( + "aria-disabled", + "false", + ); await page.getByTestId("waterfall-link").click(); await expect( page.getByTestId("navigation-warning-modal"), diff --git a/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts b/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts index d3bd468c59..27eb136bc8 100644 --- a/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts @@ -27,9 +27,9 @@ test.describe("Creating a new distro", () => { ); await expect(page).toHaveURL(`/distro/${newDistroId}/settings/general`); - await expect(page.getByTestId("delete-distro-button")).not.toHaveAttribute( + await expect(page.getByTestId("delete-distro-button")).toHaveAttribute( "aria-disabled", - "true", + "false", ); await page.getByTestId("delete-distro-button").click(); await expect(page.getByTestId("delete-distro-modal")).toBeVisible(); @@ -70,9 +70,9 @@ test.describe("Copying a distro", () => { ); await expect(page).toHaveURL(`/distro/${copyDistroId}/settings/general`); - await expect(page.getByTestId("delete-distro-button")).not.toHaveAttribute( + await expect(page.getByTestId("delete-distro-button")).toHaveAttribute( "aria-disabled", - "true", + "false", ); await page.getByTestId("delete-distro-button").click(); await expect(page.getByTestId("delete-distro-modal")).toBeVisible(); diff --git a/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts index 00fd0c1f9a..0086d330de 100644 --- a/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts @@ -17,10 +17,13 @@ test.describe("project section", () => { await expect( page.getByRole("button", { name: "Add expansion" }), - ).not.toHaveAttribute("aria-disabled", "true"); + ).toHaveAttribute("aria-disabled", "false"); await page.getByRole("button", { name: "Add expansion" }).click(); - await page.getByLabel("Key").fill("key-name"); - await page.getByLabel("Value").fill("my-value"); + + const newExpansion = page.getByTestId("expansions-list-item"); + await newExpansion.getByLabel("Key").fill("key-name"); + await newExpansion.getByLabel("Value").fill("my-value"); + await page.getByRole("button", { name: "Add project" }).click(); await page.getByLabel("Project ID").fill("spruce"); @@ -28,8 +31,8 @@ test.describe("project section", () => { await validateToast(page, "success", "Updated distro."); await page.reload(); - await expect(page.getByLabel("Key")).toHaveValue("key-name"); - await expect(page.getByLabel("Value")).toHaveValue("my-value"); + await expect(newExpansion.getByLabel("Key")).toHaveValue("key-name"); + await expect(newExpansion.getByLabel("Value")).toHaveValue("my-value"); await expect(page.getByLabel("Project ID")).toHaveValue("spruce"); await page.getByTestId("delete-item-button").first().click(); diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts index 5f6d513a23..02541a2cbf 100644 --- a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts @@ -258,9 +258,9 @@ test.describe("provider section", () => { await selectOption(page, "Region", "us-west-1"); await page.getByLabel("EC2 AMI ID").clear(); await page.getByLabel("EC2 AMI ID").fill("ami-1234560"); - await expect(page.getByLabel("SSH Key Name")).not.toHaveAttribute( + await expect(page.getByLabel("SSH Key Name")).toHaveAttribute( "aria-disabled", - "true", + "false", ); await page.getByLabel("SSH Key Name").clear(); await page.getByLabel("SSH Key Name").fill("my ssh key"); diff --git a/apps/spruce/playwright/tests/preferences/notifications.spec.ts b/apps/spruce/playwright/tests/preferences/notifications.spec.ts index f263deb627..2bde6d02e6 100644 --- a/apps/spruce/playwright/tests/preferences/notifications.spec.ts +++ b/apps/spruce/playwright/tests/preferences/notifications.spec.ts @@ -16,7 +16,7 @@ test.describe("preferences/notifications", () => { await page.getByTestId("slack-member-id-field").fill("12345"); await expect( page.getByTestId("save-profile-changes-button"), - ).not.toHaveAttribute("aria-disabled", "true"); + ).toHaveAttribute("aria-disabled", "false"); }); test("saving changes to a field should work", async ({ diff --git a/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts index 2bf59f3544..e053895f37 100644 --- a/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts @@ -49,7 +49,7 @@ test.describe("GitHub app settings", () => { page .getByTestId("replace-github-credentials-modal") .getByRole("button", { name: "Replace" }), - ).not.toHaveAttribute("aria-disabled", "true"); + ).toHaveAttribute("aria-disabled", "false"); await page .getByTestId("replace-github-credentials-modal") diff --git a/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts b/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts index f78218f781..6fcf1f0656 100644 --- a/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts @@ -59,6 +59,7 @@ test.describe("Plugins", () => { await save(page); await page.goto(patchPage); + 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", diff --git a/apps/spruce/playwright/tests/projectSettings/utils.ts b/apps/spruce/playwright/tests/projectSettings/utils.ts index 04796ebefb..716c2c46fb 100644 --- a/apps/spruce/playwright/tests/projectSettings/utils.ts +++ b/apps/spruce/playwright/tests/projectSettings/utils.ts @@ -19,7 +19,7 @@ export const expectSaveButtonEnabled = async ( ) => { const saveButton = page.getByTestId("save-settings-button"); if (isEnabled) { - await expect(saveButton).not.toHaveAttribute("aria-disabled", "true"); + await expect(saveButton).toHaveAttribute("aria-disabled", "false"); } else { await expect(saveButton).toHaveAttribute("aria-disabled", "true"); } diff --git a/apps/spruce/playwright/tests/spawn/volume.spec.ts b/apps/spruce/playwright/tests/spawn/volume.spec.ts index 8149b3ec11..6043862f44 100644 --- a/apps/spruce/playwright/tests/spawn/volume.spec.ts +++ b/apps/spruce/playwright/tests/spawn/volume.spec.ts @@ -147,7 +147,7 @@ test.describe("Spawn volume page", () => { await clickLabelForLocator(confirmCheckbox); - await expect(yesButton).not.toHaveAttribute("aria-disabled", "true"); + await expect(yesButton).toHaveAttribute("aria-disabled", "false"); await yesButton.click(); await validateToast(page, "success", "Successfully deleted the volume."); diff --git a/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx b/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx index b660ddba77..fc2463b796 100644 --- a/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx +++ b/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx @@ -50,6 +50,7 @@ const communicationMethod = { oneOf: enumSelect(communicationMethodToCopy), }, uiSchema: { + "ui:data-cy": "communication-method-select", "ui:allowDeselect": false, }, }; diff --git a/apps/spruce/src/pages/distroSettings/tabs/ProjectTab/getFormSchema.ts b/apps/spruce/src/pages/distroSettings/tabs/ProjectTab/getFormSchema.ts index 296329f5ba..ed806f7b26 100644 --- a/apps/spruce/src/pages/distroSettings/tabs/ProjectTab/getFormSchema.ts +++ b/apps/spruce/src/pages/distroSettings/tabs/ProjectTab/getFormSchema.ts @@ -49,6 +49,7 @@ export const getFormSchema = (): ReturnType => ({ "ui:orderable": false, items: { "ui:ObjectFieldTemplate": FieldRow, + "ui:data-cy": "expansions-list-item", "ui:label": false, }, }, From fa90c31f260805bdc3907ed3fbba0507e3569fca Mon Sep 17 00:00:00 2001 From: minnakt Date: Mon, 27 Apr 2026 22:32:26 -0400 Subject: [PATCH 20/32] fix: attempt to resolve loading issue --- apps/spruce/playwright/helpers/index.ts | 1 + packages/playwright-config/helpers/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 2c73ea1239..a46c1ab1ff 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -20,6 +20,7 @@ export async function selectOption( : page.getByTestId(label.testId); await input.scrollIntoViewIfNeeded(); await expect(input).toBeEnabled(); + await expect(input).not.toHaveAttribute("id", /undefined/); await input.click(); const listbox = page.locator('[role="listbox"]'); diff --git a/packages/playwright-config/helpers/index.ts b/packages/playwright-config/helpers/index.ts index 1e16fac6ab..d72abed91e 100644 --- a/packages/playwright-config/helpers/index.ts +++ b/packages/playwright-config/helpers/index.ts @@ -61,7 +61,7 @@ export const validateToast = async ( * @param locator - A locator pointing to the checkbox or radio element */ export const clickLabelForLocator = async (locator: Locator) => { - await expect(locator).toHaveAttribute("id", /.+/); // Wait for ID to be defined. + await expect(locator).not.toHaveAttribute("id", /undefined/); const id = await locator.getAttribute("id"); await locator.page().locator(`label[for="${id}"]`).click(); }; From 86b6c18f1d43640be3cb8173558e7d81bdda6283 Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 28 Apr 2026 17:05:51 -0400 Subject: [PATCH 21/32] fix: merge conflicts and probably break a million things --- .../projectSettings/repo_settings.ts | 300 ------------------ apps/spruce/playwright/helpers/index.ts | 1 + .../adminSettings/authentication.spec.ts | 20 +- .../background_processing.spec.ts | 14 +- .../tests/adminSettings/other.spec.ts | 9 +- .../tests/adminSettings/runners.spec.ts | 12 +- .../tests/adminSettings/save_function.spec.ts | 9 +- .../tests/adminSettings/service_flags.spec.ts | 10 +- .../distroSettings/general_section.spec.ts | 31 +- .../distroSettings/provider_section.spec.ts | 53 ++-- .../playwright/tests/distroSettings/utils.ts | 4 +- .../projectSettings/commit_checks.spec.ts | 93 ++++++ .../tests/projectSettings/commit_checks.ts | 65 ---- .../defaulting_to_repo.spec.ts | 83 ++--- .../not_defaulting_to_repo.spec.ts | 8 +- .../projectSettings/repo_settings.spec.ts | 34 +- .../projectSettings/stepback_bisect.spec.ts | 26 +- .../playwright/tests/spawn/host.spec.ts | 20 +- .../playwright/tests/spawn/volume.spec.ts | 9 +- .../tests/version/restart_modal.spec.ts | 15 +- .../tests/version/task_filters.spec.ts | 27 +- .../tests/version/task_table.spec.ts | 53 ++-- 22 files changed, 323 insertions(+), 573 deletions(-) delete mode 100644 apps/spruce/cypress/integration/projectSettings/repo_settings.ts create mode 100644 apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/commit_checks.ts 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/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 192402769e..69aead1309 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -166,6 +166,7 @@ export { login, logout, clickCheckbox, + clickRadio, mockGraphQLResponse, hasOperationName, } from "@evg-ui/playwright-config/helpers"; diff --git a/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts b/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts index 18df2a84b0..34f9b7f39b 100644 --- a/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickCheckbox, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("authentication", () => { @@ -18,8 +18,10 @@ test.describe("authentication", () => { await page.getByTestId("navitem-admin-global-config").click(); // Global Config section. - const allowServiceUsersCheckbox = page.getByLabel("Allow Service Users"); - await clickLabelForLocator(allowServiceUsersCheckbox); + const allowServiceUsersCheckbox = page.getByRole("checkbox", { + name: "Allow Service Users", + }); + await clickCheckbox(allowServiceUsersCheckbox); await page.getByLabel("Background Reauthentication (Mins)").clear(); await page.getByLabel("Background Reauthentication (Mins)").fill("120"); @@ -73,15 +75,19 @@ test.describe("authentication", () => { // Multi read-write section. await page.getByTestId("multi-read-write").click(); const multiReadWriteOptions = page.getByTestId("multi-read-write-options"); - const oktaCheckbox = multiReadWriteOptions.getByLabel("Okta"); - await clickLabelForLocator(oktaCheckbox); + const oktaCheckbox = multiReadWriteOptions.getByRole("checkbox", { + name: "Okta", + }); + await clickCheckbox(oktaCheckbox); await page.getByTestId("multi-read-write").click(); // Multi read-only section. await page.getByTestId("multi-read-only").click(); const multiReadOnlyOptions = page.getByTestId("multi-read-only-options"); - const naiveCheckbox = multiReadOnlyOptions.getByLabel("Naive"); - await clickLabelForLocator(naiveCheckbox); + const naiveCheckbox = multiReadOnlyOptions.getByRole("checkbox", { + name: "Naive", + }); + await clickCheckbox(naiveCheckbox); await page.getByTestId("multi-read-only").click(); // Kanopy section. diff --git a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts index 2fff5b789b..ad5b40daff 100644 --- a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts @@ -1,9 +1,5 @@ import { test, expect } from "../../fixtures"; -import { - clickLabelForLocator, - selectOption, - validateToast, -} from "../../helpers"; +import { clickCheckbox, selectOption, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("background processing", () => { @@ -33,10 +29,10 @@ test.describe("background processing", () => { await page.getByLabel("Redact Keys").fill("aNewRedactedKey"); await page.getByLabel("Redact Keys").press("Enter"); - const asyncBufferCheckbox = page.getByLabel( - "Use asynchronous buffered logger", - ); - await clickLabelForLocator(asyncBufferCheckbox); + const asyncBufferCheckbox = page.getByRole("checkbox", { + name: "Use asynchronous buffered logger", + }); + await clickCheckbox(asyncBufferCheckbox); // Notification Rate Limits section. await page.getByLabel("Time Interval (secs)", { exact: true }).clear(); diff --git a/apps/spruce/playwright/tests/adminSettings/other.spec.ts b/apps/spruce/playwright/tests/adminSettings/other.spec.ts index 7af3f6e0d8..2092af7a7f 100644 --- a/apps/spruce/playwright/tests/adminSettings/other.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/other.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickCheckbox, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("other", () => { @@ -330,9 +330,10 @@ test.describe("other", () => { ); const tracerConfiguration = page.getByTestId("tracer-configuration"); - const tracerEnabledCheckbox = - tracerConfiguration.getByLabel("Enable tracer"); - await clickLabelForLocator(tracerEnabledCheckbox); + const tracerEnabledCheckbox = tracerConfiguration.getByRole("checkbox", { + name: "Enable tracer", + }); + await clickCheckbox(tracerEnabledCheckbox); await page.getByLabel("Collector Endpoint").clear(); await page diff --git a/apps/spruce/playwright/tests/adminSettings/runners.spec.ts b/apps/spruce/playwright/tests/adminSettings/runners.spec.ts index 9ac5eeee1a..dce13ba19c 100644 --- a/apps/spruce/playwright/tests/adminSettings/runners.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/runners.spec.ts @@ -1,9 +1,5 @@ import { test, expect } from "../../fixtures"; -import { - clickLabelForLocator, - selectOption, - validateToast, -} from "../../helpers"; +import { clickCheckbox, selectOption, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("runners", () => { @@ -35,8 +31,10 @@ test.describe("runners", () => { await page.getByLabel("Default Future Host Fraction").clear(); await page.getByLabel("Default Future Host Fraction").fill("0.6"); - const groupVersionsCheckbox = page.getByLabel("Group Versions"); - await clickLabelForLocator(groupVersionsCheckbox); + const groupVersionsCheckbox = page.getByRole("checkbox", { + name: "Group Versions", + }); + await clickCheckbox(groupVersionsCheckbox); // Repotracker section. await page.getByLabel("New Revisions to Fetch").clear(); diff --git a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts index a0dbb5bdf9..a20bd49bf6 100644 --- a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickCheckbox, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("admin settings save properly", () => { @@ -47,9 +47,10 @@ test.describe("admin settings save properly", () => { await page.getByLabel("Permanently Exempt Hosts").press("Enter"); const tracerConfiguration = page.getByTestId("tracer-configuration"); - const tracerEnabledCheckbox = - tracerConfiguration.getByLabel("Enable tracer"); - await clickLabelForLocator(tracerEnabledCheckbox); + const tracerEnabledCheckbox = tracerConfiguration.getByRole("checkbox", { + name: "Enable tracer", + }); + await clickCheckbox(tracerEnabledCheckbox); await tracerConfiguration.getByLabel("Collector Endpoint").clear(); await tracerConfiguration .getByLabel("Collector Endpoint") diff --git a/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts b/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts index 388b403682..a478abe299 100644 --- a/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts +++ b/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickCheckbox, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("service flags", () => { @@ -27,7 +27,7 @@ test.describe("service flags", () => { const checkboxId = await uncheckedCheckbox.getAttribute("id"); const checkbox = page.locator(`#${checkboxId}`); - await clickLabelForLocator(checkbox); + await clickCheckbox(checkbox); // Verify checked count increased. await expect(checkedCheckboxes).toHaveCount(initialCheckedCount + 1); @@ -38,7 +38,7 @@ test.describe("service flags", () => { ); // Uncheck it again. - await clickLabelForLocator(checkbox); + await clickCheckbox(checkbox); await expect(checkedCheckboxes).toHaveCount(initialCheckedCount); await expect(page.getByTestId("save-settings-button")).toHaveAttribute( "aria-disabled", @@ -46,7 +46,7 @@ test.describe("service flags", () => { ); // Re-check and save. - await clickLabelForLocator(checkbox); + await clickCheckbox(checkbox); await save(page); await validateToast(page, "success", "Service flags saved successfully"); await page.reload(); @@ -55,7 +55,7 @@ test.describe("service flags", () => { await expect(checkedCheckboxes).toHaveCount(initialCheckedCount + 1); // Restore original state. - await clickLabelForLocator(checkbox); + await clickCheckbox(checkbox); await save(page); await validateToast(page, "success", "Service flags saved successfully"); }); diff --git a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts index 88869aaabd..9d22456954 100644 --- a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickCheckbox, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("general section", () => { @@ -23,10 +23,14 @@ test.describe("general section", () => { await page.getByLabel("Alias").fill("localhost-alias"); await page.getByLabel("Notes").fill("this is a note"); await page.getByLabel("Warnings").fill("this is a warning"); - await clickLabelForLocator( - page.getByLabel("Disable shallow clone for this distro"), - ); - await clickLabelForLocator(page.getByLabel("Admin only")); + const disableShallowCloneCheckbox = page.getByRole("checkbox", { + name: "Disable shallow clone for this distro", + }); + await clickCheckbox(disableShallowCloneCheckbox); + const adminOnlyCheckbox = page.getByRole("checkbox", { + name: "Admin only", + }); + await clickCheckbox(adminOnlyCheckbox); await save(page); await validateToast(page, "success", "Updated distro."); @@ -47,10 +51,8 @@ test.describe("general section", () => { await page.getByTestId("delete-item-button").click(); await page.getByLabel("Notes").clear(); await page.getByLabel("Warnings").clear(); - await clickLabelForLocator( - page.getByLabel("Disable shallow clone for this distro"), - ); - await clickLabelForLocator(page.getByLabel("Admin only")); + await clickCheckbox(disableShallowCloneCheckbox); + await clickCheckbox(adminOnlyCheckbox); await save(page); await validateToast(page, "success", "Updated distro."); }); @@ -82,9 +84,10 @@ test.describe("general section", () => { name: "Set distro as Single Task Distro", }), ).toHaveAttribute("aria-disabled", "false"); - await clickLabelForLocator( - page.getByLabel("Set distro as Single Task Distro"), - ); + const singleTaskCheckbox = page.getByRole("checkbox", { + name: "Set distro as Single Task Distro", + }); + await clickCheckbox(singleTaskCheckbox); await expect(page.getByTestId("single-task-banner")).toContainText( "This Distro will be converted to a Single Task Distro once saved. Please review before confirming.", ); @@ -108,9 +111,7 @@ test.describe("general section", () => { name: "Set distro as Single Task Distro", }), ).toHaveAttribute("aria-disabled", "false"); - await clickLabelForLocator( - page.getByLabel("Set distro as Single Task Distro"), - ); + await clickCheckbox(singleTaskCheckbox); await expect(page.getByTestId("single-task-banner")).toContainText( "This Distro will no longer be a Single Task Distro once saved. Please review before confirming.", ); diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts index 02541a2cbf..d348d64a55 100644 --- a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts @@ -1,9 +1,5 @@ import { test, expect } from "../../fixtures"; -import { - clickLabelForLocator, - selectOption, - validateToast, -} from "../../helpers"; +import { clickCheckbox, selectOption, validateToast } from "../../helpers"; import { save } from "./utils"; test.describe("provider section", () => { @@ -21,9 +17,10 @@ test.describe("provider section", () => { await expect(page.getByTestId("static-provider-settings")).toBeVisible(); await page.getByTestId("user-data-input").fill("my user data"); - await clickLabelForLocator( - page.getByLabel("Merge with existing user data"), + const mergeUserDataCheckbox = page.getByLabel( + "Merge with existing user data", ); + await clickCheckbox(mergeUserDataCheckbox); await page.getByRole("button", { name: "Add security group" }).click(); await page.getByLabel("Security Group ID").fill("sg-1234"); await page.getByRole("button", { name: "Add host" }).click(); @@ -32,9 +29,7 @@ test.describe("provider section", () => { await validateToast(page, "success", "Updated distro."); await page.getByTestId("user-data-input").clear(); - await clickLabelForLocator( - page.getByLabel("Merge with existing user data"), - ); + await clickCheckbox(mergeUserDataCheckbox); await page.getByTestId("delete-item-button").first().click(); await page.getByTestId("delete-item-button").first().click(); await save(page); @@ -75,9 +70,10 @@ test.describe("provider section", () => { await page.getByLabel("Username for Registries").fill("username"); await page.getByLabel("Password for Registries").fill("password"); await page.getByTestId("user-data-input").fill("my user data"); - await clickLabelForLocator( - page.getByLabel("Merge with existing user data"), + const mergeUserDataCheckbox = page.getByLabel( + "Merge with existing user data", ); + await clickCheckbox(mergeUserDataCheckbox); await save(page); await validateToast(page, "success", "Updated distro."); @@ -86,9 +82,7 @@ test.describe("provider section", () => { await page.getByLabel("Username for Registries").clear(); await page.getByLabel("Password for Registries").clear(); await page.getByTestId("user-data-input").clear(); - await clickLabelForLocator( - page.getByLabel("Merge with existing user data"), - ); + await clickCheckbox(mergeUserDataCheckbox); await save(page); await validateToast(page, "success", "Updated distro."); }); @@ -114,9 +108,10 @@ test.describe("provider section", () => { await expect(page.getByText("Default VPC Subnet ID")).toBeVisible(); await expect(page.getByText("VPC Subnet Prefix")).toBeVisible(); - await clickLabelForLocator( - page.getByLabel("Use security groups in an EC2 VPC"), - ); + const useVpcCheckbox = page.getByRole("checkbox", { + name: "Use security groups in an EC2 VPC", + }); + await clickCheckbox(useVpcCheckbox); await expect(page.getByTestId("use-vpc")).not.toBeChecked(); await expect(page.getByText("Default VPC Subnet ID")).toHaveCount(0); await expect(page.getByText("VPC Subnet Prefix")).toHaveCount(0); @@ -235,9 +230,10 @@ test.describe("provider section", () => { await expect(page.getByText("Default VPC Subnet ID")).toBeVisible(); await expect(page.getByText("VPC Subnet Prefix")).toBeVisible(); - await clickLabelForLocator( - page.getByLabel("Use security groups in an EC2 VPC"), - ); + const useVpcCheckbox = page.getByRole("checkbox", { + name: "Use security groups in an EC2 VPC", + }); + await clickCheckbox(useVpcCheckbox); await expect(page.getByText("Default VPC Subnet ID")).toHaveCount(0); await expect(page.getByText("VPC Subnet Prefix")).toHaveCount(0); }); @@ -267,9 +263,14 @@ test.describe("provider section", () => { await page .getByTestId("user-data-input") .fill(""); - await clickLabelForLocator( - page.getByLabel("Merge with existing user data"), - ); + + const mergeUserDataCheckbox = page.getByRole("checkbox", { + name: "Merge with existing user data", + }); + await clickCheckbox(mergeUserDataCheckbox); + await save(page); + await validateToast(page, "success", "Updated distro."); + await clickCheckbox(mergeUserDataCheckbox); await save(page); await validateToast(page, "success", "Updated distro."); @@ -279,9 +280,7 @@ test.describe("provider section", () => { await page.getByLabel("SSH Key Name").clear(); await page.getByLabel("SSH Key Name").fill("mci"); await page.getByTestId("user-data-input").clear(); - await clickLabelForLocator( - page.getByLabel("Merge with existing user data"), - ); + await clickCheckbox(mergeUserDataCheckbox); await save(page); await validateToast(page, "success", "Updated distro."); }); diff --git a/apps/spruce/playwright/tests/distroSettings/utils.ts b/apps/spruce/playwright/tests/distroSettings/utils.ts index c66dab7415..f3bd1f28db 100644 --- a/apps/spruce/playwright/tests/distroSettings/utils.ts +++ b/apps/spruce/playwright/tests/distroSettings/utils.ts @@ -1,5 +1,5 @@ import { Page } from "@playwright/test"; -import { clickLabelForLocator } from "@evg-ui/playwright-config/helpers"; +import { clickRadio } from "@evg-ui/playwright-config/helpers"; import { expect } from "../../fixtures"; type onSaveOptions = "NONE" | "DECOMMISSION" | "RESTART_JASPER" | "REPROVISION"; @@ -11,7 +11,7 @@ export const save = async (page: Page, onSaveValue?: onSaveOptions) => { if (onSaveValue) { const radio = page.locator(`input[value="${onSaveValue}"]`); - await clickLabelForLocator(radio); + await clickRadio(radio); } const modal = page.getByTestId("save-modal"); diff --git a/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts b/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts new file mode 100644 index 0000000000..e5dd81faf5 --- /dev/null +++ b/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts @@ -0,0 +1,93 @@ +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, + }) => { + await expect(page.getByTestId("disabled-webhook-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("checkbox", { + name: "Enabled", + }); + const githubChecksDisabledRadio = radioBox.getByRole("checkbox", { + 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("checkbox", { + 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/projectSettings/commit_checks.ts b/apps/spruce/playwright/tests/projectSettings/commit_checks.ts deleted file mode 100644 index 7e7fd91d90..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/commit_checks.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - getProjectSettingsRoute, - project, - ProjectSettingsTabRoutes, - saveButtonEnabled, -} from "./constants"; -import { clickSaveAndConfirmDiff } from "./utils"; - -describe("A project that has GitHub webhooks disabled", () => { - const origin = getProjectSettingsRoute( - "logkeeper", - ProjectSettingsTabRoutes.CommitChecks, - ); - beforeEach(() => { - cy.visit(origin); - saveButtonEnabled(false); - }); - - it("Commit Checks page shows a disabled webhooks banner when webhooks are disabled", () => { - cy.dataCy("disabled-webhook-banner") - .contains( - "GitHub features are disabled because the Evergreen GitHub App is not", - ) - .should("be.visible"); - }); - - 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 that has GitHub webhooks enabled", () => { - const origin = getProjectSettingsRoute( - project, - ProjectSettingsTabRoutes.CommitChecks, - ); - beforeEach(() => { - cy.visit(origin); - saveButtonEnabled(false); - }); - - 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").children().first().click(); - cy.contains( - "A Commit Check Definition must be specified for this feature to run.", - ).as("errorBanner"); - cy.dataCy("error-banner").should("be.visible"); - cy.dataCy("github-checks-enabled-radio-box").children().last().click(); - cy.dataCy("error-banner").should("not.exist"); - }); - - it("Saves successfully when Commit Checks are enabled and a Commit Check Definition is provided", () => { - cy.dataCy("github-checks-enabled-radio-box").children().first().click(); - cy.contains("button", "Add Definition").click(); - cy.dataCy("variant-tags-input").first().type("vtag"); - cy.dataCy("task-tags-input").first().type("ttag"); - - cy.dataCy("error-banner").should("not.exist"); - clickSaveAndConfirmDiff(); - cy.validateToast("success", "Successfully updated project"); - }); -}); diff --git a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts index 736a5abf97..cb7fa7c036 100644 --- a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickCheckbox, clickRadio, validateToast } from "../../helpers"; import { getProjectSettingsRoute, getRepoSettingsRoute, @@ -158,8 +158,8 @@ test.describe("Project Settings when defaulting to repo", () => { await page .getByTestId("var-description-input") .fill("Description for variable a"); - const privateCheckbox = page.getByTestId("var-private-input"); - await clickLabelForLocator(privateCheckbox); + const privateCheckbox = page.getByRole("checkbox", { name: "Private" }); + await clickCheckbox(privateCheckbox); await page.getByTestId("add-button").click(); await page.getByTestId("var-name-input").first().fill("b"); @@ -192,7 +192,7 @@ test.describe("Project Settings when defaulting to repo", () => { const variableToPromoteCheckbox = page .getByTestId("promote-var-checkbox") .first(); - await clickLabelForLocator(variableToPromoteCheckbox); + await clickCheckbox(variableToPromoteCheckbox); await page.getByRole("button", { name: "Move 1 variable" }).click(); await validateToast( @@ -260,8 +260,8 @@ test.describe("Project Settings when defaulting to repo", () => { .scrollIntoViewIfNeeded(); const enabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .getByLabel("Enabled"); - await clickLabelForLocator(enabledRadio); + .getByRole("radio", { name: "Enabled" }); + await clickRadio(enabledRadio); await expect( page.getByTestId("warning-banner").filter({ hasText: @@ -301,16 +301,16 @@ test.describe("Project Settings when defaulting to repo", () => { }) => { const prDisabledRadio = page .getByTestId("pr-testing-enabled-radio-box") - .getByLabel("Disabled", { exact: true }); - await clickLabelForLocator(prDisabledRadio); + .getByRole("radio", { name: "Disabled" }); + await clickRadio(prDisabledRadio); const manualDisabledRadio = page .getByTestId("manual-pr-testing-enabled-radio-box") - .getByLabel("Disabled", { exact: true }); - await clickLabelForLocator(manualDisabledRadio); + .getByRole("radio", { name: "Disabled" }); + await clickRadio(manualDisabledRadio); const githubEnabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .getByLabel("Enabled", { exact: true }); - await clickLabelForLocator(githubEnabledRadio); + .getByRole("radio", { name: "Enabled" }); + await clickRadio(githubEnabledRadio); await save(page); await validateToast( page, @@ -382,13 +382,14 @@ test.describe("Project Settings when defaulting to repo", () => { test("Patch aliases added before defaulting to repo patch aliases are cleared", async ({ authenticatedPage: page, }) => { - const overrideRepoPatchAliasesRadio = page.getByLabel( - "Override Repo Patch Aliases", + const overrideRepoPatchAliasesRadio = page.getByRole("radio", { + name: "Override Repo Patch Aliases", + }); + await clickRadio(overrideRepoPatchAliasesRadio); + await expect(overrideRepoPatchAliasesRadio).toHaveAttribute( + "aria-checked", + "true", ); - await clickLabelForLocator(overrideRepoPatchAliasesRadio); - await expect( - page.getByLabel("Override Repo Patch Aliases"), - ).toHaveAttribute("aria-checked", "true"); await expectSaveButtonEnabled(page, false); await page @@ -413,15 +414,15 @@ test.describe("Project Settings when defaulting to repo", () => { await save(page); await validateToast(page, "success", "Successfully updated project"); - const defaultToRepoRadio = page.getByLabel( - "Default to Repo Patch Aliases", - ); - await clickLabelForLocator(defaultToRepoRadio); + 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 clickLabelForLocator(overrideRepoPatchAliasesRadio); + await clickRadio(overrideRepoPatchAliasesRadio); await expect(page.getByTestId("alias-row")).toHaveCount(0); }); }); @@ -432,15 +433,17 @@ test.describe("Project Settings when defaulting to repo", () => { }); test("Enable git clone", async ({ authenticatedPage: page }) => { - const githubEnabledRadio = page.getByLabel("Enabled"); - await clickLabelForLocator(githubEnabledRadio); - await expect(page.getByLabel("Enabled")).toBeChecked(); + 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.getByLabel("Default to repo (disabled)"); + const defaultToRepoRadio = page.getByRole("radio", { + name: "Default to repo (disabled)", + }); await expect(defaultToRepoRadio).toBeChecked(); await expect(page.getByTestId("command-row")).toHaveCount(0); @@ -466,10 +469,10 @@ test.describe("Project Settings when defaulting to repo", () => { await expect(repoCommandInput).toHaveValue("a repo command"); await expect(repoCommandInput).toHaveAttribute("aria-disabled", "true"); - const overrideRepoCommandsRadio = page.getByLabel( - "Override Repo Commands", - ); - await clickLabelForLocator(overrideRepoCommandsRadio); + const overrideRepoCommandsRadio = page.getByRole("radio", { + name: "Override Repo Commands", + }); + await clickRadio(overrideRepoCommandsRadio); await expect(page.getByTestId("command-row")).toHaveCount(0); await page.getByRole("button", { name: "Add Command" }).click(); @@ -488,27 +491,27 @@ test.describe("Project Settings when defaulting to repo", () => { "false", ); - const defaultToRepoCommandsRadio = page.getByLabel( - "Default to repo commands", - ); - await clickLabelForLocator(defaultToRepoCommandsRadio); + const defaultToRepoCommandsRadio = page.getByRole("radio", { + name: "Default to repo (disabled)", + }); + await clickRadio(defaultToRepoCommandsRadio); await expect(page.getByTestId("command-row")).toHaveCount(1); await expect(repoCommandInput).toHaveValue("a repo command"); await expect(repoCommandInput).toHaveAttribute("aria-disabled", "true"); await save(page); await validateToast(page, "success", "Successfully updated project"); - await clickLabelForLocator(overrideRepoCommandsRadio); + await clickRadio(overrideRepoCommandsRadio); await expect(page.getByTestId("command-row")).toHaveCount(0); }); test("Allows overriding without adding a command", async ({ authenticatedPage: page, }) => { - const overrideRepoCommandsRadio = page.getByLabel( - "Override Repo Commands", - ); - await clickLabelForLocator(overrideRepoCommandsRadio); + 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"); diff --git a/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts index 1ad9f9e372..b02b284be0 100644 --- a/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts @@ -4,7 +4,7 @@ import { validateDatePickerDate, clearDatePickerInput, typeDatePickerDate, - clickLabelForLocator, + clickCheckbox, } from "../../helpers"; import { getProjectSettingsRoute, project } from "./constants"; import { expectSaveButtonEnabled, save } from "./utils"; @@ -146,8 +146,10 @@ test.describe("Project Settings when not defaulting to repo", () => { await page.getByTestId("add-button").click(); await page.getByTestId("var-name-input").first().fill("admin_var"); await page.getByTestId("var-value-input").first().fill("admin_value"); - const adminOnlyCheckbox = page.getByTestId("var-admin-input"); - await clickLabelForLocator(adminOnlyCheckbox); + const adminOnlyCheckbox = page.getByRole("checkbox", { + name: "Admin Only", + }); + await clickCheckbox(adminOnlyCheckbox); await expect(page.getByTestId("var-admin-input")).toBeChecked(); await save(page); await validateToast(page, "success", "Successfully updated project"); diff --git a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts index 06c57c7869..dbbbaabee6 100644 --- a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickRadio, validateToast } from "../../helpers"; import { getProjectSettingsRoute, getRepoSettingsRoute, @@ -68,8 +68,8 @@ test.describe("Repo Settings", () => { }) => { const githubChecksEnabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .getByLabel("Enabled"); - await clickLabelForLocator(githubChecksEnabledRadio); + .getByRole("checkbox", { 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.", @@ -77,8 +77,8 @@ test.describe("Repo Settings", () => { await expect(errorBanner).toBeVisible(); const githubChecksDisabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .getByLabel("Disabled"); - await clickLabelForLocator(githubChecksDisabledRadio); + .getByRole("checkbox", { name: "Disabled" }); + await clickRadio(githubChecksDisabledRadio); await expect(errorBanner).toHaveCount(0); }); @@ -147,8 +147,8 @@ test.describe("Repo Settings", () => { const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .getByLabel("Enabled"); - await clickLabelForLocator(mergeQueueEnabledRadio); + .getByRole("checkbox", { name: "Enabled" }); + await clickRadio(mergeQueueEnabledRadio); await expect( page.getByText("Merge Queue Patch Definitions"), @@ -169,8 +169,8 @@ test.describe("Repo Settings", () => { }) => { const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .getByLabel("Enabled"); - await clickLabelForLocator(mergeQueueEnabledRadio); + .getByRole("checkbox", { name: "Enabled" }); + await clickRadio(mergeQueueEnabledRadio); await expect(page.getByTestId("cq-override-radio-box")).toHaveCount(0); }); @@ -179,8 +179,8 @@ test.describe("Repo Settings", () => { }) => { const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .getByLabel("Enabled"); - await clickLabelForLocator(mergeQueueEnabledRadio); + .getByRole("checkbox", { name: "Enabled" }); + await clickRadio(mergeQueueEnabledRadio); await page .getByRole("button", { name: "Add Patch Definition" }) .click(); @@ -298,15 +298,19 @@ test.describe("Repo Settings", () => { await page.getByTestId("task-regex-input").fill(".*"); const githubPRLabel = "Schedule in GitHub Pull Requests"; - const pullRequestCheckbox = page.getByLabel(githubPRLabel); + const pullRequestCheckbox = page.getByRole("checkbox", { + name: githubPRLabel, + }); await expect(pullRequestCheckbox).not.toBeChecked(); - await clickLabelForLocator(page.getByLabel(githubPRLabel)); + await clickRadio(pullRequestCheckbox); await expect(pullRequestCheckbox).toBeChecked(); const githubMQLabel = "Schedule in GitHub Merge Queue"; - const mergeQueueCheckbox = page.getByLabel(githubMQLabel); + const mergeQueueCheckbox = page.getByRole("checkbox", { + name: githubMQLabel, + }); await expect(mergeQueueCheckbox).not.toBeChecked(); - await clickLabelForLocator(page.getByLabel(githubMQLabel)); + await clickRadio(mergeQueueCheckbox); await expect(mergeQueueCheckbox).toBeChecked(); await save(page); diff --git a/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts b/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts index d12d62e4e8..64561e3b96 100644 --- a/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickRadio, validateToast } from "../../helpers"; import { getProjectSettingsRoute, project, @@ -28,18 +28,12 @@ test.describe("Stepback bisect setting", () => { }) => { const enableRadio = page .getByTestId("stepback-bisect-group") - .getByLabel("Enable"); - await clickLabelForLocator(enableRadio); + .getByRole("radio", { name: "Enabled" }); + await clickRadio(enableRadio); await save(page); await validateToast(page, "success", "Successfully updated project"); - await page.reload(); - - await expect( - page - .getByTestId("stepback-bisect-group") - .getByRole("radio", { name: "Enable" }), - ).toHaveAttribute("aria-checked", "true"); + await expect(enableRadio).toHaveAttribute("aria-checked", "true"); }); }); @@ -63,18 +57,12 @@ test.describe("Stepback bisect setting", () => { }) => { const enableRadio = page .getByTestId("stepback-bisect-group") - .getByLabel("Enabled"); - await clickLabelForLocator(enableRadio); + .getByRole("radio", { name: "Enabled" }); + await clickRadio(enableRadio); await save(page); await validateToast(page, "success", "Successfully updated project"); - await page.reload(); - - await expect( - page - .getByTestId("stepback-bisect-group") - .getByRole("radio", { name: "Enable" }), - ).toHaveAttribute("aria-checked", "true"); + await expect(enableRadio).toHaveAttribute("aria-checked", "true"); }); }); }); diff --git a/apps/spruce/playwright/tests/spawn/host.spec.ts b/apps/spruce/playwright/tests/spawn/host.spec.ts index 2bc5248f25..19617c9fb9 100644 --- a/apps/spruce/playwright/tests/spawn/host.spec.ts +++ b/apps/spruce/playwright/tests/spawn/host.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from "../../fixtures"; import { clearDatePickerInput, - clickLabelForLocator, + clickCheckbox, typeDatePickerDate, } from "../../helpers"; @@ -223,8 +223,10 @@ test.describe("Spawn Host page", () => { await expect(page.getByText(projectSetupCheckbox)).toBeVisible(); await expect(page.getByText(startHostsCheckbox)).toBeVisible(); - const loadDataCheckbox = page.getByTestId("load-data-checkbox"); - await clickLabelForLocator(loadDataCheckbox); + const loadDataCheckbox = page.getByRole("checkbox", { + name: "Load data for dist", + }); + await clickCheckbox(loadDataCheckbox); await expect(loadDataCheckbox).not.toBeChecked(); await expect(page.getByText(projectSetupCheckbox)).toHaveCount(0); await expect(page.getByText(startHostsCheckbox)).toHaveCount(0); @@ -307,18 +309,22 @@ test.describe("Spawn Host page", () => { await page.goto( `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, ); - const projectCheckbox = page.getByTestId("project-setup-script-checkbox"); - const setupCheckbox = page.getByTestId("setup-script-checkbox"); + const projectCheckbox = page.getByRole("checkbox", { + name: "Use project-specific setup script", + }); + const setupCheckbox = page.getByRole("checkbox", { + name: "Define setup script to run after host", + }); await expect(projectCheckbox).toHaveAttribute("aria-checked", "true"); await expect(projectCheckbox).toHaveAttribute("aria-disabled", "false"); await expect(setupCheckbox).toHaveAttribute("aria-disabled", "true"); - await clickLabelForLocator(projectCheckbox); + await clickCheckbox(projectCheckbox); await expect(projectCheckbox).toHaveAttribute("aria-disabled", "false"); await expect(setupCheckbox).toHaveAttribute("aria-disabled", "false"); - await clickLabelForLocator(setupCheckbox); + await clickCheckbox(setupCheckbox); await expect(projectCheckbox).toHaveAttribute("aria-disabled", "true"); await expect(setupCheckbox).toHaveAttribute("aria-disabled", "false"); }); diff --git a/apps/spruce/playwright/tests/spawn/volume.spec.ts b/apps/spruce/playwright/tests/spawn/volume.spec.ts index 6043862f44..639d98a3c7 100644 --- a/apps/spruce/playwright/tests/spawn/volume.spec.ts +++ b/apps/spruce/playwright/tests/spawn/volume.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../../fixtures"; import { - clickLabelForLocator, + clickCheckbox, mockGraphQLResponse, selectOption, validateDatePickerDate, @@ -145,7 +145,7 @@ test.describe("Spawn volume page", () => { const yesButton = popconfirm.getByRole("button", { name: "Yes" }); await expect(yesButton).toHaveAttribute("aria-disabled", "true"); - await clickLabelForLocator(confirmCheckbox); + await clickCheckbox(confirmCheckbox); await expect(yesButton).toHaveAttribute("aria-disabled", "false"); await yesButton.click(); @@ -307,7 +307,10 @@ test.describe("Spawn volume page", () => { "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", ); await expect(saveButton).toHaveAttribute("aria-disabled", "true"); - await clickLabelForLocator(page.getByLabel("Never expire")); + const neverExpireCheckbox = page.getByRole("checkbox", { + name: "Never expire", + }); + await clickCheckbox(neverExpireCheckbox); await expect(saveButton).toHaveAttribute("aria-disabled", "false"); }); diff --git a/apps/spruce/playwright/tests/version/restart_modal.spec.ts b/apps/spruce/playwright/tests/version/restart_modal.spec.ts index 5262abcbd1..6f2ecaf660 100644 --- a/apps/spruce/playwright/tests/version/restart_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/restart_modal.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../../fixtures"; -import { clickLabelForLocator, validateToast } from "../../helpers"; +import { clickCheckbox, validateToast } from "../../helpers"; const path = "/version/5ecedafb562343215a7ff297"; @@ -73,7 +73,9 @@ test.describe("version/restart_modal", () => { await page.getByTestId("task-status-filter").click(); const options = page.getByTestId("tree-select-options"); await expect(options).toBeVisible(); - await clickLabelForLocator(options.getByTestId("all-checkbox")); + + const allCheckbox = options.getByRole("checkbox", { name: "All" }); + await clickCheckbox(allCheckbox); await page.getByTestId("task-status-filter").click(); await expect(page.getByTestId("version-restart-modal")).toContainText( @@ -81,7 +83,7 @@ test.describe("version/restart_modal", () => { ); await page.getByTestId("task-status-filter").click(); await expect(options).toBeVisible(); - await clickLabelForLocator(options.getByTestId("all-checkbox")); + await clickCheckbox(allCheckbox); await page.getByTestId("task-status-filter").click(); }); @@ -92,7 +94,10 @@ test.describe("version/restart_modal", () => { await modal.getByTestId("base-task-status-filter").click(); const options = page.getByTestId("tree-select-options"); await expect(options).toBeVisible(); - await clickLabelForLocator(options.getByTestId("succeeded-checkbox")); + const succeededCheckbox = options.getByRole("checkbox", { + name: "Succeeded", + }); + await clickCheckbox(succeededCheckbox); await modal.getByTestId("base-task-status-filter").click(); await expect(modal.getByTestId("confirmation-message")).toContainText( @@ -100,7 +105,7 @@ test.describe("version/restart_modal", () => { ); await modal.getByTestId("base-task-status-filter").click(); await expect(options).toBeVisible(); - await clickLabelForLocator(options.getByTestId("succeeded-checkbox")); + await clickCheckbox(succeededCheckbox); await modal.getByTestId("base-task-status-filter").click(); }); diff --git a/apps/spruce/playwright/tests/version/task_filters.spec.ts b/apps/spruce/playwright/tests/version/task_filters.spec.ts index add01a7b52..4ead7381b4 100644 --- a/apps/spruce/playwright/tests/version/task_filters.spec.ts +++ b/apps/spruce/playwright/tests/version/task_filters.spec.ts @@ -1,6 +1,6 @@ import { Page } from "@playwright/test"; import { test, expect } from "../../fixtures"; -import { clickLabelForLocator } from "../../helpers"; +import { clickCheckbox } from "../../helpers"; const patch = { id: "5e4ff3abe3c3317e352062e4" }; const pathTasks = `/version/${patch.id}/tasks`; @@ -129,11 +129,15 @@ test.describe("Tasks filters", () => { authenticatedPage: page, }) => { const options = page.getByTestId("tree-select-options"); - await clickLabelForLocator(options.getByTestId("failed-checkbox")); + const failedCheckbox = options.getByRole("checkbox", { name: "Failed" }); + await clickCheckbox(failedCheckbox); await expect(page).toHaveURL(/statuses=failed/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("2"); - await clickLabelForLocator(options.getByTestId("succeeded-checkbox")); + const succeededCheckbox = options.getByRole("checkbox", { + name: "Succeeded", + }); + await clickCheckbox(succeededCheckbox); await expect(page).toHaveURL( /statuses=failed-umbrella,failed,known-issue,success/, ); @@ -153,7 +157,8 @@ test.describe("Tasks filters", () => { "Dispatched", "Blocked", ]; - await clickLabelForLocator(page.getByTestId("all-checkbox")); + const allCheckbox = page.getByRole("checkbox", { name: "All" }); + await clickCheckbox(allCheckbox); for (const label of taskStatuses) { const checkbox = page.getByRole("checkbox", { name: label }).first(); await expect(checkbox).toBeChecked(); @@ -161,7 +166,7 @@ test.describe("Tasks filters", () => { await expect(page).toHaveURL(/statuses=all/); await waitForTaskTable(page); - await clickLabelForLocator(page.getByTestId("all-checkbox")); + await clickCheckbox(allCheckbox); for (const label of taskStatuses) { const checkbox = page.getByRole("checkbox", { name: label }).first(); await expect(checkbox).not.toBeChecked(); @@ -183,12 +188,15 @@ test.describe("Tasks filters", () => { test("Clicking on a base status filter filters the tasks to only those base statuses", async ({ authenticatedPage: page, }) => { - await clickLabelForLocator(page.getByTestId("succeeded-checkbox")); + const succeededCheckbox = page.getByRole("checkbox", { + name: "Succeeded", + }); + await clickCheckbox(succeededCheckbox); await expect(page).toHaveURL(/baseStatuses=success/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("44"); - await clickLabelForLocator(page.getByTestId("succeeded-checkbox")); + await clickCheckbox(succeededCheckbox); await expect(page).not.toHaveURL(/baseStatuses/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("47"); @@ -198,14 +206,15 @@ test.describe("Tasks filters", () => { authenticatedPage: page, }) => { const taskStatuses = ["All", "Succeeded", "Running"]; - await clickLabelForLocator(page.getByTestId("all-checkbox")); + const allCheckbox = page.getByRole("checkbox", { name: "All" }); + await clickCheckbox(allCheckbox); for (const label of taskStatuses) { await expect(page.getByRole("checkbox", { name: label })).toBeChecked(); } await expect(page).toHaveURL(/baseStatuses=all/); await waitForTaskTable(page); - await clickLabelForLocator(page.getByTestId("all-checkbox")); + await clickCheckbox(allCheckbox); for (const label of taskStatuses) { await expect( page.getByRole("checkbox", { name: label }), diff --git a/apps/spruce/playwright/tests/version/task_table.spec.ts b/apps/spruce/playwright/tests/version/task_table.spec.ts index 467d19c3f8..0e5aa57417 100644 --- a/apps/spruce/playwright/tests/version/task_table.spec.ts +++ b/apps/spruce/playwright/tests/version/task_table.spec.ts @@ -1,7 +1,7 @@ import { Page } from "@playwright/test"; import { SEEN_TASK_REVIEW_TOOLTIP } from "constants/cookies"; import { test, expect } from "../../fixtures"; -import { clickLabelForLocator } from "../../helpers"; +import { clickCheckbox } from "../../helpers"; const pathTasks = "/version/5e4ff3abe3c3317e352062e4/tasks"; const patchDescriptionTasksExist = "dist"; @@ -158,45 +158,44 @@ test.describe("Task table", () => { }); test.describe("task review", () => { - // Clicks the label associated with a reviewed checkbox (the input itself is hidden). - const clickReviewed = async (page: Page, testId: string) => { - await clickLabelForLocator(page.getByTestId(testId)); - }; - test("marks tasks as viewed and preserves their state on reload", async ({ authenticatedPage: page, }) => { await page.goto(pathTasks); - await clickReviewed(page, `reviewed-${firstTaskId}`); - await expect(page.getByTestId(`reviewed-${firstTaskId}`)).toBeChecked(); + const firstTaskCheckbox = page.getByRole("checkbox", { + name: firstTaskId, + }); + await clickCheckbox(firstTaskCheckbox); + await expect(firstTaskCheckbox).toBeChecked(); await page.getByRole("button", { name: "Expand row" }).click(); await expect( page.getByTestId(`reviewed-${executionTaskId1}`), ).toHaveAttribute("aria-disabled", "true"); - await clickReviewed(page, `reviewed-${executionTaskId2}`); - await expect(page.getByTestId(`reviewed-${displayTaskId}`)).toBeChecked(); - await clickReviewed(page, `reviewed-${displayTaskId}`); - await expect( - page.getByTestId(`reviewed-${displayTaskId}`), - ).not.toBeChecked(); - await expect( - page.getByTestId(`reviewed-${executionTaskId2}`), - ).not.toBeChecked(); - await clickReviewed(page, `reviewed-${displayTaskId}`); - await expect(page.getByTestId(`reviewed-${displayTaskId}`)).toBeChecked(); - await expect( - page.getByTestId(`reviewed-${executionTaskId2}`), - ).toBeChecked(); + + const executionTask2Checkbox = page.getByRole("checkbox", { + name: executionTaskId2, + }); + await clickCheckbox(executionTask2Checkbox); + await expect(executionTask2Checkbox).toBeChecked(); + + const displayTaskCheckbox = page.getByRole("checkbox", { + name: displayTaskId, + }); + await clickCheckbox(displayTaskCheckbox); + await expect(displayTaskCheckbox).not.toBeChecked(); + await expect(executionTask2Checkbox).not.toBeChecked(); + + await clickCheckbox(displayTaskCheckbox); + await expect(displayTaskCheckbox).toBeChecked(); + await expect(executionTask2Checkbox).toBeChecked(); await page.reload(); - await expect(page.getByTestId(`reviewed-${firstTaskId}`)).toBeChecked(); - await expect(page.getByTestId(`reviewed-${displayTaskId}`)).toBeChecked(); + await expect(firstTaskCheckbox).toBeChecked(); + await expect(displayTaskCheckbox).toBeChecked(); await page.getByRole("button", { name: "Expand row" }).click(); - await expect( - page.getByTestId(`reviewed-${executionTaskId2}`), - ).toBeChecked(); + await expect(executionTask2Checkbox).toBeChecked(); }); test.describe("announcement tooltip", () => { From 80fec80e235585dfafa9f6121bfbce769c0421ac Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 28 Apr 2026 17:19:11 -0400 Subject: [PATCH 22/32] fix: all of the typos --- .../tests/projectSettings/commit_checks.spec.ts | 6 +++--- .../tests/projectSettings/repo_settings.spec.ts | 16 +++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts b/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts index e5dd81faf5..97d8166d2a 100644 --- a/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts @@ -56,10 +56,10 @@ test.describe("A project that has GitHub webhooks enabled", () => { authenticatedPage: page, }) => { const radioBox = page.getByTestId("github-checks-enabled-radio-box"); - const githubChecksEnabledRadio = radioBox.getByRole("checkbox", { + const githubChecksEnabledRadio = radioBox.getByRole("radio", { name: "Enabled", }); - const githubChecksDisabledRadio = radioBox.getByRole("checkbox", { + const githubChecksDisabledRadio = radioBox.getByRole("radio", { name: "Disabled", }); @@ -77,7 +77,7 @@ test.describe("A project that has GitHub webhooks enabled", () => { authenticatedPage: page, }) => { const radioBox = page.getByTestId("github-checks-enabled-radio-box"); - const githubChecksEnabledRadio = radioBox.getByRole("checkbox", { + const githubChecksEnabledRadio = radioBox.getByRole("radio", { name: "Enabled", }); await clickRadio(githubChecksEnabledRadio); diff --git a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts index dbbbaabee6..93315453ec 100644 --- a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts @@ -68,7 +68,7 @@ test.describe("Repo Settings", () => { }) => { const githubChecksEnabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .getByRole("checkbox", { name: "Enabled" }); + .getByRole("radio", { name: "Enabled" }); await clickRadio(githubChecksEnabledRadio); const errorBanner = page.getByTestId("error-banner").filter({ hasText: @@ -77,7 +77,7 @@ test.describe("Repo Settings", () => { await expect(errorBanner).toBeVisible(); const githubChecksDisabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .getByRole("checkbox", { name: "Disabled" }); + .getByRole("radio", { name: "Disabled" }); await clickRadio(githubChecksDisabledRadio); await expect(errorBanner).toHaveCount(0); }); @@ -147,7 +147,7 @@ test.describe("Repo Settings", () => { const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .getByRole("checkbox", { name: "Enabled" }); + .getByRole("radio", { name: "Enabled" }); await clickRadio(mergeQueueEnabledRadio); await expect( @@ -169,7 +169,7 @@ test.describe("Repo Settings", () => { }) => { const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .getByRole("checkbox", { name: "Enabled" }); + .getByRole("radio", { name: "Enabled" }); await clickRadio(mergeQueueEnabledRadio); await expect(page.getByTestId("cq-override-radio-box")).toHaveCount(0); }); @@ -179,7 +179,7 @@ test.describe("Repo Settings", () => { }) => { const mergeQueueEnabledRadio = page .getByTestId("cq-enabled-radio-box") - .getByRole("checkbox", { name: "Enabled" }); + .getByRole("radio", { name: "Enabled" }); await clickRadio(mergeQueueEnabledRadio); await page .getByRole("button", { name: "Add Patch Definition" }) @@ -297,17 +297,15 @@ test.describe("Repo Settings", () => { await page.getByTestId("variant-regex-input").fill(".*"); await page.getByTestId("task-regex-input").fill(".*"); - const githubPRLabel = "Schedule in GitHub Pull Requests"; const pullRequestCheckbox = page.getByRole("checkbox", { - name: githubPRLabel, + name: "Schedule in GitHub Pull Requests", }); await expect(pullRequestCheckbox).not.toBeChecked(); await clickRadio(pullRequestCheckbox); await expect(pullRequestCheckbox).toBeChecked(); - const githubMQLabel = "Schedule in GitHub Merge Queue"; const mergeQueueCheckbox = page.getByRole("checkbox", { - name: githubMQLabel, + name: "Schedule in GitHub Merge Queue", }); await expect(mergeQueueCheckbox).not.toBeChecked(); await clickRadio(mergeQueueCheckbox); From 8796b21807fb4ebd15ead0671a709c535e352370 Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 28 Apr 2026 17:41:27 -0400 Subject: [PATCH 23/32] fix: clean up workarounds --- apps/spruce/playwright/helpers/index.ts | 2 +- .../tests/distroSettings/host_section.spec.ts | 18 ++--------- .../distroSettings/provider_section.spec.ts | 12 ++----- .../tests/distroSettings/task_section.spec.ts | 19 ++++++------ .../defaulting_to_repo.spec.ts | 6 ++-- .../projectSettings/repo_settings.spec.ts | 2 +- .../tests/version/task_filters.spec.ts | 4 ++- .../tests/version/task_table.spec.ts | 31 ++++++++----------- .../ConfigureTasks/ConfigureTasks.test.tsx | 8 ++--- .../ConfigureTasks/index.tsx | 6 ++-- .../tabs/HostTab/schemaFields.tsx | 2 -- .../tabs/TaskTab/getFormSchema.ts | 3 -- 12 files changed, 43 insertions(+), 70 deletions(-) diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 69aead1309..cc611fac03 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -54,7 +54,7 @@ export async function validateTableSort( * @param options.exact - Whether to match the option text exactly (default: false) */ export async function selectOption( - page: Page, + page: Page | Locator, label: string, option: string | RegExp, options?: { exact: boolean }, diff --git a/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts index 168ddb41da..b9337c768c 100644 --- a/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts @@ -23,11 +23,7 @@ test.describe("host section", () => { test("shows an error when selecting an incompatible host communication method", async ({ authenticatedPage: page, }) => { - await selectOption( - page, - { testId: "communication-method-select" }, - "RPC", - ); + await selectOption(page, "Host Communication Method", "RPC"); await expect( page.getByText( "Legacy and non-legacy bootstrapping and communication are incompatible.", @@ -84,16 +80,8 @@ test.describe("host section", () => { test.describe("using User Data bootstrap method", () => { test.beforeEach(async ({ authenticatedPage: page }) => { await page.goto("/distro/ubuntu1604-parent/settings/host"); - await selectOption( - page, - { testId: "bootstrap-method-select" }, - "User Data", - ); - await selectOption( - page, - { testId: "communication-method-select" }, - "RPC", - ); + await selectOption(page, "Host Bootstrap Method", "User Data"); + await selectOption(page, "Host Communication Method", "RPC"); }); test("shows Windows-only fields when the architecture is updated", async ({ diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts index d348d64a55..1be31e3253 100644 --- a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts @@ -171,11 +171,7 @@ test.describe("provider section", () => { .filter({ hasText: "New AWS Region" }), ).toBeVisible(); - await selectOption( - newExpandableCard, - { testId: "region-select" }, - "us-west-1", - ); + await selectOption(newExpandableCard, "Region", "us-west-1"); await newExpandableCard.getByLabel("EC2 AMI ID").fill("ami-1234"); await newExpandableCard.getByLabel("Instance Type").fill("m5.xlarge"); await newExpandableCard @@ -300,11 +296,7 @@ test.describe("provider section", () => { .filter({ hasText: "New AWS Region" }), ).toBeVisible(); - await selectOption( - newExpandableCard, - { testId: "region-select" }, - "us-west-1", - ); + await selectOption(newExpandableCard, "Region", "us-west-1"); await newExpandableCard.getByLabel("EC2 AMI ID").fill("ami-1234"); await newExpandableCard.getByLabel("Instance Type").fill("m5.xlarge"); await newExpandableCard diff --git a/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts index 81e0ad5e6b..248fa68a1e 100644 --- a/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts @@ -66,15 +66,16 @@ test.describe("task section", () => { await validateToast(page, "success", "Updated distro."); await page.reload(); - await expect(page.getByTestId("finder-version-select")).toContainText( - "Parallel", - ); - await expect(page.getByTestId("planner-version-select")).toContainText( - "Tunable", - ); - await expect(page.getByTestId("dispatcher-version-select")).toContainText( - "Revised with dependencies", - ); + const finder = page.getByRole("button", { name: "Task Finder Version" }); + await expect(finder).toContainText("Parallel"); + const planner = page.getByRole("button", { + name: "Task Planner Version", + }); + await expect(planner).toContainText("Tunable"); + const dispatcher = page.getByRole("button", { + name: "Task Dispatcher Version", + }); + await expect(dispatcher).toContainText("Revised with dependencies"); await selectOption(page, "Task Finder Version", "Legacy"); await selectOption(page, "Task Planner Version", "Tunable"); diff --git a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts index cb7fa7c036..3c3c276806 100644 --- a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts @@ -301,11 +301,11 @@ test.describe("Project Settings when defaulting to repo", () => { }) => { const prDisabledRadio = page .getByTestId("pr-testing-enabled-radio-box") - .getByRole("radio", { name: "Disabled" }); + .getByRole("radio", { name: "Disabled", exact: true }); await clickRadio(prDisabledRadio); const manualDisabledRadio = page .getByTestId("manual-pr-testing-enabled-radio-box") - .getByRole("radio", { name: "Disabled" }); + .getByRole("radio", { name: "Disabled", exact: true }); await clickRadio(manualDisabledRadio); const githubEnabledRadio = page .getByTestId("github-checks-enabled-radio-box") @@ -492,7 +492,7 @@ test.describe("Project Settings when defaulting to repo", () => { ); const defaultToRepoCommandsRadio = page.getByRole("radio", { - name: "Default to repo (disabled)", + name: "Default to Repo Commands", }); await clickRadio(defaultToRepoCommandsRadio); await expect(page.getByTestId("command-row")).toHaveCount(1); diff --git a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts index 93315453ec..25369182ed 100644 --- a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts @@ -77,7 +77,7 @@ test.describe("Repo Settings", () => { await expect(errorBanner).toBeVisible(); const githubChecksDisabledRadio = page .getByTestId("github-checks-enabled-radio-box") - .getByRole("radio", { name: "Disabled" }); + .getByRole("radio", { name: "Disabled", exact: true }); await clickRadio(githubChecksDisabledRadio); await expect(errorBanner).toHaveCount(0); }); diff --git a/apps/spruce/playwright/tests/version/task_filters.spec.ts b/apps/spruce/playwright/tests/version/task_filters.spec.ts index 4ead7381b4..d466c57f30 100644 --- a/apps/spruce/playwright/tests/version/task_filters.spec.ts +++ b/apps/spruce/playwright/tests/version/task_filters.spec.ts @@ -129,7 +129,9 @@ test.describe("Tasks filters", () => { authenticatedPage: page, }) => { const options = page.getByTestId("tree-select-options"); - const failedCheckbox = options.getByRole("checkbox", { name: "Failed" }); + const failedCheckbox = options + .getByRole("checkbox", { name: "Failed" }) + .first(); await clickCheckbox(failedCheckbox); await expect(page).toHaveURL(/statuses=failed/); await waitForTaskTable(page); diff --git a/apps/spruce/playwright/tests/version/task_table.spec.ts b/apps/spruce/playwright/tests/version/task_table.spec.ts index 0e5aa57417..6f768d70df 100644 --- a/apps/spruce/playwright/tests/version/task_table.spec.ts +++ b/apps/spruce/playwright/tests/version/task_table.spec.ts @@ -6,12 +6,6 @@ import { clickCheckbox } from "../../helpers"; const pathTasks = "/version/5e4ff3abe3c3317e352062e4/tasks"; const patchDescriptionTasksExist = "dist"; -const firstTaskId = - "evergreen_ubuntu1604_test_service_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48"; -const displayTaskId = "evergreen_ubuntu1604_89"; -const executionTaskId1 = "exec1"; -const executionTaskId2 = "exec2"; - const waitForTaskTable = async (page: Page) => { const table = page.getByTestId("tasks-table"); await expect(table).toBeVisible(); @@ -158,30 +152,31 @@ test.describe("Task table", () => { }); test.describe("task review", () => { + const firstTask = + "reviewed-evergreen_ubuntu1604_test_service_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48"; + const displayTask = "reviewed-evergreen_ubuntu1604_89"; + const executionTask1 = "reviewed-exec1"; + const executionTask2 = "reviewed-exec2"; + test("marks tasks as viewed and preserves their state on reload", async ({ authenticatedPage: page, }) => { await page.goto(pathTasks); - const firstTaskCheckbox = page.getByRole("checkbox", { - name: firstTaskId, - }); + const firstTaskCheckbox = page.getByTestId(firstTask); await clickCheckbox(firstTaskCheckbox); await expect(firstTaskCheckbox).toBeChecked(); await page.getByRole("button", { name: "Expand row" }).click(); - await expect( - page.getByTestId(`reviewed-${executionTaskId1}`), - ).toHaveAttribute("aria-disabled", "true"); + await expect(page.getByTestId(executionTask1)).toHaveAttribute( + "aria-disabled", + "true", + ); - const executionTask2Checkbox = page.getByRole("checkbox", { - name: executionTaskId2, - }); + const executionTask2Checkbox = page.getByTestId(executionTask2); await clickCheckbox(executionTask2Checkbox); await expect(executionTask2Checkbox).toBeChecked(); - const displayTaskCheckbox = page.getByRole("checkbox", { - name: displayTaskId, - }); + const displayTaskCheckbox = page.getByTestId(displayTask); await clickCheckbox(displayTaskCheckbox); await expect(displayTaskCheckbox).not.toBeChecked(); await expect(executionTask2Checkbox).not.toBeChecked(); diff --git a/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx b/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx index 856dd1e0e3..2f1b3d63a0 100644 --- a/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx +++ b/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx @@ -22,7 +22,7 @@ describe("configureTasks", () => { totalSelectedTaskCount={0} />, ); - expect(screen.queryAllByDataCy(/task-checkbox$/)).toHaveLength(2); + expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(2); expect(screen.getByText("compile")).toBeInTheDocument(); expect(screen.getByText("test")).toBeInTheDocument(); }); @@ -46,7 +46,7 @@ describe("configureTasks", () => { totalSelectedTaskCount={0} />, ); - expect(screen.queryAllByDataCy(/task-checkbox$/)).toHaveLength(4); + expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(4); expect(screen.getByText("compile")).toBeInTheDocument(); expect(screen.getByText("test")).toBeInTheDocument(); expect(screen.getByText("e2e")).toBeInTheDocument(); @@ -72,7 +72,7 @@ describe("configureTasks", () => { totalSelectedTaskCount={3} />, ); - expect(screen.queryAllByDataCy(/task-checkbox$/)).toHaveLength(3); + expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(3); expect(screen.getByText("compile")).toBeInTheDocument(); expect(screen.getByText("test")).toBeInTheDocument(); expect(screen.getByText("lint")).toBeInTheDocument(); @@ -249,7 +249,7 @@ describe("configureTasks", () => { />, ); await user.type(screen.getByDataCy("task-filter-input"), "compile"); - expect(screen.queryAllByDataCy(/task-checkbox$/)).toHaveLength(1); + expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(1); const checkbox = screen.getByLabelText("compile"); expect(checkbox).toBeInTheDocument(); }); diff --git a/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/index.tsx b/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/index.tsx index 80a93f09f6..9bde86ab28 100644 --- a/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/index.tsx +++ b/apps/spruce/src/pages/configurePatch/configurePatchCore/ConfigureTasks/index.tsx @@ -256,7 +256,7 @@ const ConfigureTasks: React.FC = ({ key={name} aria-label={name} checked={isTaskCheckboxChecked(state)} - data-cy={`${name}-task-checkbox`} + data-cy="task-checkbox" indeterminate={isTaskCheckboxIndeterminate(state)} label={name} onChange={onClickCheckbox(name)} @@ -272,7 +272,7 @@ const ConfigureTasks: React.FC = ({ = ({ diff --git a/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx b/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx index fc2463b796..82ddf44204 100644 --- a/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx +++ b/apps/spruce/src/pages/distroSettings/tabs/HostTab/schemaFields.tsx @@ -38,7 +38,6 @@ const bootstrapMethod = { oneOf: enumSelect(bootstrapMethodToCopy), }, uiSchema: { - "ui:data-cy": "bootstrap-method-select", "ui:allowDeselect": false, }, }; @@ -50,7 +49,6 @@ const communicationMethod = { oneOf: enumSelect(communicationMethodToCopy), }, uiSchema: { - "ui:data-cy": "communication-method-select", "ui:allowDeselect": false, }, }; diff --git a/apps/spruce/src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts b/apps/spruce/src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts index 844bf13b22..a860b66d79 100644 --- a/apps/spruce/src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts +++ b/apps/spruce/src/pages/distroSettings/tabs/TaskTab/getFormSchema.ts @@ -172,14 +172,12 @@ export const getFormSchema = ({ "ui:ObjectFieldTemplate": CardFieldTemplate, version: { "ui:allowDeselect": false, - "ui:data-cy": "finder-version-select", }, }, plannerSettings: { "ui:ObjectFieldTemplate": CardFieldTemplate, version: { "ui:allowDeselect": false, - "ui:data-cy": "planner-version-select", }, tunableOptions: { "ui:field-data-cy": "tunable-options", @@ -218,7 +216,6 @@ export const getFormSchema = ({ "ui:ObjectFieldTemplate": CardFieldTemplate, version: { "ui:allowDeselect": false, - "ui:data-cy": "dispatcher-version-select", }, }, }, From 514e7f0414bd8af3ff1b20a08cba563c60fe3638 Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 28 Apr 2026 17:57:33 -0400 Subject: [PATCH 24/32] fix: clean up helpers --- apps/spruce/playwright/helpers/index.ts | 74 +++++-------------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index cc611fac03..1b589f09f6 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -1,50 +1,5 @@ import { Page, Locator, expect } from "@playwright/test"; -type LocatorOptions = Parameters[1]; - -/** - * Gets a locator for an element with the specified data-row-key attribute - * @param page - The Playwright page object - * @param value - The data-row-key attribute value - * @param options - Optional locator options - * @returns A Playwright locator - */ -export function dataRowKeyLocator( - page: Page, - value: string, - options?: LocatorOptions, -): Locator { - return page.locator(`[data-row-key=${value}]`, options); -} - -/** - * Validates table sort direction by checking for the appropriate sort icon - * @param page - The Playwright page object - * @param direction - The expected sort direction (asc, desc, or none) - */ -export async function validateTableSort( - page: Page, - direction?: "asc" | "desc" | "none", -): Promise { - switch (direction) { - case "asc": - await expect( - page.locator("svg[aria-label='Sort Ascending Icon']"), - ).toBeVisible(); - return; - case "desc": - await expect( - page.locator("svg[aria-label='Sort Descending Icon']"), - ).toBeVisible(); - return; - case "none": - default: - await expect( - page.locator("svg[aria-label='Unsorted Icon']"), - ).toBeVisible(); - } -} - /** * Selects an option from a LeafyGreen select component * @param page - The Playwright page object @@ -53,33 +8,34 @@ export async function validateTableSort( * @param options - Additional options for selecting the option * @param options.exact - Whether to match the option text exactly (default: false) */ -export async function selectOption( +export const selectOption = async ( page: Page | Locator, label: string, option: string | RegExp, options?: { exact: boolean }, -): Promise { +): Promise => { const button = page.getByRole("button", { name: label }); + await expect(button).toHaveCount(1); await expect(button).toBeEnabled(); await button.click(); const listbox = page.locator('[role="listbox"]'); await expect(listbox).toHaveCount(1); await listbox.getByText(option, options).click(); -} +}; /** * Clears a date picker input by pressing backspace multiple times * LG Date Picker does not respond well to .clear() * @param page - The Playwright page object */ -export async function clearDatePickerInput(page: Page): Promise { +export const clearDatePickerInput = async (page: Page): Promise => { const dayInput = page.locator("input[id='day']"); await dayInput.press("Backspace"); await dayInput.press("Backspace"); await dayInput.press("Backspace"); await dayInput.press("Backspace"); await dayInput.press("Backspace"); -} +}; /** * Validates the values in a date picker component @@ -90,17 +46,17 @@ export async function clearDatePickerInput(page: Page): Promise { * @param opts.month - The expected month value * @param opts.day - The expected day value */ -export async function validateDatePickerDate( +export const validateDatePickerDate = async ( page: Page, dataCy: string, { year = "", month = "", day = "" } = {}, -): Promise { +): Promise => { const datePicker = page.getByTestId(dataCy); await expect(datePicker.locator("input[id='year']")).toHaveValue(year); await expect(datePicker.locator("input[id='month']")).toHaveValue(month); await expect(datePicker.locator("input[id='day']")).toHaveValue(day); -} +}; /** * Selects a date in a LeafyGreen date picker by navigating the year/month dropdowns @@ -112,11 +68,11 @@ export async function validateDatePickerDate( * @param opts.isoDate - The ISO date string of the day cell to click (e.g., "2025-02-28") * @param dataCy - The data-cy attribute value of the date picker (default: "date-picker") */ -export async function selectDatePickerDate( +export const selectDatePickerDate = async ( page: Page, { year = "", month = "", isoDate = "" } = {}, dataCy = "date-picker", -): Promise { +): Promise => { await page.getByTestId(dataCy).click(); const options = page.getByRole("listbox").getByRole("option"); @@ -134,7 +90,7 @@ export async function selectDatePickerDate( await options.getByText(month).click(); await page.locator(`[data-iso='${isoDate}']`).click(); -} +}; /** * Types a date into a LeafyGreen date picker by filling the year, month, and day inputs. @@ -145,11 +101,11 @@ export async function selectDatePickerDate( * @param opts.day - The day to select (e.g., "28") * @param dataCy - The data-cy attribute value of the date picker (default: "date-picker") */ -export async function typeDatePickerDate( +export const typeDatePickerDate = async ( page: Page, { year = "", month = "", day = "" } = {}, dataCy = "date-picker", -): Promise { +): Promise => { const datePicker = page.getByTestId(dataCy); const yearInput = datePicker.locator("input[id='year']"); const monthInput = datePicker.locator("input[id='month']"); @@ -158,7 +114,7 @@ export async function typeDatePickerDate( await yearInput.fill(year); await monthInput.fill(month); await dayInput.fill(day); -} +}; // Re-export shared helpers from the playwright-config package. export { From 21cb2b206a13006653505bc8013e4a1dd27d6e82 Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 28 Apr 2026 17:59:15 -0400 Subject: [PATCH 25/32] fix: unnecessary `data-cy` --- apps/spruce/src/components/Header/UserDropdown.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/spruce/src/components/Header/UserDropdown.tsx b/apps/spruce/src/components/Header/UserDropdown.tsx index 5d4ea68859..560d6a15f5 100644 --- a/apps/spruce/src/components/Header/UserDropdown.tsx +++ b/apps/spruce/src/components/Header/UserDropdown.tsx @@ -30,7 +30,6 @@ export const UserDropdown = () => { onClick: () => sendEvent({ name: "Clicked notifications link" }), }, { - "data-cy": "ui-settings-link", text: "UI Settings", to: getPreferencesRoute(PreferencesTabRoutes.UISettings), onClick: () => sendEvent({ name: "Clicked UI settings link" }), From 1802c421f212bc84e6413b566887882039b7a0ad Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 28 Apr 2026 18:08:37 -0400 Subject: [PATCH 26/32] fix: missed --- .../tests/projectSettings/repo_settings.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts index 25369182ed..25ebaa917d 100644 --- a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts +++ b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts @@ -85,11 +85,11 @@ test.describe("Repo Settings", () => { test("Allows enabling manual PR testing", async ({ authenticatedPage: page, }) => { - await page + const enabledRadio = page .getByTestId("manual-pr-testing-enabled-radio-box") - .locator("> *") - .first() - .click(); + .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 ({ From 5c7753ee12bb1450d8033b26081b357990cf39b4 Mon Sep 17 00:00:00 2001 From: minnakt Date: Tue, 28 Apr 2026 18:18:43 -0400 Subject: [PATCH 27/32] fix: better selector --- apps/spruce/playwright/helpers/index.ts | 2 +- .../distroSettings/provider_section.spec.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/spruce/playwright/helpers/index.ts b/apps/spruce/playwright/helpers/index.ts index 1b589f09f6..0c50af0a7c 100644 --- a/apps/spruce/playwright/helpers/index.ts +++ b/apps/spruce/playwright/helpers/index.ts @@ -14,7 +14,7 @@ export const selectOption = async ( option: string | RegExp, options?: { exact: boolean }, ): Promise => { - const button = page.getByRole("button", { name: label }); + const button = page.getByRole("button", { name: label, exact: true }); await expect(button).toHaveCount(1); await expect(button).toBeEnabled(); await button.click(); diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts index 1be31e3253..96fadeb604 100644 --- a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts +++ b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts @@ -26,7 +26,7 @@ test.describe("provider section", () => { await page.getByRole("button", { name: "Add host" }).click(); await page.getByLabel("Name").fill("host-1234"); await save(page); - await validateToast(page, "success", "Updated distro."); + await validateToast(page, "success", "Updated distro.", true); await page.getByTestId("user-data-input").clear(); await clickCheckbox(mergeUserDataCheckbox); @@ -75,7 +75,7 @@ test.describe("provider section", () => { ); await clickCheckbox(mergeUserDataCheckbox); await save(page); - await validateToast(page, "success", "Updated distro."); + await validateToast(page, "success", "Updated distro.", true); await selectOption(page, "Image Build Method", "Import"); await selectOption(page, "Container Pool ID", "test-pool-1"); @@ -137,7 +137,7 @@ test.describe("provider section", () => { await page.getByLabel("Device Name").fill("device name"); await page.getByLabel("Size").fill("200"); await save(page); - await validateToast(page, "success", "Updated distro."); + await validateToast(page, "success", "Updated distro.", true); await selectOption(page, "Region", "us-east-1"); await page.getByLabel("SSH Key Name").clear(); @@ -179,7 +179,8 @@ test.describe("provider section", () => { .click(); await newExpandableCard.getByLabel("Security Group ID").fill("sg-5678"); await save(page); - await validateToast(page, "success", "Updated distro."); + await validateToast(page, "success", "Updated distro.", true); + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( "aria-disabled", "true", @@ -265,10 +266,11 @@ test.describe("provider section", () => { }); await clickCheckbox(mergeUserDataCheckbox); await save(page); - await validateToast(page, "success", "Updated distro."); + await validateToast(page, "success", "Updated distro.", true); + await clickCheckbox(mergeUserDataCheckbox); await save(page); - await validateToast(page, "success", "Updated distro."); + await validateToast(page, "success", "Updated distro.", true); await selectOption(page, "Region", "us-east-1"); await page.getByLabel("EC2 AMI ID").clear(); @@ -304,7 +306,8 @@ test.describe("provider section", () => { .click(); await newExpandableCard.getByLabel("Security Group ID").fill("sg-0000"); await save(page); - await validateToast(page, "success", "Updated distro."); + await validateToast(page, "success", "Updated distro.", true); + await expect(page.getByTestId("save-settings-button")).toHaveAttribute( "aria-disabled", "true", From 7d5a1c2ca66347b70eaaa49dc2820ba806e14377 Mon Sep 17 00:00:00 2001 From: minnakt Date: Fri, 1 May 2026 15:36:32 -0400 Subject: [PATCH 28/32] delete old changes to ease merge conflicts --- .../tests/adminSettings/announcements.spec.ts | 30 - .../adminSettings/authentication.spec.ts | 214 ------- .../background_processing.spec.ts | 77 --- .../external_communications.spec.ts | 136 ----- .../tests/adminSettings/navigation.spec.ts | 19 - .../tests/adminSettings/other.spec.ts | 413 -------------- .../tests/adminSettings/restart_tasks.spec.ts | 53 -- .../tests/adminSettings/runners.spec.ts | 63 --- .../tests/adminSettings/save_function.spec.ts | 488 ---------------- .../tests/adminSettings/service_flags.spec.ts | 62 --- .../playwright/tests/adminSettings/utils.ts | 8 - .../tests/adminSettings/web.spec.ts | 59 -- .../distroSettings/general_section.spec.ts | 123 ----- .../tests/distroSettings/host_section.spec.ts | 136 ----- .../tests/distroSettings/navigation.spec.ts | 117 ---- .../tests/distroSettings/new_distro.spec.ts | 90 --- .../distroSettings/on_save_operations.spec.ts | 29 - .../tests/distroSettings/permissions.spec.ts | 65 --- .../distroSettings/project_section.spec.ts | 43 -- .../distroSettings/provider_section.spec.ts | 341 ------------ .../distroSettings/single_task_distro.spec.ts | 31 -- .../tests/distroSettings/task_section.spec.ts | 91 --- .../playwright/tests/distroSettings/utils.ts | 21 - .../tests/projectSettings/access.spec.ts | 87 --- .../projectSettings/admin_actions.spec.ts | 99 ---- .../projectSettings/attaching_to_repo.spec.ts | 63 --- .../projectSettings/commit_checks.spec.ts | 93 ---- .../tests/projectSettings/constants.ts | 31 -- .../defaulting_to_repo.spec.ts | 521 ------------------ .../github_app_settings.spec.ts | 105 ---- .../github_permission_groups.spec.ts | 80 --- .../not_defaulting_to_repo.spec.ts | 322 ----------- .../projectSettings/notifications.spec.ts | 144 ----- .../tests/projectSettings/permissions.spec.ts | 63 --- .../tests/projectSettings/plugins.spec.ts | 93 ---- .../projectSettings/project_select.spec.ts | 21 - .../projectSettings/project_settings.spec.ts | 73 --- .../projectSettings/repo_settings.spec.ts | 385 ------------- .../projectSettings/stepback_bisect.spec.ts | 68 --- .../playwright/tests/projectSettings/utils.ts | 26 - .../projectSettings/views_and_filters.spec.ts | 69 --- 41 files changed, 5052 deletions(-) delete mode 100644 apps/spruce/playwright/tests/adminSettings/announcements.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/authentication.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/external_communications.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/navigation.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/other.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/runners.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/save_function.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/utils.ts delete mode 100644 apps/spruce/playwright/tests/adminSettings/web.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/general_section.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/host_section.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/navigation.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/on_save_operations.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/permissions.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/project_section.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/single_task_distro.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/task_section.spec.ts delete mode 100644 apps/spruce/playwright/tests/distroSettings/utils.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/access.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/admin_actions.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/attaching_to_repo.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/constants.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/notifications.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/permissions.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/plugins.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/project_select.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/project_settings.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/utils.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts diff --git a/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts b/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts deleted file mode 100644 index 0e372751d3..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/announcements.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("announcements", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("can save after making changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("some more banner text"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - - await page.reload(); - await expect(bannerText).toHaveValue("some more banner text"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts b/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts deleted file mode 100644 index 34f9b7f39b..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/authentication.spec.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickCheckbox, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("authentication", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("can save after making changes to authentication settings", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByTestId("navitem-admin-global-config").click(); - - // Global Config section. - const allowServiceUsersCheckbox = page.getByRole("checkbox", { - name: "Allow Service Users", - }); - await clickCheckbox(allowServiceUsersCheckbox); - - await page.getByLabel("Background Reauthentication (Mins)").clear(); - await page.getByLabel("Background Reauthentication (Mins)").fill("120"); - - // Okta section. - const okta = page.getByTestId("okta"); - await okta.getByLabel("Client ID").clear(); - await okta.getByLabel("Client ID").fill("test-okta-client-id"); - await okta.getByLabel("Client Secret").clear(); - await okta.getByLabel("Client Secret").fill("test-okta-client-secret"); - await okta.getByLabel("Issuer").clear(); - await okta.getByLabel("Issuer").fill("https://test.okta.com"); - await okta.getByLabel("User Group").clear(); - await okta.getByLabel("User Group").fill("test-users"); - await okta.getByLabel("Expire After (Mins)").clear(); - await okta.getByLabel("Expire After (Mins)").fill("480"); - await okta.getByLabel("Scopes").fill("openid"); - await okta.getByLabel("Scopes").press("Enter"); - await okta.getByLabel("Scopes").fill("profile"); - await okta.getByLabel("Scopes").press("Enter"); - await okta.getByLabel("Scopes").fill("email"); - await okta.getByLabel("Scopes").press("Enter"); - - // GitHub section. - const github = page.getByTestId("github"); - await github.getByLabel("App ID").clear(); - await github.getByLabel("App ID").fill("54321"); - await github.getByLabel("Client ID").clear(); - await github.getByLabel("Client ID").fill("test-github-client-id"); - await github.getByLabel("Client Secret").clear(); - await github.getByLabel("Client Secret").fill("test-github-client-secret"); - await github.getByLabel("Default Owner").clear(); - await github.getByLabel("Default Owner").fill("test-owner"); - await github.getByLabel("Default Repository").clear(); - await github.getByLabel("Default Repository").fill("test-repo"); - await github.getByLabel("Organization").clear(); - await github.getByLabel("Organization").fill("test-org"); - await github.getByLabel("Users").fill("testuser1"); - await github.getByLabel("Users").press("Enter"); - await github.getByLabel("Users").fill("testuser2"); - await github.getByLabel("Users").press("Enter"); - - // Naive section. - const naive = page.getByTestId("naive"); - await naive.getByRole("button", { name: "Add" }).click(); - await naive.getByLabel("Display Name").first().fill("Test User 1"); - await naive.getByLabel("Email").first().fill("test1@example.com"); - await naive.getByLabel("Password").first().fill("password123"); - await naive.getByLabel("Username").first().fill("testuser1"); - - // Multi read-write section. - await page.getByTestId("multi-read-write").click(); - const multiReadWriteOptions = page.getByTestId("multi-read-write-options"); - const oktaCheckbox = multiReadWriteOptions.getByRole("checkbox", { - name: "Okta", - }); - await clickCheckbox(oktaCheckbox); - await page.getByTestId("multi-read-write").click(); - - // Multi read-only section. - await page.getByTestId("multi-read-only").click(); - const multiReadOnlyOptions = page.getByTestId("multi-read-only-options"); - const naiveCheckbox = multiReadOnlyOptions.getByRole("checkbox", { - name: "Naive", - }); - await clickCheckbox(naiveCheckbox); - await page.getByTestId("multi-read-only").click(); - - // Kanopy section. - const kanopy = page.getByTestId("kanopy"); - await kanopy.getByLabel("Header Name").clear(); - await kanopy.getByLabel("Header Name").fill("X-Test-Auth-Token"); - await kanopy.getByLabel("Issuer").clear(); - await kanopy.getByLabel("Issuer").fill("https://test-kanopy.example.com"); - await kanopy.getByLabel("Keyset URL").clear(); - await kanopy - .getByLabel("Keyset URL") - .fill("https://test-kanopy.example.com/.well-known/jwks.json"); - - // OAuth section. - const oauth = page.getByTestId("oauth"); - await oauth.getByLabel("Client ID").clear(); - await oauth.getByLabel("Client ID").fill("oauth-client-id"); - await oauth.getByLabel("Issuer").clear(); - await oauth.getByLabel("Issuer").fill("https://test-oauth.example.com"); - await oauth.getByLabel("Connector ID").clear(); - await oauth.getByLabel("Connector ID").fill("oauth-connector-id"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await page.getByTestId("navitem-admin-global-config").click(); - - // Verify Global Config. - await expect(page.getByLabel("Allow Service Users")).toBeChecked(); - await expect( - page.getByLabel("Background Reauthentication (Mins)"), - ).toHaveValue("120"); - - // Verify Okta settings. - await expect(okta.getByLabel("Client ID")).toHaveValue( - "test-okta-client-id", - ); - await expect(okta.getByLabel("Client Secret")).toHaveValue( - "test-okta-client-secret", - ); - await expect(okta.getByLabel("Issuer")).toHaveValue( - "https://test.okta.com", - ); - await expect(okta.getByLabel("User Group")).toHaveValue("test-users"); - await expect(okta.getByLabel("Expire After (Mins)")).toHaveValue("480"); - await expect(okta.getByTestId("filter-chip")).toHaveCount(3); - await expect( - okta.getByTestId("filter-chip").filter({ hasText: "openid" }), - ).toBeVisible(); - await expect( - okta.getByTestId("filter-chip").filter({ hasText: "profile" }), - ).toBeVisible(); - await expect( - okta.getByTestId("filter-chip").filter({ hasText: "email" }), - ).toBeVisible(); - - // Verify GitHub settings. - await expect(github.getByLabel("App ID")).toHaveValue("54321"); - await expect(github.getByLabel("Client ID")).toHaveValue( - "test-github-client-id", - ); - await expect(github.getByLabel("Client Secret")).toHaveValue( - "test-github-client-secret", - ); - await expect(github.getByLabel("Default Owner")).toHaveValue("test-owner"); - await expect(github.getByLabel("Default Repository")).toHaveValue( - "test-repo", - ); - await expect(github.getByLabel("Organization")).toHaveValue("test-org"); - await expect(github.getByTestId("filter-chip")).toHaveCount(2); - await expect( - github.getByTestId("filter-chip").filter({ hasText: "testuser1" }), - ).toBeVisible(); - await expect( - github.getByTestId("filter-chip").filter({ hasText: "testuser2" }), - ).toBeVisible(); - - // Verify Naive user was added. - await expect(naive.getByLabel("Display Name").first()).toHaveValue( - "Test User 1", - ); - await expect(naive.getByLabel("Email").first()).toHaveValue( - "test1@example.com", - ); - await expect(naive.getByLabel("Password").first()).toHaveValue( - "password123", - ); - await expect(naive.getByLabel("Username").first()).toHaveValue("testuser1"); - - // Verify Multi settings. - await page.getByTestId("multi-read-write").click(); - await expect( - page.getByTestId("multi-read-write-options").getByLabel("Okta"), - ).toBeChecked(); - await page.getByTestId("multi-read-write").click(); - - await page.getByTestId("multi-read-only").click(); - await expect( - page.getByTestId("multi-read-only-options").getByLabel("Naive"), - ).toBeChecked(); - await page.getByTestId("multi-read-only").click(); - - // Verify Kanopy settings. - await expect(kanopy.getByLabel("Header Name")).toHaveValue( - "X-Test-Auth-Token", - ); - await expect(kanopy.getByLabel("Issuer")).toHaveValue( - "https://test-kanopy.example.com", - ); - await expect(kanopy.getByLabel("Keyset URL")).toHaveValue( - "https://test-kanopy.example.com/.well-known/jwks.json", - ); - - // Verify OAuth settings. - await expect(oauth.getByLabel("Client ID")).toHaveValue("oauth-client-id"); - await expect(oauth.getByLabel("Issuer")).toHaveValue( - "https://test-oauth.example.com", - ); - await expect(oauth.getByLabel("Connector ID")).toHaveValue( - "oauth-connector-id", - ); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts b/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts deleted file mode 100644 index ad5b40daff..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/background_processing.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickCheckbox, selectOption, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("background processing", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("can save after making changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - // Amboy section. - await page.getByLabel("Single Worker Name").clear(); - await page.getByLabel("Single Worker Name").fill("new single worker name"); - - await page - .getByTestId("named-queue-list") - .getByTestId("delete-item-button") - .first() - .click(); - - // Logger section. - await selectOption(page, "Default Level", "Alert"); - - await page.getByLabel("Redact Keys").fill("aNewRedactedKey"); - await page.getByLabel("Redact Keys").press("Enter"); - - const asyncBufferCheckbox = page.getByRole("checkbox", { - name: "Use asynchronous buffered logger", - }); - await clickCheckbox(asyncBufferCheckbox); - - // Notification Rate Limits section. - await page.getByLabel("Time Interval (secs)", { exact: true }).clear(); - await page.getByLabel("Time Interval (secs)", { exact: true }).fill("1"); - - // Triggers section. - await page.getByLabel("Distro for Generated Tasks").clear(); - await page.getByLabel("Distro for Generated Tasks").fill("localhost"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Single Worker Name")).toHaveValue( - "new single worker name", - ); - await expect(page.getByLabel("Default Level")).toContainText("Alert"); - await expect( - page.getByTestId("logger").getByTestId("filter-chip"), - ).toHaveCount(4); - await expect( - page - .getByTestId("logger") - .getByTestId("filter-chip") - .filter({ hasText: "aNewRedactedKey" }), - ).toBeVisible(); - await expect( - page.getByLabel("Use asynchronous buffered logger"), - ).not.toBeChecked(); - await expect( - page.getByLabel("Time Interval (secs)", { exact: true }), - ).toHaveValue("1"); - await expect(page.getByLabel("Distro for Generated Tasks")).toHaveValue( - "localhost", - ); - - // Ensure that Notify SES Input has not changed. - await expect(page.getByLabel("SES Email")).toHaveValue( - "evg-sender@email.com", - ); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/external_communications.spec.ts b/apps/spruce/playwright/tests/adminSettings/external_communications.spec.ts deleted file mode 100644 index 394448eab6..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/external_communications.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("external communications", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("can save after making changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByTestId("navitem-admin-jira").click(); - - // JIRA section. - const jira = page.getByTestId("jira"); - await jira.getByLabel("Email").clear(); - await jira.getByLabel("Email").fill("test@example.com"); - await jira.getByLabel("Host").clear(); - await jira.getByLabel("Host").fill("jira.test.com"); - await jira.getByLabel("Personal Access Token").clear(); - await jira.getByLabel("Personal Access Token").fill("password"); - - // Slack section. - const slack = page.getByTestId("slack"); - await slack.getByLabel("Token").clear(); - await slack.getByLabel("Token").fill("xoxb-test-token"); - await slack.getByLabel("App Name").clear(); - await slack.getByLabel("App Name").fill("test-app"); - await slack.getByLabel("Channel").clear(); - await slack.getByLabel("Channel").fill("#test-channel"); - await slack.getByLabel("Fields To Set").fill("field1"); - await slack.getByLabel("Fields To Set").press("Enter"); - await slack.getByLabel("Fields To Set").fill("field2"); - await slack.getByLabel("Fields To Set").press("Enter"); - - // Splunk section. - const splunk = page.getByTestId("splunk"); - await splunk.getByLabel("Server URL").clear(); - await splunk.getByLabel("Server URL").fill("splunk.test.com"); - await splunk.getByLabel("Channel").clear(); - await splunk.getByLabel("Channel").fill("test-logs"); - - // Runtime Environments section. - const runtimeEnv = page.getByTestId("runtime-environments"); - await runtimeEnv.getByLabel("Base URL").clear(); - await runtimeEnv.getByLabel("Base URL").fill("runtime.test.com"); - - // Test Selection section. - const testSelection = page.getByTestId("test-selection"); - await testSelection.getByLabel("URL").clear(); - await testSelection.getByLabel("URL").fill("testselection.test.com"); - - // FWS section. - const fws = page.getByTestId("fws"); - await fws.getByLabel("URL").clear(); - await fws.getByLabel("URL").fill("fws.test.com"); - - // Cedar section. - const cedar = page.getByTestId("cedar"); - await cedar.getByLabel("Database URL").clear(); - await cedar.getByLabel("Database URL").fill("cedar-db.test.com"); - await cedar.getByLabel("Database Name").clear(); - await cedar.getByLabel("Database Name").fill("test-cedar-db"); - await cedar.getByLabel("SPS URL (Vanity, for hosts only)").clear(); - await cedar - .getByLabel("SPS URL (Vanity, for hosts only)") - .fill("sps.test.com"); - await cedar.getByLabel("SPS Kanopy URL").clear(); - await cedar.getByLabel("SPS Kanopy URL").fill("sps-kanopy.test.com"); - - // Sage section. - const sage = page.getByTestId("sage"); - await sage.getByLabel("Base URL").clear(); - await sage.getByLabel("Base URL").fill("sage.test.com"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - // Verify JIRA section. - await expect(jira.getByLabel("Email")).toHaveValue("test@example.com"); - await expect(jira.getByLabel("Host")).toHaveValue("jira.test.com"); - - // Verify Slack section. - await expect(slack.getByLabel("Token")).toHaveValue("xoxb-test-token"); - await expect(slack.getByLabel("App Name")).toHaveValue("test-app"); - await expect(slack.getByLabel("Channel")).toHaveValue("#test-channel"); - await expect(slack.getByTestId("filter-chip")).toHaveCount(2); - await expect( - slack.getByTestId("filter-chip").filter({ hasText: "field1" }), - ).toBeVisible(); - await expect( - slack.getByTestId("filter-chip").filter({ hasText: "field2" }), - ).toBeVisible(); - - // Verify Splunk section. - await expect(splunk.getByLabel("Server URL")).toHaveValue( - "splunk.test.com", - ); - await expect(splunk.getByLabel("Channel")).toHaveValue("test-logs"); - - // Verify Runtime Environments section. - await expect(runtimeEnv.getByLabel("Base URL")).toHaveValue( - "runtime.test.com", - ); - - // Verify Test Selection section. - await expect(testSelection.getByLabel("URL")).toHaveValue( - "testselection.test.com", - ); - - // Verify FWS section. - await expect(fws.getByLabel("URL")).toHaveValue("fws.test.com"); - - // Verify Cedar section. - await expect(cedar.getByLabel("Database URL")).toHaveValue( - "cedar-db.test.com", - ); - await expect(cedar.getByLabel("Database Name")).toHaveValue( - "test-cedar-db", - ); - await expect( - cedar.getByLabel("SPS URL (Vanity, for hosts only)"), - ).toHaveValue("sps.test.com"); - await expect(cedar.getByLabel("SPS Kanopy URL")).toHaveValue( - "sps-kanopy.test.com", - ); - - // Verify Sage section. - await expect(sage.getByLabel("Base URL")).toHaveValue("sage.test.com"); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/navigation.spec.ts b/apps/spruce/playwright/tests/adminSettings/navigation.spec.ts deleted file mode 100644 index afaedcdaf5..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/navigation.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { test, expect } from "../../fixtures"; - -test.describe("admin settings page", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("can navigate to the admin settings page", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("admin-settings-page")).toBeVisible(); - }); - - test("has a side navigation with the correct items", async ({ - authenticatedPage: page, - }) => { - await expect(page.locator("[id=announcements]")).toBeVisible(); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/other.spec.ts b/apps/spruce/playwright/tests/adminSettings/other.spec.ts deleted file mode 100644 index 2092af7a7f..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/other.spec.ts +++ /dev/null @@ -1,413 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickCheckbox, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("other", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("can save misc settings changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByLabel("Config Directory").clear(); - await page.getByLabel("Config Directory").fill("/new/config/dir"); - - await page.getByLabel("GitHub PR Creator Organization").clear(); - await page - .getByLabel("GitHub PR Creator Organization") - .fill("new.example.com"); - - await page.getByLabel("Shutdown Wait Time (secs)").clear(); - await page.getByLabel("Shutdown Wait Time (secs)").fill("45"); - - await page.getByLabel("GitHub Organizations").fill("neworg"); - await page.getByLabel("GitHub Organizations").press("Enter"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Config Directory")).toHaveValue( - "/new/config/dir", - ); - await expect(page.getByLabel("GitHub PR Creator Organization")).toHaveValue( - "new.example.com", - ); - await expect(page.getByLabel("Shutdown Wait Time (secs)")).toHaveValue( - "45", - ); - await expect( - page - .getByTestId("misc-settings") - .getByTestId("filter-chip") - .filter({ hasText: "neworg" }), - ).toBeVisible(); - }); - - test("can save cost settings changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByLabel("Finance Formula").clear(); - await page.getByLabel("Finance Formula").fill("0.5"); - await page.getByLabel("Savings Plan Discount").clear(); - await page.getByLabel("Savings Plan Discount").fill("0.15"); - await page.getByLabel("On-Demand Discount").clear(); - await page.getByLabel("On-Demand Discount").fill("0.08"); - await page.getByLabel("EBS Cost Discount").clear(); - await page.getByLabel("EBS Cost Discount").fill("0.1"); - await page.getByLabel("Upload Cost Discount").clear(); - await page.getByLabel("Upload Cost Discount").fill("0.12"); - await page.getByLabel("Standard Storage Cost Discount").clear(); - await page.getByLabel("Standard Storage Cost Discount").fill("0.18"); - await page.getByLabel("Infrequent Access Storage Cost Discount").clear(); - await page - .getByLabel("Infrequent Access Storage Cost Discount") - .fill("0.22"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Finance Formula")).toHaveValue("0.5"); - await expect(page.getByLabel("Savings Plan Discount")).toHaveValue("0.15"); - await expect(page.getByLabel("On-Demand Discount")).toHaveValue("0.08"); - await expect(page.getByLabel("EBS Cost Discount")).toHaveValue("0.1"); - await expect(page.getByLabel("Upload Cost Discount")).toHaveValue("0.12"); - await expect(page.getByLabel("Standard Storage Cost Discount")).toHaveValue( - "0.18", - ); - await expect( - page.getByLabel("Infrequent Access Storage Cost Discount"), - ).toHaveValue("0.22"); - }); - - test("can clear S3 cost discount values", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByLabel("Upload Cost Discount").clear(); - await page.getByLabel("Upload Cost Discount").fill("0.12"); - await page.getByLabel("Standard Storage Cost Discount").clear(); - await page.getByLabel("Standard Storage Cost Discount").fill("0.18"); - await page.getByLabel("Infrequent Access Storage Cost Discount").clear(); - await page - .getByLabel("Infrequent Access Storage Cost Discount") - .fill("0.22"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Upload Cost Discount")).toHaveValue("0.12"); - await expect(page.getByLabel("Standard Storage Cost Discount")).toHaveValue( - "0.18", - ); - await expect( - page.getByLabel("Infrequent Access Storage Cost Discount"), - ).toHaveValue("0.22"); - - await page.getByLabel("Upload Cost Discount").clear(); - await page.getByLabel("Standard Storage Cost Discount").clear(); - await page.getByLabel("Infrequent Access Storage Cost Discount").clear(); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Upload Cost Discount")).toHaveValue("0"); - await expect(page.getByLabel("Standard Storage Cost Discount")).toHaveValue( - "0", - ); - await expect( - page.getByLabel("Infrequent Access Storage Cost Discount"), - ).toHaveValue("0"); - }); - - test("can save single task host changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByRole("button", { name: "Add project tasks pair" }).click(); - await page.getByLabel("Project ID / Repo").first().click(); - await expect(page.locator('[role="listbox"]')).toBeVisible(); - await page.locator('[role="option"]').last().click(); - - await page.getByLabel("Allowed Tasks").first().fill("compile"); - await page.getByLabel("Allowed Tasks").first().press("Enter"); - await page.getByLabel("Allowed Tasks").first().fill("test"); - await page.getByLabel("Allowed Tasks").first().press("Enter"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Project ID / Repo")).toHaveCount(3); - }); - - test("can save bucket config changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const bucketConfig = page.getByTestId("bucket-config"); - await bucketConfig.getByLabel("Name").clear(); - await bucketConfig.getByLabel("Name").fill("new-log-bucket"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect( - page.getByTestId("bucket-config").getByLabel("Name"), - ).toHaveValue("new-log-bucket"); - }); - - test("can save SSH pairs changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const sshPairs = page.getByTestId("ssh-pairs"); - const taskHostKey = sshPairs.getByTestId("task-host-key"); - await taskHostKey.getByLabel("Name").clear(); - await taskHostKey.getByLabel("Name").fill("new-task-host-key"); - await taskHostKey.getByLabel("Secret ARN").clear(); - await taskHostKey.getByLabel("Secret ARN").fill("new-task-host-secret-arn"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(taskHostKey.getByLabel("Name")).toHaveValue( - "new-task-host-key", - ); - await expect(taskHostKey.getByLabel("Secret ARN")).toHaveValue( - "new-task-host-secret-arn", - ); - }); - - test("can save expansions changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page - .getByTestId("expansions-list") - .getByRole("button", { name: "Add" }) - .click(); - const newExpansionsRow = page.getByTestId("expansions-list-item").first(); - await newExpansionsRow.getByLabel("Key").fill("NEW_VAR"); - await newExpansionsRow.getByLabel("Value").fill("new_value"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByTestId("expansions-list-item")).toHaveCount(2); - }); - - test("can save host jasper changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const hostJasper = page.getByTestId("host-jasper"); - await hostJasper.getByLabel("Binary Name").clear(); - await hostJasper.getByLabel("Binary Name").fill("new-jasper"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect( - page.getByTestId("host-jasper").getByLabel("Binary Name"), - ).toHaveValue("new-jasper"); - }); - - test("can save JIRA notifications changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const jiraNotifications = page.getByTestId("jira-notifications"); - await jiraNotifications - .getByRole("button", { name: "Add new Jira project" }) - .click(); - - const newJiraNotification = page - .getByTestId("jira-custom-fields-list-item") - .first(); - await newJiraNotification.getByLabel("Project").fill("TEST"); - await newJiraNotification.getByLabel("Components").fill("backend"); - await newJiraNotification.getByLabel("Components").press("Enter"); - await newJiraNotification.getByLabel("Labels").fill("feature"); - await newJiraNotification.getByLabel("Labels").press("Enter"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByTestId("jira-custom-fields-list-item")).toHaveCount( - 3, - ); - }); - - test("can save spawn host changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByLabel("Unexpirable Hosts Per User").clear(); - await page.getByLabel("Unexpirable Hosts Per User").fill("5"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Unexpirable Hosts Per User")).toHaveValue( - "5", - ); - }); - - test("can save sleep schedule changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByLabel("Permanently Exempt Hosts").fill("exempt-host-1"); - await page.getByLabel("Permanently Exempt Hosts").press("Enter"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect( - page - .getByTestId("sleep-schedule") - .getByTestId("filter-chip") - .filter({ hasText: "exempt-host-1" }), - ).toBeVisible(); - }); - - test("can save tracer configuration changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const tracerConfiguration = page.getByTestId("tracer-configuration"); - const tracerEnabledCheckbox = tracerConfiguration.getByRole("checkbox", { - name: "Enable tracer", - }); - await clickCheckbox(tracerEnabledCheckbox); - - await page.getByLabel("Collector Endpoint").clear(); - await page - .getByLabel("Collector Endpoint") - .fill("https://new-collector.example.com"); - - await page.getByLabel("Trace URL Template").clear(); - await page - .getByLabel("Trace URL Template") - .fill("https://apm.example.com/trace/%s"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect( - page.getByTestId("tracer-configuration").getByLabel("Enable tracer"), - ).toBeChecked(); - await expect(page.getByLabel("Collector Endpoint")).toHaveValue( - "https://new-collector.example.com", - ); - await expect(page.getByLabel("Trace URL Template")).toHaveValue( - "https://apm.example.com/trace/%s", - ); - }); - - test("can save project creation changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByLabel("Total Project Limit").clear(); - await page.getByLabel("Total Project Limit").fill("150"); - - const projectCreationSettings = page.getByTestId( - "project-creation-settings", - ); - await projectCreationSettings - .getByRole("button", { name: "Add repository exception" }) - .click(); - const newProjectCreationException = page - .getByTestId("repo-exceptions-list-item") - .first(); - await newProjectCreationException.getByLabel("Owner").fill("test-owner"); - await newProjectCreationException - .getByLabel("Repository") - .fill("test-repo"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Total Project Limit")).toHaveValue("150"); - await expect(page.getByTestId("repo-exceptions-list-item")).toHaveCount(1); - }); - - test("can save GitHub check run changes", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByLabel("Check Run Limit").clear(); - await page.getByLabel("Check Run Limit").fill("25"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Check Run Limit")).toHaveValue("25"); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts b/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts deleted file mode 100644 index 6083a3a5bd..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/restart_tasks.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { - selectDatePickerDate, - validateDatePickerDate, - validateToast, -} from "../../helpers"; - -test.describe("restart tasks", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings/restart-tasks"); - }); - - test("can restart tasks", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("restart-tasks-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await selectDatePickerDate( - page, - { year: "2020", month: "Feb", isoDate: "2020-02-01" }, - "start-date-picker", - ); - await validateDatePickerDate(page, "start-date-picker", { - year: "2020", - month: "02", - day: "01", - }); - - await selectDatePickerDate( - page, - { year: "2021", month: "Mar", isoDate: "2021-03-01" }, - "end-date-picker", - ); - await validateDatePickerDate(page, "end-date-picker", { - year: "2021", - month: "03", - day: "01", - }); - - await expect(page.getByTestId("restart-tasks-button")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByTestId("restart-tasks-button").click(); - await expect(page.getByTestId("restart-tasks-modal")).toBeVisible(); - await expect( - page.getByTestId("restart-tasks-list").locator("li"), - ).toHaveCount(4); - await page.getByRole("button", { name: "Confirm" }).click(); - await validateToast(page, "success", "Created job to restart 4 tasks."); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/runners.spec.ts b/apps/spruce/playwright/tests/adminSettings/runners.spec.ts deleted file mode 100644 index dce13ba19c..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/runners.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickCheckbox, selectOption, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("runners", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("can save after making changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - // Notify section. - await page.getByLabel("SES Email").clear(); - await page.getByLabel("SES Email").fill("new_email@email.com"); - - // Task Limits section. - await page.getByLabel("Max Hourly Patch Tasks Per User").clear(); - await page.getByLabel("Max Hourly Patch Tasks Per User").fill("9999"); - - // Host Init section. - await page.getByLabel("Max Total Dynamic Hosts").clear(); - await page.getByLabel("Max Total Dynamic Hosts").fill("8888"); - - // Scheduler section. - await selectOption(page, "Rounding Rule", "Round up"); - - await page.getByLabel("Default Future Host Fraction").clear(); - await page.getByLabel("Default Future Host Fraction").fill("0.6"); - - const groupVersionsCheckbox = page.getByRole("checkbox", { - name: "Group Versions", - }); - await clickCheckbox(groupVersionsCheckbox); - - // Repotracker section. - await page.getByLabel("New Revisions to Fetch").clear(); - await page.getByLabel("New Revisions to Fetch").fill("5"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("SES Email")).toHaveValue( - "new_email@email.com", - ); - await expect( - page.getByLabel("Max Hourly Patch Tasks Per User"), - ).toHaveValue("9999"); - await expect(page.getByLabel("Max Total Dynamic Hosts")).toHaveValue( - "8888", - ); - await expect(page.getByLabel("Rounding Rule")).toContainText("Round up"); - await expect(page.getByLabel("Default Future Host Fraction")).toHaveValue( - "0.6", - ); - await expect(page.getByLabel("Group Versions")).not.toBeChecked(); - await expect(page.getByLabel("New Revisions to Fetch")).toHaveValue("5"); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts b/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts deleted file mode 100644 index a20bd49bf6..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/save_function.spec.ts +++ /dev/null @@ -1,488 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickCheckbox, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("admin settings save properly", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("saves changes in each section independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await page.getByLabel("Config Directory").clear(); - await page.getByLabel("Config Directory").fill("/test/config/dir"); - await page.getByLabel("GitHub Organizations").fill("testorg"); - await page.getByLabel("GitHub Organizations").press("Enter"); - - await page.getByLabel("Finance Formula").clear(); - await page.getByLabel("Finance Formula").fill("0.75"); - - const bucketConfig = page.getByTestId("bucket-config"); - await bucketConfig.getByLabel("Default Log Bucket").clear(); - await bucketConfig.getByLabel("Default Log Bucket").fill("test-logs"); - - const sshPairs = page.getByTestId("ssh-pairs"); - await sshPairs.getByLabel("Name").first().clear(); - await sshPairs.getByLabel("Name").first().fill("test-task-host-key"); - await sshPairs.getByLabel("Secret ARN").first().clear(); - await sshPairs - .getByLabel("Secret ARN") - .first() - .fill("test-task-host-secret-arn"); - - const hostJasper = page.getByTestId("host-jasper"); - await hostJasper.getByLabel("Binary Name").clear(); - await hostJasper.getByLabel("Binary Name").fill("test-jasper"); - - await page.getByLabel("Total Spawn Hosts Per User").clear(); - await page.getByLabel("Total Spawn Hosts Per User").fill("10"); - - await page.getByLabel("Permanently Exempt Hosts").fill("test-host"); - await page.getByLabel("Permanently Exempt Hosts").press("Enter"); - - const tracerConfiguration = page.getByTestId("tracer-configuration"); - const tracerEnabledCheckbox = tracerConfiguration.getByRole("checkbox", { - name: "Enable tracer", - }); - await clickCheckbox(tracerEnabledCheckbox); - await tracerConfiguration.getByLabel("Collector Endpoint").clear(); - await tracerConfiguration - .getByLabel("Collector Endpoint") - .fill("https://test-collector.com"); - await tracerConfiguration.getByLabel("Trace URL Template").clear(); - await tracerConfiguration - .getByLabel("Trace URL Template") - .fill("https://apm.test.com/trace/%s"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(page.getByLabel("Config Directory")).toHaveValue( - "/test/config/dir", - ); - await expect( - page - .getByTestId("misc-settings") - .getByTestId("filter-chip") - .filter({ hasText: "testorg" }), - ).toBeVisible(); - await expect(page.getByLabel("Finance Formula")).toHaveValue("0.75"); - await expect(bucketConfig.getByLabel("Default Log Bucket")).toHaveValue( - "test-logs", - ); - await expect(sshPairs.getByLabel("Name").first()).toHaveValue( - "test-task-host-key", - ); - await expect(sshPairs.getByLabel("Secret ARN").first()).toHaveValue( - "test-task-host-secret-arn", - ); - await expect(hostJasper.getByLabel("Binary Name")).toHaveValue( - "test-jasper", - ); - await expect(page.getByLabel("Total Spawn Hosts Per User")).toHaveValue( - "10", - ); - await expect( - page - .getByTestId("sleep-schedule") - .getByTestId("filter-chip") - .filter({ hasText: "test-host" }), - ).toBeVisible(); - await expect(tracerConfiguration.getByLabel("Enable tracer")).toBeChecked(); - await expect( - tracerConfiguration.getByLabel("Collector Endpoint"), - ).toHaveValue("https://test-collector.com"); - await expect( - tracerConfiguration.getByLabel("Trace URL Template"), - ).toHaveValue("https://apm.test.com/trace/%s"); - - // Modify single section. - await page.getByLabel("Total Project Limit").clear(); - await page.getByLabel("Total Project Limit").fill("200"); - const projectCreationSettings = page.getByTestId( - "project-creation-settings", - ); - await projectCreationSettings - .getByRole("button", { name: "Add repository exception" }) - .click(); - const newProjectCreationException = page - .getByTestId("repo-exceptions-list-item") - .first(); - await newProjectCreationException.getByLabel("Owner").fill("owner"); - await newProjectCreationException.getByLabel("Repository").fill("repo"); - - await save(page); - 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", - ); - await expect( - newProjectCreationException.getByLabel("Repository"), - ).toHaveValue("repo"); - - // Verify other sections unchanged. - await expect(page.getByLabel("Config Directory")).toHaveValue( - "/test/config/dir", - ); - await expect( - page - .getByTestId("misc-settings") - .getByTestId("filter-chip") - .filter({ hasText: "testorg" }), - ).toBeVisible(); - await expect(page.getByLabel("Finance Formula")).toHaveValue("0.75"); - await expect(bucketConfig.getByLabel("Default Log Bucket")).toHaveValue( - "test-logs", - ); - await expect(sshPairs.getByLabel("Name").first()).toHaveValue( - "test-task-host-key", - ); - await expect(sshPairs.getByLabel("Secret ARN").first()).toHaveValue( - "test-task-host-secret-arn", - ); - await expect(hostJasper.getByLabel("Binary Name")).toHaveValue( - "test-jasper", - ); - await expect(page.getByLabel("Total Spawn Hosts Per User")).toHaveValue( - "10", - ); - await expect( - page - .getByTestId("sleep-schedule") - .getByTestId("filter-chip") - .filter({ hasText: "test-host" }), - ).toBeVisible(); - await expect(tracerConfiguration.getByLabel("Enable tracer")).toBeChecked(); - await expect( - tracerConfiguration.getByLabel("Collector Endpoint"), - ).toHaveValue("https://test-collector.com"); - await expect( - tracerConfiguration.getByLabel("Trace URL Template"), - ).toHaveValue("https://apm.test.com/trace/%s"); - }); - - test.describe("save parameter store values independently", () => { - test("saves Okta Client Secret parameter store value independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const okta = page.getByTestId("okta"); - await okta.getByLabel("Client Secret").clear(); - await okta.getByLabel("Client Secret").fill("test-okta-secret"); - - const kanopy = page.getByTestId("kanopy"); - await kanopy.getByLabel("Header Name").clear(); - await kanopy.getByLabel("Header Name").fill("test-header-name"); - await kanopy.getByLabel("Issuer").clear(); - await kanopy.getByLabel("Issuer").fill("test-issuer"); - await kanopy.getByLabel("Keyset URL").clear(); - await kanopy.getByLabel("Keyset URL").fill("test-keyset-url"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(okta.getByLabel("Client Secret")).toHaveValue( - "test-okta-secret", - ); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("Okta param store test"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bannerText).toHaveValue("Okta param store test"); - await expect(okta.getByLabel("Client Secret")).toHaveValue( - "test-okta-secret", - ); - }); - - test("saves Jira Personal Access Token parameter store value independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const jira = page.getByTestId("jira"); - await jira.getByLabel("Personal Access Token").clear(); - await jira.getByLabel("Personal Access Token").fill("test-jira-pat"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(jira.getByLabel("Personal Access Token")).toHaveValue( - "test-jira-pat", - ); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("Jira param store test"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bannerText).toHaveValue("Jira param store test"); - await expect(jira.getByLabel("Personal Access Token")).toHaveValue( - "test-jira-pat", - ); - }); - - test("saves Slack and Splunk token parameter store values independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const slack = page.getByTestId("slack"); - await slack.getByLabel("Token").clear(); - await slack.getByLabel("Token").fill("xoxb-test-slack-token"); - - const splunk = page.getByTestId("splunk"); - await splunk.getByLabel("Token").clear(); - await splunk.getByLabel("Token").fill("test-splunk-token"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(slack.getByLabel("Token")).toHaveValue( - "xoxb-test-slack-token", - ); - await expect(splunk.getByLabel("Token")).toHaveValue("test-splunk-token"); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("Tokens param store test"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bannerText).toHaveValue("Tokens param store test"); - await expect(slack.getByLabel("Token")).toHaveValue( - "xoxb-test-slack-token", - ); - await expect(splunk.getByLabel("Token")).toHaveValue("test-splunk-token"); - }); - - test("saves Runtime Environments API Key parameter store value independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const runtimeEnv = page.getByTestId("runtime-environments"); - await runtimeEnv.getByLabel("API Key").clear(); - await runtimeEnv.getByLabel("API Key").fill("test-runtime-env-key"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(runtimeEnv.getByLabel("API Key")).toHaveValue( - "test-runtime-env-key", - ); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("Runtime Environments param store test"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bannerText).toHaveValue( - "Runtime Environments param store test", - ); - await expect(runtimeEnv.getByLabel("API Key")).toHaveValue( - "test-runtime-env-key", - ); - }); - - test("saves AWS EC2 Keys parameter store values independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const awsConfiguration = page.getByTestId("aws-configuration"); - await awsConfiguration.getByLabel("EC2 Key").clear(); - await awsConfiguration.getByLabel("EC2 Key").fill("test-ec2-key"); - await awsConfiguration.getByLabel("EC2 Secret").clear(); - await awsConfiguration.getByLabel("EC2 Secret").fill("test-ec2-secret"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(awsConfiguration.getByLabel("EC2 Key")).toHaveValue( - "test-ec2-key", - ); - await expect(awsConfiguration.getByLabel("EC2 Secret")).toHaveValue( - "test-ec2-secret", - ); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("AWS EC2 param store test"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bannerText).toHaveValue("AWS EC2 param store test"); - await expect(awsConfiguration.getByLabel("EC2 Key")).toHaveValue( - "test-ec2-key", - ); - await expect(awsConfiguration.getByLabel("EC2 Secret")).toHaveValue( - "test-ec2-secret", - ); - }); - - test("saves S3 Keys parameter store values independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const bucketConfig = page.getByTestId("bucket-config"); - await bucketConfig.getByLabel("S3 Key").clear(); - await bucketConfig.getByLabel("S3 Key").fill("test-s3-key"); - await bucketConfig.getByLabel("S3 Secret").clear(); - await bucketConfig.getByLabel("S3 Secret").fill("test-s3-secret"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bucketConfig.getByLabel("S3 Key")).toHaveValue( - "test-s3-key", - ); - await expect(bucketConfig.getByLabel("S3 Secret")).toHaveValue( - "test-s3-secret", - ); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("S3 Keys param store test"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bannerText).toHaveValue("S3 Keys param store test"); - await expect(bucketConfig.getByLabel("S3 Key")).toHaveValue( - "test-s3-key", - ); - await expect(bucketConfig.getByLabel("S3 Secret")).toHaveValue( - "test-s3-secret", - ); - }); - - test("saves GitHub Webhook Secret parameter store value independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const miscSettings = page.getByTestId("misc-settings"); - await miscSettings.getByLabel("Webhook Secret").clear(); - await miscSettings - .getByLabel("Webhook Secret") - .fill("test-webhook-secret"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect( - page.getByTestId("misc-settings").getByLabel("Webhook Secret"), - ).toHaveValue("test-webhook-secret"); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("GitHub Webhook Secret param store test"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bannerText).toHaveValue( - "GitHub Webhook Secret param store test", - ); - await expect( - page.getByTestId("misc-settings").getByLabel("Webhook Secret"), - ).toHaveValue("test-webhook-secret"); - }); - - test("saves Expansions List values independently", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const expansionsList = page.getByTestId("expansions-list"); - await expansionsList.getByRole("button", { name: "Add" }).click(); - const newExpansionsRow = page.getByTestId("expansions-list-item").first(); - await newExpansionsRow.getByLabel("Key").fill("TEST_KEY"); - await newExpansionsRow.getByLabel("Value").fill("test_value"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(newExpansionsRow.getByLabel("Key")).toHaveValue("TEST_KEY"); - await expect(newExpansionsRow.getByLabel("Value")).toHaveValue( - "test_value", - ); - - const bannerText = page.getByTestId("banner-text"); - await bannerText.clear(); - await bannerText.fill("Expansions List test"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(bannerText).toHaveValue("Expansions List test"); - await expect(newExpansionsRow.getByLabel("Key")).toHaveValue("TEST_KEY"); - await expect(newExpansionsRow.getByLabel("Value")).toHaveValue( - "test_value", - ); - }); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts b/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts deleted file mode 100644 index a478abe299..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/service_flags.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickCheckbox, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("service flags", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings/service-flags"); - }); - - test("can interact with and save service flags", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - const checkedCheckboxes = page.locator( - 'input[type="checkbox"][aria-checked="true"]', - ); - const initialCheckedCount = await checkedCheckboxes.count(); - - // Find the first unchecked checkbox and capture a stable locator by id. - const uncheckedCheckbox = page - .locator('input[type="checkbox"]:not([aria-checked="true"])') - .first(); - const checkboxId = await uncheckedCheckbox.getAttribute("id"); - const checkbox = page.locator(`#${checkboxId}`); - - await clickCheckbox(checkbox); - - // Verify checked count increased. - await expect(checkedCheckboxes).toHaveCount(initialCheckedCount + 1); - - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "false", - ); - - // Uncheck it again. - await clickCheckbox(checkbox); - await expect(checkedCheckboxes).toHaveCount(initialCheckedCount); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - // Re-check and save. - await clickCheckbox(checkbox); - await save(page); - await validateToast(page, "success", "Service flags saved successfully"); - await page.reload(); - - await expect(checkbox).toHaveAttribute("aria-checked", "true"); - await expect(checkedCheckboxes).toHaveCount(initialCheckedCount + 1); - - // Restore original state. - await clickCheckbox(checkbox); - await save(page); - await validateToast(page, "success", "Service flags saved successfully"); - }); -}); diff --git a/apps/spruce/playwright/tests/adminSettings/utils.ts b/apps/spruce/playwright/tests/adminSettings/utils.ts deleted file mode 100644 index 87171d7b5f..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -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(); -}; diff --git a/apps/spruce/playwright/tests/adminSettings/web.spec.ts b/apps/spruce/playwright/tests/adminSettings/web.spec.ts deleted file mode 100644 index a3b9738c53..0000000000 --- a/apps/spruce/playwright/tests/adminSettings/web.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("web", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/admin-settings"); - }); - - test("can save after making changes", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - // API section. - const apiSection = page.getByTestId("api-settings"); - await apiSection.getByLabel("HTTP Listen Address").clear(); - await apiSection - .getByLabel("HTTP Listen Address") - .fill("http://example.com/api"); - - // UI section. - const uiSection = page.getByTestId("ui-settings"); - await uiSection.getByLabel("UIv2 URL").clear(); - await uiSection.getByLabel("UIv2 URL").fill("http://example.com/ui"); - - // 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(); - await disabledGQLQueriesSection - .getByLabel("Disabled GraphQL Queries") - .fill("query1"); - await disabledGQLQueriesSection - .getByLabel("Disabled GraphQL Queries") - .press("Enter"); - - await save(page); - await validateToast(page, "success", "Settings saved successfully"); - await page.reload(); - - await expect(apiSection.getByLabel("HTTP Listen Address")).toHaveValue( - "http://example.com/api", - ); - await expect(uiSection.getByLabel("UIv2 URL")).toHaveValue( - "http://example.com/ui", - ); - await expect( - disabledGQLQueriesSection - .getByTestId("filter-chip") - .filter({ hasText: "query1" }), - ).toBeVisible(); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts deleted file mode 100644 index 9d22456954..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/general_section.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickCheckbox, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("general section", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/localhost/settings/general"); - await expect(page.getByTestId("distro-settings-page")).toBeVisible(); - }); - - test("can update fields and those changes will persist", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await expect( - page.getByRole("button", { name: "Add alias" }), - ).toHaveAttribute("aria-disabled", "false"); - await page.getByRole("button", { name: "Add alias" }).click(); - await page.getByLabel("Alias").fill("localhost-alias"); - await page.getByLabel("Notes").fill("this is a note"); - await page.getByLabel("Warnings").fill("this is a warning"); - const disableShallowCloneCheckbox = page.getByRole("checkbox", { - name: "Disable shallow clone for this distro", - }); - await clickCheckbox(disableShallowCloneCheckbox); - const adminOnlyCheckbox = page.getByRole("checkbox", { - name: "Admin only", - }); - await clickCheckbox(adminOnlyCheckbox); - await save(page); - await validateToast(page, "success", "Updated distro."); - - await page.reload(); - await expect(page.getByTestId("distro-settings-page")).toBeVisible(); - await expect(page.getByLabel("Alias")).toHaveValue("localhost-alias"); - await expect(page.getByLabel("Notes")).toHaveValue("this is a note"); - await expect(page.getByLabel("Warnings")).toHaveValue("this is a warning"); - await expect( - page.getByRole("checkbox", { - name: "Disable shallow clone for this distro", - }), - ).toBeChecked(); - await expect( - page.getByRole("checkbox", { name: "Admin only" }), - ).toBeChecked(); - - await page.getByTestId("delete-item-button").click(); - await page.getByLabel("Notes").clear(); - await page.getByLabel("Warnings").clear(); - await clickCheckbox(disableShallowCloneCheckbox); - await clickCheckbox(adminOnlyCheckbox); - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - - test.describe("container pool distro", () => { - test("warns users that the distro will not be spawned for tasks", async ({ - authenticatedPage: page, - }) => { - await page.goto("/distro/ubuntu1604-parent/settings/general"); - await expect( - page.getByText( - "Distro is a container pool, so it cannot be spawned for tasks.", - ), - ).toBeVisible(); - }); - }); - - test.describe("single task distro", () => { - test("can toggle a distro as single task distro and shows a warning banner that dismisses on save", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("single-task-banner")).toHaveCount(0); - - await page - .getByLabel("Set distro as Single Task Distro") - .scrollIntoViewIfNeeded(); - await expect( - page.getByRole("checkbox", { - name: "Set distro as Single Task Distro", - }), - ).toHaveAttribute("aria-disabled", "false"); - const singleTaskCheckbox = page.getByRole("checkbox", { - name: "Set distro as Single Task Distro", - }); - await clickCheckbox(singleTaskCheckbox); - await expect(page.getByTestId("single-task-banner")).toContainText( - "This Distro will be converted to a Single Task Distro once saved. Please review before confirming.", - ); - await save(page); - await expect(page.getByTestId("single-task-banner")).toHaveCount(0); - await validateToast(page, "success", "Updated distro."); - - await page.reload(); - await expect( - page.getByRole("checkbox", { - name: "Set distro as Single Task Distro", - }), - ).toBeChecked(); - await expect(page.getByTestId("single-task-banner")).toHaveCount(0); - - await page - .getByLabel("Set distro as Single Task Distro") - .scrollIntoViewIfNeeded(); - await expect( - page.getByRole("checkbox", { - name: "Set distro as Single Task Distro", - }), - ).toHaveAttribute("aria-disabled", "false"); - await clickCheckbox(singleTaskCheckbox); - await expect(page.getByTestId("single-task-banner")).toContainText( - "This Distro will no longer be a Single Task Distro once saved. Please review before confirming.", - ); - await save(page); - await expect(page.getByTestId("single-task-banner")).toHaveCount(0); - await validateToast(page, "success", "Updated distro."); - }); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts deleted file mode 100644 index b9337c768c..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/host_section.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { selectOption, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("host section", () => { - test.describe("using legacy ssh", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/localhost/settings/host"); - }); - - test("shows the correct fields when distro has static provider", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("authorized-keys-input")).toBeVisible(); - await expect(page.getByTestId("minimum-hosts-input")).toHaveCount(0); - await expect(page.getByTestId("maximum-hosts-input")).toHaveCount(0); - await expect(page.getByTestId("idle-time-input")).toHaveCount(0); - await expect(page.getByTestId("future-fraction-input")).toHaveCount(0); - await expect(page.getByTestId("rounding-rule-select")).toHaveCount(0); - await expect(page.getByTestId("feedback-rule-select")).toHaveCount(0); - }); - - test("shows an error when selecting an incompatible host communication method", async ({ - authenticatedPage: page, - }) => { - await selectOption(page, "Host Communication Method", "RPC"); - await expect( - page.getByText( - "Legacy and non-legacy bootstrapping and communication are incompatible.", - ), - ).toBeVisible(); - }); - - test("updates host fields", async ({ authenticatedPage: page }) => { - await selectOption(page, "Agent Architecture", "Linux ARM 64-bit"); - await page.getByLabel("Working Directory").clear(); - await page.getByLabel("Working Directory").fill("/usr/local/bin"); - await page.getByLabel("SSH User").clear(); - await page.getByLabel("SSH User").fill("sudo"); - await page.getByRole("button", { name: "Add SSH option" }).click(); - await page.getByLabel("SSH Option").fill("BatchMode=yes"); - await selectOption( - page, - "Host Overallocation Rule", - "Terminate hosts when overallocated", - ); - - await save(page); - await validateToast(page, "success", "Updated distro."); - - await selectOption(page, "Agent Architecture", "Linux 64-bit"); - await page.getByLabel("Working Directory").clear(); - await page.getByLabel("Working Directory").fill("/home/ubuntu/smoke"); - await page.getByLabel("SSH User").clear(); - await page.getByLabel("SSH User").fill("ubuntu"); - await page.getByTestId("delete-item-button").click(); - await selectOption(page, "Host Overallocation Rule", "Default"); - - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - - test("updates mountpoints", async ({ authenticatedPage: page }) => { - await expect( - page.getByRole("button", { name: "Add mountpoint" }), - ).toHaveAttribute("aria-disabled", "false"); - await page.getByRole("button", { name: "Add mountpoint" }).click(); - await page.getByLabel("Mountpoint").fill("/data"); - - await save(page); - await validateToast(page, "success", "Updated distro."); - - await page.getByTestId("delete-item-button").click(); - - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - }); - - test.describe("using User Data bootstrap method", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/ubuntu1604-parent/settings/host"); - await selectOption(page, "Host Bootstrap Method", "User Data"); - await selectOption(page, "Host Communication Method", "RPC"); - }); - - test("shows Windows-only fields when the architecture is updated", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByLabel("Root Directory")).toHaveCount(0); - await expect(page.getByLabel("Service User")).toHaveCount(0); - - await selectOption(page, "Agent Architecture", "Windows 64-bit"); - - await expect(page.getByLabel("Root Directory")).toBeVisible(); - await expect(page.getByLabel("Service User")).toBeVisible(); - }); - - test("hides resource limit fields when the architecture is not Linux", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByText("Resource Limits")).toBeVisible(); - await selectOption(page, "Agent Architecture", "Windows 64-bit"); - await expect(page.getByText("Resource Limits")).toHaveCount(0); - }); - - test("saves bootstrap settings", async ({ authenticatedPage: page }) => { - await page.getByLabel("Jasper Binary Directory").fill("/jasper/binary"); - await page - .getByLabel("Jasper Credentials Path") - .fill("/jasper/credentials"); - await page.getByLabel("Client Directory").fill("/client/dir"); - await page.getByLabel("Shell Path").fill("/shell/path"); - await page - .getByLabel("Home Volume Format Command") - .fill("echo 'Hello World'"); - await page.getByLabel("Number of Files").fill("10"); - await page.getByLabel("Number of CGroup Tasks").fill("20"); - await page.getByLabel("Number of Processes").fill("30"); - await page.getByLabel("Locked Memory").fill("128"); - await page.getByLabel("Virtual Memory").fill("256"); - - await page.getByRole("button", { name: "Add variable" }).click(); - await page.getByLabel("Key").fill("my-key"); - await page.getByLabel("Value").fill("my-value"); - - await page.getByRole("button", { name: "Add script" }).click(); - await page.getByLabel(/^Path$/).fill("/path/to/precondition/script"); - await page.getByLabel(/^Script$/).fill("script contents here"); - - await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts b/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts deleted file mode 100644 index 2e8288363f..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/navigation.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { selectOption } from "../../helpers"; - -test.describe("distroSettings/navigation", () => { - test.describe("using the distro dropdown", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/localhost/settings"); - }); - - test("navigates to distro when clicked", async ({ - authenticatedPage: page, - }) => { - await page.getByTestId("distro-select").click(); - const listbox = page.getByRole("listbox"); - await expect(listbox.getByText("Admin-only")).toBeVisible(); - await expect(listbox.getByRole("option").last()).toContainText( - "localhost2", - ); - await listbox.getByText("rhel71-power8-large").click(); - await expect(page).not.toHaveURL(/\/distro\/localhost/); - await expect(page).toHaveURL(/\/distro\/rhel71-power8-large/); - }); - - test("can navigate to the task queue for the selected distro", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("navitem-task-queue-link")).toHaveAttribute( - "href", - "/task-queue/localhost", - ); - }); - - test("can navigate to the image build information for the selected distro", async ({ - authenticatedPage: page, - }) => { - await expect( - page.getByTestId("navitem-image-build-information-link"), - ).toHaveAttribute("href", "/image/ubuntu2204/build-information"); - }); - - test("can navigate to the image event log for the selected distro", async ({ - authenticatedPage: page, - }) => { - await expect( - page.getByTestId("navitem-image-event-log-link"), - ).toHaveAttribute("href", "/image/ubuntu2204/event-log"); - }); - - test.describe("warning modal", () => { - test("warns when navigating away with unsaved changes and allows returning", async ({ - authenticatedPage: page, - }) => { - await page.getByLabel("Notes").fill("my note"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByTestId("waterfall-link").click(); - await expect( - page.getByTestId("navigation-warning-modal"), - ).toBeVisible(); - await page.getByRole("button", { name: "Cancel" }).click(); - await expect(page.getByTestId("navigation-warning-modal")).toHaveCount( - 0, - ); - await expect(page).toHaveURL("/distro/localhost/settings/general"); - }); - - test.describe("modifying the distro provider", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto( - "/distro/ubuntu1604-container-test/settings/provider", - ); - }); - - test("warns when navigating to another tab after provider changed and allows save", async ({ - authenticatedPage: page, - }) => { - await selectOption(page, "Provider", "Static"); - await expect( - page.getByTestId("save-settings-button"), - ).toHaveAttribute("aria-disabled", "false"); - await page.getByRole("link", { name: "Task Settings" }).click(); - await expect( - page.getByTestId("provider-warning-banner"), - ).toBeVisible(); - }); - - test("shows the standard save warning modal when non-provider fields have changed", async ({ - authenticatedPage: page, - }) => { - await page.getByTestId("user-data-input").fill("test user data"); - await expect( - page.getByTestId("save-settings-button"), - ).toHaveAttribute("aria-disabled", "false"); - await page.getByTestId("waterfall-link").click(); - await expect( - page.getByTestId("navigation-warning-modal"), - ).toBeVisible(); - await expect(page.getByTestId("provider-warning-banner")).toHaveCount( - 0, - ); - }); - }); - }); - }); - - test.describe("/distros redirect route", () => { - test("should redirect to the first distro available", async ({ - authenticatedPage: page, - }) => { - await page.goto("/distros"); - await expect(page).not.toHaveURL(/\/distros$/); - await expect(page).toHaveURL("/distro/archlinux-test/settings/general"); - }); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts b/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts deleted file mode 100644 index 27eb136bc8..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/new_distro.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; - -const distroSettingPage = "/distro/rhel71-power8-large/settings/general"; - -test.describe("Creating a new distro", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto(distroSettingPage); - }); - - test("allows a user to create a new distro", async ({ - authenticatedPage: page, - }) => { - const newDistroId = "new-distro"; - - await page.getByTestId("new-distro-button").click(); - await expect(page.getByTestId("new-distro-menu")).toBeVisible(); - await page.getByTestId("create-distro-button").click(); - await expect(page.getByTestId("create-distro-modal")).toBeVisible(); - await page.getByTestId("distro-id-input").fill(newDistroId); - await page.getByRole("button", { name: "Create" }).click(); - await validateToast( - page, - "success", - `Created distro “${newDistroId}”`, - true, - ); - await expect(page).toHaveURL(`/distro/${newDistroId}/settings/general`); - - await expect(page.getByTestId("delete-distro-button")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByTestId("delete-distro-button").click(); - await expect(page.getByTestId("delete-distro-modal")).toBeVisible(); - await page - .getByTestId("delete-distro-modal") - .locator("input") - .fill(newDistroId); - await page.getByRole("button", { name: "Delete", exact: true }).click(); - await validateToast( - page, - "success", - `The distro “${newDistroId}” was deleted.`, - ); - }); -}); - -test.describe("Copying a distro", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto(distroSettingPage); - }); - - test("allows a user to copy an existing distro", async ({ - authenticatedPage: page, - }) => { - const copyDistroId = "copy-distro"; - - await page.getByTestId("new-distro-button").click(); - await expect(page.getByTestId("new-distro-menu")).toBeVisible(); - await page.getByTestId("copy-distro-button").click(); - await expect(page.getByTestId("copy-distro-modal")).toBeVisible(); - await page.getByTestId("distro-id-input").fill(copyDistroId); - await page.getByRole("button", { name: "Duplicate" }).click(); - await validateToast( - page, - "success", - `Created distro “${copyDistroId}”`, - true, - ); - await expect(page).toHaveURL(`/distro/${copyDistroId}/settings/general`); - - await expect(page.getByTestId("delete-distro-button")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByTestId("delete-distro-button").click(); - await expect(page.getByTestId("delete-distro-modal")).toBeVisible(); - await page - .getByTestId("delete-distro-modal") - .locator("input") - .fill(copyDistroId); - await page.getByRole("button", { name: "Delete", exact: true }).click(); - await validateToast( - page, - "success", - `The distro “${copyDistroId}” was deleted.`, - ); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/on_save_operations.spec.ts b/apps/spruce/playwright/tests/distroSettings/on_save_operations.spec.ts deleted file mode 100644 index 4e503ad454..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/on_save_operations.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { test } from "../../fixtures"; -import { validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("using an on-save operation", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/localhost/settings/general"); - }); - - test("notes how many hosts were updated in the resulting toast", async ({ - authenticatedPage: page, - }) => { - await page.getByLabel("Notes").fill("My note"); - await save(page, "DECOMMISSION"); - await validateToast( - page, - "success", - "Updated distro and scheduled 0 hosts to update.", - true, - ); - await page.getByLabel("Notes").clear(); - await save(page, "RESTART_JASPER"); - await validateToast( - page, - "success", - "Updated distro and scheduled 0 hosts to update.", - ); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/permissions.spec.ts b/apps/spruce/playwright/tests/distroSettings/permissions.spec.ts deleted file mode 100644 index bbcd74f9fa..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/permissions.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { users } from "@evg-ui/playwright-config/constants"; -import { test, expect } from "../../fixtures"; -import { login, logout } from "../../helpers"; - -const distroRoute = "/distro/rhel71-power8-large/settings/general"; - -test.describe("distro permissions", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await logout(page); - }); - - test("hides the new distro button when a user cannot create distros", async ({ - authenticatedPage: page, - }) => { - await login(page, users.privileged); - await page.goto(distroRoute); - await expect(page.getByTestId("new-distro-button")).toHaveCount(0); - await expect(page.getByTestId("delete-distro-button")).toHaveAttribute( - "aria-disabled", - "false", - ); - await expect(page.locator("textarea").first()).toBeEnabled(); - }); - - test("disables the delete button when user lacks admin permissions", async ({ - authenticatedPage: page, - }) => { - await login(page, users.regular); - await page.goto(distroRoute); - await expect(page.getByTestId("delete-distro-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - - test("disables fields when user lacks edit permissions", async ({ - authenticatedPage: page, - }) => { - await login(page, users.regular); - await page.goto(distroRoute); - const settingsPage = page.getByTestId("distro-settings-page"); - await expect( - settingsPage.locator('input[type="checkbox"]').first(), - ).toHaveAttribute("aria-disabled", "true"); - await expect(settingsPage.locator("textarea").first()).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - - test("enables fields if user has edit permissions for a particular distro", async ({ - authenticatedPage: page, - }) => { - await login(page, users.regular); - await page.goto("/distro/localhost/settings/general"); - const settingsPage = page.getByTestId("distro-settings-page"); - await expect( - settingsPage.locator('input[type="checkbox"]').first(), - ).toHaveAttribute("aria-disabled", "false"); - await expect(settingsPage.locator("textarea").first()).toHaveAttribute( - "aria-disabled", - "false", - ); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts deleted file mode 100644 index 0086d330de..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/project_section.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("project section", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/localhost/settings/project"); - }); - - test("can update fields and those changes will persist", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await expect( - page.getByRole("button", { name: "Add expansion" }), - ).toHaveAttribute("aria-disabled", "false"); - await page.getByRole("button", { name: "Add expansion" }).click(); - - const newExpansion = page.getByTestId("expansions-list-item"); - await newExpansion.getByLabel("Key").fill("key-name"); - await newExpansion.getByLabel("Value").fill("my-value"); - - await page.getByRole("button", { name: "Add project" }).click(); - await page.getByLabel("Project ID").fill("spruce"); - - await save(page); - await validateToast(page, "success", "Updated distro."); - - await page.reload(); - await expect(newExpansion.getByLabel("Key")).toHaveValue("key-name"); - await expect(newExpansion.getByLabel("Value")).toHaveValue("my-value"); - await expect(page.getByLabel("Project ID")).toHaveValue("spruce"); - - await page.getByTestId("delete-item-button").first().click(); - await page.getByTestId("delete-item-button").first().click(); - await save(page); - await validateToast(page, "success", "Updated distro."); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts deleted file mode 100644 index 96fadeb604..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/provider_section.spec.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickCheckbox, selectOption, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("provider section", () => { - test.describe("static", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/localhost/settings/provider"); - }); - - test("successfully updates static provider fields", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("provider-select")).toContainText( - "Static IP/VM", - ); - await expect(page.getByTestId("static-provider-settings")).toBeVisible(); - - await page.getByTestId("user-data-input").fill("my user data"); - const mergeUserDataCheckbox = page.getByLabel( - "Merge with existing user data", - ); - await clickCheckbox(mergeUserDataCheckbox); - await page.getByRole("button", { name: "Add security group" }).click(); - await page.getByLabel("Security Group ID").fill("sg-1234"); - await page.getByRole("button", { name: "Add host" }).click(); - await page.getByLabel("Name").fill("host-1234"); - await save(page); - await validateToast(page, "success", "Updated distro.", true); - - await page.getByTestId("user-data-input").clear(); - await clickCheckbox(mergeUserDataCheckbox); - await page.getByTestId("delete-item-button").first().click(); - await page.getByTestId("delete-item-button").first().click(); - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - }); - - test.describe("docker", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/ubuntu1604-container-test/settings/provider"); - }); - - 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( - "placeholder", - /test-pool-1/, - ); - await selectOption(page, "Container Pool ID", "test-pool-2"); - await expect(page.getByLabel("Pool Mapping Information")).toHaveAttribute( - "placeholder", - /test-pool-2/, - ); - }); - - test("successfully updates docker provider fields", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("provider-select")).toContainText("Docker"); - await expect(page.getByTestId("docker-provider-settings")).toBeVisible(); - - await selectOption(page, "Image Build Method", "Pull"); - await selectOption(page, "Container Pool ID", "test-pool-2"); - await page.getByLabel("Username for Registries").fill("username"); - await page.getByLabel("Password for Registries").fill("password"); - await page.getByTestId("user-data-input").fill("my user data"); - const mergeUserDataCheckbox = page.getByLabel( - "Merge with existing user data", - ); - await clickCheckbox(mergeUserDataCheckbox); - await save(page); - await validateToast(page, "success", "Updated distro.", true); - - await selectOption(page, "Image Build Method", "Import"); - await selectOption(page, "Container Pool ID", "test-pool-1"); - await page.getByLabel("Username for Registries").clear(); - await page.getByLabel("Password for Registries").clear(); - await page.getByTestId("user-data-input").clear(); - await clickCheckbox(mergeUserDataCheckbox); - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - }); - - test.describe("ec2 fleet", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/ubuntu1804-workstation/settings/provider"); - await expect( - page.getByTestId("ec2-fleet-provider-settings"), - ).toBeVisible(); - await expect( - page - .getByTestId("expandable-card-title") - .filter({ hasText: "us-east-1" }), - ).toBeVisible(); - }); - - test("shows and hides fields correctly", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("use-vpc")).toBeChecked(); - await expect(page.getByText("Default VPC Subnet ID")).toBeVisible(); - await expect(page.getByText("VPC Subnet Prefix")).toBeVisible(); - - const useVpcCheckbox = page.getByRole("checkbox", { - name: "Use security groups in an EC2 VPC", - }); - await clickCheckbox(useVpcCheckbox); - await expect(page.getByTestId("use-vpc")).not.toBeChecked(); - await expect(page.getByText("Default VPC Subnet ID")).toHaveCount(0); - await expect(page.getByText("VPC Subnet Prefix")).toHaveCount(0); - }); - - test("successfully updates ec2 fleet provider fields", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("provider-select")).toContainText( - "EC2 Fleet", - ); - await expect( - page.getByTestId("ec2-fleet-provider-settings"), - ).toBeVisible(); - await expect(page.getByTestId("region-select")).toContainText( - "us-east-1", - ); - - await selectOption(page, "Region", "us-west-1"); - await page.getByLabel("SSH Key Name").clear(); - await page.getByLabel("SSH Key Name").fill("my ssh key"); - await page.getByRole("button", { name: "Add mount point" }).click(); - await page.getByLabel("Device Name").fill("device name"); - await page.getByLabel("Size").fill("200"); - await save(page); - await validateToast(page, "success", "Updated distro.", true); - - await selectOption(page, "Region", "us-east-1"); - await page.getByLabel("SSH Key Name").clear(); - await page.getByLabel("SSH Key Name").fill("mci"); - await page - .getByTestId("mount-points") - .getByTestId("delete-item-button") - .click(); - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - - test("can add and delete region settings", async ({ - authenticatedPage: page, - }) => { - await expect( - page - .getByTestId("expandable-card-title") - .filter({ hasText: "us-east-1" }), - ).toBeVisible(); - - await page.getByRole("button", { name: "Add region settings" }).click(); - await expect( - page.getByRole("button", { name: "Add region settings", exact: true }), - ).toHaveCount(0); - - const newExpandableCard = page.getByTestId("expandable-card").first(); - await expect( - newExpandableCard - .getByTestId("expandable-card-title") - .filter({ hasText: "New AWS Region" }), - ).toBeVisible(); - - await selectOption(newExpandableCard, "Region", "us-west-1"); - await newExpandableCard.getByLabel("EC2 AMI ID").fill("ami-1234"); - await newExpandableCard.getByLabel("Instance Type").fill("m5.xlarge"); - await newExpandableCard - .getByRole("button", { name: "Add security group" }) - .click(); - await newExpandableCard.getByLabel("Security Group ID").fill("sg-5678"); - await save(page); - await validateToast(page, "success", "Updated distro.", true); - - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - await expect( - page - .getByTestId("expandable-card-title") - .filter({ hasText: "us-west-1" }), - ).toBeVisible(); - - await page - .getByTestId("expandable-card-title") - .filter({ hasText: "us-west-1" }) - .locator("..") - .getByTestId("delete-item-button") - .first() - .click(); - await expect(page.getByTestId("expandable-card")).toHaveCount(1); - await expect( - page - .getByTestId("expandable-card-title") - .filter({ hasText: "us-west-1" }), - ).toHaveCount(0); - await save(page); - await validateToast(page, "success", "Updated distro."); - await expect( - page.getByRole("button", { name: "Add region settings" }), - ).toBeVisible(); - }); - }); - - test.describe("ec2 on-demand", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/ubuntu1604-parent/settings/provider"); - await expect( - page.getByTestId("ec2-on-demand-provider-settings"), - ).toBeVisible(); - }); - - test("shows and hides fields correctly", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("use-vpc")).toBeChecked(); - await expect(page.getByText("Default VPC Subnet ID")).toBeVisible(); - await expect(page.getByText("VPC Subnet Prefix")).toBeVisible(); - - const useVpcCheckbox = page.getByRole("checkbox", { - name: "Use security groups in an EC2 VPC", - }); - await clickCheckbox(useVpcCheckbox); - await expect(page.getByText("Default VPC Subnet ID")).toHaveCount(0); - await expect(page.getByText("VPC Subnet Prefix")).toHaveCount(0); - }); - - test("successfully updates ec2 on-demand provider fields", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("provider-select")).toContainText( - "EC2 On-Demand", - ); - await expect( - page.getByTestId("ec2-on-demand-provider-settings"), - ).toBeVisible(); - await expect(page.getByTestId("region-select")).toContainText( - "us-east-1", - ); - - await selectOption(page, "Region", "us-west-1"); - await page.getByLabel("EC2 AMI ID").clear(); - await page.getByLabel("EC2 AMI ID").fill("ami-1234560"); - await expect(page.getByLabel("SSH Key Name")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByLabel("SSH Key Name").clear(); - await page.getByLabel("SSH Key Name").fill("my ssh key"); - await page - .getByTestId("user-data-input") - .fill(""); - - const mergeUserDataCheckbox = page.getByRole("checkbox", { - name: "Merge with existing user data", - }); - await clickCheckbox(mergeUserDataCheckbox); - await save(page); - await validateToast(page, "success", "Updated distro.", true); - - await clickCheckbox(mergeUserDataCheckbox); - await save(page); - await validateToast(page, "success", "Updated distro.", true); - - await selectOption(page, "Region", "us-east-1"); - await page.getByLabel("EC2 AMI ID").clear(); - await page.getByLabel("EC2 AMI ID").fill("ami-0000"); - await page.getByLabel("SSH Key Name").clear(); - await page.getByLabel("SSH Key Name").fill("mci"); - await page.getByTestId("user-data-input").clear(); - await clickCheckbox(mergeUserDataCheckbox); - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - - test("can add and delete region settings", async ({ - authenticatedPage: page, - }) => { - await page.getByRole("button", { name: "Add region settings" }).click(); - await expect( - page.getByRole("button", { name: "Add region settings", exact: true }), - ).toHaveCount(0); - - const newExpandableCard = page.getByTestId("expandable-card").first(); - await expect( - newExpandableCard - .getByTestId("expandable-card-title") - .filter({ hasText: "New AWS Region" }), - ).toBeVisible(); - - await selectOption(newExpandableCard, "Region", "us-west-1"); - await newExpandableCard.getByLabel("EC2 AMI ID").fill("ami-1234"); - await newExpandableCard.getByLabel("Instance Type").fill("m5.xlarge"); - await newExpandableCard - .getByRole("button", { name: "Add security group" }) - .click(); - await newExpandableCard.getByLabel("Security Group ID").fill("sg-0000"); - await save(page); - await validateToast(page, "success", "Updated distro.", true); - - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - await expect( - page - .getByTestId("expandable-card-title") - .filter({ hasText: "us-west-1" }), - ).toBeVisible(); - - await page - .getByTestId("expandable-card-title") - .filter({ hasText: "us-west-1" }) - .locator("..") - .getByTestId("delete-item-button") - .first() - .click(); - await expect(page.getByTestId("expandable-card")).toHaveCount(1); - await expect( - page - .getByTestId("expandable-card-title") - .filter({ hasText: "us-west-1" }), - ).toHaveCount(0); - await save(page); - await validateToast(page, "success", "Updated distro."); - await expect( - page.getByRole("button", { name: "Add region settings" }), - ).toBeVisible(); - }); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/single_task_distro.spec.ts b/apps/spruce/playwright/tests/distroSettings/single_task_distro.spec.ts deleted file mode 100644 index b0f2074ffb..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/single_task_distro.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { test, expect } from "../../fixtures"; - -test.describe("single task distro", () => { - test("should render allowed projects and tasks", async ({ - authenticatedPage: page, - }) => { - await page.goto("/distro/archlinux-test/settings"); - await page.getByTestId("navitem-single-task-distros").click(); - await expect(page).toHaveURL( - "/distro/archlinux-test/settings/single-task-distros", - ); - - const cards = page.getByTestId("expandable-card-title"); - await expect(cards).toHaveCount(2); - await expect(cards.nth(0)).toHaveText("evergreen"); - await expect(cards.nth(1)).toHaveText("spruce"); - - await cards.nth(0).click(); - const inputs = page.getByTestId("expandable-card").locator("input"); - await expect(inputs.nth(0)).toHaveValue("evergreen"); - await expect(inputs.nth(1)).toHaveValue("compile"); - await expect(inputs.nth(2)).toHaveValue("test"); - await expect(inputs.nth(3)).toHaveValue("ubuntu1604"); - await expect(inputs.nth(4)).toHaveValue("windows"); - - await cards.nth(1).click(); - await expect(inputs.nth(5)).toHaveValue("spruce"); - await expect(inputs.nth(6)).toHaveValue("lint"); - await expect(inputs.nth(7)).toHaveValue("storybook"); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts b/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts deleted file mode 100644 index 248fa68a1e..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/task_section.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { selectOption, validateToast } from "../../helpers"; -import { save } from "./utils"; - -test.describe("task section", () => { - test.describe("static provider", () => { - test("should not show tunable options", async ({ - authenticatedPage: page, - }) => { - await page.goto("/distro/localhost/settings/task"); - await selectOption(page, "Task Planner Version", "Tunable"); - await expect(page.getByTestId("tunable-options")).toHaveCount(0); - }); - }); - - test.describe("docker provider", () => { - test("should not show tunable options", async ({ - authenticatedPage: page, - }) => { - await page.goto("/distro/ubuntu1604-container-test/settings/task"); - await selectOption(page, "Task Planner Version", "Tunable"); - await expect(page.getByTestId("tunable-options")).toHaveCount(0); - }); - }); - - test.describe("ec2 provider", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto("/distro/ubuntu1804-workstation/settings/task"); - }); - - test("should only show tunable options if planner version is tunable", async ({ - authenticatedPage: page, - }) => { - await selectOption(page, "Task Planner Version", "Tunable"); - await expect(page.getByTestId("tunable-options")).toBeVisible(); - }); - - test("should surface warnings for invalid number inputs", async ({ - authenticatedPage: page, - }) => { - await selectOption(page, "Task Planner Version", "Tunable"); - await page.getByLabel("Patch Factor").clear(); - await page.getByLabel("Patch Factor").fill("500"); - await expect(page.getByText("Value should be <= 100.")).toBeVisible(); - await page.getByLabel("Patch Factor").clear(); - await page.getByLabel("Patch Factor").fill("-500"); - await expect(page.getByText("Value should be >= 0.")).toBeVisible(); - }); - - test("can update fields and those changes will persist", async ({ - authenticatedPage: page, - }) => { - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - - await selectOption(page, "Task Finder Version", "Parallel"); - await selectOption(page, "Task Planner Version", "Tunable"); - await selectOption( - page, - "Task Dispatcher Version", - "Revised with dependencies", - ); - await save(page); - await validateToast(page, "success", "Updated distro."); - - await page.reload(); - const finder = page.getByRole("button", { name: "Task Finder Version" }); - await expect(finder).toContainText("Parallel"); - const planner = page.getByRole("button", { - name: "Task Planner Version", - }); - await expect(planner).toContainText("Tunable"); - const dispatcher = page.getByRole("button", { - name: "Task Dispatcher Version", - }); - await expect(dispatcher).toContainText("Revised with dependencies"); - - await selectOption(page, "Task Finder Version", "Legacy"); - await selectOption(page, "Task Planner Version", "Tunable"); - await selectOption( - page, - "Task Dispatcher Version", - "Revised with dependencies", - ); - await save(page); - await validateToast(page, "success", "Updated distro."); - }); - }); -}); diff --git a/apps/spruce/playwright/tests/distroSettings/utils.ts b/apps/spruce/playwright/tests/distroSettings/utils.ts deleted file mode 100644 index f3bd1f28db..0000000000 --- a/apps/spruce/playwright/tests/distroSettings/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Page } from "@playwright/test"; -import { clickRadio } from "@evg-ui/playwright-config/helpers"; -import { expect } from "../../fixtures"; - -type onSaveOptions = "NONE" | "DECOMMISSION" | "RESTART_JASPER" | "REPROVISION"; - -export const save = async (page: Page, onSaveValue?: onSaveOptions) => { - const saveButton = page.getByTestId("save-settings-button"); - await expect(saveButton).not.toHaveAttribute("aria-disabled", "true"); - await saveButton.click(); - - if (onSaveValue) { - const radio = page.locator(`input[value="${onSaveValue}"]`); - await clickRadio(radio); - } - - const modal = page.getByTestId("save-modal"); - const confirmButton = modal.getByRole("button", { name: "Save" }); - await expect(confirmButton).toHaveAttribute("aria-disabled", "false"); - await confirmButton.click(); -}; diff --git a/apps/spruce/playwright/tests/projectSettings/access.spec.ts b/apps/spruce/playwright/tests/projectSettings/access.spec.ts deleted file mode 100644 index a4b3867d86..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/access.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { 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); - await expect(page.getByTestId("default-to-repo-button")).toBeVisible(); - await expect(page.getByTestId("default-to-repo-button")).toBeEnabled(); - }); - - test("Changing settings and clicking the save button produces a success toast and the changes are persisted", async ({ - authenticatedPage: page, - }) => { - await page.getByText("Unrestricted", { exact: true }).click(); - await expect( - page.getByLabel("Unrestricted", { exact: true }), - ).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, - }) => { - await expect(page.getByTestId("default-to-repo-button")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByTestId("default-to-repo-button").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"); - await expect( - page.getByLabel("Default to repo (unrestricted)"), - ).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/projectSettings/admin_actions.spec.ts b/apps/spruce/playwright/tests/projectSettings/admin_actions.spec.ts deleted file mode 100644 index 4a485edcc4..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/admin_actions.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -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.getByTestId("delete-project-button").scrollIntoViewIfNeeded(); - await page.getByTestId("delete-project-button").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/projectSettings/attaching_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/attaching_to_repo.spec.ts deleted file mode 100644 index 43e4d7c532..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/attaching_to_repo.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { 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")).toHaveAttribute( - "aria-disabled", - "true", - ); - await save(page); - await validateToast(page, "success", "Successfully updated project", true); - - 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"); - - 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(); - await page - .getByTestId("cq-enabled-radio-box") - .getByText("Enabled", { exact: true }) - .click(); - await expect( - page.getByTestId("cq-card").getByTestId("error-banner"), - ).toBeVisible(); - }); -}); 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 97d8166d2a..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -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, - }) => { - await expect(page.getByTestId("disabled-webhook-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/projectSettings/constants.ts b/apps/spruce/playwright/tests/projectSettings/constants.ts deleted file mode 100644 index 96343a8400..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -export enum ProjectSettingsTabRoutes { - General = "general", - Access = "access", - Variables = "variables", - GithubCommitQueue = "github-commitqueue", - Notifications = "notifications", - PatchAliases = "patch-aliases", - VirtualWorkstation = "virtual-workstation", - ProjectTriggers = "project-triggers", - PeriodicBuilds = "periodic-builds", - Plugins = "plugins", - EventLog = "event-log", - ViewsAndFilters = "views-and-filters", - GithubAppSettings = "github-app-settings", - GithubPermissionGroups = "github-permission-groups", - CommitChecks = "commit-checks", -} - -export const getProjectSettingsRoute = ( - identifier: string, - tab: ProjectSettingsTabRoutes = ProjectSettingsTabRoutes.General, -) => `/project/${identifier}/settings/${tab}`; - -export const getRepoSettingsRoute = ( - repoId: string, - tab: ProjectSettingsTabRoutes = ProjectSettingsTabRoutes.General, -) => `/repo/${repoId}/settings/${tab}`; - -export const project = "spruce"; -export const projectUseRepoEnabled = "evergreen"; -export const repo = "602d70a2b2373672ee493184"; diff --git a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts deleted file mode 100644 index 3c3c276806..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/defaulting_to_repo.spec.ts +++ /dev/null @@ -1,521 +0,0 @@ -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, - }) => { - await expect(page.getByTestId("default-to-repo-button")).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.getByTestId("add-button").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.getByTestId("add-button").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.getByTestId("add-button").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"); - await githubSection - .getByText("Override Repo Patch Definition", { exact: true }) - .click(); - await expect( - githubSection.getByLabel("Override Repo Patch Definition", { - exact: true, - }), - ).toBeChecked(); - - 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, - }) => { - await page - .getByTestId("github-checks-enabled-radio-box") - .scrollIntoViewIfNeeded(); - 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, - }) => { - await expect( - page.getByTestId("cq-override-radio-box").locator("input").first(), - ).toBeChecked(); - }); - - 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 page.getByTestId("accordion-toggle").scrollIntoViewIfNeeded(); - 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, - }) => { - await expect( - page.getByLabel("Default to Repo Patch Aliases"), - ).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 - .getByTestId("add-button") - .filter({ hasText: "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"); - - 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(); - - await expect(page.getByTestId("command-row")).toHaveCount(1); - const repoCommandInput = page.getByTestId("repo-command-input"); - await expect(repoCommandInput).toHaveValue("a repo command"); - await expect(repoCommandInput).toHaveAttribute("aria-disabled", "true"); - - const overrideRepoCommandsRadio = page.getByRole("radio", { - name: "Override Repo Commands", - }); - await clickRadio(overrideRepoCommandsRadio); - - await expect(page.getByTestId("command-row")).toHaveCount(0); - await page.getByRole("button", { name: "Add Command" }).click(); - const projectCommandInput = page.getByTestId("command-input"); - await projectCommandInput.fill("a project command"); - await save(page); - await validateToast( - page, - "success", - "Successfully updated project", - true, - ); - await expect(projectCommandInput).toHaveValue("a project command"); - await expect(projectCommandInput).toHaveAttribute( - "aria-disabled", - "false", - ); - - const defaultToRepoCommandsRadio = page.getByRole("radio", { - name: "Default to Repo Commands", - }); - await clickRadio(defaultToRepoCommandsRadio); - await expect(page.getByTestId("command-row")).toHaveCount(1); - await expect(repoCommandInput).toHaveValue("a repo command"); - await expect(repoCommandInput).toHaveAttribute("aria-disabled", "true"); - await save(page); - await validateToast(page, "success", "Successfully updated project"); - - await clickRadio(overrideRepoCommandsRadio); - await expect(page.getByTestId("command-row")).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/projectSettings/github_app_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts deleted file mode 100644 index e053895f37..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/github_app_settings.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -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(); - await expect( - page.getByTestId("replace-github-credentials-modal"), - ).toBeVisible(); - - await expect( - page - .getByTestId("replace-github-credentials-modal") - .getByRole("button", { name: "Replace" }), - ).toHaveAttribute("aria-disabled", "true"); - - await page.getByTestId("replace-app-id-input").fill("99999"); - await page.getByTestId("replace-private-key-input").fill("new-private-key"); - - await expect( - page - .getByTestId("replace-github-credentials-modal") - .getByRole("button", { name: "Replace" }), - ).toHaveAttribute("aria-disabled", "false"); - - await page - .getByTestId("replace-github-credentials-modal") - .getByRole("button", { name: "Replace" }) - .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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - await expectSaveButtonEnabled(page, true); - await save(page); - await validateToast(page, "success", "Successfully updated project"); - }); -}); diff --git a/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts b/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts deleted file mode 100644 index d8f4262b80..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/github_permission_groups.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -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-item")).toHaveCount(0); - await expectSaveButtonEnabled(page, false); - }); - - test("should throw an error if permission group definitions are invalid", async ({ - authenticatedPage: page, - }) => { - await expect( - page.getByRole("button", { name: /^Add permission group$/ }), - ).toBeVisible(); - await page.getByRole("button", { name: /^Add permission group$/ }).click(); - await expect(page.getByTestId("permission-group-item")).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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - 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, - }) => { - await expect( - page.getByRole("button", { name: /^Add permission group$/ }), - ).toBeVisible(); - await page.getByRole("button", { name: /^Add permission group$/ }).click(); - await expect(page.getByTestId("permission-group-item")).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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - await save(page); - await validateToast(page, "success", "Successfully updated project"); - - await page.reload(); - await expect(page.getByTestId("permission-group-item")).toHaveCount(1); - await page.getByTestId("delete-item-button").click(); - await expect(page.getByTestId("permission-group-item")).toHaveCount(0); - await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - await save(page); - await validateToast(page, "success", "Successfully updated project"); - }); -}); diff --git a/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts b/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts deleted file mode 100644 index b02b284be0..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/not_defaulting_to_repo.spec.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { - validateToast, - validateDatePickerDate, - clearDatePickerInput, - typeDatePickerDate, - clickCheckbox, -} from "../../helpers"; -import { getProjectSettingsRoute, project } from "./constants"; -import { expectSaveButtonEnabled, save } 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"); - }); - - test("Allows enabling Run Every Mainline Commit", async ({ - authenticatedPage: page, - }) => { - await page.getByTestId("navitem-general").click(); - await page - .getByTestId("run-every-mainline-commit-radio-box") - .getByText("Enabled") - .click(); - await save(page); - await validateToast(page, "success", "Successfully updated project"); - await expect( - page - .getByTestId("run-every-mainline-commit-radio-box") - .getByLabel("Enabled"), - ).toHaveAttribute("aria-checked", "true"); - }); - - test.describe("Variables page", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - 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.getByTestId("add-button").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"); - await expect(page.getByTestId("var-private-input")).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")).toHaveAttribute( - "aria-disabled", - "true", - ); - await expect(page.getByTestId("var-value-input")).toHaveAttribute( - "aria-disabled", - "true", - ); - await expect(page.getByTestId("var-private-input")).toHaveAttribute( - "aria-disabled", - "true", - ); - await expect(page.getByTestId("var-admin-input")).toHaveAttribute( - "aria-disabled", - "false", - ); - await expect(page.getByTestId("var-description-input")).toHaveAttribute( - "aria-disabled", - "false", - ); - }); - - test("Typing a duplicate variable name will disable saving and show an error message", async ({ - authenticatedPage: page, - }) => { - await page.getByTestId("add-button").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.getByTestId("add-button").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.getByTestId("add-button").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(page.getByTestId("var-admin-input")).toBeChecked(); - await save(page); - await validateToast(page, "success", "Successfully updated project"); - }); - - test("Should persist saved variables and allow deletion", async ({ - authenticatedPage: page, - }) => { - await page.getByTestId("add-button").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.getByTestId("add-button").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.getByTestId("add-button").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); - }); - }); - - test.describe("GitHub page", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.getByTestId("navitem-github-commitqueue").click(); - }); - - test("Allows adding a git tag alias", async ({ - authenticatedPage: page, - }) => { - await page - .getByTestId("git-tag-enabled-radio-box") - .getByText("Enabled") - .click(); - await page - .getByTestId("add-button") - .filter({ hasText: "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", - ); - }); - }); - - test.describe("Periodic Builds page", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - 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.getByTestId("add-button").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.getByTestId("add-button").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"); - }); - }); - - test.describe("Project Triggers page", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.getByTestId("navitem-project-triggers").click(); - }); - - test("Saves a project trigger", async ({ authenticatedPage: page }) => { - await page.getByTestId("add-button").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/projectSettings/notifications.spec.ts b/apps/spruce/playwright/tests/projectSettings/notifications.spec.ts deleted file mode 100644 index 5016adbff4..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/notifications.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -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); - await expect( - page.getByTestId("add-button").getByText("Add Subscription"), - ).toBeVisible(); - await page.getByTestId("add-button").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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - await save(page); - await validateToast(page, "success", "Successfully updated project"); - - await expectSaveButtonEnabled(page, false); - const subscriptionItem = page.getByTestId("expandable-card"); - await subscriptionItem.scrollIntoViewIfNeeded(); - 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - 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); - await expect( - page.getByTestId("add-button").getByText("Add Subscription"), - ).toBeVisible(); - await page.getByTestId("add-button").click(); - const expandableCard = page.getByTestId("expandable-card"); - await expandableCard.scrollIntoViewIfNeeded(); - 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - await expectSaveButtonEnabled(page, false); - }); - - test("should not be able to save a subscription if an input is invalid", async ({ - authenticatedPage: page, - }) => { - await page.getByTestId("add-button").click(); - const expandableCard = page.getByTestId("expandable-card"); - await expandableCard.scrollIntoViewIfNeeded(); - 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - 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/projectSettings/permissions.spec.ts b/apps/spruce/playwright/tests/projectSettings/permissions.spec.ts deleted file mode 100644 index 742ada3057..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/permissions.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { users } from "@evg-ui/playwright-config/constants"; -import { test, expect } from "../../fixtures"; -import { login, logout } from "../../helpers"; -import { - getProjectSettingsRoute, - getRepoSettingsRoute, - projectUseRepoEnabled, - repo, -} from "./constants"; - -test.describe("project/repo 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(), - ).toHaveAttribute("aria-disabled", "true"); - }); - - 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(), - ).toHaveAttribute("aria-disabled", "false"); - }); - }); - - test.describe("repos", () => { - 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(), - ).toHaveAttribute("aria-disabled", "true"); - }); - - 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(), - ).toHaveAttribute("aria-disabled", "false"); - }); - }); -}); diff --git a/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts b/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts deleted file mode 100644 index 6fcf1f0656..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/plugins.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -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" }) - .scrollIntoViewIfNeeded(); - await page.getByRole("button", { name: "Add metadata link" }).click(); - - const mostRecentMetadataLink = page - .getByTestId("metadata-link-item") - .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-item")).toHaveCount(2); - await save(page); - - await page.goto(patchPage); - 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 page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - await save(page); - - await page.goto(patchPage); - await expect(page.getByTestId("external-link")).toHaveCount(0); - }); -}); diff --git a/apps/spruce/playwright/tests/projectSettings/project_select.spec.ts b/apps/spruce/playwright/tests/projectSettings/project_select.spec.ts deleted file mode 100644 index 8f1fbe842f..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/project_select.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { getProjectSettingsRoute, project } from "./constants"; - -test.describe("Clicking on The Project Select Dropdown", () => { - const origin = getProjectSettingsRoute(project); - - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto(origin); - }); - - test("Headers are clickable", async ({ authenticatedPage: page }) => { - await expect(page.getByTestId("project-select")).toBeVisible(); - await page.getByTestId("project-select").click(); - await expect(page.getByTestId("project-select-options")).toBeVisible(); - await page - .getByTestId("project-select-options") - .getByText("evergreen-ci/evergreen") - .click(); - await expect(page).not.toHaveURL(new RegExp(origin)); - }); -}); diff --git a/apps/spruce/playwright/tests/projectSettings/project_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/project_settings.spec.ts deleted file mode 100644 index dc058e5365..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/project_settings.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; -import { - getProjectSettingsRoute, - project, - ProjectSettingsTabRoutes, -} from "./constants"; -import { save } from "./utils"; - -test.describe("projectSettings/project_settings", () => { - test.describe("Renaming the identifier", () => { - const origin = getProjectSettingsRoute(project); - - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto(origin); - }); - - test("Update identifier", async ({ authenticatedPage: page }) => { - 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.describe("A project that has GitHub webhooks disabled", () => { - const origin = getProjectSettingsRoute( - "logkeeper", - ProjectSettingsTabRoutes.GithubCommitQueue, - ); - - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto(origin); - }); - - test("Disables all interactive elements on the page", async ({ - authenticatedPage: page, - }) => { - await expect( - page.getByTestId("project-settings-page").getByRole("button").first(), - ).toHaveAttribute("aria-disabled", "true"); - await expect(page.locator("input").first()).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - }); - - test.describe("A 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/projectSettings/repo_settings.spec.ts b/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts deleted file mode 100644 index 25ebaa917d..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/repo_settings.spec.ts +++ /dev/null @@ -1,385 +0,0 @@ -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("Repo Settings", () => { - const origin = getRepoSettingsRoute(repo); - - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.goto(origin); - }); - - test.describe("General settings page", () => { - 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"); - }); - }); - - test.describe("GitHub page", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - 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.scrollIntoViewIfNeeded(); - 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 page - .getByText("Merge Queue Patch Definitions") - .scrollIntoViewIfNeeded(); - 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"); - }); - }); - }); - - test.describe("Patch Aliases page", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - 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 - .getByTestId("add-button") - .filter({ hasText: "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 - .getByTestId("add-button") - .filter({ hasText: "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(); - - await page - .getByText("Pull Request Trigger Aliases") - .scrollIntoViewIfNeeded(); - 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(); - await expect(page.getByTestId("pta-tooltip")).toHaveCount(1); - await expect(page.getByTestId("pta-tooltip")).toBeVisible(); - await expect(page.getByTestId("pta-tooltip")).toContainText("spruce"); - await expect(page.getByTestId("pta-tooltip")).toContainText( - "module_name", - ); - await expect(page.getByTestId("pta-tooltip")).toContainText( - "Variant/Task Regex Pairs", - ); - - await page - .getByText("Merge Queue Trigger Aliases") - .scrollIntoViewIfNeeded(); - 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(); - await expect(page.getByTestId("pta-tooltip")).toHaveCount(1); - await expect(page.getByTestId("pta-tooltip")).toBeVisible(); - await expect(page.getByTestId("pta-tooltip")).toContainText("spruce"); - await expect(page.getByTestId("pta-tooltip")).toContainText( - "module_name", - ); - await expect(page.getByTestId("pta-tooltip")).toContainText( - "Variant/Task Regex Pairs", - ); - }); - }); - - test.describe("Virtual Workstation page", () => { - test.beforeEach(async ({ authenticatedPage: page }) => { - await page.getByTestId("navitem-virtual-workstation").click(); - }); - - test("Adds two commands and then reorders them", async ({ - authenticatedPage: page, - }) => { - await expectSaveButtonEnabled(page, false); - await page.getByTestId("add-button").click(); - await page.getByTestId("command-input").fill("command 1"); - await page.getByTestId("directory-input").fill("mongodb.user.directory"); - - await page.getByTestId("add-button").click(); - await page.getByTestId("command-input").nth(1).fill("command 2"); - await save(page); - await validateToast(page, "success", "Successfully updated repo"); - await page.getByTestId("array-down-button").click(); - await page.getByTestId("save-settings-button").scrollIntoViewIfNeeded(); - 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/projectSettings/stepback_bisect.spec.ts b/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts deleted file mode 100644 index 64561e3b96..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/stepback_bisect.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { test, expect } from "../../fixtures"; -import { clickRadio, validateToast } from "../../helpers"; -import { - getProjectSettingsRoute, - project, - projectUseRepoEnabled, -} from "./constants"; -import { save } from "./utils"; - -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: "Disable" }), - ).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/projectSettings/utils.ts b/apps/spruce/playwright/tests/projectSettings/utils.ts deleted file mode 100644 index 716c2c46fb..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Page } from "@playwright/test"; -import { expect } from "../../fixtures"; - -export const save = async (page: Page) => { - const saveButton = page.getByTestId("save-settings-button"); - await saveButton.scrollIntoViewIfNeeded(); - 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/views_and_filters.spec.ts b/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts deleted file mode 100644 index cf4529886b..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/views_and_filters.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -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-item")).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"); - await expect(page.getByTestId("parsley-filter-item")).toHaveCount(3); - - await page - .getByTestId("delete-item-button") - .first() - .scrollIntoViewIfNeeded(); - await page.getByTestId("delete-item-button").first().click(); - await save(page); - await validateToast(page, "success", "Successfully updated project"); - await expect(page.getByTestId("parsley-filter-item")).toHaveCount(2); - }); - }); -}); From 279ca4752c13406916544eed4fc14d5dd6030f4b Mon Sep 17 00:00:00 2001 From: minnakt Date: Fri, 1 May 2026 15:45:17 -0400 Subject: [PATCH 29/32] fix: merge conflicts --- .../projectSettings/commit_checks.spec.ts | 106 ----------------- .../tests/projectSettings/git_tags.spec.ts | 86 -------------- .../tests/projectSettings/merge_queue.spec.ts | 100 ---------------- .../projectSettings/pull_requests.spec.ts | 111 ------------------ .../tabs/ProjectTab/getFormSchema.ts | 3 +- 5 files changed, 1 insertion(+), 405 deletions(-) delete mode 100644 apps/spruce/playwright/tests/projectSettings/commit_checks.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/git_tags.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/merge_queue.spec.ts delete mode 100644 apps/spruce/playwright/tests/projectSettings/pull_requests.spec.ts 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/merge_queue.spec.ts b/apps/spruce/playwright/tests/projectSettings/merge_queue.spec.ts deleted file mode 100644 index 9dbe93942d..0000000000 --- a/apps/spruce/playwright/tests/projectSettings/merge_queue.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { SEEN_GITHUB_NAV_GUIDE_CUE } from "../../../src/constants/cookies"; -import { test, expect } from "../../fixtures"; -import { validateToast } from "../../helpers"; - -test.describe("Merge Queue 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/merge-queue"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - - test("Merge Queue 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("Merge Queue 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/merge-queue"); - await expect(page.getByTestId("save-settings-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - }); - - 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(); - await expect(page.getByText("Merge Queue Patch Definitions")).toBeVisible(); - - const errorBanner = page.getByTestId("error-banner"); - await expect(errorBanner).toBeVisible(); - await expect(errorBanner).toContainText( - "A Merge Queue Patch Definition must be specified for this feature to run.", - ); - }); - - test("Saves a merge queue definition", async ({ - authenticatedPage: page, - }) => { - await page - .getByTestId("mq-enabled-radio-box") - .locator("label", { hasText: "Enabled" }) - .click(); - 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 validateToast(page, "success", "Successfully updated project"); - }); -}); 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/distroSettings/tabs/ProjectTab/getFormSchema.ts b/apps/spruce/src/pages/distroSettings/tabs/ProjectTab/getFormSchema.ts index f52c8f6536..3ec279d141 100644 --- a/apps/spruce/src/pages/distroSettings/tabs/ProjectTab/getFormSchema.ts +++ b/apps/spruce/src/pages/distroSettings/tabs/ProjectTab/getFormSchema.ts @@ -49,9 +49,8 @@ export const getFormSchema = (): ReturnType => ({ "ui:orderable": false, items: { "ui:ObjectFieldTemplate": FieldRow, - "ui:data-cy": "expansions-list-item", - "ui:label": false, "ui:data-cy": "expansion-item", + "ui:label": false, }, }, validProjects: { From ce6ab8e03813c947f77b547bc748985d4387f06f Mon Sep 17 00:00:00 2001 From: minnakt Date: Fri, 1 May 2026 15:46:00 -0400 Subject: [PATCH 30/32] fix: only leave fake test --- .../integration/projectSettings/constants.ts | 46 --- .../projectSettings/not_defaulting_to_repo.ts | 274 ------------------ 2 files changed, 320 deletions(-) delete mode 100644 apps/spruce/cypress/integration/projectSettings/constants.ts delete mode 100644 apps/spruce/cypress/integration/projectSettings/not_defaulting_to_repo.ts diff --git a/apps/spruce/cypress/integration/projectSettings/constants.ts b/apps/spruce/cypress/integration/projectSettings/constants.ts deleted file mode 100644 index c8a98e9efc..0000000000 --- a/apps/spruce/cypress/integration/projectSettings/constants.ts +++ /dev/null @@ -1,46 +0,0 @@ -export enum ProjectSettingsTabRoutes { - General = "general", - Access = "access", - Variables = "variables", - GithubCommitQueue = "github-commitqueue", - Notifications = "notifications", - PatchAliases = "patch-aliases", - VirtualWorkstation = "virtual-workstation", - ProjectTriggers = "project-triggers", - PeriodicBuilds = "periodic-builds", - Plugins = "plugins", - EventLog = "event-log", - ViewsAndFilters = "views-and-filters", - GithubAppSettings = "github-app-settings", - GithubPermissionGroups = "github-permission-groups", - MergeQueue = "merge-queue", - PullRequests = "pull-requests", - CommitChecks = "commit-checks", - GitTags = "git-tags", -} - -export const getProjectSettingsRoute = ( - identifier: string, - tab: ProjectSettingsTabRoutes = ProjectSettingsTabRoutes.General, -) => `project/${identifier}/settings/${tab}`; - -export const getRepoSettingsRoute = ( - repoId: string, - tab: ProjectSettingsTabRoutes = ProjectSettingsTabRoutes.General, -) => `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/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"); - }); - }); -}); From 48d495837a084bd0441b0ba6185d3dbf57fde518 Mon Sep 17 00:00:00 2001 From: minnakt Date: Fri, 1 May 2026 16:40:56 -0400 Subject: [PATCH 31/32] cleanup: `spawn` tests --- .../playwright/tests/spawn/host.spec.ts | 123 ++++++++------ .../playwright/tests/spawn/volume.spec.ts | 154 ++++++++---------- .../MigrateVolumeModal.tsx | 2 +- 3 files changed, 138 insertions(+), 141 deletions(-) diff --git a/apps/spruce/playwright/tests/spawn/host.spec.ts b/apps/spruce/playwright/tests/spawn/host.spec.ts index 19617c9fb9..33cac66d47 100644 --- a/apps/spruce/playwright/tests/spawn/host.spec.ts +++ b/apps/spruce/playwright/tests/spawn/host.spec.ts @@ -2,6 +2,7 @@ import { test, expect } from "../../fixtures"; import { clearDatePickerInput, clickCheckbox, + clickRadio, typeDatePickerDate, } from "../../helpers"; @@ -29,10 +30,12 @@ const ascendingSortSpawnHostOrderByExpiration = [ const hostTaskId = "evergreen_ubuntu1604_dist_patch_33016573166a36bd5f46b4111151899d5c4e95b1_5ecedafb562343215a7ff297_20_05_27_21_39_46"; const distroId = "windows-64-vs2015-small"; -const projectSetupCheckbox = +const projectSetupCheckboxLabel = "Use project-specific setup script defined at /path"; -const startHostsCheckbox = +const setupScriptCheckboxLabel = "Define setup script to run after host"; +const startHostsCheckboxLabel = "Also start any hosts this task started (if applicable)"; +const loadDataCheckboxLabel = "Load data for dist"; test.describe("Spawn Host page", () => { test.beforeEach(async ({ authenticatedPage: page }) => { @@ -73,8 +76,8 @@ test.describe("Spawn Host page", () => { authenticatedPage: page, }) => { await page.goto("/spawn/host?host=i-092593689871a50dc"); - await expect(page.getByTestId("spawn-host-card").first()).toBeVisible(); await expect(page.getByTestId("spawn-host-card")).toHaveCount(1); + await expect(page.getByTestId("spawn-host-card")).toBeVisible(); }); test("Clicking on the Event Log link should redirect to /host/:hostId", async ({ @@ -145,7 +148,7 @@ test.describe("Spawn Host page", () => { test("Should disable 'Unexpirable Host' radio box when max number of unexpirable hosts is met (2)", async ({ authenticatedPage: page, }) => { - await page.getByText("Spawn a host").click(); + await page.getByRole("button", { name: "Spawn a host" }).click(); await page.getByTestId("distro-input").click(); await page.getByTestId("distro-option-ubuntu1804-workstation").click(); @@ -170,7 +173,7 @@ test.describe("Spawn Host page", () => { authenticatedPage: page, }) => { await expect(page.getByTestId("spawn-host-modal")).toHaveCount(0); - await page.getByTestId("spawn-host-button").click(); + await page.getByRole("button", { name: "Spawn a host" }).click(); await expect(page.getByTestId("spawn-host-modal")).toBeVisible(); }); @@ -201,15 +204,19 @@ test.describe("Spawn Host page", () => { await page.goto( `/spawn/host?spawnHost=True&distroId=rhel71-power8-large&taskId=${hostTaskId}`, ); - await expect(page.getByTestId("spawn-host-modal")).toContainText( - projectSetupCheckbox, - ); - await expect(page.getByTestId("spawn-host-modal")).toContainText( - "Load data for dist on ubuntu1604", - ); - await expect(page.getByTestId("spawn-host-modal")).toContainText( - startHostsCheckbox, - ); + const projectSetupCheckbox = page.getByRole("checkbox", { + name: projectSetupCheckboxLabel, + }); + const loadDataCheckbox = page.getByRole("checkbox", { + name: loadDataCheckboxLabel, + }); + const startHostsCheckbox = page.getByRole("checkbox", { + name: startHostsCheckboxLabel, + }); + + await expect(projectSetupCheckbox).toHaveCount(1); + await expect(loadDataCheckbox).toHaveCount(1); + await expect(startHostsCheckbox).toHaveCount(1); }); test("Unchecking 'Load data for dist' hides nested checkbox selections and checking shows them", async ({ @@ -219,17 +226,24 @@ test.describe("Spawn Host page", () => { `/spawn/host?spawnHost=True&distroId=rhel71-power8-large&taskId=${hostTaskId}`, ); await expect(page.getByTestId("spawn-host-modal")).toBeVisible(); - await expect(page.getByTestId("load-data-checkbox")).toBeChecked(); - await expect(page.getByText(projectSetupCheckbox)).toBeVisible(); - await expect(page.getByText(startHostsCheckbox)).toBeVisible(); - const loadDataCheckbox = page.getByRole("checkbox", { - name: "Load data for dist", + name: loadDataCheckboxLabel, }); + const projectSetupCheckbox = page.getByRole("checkbox", { + name: projectSetupCheckboxLabel, + }); + const startHostsCheckbox = page.getByRole("checkbox", { + name: startHostsCheckboxLabel, + }); + + await expect(loadDataCheckbox).toBeChecked(); + await expect(projectSetupCheckbox).toHaveCount(1); + await expect(startHostsCheckbox).toHaveCount(1); + await clickCheckbox(loadDataCheckbox); await expect(loadDataCheckbox).not.toBeChecked(); - await expect(page.getByText(projectSetupCheckbox)).toHaveCount(0); - await expect(page.getByText(startHostsCheckbox)).toHaveCount(0); + await expect(projectSetupCheckbox).toHaveCount(0); + await expect(startHostsCheckbox).toHaveCount(0); }); test("Visiting the spawn host page with a task and distro supplied in the url should populate the distro input", async ({ @@ -253,10 +267,7 @@ test.describe("Spawn Host page", () => { await page.getByTestId("distro-input").click(); await expect(page.getByText("Admin-only distros")).toHaveCount(0); await page.getByTestId("distro-option-ubuntu1804-workstation").click(); - await expect(page.getByTestId("volume-select")).toHaveAttribute( - "aria-disabled", - "true", - ); + await expect(page.getByTestId("volume-select")).toBeDisabled(); }); test("Clicking 'Add new key' hides the key name dropdown and shows the key value text area", async ({ @@ -267,7 +278,8 @@ test.describe("Spawn Host page", () => { ); await expect(page.getByTestId("key-select")).toBeVisible(); await expect(page.getByTestId("key-value-text-area")).toHaveCount(0); - await page.getByText("Add new key").click(); + const addNewKeyRadio = page.getByRole("radio", { name: "Add new key" }); + await clickRadio(addNewKeyRadio); await expect(page.getByTestId("key-select")).toHaveCount(0); await expect(page.getByTestId("key-value-text-area")).toBeVisible(); }); @@ -278,13 +290,15 @@ test.describe("Spawn Host page", () => { await page.goto( `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, ); - await expect( - page.getByTestId("run-user-data-script-text-area"), - ).toHaveCount(0); - await page.getByText("Run Userdata script on start").click(); - await expect( - page.getByTestId("user-data-script-text-area"), - ).toBeVisible(); + const userDataScriptInput = page.getByRole("textbox", { + name: "Userdata Script", + }); + await expect(userDataScriptInput).toHaveCount(0); + const userDataScriptCheckbox = page.getByRole("checkbox", { + name: "Run Userdata script on start", + }); + await clickCheckbox(userDataScriptCheckbox); + await expect(userDataScriptInput).toBeVisible(); }); test("Checking 'Define setup script...' shows the setup script text area", async ({ @@ -293,14 +307,22 @@ test.describe("Spawn Host page", () => { await page.goto( `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, ); - await expect(page.getByTestId("setup-script-text-area")).toBeHidden(); - await page.getByText("Use project-specific setup script").click(); - await expect(page.getByTestId("setup-script-checkbox")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByText("Define setup script to run after host").click(); - await expect(page.getByTestId("setup-script-text-area")).toBeVisible(); + const setupScriptInput = page.getByRole("textbox", { + name: "Setup Script", + }); + await expect(setupScriptInput).toHaveCount(0); + + const projectSetupScriptCheckbox = page.getByRole("checkbox", { + name: projectSetupCheckboxLabel, + }); + await clickCheckbox(projectSetupScriptCheckbox); + + const setupScriptCheckbox = page.getByRole("checkbox", { + name: setupScriptCheckboxLabel, + }); + await expect(setupScriptCheckbox).toBeEnabled(); + await clickCheckbox(setupScriptCheckbox); + await expect(setupScriptInput).toHaveCount(1); }); test("Conditionally disables setup script and project setup script checkboxes based on the other's value", async ({ @@ -310,23 +332,24 @@ test.describe("Spawn Host page", () => { `/spawn/host?spawnHost=True&distroId=${distroId}&taskId=${hostTaskId}`, ); const projectCheckbox = page.getByRole("checkbox", { - name: "Use project-specific setup script", + name: projectSetupCheckboxLabel, }); const setupCheckbox = page.getByRole("checkbox", { - name: "Define setup script to run after host", + name: setupScriptCheckboxLabel, }); - await expect(projectCheckbox).toHaveAttribute("aria-checked", "true"); - await expect(projectCheckbox).toHaveAttribute("aria-disabled", "false"); - await expect(setupCheckbox).toHaveAttribute("aria-disabled", "true"); + await expect(projectCheckbox).toBeChecked(); + await expect(projectCheckbox).toBeEnabled(); + await expect(setupCheckbox).toBeDisabled(); await clickCheckbox(projectCheckbox); - await expect(projectCheckbox).toHaveAttribute("aria-disabled", "false"); - await expect(setupCheckbox).toHaveAttribute("aria-disabled", "false"); + await expect(projectCheckbox).not.toBeChecked(); + await expect(projectCheckbox).toBeEnabled(); + await expect(setupCheckbox).toBeEnabled(); await clickCheckbox(setupCheckbox); - await expect(projectCheckbox).toHaveAttribute("aria-disabled", "true"); - await expect(setupCheckbox).toHaveAttribute("aria-disabled", "false"); + await expect(projectCheckbox).toBeDisabled(); + await expect(setupCheckbox).toBeEnabled(); }); }); diff --git a/apps/spruce/playwright/tests/spawn/volume.spec.ts b/apps/spruce/playwright/tests/spawn/volume.spec.ts index 639d98a3c7..1f90fe2dae 100644 --- a/apps/spruce/playwright/tests/spawn/volume.spec.ts +++ b/apps/spruce/playwright/tests/spawn/volume.spec.ts @@ -76,7 +76,6 @@ test.describe("Spawn volume page", () => { const targetVolume = "vol-0ea662ac92f611ed4"; await page.goto(`/spawn/volume?volume=${targetVolume}`); const card = page.getByTestId(`spawn-volume-card-${targetVolume}`); - await card.scrollIntoViewIfNeeded(); await expect(card).toBeVisible(); }); @@ -140,14 +139,11 @@ test.describe("Spawn volume page", () => { const confirmCheckbox = popconfirm.getByRole("checkbox", { name: "I understand this volume is currently mounted to a host.", }); - await expect(confirmCheckbox).not.toBeChecked(); - const yesButton = popconfirm.getByRole("button", { name: "Yes" }); - await expect(yesButton).toHaveAttribute("aria-disabled", "true"); - + await expect(confirmCheckbox).not.toBeChecked(); + await expect(yesButton).toBeDisabled(); await clickCheckbox(confirmCheckbox); - - await expect(yesButton).toHaveAttribute("aria-disabled", "false"); + await expect(yesButton).toBeEnabled(); await yesButton.click(); await validateToast(page, "success", "Successfully deleted the volume."); @@ -160,14 +156,12 @@ test.describe("Spawn volume page", () => { data: { detachVolumeFromHost: true }, errors: null, }); - await page - .getByTestId( - "detach-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b857", - ) - .click(); + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b857"; + await page.getByTestId(`detach-btn-${targetVolume}`).click(); const popconfirm = page.getByTestId("unmount-volume-popconfirm"); - await expect(popconfirm).toBeVisible(); - await popconfirm.getByRole("button", { name: "Yes" }).click(); + const yesButton = popconfirm.getByRole("button", { name: "Yes" }); + await yesButton.click(); await validateToast(page, "success", "Successfully unmounted the volume."); }); @@ -205,25 +199,21 @@ test.describe("Spawn volume page", () => { test("Clicking on 'Edit' should open the Edit Volume Modal", async ({ authenticatedPage: page, }) => { - await page - .getByTestId( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858"; + await page.getByTestId(`edit-btn-${targetVolume}`).click(); await expect(page.getByTestId("update-volume-modal")).toBeVisible(); }); test("name, size, expiration inputs should be populated on initial render", async ({ authenticatedPage: page, }) => { - await page - .getByTestId( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858"; + await page.getByTestId(`edit-btn-${targetVolume}`).click(); await expect(page.getByTestId("update-volume-modal")).toBeVisible(); await expect(page.getByTestId("volume-name-input")).toHaveValue( - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + targetVolume, ); await expect(page.getByTestId("volume-size-input")).toHaveValue("100"); await validateDatePickerDate(page, "date-picker", { @@ -238,103 +228,84 @@ test.describe("Spawn volume page", () => { test("Reopening the edit volume modal should reset form input fields", async ({ authenticatedPage: page, }) => { - await page - .getByTestId( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858"; + await page.getByTestId(`edit-btn-${targetVolume}`).click(); await expect(page.getByTestId("update-volume-modal")).toBeVisible(); - await page.getByTestId("volume-name-input").type("Hello, World"); + await page.getByTestId("volume-name-input").fill("Hello, World"); await page .getByTestId("update-volume-modal") .getByRole("button", { name: "Cancel" }) .click(); - await page - .getByTestId( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + await page.getByTestId(`edit-btn-${targetVolume}`).click(); await expect(page.getByTestId("volume-name-input")).toHaveValue( - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + targetVolume, ); }); test("size field is validated correctly", async ({ authenticatedPage: page, }) => { - await page - .getByTestId( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858"; + await page.getByTestId(`edit-btn-${targetVolume}`).click(); await expect(page.getByTestId("update-volume-modal")).toBeVisible(); await page.getByTestId("volume-size-input").clear(); await page.getByTestId("volume-size-input").fill("10000"); - await expect(page.getByRole("button", { name: "Save" })).toHaveAttribute( - "aria-disabled", - "true", - ); + await expect(page.getByRole("button", { name: "Save" })).toBeDisabled(); await page.getByTestId("volume-size-input").clear(); await page.getByTestId("volume-size-input").fill("2"); - await expect(page.getByRole("button", { name: "Save" })).toHaveAttribute( - "aria-disabled", - "true", - ); + await expect(page.getByRole("button", { name: "Save" })).toBeDisabled(); }); test("Submit button should be enabled when the volume details input value differs from what already exists", async ({ authenticatedPage: page, }) => { - await page - .getByTestId( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858"; + await page.getByTestId(`edit-btn-${targetVolume}`).click(); await expect(page.getByTestId("update-volume-modal")).toBeVisible(); const saveButton = page.getByRole("button", { name: "Save" }); const volumeInput = page.getByTestId("volume-name-input"); - await expect(saveButton).toHaveAttribute("aria-disabled", "true"); + await expect(saveButton).toBeDisabled(); await volumeInput.fill("Hello, World"); - await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + await expect(saveButton).toBeEnabled(); await volumeInput.clear(); - await volumeInput.fill( - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ); - await expect(saveButton).toHaveAttribute("aria-disabled", "true"); + await volumeInput.fill(targetVolume); + await expect(saveButton).toBeDisabled(); + const neverExpireCheckbox = page.getByRole("checkbox", { name: "Never expire", }); await clickCheckbox(neverExpireCheckbox); - await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + await expect(saveButton).toBeEnabled(); }); test("Clicking on save button should close the modal and show a success toast", async ({ authenticatedPage: page, }) => { + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858"; mockGraphQLResponse(page, "UpdateVolume", { data: { updateVolume: { - id: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", + id: targetVolume, name: "Hello, World", }, }, errors: null, }); - await page - .getByTestId( - "edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + await page.getByTestId(`edit-btn-${targetVolume}`).click(); await expect(page.getByTestId("update-volume-modal")).toBeVisible(); await page.getByTestId("volume-name-input").fill("Hello, World"); const saveButton = page.getByRole("button", { name: "Save" }); - await expect(saveButton).toHaveAttribute("aria-disabled", "false"); + await expect(saveButton).toBeEnabled(); await saveButton.click(); await validateToast(page, "success", "Successfully updated volume"); await expect(page.getByTestId("update-volume-modal")).toHaveCount(0); @@ -349,54 +320,57 @@ test.describe("Spawn volume page", () => { test("migrate button is disabled for volumes with the migrating status", async ({ authenticatedPage: page, }) => { + const targetVolume = "vol-0ae8720b445b771b6"; const migratingRow = page .getByTestId("leafygreen-table-row") - .filter({ hasText: "vol-0ae8720b445b771b6" }); + .filter({ hasText: targetVolume }); await expect( migratingRow.getByTestId("volume-status-badge"), ).toContainText("Migrating"); await expect( - page.getByTestId("migrate-btn-vol-0ae8720b445b771b6"), - ).toHaveAttribute("aria-disabled", "true"); + page.getByTestId(`migrate-btn-${targetVolume}`), + ).toBeDisabled(); }); test("clicking cancel during confirmation renders the Migrate modal form", async ({ authenticatedPage: page, }) => { - await page - .getByTestId( - "migrate-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858"; + await page.getByTestId(`migrate-btn-${targetVolume}`).click(); await page.getByTestId("distro-input").click(); await page.getByTestId("distro-option-ubuntu1804-workstation").click(); - await page.getByTestId("migrate-modal").getByText("Next").click(); + await page + .getByTestId("migrate-modal") + .getByRole("button", { name: "Next" }) + .click(); await expect(page.getByTestId("migrate-modal")).toContainText( "Are you sure you want to migrate this home volume?", ); await expect(page.getByTestId("distro-input")).toHaveCount(0); - await page.getByTestId("migrate-modal").getByText("Cancel").click(); + await page + .getByTestId("migrate-modal") + .getByRole("button", { name: "Cancel" }) + .click(); await expect(page.getByTestId("distro-input")).toBeVisible(); }); test("open the Migrate modal and spawn a host", async ({ authenticatedPage: page, }) => { - await page - .getByTestId( - "migrate-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", - ) - .click(); + const targetVolume = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858"; + await page.getByTestId(`migrate-btn-${targetVolume}`).click(); await page.getByTestId("distro-input").click(); await page.getByTestId("distro-option-ubuntu1804-workstation").click(); - await expect(page.getByTestId("region-select")).toHaveAttribute( - "aria-disabled", - "true", - ); - await page.getByTestId("migrate-modal").getByText("Next").click(); + await expect(page.getByTestId("region-select")).toBeDisabled(); + await page + .getByTestId("migrate-modal") + .getByRole("button", { name: "Next" }) + .click(); await page .getByTestId("migrate-modal") - .getByText("Migrate Volume") + .getByRole("button", { name: "Migrate volume" }) .click(); await validateToast( page, diff --git a/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/MigrateVolumeModal.tsx b/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/MigrateVolumeModal.tsx index e6a9bb8533..75d7d5d8bb 100644 --- a/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/MigrateVolumeModal.tsx +++ b/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/MigrateVolumeModal.tsx @@ -154,7 +154,7 @@ export const MigrateVolumeModal: React.FC = ({ ? "Migrate Volume" : "Are you sure you want to migrate this home volume?"; - let buttonText = "Migrate Volume"; + let buttonText = "Migrate volume"; if (loadingMigration) { buttonText = "Migrating"; } else if (onPageOne) { From 1e55dce061ceec50b25fd537a01ab33cc23142d3 Mon Sep 17 00:00:00 2001 From: minnakt Date: Fri, 1 May 2026 17:59:23 -0400 Subject: [PATCH 32/32] cleanup: `version` tests --- .../tests/version/action_buttons.spec.ts | 46 +++++++++++-------- .../playwright/tests/version/banners.spec.ts | 14 +++--- .../tests/version/breadcrumbs.spec.ts | 24 +++++----- .../tests/version/downstream_projects.spec.ts | 4 +- .../tests/version/name_change_modal.spec.ts | 31 ++++++------- .../tests/version/page_tabs.spec.ts | 37 +++++++-------- .../tests/version/restart_modal.spec.ts | 36 +++++++-------- .../playwright/tests/version/routes.spec.ts | 33 ++++++------- .../tests/version/schedule_modal.spec.ts | 8 ++-- .../tests/version/subscription_modal.spec.ts | 19 ++++++-- .../tests/version/task_duration.spec.ts | 25 ++++++---- .../tests/version/task_filters.spec.ts | 3 +- .../tests/version/task_table.spec.ts | 19 +++----- .../tests/version/version_timing.spec.ts | 20 ++------ .../components/Notifications/form/event.ts | 2 +- 15 files changed, 157 insertions(+), 164 deletions(-) diff --git a/apps/spruce/playwright/tests/version/action_buttons.spec.ts b/apps/spruce/playwright/tests/version/action_buttons.spec.ts index 1e54187007..057544a012 100644 --- a/apps/spruce/playwright/tests/version/action_buttons.spec.ts +++ b/apps/spruce/playwright/tests/version/action_buttons.spec.ts @@ -14,7 +14,7 @@ test.describe("Action Buttons", () => { test("Clicking 'Schedule' button shows modal and clicking on 'Cancel' closes it", async ({ authenticatedPage: page, }) => { - await page.getByTestId("schedule-patch").click(); + await page.getByRole("button", { name: "Schedule" }).click(); await expect(page.getByTestId("schedule-tasks-modal")).toBeVisible(); await page.getByRole("button", { name: "Cancel" }).click(); await expect(page.getByTestId("schedule-tasks-modal")).toBeHidden(); @@ -23,11 +23,11 @@ test.describe("Action Buttons", () => { test("Clicking ellipses dropdown shows ellipses options", async ({ authenticatedPage: page, }) => { - await expect(page.getByTestId("ellipses-btn")).toHaveCount(0); - await page.getByTestId("ellipsis-btn").click(); + const ellipsisButton = page.getByTestId("ellipsis-btn"); + await expect(ellipsisButton).toHaveCount(0); + await ellipsisButton.click(); await expect(page.getByTestId("card-dropdown")).toBeVisible(); - - await page.getByTestId("ellipsis-btn").click(); + await ellipsisButton.click(); await expect(page.getByTestId("card-dropdown")).toHaveCount(0); }); }); @@ -52,7 +52,9 @@ test.describe("Action Buttons", () => { }, ], }); - await page.getByTestId("unschedule-patch").click(); + await page + .getByRole("menuitem", { name: "Unschedule all tasks" }) + .click(); await page.getByRole("button", { name: "Yes" }).click(); await validateToast( page, @@ -64,7 +66,9 @@ test.describe("Action Buttons", () => { test("Clicking 'Unschedule' button show popconfirm with abort checkbox and a toast on success", async ({ authenticatedPage: page, }) => { - await page.getByTestId("unschedule-patch").click(); + await page + .getByRole("menuitem", { name: "Unschedule all tasks" }) + .click(); await page.getByRole("button", { name: "Yes" }).click(); await validateToast( page, @@ -77,9 +81,10 @@ test.describe("Action Buttons", () => { authenticatedPage: page, }) => { const priority = "99"; - await page.getByTestId("set-priority-menu-item").click(); - await page.getByTestId("patch-priority-input").fill(priority); - await page.getByTestId("patch-priority-input").press("Enter"); + await page.getByRole("menuitem", { name: "Set patch priority" }).click(); + const patchPriorityInput = page.getByTestId("patch-priority-input"); + await patchPriorityInput.fill(priority); + await patchPriorityInput.press("Enter"); await validateToast(page, "success", priority); }); @@ -96,27 +101,32 @@ test.describe("Action Buttons", () => { }, ], }); - await page.getByTestId("set-priority-menu-item").click(); - await page.getByTestId("patch-priority-input").fill("80"); - await page.getByTestId("patch-priority-input").press("Enter"); + await page.getByRole("menuitem", { name: "Set patch priority" }).click(); + const patchPriorityInput = page.getByTestId("patch-priority-input"); + await patchPriorityInput.fill("80"); + await patchPriorityInput.press("Enter"); await validateToast(page, "error", "Error updating priority for patch"); }); test("Sets priority for multiple tasks when version page table is filtered", async ({ authenticatedPage: page, }) => { - const priority = 10; await page.goto( `${versionPath(mainlineCommit)}/tasks?statuses=failed-umbrella,failed,known-issue`, ); await page.getByTestId("ellipsis-btn").click(); await expect(page.getByTestId("card-dropdown")).toBeVisible(); - await expect(page.getByTestId("set-priority-menu-item")).toContainText( + + const setTaskPriorityButton = page.getByRole("menuitem", { + name: "Set task priority", + }); + await expect(setTaskPriorityButton).toContainText( "Set task priority (2)", ); - await page.getByTestId("set-priority-menu-item").click(); - await page.getByTestId("task-priority-input").fill(`${priority}`); - await page.getByTestId("task-priority-input").press("Enter"); + await setTaskPriorityButton.click(); + const taskPriorityInput = page.getByTestId("task-priority-input"); + await taskPriorityInput.fill("10"); + await taskPriorityInput.press("Enter"); await validateToast(page, "success", "Priority updated for 2 tasks."); }); diff --git a/apps/spruce/playwright/tests/version/banners.spec.ts b/apps/spruce/playwright/tests/version/banners.spec.ts index 464a963f86..135c54f156 100644 --- a/apps/spruce/playwright/tests/version/banners.spec.ts +++ b/apps/spruce/playwright/tests/version/banners.spec.ts @@ -12,11 +12,10 @@ test.describe("banners", () => { test("should display the number of configuration errors", async ({ authenticatedPage: page, }) => { + const errorBanner = page.getByTestId("configuration-errors-banner"); + await expect(errorBanner).toBeVisible(); await expect( - page.getByTestId("configuration-errors-banner"), - ).toBeVisible(); - await expect( - page.getByText("4 errors in configuration file"), + errorBanner.getByText("4 errors in configuration file"), ).toBeVisible(); }); @@ -39,11 +38,10 @@ test.describe("banners", () => { test("should display the number of configuration warnings", async ({ authenticatedPage: page, }) => { + const warningBanner = page.getByTestId("configuration-warnings-banner"); + await expect(warningBanner).toBeVisible(); await expect( - page.getByTestId("configuration-warnings-banner"), - ).toBeVisible(); - await expect( - page.getByText("3 warnings in configuration file"), + warningBanner.getByText("3 warnings in configuration file"), ).toBeVisible(); }); diff --git a/apps/spruce/playwright/tests/version/breadcrumbs.spec.ts b/apps/spruce/playwright/tests/version/breadcrumbs.spec.ts index 7782937b02..b7bb53dae1 100644 --- a/apps/spruce/playwright/tests/version/breadcrumbs.spec.ts +++ b/apps/spruce/playwright/tests/version/breadcrumbs.spec.ts @@ -14,8 +14,9 @@ test.describe("Viewing a patch requester", () => { test("Clicking on the display task breadcrumb should take you to that task", async ({ authenticatedPage: page, }) => { - await expect(page.getByTestId("bc-display-task")).toContainText("asdf"); - await page.getByTestId("bc-display-task").click(); + const breadcrumb = page.getByTestId("bc-display-task"); + await expect(breadcrumb).toContainText("asdf"); + await breadcrumb.click(); await expect(page).toHaveURL(new RegExp(`/task/${displayTaskId}`)); }); @@ -37,10 +38,9 @@ test.describe("Viewing a patch requester", () => { test("Clicking on the patch name breadcrumb from a task should take you to that version", async ({ authenticatedPage: page, }) => { - await expect(page.getByTestId("bc-message")).toContainText( - "Patch 1251 - dist", - ); - await page.getByTestId("bc-message").click(); + const breadcrumb = page.getByTestId("bc-message"); + await expect(breadcrumb).toContainText("Patch 1251 - dist"); + await breadcrumb.click(); await expect(page).toHaveURL( "/version/5ecedafb562343215a7ff297/tasks?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC", ); @@ -65,10 +65,9 @@ test.describe("Viewing a mainline commit requester", () => { test("Clicking the commit message breadcrumb from a task should take you to that version", async ({ authenticatedPage: page, }) => { - await expect(page.getByTestId("bc-message")).toContainText( - "5e823e1 - 'ever…reen/pull/3186)", - ); - await page.getByTestId("bc-message").click(); + const breadcrumb = page.getByTestId("bc-message"); + await expect(breadcrumb).toContainText("5e823e1 - 'ever…reen/pull/3186)"); + await breadcrumb.click(); await expect(page).toHaveURL( "/version/5e4ff3abe3c3317e352062e4/tasks?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC", ); @@ -77,8 +76,9 @@ test.describe("Viewing a mainline commit requester", () => { test("Clicking on the commits link should take you to that versions waterfall", async ({ authenticatedPage: page, }) => { - await expect(page.getByTestId("bc-waterfall")).toContainText("evergreen"); - await page.getByTestId("bc-waterfall").click(); + const breadcrumb = page.getByTestId("bc-waterfall"); + await expect(breadcrumb).toContainText("evergreen"); + await breadcrumb.click(); await expect(page).toHaveURL("/project/evergreen/waterfall"); }); }); diff --git a/apps/spruce/playwright/tests/version/downstream_projects.spec.ts b/apps/spruce/playwright/tests/version/downstream_projects.spec.ts index 39bda0641e..b1dea3467e 100644 --- a/apps/spruce/playwright/tests/version/downstream_projects.spec.ts +++ b/apps/spruce/playwright/tests/version/downstream_projects.spec.ts @@ -31,7 +31,9 @@ test.describe("Downstream Projects Tab", () => { ); }); - test("filters by task name", async ({ authenticatedPage: page }) => { + test("filters by task name but doesn't update URL", async ({ + authenticatedPage: page, + }) => { await page.getByTestId("task-name-filter").nth(1).click(); const taskInput = page .getByTestId("task-name-filter-wrapper") diff --git a/apps/spruce/playwright/tests/version/name_change_modal.spec.ts b/apps/spruce/playwright/tests/version/name_change_modal.spec.ts index 9b8637411a..44c7b163bb 100644 --- a/apps/spruce/playwright/tests/version/name_change_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/name_change_modal.spec.ts @@ -14,10 +14,11 @@ test.describe("Name change modal", () => { await page.getByTestId("name-change-modal-trigger").click(); const newName = "a different name"; - await page.locator("textarea").clear(); - await page.locator("textarea").fill(newName); + const nameInput = page.locator("textarea"); + await nameInput.clear(); + await nameInput.fill(newName); await page.getByRole("button", { name: "Confirm" }).click(); - await expect(page.locator("textarea")).toBeHidden(); + await expect(nameInput).toBeHidden(); await expect(page.getByText(newName)).toBeVisible(); await validateToast( page, @@ -30,25 +31,19 @@ test.describe("Name change modal", () => { authenticatedPage: page, }) => { await page.getByTestId("name-change-modal-trigger").click(); - await page.locator("textarea").clear(); - await expect(page.getByRole("button", { name: "Confirm" })).toHaveAttribute( - "aria-disabled", - "true", - ); + const nameInput = page.locator("textarea"); + const confirmButton = page.getByRole("button", { name: "Confirm" }); - await page.locator("textarea").fill("lol"); - await expect(page.getByRole("button", { name: "Confirm" })).toHaveAttribute( - "aria-disabled", - "false", - ); + await nameInput.clear(); + await expect(confirmButton).toBeDisabled(); + + await nameInput.fill("lol"); + await expect(confirmButton).toBeEnabled(); const over300Chars = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - await page.locator("textarea").fill(over300Chars); - await expect(page.getByRole("button", { name: "Confirm" })).toHaveAttribute( - "aria-disabled", - "true", - ); + await nameInput.fill(over300Chars); + await expect(confirmButton).toBeDisabled(); await expect( page.getByText("Value cannot exceed 300 characters"), ).toBeVisible(); diff --git a/apps/spruce/playwright/tests/version/page_tabs.spec.ts b/apps/spruce/playwright/tests/version/page_tabs.spec.ts index 46534f2a22..6736030f0b 100644 --- a/apps/spruce/playwright/tests/version/page_tabs.spec.ts +++ b/apps/spruce/playwright/tests/version/page_tabs.spec.ts @@ -3,9 +3,9 @@ import { test, expect } from "../../fixtures"; const versionId = "5ecedafb562343215a7ff297"; const versionRoute = `/version/${versionId}`; const versions = { - changes: { route: `${versionRoute}/changes`, button: "changes-tab" }, - tasks: { route: `${versionRoute}/tasks`, button: "task-tab" }, - duration: { route: `${versionRoute}/task-duration`, button: "duration-tab" }, + changes: { route: `${versionRoute}/changes`, name: "Changes" }, + tasks: { route: `${versionRoute}/tasks`, name: "Tasks" }, + duration: { route: `${versionRoute}/task-duration`, name: "Duration" }, }; test.describe("page tabs", () => { @@ -13,10 +13,9 @@ test.describe("page tabs", () => { authenticatedPage: page, }) => { await page.goto(versionRoute); - await expect(page.getByTestId(versions.tasks.button)).toHaveAttribute( - "aria-selected", - "true", - ); + await expect( + page.getByRole("tab", { name: versions.tasks.name }), + ).toHaveAttribute("aria-selected", "true"); await expect(page).toHaveURL( `${versions.tasks.route}?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC`, ); @@ -26,10 +25,9 @@ test.describe("page tabs", () => { authenticatedPage: page, }) => { await page.goto(`${versionRoute}/task-duration`); - await expect(page.getByTestId(versions.duration.button)).toHaveAttribute( - "aria-selected", - "true", - ); + await expect( + page.getByRole("tab", { name: versions.duration.name }), + ).toHaveAttribute("aria-selected", "true"); await expect(page).toHaveURL( `${versions.duration.route}?sorts=DURATION%3ADESC`, ); @@ -39,13 +37,11 @@ test.describe("page tabs", () => { authenticatedPage: page, }) => { await page.goto(`${versionRoute}/changes`); - await expect(page.getByTestId(versions.changes.button)).toHaveAttribute( - "aria-selected", - "true", - ); + await expect( + page.getByRole("tab", { name: versions.changes.name }), + ).toHaveAttribute("aria-selected", "true"); await expect(page).toHaveURL(versions.changes.route); - - await page.getByTestId("task-tab").first().click(); + await page.getByRole("tab", { name: versions.tasks.name }).first().click(); await expect(page).toHaveURL( `${versions.tasks.route}?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC`, ); @@ -56,8 +52,7 @@ test.describe("page tabs", () => { }) => { await page.goto(versionRoute); await expect(page).toHaveURL(/sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC/); - - await page.getByTestId(versions.changes.button).click(); + await page.getByRole("tab", { name: versions.changes.name }).click(); await expect(page).toHaveURL( `${versions.changes.route}?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC`, ); @@ -76,9 +71,9 @@ test.describe("page tabs", () => { authenticatedPage: page, }) => { await page.goto(versionRoute); - await page.getByTestId(versions.changes.button).click(); + await page.getByRole("tab", { name: versions.changes.name }).click(); await expect(page.getByTestId("code-changes")).toBeVisible(); - await page.getByTestId(versions.tasks.button).click(); + await page.getByRole("tab", { name: versions.tasks.name }).click(); await expect(page.getByTestId("total-count")).toBeVisible(); }); }); diff --git a/apps/spruce/playwright/tests/version/restart_modal.spec.ts b/apps/spruce/playwright/tests/version/restart_modal.spec.ts index 6f2ecaf660..bced8ec902 100644 --- a/apps/spruce/playwright/tests/version/restart_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/restart_modal.spec.ts @@ -45,10 +45,10 @@ test.describe("version/restart_modal", () => { await expect(taskStatusBadge).toContainText("0 of 1 Selected"); const selectUbuntuCheckbox = page .getByTestId("version-restart-modal") - .getByText("Ubuntu 16.04"); - await selectUbuntuCheckbox.click(); + .getByRole("checkbox", { name: "Ubuntu 16.04" }); + await clickCheckbox(selectUbuntuCheckbox); await expect(taskStatusBadge).toContainText("1 of 1 Selected"); - await selectUbuntuCheckbox.click(); + await clickCheckbox(selectUbuntuCheckbox); await expect(taskStatusBadge).toContainText("0 of 1 Selected"); }); @@ -58,8 +58,8 @@ test.describe("version/restart_modal", () => { await page.getByTestId("variant-accordion").first().click(); const taskCheckbox = page .getByTestId("version-restart-modal") - .getByText("dist"); - await taskCheckbox.click(); + .getByRole("checkbox", { name: "dist" }); + await clickCheckbox(taskCheckbox); await expect( page .getByTestId("version-restart-modal") @@ -116,8 +116,8 @@ test.describe("version/restart_modal", () => { await modal.getByTestId("variant-accordion").first().click(); const taskCheckbox = page .getByTestId("version-restart-modal") - .getByText("dist"); - await taskCheckbox.click(); + .getByRole("checkbox", { name: "dist" }); + await clickCheckbox(taskCheckbox); await modal.getByRole("button", { name: "Restart" }).click(); await expect(page.getByTestId("version-restart-modal")).toBeHidden(); await validateToast(page, "success", "Successfully restarted tasks!"); @@ -131,21 +131,19 @@ test.describe("version/restart_modal", () => { await page.goto( "/version/spruce_ab494436448fbb1d244833046ea6f6af1544e86d", ); - await expect(page.getByTestId("restart-version")).toHaveAttribute( - "aria-disabled", - "false", - ); - await page.getByTestId("restart-version").click(); + const restartButton = page.getByRole("button", { name: "Restart" }); + await expect(restartButton).toBeEnabled(); + await restartButton.click(); await expect(page.getByTestId("version-restart-modal")).toBeVisible(); - const modal = page.getByTestId("version-restart-modal"); await modal.getByTestId("accordion-toggle").click(); - const taskCheckbox = modal.getByText("check_codegen"); - - await taskCheckbox.click(); - const restartButton = modal.getByRole("button", { name: "Restart" }); - await expect(restartButton).toHaveAttribute("aria-disabled", "false"); - await restartButton.click(); + const taskCheckbox = modal.getByRole("checkbox", { + name: "check_codegen", + }); + await clickCheckbox(taskCheckbox); + const confirmButton = modal.getByRole("button", { name: "Restart" }); + await expect(confirmButton).toHaveAttribute("aria-disabled", "false"); + await confirmButton.click(); await validateToast(page, "success", "Successfully restarted tasks!"); }); }); diff --git a/apps/spruce/playwright/tests/version/routes.spec.ts b/apps/spruce/playwright/tests/version/routes.spec.ts index 77eea3b7bd..4f7cca30a8 100644 --- a/apps/spruce/playwright/tests/version/routes.spec.ts +++ b/apps/spruce/playwright/tests/version/routes.spec.ts @@ -46,7 +46,7 @@ test.describe("Version route", () => { await page.goto(versionRoute(versions[0])); const table = page.getByTestId("tasks-table"); await expect(table).toBeVisible(); - await expect(table).not.toHaveAttribute("data-loading", "true"); + await expect(table).toHaveAttribute("data-loading", "false"); }); test("Lists the patch's build variants", async ({ @@ -75,21 +75,18 @@ test.describe("Version route", () => { test("Navigates to task tab and applies filters when clicking on grouped task status badge", async ({ authenticatedPage: page, }) => { - await page.getByTestId("changes-tab").first().click(); - await expect(page.getByTestId("task-tab")).toHaveAttribute( - "aria-selected", - "false", - ); + const changesTab = page.getByRole("tab", { name: "Changes" }); + const tasksTab = page.getByRole("tab", { name: "Tasks" }); + + await changesTab.first().click(); + await expect(tasksTab).toHaveAttribute("aria-selected", "false"); await page .getByTestId("build-variants") .getByTestId("grouped-task-status-badge") .first() .click(); - await expect(page.getByTestId("task-tab")).toHaveAttribute( - "aria-selected", - "true", - ); + await expect(tasksTab).toHaveAttribute("aria-selected", "true"); await expect(page).toHaveURL( /sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&statuses=success&variant=%5Eubuntu1604%24/, ); @@ -137,17 +134,14 @@ test.describe("Version route", () => { }) => { await page.getByTestId("clear-all-filters").click(); - await page.getByTestId("changes-tab").first().click(); - await expect(page.getByTestId("task-tab")).toHaveAttribute( - "aria-selected", - "false", - ); + const changesTab = page.getByRole("tab", { name: "Changes" }); + const tasksTab = page.getByRole("tab", { name: "Tasks" }); + + await changesTab.first().click(); + await expect(tasksTab).toHaveAttribute("aria-selected", "false"); await page.getByTestId("build-variant-display-name").first().click(); - await expect(page.getByTestId("task-tab")).toHaveAttribute( - "aria-selected", - "true", - ); + await expect(tasksTab).toHaveAttribute("aria-selected", "true"); await expect(page).toHaveURL( /sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC&variant=%5Eubuntu1604%24/, ); @@ -162,7 +156,6 @@ test.describe("Version route", () => { authenticatedPage: page, }) => { await page.getByTestId("clear-all-filters").click(); - await page.getByTestId("task-name-filter").click(); const taskNameInput = page .getByTestId("task-name-filter-wrapper") diff --git a/apps/spruce/playwright/tests/version/schedule_modal.spec.ts b/apps/spruce/playwright/tests/version/schedule_modal.spec.ts index 0024ac824f..129baeb6cb 100644 --- a/apps/spruce/playwright/tests/version/schedule_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/schedule_modal.spec.ts @@ -7,7 +7,7 @@ test.describe("Restarting and scheduling mainline commits", () => { }) => { await page.goto("/version/spruce_e695f654c8b4b959d3e12e71696c3e318bcd4c33"); - const schedulePatchButton = page.getByTestId("schedule-patch"); + const schedulePatchButton = page.getByRole("button", { name: "Schedule" }); await expect(schedulePatchButton).toBeVisible(); await expect(schedulePatchButton).toBeEnabled(); await schedulePatchButton.click(); @@ -18,9 +18,9 @@ test.describe("Restarting and scheduling mainline commits", () => { const taskCheckbox = modal.getByText("check_codegen"); await taskCheckbox.click(); - const scheduleButton = modal.getByRole("button", { name: "Schedule" }); - await expect(scheduleButton).toHaveAttribute("aria-disabled", "false"); - await scheduleButton.click(); + const confirmButton = modal.getByRole("button", { name: "Schedule" }); + await expect(confirmButton).toHaveAttribute("aria-disabled", "false"); + await confirmButton.click(); await validateToast(page, "success", "Successfully scheduled tasks!"); }); }); diff --git a/apps/spruce/playwright/tests/version/subscription_modal.spec.ts b/apps/spruce/playwright/tests/version/subscription_modal.spec.ts index 134a974a03..991185c5fe 100644 --- a/apps/spruce/playwright/tests/version/subscription_modal.spec.ts +++ b/apps/spruce/playwright/tests/version/subscription_modal.spec.ts @@ -183,7 +183,10 @@ test.describe("Version Subscription Modal", () => { "A build-variant in this version finishes", ); await expect(page.getByTestId("regex-selector-row")).toHaveCount(0); - await page.getByText("Add Additional Criteria").click(); + const addCriteriaButton = page.getByRole("button", { + name: "Add additional criteria", + }); + await addCriteriaButton.click(); await expect(page.getByTestId("regex-selector-row")).toHaveCount(1); await page.getByTestId("delete-item-button").first().click(); await expect(page.getByTestId("regex-selector-row")).toHaveCount(0); @@ -198,9 +201,12 @@ test.describe("Version Subscription Modal", () => { "Event", "A build-variant in this version finishes", ); - await page.getByText("Add Additional Criteria").click(); + const addCriteriaButton = page.getByRole("button", { + name: "Add additional criteria", + }); + await addCriteriaButton.click(); await expect(page.getByText("Build Variant ID")).toBeVisible(); - await page.getByText("Add Additional Criteria").click(); + await addCriteriaButton.click(); await expect(page.getByText("Build Variant Name")).toBeVisible(); await page.getByTestId("regex-select").last().click(); await expect( @@ -235,7 +241,10 @@ test.describe("Version Subscription Modal", () => { "Event", "A build-variant in this version finishes", ); - await page.getByText("Add Additional Criteria").click(); + const addCriteriaButton = page.getByRole("button", { + name: "Add additional criteria", + }); + await addCriteriaButton.click(); await selectOption(page, "Field name", "Build Variant Name"); await page.getByTestId("regex-input").fill("stuff"); await page.getByTestId("jira-comment-input").fill("EVG-2000"); @@ -253,7 +262,7 @@ test.describe("Version Subscription Modal", () => { "A build-variant in this version finishes", ); const addCriteriaButton = page.getByRole("button", { - name: "Add Additional Criteria", + name: "Add additional criteria", }); await expect(addCriteriaButton).toHaveAttribute("aria-disabled", "false"); await addCriteriaButton.click(); diff --git a/apps/spruce/playwright/tests/version/task_duration.spec.ts b/apps/spruce/playwright/tests/version/task_duration.spec.ts index 48ba4e594d..51caae8762 100644 --- a/apps/spruce/playwright/tests/version/task_duration.spec.ts +++ b/apps/spruce/playwright/tests/version/task_duration.spec.ts @@ -1,3 +1,4 @@ +import { clickCheckbox } from "@evg-ui/playwright-config/helpers"; import { test, expect } from "../../fixtures"; test.describe("Task Duration Tab", () => { @@ -9,10 +10,9 @@ test.describe("Task Duration Tab", () => { test("updates URL appropriately when task name filter is applied", async ({ authenticatedPage: page, }) => { - const filterText = "test-annotation"; await page.getByTestId("task-name-filter-popover").click(); const filterInput = page.getByPlaceholder("Task name regex"); - await filterInput.fill(filterText); + await filterInput.fill("test-annotation"); await filterInput.press("Enter"); await expect(page.getByTestId("task-duration-table-row")).toHaveCount(1); await expect(page).toHaveURL( @@ -33,13 +33,24 @@ test.describe("Task Duration Tab", () => { await page.getByTestId("status-filter-popover").click(); const options = page.getByTestId("tree-select-options"); await expect(options).toBeVisible(); - await options.getByText("Running").first().click(); + + const runningCheckbox = options + .getByRole("checkbox", { + name: "Running", + }) + .first(); + await clickCheckbox(runningCheckbox); + await expect(page.getByTestId("task-duration-table-row")).toHaveCount(3); await expect(page).toHaveURL( /statuses=running-umbrella,started,dispatched/, ); await expect(options).toBeVisible(); - await options.getByText("Succeeded").click(); + + const succeededCheckbox = options.getByRole("checkbox", { + name: "Succeeded", + }); + await clickCheckbox(succeededCheckbox); await expect(page).toHaveURL( /statuses=running-umbrella,started,dispatched,success/, ); @@ -48,10 +59,9 @@ test.describe("Task Duration Tab", () => { test("updates URL appropriately when build variant filter is applied", async ({ authenticatedPage: page, }) => { - const filterText = "Lint"; await page.getByTestId("build-variant-filter-popover").click(); const filterInput = page.getByPlaceholder("Build variant regex"); - await filterInput.fill(filterText); + await filterInput.fill("Lint"); await filterInput.press("Enter"); await expect(page.getByTestId("task-duration-table-row")).toHaveCount(2); await expect(page).toHaveURL(/page=0&sorts=DURATION%3ADESC&variant=Lint/); @@ -131,10 +141,9 @@ test.describe("Task Duration Tab", () => { test("shows message when no test results are found", async ({ authenticatedPage: page, }) => { - const filterText = "this_does_not_exist"; await page.getByTestId("task-name-filter-popover").click(); const filterInput = page.getByPlaceholder("Task name regex"); - await filterInput.fill(filterText); + await filterInput.fill("this_does_not_exist"); await filterInput.press("Enter"); await expect( page.getByTestId("task-name-filter-popover-task-duration-table-row"), diff --git a/apps/spruce/playwright/tests/version/task_filters.spec.ts b/apps/spruce/playwright/tests/version/task_filters.spec.ts index d466c57f30..e46e9ca1fb 100644 --- a/apps/spruce/playwright/tests/version/task_filters.spec.ts +++ b/apps/spruce/playwright/tests/version/task_filters.spec.ts @@ -10,7 +10,7 @@ const defaultPath = `${pathTasks}?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC`; const waitForTaskTable = async (page: Page) => { const table = page.getByTestId("tasks-table"); await expect(table).toBeVisible(); - await expect(table).not.toHaveAttribute("data-loading", "true"); + await expect(table).toHaveAttribute("data-loading", "false"); }; test.describe("Tasks filters", () => { @@ -136,6 +136,7 @@ test.describe("Tasks filters", () => { await expect(page).toHaveURL(/statuses=failed/); await waitForTaskTable(page); await expect(page.getByTestId("filtered-count")).toHaveText("2"); + const succeededCheckbox = options.getByRole("checkbox", { name: "Succeeded", }); diff --git a/apps/spruce/playwright/tests/version/task_table.spec.ts b/apps/spruce/playwright/tests/version/task_table.spec.ts index 6f768d70df..0f1ed059f6 100644 --- a/apps/spruce/playwright/tests/version/task_table.spec.ts +++ b/apps/spruce/playwright/tests/version/task_table.spec.ts @@ -9,7 +9,7 @@ const patchDescriptionTasksExist = "dist"; const waitForTaskTable = async (page: Page) => { const table = page.getByTestId("tasks-table"); await expect(table).toBeVisible(); - await expect(table).not.toHaveAttribute("data-loading", "true"); + await expect(table).toHaveAttribute("data-loading", "false"); }; test.describe("Task table", () => { @@ -115,10 +115,7 @@ test.describe("Task table", () => { }) => { await page.goto(`${pathTasks}?page=0`); await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); - await expect(page.getByTestId("prev-page-button")).toHaveAttribute( - "aria-disabled", - "true", - ); + await expect(page.getByTestId("prev-page-button")).toBeDisabled(); }); test("Does not update results or URL when right arrow is clicked and next page does not exist", async ({ @@ -126,10 +123,7 @@ test.describe("Task table", () => { }) => { await page.goto(`${pathTasks}?page=4`); await expect(page.getByTestId("tasks-table-row").first()).toBeVisible(); - await expect(page.getByTestId("next-page-button")).toHaveAttribute( - "aria-disabled", - "true", - ); + await expect(page.getByTestId("next-page-button")).toBeDisabled(); }); }); @@ -142,10 +136,11 @@ test.describe("Task table", () => { test("shows the blocking tasks when hovering over status badge", async ({ authenticatedPage: page, }) => { - await expect(page.getByTestId("depends-on-tooltip")).toHaveCount(0); + const dependsOnTooltip = page.getByTestId("depends-on-tooltip"); + await expect(dependsOnTooltip).toHaveCount(0); await page.getByTestId("task-status-badge").getByText("Blocked").hover(); - await expect(page.getByTestId("depends-on-tooltip")).toBeVisible(); - await expect(page.getByTestId("depends-on-tooltip")).toContainText( + await expect(dependsOnTooltip).toBeVisible(); + await expect(dependsOnTooltip).toContainText( `Depends on tasks: “test-migrations”, “test-graphql”`, ); }); diff --git a/apps/spruce/playwright/tests/version/version_timing.spec.ts b/apps/spruce/playwright/tests/version/version_timing.spec.ts index 43b706b409..352bb020d6 100644 --- a/apps/spruce/playwright/tests/version/version_timing.spec.ts +++ b/apps/spruce/playwright/tests/version/version_timing.spec.ts @@ -30,18 +30,9 @@ test.describe("Version Timing Tab without a variant selected", () => { await expect( page.locator("button[aria-labelledby='page-size-select']"), ).toHaveAttribute("aria-disabled", "true"); - await expect(page.getByTestId("next-page-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - await expect(page.getByTestId("prev-page-button")).toHaveAttribute( - "aria-disabled", - "true", - ); - await expect(page.getByTestId("clear-all-filters")).toHaveAttribute( - "aria-disabled", - "true", - ); + await expect(page.getByTestId("next-page-button")).toBeDisabled(); + await expect(page.getByTestId("prev-page-button")).toBeDisabled(); + await expect(page.getByTestId("clear-all-filters")).toBeDisabled(); }); }); @@ -141,10 +132,7 @@ test.describe("Version Timing Tab with a variant selected", () => { await page.goto( "/version/5e4ff3abe3c3317e352062e4/version-timing?taskName=agent&variant=^ubuntu1604%24", ); - await expect(page.getByTestId("next-page-button")).toHaveAttribute( - "aria-disabled", - "true", - ); + await expect(page.getByTestId("next-page-button")).toBeDisabled(); await expect( page.locator(chart).getByText("test-agent", { exact: true }), ).toBeVisible(); diff --git a/apps/spruce/src/components/Notifications/form/event.ts b/apps/spruce/src/components/Notifications/form/event.ts index 900ebaf313..d9cfad57f1 100644 --- a/apps/spruce/src/components/Notifications/form/event.ts +++ b/apps/spruce/src/components/Notifications/form/event.ts @@ -464,7 +464,7 @@ export const getEventSchema = ( "Regex can be specified for at most one name and one ID.", "ui:orderable": false, "ui:addToEnd": true, - "ui:addButtonText": "Add Additional Criteria", + "ui:addButtonText": "Add additional criteria", items: { "ui:ObjectFieldTemplate": RegexSelectorRow, "ui:label": false,