Skip to content

Commit aa04cc3

Browse files
Wrote Part 4 section 1
1 parent 5441e77 commit aa04cc3

File tree

6 files changed

+215
-3
lines changed

6 files changed

+215
-3
lines changed

pages/__init__.py

Whitespace-only changes.

pages/result.py

Whitespace-only changes.

pages/search.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
This module contains DuckDuckGoSearchPage,
3+
the page object for the DuckDuckGo search page.
4+
"""
5+
6+
class DuckDuckGoSearchPage:
7+
8+
SEARCH_BUTTON = '#search_button_homepage'
9+
SEARCH_INPUT = '#search_form_input_homepage'
10+
11+
URL = 'https://www.duckduckgo.com'
12+
13+
def __init__(self, page):
14+
self.page = page
15+
16+
def load(self):
17+
self.page.goto(self.URL)
18+
19+
def search(self, phrase):
20+
self.page.fill(self.SEARCH_INPUT, phrase)
21+
self.page.click(self.SEARCH_BUTTON)

tests/test_search.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
These tests cover DuckDuckGo searches.
33
"""
44

5+
from pages.search import DuckDuckGoSearchPage
6+
57

68
def test_basic_duckduckgo_search(page):
9+
search_page = DuckDuckGoSearchPage(page)
710

811
# Given the DuckDuckGo home page is displayed
9-
page.goto('https://www.duckduckgo.com')
12+
search_page.load()
1013

1114
# When the user searches for a phrase
12-
page.fill('#search_form_input_homepage', 'panda')
13-
page.click('#search_button_homepage')
15+
search_page.search('panda')
1416

1517
# Then the search result query is the phrase
1618
assert 'panda' == page.input_value('#search_form_input')

workshop/4-page-objects.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Refactoring using page objects
2+
3+
As we saw in the previous part, Playwright calls are wonderfully concise.
4+
We may be tempted to use raw Playwright calls in all our tests.
5+
However, raw calls quickly lead to code duplication.
6+
7+
In this part, we will refactor our DuckDuckGo search test using the
8+
[Page Object Model](https://www.selenium.dev/documentation/guidelines/page_object_models/) (POM).
9+
Page objects, though imperfect, provide decent structure and helpful reusability.
10+
They are superior to raw Playwright calls when automating multiple tests instead of only one script.
11+
12+
13+
## The search page
14+
15+
Our search tests interacts with two pages:
16+
17+
1. The DuckDuckGo search page
18+
2. The DuckDuckGo result page
19+
20+
Each page should be modeled by its own class.
21+
Page object classes should be located in a package outside of the `tests` directory so that they can be imported by tests.
22+
23+
Create a new directory named `pages`, and inside it, create blank files with the following names:
24+
25+
* `__init__.py`
26+
* `search.py`
27+
* `result.py`
28+
29+
Your project's directory layout should look like this:
30+
31+
```
32+
tau-playwright-workshop
33+
├── pages
34+
│ ├── __init__.py
35+
│ ├── search.py
36+
│ └── result.py
37+
└── tests
38+
└── test_search.py
39+
```
40+
41+
The `__init__.py` file turns the `pages` directory into a Python package so that other Python modules can import it.
42+
It will stay permanently empty.
43+
The `search.py` and `result.py` modules will contain the search and result page object classes respectively.
44+
45+
Let's implement the search page first.
46+
We will use much of our original code.
47+
48+
A page object class typically has three main parts:
49+
50+
1. Selectors and any other data stored as variables
51+
2. Dependency injection of the browser automator through a constructor
52+
3. Interaction methods that use the browser automator and the selectors
53+
54+
Let's add these one at a time.
55+
Inside `pages/search.py`, add a class definition for the page object:
56+
57+
```python
58+
class DuckDuckGoSearchPage:
59+
```
60+
61+
Inside this class, add the selectors we used in our test for the search input and search button:
62+
63+
```python
64+
SEARCH_BUTTON = '#search_button_homepage'
65+
SEARCH_INPUT = '#search_form_input_homepage'
66+
```
67+
68+
These will be class variables, not instance variables.
69+
They are also plain-old strings.
70+
We could use these selectors for many different actions.
71+
72+
Let's also add the DuckDuckGo URL:
73+
74+
```python
75+
URL = 'https://www.duckduckgo.com'
76+
```
77+
78+
*(Warning:
79+
Base URLs should typically be passed into automation code as an input, not hard-coded in a page object.
80+
We are doing this here as a matter of simplicity for this workshop.)*
81+
82+
Next, let's handle dependency injection for the browser automator.
83+
Since each test will have its own Playwright page, we should inject that page.
84+
(If we were using Selenium WebDriver, then we would inject the WebDriver instance.)
85+
Add the following initializer method to the class:
86+
87+
```python
88+
def __init__(self, page):
89+
self.page = page
90+
```
91+
92+
The `__init__` method is essentially a constructor for Python classes
93+
(but with a bit of nuance that doesn't matter for this workshop).
94+
It has one argument named `page` for the Playwright page,
95+
which it stores as an instance variable (via `self`).
96+
97+
With the page injected, we can now use it to make interactions.
98+
One interaction our test performs is loading the DuckDuckGo search page.
99+
Here's a method to do that:
100+
101+
```python
102+
def load(self):
103+
self.page.goto(self.URL)
104+
```
105+
106+
It uses the injected page as well as the `URL` variable.
107+
108+
The other interaction our test performs is searching for a phrase.
109+
Here's a method to do that:
110+
111+
```python
112+
def search(self, phrase):
113+
self.page.fill(self.SEARCH_INPUT, phrase)
114+
self.page.click(self.SEARCH_BUTTON)
115+
```
116+
117+
This `search` method uses the injected page and the selector variables.
118+
It also takes in the search phrase as an argument so that it can handle any phrase.
119+
120+
The completed search page object class should look like this:
121+
122+
```python
123+
class DuckDuckGoSearchPage:
124+
125+
SEARCH_BUTTON = '#search_button_homepage'
126+
SEARCH_INPUT = '#search_form_input_homepage'
127+
128+
URL = 'https://www.duckduckgo.com'
129+
130+
def __init__(self, page):
131+
self.page = page
132+
133+
def load(self):
134+
self.page.goto(self.URL)
135+
136+
def search(self, phrase):
137+
self.page.fill(self.SEARCH_INPUT, phrase)
138+
self.page.click(self.SEARCH_BUTTON)
139+
```
140+
141+
This diagram shows how each section of this class fits the standard sections of a page object class:
142+
143+
![Page object structure](images/page-object-structure.png)
144+
145+
We can now refactor the original test case to use this new page object!
146+
Replace this old code:
147+
148+
```python
149+
def test_basic_duckduckgo_search(page):
150+
151+
# Given the DuckDuckGo home page is displayed
152+
page.goto('https://www.duckduckgo.com')
153+
154+
# When the user searches for a phrase
155+
page.fill('#search_form_input_homepage', 'panda')
156+
page.click('#search_button_homepage')
157+
```
158+
159+
With this new code:
160+
161+
```python
162+
from pages.search import DuckDuckGoSearchPage
163+
164+
def test_basic_duckduckgo_search(page):
165+
search_page = DuckDuckGoSearchPage(page)
166+
167+
# Given the DuckDuckGo home page is displayed
168+
search_page.load()
169+
170+
# When the user searches for a phrase
171+
search_page.search('panda')
172+
```
173+
174+
The new code must import `DuckDuckGoSearchPage` from the `pages.search` module.
175+
The test then constructs a `DuckDuckGoSearchPage` object and uses it to perform interactions.
176+
Notice that the test case no longer has hard-coded selectors or URLs.
177+
The code is also more self-documenting.
178+
179+
Rerun the test (`python3 -m pytest tests --headed --slowmo 1000`).
180+
The test should pass.
181+
Nothing has functionally changed for the test:
182+
it still performs the same operations.
183+
Now, it just uses a page object for the search page instead of raw calls.
184+
185+
186+
## The result page
187+
188+
189+
## Page object fixtures
287 KB
Loading

0 commit comments

Comments
 (0)