From e7b34c5792332a29d58b9b8e849067fea92028cc Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Thu, 3 Aug 2023 14:48:15 +0800 Subject: [PATCH] Test/review requests (#1379) * feat: add utils for review request * fix: allow remove collaborators to work with more than 2 additional collaborators * feat: add addAdminCollaborator utils * feat: add additional api utils * Fix: sets removeOtherCollaborators to remove only the second collaborator * feat: add review request tests * fix: revert e2e_email_collab * chore: remame e2e_email_collab_2 * chore: rename removeOtherCollaborators * nit: modify comments * feat: add check for automatic redirect * refactor: move common interceptors into util method * fix: remove unnecessary waits and change wait for dropdown * feat: add additional test case for collaborators not approving * fix: add removeOtherCollaborators * chore: move cypress support method to utils * fix: remove unused import --- cypress/api/pages.api.ts | 142 +++++++ cypress/api/reviewRequest.api.ts | 10 +- cypress/e2e/collaborators.spec.ts | 6 +- cypress/e2e/comments.spec.ts | 4 +- cypress/e2e/reviewRequests.spec.ts | 592 +++++++++++++++++++++++++---- cypress/fixtures/constants.ts | 8 + cypress/support/index.d.ts | 7 + cypress/support/session.ts | 13 +- cypress/utils/collaborators.ts | 30 +- cypress/utils/index.ts | 1 + cypress/utils/reviewRequests.ts | 11 + 11 files changed, 745 insertions(+), 79 deletions(-) create mode 100644 cypress/utils/reviewRequests.ts diff --git a/cypress/api/pages.api.ts b/cypress/api/pages.api.ts index 119469bbd..0f8d0fb21 100644 --- a/cypress/api/pages.api.ts +++ b/cypress/api/pages.api.ts @@ -1,6 +1,71 @@ +import { DirectoryData } from "types/directory" + import { UnlinkedPageDto } from "../../src/types/pages" import { BACKEND_URL } from "../fixtures/constants" +export const listCollectionPages = ( + site: string, + collectionName: string +): Cypress.Chainable => { + return cy + .request( + "GET", + `${BACKEND_URL}/sites/${site}/collections/${collectionName}` + ) + .then(({ body }) => body) +} + +export const listUnlinkedPages = ( + site: string +): Cypress.Chainable => { + return cy + .request("GET", `${BACKEND_URL}/sites/${site}/pages`) + .then(({ body }) => body) +} + +export const addCollectionPage = ( + pageName: string, + collectionName: string, + title: string, + permalink: string, + pageContent: string, + site: string +): void => { + cy.request( + "POST", + `${BACKEND_URL}/sites/${site}/collections/${collectionName}/pages`, + { + content: { + frontMatter: { + title, + permalink, + }, + pageBody: pageContent, + }, + newFileName: pageName, + } + ) +} + +export const addUnlinkedPage = ( + pageName: string, + title: string, + permalink: string, + pageContent: string, + site: string +): void => { + cy.request("POST", `${BACKEND_URL}/sites/${site}/pages/pages`, { + content: { + frontMatter: { + title, + permalink, + }, + pageBody: pageContent, + }, + newFileName: pageName, + }) +} + export const editUnlinkedPage = ( pageName: string, pageContent: string, @@ -29,3 +94,80 @@ export const readUnlinkedPage = ( .request("GET", `${BACKEND_URL}/sites/${site}/pages/pages/${pageName}`) .then(({ body }) => body) } + +export const readCollectionPage = ( + pageName: string, + collectionName: string, + site: string +): Cypress.Chainable => { + return cy + .request( + "GET", + `${BACKEND_URL}/sites/${site}/collections/${collectionName}/pages/${pageName}` + ) + .then(({ body }) => body) +} + +export const deleteUnlinkedPage = (pageName: string, site: string): void => { + readUnlinkedPage(pageName, site).then(({ sha }) => { + return cy.request( + "DELETE", + `${BACKEND_URL}/sites/${site}/pages/pages/${pageName}`, + { + sha, + } + ) + }) +} + +export const deleteCollectionPage = ( + pageName: string, + collectionName: string, + site: string +): void => { + readCollectionPage(pageName, collectionName, site).then(({ sha }) => { + return cy.request( + "DELETE", + `${BACKEND_URL}/sites/${site}/collections/${collectionName}/pages/${pageName}`, + { + sha, + } + ) + }) +} + +export const renameUnlinkedPage = ( + pageName: string, + newPageName: string, + site: string +): void => { + readUnlinkedPage(pageName, site).then(({ sha, content }) => { + return cy.request( + "POST", + `${BACKEND_URL}/sites/${site}/pages/pages/${pageName}`, + { + content, + sha, + newFileName: newPageName, + } + ) + }) +} + +export const moveUnlinkedPage = ( + pageName: string, + targetCollectionName: string, + site: string +): void => { + cy.request("POST", `${BACKEND_URL}/sites/${site}/pages/move`, { + target: { + collectionName: targetCollectionName, + }, + items: [ + { + name: pageName, + type: "file", + }, + ], + }) +} diff --git a/cypress/api/reviewRequest.api.ts b/cypress/api/reviewRequest.api.ts index 98e92b715..59b6d3cd0 100644 --- a/cypress/api/reviewRequest.api.ts +++ b/cypress/api/reviewRequest.api.ts @@ -21,7 +21,9 @@ export const createReviewRequest = ( .then(({ body }) => body.pullRequestNumber) } -export const listReviewRequests = (): Cypress.Chainable<{ id: number }[]> => { +export const listReviewRequests = (): Cypress.Chainable< + { id: number; author: string; status: string }[] +> => { return cy .request("GET", `${BASE_URL}/summary`) .then(({ body }) => body.reviews) @@ -29,8 +31,10 @@ export const listReviewRequests = (): Cypress.Chainable<{ id: number }[]> => { export const closeReviewRequests = (): void => { listReviewRequests().then((reviewRequests) => { - reviewRequests.forEach(({ id }) => { - closeReviewRequest(id) + reviewRequests.forEach(({ id, author }) => { + // Only the requestor can close their own review request + cy.actAsEmailUser(author, "Email admin") + cy.request("DELETE", `${BASE_URL}/${id}`) }) }) } diff --git a/cypress/e2e/collaborators.spec.ts b/cypress/e2e/collaborators.spec.ts index d90787524..110eb03fa 100644 --- a/cypress/e2e/collaborators.spec.ts +++ b/cypress/e2e/collaborators.spec.ts @@ -17,7 +17,7 @@ import { addCollaborator, getCollaboratorsModal, inputCollaborators, - removeOtherCollaborators, + removeFirstCollaborator, } from "../utils/collaborators" const collaborator = E2E_EMAIL_COLLAB.email @@ -62,7 +62,7 @@ describe("collaborators flow", () => { }) describe("Admin adding a collaborator", () => { - after(() => removeOtherCollaborators()) + after(() => removeFirstCollaborator()) it("should not be able to click the add collaborator button when the input is empty", () => { // Act @@ -137,7 +137,7 @@ describe("collaborators flow", () => { it("should not be able to remove the last site member", () => { // Act // NOTE: Remove all collaborators except the initial admin - removeOtherCollaborators() + removeFirstCollaborator() // Assert cy.get(DELETE_COLLABORATOR_BUTTON_SELECTOR).should("be.disabled") diff --git a/cypress/e2e/comments.spec.ts b/cypress/e2e/comments.spec.ts index 19546e82b..402de936f 100644 --- a/cypress/e2e/comments.spec.ts +++ b/cypress/e2e/comments.spec.ts @@ -15,7 +15,7 @@ import { ignoreNotFoundError, openCommentsDrawer, openReviewRequest, - removeOtherCollaborators, + removeFirstCollaborator, setUserAsUnauthorised, visitE2eEmailTestRepo, getCommentInput, @@ -201,7 +201,7 @@ describe("Comments", () => { .then((id) => { reviewId = id }) - removeOtherCollaborators() + removeFirstCollaborator() }) // This is required so that subsequent tests do not fail diff --git a/cypress/e2e/reviewRequests.spec.ts b/cypress/e2e/reviewRequests.spec.ts index c0acf18ea..bf46f9511 100644 --- a/cypress/e2e/reviewRequests.spec.ts +++ b/cypress/e2e/reviewRequests.spec.ts @@ -1,30 +1,54 @@ import { addAdmin, approveReviewRequest, + closeReviewRequest, closeReviewRequests, createReviewRequest, - listReviewRequests, + mergeReviewRequest, } from "../api" -import { editUnlinkedPage } from "../api/pages.api" import { - E2E_EMAIL_COLLAB, - E2E_EMAIL_REPO_STAGING_LINK, + addUnlinkedPage, + deleteUnlinkedPage, + editUnlinkedPage, + listCollectionPages, + listUnlinkedPages, + moveUnlinkedPage, + deleteCollectionPage, + renameUnlinkedPage, +} from "../api/pages.api" +import { + E2E_EMAIL_ADMIN, + E2E_EMAIL_COLLAB_NON_GOV, + E2E_EMAIL_ADMIN_2, E2E_EMAIL_TEST_SITE, MOCK_REVIEW_DESCRIPTION, MOCK_REVIEW_TITLE, } from "../fixtures/constants" -import { USER_TYPES } from "../fixtures/users" import { + addAdminCollaborator, addCollaborator, + awaitDashboardLoad, + awaitReviewRequestSummary, removeOtherCollaborators, visitE2eEmailTestRepo, } from "../utils" const getReviewRequestButton = () => cy.contains("button", "Request a Review") const getSubmitReviewButton = () => cy.contains("button", "Submit Review") +const getViewReviewRequestButton = () => cy.contains("p", "changed file") +const getReviewRequestOverviewPageEditButton = (itemName: string) => + cy + .contains(itemName) + .parent() + .parent() + .parent() + .find('[aria-label="edit file"]') +const getPublishButton = () => cy.contains("button", "Publish now") +const openReviewRequestStateDropdown = (currentState: string) => + cy.contains("button", currentState).next().click() const selectReviewer = (email: string) => { - cy.get("div[id^='react-select']").contains(E2E_EMAIL_COLLAB.email).click() + cy.get("div[id^='react-select']").contains(email).click() } const removeReviewers = () => cy.get('div[aria-label^="Remove"]').click() @@ -41,8 +65,27 @@ const REVIEW_MODAL_SUBTITLE = describe("Review Requests", () => { before(() => { + cy.setupDefaultInterceptors() cy.setEmailSessionDefaults("Email admin") closeReviewRequests() + // Defensive edit - ensures that we have at least one edit to create a review request + editUnlinkedPage( + "faq.md", + "some original content", + E2E_EMAIL_TEST_SITE.repo + ) + // Reset to no changes in the repo + addAdmin(E2E_EMAIL_COLLAB_NON_GOV.email) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_COLLAB_NON_GOV.email], + MOCK_REVIEW_DESCRIPTION + ).then((id) => { + cy.actAsEmailUser(E2E_EMAIL_COLLAB_NON_GOV.email, "Email admin") + approveReviewRequest(id) + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + mergeReviewRequest(id) + }) }) beforeEach(() => { cy.setupDefaultInterceptors() @@ -53,9 +96,26 @@ describe("Review Requests", () => { beforeEach(() => { // NOTE: We want to start this test suite on a clean slate // and prevent other tests from interfering + cy.setupDefaultInterceptors() + cy.setEmailSessionDefaults("Email admin") + visitE2eEmailTestRepo() removeOtherCollaborators() }) + it("should not be able to create a review request when there are no changes", () => { + // Arrange + addAdminCollaborator(E2E_EMAIL_ADMIN_2.email) + + // Act + getReviewRequestButton().click() + cy.contains(REVIEW_MODAL_SUBTITLE).should("be.visible") + cy.get(TITLE_INPUT_SELECTOR).type(MOCK_REVIEW_TITLE) + getAddReviewerDropdown().click() + selectReviewer(E2E_EMAIL_ADMIN_2.email) + + // Assert + getSubmitReviewButton().should("be.disabled") + }) it("should not be able to create a review request when the site has 1 collaborator", () => { // Arrange getReviewRequestButton().click() @@ -69,12 +129,7 @@ describe("Review Requests", () => { }) it("should not be able to create a review request without a reviewer", () => { // Arrange - cy.createEmailUser( - E2E_EMAIL_COLLAB.email, - USER_TYPES.Email.Collaborator, - USER_TYPES.Email.Admin - ) - addCollaborator(E2E_EMAIL_COLLAB.email) + addCollaborator(E2E_EMAIL_COLLAB_NON_GOV.email) // Act // NOTE: There should be at least 1 other admin to be able to create a review request @@ -85,18 +140,12 @@ describe("Review Requests", () => { getAddReviewerDropdown().click() cy.contains("No options").should("be.visible") }) - it.skip("should not be able to create a review request when there are no changes", () => { - // Arrange - // Act - // Assert - throw new Error("Not implemented") - }) it("should be able to create a review request successfully", () => { // Arrange - addAdmin(E2E_EMAIL_COLLAB.email) + addAdmin(E2E_EMAIL_ADMIN_2.email) editUnlinkedPage( "faq.md", - "some asdfasdfasdf content", + "some review request success content", E2E_EMAIL_TEST_SITE.repo ) @@ -108,12 +157,12 @@ describe("Review Requests", () => { getSubmitReviewButton().should("be.disabled") getAddReviewerDropdown().click() // select reviewer and click - selectReviewer(E2E_EMAIL_COLLAB.email) + selectReviewer(E2E_EMAIL_ADMIN_2.email) removeReviewers() getSubmitReviewButton().should("be.disabled") // add back reviewer getAddReviewerDropdown().click() - selectReviewer(E2E_EMAIL_COLLAB.email) + selectReviewer(E2E_EMAIL_ADMIN_2.email) cy.get(DESCRIPTION_TEXTAREA_SELECTOR).type(MOCK_REVIEW_DESCRIPTION) // Assert @@ -121,37 +170,215 @@ describe("Review Requests", () => { cy.contains("Review request submitted").should("be.visible") }) }) - describe.skip("has pending review requests", () => { + describe("has pending review requests", () => { before(() => { - addAdmin(E2E_EMAIL_COLLAB.email) - // NOTE: Create a review request if none exists - listReviewRequests().then((requests) => { - if (!requests || !requests.length) { - createReviewRequest( - MOCK_REVIEW_TITLE, - [E2E_EMAIL_COLLAB.email], - MOCK_REVIEW_DESCRIPTION - ) - } - }) + cy.setupDefaultInterceptors() + cy.setEmailSessionDefaults("Email admin") + removeOtherCollaborators() + addAdmin(E2E_EMAIL_ADMIN_2.email) + addCollaborator(E2E_EMAIL_COLLAB_NON_GOV.email) + editUnlinkedPage( + "faq.md", + "some pending review requests content", + E2E_EMAIL_TEST_SITE.repo + ) }) - it("should have the pending review alert on the workspace", () => { + beforeEach(() => { + // NOTE: We want to start this test suite on a clean slate + // and prevent other tests from interfering + cy.setupDefaultInterceptors() + cy.setEmailSessionDefaults("Email admin") + visitE2eEmailTestRepo() + closeReviewRequests() + }) + it('should show the review request on the workspace with the tag "Pending review"', () => { // Arrange + cy.actAsEmailUser(E2E_EMAIL_ADMIN_2.email, "Email admin") + createReviewRequest(MOCK_REVIEW_TITLE, [E2E_EMAIL_ADMIN.email]) + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + visitE2eEmailTestRepo() // Act + cy.contains("Review required").should("be.visible") + cy.visit(`/sites/${E2E_EMAIL_TEST_SITE.repo}/workspace`) + awaitReviewRequestSummary() + // Assert + cy.contains("Review request pending approval").should("be.visible") }) it("should prevent the requestor from approving their own review request", () => { // Arrange + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN_2.email], + MOCK_REVIEW_DESCRIPTION + ) + visitE2eEmailTestRepo() + // Act + cy.contains("Review required").should("not.exist") + getViewReviewRequestButton().click() + openReviewRequestStateDropdown("In review") + // Assert + cy.contains("Approved").should("not.exist") }) - it('should show the review request on the workspace with the tag "Pending review"', () => { + it("should be able to have the request created by a collaborator approved", () => { + // Arrange + cy.actAsEmailUser(E2E_EMAIL_COLLAB_NON_GOV.email, "Email collaborator") + cy.setEmailSessionDefaults("Email collaborator") + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN.email], + MOCK_REVIEW_DESCRIPTION + ) + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + cy.setEmailSessionDefaults("Email admin") + visitE2eEmailTestRepo() + // Act + getViewReviewRequestButton().click() + openReviewRequestStateDropdown("In review") + + // Assert + cy.contains("Approved").should("exist") + cy.contains("Approved").click() + cy.contains("This Review request has been approved!").should("exist") + }) + it("should not be able to approve the request as a collaborator", () => { // Arrange + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + cy.setEmailSessionDefaults("Email admin") + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN.email], + MOCK_REVIEW_DESCRIPTION + ) + + cy.actAsEmailUser(E2E_EMAIL_COLLAB_NON_GOV.email, "Email collaborator") + cy.setEmailSessionDefaults("Email collaborator") + + visitE2eEmailTestRepo() // Act + getViewReviewRequestButton().click() + // Assert + cy.contains("button", "In review").should("be.disabled") }) - it("should have changes reflected in the currently open review request", () => { + it.skip("should have changes reflected in the currently open review request", () => { + // Note: we're currently skipping this test because it's incredibly flaky to get a consistent state + // This issue seems to stem from the github side - some actions seem to be failing when taken too close to others + // e.g. Adding a file soon after deleting it throws a 409 error from github // Arrange + const RR_OVERVIEW_INTERCEPTOR = "getReviewRequest" + const regex = /\/review\/\d+\// + cy.intercept("GET", regex).as(RR_OVERVIEW_INTERCEPTOR) + const TEST_PAGE_ADD = "page add" + const TEST_PAGE_EDIT = "page edit" + const TEST_PAGE_DELETE = "page delete" + const TEST_PAGE_RENAME = "page rename" + const TEST_PAGE_MOVE = "page move" + const TARGET_COLLECTION = "example-folder" + listUnlinkedPages(E2E_EMAIL_TEST_SITE.repo).then((pages) => { + // eslint-disable-next-line no-restricted-syntax + for (const page of pages) { + // Needs to be done sequentially to prevent mutex locks + switch (page.name) { + case `${TEST_PAGE_ADD}.md`: + deleteUnlinkedPage( + `${TEST_PAGE_ADD}.md`, + E2E_EMAIL_TEST_SITE.repo + ) + break + case `${TEST_PAGE_EDIT}.md`: + deleteUnlinkedPage( + `${TEST_PAGE_EDIT}.md`, + E2E_EMAIL_TEST_SITE.repo + ) + break + case `${TEST_PAGE_DELETE}.md`: + deleteUnlinkedPage( + `${TEST_PAGE_DELETE}.md`, + E2E_EMAIL_TEST_SITE.repo + ) + break + case `${TEST_PAGE_RENAME}.md`: + deleteUnlinkedPage( + `${TEST_PAGE_RENAME}.md`, + E2E_EMAIL_TEST_SITE.repo + ) + break + case `${TEST_PAGE_MOVE}.md`: + deleteUnlinkedPage( + `${TEST_PAGE_MOVE}.md`, + E2E_EMAIL_TEST_SITE.repo + ) + break + default: + break + } + } + }) + listCollectionPages(E2E_EMAIL_TEST_SITE.repo, TARGET_COLLECTION).then( + (pages) => { + // eslint-disable-next-line no-restricted-syntax + for (const page of pages) { + // Needs to be done sequentially to prevent mutex locks + switch (page.name) { + case `${TEST_PAGE_MOVE}.md`: + deleteCollectionPage( + `${TEST_PAGE_MOVE}.md`, + TARGET_COLLECTION, + E2E_EMAIL_TEST_SITE.repo + ) + break + default: + break + } + } + } + ) + // Wait for github to update and clear local storage - attempting to create a recently deleted file will return a 409 error on github's end + cy.wait(60000) + + addUnlinkedPage( + `${TEST_PAGE_EDIT}.md`, + TEST_PAGE_EDIT, + "/permalink", + "blahblah", + E2E_EMAIL_TEST_SITE.repo + ) + addUnlinkedPage( + `${TEST_PAGE_DELETE}.md`, + TEST_PAGE_DELETE, + "/definitely a different permalink", + "this is a different page from the add for realsies", + E2E_EMAIL_TEST_SITE.repo + ) + addUnlinkedPage( + `${TEST_PAGE_RENAME}.md`, + TEST_PAGE_RENAME, + "/permalink", + "blahblah", + E2E_EMAIL_TEST_SITE.repo + ) + addUnlinkedPage( + `${TEST_PAGE_MOVE}.md`, + TEST_PAGE_MOVE, + "/permalink", + "blahblah", + E2E_EMAIL_TEST_SITE.repo + ) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN_2.email], + MOCK_REVIEW_DESCRIPTION + ).then((id) => { + cy.actAsEmailUser(E2E_EMAIL_ADMIN_2.email, "Email admin") + approveReviewRequest(id) + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + mergeReviewRequest(id) + }) + // Wait for github to update + cy.wait(60000) // TODO: If this is hard, maybe we can just do delete + add content + have new page // NOTE: We define changes as being // 1. Adding a new page @@ -159,76 +386,313 @@ describe("Review Requests", () => { // 3. Deleting an existing page // 4. Rename an existing page // 5. Moving an existing page + + addUnlinkedPage( + `${TEST_PAGE_ADD}.md`, + TEST_PAGE_ADD, + "/permalink", + "blahblahblah", + E2E_EMAIL_TEST_SITE.repo + ) + editUnlinkedPage( + `${TEST_PAGE_EDIT}.md`, + "some asdfasdfasdf content", + E2E_EMAIL_TEST_SITE.repo + ) + moveUnlinkedPage( + `${TEST_PAGE_MOVE}.md`, + "example-folder", + E2E_EMAIL_TEST_SITE.repo + ) + deleteUnlinkedPage(`${TEST_PAGE_DELETE}.md`, E2E_EMAIL_TEST_SITE.repo) + renameUnlinkedPage( + `${TEST_PAGE_RENAME}.md`, + `new ${TEST_PAGE_RENAME}.md`, + E2E_EMAIL_TEST_SITE.repo + ) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN_2.email], + MOCK_REVIEW_DESCRIPTION + ) + visitE2eEmailTestRepo() + // Act + getViewReviewRequestButton().click() + // Assert - }) - it("should be able to have the request approved as a collaborator", () => { - // Arrange - cy.setEmailSessionDefaults("Email collaborator") - // Act - // Assert + cy.contains(`${TEST_PAGE_ADD}.md`).should("exist") + getReviewRequestOverviewPageEditButton(`${TEST_PAGE_ADD}.md`).should( + "not.be.disabled" + ) + cy.contains(`${TEST_PAGE_EDIT}.md`).should("exist") + getReviewRequestOverviewPageEditButton(`${TEST_PAGE_EDIT}.md`).should( + "not.be.disabled" + ) + cy.contains(`${TEST_PAGE_MOVE}.md`).should("exist") + getReviewRequestOverviewPageEditButton(`${TEST_PAGE_MOVE}.md`).should( + "not.be.disabled" + ) + cy.contains(`${TEST_PAGE_MOVE}.md`) + .parent() + .contains(TARGET_COLLECTION) + .should("exist") + cy.contains(`${TEST_PAGE_DELETE}.md`).should("exist") + getReviewRequestOverviewPageEditButton(`${TEST_PAGE_DELETE}.md`).should( + "be.disabled" + ) + cy.contains(`new ${TEST_PAGE_RENAME}.md`).should("exist") + getReviewRequestOverviewPageEditButton( + `new ${TEST_PAGE_RENAME}.md` + ).should("not.be.disabled") }) }) - describe.skip("has approved review requests", () => { + describe("has approved review requests", () => { before(() => { - addAdmin(E2E_EMAIL_COLLAB.email) - // NOTE: Create a review request if none exists - listReviewRequests().then((requests) => { - if (!requests || !requests.length) { - createReviewRequest( - MOCK_REVIEW_TITLE, - [E2E_EMAIL_COLLAB.email], - MOCK_REVIEW_DESCRIPTION - ).then((id) => approveReviewRequest(id)) - } + addAdmin(E2E_EMAIL_ADMIN_2.email) + closeReviewRequests() + cy.setEmailSessionDefaults("Email admin") + cy.actAsEmailUser(E2E_EMAIL_ADMIN_2.email, "Email admin") + editUnlinkedPage( + "faq.md", + "some approved review requests content", + E2E_EMAIL_TEST_SITE.repo + ) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN.email], + MOCK_REVIEW_DESCRIPTION + ).then((id) => { + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + approveReviewRequest(id) }) }) - it("should have the review request approved alert stating that editing is disabled when on the workspace", () => { - // Arrange - // Act - // Assert + beforeEach(() => { + // NOTE: We want to start this test suite on a clean slate + // and prevent other tests from interfering + cy.setupDefaultInterceptors() + cy.setEmailSessionDefaults("Email admin") + visitE2eEmailTestRepo() }) - it("should show the review request as being approved on the dashboard", () => { + it("should have the review request approved alert stating that editing is disabled when on the workspace", () => { // Arrange // Act + cy.visit(`/sites/${E2E_EMAIL_TEST_SITE.repo}/workspace`) + awaitReviewRequestSummary() + // Assert + // Toast + cy.contains("There is currently an approved review request!").should( + "exist" + ) + // Redirect to dashboard + cy.url().should("include", `/sites/${E2E_EMAIL_TEST_SITE.repo}/dashboard`) + // Tag + cy.contains("Approved") }) it("should prevent edits while the request is not merged", () => { // Arrange + // Act + cy.visit(`/sites/${E2E_EMAIL_TEST_SITE.repo}/workspace`) + cy.contains("Homepage").should(($element) => { + const styles = getComputedStyle($element[0]) + const pointerEventsValue = styles.getPropertyValue("pointer-events") + + // Assert the pointer-events value is set to 'none' + expect(pointerEventsValue).to.eq("none") + }) + cy.visit(`/sites/${E2E_EMAIL_TEST_SITE.repo}/editPage/example-page.md`) + // Assert + awaitDashboardLoad() + // Redirect to dashboard + cy.url().should("include", `/sites/${E2E_EMAIL_TEST_SITE.repo}/dashboard`) + // Tag + cy.contains("Approved") }) it("should allow the requestor to merge the review request", () => { // Arrange + const MERGE_INTERCEPTOR = "getReviewRequest" + cy.intercept("POST", "**/merge").as(MERGE_INTERCEPTOR) + visitE2eEmailTestRepo() + // Act + getViewReviewRequestButton().click() + getPublishButton().click() + cy.wait(`@${MERGE_INTERCEPTOR}`) + // Assert + cy.contains("Your changes have been published!").should("exist") }) it("should allow the reviewer to merge the review request", () => { // Arrange + const MERGE_INTERCEPTOR = "getReviewRequest" + cy.intercept("POST", "**/merge").as(MERGE_INTERCEPTOR) + closeReviewRequests() + cy.setEmailSessionDefaults("Email admin") + editUnlinkedPage( + "faq.md", + "some reviewer merge review request content", + E2E_EMAIL_TEST_SITE.repo + ) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN_2.email], + MOCK_REVIEW_DESCRIPTION + ).then((id) => { + cy.actAsEmailUser(E2E_EMAIL_ADMIN_2.email, "Email admin") + approveReviewRequest(id) + }) + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + visitE2eEmailTestRepo() + // Act + getViewReviewRequestButton().click() + getPublishButton().click() + cy.wait(`@${MERGE_INTERCEPTOR}`) + // Assert + cy.contains("Your changes have been published!").should("exist") }) }) - describe.skip("changing review request states", () => { + describe("changing review request states", () => { + before(() => { + addAdmin(E2E_EMAIL_ADMIN_2.email) + closeReviewRequests() + }) + beforeEach(() => { + // NOTE: We want to start this test suite on a clean slate + // and prevent other tests from interfering + cy.setupDefaultInterceptors() + cy.setEmailSessionDefaults("Email admin") + closeReviewRequests() + visitE2eEmailTestRepo() + }) it("should allow the reviewer to unapprove the request", () => { // Arrange + cy.setEmailSessionDefaults("Email admin") + cy.actAsEmailUser(E2E_EMAIL_ADMIN_2.email, "Email admin") + editUnlinkedPage( + "faq.md", + "some reviewer unapprove review request content", + E2E_EMAIL_TEST_SITE.repo + ) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN.email], + MOCK_REVIEW_DESCRIPTION + ).then((id) => { + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + approveReviewRequest(id) + }) + visitE2eEmailTestRepo() + // Act + getViewReviewRequestButton().click() + openReviewRequestStateDropdown("Approved") + const reviewButton = cy.contains("p", "In review") + reviewButton.should("be.visible").click() + // Wait for dropdown menu to disappear + reviewButton.should("not.be.visible") + // Assert + cy.contains("button", "Approved").should("not.be.visible") + cy.contains("button", "In review").should("be.visible") }) it("should allow the requestor to close the review request", () => { // Arrange + const RR_CANCEL_INTERCEPTOR = "getReviewRequest" + // Matches any url like /review/{digits}, since we don't know the rr number until the test runs + const regex = /\/review\/\d+\// + cy.intercept("DELETE", regex).as(RR_CANCEL_INTERCEPTOR) + cy.setEmailSessionDefaults("Email admin") + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + editUnlinkedPage( + "faq.md", + "some requestor close review request content", + E2E_EMAIL_TEST_SITE.repo + ) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN_2.email], + MOCK_REVIEW_DESCRIPTION + ) + visitE2eEmailTestRepo() + // Act + getViewReviewRequestButton().click() + openReviewRequestStateDropdown("In review") + cy.contains("Cancel request").should("exist").click() + cy.contains("Yes, cancel").click() + cy.wait(`@${RR_CANCEL_INTERCEPTOR}`) + awaitReviewRequestSummary() + // Assert + cy.url().should("include", "/dashboard") + visitE2eEmailTestRepo() + getReviewRequestButton().should("exist") }) it("should disallow users from viewing a closed review request", () => { // Arrange - // Act - // Assert + editUnlinkedPage( + "faq.md", + "some disallow view closed review request content", + E2E_EMAIL_TEST_SITE.repo + ) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN_2.email], + MOCK_REVIEW_DESCRIPTION + ).then((id) => { + closeReviewRequest(id) + // Act + awaitDashboardLoad() + cy.visit(`/sites/e2e-email-test-repo/review/${id}`) + awaitDashboardLoad() + // Redirect to dashboard + cy.url().should( + "include", + `/sites/${E2E_EMAIL_TEST_SITE.repo}/dashboard` + ) + + // Assert + cy.contains( + "Please ensure that you have selected a valid review request." + ) + }) }) it("should disallow users from viewing a merged review request", () => { // Arrange - // Act - // Assert + editUnlinkedPage( + "faq.md", + "some disallow view merged review request content", + E2E_EMAIL_TEST_SITE.repo + ) + createReviewRequest( + MOCK_REVIEW_TITLE, + [E2E_EMAIL_ADMIN_2.email], + MOCK_REVIEW_DESCRIPTION + ).then((id) => { + cy.actAsEmailUser(E2E_EMAIL_ADMIN_2.email, "Email admin") + approveReviewRequest(id) + cy.actAsEmailUser(E2E_EMAIL_ADMIN.email, "Email admin") + mergeReviewRequest(id) + + // Act + cy.visit(`/sites/e2e-email-test-repo/review/${id}`) + // Redirect to dashboard + awaitDashboardLoad() + cy.url().should( + "include", + `/sites/${E2E_EMAIL_TEST_SITE.repo}/dashboard` + ) + + // Assert + cy.contains( + "Please ensure that you have selected a valid review request." + ) + }) }) }) }) diff --git a/cypress/fixtures/constants.ts b/cypress/fixtures/constants.ts index 7c0f3340a..a1bf7d79e 100644 --- a/cypress/fixtures/constants.ts +++ b/cypress/fixtures/constants.ts @@ -10,10 +10,18 @@ export const E2E_EMAIL_ADMIN = { email: "admin@e2e.gov.sg", } as const +export const E2E_EMAIL_ADMIN_2 = { + email: "twodmin@e2e.gov.sg", +} as const + export const E2E_EMAIL_COLLAB = { email: "collab@e2e.gov.sg", } as const +export const E2E_EMAIL_COLLAB_NON_GOV = { + email: "collab@e2etest.com", +} as const + export const E2E_EMAIL_TEST_SITE = { name: "e2e email test site", repo: "e2e-email-test-repo", diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 634949b19..fa4a61a3e 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -77,5 +77,12 @@ declare namespace Cypress { initialUserType: "Email admin" | "Email collaborator", site?: string ): Chainable + actAsEmailUser( + email: string, + // NOTE: have to (re)declare the type here + // otherwise the import leads to a type error + userType: "Email admin" | "Email collaborator", + site?: string + ): Chainable } } diff --git a/cypress/support/session.ts b/cypress/support/session.ts index d75147899..778773e23 100644 --- a/cypress/support/session.ts +++ b/cypress/support/session.ts @@ -42,7 +42,6 @@ Cypress.Commands.add( initialUserType: EmailUserTypes, site = "" ) => { - cy.clearCookies() setCookieWithDomain(E2E_COOKIE.Site.key, site) setCookieWithDomain(COOKIE_NAME, COOKIE_VALUE) setCookieWithDomain(E2E_COOKIE.Auth.key, E2E_COOKIE.Auth.value) @@ -54,3 +53,15 @@ Cypress.Commands.add( cy.visit(`${CMS_BASEURL}/sites/${E2E_EMAIL_TEST_SITE.repo}/dashboard`) } ) + +Cypress.Commands.add( + "actAsEmailUser", + (email: string, userType: EmailUserTypes, site = "") => { + setCookieWithDomain(E2E_COOKIE.Site.key, site) + setCookieWithDomain(COOKIE_NAME, COOKIE_VALUE) + setCookieWithDomain(E2E_COOKIE.Auth.key, E2E_COOKIE.Auth.value) + setCookieWithDomain(E2E_COOKIE.EmailUserType.key, userType) + setCookieWithDomain(E2E_COOKIE.Email.key, email) + cy.request("GET", `${BACKEND_URL}/auth/whoami`) + } +) diff --git a/cypress/utils/collaborators.ts b/cypress/utils/collaborators.ts index c56c5b8fb..894eb91e9 100644 --- a/cypress/utils/collaborators.ts +++ b/cypress/utils/collaborators.ts @@ -20,28 +20,46 @@ export const getCollaboratorsModal = (): Cypress.Chainable< return cy.get("form").should("be.visible") } -export const removeOtherCollaborators = (): void => { +export const removeFirstCollaborator = (): void => { + // Note: only removes a single other collaborator, due to concurrency issues getCollaboratorsModal() .get(DELETE_BUTTON_SELECTOR) .then((buttons) => { if (buttons.length > 1) { - buttons.slice(1).each((_, button) => { - button.click() - cy.contains("button", "Remove collaborator").click() - cy.contains("Collaborator removed successfully").should("be.visible") - }) + buttons[1].click() + cy.contains("button", "Remove collaborator").click() + cy.contains("Collaborator removed successfully").should("be.visible") } }) closeModal() } +export const removeOtherCollaborators = (): void => { + // We have up to 3 other collaborators - this ensures all are removed + removeFirstCollaborator() + removeFirstCollaborator() + removeFirstCollaborator() +} + export const inputCollaborators = (user: string): void => { getCollaboratorsModal().get(ADD_COLLABORATOR_INPUT_SELECTOR).type(user).blur() // NOTE: need to ignore the 422 w/ specific error message because we haven't ack yet cy.contains("Add collaborator").click().wait(Interceptors.POST) } +export const addAdminCollaborator = (collaborator: string): void => { + cy.createEmailUser( + collaborator, + USER_TYPES.Email.Admin, + USER_TYPES.Email.Admin + ) + + inputCollaborators(collaborator) + + closeModal() +} + export const addCollaborator = (collaborator: string): void => { cy.createEmailUser( collaborator, diff --git a/cypress/utils/index.ts b/cypress/utils/index.ts index 40846ab39..778cd59d7 100644 --- a/cypress/utils/index.ts +++ b/cypress/utils/index.ts @@ -6,3 +6,4 @@ export * from "./dashboard" export * from "./session" export * from "./ignores" export * from "./pages" +export * from "./reviewRequests" diff --git a/cypress/utils/reviewRequests.ts b/cypress/utils/reviewRequests.ts new file mode 100644 index 000000000..067f4062e --- /dev/null +++ b/cypress/utils/reviewRequests.ts @@ -0,0 +1,11 @@ +export const awaitDashboardLoad = () => { + const DASHBOARD_INTERCEPTOR = "getCollaboratorDetails" + cy.intercept("GET", "**/collaborators").as(DASHBOARD_INTERCEPTOR) + cy.wait(`@${DASHBOARD_INTERCEPTOR}`) +} + +export const awaitReviewRequestSummary = () => { + const RR_INTERCEPTOR = "getReviewRequest" + cy.intercept("GET", "**/summary").as(RR_INTERCEPTOR) + cy.wait(`@${RR_INTERCEPTOR}`) +}