diff --git a/Frontend/cypress/integration/orgchart.spec.js b/Frontend/cypress/integration/orgchart.spec.js new file mode 100644 index 00000000..88f75914 --- /dev/null +++ b/Frontend/cypress/integration/orgchart.spec.js @@ -0,0 +1,144 @@ +/// + +describe("Org chart", () => { + const baseUrl = Cypress.env("baseUrl"); + const timeout = Cypress.env("timeoutInMs"); + + const hierarchy = { + 10001: { + 10001: [10002, 10101], + }, + 20004: { + 20006: [20002, 20003, { 20004: [20104] }, 20005, 20106], + }, + 20104: { + 20004: [20104], + }, + }; + + // check whether worker is valid, and optionally check for skills to search transition + const checkValidWorker = (workerId) => { + if (!hierarchy[workerId]) { + cy.contains( + "Sorry, there is no employee or contractor with matching id." + ).should("exist"); + } else { + // check for current + cy.get(`#${workerId}`).find(".current").should("exist"); + + const currentHierarchy = hierarchy[workerId]; + const firstLevelId = Object.keys(currentHierarchy)[0]; + const firstLevelDiv = () => cy.get(`#${firstLevelId}`); + firstLevelDiv().should("exist"); + + const secondLevel = currentHierarchy[firstLevelId]; + for (const peer of secondLevel) { + if (typeof peer === "number") { + // no subordinate + firstLevelDiv().next().find(`#${peer}`).should("exist"); + } else { + // have subordinates + const centerId = Object.keys(peer)[0]; + const centerDiv = () => + firstLevelDiv().next().find(`#${centerId}`); + + centerDiv().should("exist"); + + const thirdLevelIds = peer[centerId]; + for (const subordinateId of thirdLevelIds) { + centerDiv() + .next() + .find(`#${subordinateId}`) + .should("exist"); + } + } + } + } + }; + + it("valid worker from url", () => { + // no supervisor + cy.visit(`${baseUrl}/orgchart/10001`); + + cy.get('[data-cy="loading-orgchart"]').should("exist"); + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + checkValidWorker(10001); + + // normal worker + cy.visit(`${baseUrl}/orgchart/20004`); + + cy.get('[data-cy="loading-orgchart"]').should("exist"); + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + checkValidWorker(20004); + + // no subordinates + cy.visit(`${baseUrl}/orgchart/20104`); + + cy.get('[data-cy="loading-orgchart"]').should("exist"); + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + checkValidWorker(20104); + }); + + it("valid worker from other tabs", () => { + // from search results (using COO as the tag) + + cy.visit(baseUrl); + + cy.get('[data-cy="loading-filters"]').should("exist"); + cy.get('[data-cy="loading-filters"]', { timeout }).should("not.exist"); + + cy.get(".MuiChip-deleteIcon", { timeout }).click(); + + cy.get(".filter-form") + .contains("Filter by title") + .get(`[data-cy="expand-title-filters"]`, { timeout }) + .click(); + cy.get(".filter-list-button").contains("President and CEO").click(); + + cy.get('[data-cy="employee-card"]', { timeout }) + .find('[data-cy="orgchart-icon-10001"]') + .click(); + + cy.get('[data-cy="loading-orgchart"]').should("exist"); + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + checkValidWorker(10001); + + // from profile + + cy.visit(`${baseUrl}/profile/20104`); + + cy.get('[data-cy="loading-profile"]').should("exist"); + cy.get('[data-cy="loading-profile"]').should("not.exist"); + + cy.contains("Organization Chart").click(); + + cy.get('[data-cy="loading-orgchart"]').should("not.exist"); + + checkValidWorker(20104); + }); + + it("org chart navigation", () => { + cy.visit(`${baseUrl}/orgchart/20004`); + + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + cy.get("#20104").click(); + + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + checkValidWorker(20104); + }); + + it("invalid worker", () => { + cy.visit(`${baseUrl}/orgchart/100030`); + + cy.get('[data-cy="loading-orgchart"]').should("exist"); + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + checkValidWorker(100030); + }); +}); diff --git a/Frontend/cypress/integration/predictive_search_by_filter.spec.js b/Frontend/cypress/integration/predictive_search_by_filter.spec.js new file mode 100644 index 00000000..3ca07850 --- /dev/null +++ b/Frontend/cypress/integration/predictive_search_by_filter.spec.js @@ -0,0 +1,306 @@ +/// + +describe("Predictive search by filters", () => { + const baseUrl = Cypress.env("baseUrl"); + const timeout = Cypress.env("timeoutInMs"); + + it("non-skill filter", () => { + cy.visit(baseUrl); + + cy.get('[data-cy="loading-filters"]').should("exist"); + cy.get('[data-cy="loading-filters"]', { timeout }).should("not.exist"); + + // dependent on database knowledge + const testGroups = [ + { + type: "location", + initialLength: 4, + actions: [ + { typeIn: "V", results: ["Vancouver", "Victoria"] }, + { typeIn: "an", results: ["Vancouver"] }, + { typeIn: "xxx", results: [] }, + ], + }, + { + type: "title", + actions: [ + { + typeIn: "CO", + results: [ + "COO", + "Manager-HR/Accounting", + "President and CEO", + "Office Manager - Kelowna", + "Office Manager - Prince George", + ], + }, + { + typeIn: "G", + results: [ + "Manager-HR/Accounting", + "Office Manager - Prince George", + ], + }, + { + typeIn: "E", + results: ["Office Manager - Prince George"], + }, + { typeIn: "A", results: [] }, + ], + }, + { + type: "company", + initialLength: 3, + actions: [ + { + typeIn: "aa", + results: ["Acme Harvesting Ltd.", "Acme Planting Ltd."], + }, + { typeIn: "E", results: ["Acme Harvesting Ltd."] }, + { typeIn: "A", results: [] }, + ], + }, + { + type: "department", + initialLength: 9, + actions: [ + { + typeIn: "at", + results: [ + "Administration", + "Operations", + "Marketing", + "Marketing & Sales", + "Accounting", + ], + }, + { typeIn: "o", results: ["Administration", "Operations"] }, + { typeIn: "S", results: ["Operations"] }, + { typeIn: "d", results: [] }, + ], + }, + ]; + + for (const { type, initialLength, actions } of testGroups) { + cy.get(`[data-cy="expand-${type}-filters"]`).click(); + // do not check initial length of title filters (db could add random titles) + if (type !== "title") { + cy.get(".filter-list-button").should( + "have.length", + initialLength + ); + } + cy.get(`[data-cy="expand-${type}-filters"]`).click(); + + for (const action of actions) { + cy.get(`[data-cy="${type}-input"]`).type(action.typeIn); + + for (const result of action.results) { + cy.get(".filter-list-button") + .contains(result) + .should("exist"); + } + // again, do not check length of title filters (db could add random titles) + if (type !== "title") { + cy.get(".filter-list-button").should( + "have.length", + action.results.length + ); + } + } + + // clear would auto-hide + cy.get(`[data-cy="${type}-input"]`).find("input").clear(); + cy.get(".filter-list-button").should("not.exist"); + } + }); + + it("skill filter", () => { + cy.visit(baseUrl); + + cy.get('[data-cy="loading-filters"]').should("exist"); + cy.get('[data-cy="loading-filters"]', { timeout }).should("not.exist"); + + const skillArea = () => + cy.get(".filter-form").contains("Filter by skill"); + + // dependent on database knowledge + const getCategoryTitle = (category) => { + return skillArea().get(`[data-cy="category-title-${category}"]`); + }; + + const getCategoryCheckboxes = (category) => { + return skillArea().get( + `[data-cy="category-checkboxes-${category}"]` + ); + }; + + const checkCategoryHasSkills = (cyCategory, array) => { + if (array.length === 0) { + cyCategory.should("not.exist"); + } else { + cyCategory.should("have.length", array.length); + for (const text of array) { + cyCategory.should("contain", text); + } + } + }; + + // initial check + skillArea().get(`[data-cy="expand-skill-filters"]`).click(); + skillArea().get(".category").should("have.length", 4); + + getCategoryCheckboxes("Accounting").should("not.exist"); + getCategoryTitle("Accounting").click(); + checkCategoryHasSkills( + getCategoryCheckboxes("Accounting").find(".filter-list-button"), + ["Auditing", "Reconciling", "Transaction Processing"] + ); + + getCategoryCheckboxes("Agriculture").should("not.exist"); + getCategoryTitle("Agriculture").click(); + checkCategoryHasSkills( + getCategoryCheckboxes("Agriculture").find(".filter-list-button"), + [ + "Fertilizing", + "Harvesting", + "Irrigating", + "Planting", + "Soil Preparation", + ] + ); + + getCategoryCheckboxes("Management").should("not.exist"); + getCategoryTitle("Management").click(); + checkCategoryHasSkills( + getCategoryCheckboxes("Management").find(".filter-list-button"), + ["Budgeting", "Performance Reviews", "Planning"] + ); + + getCategoryCheckboxes("Marketing & Sales").should("not.exist"); + getCategoryTitle("Marketing & Sales").click(); + checkCategoryHasSkills( + getCategoryCheckboxes("Marketing & Sales").find( + ".filter-list-button" + ), + [ + "Customer Service", + "Marketing Strategies", + "Preparing Marketing Materials", + ] + ); + + // on click all hidden + skillArea().get(`[data-cy="expand-skill-filters"]`).click(); + skillArea().get(".category").should("not.exist"); + getCategoryCheckboxes("Accounting").should("not.exist"); + getCategoryCheckboxes("Agriculture").should("not.exist"); + getCategoryCheckboxes("Management").should("not.exist"); + getCategoryCheckboxes("Marketing & Sales").should("not.exist"); + + const actions = [ + { + typeIn: "ac", + results: { + Accounting: [ + "Auditing", + "Reconciling", + "Transaction Processing", + ], + Agriculture: [ + "Fertilizing", + "Harvesting", + "Irrigating", + "Planting", + "Soil Preparation", + ], + Management: ["Performance Reviews"], + "Marketing & Sales": [], + }, + }, + { + typeIn: "c", + results: { + Accounting: [ + "Auditing", + "Reconciling", + "Transaction Processing", + ], + Agriculture: [], + Management: [], + "Marketing & Sales": [], + }, + }, + { + typeIn: "c", + results: { + Accounting: [], + Agriculture: [], + Management: [], + "Marketing & Sales": [], + }, + }, + { + typeIn: "{backspace}{backspace}{backspace}", + results: { + Accounting: [ + "Auditing", + "Reconciling", + "Transaction Processing", + ], + Agriculture: [ + "Fertilizing", + "Harvesting", + "Irrigating", + "Planting", + "Soil Preparation", + ], + Management: [ + "Budgeting", + "Performance Reviews", + "Planning", + ], + "Marketing & Sales": [ + "Customer Service", + "Marketing Strategies", + "Preparing Marketing Materials", + ], + }, + }, + { + typeIn: "{backspace}{backspace}{backspace}", + results: { + Accounting: [], + Agriculture: [], + Management: [], + "Marketing & Sales": [], + }, + }, + ]; + + const categories = [ + "Accounting", + "Agriculture", + "Management", + "Marketing & Sales", + ]; + + for (const { typeIn, results } of actions) { + cy.get(`[data-cy="skill-input"]`).type(typeIn); + for (const category of categories) { + if (results[category].length === 0) { + getCategoryTitle(category).should("not.exist"); + getCategoryCheckboxes(category).should("not.exist"); + } else { + getCategoryTitle(category).should("exist"); + checkCategoryHasSkills( + getCategoryCheckboxes(category).find( + ".filter-list-button" + ), + results[category] + ); + } + } + } + }); +}); diff --git a/Frontend/cypress/integration/profile.spec.js b/Frontend/cypress/integration/profile.spec.js new file mode 100644 index 00000000..8e0ae8eb --- /dev/null +++ b/Frontend/cypress/integration/profile.spec.js @@ -0,0 +1,254 @@ +/// + +describe("Predictive search by filters", () => { + const baseUrl = Cypress.env("baseUrl"); + const timeout = Cypress.env("timeoutInMs"); + + const coreInfoKeys = [ + "cell", + "phone", + "employeementType", + "yearsPriorExperience", + "division", + "companyName", + "officeLocation", + "physicalLocation", + "hireDate", + ]; + const validWorkers = { + 10003: { + name: "Saul Sampson", + title: "Manager-Marketing", + email: "sampsons@acme.ca", + cell: "604-123-7654", + phone: "604-123-4567", + employeementType: "Salary", + yearsPriorExperience: "0.0", + division: "Marketing", + companyName: "Acme Seeds Inc.", + officeLocation: "Corporate, Vancouver", + physicalLocation: "Vancouver", + hireDate: "10/1/1999", + skills: {}, + }, + 60105: { + name: "Allison Martin", + title: "Associate", + email: "martin101@acme.ca", + cell: "778-565-1101", + phone: "604-132-1101", + employeementType: "Salary", + yearsPriorExperience: "2.0", + division: "Accounting", + companyName: "Acme Seeds Inc.", + officeLocation: "Corporate, Vancouver", + physicalLocation: "Victoria", + hireDate: "1/1/2020", + skills: { + Agriculture: ["Soil Preparation"], + }, + }, + 60815: { + name: "Amanda Hamiton", + title: "Associate", + email: "hamiton811@acme.ca", + cell: "778-565-1811", + phone: "604-132-1811", + employeementType: "Salary", + yearsPriorExperience: "2.0", + division: "Marketing & Sales", + companyName: "Acme Harvesting Ltd.", + officeLocation: "Kelowna", + physicalLocation: "Victoria", + hireDate: "5/6/1998", + skills: {}, + }, + 10005: { + name: "Connie Conner", + title: "Manager-HR/Accounting", + email: "connerc@acme.ca", + cell: "604-123-7654", + phone: "604-123-4567", + employeementType: "Salary", + yearsPriorExperience: "3.2", + division: "Human Resources", + companyName: "Acme Seeds Inc.", + officeLocation: "Corporate, Vancouver", + physicalLocation: "Vancouver", + hireDate: "5/28/1997", + skills: { + Accounting: [ + "Reconciling", + "Transaction Processing", + "Auditing", + ], + Management: ["Planning", "Performance Reviews", "Budgeting"], + Agriculture: ["Harvesting", "Fertilizing", "Soil Preparation"], + "Marketing & Sales": [ + "Preparing Marketing Materials", + "Customer Service", + ], + }, + }, + }; + + // check whether worker is valid, and optionally check for skills to search transition + const checkValidWorker = (workerId, checkForSkills = false) => { + if (!validWorkers[workerId]) { + cy.contains( + "Sorry, there is no employee or contractor with matching id." + ).should("exist"); + } else { + // check card + const card = () => cy.get('[data-cy="employee-card"]'); + const expectedResult = validWorkers[workerId]; + card() + .find(`.card-name-${workerId}`) + .should("contain.text", expectedResult.name); + card() + .find(`.card-title-${workerId}`) + .should("contain.text", expectedResult.title); + card() + .find(`.card-email-${workerId}`) + .should("contain.text", expectedResult.email); + + // check core info + const coreInfo = () => + cy.get('[data-cy="core-info-content"]').find("span"); + for (let counter = 0; counter < coreInfoKeys.length; counter++) { + coreInfo() + .eq(counter) + .should( + "contain.text", + expectedResult[coreInfoKeys[counter]] + ); + } + + // check skills + const skills = () => cy.get('[data-cy="profile-skill-content"]'); + const skillRow = (skillCategory) => + skills().find( + `[data-cy="profile-skill-group-${skillCategory}"]` + ); + if (Object.keys(expectedResult.skills).length === 0) { + skills().should("contain.text", "No skills"); + } else { + for (const skillCategory in expectedResult.skills) { + skillRow(skillCategory) + .find("td") + .should("contain.text", skillCategory); + for (const skill of expectedResult.skills[skillCategory]) { + if (!checkForSkills) { + skillRow(skillCategory) + .find(`[data-cy="profile-skill-chip-${skill}"]`) + .should("exist"); + } else { + // click on each chip and make sure search page has the corresponding chip in filter area + skillRow(skillCategory) + .find(`[data-cy="profile-skill-chip-${skill}"]`) + .click(); + + cy.get(".MuiChip-label").should("have.length", 1); + cy.get(".MuiChip-label").should( + "contain.text", + `${skill} (${skillCategory})` + ); + cy.contains("Profile View").click(); + } + } + } + + // check "search with these skills" + if (checkForSkills) { + cy.contains("Search with these skills").click(); + let totalSkillCount = 0; + for (const skillCategory in expectedResult.skills) { + for (const skill of expectedResult.skills[ + skillCategory + ]) { + cy.get(".MuiChip-label").should( + "contain.text", + `${skill} (${skillCategory})` + ); + totalSkillCount++; + } + } + cy.get(".MuiChip-label").should( + "have.length", + totalSkillCount + ); + } + } + } + }; + + it("valid workers from url", () => { + cy.visit(`${baseUrl}/profile/10003`); + + cy.get('[data-cy="loading-profile"]').should("exist"); + cy.get('[data-cy="loading-profile"]', { timeout }).should("not.exist"); + + checkValidWorker(10003, true); + + cy.visit(`${baseUrl}/profile/10005`); + + cy.get('[data-cy="loading-profile"]').should("exist"); + cy.get('[data-cy="loading-profile"]', { timeout }).should("not.exist"); + + checkValidWorker(10005, true); + }); + + it("invalid worker from url", () => { + cy.visit(`${baseUrl}/profile/123456`); + + cy.get('[data-cy="loading-profile"]').should("exist"); + cy.get('[data-cy="loading-profile"]', { timeout }).should("not.exist"); + + checkValidWorker(123456); + }); + + it("valid worker from profile + previous / next", () => { + cy.visit(baseUrl); + + cy.get('[data-cy="loading-results"]').should("exist"); + cy.get('[data-cy="loading-results"]', { timeout }).should("not.exist"); + + cy.contains("Allison Martin").click(); + + cy.get('[data-cy="loading-profile"]').should("not.exist"); + checkValidWorker(60105); + + cy.contains("Next").click(); + + cy.get('[data-cy="loading-profile"]').should("not.exist"); + checkValidWorker(60815); + + cy.contains("Previous").click(); + + cy.get('[data-cy="loading-profile"]').should("not.exist"); + checkValidWorker(60105); + }); + + it("valid worker from org chart", () => { + cy.visit(`${baseUrl}/orgchart/10003`); + + cy.get('[data-cy="loading-orgchart"]').should("exist"); + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + cy.contains("Profile View").click(); + + cy.get('[data-cy="loading-profile"]').should("not.exist"); + checkValidWorker(10003); + + cy.contains("Organization Chart").click(); + + cy.contains("Connie Conner").click(); + + cy.get('[data-cy="loading-orgchart"]').should("exist"); + cy.get('[data-cy="loading-orgchart"]', { timeout }).should("not.exist"); + + cy.contains("Profile View").click(); + + checkValidWorker(10005); + }); +}); diff --git a/Frontend/src/components/AddContractor.jsx b/Frontend/src/components/AddContractor.jsx index b6cdc59f..88ddaa91 100644 --- a/Frontend/src/components/AddContractor.jsx +++ b/Frontend/src/components/AddContractor.jsx @@ -30,7 +30,10 @@ import { PagePathEnum } from "./common/constants"; import { Storage } from "aws-amplify"; import config from "../config"; import { coordinatedDebounce } from "./common/helpers"; -import { setSnackbarState } from 'actions/generalAction'; +import { setSnackbarState } from "actions/generalAction"; + +// counter for timeout in case of supervisor input change +const predictiveSearchTimer = {}; function AddContractor(props) { const { filterData, isAdmin, setSnackbarState } = props; @@ -61,8 +64,6 @@ function AddContractor(props) { selectedSkills: [], }; const [formState, setFormState] = React.useState(defaultState); - // counter for timeout in case of supervisor input change - const predictiveSearchTimer = {}; React.useEffect(() => { if ( @@ -504,10 +505,10 @@ function AddContractor(props) { /> @@ -532,10 +533,12 @@ function AddContractor(props) { /> )} renderOption={(option) => + option === "loading" || formState.loadingState["supervisor"] ? ( -
+ data-cy="loading-supervisor-result" + >

option} className={classes.textField} renderInput={(params) => ( @@ -736,24 +739,23 @@ function AddContractor(props) { /> )} /> -
- +
+
{ }; const mapDispatchToProps = (dispatch) => ({ - setSnackbarState: (snackbarState) => dispatch(setSnackbarState(snackbarState)), + setSnackbarState: (snackbarState) => + dispatch(setSnackbarState(snackbarState)), }); -export default withRouter(connect(mapStateToProps, - mapDispatchToProps)(AddContractor)); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(AddContractor) +); const useStyles = makeStyles(() => ({ root: { display: "flex", - justifyContent:"center", + justifyContent: "center", }, input: { display: "none", @@ -813,7 +817,7 @@ const useStyles = makeStyles(() => ({ }, submitBtnWrapper: { display: "flex", - justifyContent:"center", + justifyContent: "center", }, submitBtn: { width: 200, diff --git a/Frontend/src/components/ProfilePageContainer.jsx b/Frontend/src/components/ProfilePageContainer.jsx index 8e167e45..8385e486 100644 --- a/Frontend/src/components/ProfilePageContainer.jsx +++ b/Frontend/src/components/ProfilePageContainer.jsx @@ -68,6 +68,7 @@ export function ProfilePageContainer(props) { ); diff --git a/Frontend/src/components/profilePage/CoreInfoArea.jsx b/Frontend/src/components/profilePage/CoreInfoArea.jsx index de78b55b..ee0642bf 100644 --- a/Frontend/src/components/profilePage/CoreInfoArea.jsx +++ b/Frontend/src/components/profilePage/CoreInfoArea.jsx @@ -44,6 +44,7 @@ function CoreInfoArea(props) { color="textPrimary" // @ts-ignore component="p" + data-cy="core-info-content" > {information.map((entry) => getInfoEntry(entry[0], entry[1]))} diff --git a/Frontend/src/components/profilePage/SkillsArea.jsx b/Frontend/src/components/profilePage/SkillsArea.jsx index 8988a6af..28e86e55 100644 --- a/Frontend/src/components/profilePage/SkillsArea.jsx +++ b/Frontend/src/components/profilePage/SkillsArea.jsx @@ -76,7 +76,7 @@ const parseSkillsToTable = ( for (const skillCategory in skillObject) { let skillCounter = 0; skillEntries.push( - + { const skills = {}; skills[skillCategory] = [skill]; @@ -140,7 +141,7 @@ function SkillsArea(props) { Search with these skills - + {parseSkillsToTable( skillObject, styles, diff --git a/Frontend/src/components/searchPage/searchArea/ApplyFilterWidget.jsx b/Frontend/src/components/searchPage/searchArea/ApplyFilterWidget.jsx index cc96f099..55175d15 100644 --- a/Frontend/src/components/searchPage/searchArea/ApplyFilterWidget.jsx +++ b/Frontend/src/components/searchPage/searchArea/ApplyFilterWidget.jsx @@ -153,6 +153,7 @@ function CollapsableCategoryBox(props) { button className="category" onClick={handleExpandMoreClick} + data-cy={`category-title-${label}`} > {!expanded ? ( @@ -161,7 +162,12 @@ function CollapsableCategoryBox(props) { )} - + {children} diff --git a/Frontend/src/components/searchPage/searchArea/SearchByNameBar.jsx b/Frontend/src/components/searchPage/searchArea/SearchByNameBar.jsx index 3eec6594..4f2ea797 100644 --- a/Frontend/src/components/searchPage/searchArea/SearchByNameBar.jsx +++ b/Frontend/src/components/searchPage/searchArea/SearchByNameBar.jsx @@ -126,13 +126,13 @@ function SearchByNameBar(props) { getOptionLabel={() => inputValue} openOnFocus={true} freeSolo={true} - data-cy="search-by-name" renderInput={(params) => ( )} renderOption={(option, state) => {