Tutorial: Step by Step Implement Continuous Testing with Playwright: Part 3: Run your first Playwright Test
Writing and running your first own Playwright test.
This article series target absolute beginners who want to learn and use Playwright E2E (Web) Test Automation.
Part 1: Set up Playwright
Part 3: Run your first Playwright Test
Part 4: Tool & Project Structure (upcoming)
Part 5: Gain Productivity (upcoming)
Part 6: Design with Maintenance in mind (upcoming)
Part 7: More Tests * (upcoming)
Part 8: Set up Continuous Execution
Part 9: Run a small suite of Playwright tests in BuildWise, sequentially
Part 10: Parallel Playwright test execution in BuildWise
Part 11: FAQ
Previously, we have set up a Playwright TypeScript project and ran the example test. Now it’s time to create a test of your own and run it.
Let’s start by learning from Playwright’s provided test. The one we ran was ./tests/example.spec.ts
.
Explanation of the file name:
.spec
means specification — it is a test script to test a business behaviour.ts
means it is a TypeScript file. You might see Playwright files ending in.js
, those are Playwright JavaScript test files.
The contents of example.spec.ts
:
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
My book “Web Test Automation in Action: Volume 1” includes a set of Playwright tests for a sample test site: Agile Travel. In this article series, I will do a few tests against a real webapp: WhenWise (whenwise.agileway.net).
WhenWise was originally from a high school IT project I did: BookPhysio.com, and later AgileWay commercialised it. My father has developed and maintained 500+ Selenium tests for it).
First Test: User Login
The first automated test case for web apps is almost always user login, so let’s start there.
Test data:
Site URL: https://whenwise.agileway.net
(a sandbox site)
User Login:
driving@biz.com
; Password:test01
(a list of test users are shown on the sandbox site)
Step 1: Rename the test script file and add the first step
I recommend using a more meaningful test script file name, i.e, rename example.spec.ts
to login_ok.spec.ts
.
Then replace the file with the following content:
import { test, expect } from '@playwright/test';
test('User can sign in', async ({ page }) => {
await page.goto('https://whenwise.agileway.net');
});
To perform the above, no Playwright knowledge is required. Really, just delete some lines and do a simple string replacement.
Run it. Make sure you are in the project folder (it’s a bit inconvenient but it won’t run outside of it).
npx playwright test
You’ll see a Chromium browser launch and navigate to the WhenWise sandbox site, but it will close very quickly (maybe even before the page content loads).
To keep the browser open for three seconds so you can actually see something, add the following line to the end of the test:
await new Promise(resolve => setTimeout(resolve, 3 * 1000));
Run the test again, you should be able to see WhenWise home page content load.
The hard-wait syntax is a bit complex, however, that’s is natural for JavaScript developers. Don’t worry too much about it for now, we can optimize it later.
Step 2. Add more user login steps and finish the login test case complete
Next, let’s finish this login test.
I don’t really use record-n-playback utility. I prefer to manually inspect the element (or control) in the browser, and come up with the best locator, then type the test step in the testing IDE.
With a good testing IDE, such as TestWise, manually entering test steps can be quite efficient (e.g. autocomplete/shortcuts). The real importance here is the recorded test steps are usually not good quality, it might work but is unmaintainable.
Step 3. Run your test
Here is the initial draft test script I created.
test('User can sign in OK', async ({ page }) => {
await page.goto('https://whenwise.agileway.net');
await page.locator('text=SIGN IN').click();
await page.fill("#username", "driving@biz.com");
await page.fill("#password", "test01");
await page.click("#login-btn");
await new Promise(resolve => setTimeout(resolve, 3 * 1000));
});
The test failed after a 30-second time-out.
This script is intentionally set up to fail — I used the wrong ID for the username field. The ID should be email
, not username
.
Why am I showing you this failing test? It’s common for beginners to make typos, which can lead to execution failures like the one above. While we know the cause of the failure, it highlights an important point: Playwright’s default 30-second timeout isn’t ideal in this case. We prefer tests to fail fast rather than wait unnecessarily. This long wait comes from Playwright’s “auto-wait” feature — which isn’t as fancy as it sounds and can actually be confusing for newcomers. In comparison, Selenium WebDriver’s approach is more straightforward and beginner-friendly.
Anyway, this article series is not about Playwright specifics, for that, you can check out my book or other online resources.
import { test, Page, expect } from '@playwright/test';
test.describe.configure({ mode: 'serial' });
test('User can sign in OK', async ({ page }) => {
await page.goto('https://whenwise.agileway.net');
await page.locator('text=SIGN IN').click();
await page.fill("#email", "driving@biz.com");
await page.fill("#password", "test01");
await page.click("#login-btn");
const pageText = await page.textContent("body");
expect(pageText).toContain("You have signed in successfully");
await new Promise(resolve => setTimeout(resolve, 1 * 1000));
});
The above test script (with one test case) runs fine.
Step 4. Add a user login failed test case in a separate test script
Usually, we need at least verify user login failed test scenario.
Create another file: login_failed.spec.ts
with the content below:
import { test, Page, expect } from '@playwright/test';
test.describe.configure({ mode: 'serial' });
test('User can sign in failed: password invalid entered', async ({ page }) => {
await page.goto('https://whenwise.agileway.net');
await page.locator('text=SIGN IN').click();
await page.fill("#email", "driving@biz.com");
await page.fill("#password", "wrongpass");
await page.click("#login-btn");
const pageText = await page.textContent("body");
expect(pageText).toContain("Password/email is invalid");
});
Run the test project. In the Playwright report, you shall see two test results.
Step 5. Refactoring: Merge two user login test cases into one test script file.
In the above, we defined two test cases in two separate files. In this case, it is actually better to merge into one, because they are related; they’re both login tests. Furthermore, we might be able to reuse some test steps in beforeAll
, beforeEach
, afterEach
, and afterAll
blocks.
Create a new login.spec.ts
including the two previous test cases. It is not a simple concat, rather, put them into a test syntax structure. Because Playwright/Test blurs the automation and syntax framework, not a good practice. We have to look up Playwright documentation, instead of using a well-known one such as the test syntax framework Mocha.
import { test, Page, expect } from '@playwright/test';
test.describe.configure({ mode: 'serial' });
//Reuse the page among the test cases in the test script file
let page: Page;
test.beforeAll(async ({ browser }) => {
// Create page once.
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
test.beforeEach(async () => {
await page.goto('https://whenwise.agileway.net');
await page.locator('text=SIGN IN').click();
});
test('User can sign in OK', async () => {
await page.fill("#email", "driving@biz.com");
await page.fill("#password", "test01");
await page.click("#login-btn");
const pageText = await page.textContent("body");
expect(pageText).toContain("You have signed in successfully");
await page.goto('https://whenwise.agileway.net/sign-out');
});
test('User can sign in failed: wrong password', async () => {
await page.fill("#email", "driving@biz.com");
await page.fill("#password", "wrongpass");
await page.click("#login-btn");
const pageText = await page.textContent("body");
expect(pageText).toContain("Password/email is invalid");
});
Please note that the difference in the two test case declaration lines, async()
vs async({page})
(in the above). It is necessary if we want to reuse the page
object. Anyway, this is one of several JS annoy syntax I prefer not explaining to beginners. My advice is to just follow one specific syntax you are happy with, and stick with it. As for End-to-end testing, as long as it works, it is fine (at least for now).
Also, you might notice that at the end of the “User can sign in OK” test, we sign out (await page.goto(‘https://whenwise.agileway.net/sign-out');
), which we did not need when the tests were in two separate test files. I’ll leave it to you to figure out why!
Related reading: