|
| 1 | +# Part 2: First steps with Playwright |
| 2 | + |
| 3 | +Part 2 of the workshop shows how to take your first steps with Playwright calls. |
| 4 | +It will explain browsers, contexts, and pages. |
| 5 | +It will also cover basic Playwright API calls. |
| 6 | + |
| 7 | + |
| 8 | +## Browsers, contexts, and pages |
| 9 | + |
| 10 | +Before we can automate interactions using Playwright's API, we must first understand how Playwright interacts with browsers. |
| 11 | +There are three main layers to automation: *browsers*, *browser contexts*, and *pages*: |
| 12 | + |
| 13 | +1. A [browser](https://playwright.dev/python/docs/browsers/) |
| 14 | + is a single instance of a web browser. |
| 15 | + Playwright will automatically launch a browser instance specified by code or by inputs. |
| 16 | + Typically, this is either the Chromium, Firefox, or WebKit instance installed via `playwright install`, |
| 17 | + but it may also be other browsers installed on your local machine. |
| 18 | +2. A [browser context](https://playwright.dev/python/docs/browser-contexts/) |
| 19 | + is an isolated incognito-alike session within a browser instance. |
| 20 | + They are fast and cheap to create. |
| 21 | + One browser may have multiple browser contexts. |
| 22 | + The recommended practice is for all tests to share one browser instance but for each test to have its own browser context. |
| 23 | +3. A [page](https://playwright.dev/python/docs/pages/) |
| 24 | + is a single tab or window within a browser context. |
| 25 | + A browser context may have multiple pages. |
| 26 | + Typically, an individual test should interact with only one page. |
| 27 | + |
| 28 | +Below is a diagram illustrating how these three pieces work together: |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | +Typically, we would need the following Playwright calls to set up a browser, browser context, and page: |
| 33 | + |
| 34 | +```python |
| 35 | +from playwright.sync_api import sync_playwright |
| 36 | + |
| 37 | +with sync_playwright() as p: |
| 38 | + browser = p.chromium.launch() |
| 39 | + context = browser.new_context() |
| 40 | + page = context.new_page() |
| 41 | +``` |
| 42 | + |
| 43 | +However, the `pytest-playwright` plugin takes care of these things automatically with the following fixtures: |
| 44 | + |
| 45 | +* The `browser` fixture provides the browser instance launched by Playwright. |
| 46 | +* The `context` fixture provides a new browser context for a test. |
| 47 | +* The `page` fixture provides a new browser page for a test. |
| 48 | + |
| 49 | +All of the Playwright calls with `pytest-playwright` use the synchronous API instead of the async API. |
| 50 | +The `browser` fixture has session scope, meaning all tests will share one browser instance. |
| 51 | +The `context` and `page` fixtures have function scope, meaning each test gets new ones. |
| 52 | +Typically, a test will only need to call the `page` fixture directly. |
| 53 | +These fixtures will also automatically clean up everything after testing is complete. |
| 54 | +You do not need to explicitly close the browser. |
| 55 | + |
| 56 | +Let's update our test stub to call the `page` fixture. |
| 57 | +In `tests/test_search.py`, change the test function signature from this: |
| 58 | + |
| 59 | +```python |
| 60 | +def test_basic_duckduckgo_search(): |
| 61 | +``` |
| 62 | + |
| 63 | +To this: |
| 64 | + |
| 65 | +```python |
| 66 | +def test_basic_duckduckgo_search(page): |
| 67 | +``` |
| 68 | + |
| 69 | + |
| 70 | +## Navigating to a web page |
| 71 | + |
| 72 | +Now the test has access to a fresh page in a new browser context. |
| 73 | +If we write multiple tests, each test will get its own page and context, |
| 74 | +but they will all share the same browser instance. |
| 75 | + |
| 76 | +Now that we have a page, let's do something on it! |
| 77 | +Our first test step is, "Given the DuckDuckGo home page is displayed". |
| 78 | +Let's [navigate](https://playwright.dev/python/docs/navigations) to the DuckDuckGo home page like this: |
| 79 | + |
| 80 | +```python |
| 81 | +def test_basic_duckduckgo_search(page): |
| 82 | + # Given the DuckDuckGo home page is displayed |
| 83 | + page.goto('https://www.duckduckgo.com') |
| 84 | +``` |
| 85 | + |
| 86 | +If you are familiar with Selenium WebDriver, then this command probably looks similar to the `driver.get(...)` method. |
| 87 | +However, Playwright's [`goto`](https://playwright.dev/python/docs/api/class-page#page-goto) method is more sophisticated: |
| 88 | +it waits for the page to fire the `load` event. |
| 89 | +Selenium WebDriver does not automatically wait for any event, |
| 90 | +which frequently leads to race conditions that cause flaky tests. |
| 91 | + |
| 92 | +In Playwright, you can also wait for other page events like this: |
| 93 | + |
| 94 | +```python |
| 95 | +def test_basic_duckduckgo_search(page): |
| 96 | + # Given the DuckDuckGo home page is displayed |
| 97 | + page.goto('https://www.duckduckgo.com', wait_until='networkidle') |
| 98 | +``` |
| 99 | + |
| 100 | +For our test, however, the default `load` event will suffice. |
| 101 | + |
| 102 | +Let's try running our test to make sure Playwright works. |
| 103 | +Launch pytest using the following command: |
| 104 | + |
| 105 | +```bash |
| 106 | +$ python3 -m pytest tests --headed --slowmo 1000 |
| 107 | +``` |
| 108 | + |
| 109 | +This invocation has two new arguments. |
| 110 | +The first one is `--headed`. |
| 111 | +By default, Playwright runs tests in *headless* mode, in which the browser is not visibly rendered. |
| 112 | +Headless mode is faster than headed mode and thus ideal for "real" testing (like in CI). |
| 113 | +However, *headed* mode is better when developing tests so that you can see what is happening. |
| 114 | + |
| 115 | +The second new argument is `--slowmo`. |
| 116 | +By default, Playwright runs interactions as fast as it can. |
| 117 | +Again, this is great for "real" testing, but it might be too fast for humans to watch when developing and debugging. |
| 118 | +The `--slowmo` option lets the caller set a hard sleep time after every Playwright call. |
| 119 | +For example, `--slowmo 1000` will pause execution for 1 second (1000 ms) after each call. |
| 120 | +This option is a much better way to slow down tests than to add `time.sleep(...)` calls everywhere! |
| 121 | + |
| 122 | +When you launch pytest, Chromium should pop up, navigate to the DuckDuckGo home page, and close. |
| 123 | +Try running it with and without the `--headed` and `--slowmo` options, too. |
| 124 | +Verify that Playwright calls work and the test passes before moving on. |
| 125 | + |
| 126 | + |
| 127 | +## Performing a search |
| 128 | + |
| 129 | +Next, let's try to interact with page elements. |
| 130 | +Playwright is able to locate any element on the page using [selectors](https://playwright.dev/python/docs/selectors). |
| 131 | +Out of the box, Playwright supports the following types of selectors: |
| 132 | + |
| 133 | +* Text |
| 134 | +* CSS |
| 135 | +* XPath |
| 136 | +* N-th element |
| 137 | +* React |
| 138 | +* Vue |
| 139 | + |
| 140 | +Text and CSS selectors also pierce the Shadow DOM by default! |
| 141 | + |
| 142 | +In general, you should keep selectors as simple as possible. |
| 143 | +Try to stick to text, IDs, or CSS selectors. |
| 144 | +Use more complicated selectors only as necessary. |
| 145 | + |
| 146 | +This workshop will not cover recommended practices for element selectors deeply. |
| 147 | +If you want to learn more about selectors, |
| 148 | +read the [Element selectors](https://playwright.dev/python/docs/selectors) page in the Playwright docs, |
| 149 | +or take the [Web Element Locator Strategies](https://testautomationu.applitools.com/web-element-locator-strategies/) course |
| 150 | +from [Test Automation University](https://testautomationu.applitools.com/). |
| 151 | + |
| 152 | +The next step in our test case is, "When the user searches for a phrase". |
| 153 | +This is actually a compound interaction with two parts: |
| 154 | + |
| 155 | +1. Entering text into the search input field |
| 156 | +2. Clicking the search button |
| 157 | + |
| 158 | +Let's start with the first part of the interaction: entering text into the search input field. |
| 159 | +We need to find a selector for the search input. |
| 160 | +One of the best ways to find selectors is to inspect elements through Chrome DevTools. |
| 161 | +In Chrome, simply right-click any page and select "Inspect" to open DevTools. |
| 162 | + |
| 163 | +Here's the inspection panel for the search input element: |
| 164 | + |
| 165 | + |
| 166 | + |
| 167 | +Thankfully, this element has an ID. |
| 168 | +We can use the selector `#search_form_input_homepage` to uniquely identify this element. |
| 169 | + |
| 170 | +To enter text into this input element, we must use Playwright's |
| 171 | +[`fill`](https://playwright.dev/python/docs/api/class-page#page-fill) method. |
| 172 | +Append the following line to the test case: |
| 173 | + |
| 174 | +```python |
| 175 | + page.fill('#search_form_input_homepage', 'panda') |
| 176 | +``` |
| 177 | + |
| 178 | +Using Selenium WebDriver, we would need to locate the element and then send the interaction to it. |
| 179 | +However, in Playwright, these two parts are combined into a single call. |
| 180 | +We are arbitrarily using the phrase `'panda'` as our search phrase because, well, why not? |
| 181 | + |
| 182 | +Let's handle the second part of the interaction: clicking the search button. |
| 183 | +Here's the inspection panel for the search button: |
| 184 | + |
| 185 | + |
| 186 | + |
| 187 | +This element also has an ID: `#search_button_homepage`. Nice! |
| 188 | + |
| 189 | +To click an element, we must use Playwright's |
| 190 | +[`click`](https://playwright.dev/python/docs/api/class-page#page-click) method. |
| 191 | +Append the following line to the test case: |
| 192 | + |
| 193 | +```python |
| 194 | + page.click('#search_button_homepage') |
| 195 | +``` |
| 196 | + |
| 197 | +Again, Playwright is nice and concise. |
| 198 | + |
| 199 | +Our test case should now look like this: |
| 200 | + |
| 201 | +```python |
| 202 | +def test_basic_duckduckgo_search(page): |
| 203 | + |
| 204 | + # Given the DuckDuckGo home page is displayed |
| 205 | + page.goto('https://www.duckduckgo.com') |
| 206 | + |
| 207 | + # When the user searches for a phrase |
| 208 | + page.fill('#search_form_input_homepage', 'panda') |
| 209 | + page.click('#search_button_homepage') |
| 210 | + |
| 211 | + # Then the search result query is the phrase |
| 212 | + # And the search result links pertain to the phrase |
| 213 | + # And the search result title contains the phrase |
| 214 | + pass |
| 215 | +``` |
| 216 | + |
| 217 | +Rerun the test using `python3 -m pytest tests --headed --slowmo 1000`. |
| 218 | +Now, you should see the test actually perform the search! |
| 219 | + |
| 220 | +Navigation, input filling, and clicking are only three of many page interactions you can do with Playwright. |
| 221 | +Anything a user can do on a web page, Playwright can do as well. |
| 222 | +Check out the Playwright [Page](https://playwright.dev/python/docs/api/class-page) API to see *all* methods and attributes. |
| 223 | +We will use more of these calls in the next workshop parts. |
0 commit comments