Skip to content

Commit edefb0e

Browse files
Merge branch '1-getting-started' into 2-raw-playwright
2 parents 391c960 + b48c6da commit edefb0e

File tree

4 files changed

+220
-9
lines changed

4 files changed

+220
-9
lines changed

README.md

Lines changed: 220 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ This workshop has five main parts:
5454
3. Test project setup
5555
2. First steps with Playwright
5656
1. Raw Playwright calls
57-
3. Refactoring using page objects
58-
1. The search page
59-
4. Writing assertions
57+
3. Writing assertions
6058
1. Checking the search field
6159
2. Checking the result links
6260
3. Checking the title
61+
4. Refactoring using page objects
62+
1. The search page
6363
5. Nifty Playwright tricks
6464
1. Testing different browsers
6565
2. Capturing screenshots and videos
@@ -76,9 +76,9 @@ The branch names are:
7676
| ------ | ------------------- |
7777
| Start | 0-initial-project |
7878
| Part 1 | 1-getting-started |
79-
| Part 2 | 2-raw-playwright |
80-
| Part 3 | 3-page-objects |
81-
| Part 4 | 4-assertions |
79+
| Part 2 | 2-first-steps |
80+
| Part 3 | 3-assertions |
81+
| Part 4 | 4-page-objects |
8282
| Part 5 | 5-playwright-tricks |
8383

8484

@@ -184,7 +184,10 @@ They should look something like this:
184184
```bash
185185
$ pip3 freeze
186186
attrs==21.2.0
187+
certifi==2021.10.8
188+
charset-normalizer==2.0.8
187189
greenlet==1.1.2
190+
idna==3.3
188191
iniconfig==1.1.1
189192
packaging==21.3
190193
playwright==1.17.0
@@ -193,7 +196,13 @@ py==1.11.0
193196
pyee==8.2.2
194197
pyparsing==3.0.6
195198
pytest==6.2.5
199+
pytest-base-url==1.4.2
200+
pytest-playwright==0.2.2
201+
python-slugify==5.0.2
202+
requests==2.26.0
203+
text-unidecode==1.3
196204
toml==0.10.2
205+
urllib3==1.26.7
197206
websockets==10.1
198207
```
199208

@@ -259,15 +268,217 @@ Therefore, I always recommend running the full `python3 -m pytest tests` command
259268

260269
### Part 2: First steps with Playwright
261270

262-
TBD
271+
Before we can automate interactions using Playwright's API, we must first understand how Playwright interacts with browsers.
272+
There are three main layers to automation: *browsers*, *browser contexts*, and *pages*:
273+
274+
1. A [browser](https://playwright.dev/python/docs/browsers/)
275+
is a single instance of a web browser.
276+
Playwright will automatically launch a browser instance specified by code or by inputs.
277+
Typically, this is either the Chromium, Firefox, or WebKit instance installed via `playwright install`,
278+
but it may also be other browsers installed on your local machine.
279+
2. A [browser context](https://playwright.dev/python/docs/browser-contexts/)
280+
is an isolated incognito-alike session within a browser instance.
281+
They are fast and cheap to create.
282+
One browser may have multiple browser contexts.
283+
The recommended practice is for all tests to share one browser instance but for each test to have its own browser context.
284+
3. A [page](https://playwright.dev/python/docs/pages/)
285+
is a single tab or window within a browser context.
286+
A browser context may have multiple pages.
287+
Typically, an individual test should interact with only one page.
288+
289+
Below is a diagram illustrating how these three pieces work together:
290+
291+
![Browser Diagram](images/browser-diagram.png)
292+
293+
Typically, we would need the following Playwright calls to set up a browser, browser context, and page:
294+
295+
```python
296+
from playwright.sync_api import sync_playwright
297+
298+
with sync_playwright() as p:
299+
browser = p.chromium.launch()
300+
context = browser.new_context()
301+
page = context.new_page()
302+
```
303+
304+
However, the `pytest-playwright` plugin takes care of these things automatically with the following fixtures:
305+
306+
* The `browser` fixture provides the browser instance launched by Playwright.
307+
* The `context` fixture provides a new browser context for a test.
308+
* The `page` fixture provides a new browser page for a test.
309+
310+
All of the Playwright calls with `pytest-playwright` use the synchronous API instead of the async API.
311+
The `browser` fixture has session scope, meaning all tests will share one browser instance.
312+
The `context` and `page` fixtures have function scope, meaning each test gets new ones.
313+
Typically, a test will only need to call the `page` fixture directly.
314+
These fixtures will also automatically clean up everything after testing is complete.
315+
You do not need to explicitly close the browser.
316+
317+
Let's update our test stub to call the `page` fixture.
318+
In `tests/test_search.py`, change the test function signature from this:
319+
320+
```python
321+
def test_basic_duckduckgo_search():
322+
```
323+
324+
To this:
325+
326+
```python
327+
def test_basic_duckduckgo_search(page):
328+
```
329+
330+
Now the test has access to a fresh page in a new browser context.
331+
If we write multiple tests, each test will get its own page and context,
332+
but they will all share the same browser instance.
333+
334+
Now that we have a page, let's do something on it!
335+
Our first test step is, "Given the DuckDuckGo home page is displayed".
336+
Let's [navigate](https://playwright.dev/python/docs/navigations) to the DuckDuckGo home page like this:
337+
338+
```python
339+
def test_basic_duckduckgo_search(page):
340+
# Given the DuckDuckGo home page is displayed
341+
page.goto('https://www.duckduckgo.com')
342+
```
343+
344+
If you are familiar with Selenium WebDriver, then this command probably looks similar to the `driver.get(...)` method.
345+
However, Playwright's [`goto`](https://playwright.dev/python/docs/api/class-page#page-goto) method is more sophisticated:
346+
it waits for the page to fire the `load` event.
347+
Selenium WebDriver does not automatically wait for any event,
348+
which frequently leads to race conditions that cause flaky tests.
349+
350+
In Playwright, you can also wait for other page events like this:
351+
352+
```python
353+
def test_basic_duckduckgo_search(page):
354+
# Given the DuckDuckGo home page is displayed
355+
page.goto('https://www.duckduckgo.com', wait_until='networkidle')
356+
```
357+
358+
For our test, however, the default `load` event will suffice.
359+
360+
Let's try running our test to make sure Playwright works.
361+
Launch pytest using the following command:
362+
363+
```bash
364+
$ python3 -m pytest tests --headed --slowmo 1000
365+
```
366+
367+
This invocation has two new arguments.
368+
The first one is `--headed`.
369+
By default, Playwright runs tests in *headless* mode, in which the browser is not visibly rendered.
370+
Headless mode is faster than headed mode and thus ideal for "real" testing (like in CI).
371+
However, *headed* mode is better when developing tests so that you can see what is happening.
372+
373+
The second new argument is `--slowmo`.
374+
By default, Playwright runs interactions as fast as it can.
375+
Again, this is great for "real" testing, but it might be too fast for humans to watch when developing and debugging.
376+
The `--slowmo` option lets the caller set a hard sleep time after every Playwright call.
377+
For example, `--slowmo 1000` will pause execution for 1 second (1000 ms) after each call.
378+
This option is a much better way to slow down tests than to add `time.sleep(...)` calls everywhere!
379+
380+
When you launch pytest, Chromium should pop up, navigate to the DuckDuckGo home page, and close.
381+
Try running it with and without the `--headed` and `--slowmo` options, too.
382+
Verify that Playwright calls work and the test passes before moving on.
383+
384+
Next, let's try to interact with page elements.
385+
Playwright is able to locate any element on the page using [selectors](https://playwright.dev/python/docs/selectors).
386+
Out of the box, Playwright supports the following types of selectors:
387+
388+
* Text
389+
* CSS
390+
* XPath
391+
* N-th element
392+
* React
393+
* Vue
394+
395+
Text and CSS selectors also pierce the Shadow DOM by default!
396+
397+
In general, you should keep selectors as simple as possible.
398+
Try to stick to text, IDs, or CSS selectors.
399+
Use more complicated selectors only as necessary.
400+
401+
This workshop will not cover recommended practices for element selectors deeply.
402+
If you want to learn more about selectors,
403+
read the [Element selectors](https://playwright.dev/python/docs/selectors) page in the Playwright docs,
404+
or take the [Web Element Locator Strategies](https://testautomationu.applitools.com/web-element-locator-strategies/) course
405+
from [Test Automation University](https://testautomationu.applitools.com/).
406+
407+
The next step in our test case is, "When the user searches for a phrase".
408+
This is actually a compound interaction with two parts:
409+
410+
1. Entering text into the search input field
411+
2. Clicking the search button
412+
413+
Let's start with the first part of the interaction: entering text into the search input field.
414+
We need to find a selector for the search input.
415+
One of the best ways to find selectors is to inspect elements through Chrome DevTools.
416+
In Chrome, simply right-click any page and select "Inspect" to open DevTools.
417+
418+
Here's the inspection panel for the search input element:
419+
420+
![Inspecting the search input element](images/inspect-search-input.png)
421+
422+
Thankfully, this element has an ID.
423+
We can use the selector `#search_form_input_homepage` to uniquely identify this element.
424+
425+
To enter text into this input element, we must use Playwright's
426+
[`fill`](https://playwright.dev/python/docs/api/class-page#page-fill) method.
427+
Append the following line to the test case:
428+
429+
```python
430+
page.fill('#search_form_input_homepage', 'panda')
431+
```
432+
433+
Using Selenium WebDriver, we would need to locate the element and then send the interaction to it.
434+
However, in Playwright, these two parts are combined into a single call.
435+
We are arbitrarily using the phrase `'panda'` as our search phrase because, well, why not?
436+
437+
Let's handle the second part of the interaction: clicking the search button.
438+
Here's the inspection panel for the search button:
439+
440+
![Inspecting the search button element](images/inspect-search-button.png)
441+
442+
This element also has an ID: `#search_button_homepage`. Nice!
443+
444+
To click an element, we must use Playwright's
445+
[`click`](https://playwright.dev/python/docs/api/class-page#page-click) method.
446+
Append the following line to the test case:
447+
448+
```python
449+
page.click('#search_button_homepage')
450+
```
451+
452+
Again, Playwright is nice and concise.
453+
454+
Our test case should now look like this:
455+
456+
```python
457+
def test_basic_duckduckgo_search(page):
458+
459+
# Given the DuckDuckGo home page is displayed
460+
page.goto('https://www.duckduckgo.com')
461+
462+
# When the user searches for a phrase
463+
page.fill('#search_form_input_homepage', 'panda')
464+
page.click('#search_button_homepage')
465+
466+
# Then the search result query is the phrase
467+
# And the search result links pertain to the phrase
468+
# And the search result title contains the phrase
469+
pass
470+
```
471+
472+
Rerun the test using `python3 -m pytest tests --headed --slowmo 1000`.
473+
Now, you should see the test actually perform the search!
263474

264475

265-
### Part 3: Refactoring using page objects
476+
### Part 3: Writing assertions
266477

267478
TBD
268479

269480

270-
### Part 4: Writing assertions
481+
### Part 4: Refactoring using page objects
271482

272483
TBD
273484

images/browser-diagram.png

429 KB
Loading

images/inspect-search-button.png

1.12 MB
Loading

images/inspect-search-input.png

1.08 MB
Loading

0 commit comments

Comments
 (0)