Skip to content

Commit 87b75c2

Browse files
Merge branch '5-playwright-tricks' into 6-api-testing
2 parents aa6a732 + 62371c4 commit 87b75c2

File tree

10 files changed

+228
-186
lines changed

10 files changed

+228
-186
lines changed

pages/result.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,22 @@
33
the page object for the DuckDuckGo result page.
44
"""
55

6+
from playwright.sync_api import Page
7+
from typing import List
8+
9+
610
class DuckDuckGoResultPage:
711

8-
RESULT_LINKS = '.result__title a.result__a'
9-
SEARCH_INPUT = '#search_form_input'
10-
11-
def __init__(self, page):
12+
def __init__(self, page: Page) -> None:
1213
self.page = page
14+
self.result_links = page.locator('.result__title a.result__a')
15+
self.search_input = page.locator('#search_form_input')
1316

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
17+
def result_link_titles(self) -> List[str]:
18+
self.result_links.nth(4).wait_for()
19+
return self.result_links.all_text_contents()
1820

19-
def result_link_titles_contain_phrase(self, phrase, minimum=1):
21+
def result_link_titles_contain_phrase(self, phrase: str, minimum: int = 1) -> bool:
2022
titles = self.result_link_titles()
2123
matches = [t for t in titles if phrase.lower() in t.lower()]
2224
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()

pages/search.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@
33
the page object for the DuckDuckGo search page.
44
"""
55

6-
class DuckDuckGoSearchPage:
6+
from playwright.sync_api import Page
7+
78

8-
SEARCH_BUTTON = '#search_button_homepage'
9-
SEARCH_INPUT = '#search_form_input_homepage'
9+
class DuckDuckGoSearchPage:
1010

1111
URL = 'https://www.duckduckgo.com'
1212

13-
def __init__(self, page):
13+
def __init__(self, page: Page) -> None:
1414
self.page = page
15+
self.search_button = page.locator('#search_button_homepage')
16+
self.search_input = page.locator('#search_form_input_homepage')
1517

16-
def load(self):
18+
def load(self) -> None:
1719
self.page.goto(self.URL)
1820

19-
def search(self, phrase):
20-
self.page.fill(self.SEARCH_INPUT, phrase)
21-
self.page.click(self.SEARCH_BUTTON)
21+
def search(self, phrase: str) -> None:
22+
self.search_input.fill(phrase)
23+
self.search_button.click()

tests/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@
1616

1717
from pages.result import DuckDuckGoResultPage
1818
from pages.search import DuckDuckGoSearchPage
19+
from playwright.sync_api import Page
20+
1921

2022

2123
@pytest.fixture
22-
def result_page(page):
24+
def result_page(page: Page) -> DuckDuckGoResultPage:
2325
return DuckDuckGoResultPage(page)
2426

2527

2628
@pytest.fixture
27-
def search_page(page):
29+
def search_page(page: Page) -> DuckDuckGoSearchPage:
2830
return DuckDuckGoSearchPage(page)
2931

3032

tests/test_search.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
import pytest
66

7+
from pages.result import DuckDuckGoResultPage
8+
from pages.search import DuckDuckGoSearchPage
9+
from playwright.sync_api import expect, Page
10+
711

812
ANIMALS = [
913
'panda',
@@ -20,19 +24,23 @@
2024

2125

2226
@pytest.mark.parametrize('phrase', ANIMALS)
23-
def test_basic_duckduckgo_search(search_page, result_page, phrase):
24-
27+
def test_basic_duckduckgo_search(
28+
phrase: str,
29+
page: Page,
30+
search_page: DuckDuckGoSearchPage,
31+
result_page: DuckDuckGoResultPage) -> None:
32+
2533
# Given the DuckDuckGo home page is displayed
2634
search_page.load()
2735

2836
# When the user searches for a phrase
2937
search_page.search(phrase)
3038

3139
# Then the search result query is the phrase
32-
assert phrase == result_page.search_input_value()
40+
expect(result_page.search_input).to_have_value(phrase)
3341

3442
# And the search result links pertain to the phrase
3543
assert result_page.result_link_titles_contain_phrase(phrase)
3644

3745
# And the search result title contains the phrase
38-
assert phrase in result_page.title()
46+
expect(page).to_have_title(f'{phrase} at DuckDuckGo')

tutorial/1-getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ $ touch tests/test_search.py
162162
Add the following code to `tests/test_search.py`:
163163

164164
```python
165-
def test_basic_duckduckgo_search():
165+
def test_basic_duckduckgo_search() -> None:
166166
# Given the DuckDuckGo home page is displayed
167167
# When the user searches for a phrase
168168
# Then the search result query is the phrase

tutorial/2-first-steps.md

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,22 @@ You do not need to explicitly close the browser.
5858
> Asynchronous calls could be useful for other types of automation, such as web scraping.
5959
6060
Let's update our test stub to call the `page` fixture.
61-
In `tests/test_search.py`, change the test function signature from this:
61+
In `tests/test_search.py`, add the following import statement:
6262

6363
```python
64-
def test_basic_duckduckgo_search():
64+
from playwright.sync_api import Page
65+
```
66+
67+
Then, change the test function signature from this:
68+
69+
```python
70+
def test_basic_duckduckgo_search() -> None:
6571
```
6672

6773
To this:
6874

6975
```python
70-
def test_basic_duckduckgo_search(page):
76+
def test_basic_duckduckgo_search(page: Page) -> None:
7177
```
7278

7379
Now the test has access to a fresh page in a new browser context.
@@ -172,22 +178,32 @@ Here's the inspection panel for the search input element:
172178
Thankfully, this element has an ID.
173179
We can use the selector `#search_form_input_homepage` to uniquely identify this element.
174180

175-
To enter text into this input element, we must use Playwright's
176-
[`fill`](https://playwright.dev/python/docs/api/class-page#page-fill) method.
181+
To interact with elements with Playwright, we must use [locators](https://playwright.dev/python/docs/locators).
182+
The [Locator](https://playwright.dev/python/docs/next/api/class-locator) class
183+
takes in a selector and produces an object that can interact with the target element.
184+
185+
For example, to enter text into this input element, we must use `Locator`'s
186+
[`fill`](https://playwright.dev/python/docs/next/api/class-locator#locator-fill) method.
177187
Append the following line to the test case:
178188

179189
```python
180-
page.fill('#search_form_input_homepage', 'panda')
190+
page.locator('#search_form_input_homepage').fill('panda')
181191
```
182192

183193
> Since `search_form_input_homepage` is an ID, we could also use Playwright's
184194
> [ID attribute selector](https://playwright.dev/python/docs/selectors#id-data-testid-data-test-id-data-test-selectors):
185195
>
186-
> `page.fill('id=search_form_input_homepage', 'panda')`
196+
> `page.locator('id=search_form_input_homepage').fill('panda')`
187197
188-
Using Selenium WebDriver, we would need to locate the element and then send the interaction to it.
189-
However, in Playwright, these two parts are combined into a single call.
190-
Furthermore, Playwright waits for the target element to be visible and editable before it attempts to enter the text.
198+
> Playwright's `Page` class also provides methods for element interactions like this:
199+
>
200+
> `page.fill('#search_form_input_homepage', 'panda')`
201+
>
202+
> However, using locators is recommended over direct page calls.
203+
> Locators use "strict" mode - a locator raises an exception if its selector finds more than one element.
204+
> Locators are also more reusable, especially when using page object classes.
205+
206+
Playwright waits for the target element to be visible and editable before it attempts to enter the text.
191207
We are arbitrarily using the phrase `'panda'` as our search phrase because, well, why not?
192208

193209
Let's handle the second part of the interaction: clicking the search button.
@@ -198,11 +214,11 @@ Here's the inspection panel for the search button:
198214
This element also has an ID: `#search_button_homepage`. Nice!
199215

200216
To click an element, we must use Playwright's
201-
[`click`](https://playwright.dev/python/docs/api/class-page#page-click) method.
217+
[`click`](https://playwright.dev/python/docs/next/api/class-locator#locator-click) method.
202218
Append the following line to the test case:
203219

204220
```python
205-
page.click('#search_button_homepage')
221+
page.locator('#search_button_homepage').click()
206222
```
207223

208224
Again, Playwright is nice and concise.
@@ -211,14 +227,15 @@ The `click` method waits for the target element to be ready to receive clicks, t
211227
Our test case should now look like this:
212228

213229
```python
214-
def test_basic_duckduckgo_search(page):
230+
from playwright.sync_api import Page
215231

232+
def test_basic_duckduckgo_search(page: Page) -> None:
216233
# Given the DuckDuckGo home page is displayed
217234
page.goto('https://www.duckduckgo.com')
218235

219236
# When the user searches for a phrase
220-
page.fill('#search_form_input_homepage', 'panda')
221-
page.click('#search_button_homepage')
237+
page.locator('#search_form_input_homepage').fill('panda')
238+
page.locator('#search_button_homepage').click()
222239

223240
# Then the search result query is the phrase
224241
# And the search result links pertain to the phrase
@@ -231,5 +248,7 @@ Now, you should see the test actually perform the search!
231248

232249
Navigation, input filling, and clicking are only three of many page interactions you can do with Playwright.
233250
Anything a user can do on a web page, Playwright can do as well.
234-
Check out the Playwright [Page](https://playwright.dev/python/docs/api/class-page) API to see *all* methods and attributes.
251+
Check out the Playwright [Page](https://playwright.dev/python/docs/api/class-page)
252+
and [Locator](https://playwright.dev/python/docs/next/api/class-locator) classes
253+
to see *all* methods and attributes.
235254
We will use more of these calls in the next tutorial parts.

0 commit comments

Comments
 (0)