|
1 | 1 | # Install & configure |
2 | 2 |
|
3 | | -## Summary |
| 3 | +## Installation |
4 | 4 |
|
5 | | -1. Install Playwright. |
6 | | -1. Create `playwright.config.ts`. |
7 | | -1. Specify `projects` using Chromium as the browser. |
8 | | -1. Specify quality-of-life options like `fullyParallel` or `forbidOnly`. |
9 | | -1. A brief mention of `workers` set to `1` on CI. |
| 5 | +### Choosing the right package |
10 | 6 |
|
11 | | ---- |
| 7 | +There are actually _two_ different packages shipped by Playwright: |
12 | 8 |
|
13 | | -1. Create a simple test at `./tests/epicweb.test.ts`. |
14 | | -1. Test block structure (`test()`). |
15 | | -1. The `page` fixture. |
16 | | -1. Use `page.goto()` to visit pages (external and internal). |
17 | | -1. Promise-based `expect()` assertions (retriability). |
| 9 | +- `playwright`, which is a library to help you automate browsers; |
| 10 | +- `@playwright/test`, which is a testing framework build on top of the aforementioned library. |
18 | 11 |
|
19 | | ---- |
| 12 | +Today, you would need the latter so let's get it installed: |
20 | 13 |
|
21 | | -1. Add the `test:e2e` script in `package.json` that runs Playwright. |
22 | | -1. Run the test via `npm run test:e2e`. See the result. |
| 14 | +```sh |
| 15 | +npm i @playwright/test --save-dev |
| 16 | +``` |
| 17 | + |
| 18 | +> I'm installing this package as a development dependency (`--save-dev`), but this is not required. The distinction between save/dev dependencies is irrelevant in the context of enterprise applications. |
| 19 | +
|
| 20 | +### Installing browser binaries |
| 21 | + |
| 22 | +You can think of Playwright as a wrapped around the browser. And as such, it doesn't actually ship any browsers with it. You have to install the exact browser binaries you need to test your application. |
| 23 | + |
| 24 | +Luckily, Playwright comes with a CLI to help you do that (**don't run it just yet**): |
| 25 | + |
| 26 | +```sh |
| 27 | +npx playwright install |
| 28 | +``` |
| 29 | + |
| 30 | +Without any arguments, this command will install _all binaries for all supported browsers_. This includes Chromium, Firefox, WebKit... That's quite a lot! Especially if you aren't using all of those browsers to test your app. |
| 31 | + |
| 32 | +In this workshop, you will be using Chromium exclusively so it makes sense to install just that binary: |
| 33 | + |
| 34 | +```sh |
| 35 | +npx playwright install chromium --with-deps |
| 36 | +``` |
| 37 | + |
| 38 | +> Notice the `--with-deps` flag here, which makes sure that all the system dependencies required by this browser binary are also installed. This is less relevant for local use but is extremely handy when running Playwright on CI. |
| 39 | +
|
| 40 | +## Configuration |
| 41 | + |
| 42 | +Okay, so you've got Playwright installed. Now it's time to configure it. |
| 43 | + |
| 44 | +Create a file called `playwright.config.ts` at the root of your project and fill it with a minimal configuration options: |
| 45 | + |
| 46 | +```ts filename=playwright.config.ts |
| 47 | +import { defineConfig, devices } from '@playwright/test' |
| 48 | + |
| 49 | +export default defineConfig({ |
| 50 | + projects: [ |
| 51 | + { |
| 52 | + name: 'chromium', |
| 53 | + use: { |
| 54 | + ...devices['Desktop Chrome'], |
| 55 | + }, |
| 56 | + }, |
| 57 | + ], |
| 58 | + fullyParallel: true, |
| 59 | + forbidOnly: !!process.env.CI, |
| 60 | + workers: process.env.CI ? 1 : undefined, |
| 61 | +}) |
| 62 | +``` |
| 63 | + |
| 64 | +Let's go through every option and see what it does one-by-one. |
| 65 | + |
| 66 | +### Projects |
| 67 | + |
| 68 | +Similar to Vitest, you can have multiple test projects in Playwright. This allows you to decouple the test suites from the orchestration that runs it. In practice, this means you can: |
| 69 | + |
| 70 | +- Have the same tests run in multiple browsers; |
| 71 | +- Have unique test suites that run only in some browsers; |
| 72 | +- Have browsers configured a certain way to make test cases possible (e.g. specific viewport sizes, enabling `prefers-reduced-motion`, using different locales or timezones). |
| 73 | + |
| 74 | +```ts filename=playwright.config.ts highlight=4-11 |
| 75 | +// ... |
| 76 | + |
| 77 | +export default defineConfig({ |
| 78 | + projects: [ |
| 79 | + { |
| 80 | + name: 'chromium', |
| 81 | + use: { |
| 82 | + ...devices['Desktop Chrome'], |
| 83 | + }, |
| 84 | + }, |
| 85 | + ], |
| 86 | + // ... |
| 87 | +}) |
| 88 | +``` |
| 89 | + |
| 90 | +In our case, we have a single project that uses `Desktop Chrome` as the browser. |
| 91 | + |
| 92 | +### Full parallelization |
| 93 | + |
| 94 | +Similar to Vitest, Playwright has the same behaviors when it comes to concurrency and parallelization: |
| 95 | + |
| 96 | +- All test _files_ run in parallel in isolated workers; |
| 97 | +- All test _cases_ in those files run sequentially. |
| 98 | + |
| 99 | +This is a good default, but we are going to opt out from it and make all our test cases run concurrently by setting `fullyParallel` option to `true`: |
| 100 | + |
| 101 | +```ts filename=playwright.config.ts highlight=5 |
| 102 | +// ... |
| 103 | + |
| 104 | +export default defineConfig({ |
| 105 | + // ... |
| 106 | + fullyParallel: true, |
| 107 | + // ... |
| 108 | +}) |
| 109 | +``` |
| 110 | + |
| 111 | +### Forbidding `test.only` |
| 112 | + |
| 113 | +Isolating invididual test cases while writing or debugging tests can be really handy. Playwright supports that via `test.only()`. The problem is, it's extremely easy to forget about a test case you've isolated and get your CI running just that single test 😬 |
| 114 | + |
| 115 | +There's an option to guard you from that mistake by making Playwright throw an error if it detects `test.only`. Naturally, let's set that option to `true` only for remote, CI runs: |
| 116 | + |
| 117 | +```ts filename=playwright.config.ts highlight=5 |
| 118 | +// ... |
| 119 | + |
| 120 | +export default defineConfig({ |
| 121 | + // ... |
| 122 | + forbidOnly: !!process.env.CI, |
| 123 | + // ... |
| 124 | +}) |
| 125 | +``` |
| 126 | + |
| 127 | +> The `process.env.CI` environment variable is commonly set by CI providers, like GitHub Actions. Make sure to fine-tune this condition if your provider marks the environment differently. |
| 128 | +
|
| 129 | +### Worker count |
| 130 | + |
| 131 | +The last thing we are going to configure is the `workers` option that controls the number of parallel workers spawned for each test run. By default, Playwright will use _half_ of your available CPU cores for that, but that can cause trouble in CI runs. |
| 132 | + |
| 133 | +It's generally recommended to use a _single_ worker for your end-to-end tests on CI. |
| 134 | + |
| 135 | +```ts filename=playwright.config.ts highlight=5 |
| 136 | +// ... |
| 137 | + |
| 138 | +export default defineConfig({ |
| 139 | + // ... |
| 140 | + workers: process.env.CI ? 1 : undefined, |
| 141 | + // ... |
| 142 | +}) |
| 143 | +``` |
| 144 | + |
| 145 | +This means _slower_ but more _reliable_ test results on machines that are normally nowhere near as powerful as those you use for development. |
| 146 | + |
| 147 | +> Note that you should configure Playwright to suit the specifics of your CI context. Today, we are going through the safe defaults that make sense in the majority of projects. The beauty is, **you can opt out from them!** And I strongly encourage you to experiment with your environment and find the optimal options for your end-to-end tests. |
| 148 | +
|
| 149 | +## Writing a test |
| 150 | + |
| 151 | +End-to-end tests follow the same test anatomy you already know. |
| 152 | + |
| 153 | +```ts |
| 154 | +import { test, expect } from '@playwright/test' |
| 155 | + |
| 156 | +test('test title', testFunction) |
| 157 | +``` |
| 158 | + |
| 159 | +> Note that Playwright does _not_ expose its functions, like `test` or `expect` globally, and those have to be imported. |
| 160 | +
|
| 161 | +The test case we have at hand today is the one that makes sure a correct heading is displayed on the Epic Web website. So let's give this test a descriptive name and navigate to `https://epicweb.dev/` for starters. |
| 162 | + |
| 163 | +```ts add=2 |
| 164 | +test('displays the page heading', async ({ page }) => { |
| 165 | + await page.goto('https://epicweb.dev/') |
| 166 | +}) |
| 167 | +``` |
| 168 | + |
| 169 | +In Playwright, you use the `page` object from the test context argument to interact with the underlying browser page. For example, calling `page.goto()` will trigger a navigation to the given URL! |
| 170 | + |
| 171 | +<callout-warning> |
| 172 | + |
| 173 | +The test context **MUST** be destructured in Playwright. The framework analyzes which context properties each test uses to implement lazy fixtures. |
| 174 | + |
| 175 | +```ts remove=1-3 add=5-7 |
| 176 | +test('displays the page heading', async (context) => { |
| 177 | + await context.page.goto(url) |
| 178 | +}) |
| 179 | + |
| 180 | +test('displays the page heading', async ({ page }) => { |
| 181 | + await page.goto(url) |
| 182 | +}) |
| 183 | +``` |
| 184 | + |
| 185 | +</callout-warning> |
| 186 | + |
| 187 | +### Locating elements |
| 188 | + |
| 189 | +Next, let's find the `<h1>` element on the page that we want to assert on. You can use many of the `page.getBy*` utilities to locate elements by their accessible role, associated label text, text content, etc. |
| 190 | + |
| 191 | +Let's use `page.getByRole()` to locate the heading we need by its role (`heading`) and its accessible name, which in case of the heading elements equals to its text content. |
| 192 | + |
| 193 | +```ts add=6-8 |
| 194 | +import { test, expect } from '@playwright/test' |
| 195 | + |
| 196 | +test('displays the page heading', async ({ page }) => { |
| 197 | + await page.goto('https://epicweb.dev/') |
| 198 | + |
| 199 | + page.getByRole('heading', { |
| 200 | + name: 'Full Stack Workshop Training for Professional Web Developers', |
| 201 | + }) |
| 202 | +}) |
| 203 | +``` |
| 204 | + |
| 205 | +When you find elements on the page, you don't get a direct reference to their DOM nodes straight away. Instead, Playwright gives you a _locator_. Think of it as a promise to that element that will be resolved only when the element is needed. For example, when you assert on it. |
| 206 | + |
| 207 | +### Assertions |
| 208 | + |
| 209 | +Finally, let's express some expectations, shall we? |
| 210 | + |
| 211 | +We've already located the heading element we need, and all that remains is to make sure that it is visible on the page. To do that, use the `expect()` function, provide it the locator of the element you want to assert on, and use one of the many available matchers, like `.toBeVisible()`, for example. |
| 212 | + |
| 213 | +```ts add=6-10 |
| 214 | +import { test, expect } from '@playwright/test' |
| 215 | + |
| 216 | +test('displays the page heading', async ({ page }) => { |
| 217 | + await page.goto('https://epicweb.dev/') |
| 218 | + |
| 219 | + await expect( |
| 220 | + page.getByRole('heading', { |
| 221 | + name: 'Full Stack Workshop Training for Professional Web Developers', |
| 222 | + }), |
| 223 | + ).toBeVisible() |
| 224 | +}) |
| 225 | +``` |
| 226 | + |
| 227 | +> Note that matchers in Playwright _return a promise_ that you have to await. That is because every assertion is retryable by default to deal with asynchronicity and race conditions—two common problems that result in flakiness if left unchecked. |
| 228 | +
|
| 229 | +## Running tests |
| 230 | + |
| 231 | +Now that the test is ready, let's run it, using the npm script we've prepared earlier: |
| 232 | + |
| 233 | +``` |
| 234 | +npm run test:e2e |
| 235 | +``` |
| 236 | + |
| 237 | +``` |
| 238 | +Running 1 test using 1 worker |
| 239 | +
|
| 240 | + ✓ 1 [chromium] › tests/epicweb.test.ts:3:5 › displays the page heading (935ms) |
| 241 | +
|
| 242 | + 1 passed (2.8s) |
| 243 | +``` |
| 244 | + |
| 245 | +Sweet! :tada: |
0 commit comments