From 7df850bcb11de178abf9a7f2c657bf6871a17910 Mon Sep 17 00:00:00 2001 From: Karolina Rakoczy Date: Mon, 7 Jun 2021 13:35:53 +0200 Subject: [PATCH] Saleor 3391 tests for staff members (#1122) * invite staff * staff members * all tests for staff members * remove unused import * update snap * remove /dashboard/ * uptade test.yml --- .github/workflows/test.yml | 4 +- cypress/apiRequests/permissions.js | 12 ++ cypress/apiRequests/staffMember.js | 88 ++++++++++++++ cypress/elements/account/setPassword.js | 4 + .../staffMembers/inviteStaffMemberForm.js | 5 + .../staffMembers/staffMemberDetails.js | 5 + .../elements/staffMembers/staffMembersList.js | 3 + .../integration/allEnv/homePage/homePage.js | 18 +-- .../integration/stagedOnly/staffMembers.js | 111 ++++++++++++++++++ cypress/plugins/index.js | 1 + cypress/steps/homePageSteps.js | 10 ++ cypress/steps/user.js | 46 ++++++++ cypress/support/index.js | 1 + cypress/url/urlList.js | 5 + cypress/utils/users.js | 44 +++++++ package-lock.json | 5 + package.json | 1 + .../NewPasswordPage/NewPasswordPage.tsx | 1 + .../StaffAddMemberDialog.tsx | 1 + .../StaffListPage/StaffListPage.tsx | 1 + .../__snapshots__/Stories.test.ts.snap | 7 ++ 21 files changed, 359 insertions(+), 14 deletions(-) create mode 100644 cypress/apiRequests/permissions.js create mode 100644 cypress/apiRequests/staffMember.js create mode 100644 cypress/elements/account/setPassword.js create mode 100644 cypress/elements/staffMembers/inviteStaffMemberForm.js create mode 100644 cypress/elements/staffMembers/staffMemberDetails.js create mode 100644 cypress/elements/staffMembers/staffMembersList.js create mode 100644 cypress/integration/stagedOnly/staffMembers.js create mode 100644 cypress/steps/user.js create mode 100644 cypress/utils/users.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5da29b5d..22f5c7922 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,6 +103,7 @@ jobs: wait-on: http://localhost:9000/ wait-on-timeout: 120 spec: cypress/integration/allEnv/**/*.js + - name: Cypress run if: ${{ steps.api_uri.outputs.custom_api_uri}} == 'https://qa.staging.saleor.cloud/graphql/' uses: cypress-io/github-action@v2 @@ -114,6 +115,7 @@ jobs: CYPRESS_SECOND_USER_NAME: ${{ secrets.CYPRESS_SECOND_USER_NAME }} CYPRESS_USER_PASSWORD: ${{ secrets.CYPRESS_USER_PASSWORD }} CYPRESS_PERMISSIONS_USERS_PASSWORD: ${{ secrets.CYPRESS_PERMISSIONS_USERS_PASSWORD }} + CYPRESS_MAILHOG: ${{ secrets.CYPRESS_MAILHOG }} with: build: npm run build start: npx local-web-server --spa index.html @@ -124,7 +126,7 @@ jobs: with: name: cypress-videos path: cypress/videos - + translation-messages: runs-on: ubuntu-latest steps: diff --git a/cypress/apiRequests/permissions.js b/cypress/apiRequests/permissions.js new file mode 100644 index 000000000..e89dd670f --- /dev/null +++ b/cypress/apiRequests/permissions.js @@ -0,0 +1,12 @@ +export function getPermissionsArray(first) { + const query = `query{ + permissionGroups(first:${first}){ + edges{ + node{ + id + } + } + } + }`; + return cy.sendRequestWithQuery(query).its("body.data.permissionGroups.edges"); +} diff --git a/cypress/apiRequests/staffMember.js b/cypress/apiRequests/staffMember.js new file mode 100644 index 000000000..36554441f --- /dev/null +++ b/cypress/apiRequests/staffMember.js @@ -0,0 +1,88 @@ +import { urlList } from "../url/urlList"; + +export function getStaffMembersStartsWith(startsWith) { + const query = `query{ + staffUsers(first:100 filter:{ + search:"${startsWith}" + }){ + edges{ + node{ + id + email + } + } + } + }`; + return cy.sendRequestWithQuery(query); +} + +export function inviteStaffMember({ + email, + isActive = true, + firstName = "", + lastName = "", + permissionId +}) { + const mutation = `mutation createStaff{ + staffCreate(input:{ + firstName: "${firstName}" + lastName: "${lastName}" + email: "${email}", + isActive: ${isActive}, + addGroups:"${permissionId}" + redirectUrl: "${Cypress.config().baseUrl}${urlList.newPassword}" + }){ + user{ + id + } + errors{ + field + message + } + } + } + `; + return cy.sendRequestWithQuery(mutation).its("body.data.staffCreate"); +} + +export function updateStaffMember({ userId, isActive }) { + const mutation = `mutation{ + staffUpdate(id:"${userId}", input:{ + isActive:${isActive} + }){ + user{ + id + } + errors{ + field + message + } + } + }`; + return cy.sendRequestWithQuery(mutation).its("body.data.staffUpdate"); +} + +export function deleteStaffMember(staffId) { + const mutation = `mutation{ + staffDelete(id:"${staffId}"){ + errors{ + field + message + } + } + }`; + return cy.sendRequestWithQuery(mutation).its("body.data.staffDelete"); +} + +export function deleteStaffMembersStartsWith(startsWith) { + getStaffMembersStartsWith(startsWith).then(resp => { + if (resp.body.data.staffUsers) { + const staffMembers = resp.body.data.staffUsers.edges; + staffMembers.forEach(element => { + if (element.node.email.includes(startsWith)) { + deleteStaffMember(element.node.id); + } + }); + } + }); +} diff --git a/cypress/elements/account/setPassword.js b/cypress/elements/account/setPassword.js new file mode 100644 index 000000000..cc8bc9905 --- /dev/null +++ b/cypress/elements/account/setPassword.js @@ -0,0 +1,4 @@ +export const SET_PASSWORD = { + passwordInput: '[data-test="password"]', + confirmPasswordInput: '[data-test="confirm-password"]' +}; diff --git a/cypress/elements/staffMembers/inviteStaffMemberForm.js b/cypress/elements/staffMembers/inviteStaffMemberForm.js new file mode 100644 index 000000000..725ff0086 --- /dev/null +++ b/cypress/elements/staffMembers/inviteStaffMemberForm.js @@ -0,0 +1,5 @@ +export const INVITE_STAFF_MEMBER_FORM = { + firstNameInput: '[name="firstName"]', + lastNameInput: '[name="lastName"]', + emailInput: '[name="email"]' +}; diff --git a/cypress/elements/staffMembers/staffMemberDetails.js b/cypress/elements/staffMembers/staffMemberDetails.js new file mode 100644 index 000000000..e6206526f --- /dev/null +++ b/cypress/elements/staffMembers/staffMemberDetails.js @@ -0,0 +1,5 @@ +export const STAFF_MEMBER_DETAILS = { + permissionsSelect: '[data-test="permissionGroups"]', + isActiveCheckBox: '[name="isActive"]', + removePermissionButton: '[data-test-id="remove"]' +}; diff --git a/cypress/elements/staffMembers/staffMembersList.js b/cypress/elements/staffMembers/staffMembersList.js new file mode 100644 index 000000000..d2cf59b7b --- /dev/null +++ b/cypress/elements/staffMembers/staffMembersList.js @@ -0,0 +1,3 @@ +export const STAFF_MEMBERS_LIST = { + inviteStaffMemberButton: '[data-test-id="inviteStaffMember"]' +}; diff --git a/cypress/integration/allEnv/homePage/homePage.js b/cypress/integration/allEnv/homePage/homePage.js index 96c525e92..d2100d962 100644 --- a/cypress/integration/allEnv/homePage/homePage.js +++ b/cypress/integration/allEnv/homePage/homePage.js @@ -1,27 +1,19 @@ import { TEST_ADMIN_USER, USER_WITHOUT_NAME } from "../../../Data/users"; -import { HOMEPAGE_SELECTORS } from "../../../elements/homePage/homePage-selectors"; +import { expectWelcomeMessageIncludes } from "../../../steps/homePageSteps"; import { urlList } from "../../../url/urlList"; describe("Displaying welcome message on home page", () => { it("should display user name on home page", () => { cy.loginUserViaRequest(); cy.visit(urlList.homePage); - cy.get(HOMEPAGE_SELECTORS.welcomeMessage) - .invoke("text") - .then(text => { - expect(text).to.contains( - `${TEST_ADMIN_USER.name} ${TEST_ADMIN_USER.lastName}` - ); - }); + expectWelcomeMessageIncludes( + `${TEST_ADMIN_USER.name} ${TEST_ADMIN_USER.lastName}` + ); }); it("should display user email on home page", () => { cy.loginUserViaRequest("auth", USER_WITHOUT_NAME); cy.visit(urlList.homePage); - cy.get(HOMEPAGE_SELECTORS.welcomeMessage) - .invoke("text") - .then(text => { - expect(text).to.contains(`${USER_WITHOUT_NAME.email}`); - }); + expectWelcomeMessageIncludes(`${USER_WITHOUT_NAME.email}`); }); }); diff --git a/cypress/integration/stagedOnly/staffMembers.js b/cypress/integration/stagedOnly/staffMembers.js new file mode 100644 index 000000000..501a0067c --- /dev/null +++ b/cypress/integration/stagedOnly/staffMembers.js @@ -0,0 +1,111 @@ +import faker from "faker"; + +import { + deleteStaffMembersStartsWith, + updateStaffMember +} from "../../apiRequests/staffMember"; +import { LEFT_MENU_SELECTORS } from "../../elements/account/left-menu/left-menu-selectors"; +import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors"; +import { STAFF_MEMBER_DETAILS } from "../../elements/staffMembers/staffMemberDetails"; +import { STAFF_MEMBERS_LIST } from "../../elements/staffMembers/staffMembersList"; +import { expectWelcomeMessageIncludes } from "../../steps/homePageSteps"; +import { getDisplayedSelectors } from "../../steps/permissions"; +import { + fillUpSetPassword, + fillUpUserDetails, + updateUserActiveFlag +} from "../../steps/user"; +import { urlList, userDetailsUrl } from "../../url/urlList"; +import { + getMailActivationLinkForUser, + inviteStaffMemberWithFirstPermission +} from "../../utils/users"; + +describe("Staff members", () => { + const startsWith = "Cypress"; + const password = Cypress.env("USER_PASSWORD"); + const lastName = faker.name.lastName(); + const email = `${startsWith}${lastName}@example.com`; + let user; + + before(() => { + cy.clearSessionData().loginUserViaRequest(); + deleteStaffMembersStartsWith(startsWith); + + inviteStaffMemberWithFirstPermission({ email }) + .then(({ user: userResp }) => { + user = userResp; + getMailActivationLinkForUser(email); + }) + .then(urlLink => { + cy.clearSessionData().visit(urlLink); + fillUpSetPassword(password); + cy.clearSessionData(); + }); + }); + + beforeEach(() => { + cy.clearSessionData().loginUserViaRequest(); + }); + + it("should invite user", () => { + const firstName = faker.name.firstName(); + const emailInvite = `${startsWith}${firstName}@example.com`; + + cy.visit(urlList.staffMembers) + .get(STAFF_MEMBERS_LIST.inviteStaffMemberButton) + .click(); + fillUpUserDetails(firstName, lastName, emailInvite); + getMailActivationLinkForUser(emailInvite).then(urlLink => { + cy.clearSessionData().visit(urlLink); + fillUpSetPassword(password); + expectWelcomeMessageIncludes(`${firstName} ${lastName}`); + }); + }); + + it("should deactivate user", () => { + updateStaffMember({ userId: user.id, isActive: true }); + updateUserActiveFlag(user.id); + cy.clearSessionData() + .loginUserViaRequest("auth", { email, password }) + .its("body.data.tokenCreate") + .then(tokenCreate => { + chai + .softExpect( + tokenCreate.errors[0].code, + "logging in should return error" + ) + .to.be.eq("INVALID_CREDENTIALS"); + expect(tokenCreate.token).to.be.not.ok; + }); + }); + + it("should activate user", () => { + updateStaffMember({ userId: user.id, isActive: false }); + updateUserActiveFlag(user.id); + cy.clearSessionData() + .loginUserViaRequest("auth", { email, password }) + .visit(urlList.homePage); + expectWelcomeMessageIncludes(email); + }); + + it("should remove user permissions", () => { + cy.visit(userDetailsUrl(user.id)) + .get(STAFF_MEMBER_DETAILS.removePermissionButton) + .click() + .addAliasToGraphRequest("StaffMemberUpdate") + .get(BUTTON_SELECTORS.confirm) + .click() + .wait("@StaffMemberUpdate") + .clearSessionData() + .loginUserViaRequest("auth", { email, password }) + .visit(urlList.homePage); + expectWelcomeMessageIncludes(email); + getDisplayedSelectors().then(displayedSelectors => { + expect(Object.values(displayedSelectors)).to.have.length(1); + expect(Object.values(displayedSelectors)[0]).to.eq( + LEFT_MENU_SELECTORS.home + ); + }); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 3adf44d66..7a5bc3758 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -19,6 +19,7 @@ module.exports = (on, config) => { // make env variables visible for cypress config.env.API_URI = process.env.API_URI; config.env.APP_MOUNT_URI = process.env.APP_MOUNT_URI; + config.env.mailHogUrl = process.env.CYPRESS_MAILHOG; on("before:browser:launch", (browser = {}, launchOptions) => { launchOptions.args.push("--proxy-bypass-list=<-loopback>"); diff --git a/cypress/steps/homePageSteps.js b/cypress/steps/homePageSteps.js index d9f2ee2eb..98349abb5 100644 --- a/cypress/steps/homePageSteps.js +++ b/cypress/steps/homePageSteps.js @@ -1,4 +1,6 @@ import { HEADER_SELECTORS } from "../elements/header/header-selectors"; +import { HOMEPAGE_SELECTORS } from "../elements/homePage/homePage-selectors"; + export function changeChannel(channelName) { cy.get(HEADER_SELECTORS.channelSelect).click(); cy.addAliasToGraphRequest("Home"); @@ -7,3 +9,11 @@ export function changeChannel(channelName) { .click(); cy.wait("@Home"); } + +export function expectWelcomeMessageIncludes(name) { + cy.get(HOMEPAGE_SELECTORS.welcomeMessage) + .invoke("text") + .then(text => { + expect(text, `welcome message should contains ${name}`).to.contains(name); + }); +} diff --git a/cypress/steps/user.js b/cypress/steps/user.js new file mode 100644 index 000000000..cf3a02c46 --- /dev/null +++ b/cypress/steps/user.js @@ -0,0 +1,46 @@ +import { SET_PASSWORD } from "../elements/account/setPassword"; +import { BUTTON_SELECTORS } from "../elements/shared/button-selectors"; +import { SHARED_ELEMENTS } from "../elements/shared/sharedElements"; +import { INVITE_STAFF_MEMBER_FORM } from "../elements/staffMembers/inviteStaffMemberForm"; +import { STAFF_MEMBER_DETAILS } from "../elements/staffMembers/staffMemberDetails"; +import { userDetailsUrl } from "../url/urlList"; +import { fillAutocompleteSelect } from "./shared/autocompleteSelect"; + +export function fillUpSetPassword(password) { + cy.get(SET_PASSWORD.confirmPasswordInput) + .type(password) + .get(SET_PASSWORD.passwordInput) + .type(password) + .addAliasToGraphRequest("SetPassword") + .get(BUTTON_SELECTORS.confirm) + .click() + .wait("@SetPassword"); +} + +export function fillUpUserDetails(firstName, lastName, email) { + cy.get(INVITE_STAFF_MEMBER_FORM.firstNameInput) + .type(firstName) + .get(INVITE_STAFF_MEMBER_FORM.lastNameInput) + .type(lastName) + .get(INVITE_STAFF_MEMBER_FORM.emailInput) + .type(email) + .get(BUTTON_SELECTORS.submit) + .click(); + fillAutocompleteSelect(STAFF_MEMBER_DETAILS.permissionsSelect); + cy.addAliasToGraphRequest("StaffMemberUpdate"); + cy.get(BUTTON_SELECTORS.confirm) + .click() + .wait("@StaffMemberUpdate"); +} + +export function updateUserActiveFlag(userId) { + cy.visit(userDetailsUrl(userId)) + .get(SHARED_ELEMENTS.progressBar) + .should("not.be.visible") + .get(STAFF_MEMBER_DETAILS.isActiveCheckBox) + .click() + .addAliasToGraphRequest("StaffMemberUpdate"); + cy.get(BUTTON_SELECTORS.confirm) + .click() + .wait("@StaffMemberUpdate"); +} diff --git a/cypress/support/index.js b/cypress/support/index.js index 611f817f4..700e0885f 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -2,6 +2,7 @@ import "./user"; import "./softAssertions"; import "./deleteElement/index.js"; import "./elements/index"; +import "cypress-mailhog"; import { urlList } from "../url/urlList"; diff --git a/cypress/url/urlList.js b/cypress/url/urlList.js index e95136ed2..bccce6d9e 100644 --- a/cypress/url/urlList.js +++ b/cypress/url/urlList.js @@ -12,10 +12,15 @@ export const urlList = { collections: "collections/", vouchers: "discounts/vouchers/", staffMembers: "staff/", + newPassword: "new-password/", permissionsGroups: "permission-groups/" }; export const productDetailsUrl = productId => `${urlList.products}${productId}`; + +export const userDetailsUrl = userId => `${urlList.staffMembers}${userId}`; + export const staffMemberDetailsUrl = staffMemberId => `${urlList.staffMembers}${staffMemberId}`; + export const permissionGroupDetails = permissionGroupId => `${urlList.permissionsGroups}${permissionGroupId}`; diff --git a/cypress/utils/users.js b/cypress/utils/users.js new file mode 100644 index 000000000..f7befc793 --- /dev/null +++ b/cypress/utils/users.js @@ -0,0 +1,44 @@ +import { getPermissionsArray } from "../apiRequests/permissions"; +import { inviteStaffMember } from "../apiRequests/staffMember"; + +export function inviteStaffMemberWithFirstPermission({ + email, + isActive = true, + firstName = "", + lastName = "" +}) { + return getPermissionsArray(1).then(permissions => { + inviteStaffMember({ + firstName, + lastName, + email, + isActive, + permissionId: permissions[0].node.id + }); + }); +} + +/** + * Function mhGetMailsByRecipient first get all emails from mailhog with a timeout, and after that it finds email from recipient. + * It cloud happened that invite email from saleor has not been received yet, so in this case the action should be retried. + */ +export function getMailActivationLinkForUser(email, i = 0) { + if (i > 3) { + throw new Error(`There is no email invitation for user ${email}`); + } + return cy.mhGetMailsByRecipient(email).should(mails => { + if (!mails.length) { + getMailActivationLinkForUser(email, i + 1); + } else { + cy.wrap(mails) + .mhFirst() + .should("not.eq", undefined) + .mhGetBody() + .then(body => { + const urlRegex = /\[([^\]]*)\]/; + const bodyWithoutWhiteSpaces = body.replace(/(\r\n|\n|\r|\s)/gm, ""); + return urlRegex.exec(bodyWithoutWhiteSpaces)[1]; + }); + } + }); +} diff --git a/package-lock.json b/package-lock.json index a5e94a1c6..11c2c973e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11133,6 +11133,11 @@ } } }, + "cypress-mailhog": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cypress-mailhog/-/cypress-mailhog-1.3.0.tgz", + "integrity": "sha512-SS2dK+ea/3APLmWcoUrnCxNtFbrTfVFsESnnEUq7qnwQYKs52tnCFzhlpN9akRgcCZ7V4ihpxYYeJFkBG1rg2g==" + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", diff --git a/package.json b/package.json index dbb86f566..297fc3dd9 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "classnames": "^2.2.6", "crc-32": "^1.2.0", "currency-codes": "^2.1.0", + "cypress-mailhog": "^1.3.0", "downshift": "^1.31.16", "editorjs-inline-tool": "^0.4.0", "editorjs-undo": "^0.1.4", diff --git a/src/auth/components/NewPasswordPage/NewPasswordPage.tsx b/src/auth/components/NewPasswordPage/NewPasswordPage.tsx index 3e1fbe65c..00af9f178 100644 --- a/src/auth/components/NewPasswordPage/NewPasswordPage.tsx +++ b/src/auth/components/NewPasswordPage/NewPasswordPage.tsx @@ -112,6 +112,7 @@ const NewPasswordPage: React.FC = props => { /> = ({ } >