Skip to content

Commit fbdbfdf

Browse files
Wrote Part 4 section 2
1 parent aa04cc3 commit fbdbfdf

File tree

3 files changed

+169
-6
lines changed

3 files changed

+169
-6
lines changed

pages/result.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
This module contains DuckDuckGoResultPage,
3+
the page object for the DuckDuckGo result page.
4+
"""
5+
6+
class DuckDuckGoResultPage:
7+
8+
RESULT_LINKS = '.result__title a.result__a'
9+
SEARCH_INPUT = '#search_form_input'
10+
11+
def __init__(self, page):
12+
self.page = page
13+
14+
def result_link_titles(self):
15+
self.page.locator(f'{self.RESULT_LINKS} >> nth=4').wait_for()
16+
titles = self.page.locator(self.RESULT_LINKS).all_text_contents()
17+
return titles
18+
19+
def result_link_titles_contain_phrase(self, phrase, minimum=1):
20+
titles = self.result_link_titles()
21+
matches = [t for t in titles if phrase.lower() in t.lower()]
22+
return len(matches) >= minimum
23+
24+
def search_input_value(self):
25+
return self.page.input_value(self.SEARCH_INPUT)
26+
27+
def title(self):
28+
return self.page.title()

tests/test_search.py

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

5+
from pages.result import DuckDuckGoResultPage
56
from pages.search import DuckDuckGoSearchPage
67

78

89
def test_basic_duckduckgo_search(page):
910
search_page = DuckDuckGoSearchPage(page)
11+
result_page = DuckDuckGoResultPage(page)
1012

1113
# Given the DuckDuckGo home page is displayed
1214
search_page.load()
@@ -15,13 +17,10 @@ def test_basic_duckduckgo_search(page):
1517
search_page.search('panda')
1618

1719
# Then the search result query is the phrase
18-
assert 'panda' == page.input_value('#search_form_input')
20+
assert 'panda' == result_page.search_input_value()
1921

2022
# And the search result links pertain to the phrase
21-
page.locator('.result__title a.result__a >> nth=4').wait_for()
22-
titles = page.locator('.result__title a.result__a').all_text_contents()
23-
matches = [t for t in titles if 'panda' in t.lower()]
24-
assert len(matches) > 0
23+
assert result_page.result_link_titles_contain_phrase('panda')
2524

2625
# And the search result title contains the phrase
27-
assert 'panda' in page.title()
26+
assert 'panda' in result_page.title()

workshop/4-page-objects.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,5 +185,141 @@ Now, it just uses a page object for the search page instead of raw calls.
185185

186186
## The result page
187187

188+
After writing the search page class, the result page class will be straightforward.
189+
It will follow the same structure.
190+
The main difference is that each interaction methods in the result page class will return a value
191+
because test assertions will check page values.
192+
193+
Start by adding the class definition to `pages/result.py`:
194+
195+
```python
196+
class DuckDuckGoResultPage:
197+
```
198+
199+
Add the selectors we used in the test case:
200+
201+
```python
202+
RESULT_LINKS = '.result__title a.result__a'
203+
SEARCH_INPUT = '#search_form_input'
204+
```
205+
206+
Add dependency injection:
207+
208+
```python
209+
def __init__(self, page):
210+
self.page = page
211+
```
212+
213+
Now, let's add interaction methods for all the things assertions must check.
214+
The first assertion checked the input value of the search input.
215+
Let's add a method to get and return that input value:
216+
217+
```python
218+
def search_input_value(self):
219+
return self.page.input_value(self.SEARCH_INPUT)
220+
```
221+
222+
The second assertion was the most complex.
223+
It checked if at least one result link title contained the search phrase.
224+
We can break this down into two methods:
225+
226+
1. A method to get all result link titles as a list.
227+
2. A method to check if the list of result link titles contains a phrase.
228+
229+
Add the following methods to the class:
230+
231+
```python
232+
def result_link_titles(self):
233+
self.page.locator(f'{self.RESULT_LINKS} >> nth=4').wait_for()
234+
titles = self.page.locator(self.RESULT_LINKS).all_text_contents()
235+
return titles
236+
237+
def result_link_titles_contain_phrase(self, phrase, minimum=1):
238+
titles = self.result_link_titles()
239+
matches = [t for t in titles if phrase.lower() in t.lower()]
240+
return len(matches) >= minimum
241+
```
242+
243+
In the first method, the `RESULT_LINKS` selector is used twice.
244+
The first time it is used, it is concatenated with the N-th element selector using an
245+
[f-string](https://realpython.com/python-f-strings/).
246+
247+
The second method takes in a search phases and a minimum limit for matches.
248+
It calls the first method to get the list of titles,
249+
filters the titles using a list comprehension,
250+
and returns a Boolean value indicating if the number of matches meets the minimum threshold.
251+
Notice that this method does **not** perform an asssertion.
252+
Assertions should *not* be done in page objects.
253+
They should only be done in test cases.
254+
255+
The third assertion checked the page title.
256+
Let's add one final method to the result page class to get the title:
257+
258+
```python
259+
def title(self):
260+
return self.page.title()
261+
```
262+
263+
The full code for `pages/result.py` should look like this
264+
(after rearranging methods alphabetically):
265+
266+
```python
267+
class DuckDuckGoResultPage:
268+
269+
RESULT_LINKS = '.result__title a.result__a'
270+
SEARCH_INPUT = '#search_form_input'
271+
272+
def __init__(self, page):
273+
self.page = page
274+
275+
def result_link_titles(self):
276+
self.page.locator(f'{self.RESULT_LINKS} >> nth=4').wait_for()
277+
titles = self.page.locator(self.RESULT_LINKS).all_text_contents()
278+
return titles
279+
280+
def result_link_titles_contain_phrase(self, phrase, minimum=1):
281+
titles = self.result_link_titles()
282+
matches = [t for t in titles if phrase.lower() in t.lower()]
283+
return len(matches) >= minimum
284+
285+
def search_input_value(self):
286+
return self.page.input_value(self.SEARCH_INPUT)
287+
288+
def title(self):
289+
return self.page.title()
290+
```
291+
292+
After rewriting the original test case to use `DuckDuckGoResultPage`,
293+
the code in `tests/test_search.py` should look like this:
294+
295+
```python
296+
from pages.result import DuckDuckGoResultPage
297+
from pages.search import DuckDuckGoSearchPage
298+
299+
def test_basic_duckduckgo_search(page):
300+
search_page = DuckDuckGoSearchPage(page)
301+
result_page = DuckDuckGoResultPage(page)
302+
303+
# Given the DuckDuckGo home page is displayed
304+
search_page.load()
305+
306+
# When the user searches for a phrase
307+
search_page.search('panda')
308+
309+
# Then the search result query is the phrase
310+
assert 'panda' == result_page.search_input_value()
311+
312+
# And the search result links pertain to the phrase
313+
assert result_page.result_link_titles_contain_phrase('panda')
314+
315+
# And the search result title contains the phrase
316+
assert 'panda' in result_page.title()
317+
```
318+
319+
These calls look less "code-y" than the raw Playwright calls.
320+
They read much more like a test case.
321+
322+
Rerun the test again to make sure everything is still working.
323+
188324

189325
## Page object fixtures

0 commit comments

Comments
 (0)