@@ -47,45 +47,40 @@ We will use much of our original code.
47
47
48
48
A page object class typically has three main parts:
49
49
50
- 1 . Selectors and any other data stored as variables
51
- 2 . Dependency injection of the browser automator through a constructor
50
+ 1 . Dependency injection of the browser automator through a constructor
51
+ 2 . Locators and any other data stored as variables
52
52
3 . Interaction methods that use the browser automator and the selectors
53
53
54
54
Let's add these one at a time.
55
- Inside ` pages/search.py ` , add a class definition for the page object :
55
+ Inside ` pages/search.py ` , import Playwright's ` Page ` class :
56
56
57
57
``` python
58
- class DuckDuckGoSearchPage :
58
+ from playwright.sync_api import Page
59
59
```
60
60
61
- Inside this class, add the selectors we used in our test for the search input and search button :
61
+ Add a class definition for the page object :
62
62
63
63
``` python
64
- SEARCH_BUTTON = ' #search_button_homepage'
65
- SEARCH_INPUT = ' #search_form_input_homepage'
64
+ class DuckDuckGoSearchPage :
66
65
```
67
66
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:
67
+ Inside this class, add the DuckDuckGo URL:
73
68
74
69
``` python
75
70
URL = ' https://www.duckduckgo.com'
76
71
```
77
72
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 tutorial.) *
73
+ > * Warning:*
74
+ > Base URLs should typically be passed into automation code as an input, not hard-coded in a page object.
75
+ > We are doing this here as a matter of simplicity for this tutorial.
81
76
82
77
Next, let's handle dependency injection for the browser automator.
83
78
Since each test will have its own Playwright page, we should inject that page.
84
79
(If we were using Selenium WebDriver, then we would inject the WebDriver instance.)
85
80
Add the following initializer method to the class:
86
81
87
82
``` python
88
- def __init__ (self , page ) :
83
+ def __init__ (self , page : Page) -> None :
89
84
self .page = page
90
85
```
91
86
@@ -94,12 +89,22 @@ The `__init__` method is essentially a constructor for Python classes
94
89
It has one argument named ` page ` for the Playwright page,
95
90
which it stores as an instance variable (via ` self ` ).
96
91
97
- With the page injected, we can now use it to make interactions.
92
+ Let's also add locators for search page elements to the constructor.
93
+ Our test needs locators for the search button and the search input:
94
+
95
+ ``` python
96
+ self .search_button = page.locator(' #search_button_homepage' )
97
+ self .search_input = page.locator(' #search_form_input_homepage' )
98
+ ```
99
+
100
+ These locators are created once and can be used anywhere.
101
+ We can use them to make interactions.
102
+
98
103
One interaction our test performs is loading the DuckDuckGo search page.
99
104
Here's a method to do that:
100
105
101
106
``` python
102
- def load (self ):
107
+ def load (self ) -> None :
103
108
self .page.goto(self .URL )
104
109
```
105
110
@@ -109,61 +114,58 @@ The other interaction our test performs is searching for a phrase.
109
114
Here's a method to do that:
110
115
111
116
``` python
112
- def search (self , phrase ) :
113
- self .page .fill(self . SEARCH_INPUT , phrase)
114
- self .page .click(self . SEARCH_BUTTON )
117
+ def search (self , phrase : str ) -> None :
118
+ self .search_input .fill(phrase)
119
+ self .search_button .click()
115
120
```
116
121
117
- This ` search ` method uses the injected page and the selector variables .
122
+ This ` search ` method uses the page objects to perform the search .
118
123
It also takes in the search phrase as an argument so that it can handle any phrase.
119
124
120
125
The completed search page object class should look like this:
121
126
122
127
``` python
123
- class DuckDuckGoSearchPage :
128
+ from playwright.sync_api import Page
124
129
125
- SEARCH_BUTTON = ' #search_button_homepage'
126
- SEARCH_INPUT = ' #search_form_input_homepage'
130
+ class DuckDuckGoSearchPage :
127
131
128
132
URL = ' https://www.duckduckgo.com'
129
133
130
- def __init__ (self , page ) :
134
+ def __init__ (self , page : Page) -> None :
131
135
self .page = page
136
+ self .search_button = page.locator(' #search_button_homepage' )
137
+ self .search_input = page.locator(' #search_form_input_homepage' )
132
138
133
- def load (self ):
139
+ def load (self ) -> None :
134
140
self .page.goto(self .URL )
135
141
136
- def search (self , phrase ) :
137
- self .page .fill(self . SEARCH_INPUT , phrase)
138
- self .page .click(self . SEARCH_BUTTON )
142
+ def search (self , phrase : str ) -> None :
143
+ self .search_input .fill(phrase)
144
+ self .search_button .click()
139
145
```
140
146
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
147
We can now refactor the original test case to use this new page object!
146
148
Replace this old code:
147
149
148
150
``` python
149
- def test_basic_duckduckgo_search (page ) :
150
-
151
+ def test_basic_duckduckgo_search (page : Page) -> None :
152
+
151
153
# Given the DuckDuckGo home page is displayed
152
154
page.goto(' https://www.duckduckgo.com' )
153
155
154
156
# When the user searches for a phrase
155
- page.fill (' #search_form_input_homepage' , ' panda' )
156
- page.click (' #search_button_homepage' )
157
+ page.locator (' #search_form_input_homepage' ).fill( ' panda' )
158
+ page.locator (' #search_button_homepage' ).click( )
157
159
```
158
160
159
161
With this new code:
160
162
161
163
``` python
162
164
from pages.search import DuckDuckGoSearchPage
163
165
164
- def test_basic_duckduckgo_search (page ) :
166
+ def test_basic_duckduckgo_search (page : Page) -> None :
165
167
search_page = DuckDuckGoSearchPage(page)
166
-
168
+
167
169
# Given the DuckDuckGo home page is displayed
168
170
search_page.load()
169
171
@@ -190,37 +192,34 @@ It will follow the same structure.
190
192
The main difference is that each interaction methods in the result page class will return a value
191
193
because test assertions will check page values.
192
194
193
- Start by adding the class definition to ` pages/result.py ` :
195
+ Start by adding the following imports for type checking to ` pages/result.py ` :
194
196
195
197
``` python
196
- class DuckDuckGoResultPage :
198
+ from playwright.sync_api import Page
199
+ from typing import List
197
200
```
198
201
199
- Add the selectors we used in the test case :
202
+ Add the class definition :
200
203
201
204
``` python
202
- RESULT_LINKS = ' .result__title a.result__a'
203
- SEARCH_INPUT = ' #search_form_input'
205
+ class DuckDuckGoResultPage :
204
206
```
205
207
206
- Add dependency injection:
208
+ Add dependency injection with locators :
207
209
208
210
``` python
209
- def __init__ (self , page ) :
211
+ def __init__ (self , page : Page) -> None :
210
212
self .page = page
213
+ self .result_links = page.locator(' .result__title a.result__a' )
214
+ self .search_input = page.locator(' #search_form_input' )
211
215
```
212
216
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.
217
+ Now, let's add interaction methods.
218
+ Since the verifications for the search input and title are simple,
219
+ we don't need new methods for those.
220
+ The test case function can call the ` search_input ` locator and the ` page ` object directly for those.
221
+ However, the verification for search result links has some complex code
222
+ that should be handled within the page object.
224
223
We can break this down into two methods:
225
224
226
225
1 . A method to get all result link titles as a list.
@@ -229,20 +228,21 @@ We can break this down into two methods:
229
228
Add the following methods to the class:
230
229
231
230
``` 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
231
+ def result_link_titles (self ) -> List[str ]:
232
+ self .result_links.nth(4 ).wait_for()
233
+ return self .result_links.all_text_contents()
236
234
237
- def result_link_titles_contain_phrase (self , phrase , minimum = 1 ) :
235
+ def result_link_titles_contain_phrase (self , phrase : str , minimum : int = 1 ) -> bool :
238
236
titles = self .result_link_titles()
239
237
matches = [t for t in titles if phrase.lower() in t.lower()]
240
238
return len (matches) >= minimum
241
239
```
242
240
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/ ) .
241
+ In the first method, the ` result_links ` locator is used twice.
242
+ The first time it is called,
243
+ it is concatenated with the N-th element fetcher to wait for at least 5 elements to appear.
244
+ The second time it is called,
245
+ it gets all the text contents for the elements it finds.
246
246
247
247
The second method takes in a search phases and a minimum limit for matches.
248
248
It calls the first method to get the list of titles,
@@ -252,41 +252,28 @@ Notice that this method does **not** perform an asssertion.
252
252
Assertions should * not* be done in page objects.
253
253
They should only be done in test cases.
254
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
255
The full code for ` pages/result.py ` should look like this
264
256
(after rearranging methods alphabetically):
265
257
266
258
``` python
259
+ from playwright.sync_api import Page
260
+ from typing import List
261
+
267
262
class DuckDuckGoResultPage :
268
263
269
- RESULT_LINKS = ' .result__title a.result__a'
270
- SEARCH_INPUT = ' #search_form_input'
271
-
272
- def __init__ (self , page ):
264
+ def __init__ (self , page : Page) -> None :
273
265
self .page = page
266
+ self .result_links = page.locator(' .result__title a.result__a' )
267
+ self .search_input = page.locator(' #search_form_input' )
274
268
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
269
+ def result_link_titles (self ) -> List[str ]:
270
+ self .result_links.nth(4 ).wait_for()
271
+ return self .result_links.all_text_contents()
279
272
280
- def result_link_titles_contain_phrase (self , phrase , minimum = 1 ) :
273
+ def result_link_titles_contain_phrase (self , phrase : str , minimum : int = 1 ) -> bool :
281
274
titles = self .result_link_titles()
282
275
matches = [t for t in titles if phrase.lower() in t.lower()]
283
276
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
277
```
291
278
292
279
After rewriting the original test case to use ` DuckDuckGoResultPage ` ,
@@ -295,8 +282,9 @@ the code in `tests/test_search.py` should look like this:
295
282
``` python
296
283
from pages.result import DuckDuckGoResultPage
297
284
from pages.search import DuckDuckGoSearchPage
285
+ from playwright.sync_api import expect, Page
298
286
299
- def test_basic_duckduckgo_search (page ) :
287
+ def test_basic_duckduckgo_search (page : Page) -> None :
300
288
search_page = DuckDuckGoSearchPage(page)
301
289
result_page = DuckDuckGoResultPage(page)
302
290
@@ -307,13 +295,13 @@ def test_basic_duckduckgo_search(page):
307
295
search_page.search(' panda' )
308
296
309
297
# Then the search result query is the phrase
310
- assert ' panda ' == result_page.search_input_value( )
298
+ expect( result_page.search_input).to_have_value( ' panda ' )
311
299
312
300
# And the search result links pertain to the phrase
313
301
assert result_page.result_link_titles_contain_phrase(' panda' )
314
302
315
303
# And the search result title contains the phrase
316
- assert ' panda' in result_page.title( )
304
+ expect(page).to_have_title( ' panda at DuckDuckGo ' )
317
305
```
318
306
319
307
These calls look less "code-y" than the raw Playwright calls.
@@ -352,13 +340,14 @@ import pytest
352
340
353
341
from pages.result import DuckDuckGoResultPage
354
342
from pages.search import DuckDuckGoSearchPage
343
+ from playwright.sync_api import Page
355
344
356
345
@pytest.fixture
357
- def result_page (page ) :
346
+ def result_page (page : Page) -> DuckDuckGoResultPage :
358
347
return DuckDuckGoResultPage(page)
359
348
360
349
@pytest.fixture
361
- def search_page (page ) :
350
+ def search_page (page : Page) -> DuckDuckGoSearchPage :
362
351
return DuckDuckGoSearchPage(page)
363
352
```
364
353
@@ -374,33 +363,39 @@ You can learn more about fixtures from the
374
363
To use these new fixtures, rewrite ` tests/test_search.py ` like this:
375
364
376
365
``` python
377
- def test_basic_duckduckgo_search (search_page , result_page ):
366
+ from pages.result import DuckDuckGoResultPage
367
+ from pages.search import DuckDuckGoSearchPage
368
+ from playwright.sync_api import expect, Page
378
369
370
+ def test_basic_duckduckgo_search (
371
+ page : Page,
372
+ search_page : DuckDuckGoSearchPage,
373
+ result_page : DuckDuckGoResultPage) -> None :
374
+
379
375
# Given the DuckDuckGo home page is displayed
380
376
search_page.load()
381
377
382
378
# When the user searches for a phrase
383
379
search_page.search(' panda' )
384
380
385
381
# Then the search result query is the phrase
386
- assert ' panda ' == result_page.search_input_value( )
382
+ expect( result_page.search_input).to_have_value( ' panda ' )
387
383
388
384
# And the search result links pertain to the phrase
389
385
assert result_page.result_link_titles_contain_phrase(' panda' )
390
386
391
387
# And the search result title contains the phrase
392
- assert ' panda' in result_page.title( )
388
+ expect(page).to_have_title( ' panda at DuckDuckGo ' )
393
389
```
394
390
395
391
Notice a few things:
396
392
397
- * This test module no longer needs to import the page object classes from the ` pages ` module.
398
393
* The ` search_page ` and ` result_page ` fixtures are declared as arguments for the test function.
399
394
* The test function no longer explicitly constructs page objects.
400
- * The Playwright ` page ` fixture is no longer declared because it is no longer called directly by the test function .
395
+ * Each test step is only one line long .
401
396
402
397
If you use page objects, then ** all interactions should be performed using page objects** .
403
- It is not recommended to mix raw Playwright calls with page object calls.
398
+ It is not recommended to mix raw Playwright calls (except ` expect ` assertions) with page object calls.
404
399
That becomes confusing, and it encourages poor practices like dirty hacks and copypasta.
405
400
It also causes a test automation project to lose strength from a lack of conformity in design.
406
401
0 commit comments