In the login POM we can observe the following improvements that can be made:
- Move all selectors to constants
- Use the short-hand notation for initializing the class variables
- Move all texts to constants
- 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};