Skip to content
63 changes: 63 additions & 0 deletions tests/e2e/data/manager/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,69 @@ module.exports = {
"Shraddha Gore",
],
},
TC60: {
cell: {
rowName: "TC60 Project",
col: "Tue",
},
payloadCreateProject: {
project_name: "TC60 Project",
company: "rtCamp Solutions Pvt. Ltd.",
customer: "Google",
billing_type: "Fixed Cost",
currency: "INR",
project_type: "Fixed Cost",
business_unit: "Jupitor",
estimated_cost: 235000,
custom_default_hourly_billing_rate: 300,
custom_project_budget_hours: [],
},
employee: process.env.EMP3_NAME,
payloadDeleteProject: {
projectId: "filled-automatically-from-createProjects",
},
payloadCreateTask: {
subject: "TC60 Billable Task",
project: "filled-automatically-from-createProjects",
description: "Task for TC60 created through automation",
custom_is_billable: 1,
},
payloadDeleteTask: {
taskID: "filled-automatically-from-createTasks",
},
payloadCreateTimesheet: {
task: "filled-automatically-from-createTasks",
description: "<p>TC60 - Task added via automation.</p>",
hours: "5",
},
payloadFilterTimeEntry: {
subject: "TC60 Billable Task",
description: "TC60 - Task added via automation.",
project_name: "TC60 Project",
max_week: "1",
},
},
TC61: {
weeklyTime: "0 / 40",
},
TC68: {
payloadCreateProject: {
project_name: "TC68 Project",
company: "rtCamp Solutions Pvt. Ltd.",
customer: "Google",
custom_billing_type: "Fixed Cost",
custom_currency: "INR",
project_type: "Fixed Cost",
business_unit: "Jupitor",
estimated_cost: 360000,
custom_default_hourly_billing_rate: 0,
custom_project_budget_hours: [],
},
employee: process.env.EMP3_NAME,
payloadDeleteProject: {
projectId: "filled-automatically-from-createProjects",
},
},
TC91: {
payloadCreateEmployee: {
first_name: "Playwright-",
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/helpers/timesheetHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function updateTimeEntries(testCaseIDs = [], jsonDir) {
let employeeID;
if (testCaseID === "TC2") {
employeeID = emp2ID;
} else if (["TC6", "TC7", "TC92"].includes(testCaseID)) {
} else if (["TC6", "TC7", "TC92", "TC60"].includes(testCaseID)) {
employeeID = emp3ID;
} else {
employeeID = empID;
Expand Down
14 changes: 13 additions & 1 deletion tests/e2e/pageObjects/resourceManagement/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,19 @@ export class ProjectPage extends TimelinePage {
this.totalHoursTextField.fill("");
this.totalHoursTextField.fill(totalAllocatedHours);
await this.setHoursPerDay(hoursPerDay);
await this.clickCreateButton();
// Wait for the allocation API response and click the create button in parallel
const [response] = await Promise.all([
this.page.waitForResponse(
(response) =>
response.url().includes("/api/method/next_pms.resource_management.api.allocation.handle_allocation") &&
response.status() === 200
),
this.clickCreateButton(),
]);

const responseBody = await response.json();
const updatedAllocationName = responseBody.message.name;
return { updatedAllocationName };
}

/**
Expand Down
162 changes: 160 additions & 2 deletions tests/e2e/pageObjects/resourceManagement/team.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,65 @@
import { TimelinePage } from "./timeline";
import { expect } from "@playwright/test";

export class TeamPage extends TimelinePage {
constructor(page) {
super(page);
this.deleteButton = page.getByRole("img", { name: "Delete" });
this.firstEmployeeFromTable = page.locator("table:nth-child(1) tbody > div:nth-child(1) p").first(); // using locator to fetch dynamically the first employee
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jayson31 , please use playwright locator here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add .locator here as this fetches the first employee name from the list. This is used to fetch the first employee after filtering.

this.extendedListResourceAllocation = page.locator("div[data-state=open] .pt-0.pb-0");
this.reportsToDropdown = page.getByRole("button", { name: "Reporting Manager" });
this.employeeCountFromTable = page.locator("table:nth-child(1) tbody > div"); // locator for all employees in the table
this.leftSidebar = page.getByText("Next PMSHomeTimesheetTeamProjectTaskResource");

// filter locators related to skill filter dropdown
this.skillFilterDropdown = page.getByRole("button", { name: "Skill" });
this.searchSkillInput = page.getByRole("textbox", { name: "Search skills..." });
this.skillSelectorFromModal = (value) => page.getByRole("button", { name: value });
this.twoStarsSelector = page.getByRole("dialog").getByRole("button").filter({ hasText: /^$/ }).nth(1);
this.searchSkillButton = page.getByRole("button", { name: "Search" });
this.clearSkillButton = page.getByRole("button", { name: "Clear" });

// filter locators related to business unit filter dropdown
this.businessUnitFilterDropdown = page.getByRole("button", { name: "Business Unit" });
this.businessUnitOptionSelector = (value) => page.getByRole("option", { name: value, exact: true });

// filter locators related to designation filter dropdown
this.designationFilterDropdown = page.getByRole("button", { name: "Designation" });
this.designationSearchDropdown = page.getByPlaceholder("Designation");
this.designationOptionSelector = (value) => page.getByRole("option", { name: value });

// filter locators related to allocation type filter dropdown
this.allocationTypeFilterDropdown = page.getByRole("button", { name: "Allocation Type" });
this.allocationTypeOptionSelector = (value) => page.getByRole("option", { name: value, exact: true });

// filter locators related to views filter dropdown
this.viewsFilterDropdown = page.getByRole("combobox");
this.selectViewOption = (view) => page.getByRole("option", { name: view });

// filter locators related to employee filter dropdown
this.combineWeekHoursCheckbox = page.locator("#combineWeekHours");
this.previousWeekButton = page.getByRole("button", { name: "previous-week" });
this.nextWeekButton = page.getByRole("button", { name: "next-week" });
}

/**
* Navigates to the team page and waits for it to fully load.
*/
async goto() {
await this.page.goto("/next-pms/resource-management/team", { waitUntil: "domcontentloaded" });
await Promise.all([
this.page.waitForResponse(
(resp) =>
resp
.url()
.includes("/api/method/next_pms.resource_management.api.team.get_resource_management_team_view_data") &&
resp.status() === 200
),
this.page.goto("/next-pms/resource-management/team", { waitUntil: "domcontentloaded" }),
]);
}

async isPageVisible() {
await expect(page).toHaveURL("/resource-management/team");
await expect(this.page).toContain("/resource-management/team");
}

/**
Expand Down Expand Up @@ -60,4 +105,117 @@ export class TeamPage extends TimelinePage {
await this.deleteButton.click();
await this.confirmDeleteButton.click();
}

/**
* Fetches the first employee by their name in the table.
*/
async getFirstEmployeeNameFromTable() {
return this.firstEmployeeFromTable.textContent();
}

async checkIfExtendedResourceAllocationIsVisible() {
return this.extendedListResourceAllocation.isVisible({ timeout: 5000 });
}

/**
* Clicks on the first employee from the table.
* This is useful for selecting the first employee in the team view.
*/
async clickFirstEmployeeFromTable() {
await this.page.waitForTimeout(200); // slight delay for the table to load
await this.firstEmployeeFromTable.click();
}

/**
* Filters the team by employee name.
*/
async getEmployeeCountFromTable() {
await this.page.waitForTimeout(200); // slight delay for the table to load
return await this.employeeCountFromTable.count();
}

/**
* Performs a search and selection within a modal based on a placeholder text.
*/
async searchAndSelectOption(placeholder, value) {
const searchInput = this.page.getByRole("dialog").getByPlaceholder(`${placeholder}`);
await searchInput.fill(value);
await this.page.waitForTimeout(1000);
await this.page.getByRole("option", { name: value }).click();
}

/**
* Applies the 'Reports To' filter by selecting an employee from the dropdown.
*/
async applyReportsTo(name) {
await this.reportsToDropdown.click();
await this.searchAndSelectOption("Search Employee", name);
await this.page.waitForTimeout(1000);
}

/**
* Adds a filter based on the filter name and value.
*/
async addfilter(filterName, value) {
switch (filterName) {
case "Skill":
await this.skillFilterDropdown.click();
await this.searchSkillInput.fill(value);
await this.skillSelectorFromModal(value).click();
await this.twoStarsSelector.click(); // Selects the 3-star rating
await this.searchSkillButton.click();
break;
case "Designation":
await this.designationFilterDropdown.click();
await this.designationSearchDropdown.fill(value);
await this.designationOptionSelector(value).click();
break;
case "Business Unit":
await this.businessUnitFilterDropdown.click();
await this.businessUnitOptionSelector(value).click();
break;
case "Allocation Type":
await this.allocationTypeFilterDropdown.click();
await this.allocationTypeOptionSelector(value).click();
break;
default:
throw new Error(`Unknown filter: ${filterName}`);
}
await this.page.locator("html").click();
}

/**
* Clicks on the Combine Week Hours checkbox in header.
*/
async clickCombineWeekHoursCheckbox() {
await this.combineWeekHoursCheckbox.click();
}

/**
* Clicks the view Previous Week button in header.
*/
async clickPreviousWeekButton() {
await this.previousWeekButton.click();
}

/**
* Clicks the view Next Week button in header.
*/
async clickNextWeekButton() {
await this.nextWeekButton.click();
}

/**
* Selects a specific view from the views filter dropdown.
*
*/
async selectView(view) {
await this.viewsFilterDropdown.click();
try {
await this.selectViewOption(view).click();
} catch {
throw new Error(`View option '${view}' not found in the dropdown.`);
}
await this.page.locator("html").click(); // Click outside to close the dropdown
}
}
20 changes: 19 additions & 1 deletion tests/e2e/pageObjects/resourceManagement/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class TimelinePage {
// header elements
this.addAllocatioButtton = page.getByRole("button", { name: "add-allocation" });
this.searchEmployeeFilter = page.locator("#filters input");
this.clearFilterIcons = page.locator("div#filters:nth-child(2) svg");

//add allocation modal elements
this.modalErrorMessage = page.locator("p[id*='form-item-message']");
Expand Down Expand Up @@ -96,7 +97,14 @@ export class TimelinePage {
* Selects a date range for the allocation.
*/
async addDateRange(formattedDate = this.formattedDate) {
const dayNumber = formattedDate.split(" ")[1];
let dayNumber;
if (/^\d{4}-\d{2}-\d{2}$/.test(formattedDate)) {
// Handles YYYY-MM-DD format
dayNumber = formattedDate.split("-")[2].replace(/^0/, ""); // Extracts the day and removes leading zero
} else {
// Handles date format like June 15
dayNumber = formattedDate.split(" ")[1].replace(",", ""); // Extracts the day and removes the comma
}
await this.startDateSelector.click();
await this.page
.getByRole("gridcell", { name: dayNumber, exact: true })
Expand Down Expand Up @@ -199,4 +207,14 @@ export class TimelinePage {
async getErrorFromAllocationModal() {
return await this.modalErrorMessage.textContent();
}

/**
* Clear Applied filters
*/
async clearFilters() {
while ((await this.clearFilterIcons.count()) > 0) {
const clearFilterIcon = this.clearFilterIcons.first();
await clearFilterIcon.click();
}
}
}
Loading
Loading