If you write automated tests with Playwright, you will quickly reach a point where running them only on your local machine is not enough.
At the beginning, everything looks simple.
You run the tests locally, everything passes, the result is green, and it feels like the topic is covered. The problem usually appears later, when someone changes something in the application, creates a pull request, or deploys a new version, and suddenly an important process stops working.
That is why automated tests should not run only locally. They should also run as part of the CI/CD process.
In this article, I will show you how to run Playwright tests in GitHub Actions.
Why run Playwright tests in GitHub Actions?
The simplest answer is: so you do not have to check everything manually after every change.
GitHub Actions can automatically run tests after a push to the repository or when a pull request is created. This helps you detect much faster whether a change has broken login, registration, shopping cart, payment, contact form, or any other important flow in your application.
In practice, the process looks like this:
- A developer makes a change in the code.
- The change is pushed to GitHub.
- GitHub Actions starts the workflow.
- Dependencies are installed.
- Playwright tests are executed.
- At the end, you can see whether the tests passed or failed.
This is the moment where automation starts to make much more sense.
Tests are no longer something that a tester runs locally from time to time. They become part of the software delivery process.
Example Playwright project
I assume that you already have a Playwright project.
If not, you can create one with the following command:
npm init playwright@latestAfter installation, your project should contain files and folders such as:
tests/
playwright.config.ts
package.jsonYou can run your tests locally with:
npx playwright testIf everything works locally, we can move on to the GitHub Actions configuration.
Where to add the GitHub Actions configuration?
In your repository, create the following directory:
.github/workflowsInside it, create a file, for example:
playwright.ymlThe final path should look like this:
.github/workflows/playwright.ymlThis is the file where we define when and how our tests should run.
Basic Playwright workflow
For the beginning, we can use a simple configuration:
name: Playwright Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
name: Run Playwright tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7This is already a working foundation.
After adding this file to your repository, GitHub Actions will run your tests after a push to the main branch and for pull requests targeting main.
What do these steps do?
Let’s go through this file calmly, because YAML may look a bit strange at first.
name: Playwright TestsThis is the name of the workflow. It will be visible in the Actions tab on GitHub.
on:
push:
branches:
- main
pull_request:
branches:
- mainHere we define when the workflow should start.
In this case, tests will run after a push to main and for pull requests targeting main.
runs-on: ubuntu-latestThis means that the job will run on an Ubuntu machine. In most cases, this is enough for Playwright tests.
uses: actions/checkout@v4This step checks out the repository code into the GitHub Actions environment.
uses: actions/setup-node@v4Here we install Node.js. In this example, I use version 22.
run: npm ciThis command installs project dependencies based on the package-lock.json file.
In CI, it is usually better to use npm ci instead of npm install, because it is more predictable and fits automated environments better.
run: npx playwright install --with-depsThis step installs the browsers and system dependencies required by Playwright.
This is important because on a fresh GitHub Actions machine we cannot assume that everything is already installed.
run: npx playwright testThis is the step where the tests are executed.
if: always()This part in the report upload step means that the report should be saved even if the tests fail.
And this is very important.
When tests fail, that is exactly when we need reports, screenshots, or traces the most.
HTML report from Playwright tests
Playwright can generate an HTML report by default. After the tests are executed, this report can be saved as an artifact in GitHub Actions.
In our workflow, this part is responsible for that:
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7After the workflow finishes, you can open the specific run in GitHub Actions and download the report from the artifacts section.
This is very useful when analyzing failures.
Instead of guessing what happened, you can check which test failed, at which step the problem occurred, and what the browser actually saw.
Trace Viewer as help with difficult failures
One thing I really like about Playwright is the Trace Viewer.
If a test fails, the trace allows you to analyze the test execution step by step. You can see actions, assertions, screenshots, logs, requests, and responses.
In practice, this is much more comfortable than reading only the error message in the console.
In the playwright.config.ts file, it is worth having a configuration like this:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
retries: process.env.CI ? 2 : 0,
reporter: 'html',
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});In this example:
- trace is saved on the first retry,
- screenshot is saved only when a test fails,
- video is kept only when a test fails,
- in CI, tests are retried two times.
Of course, you do not always have to use retries. Well-written tests should be stable. But in real projects, retries sometimes help separate temporary environment problems from actual application defects.
What if the application must be started before the tests?
Very often, end-to-end tests require a running application.
Locally, your application may run at:
http://localhost:3000But in GitHub Actions, it has to be started first.
There are several ways to handle this. One convenient option is the webServer configuration in Playwright.
Example:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
webServer: {
command: 'npm run dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
use: {
baseURL: 'http://127.0.0.1:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});With this configuration, Playwright starts the application before the tests, waits until it is available at the given address, and only then runs the tests.
In your tests, you can then use relative paths:
import { test, expect } from '@playwright/test';
test('homepage should load', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Example/);
});This is much more convenient than writing the full address in every test.
Running tests only for pull requests
You do not always have to run tests after every push to every branch.
A common approach is to run tests for pull requests targeting main.
Example:
on:
pull_request:
branches:
- mainThis way, the change is checked before it gets merged into the main branch.
In team projects, this makes a lot of sense, because a pull request should usually not be merged if automated tests do not pass.
Running tests on multiple browsers
Playwright allows you to test an application in different browsers, such as Chromium, Firefox, and WebKit.
In the playwright.config.ts file, the configuration can look like this:
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],Should you always run tests in three browsers?
It depends.
If the project is small, Chromium may be enough at the beginning. If the application is used by many customers and must work correctly across different browsers, then broader browser coverage is worth considering.
I usually start with the most important scenarios in Chromium and add other browsers later, only where it really makes sense for the project.
Common problems in GitHub Actions
During the first Playwright configuration in GitHub Actions, similar problems often appear.
1. Browsers are not installed
If you see an error related to a missing browser, check whether your workflow contains this step:
- name: Install Playwright browsers
run: npx playwright install --with-depsWithout this step, Playwright may not have the environment needed to run the tests.
2. The application did not start before the tests
If tests try to open localhost, but the application is not running, they will obviously fail.
In this case, it is worth using webServer in the Playwright configuration or adding a separate workflow step that starts the application before the tests.
3. The test works locally but fails in CI
This is a classic problem.
The most common causes are:
- dependency on local data,
- timeouts that are too strict,
- weak selectors,
- missing waits for elements,
- differences between environments,
- tests depending on execution order.
In such cases, trace, screenshots, and the HTML report help a lot.
4. Tests take too long
When the number of tests grows, the workflow may eventually take too long.
At the beginning, this is not a major problem. Later, it is worth thinking about:
- splitting tests,
- running only the most important tests on each pull request,
- using a separate workflow for full regression,
- test sharding,
- running the full test set once a day or before a larger release.
Should Playwright tests block a merge?
In my opinion: yes, but with common sense.
If you have a few stable tests that check the most important flows in the application, they can definitely block a merge.
For example:
- login,
- registration,
- shopping cart,
- payment,
- form submission,
- basic purchase flow,
- the most important user journey.
I would not start by blocking merges with one hundred unstable tests that randomly pass and fail.
It is better to start with a small but stable test suite. Then you can expand it step by step.
Good practice: separate workflow for smoke tests
In many projects, splitting tests works very well.
For example:
- smoke tests run on every pull request,
- full regression runs manually or on a schedule,
- cross-browser tests run before a release.
This way, we do not block the team with a very long pipeline, but we still have automatic quality control.
Smoke tests can check only the most important features of the application:
npx playwright test --grep @smokeAnd in the tests, we can mark scenarios in the test name:
test('@smoke user can open homepage', async ({ page }) => {
await page.goto('/');
await expect(page).toBeVisible();
});This is a simple way to separate fast tests from a larger regression suite.
Summary
Running Playwright tests in GitHub Actions is one of those steps that really increases the value of test automation.
As long as tests are run only locally, they are easy to forget. Only when they become part of the pipeline do they truly support the team.
A well-configured workflow allows you to:
- run tests after a push or pull request,
- detect bugs faster,
- save HTML reports,
- analyze traces and screenshots,
- block a merge if important scenarios fail.
You do not need a perfect configuration at the beginning.
A simple workflow, a few stable tests, and a report saved as an artifact are enough to start. Later, you can gradually add more elements: retries, traces, smoke tests, sharding, tests in multiple browsers, or full regression.
The most important thing is this: automated tests should run where the team actually makes decisions about code quality.
And in many projects, that place is GitHub Actions.
