neetoMolecules.browserSupport.unsupportedBrowser.description

Lesson

28.3Refactoring the tasks POM

In the login POM we can observe the following improvements that can be made:

  1. Move all selectors to constants
  2. Use the short-hand notation for initializing the class variables
  3. Move all texts to constants
  4. Avoid nth methods

Since we are already familiar with the first two action items, let's implement them now.

1// e2e/constants/selectors/createTask.ts 2 3export const CREATE_TASK_SELECTORS = { 4 taskTitleField: "form-title-field", 5 memberSelectContainer: ".css-2b097c-container", 6 memberOptionField: ".css-26l3qy-menu", 7 createTaskButton: "form-submit-button", 8};
1// e2e/constants/selectors/dashboard.ts 2 3export const NAVBAR_SELECTORS = { 4 usernameLabel: "navbar-username-label", 5 logoutButton: "navbar-logout-link", 6 addTodoButton: "navbar-add-todo-link", 7}; 8 9export const TASKS_TABLE_SELECTORS = { 10 pendingTasksTable: "tasks-pending-table", 11 completedTasksTable: "tasks-completed-table", 12 starUnstarButton: "pending-task-star-or-unstar-link", 13};
1// e2e/constants/selectors/index.ts 2 3import { CREATE_TASK_SELECTORS } from "./createTask"; 4import { NAVBAR_SELECTORS, TASKS_TABLE_SELECTORS } from "./dashboard"; 5import { LOGIN_SELECTORS } from "./login"; 6 7export { 8 NAVBAR_SELECTORS, 9 LOGIN_SELECTORS, 10 TASKS_TABLE_SELECTORS, 11 CREATE_TASK_SELECTORS, 12};
1import { Page, expect } from "@playwright/test"; 2import { 3 CREATE_TASK_SELECTORS, 4 NAVBAR_SELECTORS, 5 TASKS_TABLE_SELECTORS, 6} from "../constants/selectors"; 7 8interface TaskName { 9 taskName: string; 10} 11 12interface CreateNewTaskProps extends TaskName { 13 userName?: string; 14} 15 16export class TaskPage { 17 constructor(private page: Page) {} 18 19 createTaskAndVerify = async ({ 20 taskName, 21 userName = "Oliver Smith", 22 }: CreateNewTaskProps) => { 23 await this.page.getByTestId(NAVBAR_SELECTORS.addTodoButton).click(); 24 await this.page 25 .getByTestId(CREATE_TASK_SELECTORS.taskTitleField) 26 .fill(taskName); 27 28 await this.page 29 .locator(CREATE_TASK_SELECTORS.memberSelectContainer) 30 .click(); 31 await this.page 32 .locator(CREATE_TASK_SELECTORS.memberOptionField) 33 .getByText(userName) 34 .click(); 35 await this.page.getByTestId(CREATE_TASK_SELECTORS.createTaskButton).click(); 36 const taskInDashboard = this.page 37 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 38 .getByRole("row", { 39 name: new RegExp(taskName, "i"), 40 }); 41 await taskInDashboard.scrollIntoViewIfNeeded(); 42 await expect(taskInDashboard).toBeVisible(); 43 }; 44 45 markTaskAsCompletedAndVerify = async ({ taskName }: TaskName) => { 46 await expect( 47 this.page.getByRole("heading", { name: "Loading..." }) 48 ).toBeHidden(); 49 50 const completedTaskInDashboard = this.page 51 .getByTestId(TASKS_TABLE_SELECTORS.completedTasksTable) 52 .getByRole("row", { name: taskName }); 53 54 const isTaskCompleted = await completedTaskInDashboard.count(); 55 56 if (isTaskCompleted) return; 57 58 await this.page 59 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 60 .getByRole("row", { name: taskName }) 61 .getByRole("checkbox") 62 .click(); 63 await completedTaskInDashboard.scrollIntoViewIfNeeded(); 64 await expect(completedTaskInDashboard).toBeVisible(); 65 }; 66 67 starTaskAndVerify = async ({ taskName }: TaskName) => { 68 const starIcon = this.page 69 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 70 .getByRole("row", { name: taskName }) 71 .getByTestId(TASKS_TABLE_SELECTORS.starUnstarButton); 72 await starIcon.click(); 73 await expect(starIcon).toHaveClass(/ri-star-fill/i); 74 await expect( 75 this.page 76 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 77 .getByRole("row") 78 .nth(1) 79 ).toContainText(taskName); 80 }; 81}

Now let's deal with the 3rd action item. Let's move all the hard-coded texts to constants. For this let's create a directory called texts within the constants directory. Since all the texts we're dealing with are on the dashboard page, we will create a new file called dashboard.ts where we will extract all the texts related to the dashboard page. Just like for the selectors, we will create an index.ts file which will export all the constants for easier imports across modules.

Additionally there are some texts which are common across multiple pages like the username Oliver Smith. We will move such texts to a file called common.ts within the texts directory.

1mkdir e2e/constants/texts 2touch e2e/constants/texts/dashboard.ts 3touch e2e/constants/texts/index.ts 4touch e2e/constants/texts/common.ts

Now that we have created all the files and directories, let's extract the texts and refactor the code.

1// e2e/constants/texts/common.ts 2 3export const COMMON_TEXTS = { 4 defaultUserName: "Oliver Smith", 5};
1// e2e/constants/texts/dashboard.ts 2 3export const DASHBOARD_TEXTS = { 4 loading: "Loading...", 5 starredTaskClass: /ri-star-fill/i, 6};
1// e2e/constants/texts/index.ts 2 3import { DASHBOARD_TEXTS } from "./dashboard"; 4import { COMMON_TEXTS } from "./common"; 5 6export { DASHBOARD_TEXTS, COMMON_TEXTS };
1// e2e/poms/tasks.ts 2 3import { Page, expect } from "@playwright/test"; 4import { 5 CREATE_TASK_SELECTORS, 6 NAVBAR_SELECTORS, 7 TASKS_TABLE_SELECTORS, 8} from "../constants/selectors"; 9import { COMMON_TEXTS, DASHBOARD_TEXTS } from "../constants/texts"; 10 11interface TaskName { 12 taskName: string; 13} 14 15interface CreateNewTaskProps extends TaskName { 16 userName?: string; 17} 18 19export class TaskPage { 20 constructor(private page: Page) {} 21 22 createTaskAndVerify = async ({ 23 taskName, 24 userName = COMMON_TEXTS.defaultUserName, 25 }: CreateNewTaskProps) => { 26 await this.page.getByTestId(NAVBAR_SELECTORS.addTodoButton).click(); 27 await this.page 28 .getByTestId(CREATE_TASK_SELECTORS.taskTitleField) 29 .fill(taskName); 30 31 await this.page 32 .locator(CREATE_TASK_SELECTORS.memberSelectContainer) 33 .click(); 34 await this.page 35 .locator(CREATE_TASK_SELECTORS.memberOptionField) 36 .getByText(userName) 37 .click(); 38 await this.page.getByTestId(CREATE_TASK_SELECTORS.createTaskButton).click(); 39 const taskInDashboard = this.page 40 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 41 .getByRole("row", { 42 name: new RegExp(taskName, "i"), 43 }); 44 await taskInDashboard.scrollIntoViewIfNeeded(); 45 await expect(taskInDashboard).toBeVisible(); 46 }; 47 48 markTaskAsCompletedAndVerify = async ({ taskName }: TaskName) => { 49 await expect( 50 this.page.getByRole("heading", { name: DASHBOARD_TEXTS.loading }) 51 ).toBeHidden(); 52 53 const completedTaskInDashboard = this.page 54 .getByTestId(TASKS_TABLE_SELECTORS.completedTasksTable) 55 .getByRole("row", { name: taskName }); 56 57 const isTaskCompleted = await completedTaskInDashboard.count(); 58 59 if (isTaskCompleted) return; 60 61 await this.page 62 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 63 .getByRole("row", { name: taskName }) 64 .getByRole("checkbox") 65 .click(); 66 await completedTaskInDashboard.scrollIntoViewIfNeeded(); 67 await expect(completedTaskInDashboard).toBeVisible(); 68 }; 69 70 starTaskAndVerify = async ({ taskName }: TaskName) => { 71 const starIcon = this.page 72 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 73 .getByRole("row", { name: taskName }) 74 .getByTestId(TASKS_TABLE_SELECTORS.starUnstarButton); 75 await starIcon.click(); 76 await expect(starIcon).toHaveClass(DASHBOARD_TEXTS.starredTaskClass); 77 await expect( 78 this.page 79 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 80 .getByRole("row") 81 .nth(1) 82 ).toContainText(taskName); 83 }; 84}

Great! Now we have to deal with the final action item for the POM. This involves removing all the nth methods. But our use-case here is to ensure that the starred task is moved to the top of the list. That means we need to ensure that the first row of the table is the starred task. This is a genuine use case for the nth methods and it cannot be avoided. So instead of removing it, let's add a comment in the code stating our intentions for breaking a best practice.

1// e2e/poms/tasks.ts 2 3starTaskAndVerify = async ({ taskName }: TaskName) => { 4 const starIcon = this.page 5 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 6 .getByRole("row", { name: taskName }) 7 .getByTestId(TASKS_TABLE_SELECTORS.starUnstarButton); 8 await starIcon.click(); 9 await expect(starIcon).toHaveClass(DASHBOARD_TEXTS.starredTaskClass); 10 await expect( 11 this.page 12 .getByTestId(TASKS_TABLE_SELECTORS.pendingTasksTable) 13 .getByRole("row") 14 .nth(1) // Using nth methods here since we want to verify the first row of the table 15 ).toContainText(taskName); 16};