In this blog post, we will look into using bun to install a new Playwright framework.
Create the Playwright Framework
Once I installed bun on my Windows PC and was up and running I created a new folder, "bun-create-playwright".
I opened up that folder in VS Code, along with a new PowerShell terminal. In that terminal I entered:
- bun create playwright
This activated the interactive Playwright installer that Playwright comes with.
- I selected I wanted it to create for me a TypeScript project, placing the tests in the default folder, tests, but I decided not to add a GitHub Actions workflow. I wanted to experiment with using GitLab.
- I chose it to set up and install all the browsers for me... which it did... using NPX, part of Node.
Wait a second, when it comes to our Playwright framework, doesn't bun replace node?
No. Bun might be a JavaScript runtime, a package manager, and a bundler shipped as a single software tool, but in our case we are simply using it as a package manager in our Playwright framework.
Bun works alongside Node.js in the Playwright project. We still use Playwright's test runner, a Node.js program. Bun handles installations and run tasks.
Alert: Windows Bug!
Initially, that wasn't enough to install Playwright's internal browsers. There is a bug with bun + Playwright Windows where we also need to:
- bun add --dev @playwright/test
- bunx playwright install
That was enough for the browsers to install.
Create the Bun Lock File
After the browsers are installed, we need to install bun in our project, to create from the package.json a bun installation.
- Run in the Terminal: bun install
- Review the newly created file bun.lock.file in the root directory of the project.
- Delete the package-lock.json file.
Add the test script to the Package.json
If you look in the package.json file, you can see that the scripts code block is currently empty.
"scripts": {}
Inside this code block, we will be adding:
"scripts": {
"test": "playwright test"
},
Examine the Included Tests
Two tests will automatically be set up for you:
- has title: Navigates to playwright.dev and expects the title of the page to have the word "Playwright".
- get started: Navigates to playwright.dev and checks the "Get started" link.
tests/ 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();
});
- Arrange: Setup things like opening up a browser page.
- Act: Go to a web site and see if you can get a link and click on it.
- Assert: Expect that the conditions of the tests are met, failing the test if the expected results and actual results do not match up.
import pulls code that lives in another file or package so you can use it in your project. We import two specific named functions, test and expect, from the @playwright/test package (the Playwright Test framework's core library).
- test is a function used to define an individual test case.
- expect is a function used to create assertions (checks that something is true, and fail the test if it isn't). [ See Playwright.dev / Assertions ]
test blocks set up two Playwright tests. Strings such as "has title" sets up the name of the test.
async sets up this test function to be asynchronous, with many tests allowed to be executed at the same time as this one. The browser to run the first "has title" test might not be able to navigate to the page right away. The aync keyword allows the test to pause and wait for any slower actions to be completed before moving to the next part of the test. Once we set up async, we can use await later in the test. Meanwhile, it returns a JavaScript Promise, a placeholder container until we determine the success or failure of the action, when it actually will contain a value. This promise will be either pending, fulfilled, or rejected.
page sets up the pre-defined Playwright page fixture [ See Playwright.dev / Fixtures ]. With this call, before the individual test starts, a fresh isolated page is launched just for this test, so the "has title" test does not interfere with the "get started" test. This page fixture has automatic setup and teardown.
=> is a "hash rocket", representing the "arrow function", separating the earlier parameters of the test with the coming body of the test function.
await returns a Promise, a placeholder, so if we can't do something like go to a page right away, the test won't automatically go to the next line of code and possibly fail. Playwright by default waits 30 seconds for a goto to complete, 30 seconds for an action to happen, but only 5 seconds on an assertion like "expect". If it goes over that timeout, the test will fail.
goto is part of the Page fixture, that automatically handles page navigation.
expect is an assertion. By default, the test expects this condition to be met within five minutes, else the test fails.
toHaveTitle is a built in Playwright page assertion. It allows regular expressions such as /Playwright/ to let the test know that having the word "Playwright" in the title is acceptable. Playwright has many assertions, such as the PageAssertions toHaveTitle, toHaveURL, toHaveScrreenshot(name).
getByRole is a Playwright Locator, that helps you find a web element on the page. Are you looking for a certain alert, alertDialog, banner, button, checkbox, heading, img, link, listbox, menu, menubar, list, listbox, progressbar, searchbox, table, or textbox, tooltip, amongst others? You can try to getByRole. With this you are locating elements by their ARIA role, ARIA attributes and accessible name.
... Note that with locators, you can also getByTestId, getByAltText, getByText, getByTitle, etc.
.click is a Playwright action you can perform.
- Once you found an element by a locator, you can .click or .dblclick it, .click({ button: 'right' }) or .click({ modifiers: ['Shift']}), and .hover.
- You can .check a checkbox or .selectOption on a radio button, and expect it .toBeChecked.
- You can drag and drop an item.
- You can fill a textbox, input field, or text area.
.toByVisible is a Playwright Locator Assertion.
- Once you found an element by a Playwright locator, you can in the test expect it .toBeChecked, .toBeDisabled, .toBeEditable, .toBeEmpty, .toBeEnabled, .toBeFocused, .toBeHidden, .toBeInViewport, .toBeVisible, .toContainClass, .toContainText, /toHavaAccessibleDescription, .toHaveAttribute, .toHaveId, .toHaveRole, .toHaveText, .toHaveValue.
- Each of these methods can be negated chaining a "not" value. Expect a text box .not.toBeEmpty.
Execute the Playwright Tests: Bun Run Test
Now, we should be able to run the tests that come with the initial Playwright scaffolding:
- bun run test
The Playwright tests, are automatically run in Chromium, Firefox, and WebKit / Safari, and should pass.
You can see the results if you open up the bun-create-playwright folder in an IDE such as VS Code:
- Go to the playwright-report folder
- Right click on the index.html file that was produced.
- Select "Open with Live Server"
What Does Playwright Install?
The Playwright scaffolder installs:
node_modules folder:
Created by a package manager (npm, yarn, bun, pnpm) when you install dependencies. Contains the actual source code of every package your project depends on, plus the dependencies of those dependencies (transitive dependencies), recursively. It's populated by running an install command (npm install, bun install, etc.), and never edited by hand.
Node's module resolution algorithm looks inside node_modules when you import/require a package by name.
Contains .bin, @playwright, @types. bun-types, playwright, playwright-core, typescript, undici-types. According to Claude.ai:
.bin: Executable scripts for installed packages. This is where npm/bun puts symlinks (symbolic links) to CLI (Command Line Interface) tools so you can run them via npx playwright or through package.json scripts, instead of needing global installation of every executable.
@playwright: Scoped npm namespace. Typically contains @playwright/test, the Playwright Test runner package (test runner, assertions, fixtures, config handling — what you import when you write import { test, expect } from '@playwright/test').
@types: Folder that Bun creates to hold TypeScript type declaration packages published under the @types scope on npm. These packages come from DefinitelyTyped.org, a large community-maintained repository on GitHub where volunteers write and publish TypeScript type declarations for JavaScript libraries that don't include their own. When a package doesn't ship built-in types (common for older libraries or runtime built-ins like Node.js itself), DefinitelyTyped contributors write a separate package describing that library's functions, arguments, and return values, then publish it to npm as @types/<library-name>These give you IntelliSense and type-checking for JS APIs.
bun-types: Type declarations for Bun's runtime APIs (Bun.file, Bun.serve, etc.), letting TypeScript understand Bun-specific globals if you're running/building with Bun instead of Node.
playwright: The full Playwright package, including the browser automation driver and CLI. This is distinct from @playwright/test: playwright is the core library (browser launching, contexts, pages), while @playwright/test builds the test runner on top of it.
playwright-core: The stripped-down core of Playwright without the browser-download step. playwright depends on this internally; you'd typically only see it listed as a top-level dependency if something needs Playwright's automation APIs without auto-downloading browser binaries.
typescript: The TypeScript compiler (tsc) itself, used for type-checking and/or transpiling .ts files.
undici-types: type declarations for undici, Node's HTTP client, pulled in automatically by @types/node.
playwright-report folder:
A folder to store HTML Reports produced after a test run.
According to Playwright.dev / HTML Reporter, "HTML reporter produces a self-contained folder that contains report for the test run that can be served as a web page.
"[...] By default, HTML report is opened automatically if some of the tests failed. You can control this behavior via the open property in the Playwright config or the PLAYWRIGHT_HTML_OPEN environmental variable. The possible values for that property are always, never and on-failure (default)".
test-results folder:
After running the test, it contains .last-run.json which contains "status: passed" with an empty array of failedTests.
tests folder:
example.spec.ts contains the pre-installed Playwright tests we have already covered.
.gitignore:
Want something to not be checked in, committed, and merged to an outside source? Place it here.
Playwright automatically has it so node_modules, local test-results, local playwright-reports, and things in the local .cache and .auth do not get committed.
bun.lock
This lockfile is produced by "bun install". Why have a lockfile? According to Code Protection Hub,
"The entire philosophy of lock file programming revolves around a concept called Semantic Versioning (SemVer). In a manifest file (like package.json), developers specify acceptable version ranges using caret (^) or tilde (~) symbols. For example, ^2.4.1 allows the package manager to download minor updates and bug fixes (up to version 3.0.0).
"While this allows projects to automatically receive security patches, it introduces a severe risk: Dependency Drift.
"Imagine Developer A runs npm install on Monday and receives version 2.4.1. On Friday, the package author publishes version 2.5.0 containing a subtle bug. When Developer B joins the team and runs npm install, they receive the buggy 2.5.0 version. The application breaks for Developer B, but works perfectly for Developer A. This is the classic "it works on my machine" nightmare.
The lock file acts as a historical snapshot. It records the exact version, the registry URL, and the cryptographic hash (SHA-512) of the code downloaded on Monday. When Developer B installs the project, the package manager ignores the flexible package.json ranges and strictly enforces the exact tree defined in the lock file".
package.json:
From HeyNode / What is Package.json:
"If you’ve worked with Node.js before, you have likely encountered a package.json file. It is a JSON file that lives in the root directory of your project. Your package.json holds important information about the project. It contains human-readable metadata about the project (like the project name and description) as well as functional metadata like the package version number and a list of dependencies required by the application.
"Your project’s package.json is the central place to configure and describe how to interact with and run your application. It is used by the npm CLI (and yarn) to identify your project and understand how to handle the project’s dependencies. It’s the package.json file that enables npm to start your project, run scripts, install dependencies, publish to the NPM registry, and many other useful tasks. The npm CLI is also the best way to manage your package.json because it helps generate and update your package.json file throughout a project’s life.
"Your package.json fills several roles in the lifecycle of your project, some of which only apply for packages published to NPM. If you’re not publishing your project to the NPM registry or otherwise making it publicly available to others, your package.json is still essential to the development flow.
"The name field defines the name of the package. When publishing to the NPM registry, this is the name the package will be listed under. It must be no more than 214 characters, only lowercase letters, and it must be URL-safe (hyphens and underscores allowed, but no spaces or other characters disallowed in URLs).
"The version field is very important for any published package, and required before publishing. It is the current version of the software that the package.json is describing.
"You are not required to use SemVer, but it is the standard used in the Node.js ecosystem and highly recommended. For an unpublished package, this property isn’t strictly required. Typically, the version number is bumped according to SemVer before publishing new versions to NPM.
"The license field lets us define what license applies to the code the package.json is describing. Again, this is very important when publishing a project to the NPM registry, as the license may limit the use of your software by some developers or organizations. Having a clear license in place helps clearly define what terms the software is able to be used under".
{
"name": "bun-create-playwright",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "playwright test"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@playwright/test": "^1.62.1",
"@types/bun": "latest",
"@types/node": "^26.1.2"
},
"private": true,
"peerDependencies": {
"typescript": "^5"
}
}
playwright.config.ts:
This Playwright Configuration file is the central configuration file for a Playwright test project. It lives at the root of your project and tells Playwright how to run your tests: which browsers to use, where to find test files, how to handle failures, what reports to generate, etc. Instead of passing a dozen command-line flags every time you run tests, you define the settings once here.
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
Breakdown of the main components
- testDir: points Playwright to the folder containing your spec files.
- fullyParallel: controls whether tests within a single file run in parallel workers rather than sequentially.
- forbidOnly: a safety check, usually enabled only in CI, that fails the build if test.only was left in the code.
- retries: how many times a failed test is retried before being marked as failed. Commonly set higher in CI than locally.
- workers: how many tests run concurrently. CI environments often limit this to avoid resource contention.
- reporter: determines the output format for test results (HTML report, list in the terminal, JUnit XML for CI integration, etc). You can specify multiple reporters at once.
- use: shared defaults inherited by every test, unless overridden at the project or test level. Common settings here:
- baseURL for relative navigation
- trace for capturing a debuggable trace of the test run
- screenshot and video for failure artifacts
- headless to control whether the browser UI is shown
- projects: defines the different browser/device combinations to run the same test suite against. Each project can also override anything in use. This is how you get cross-browser coverage without duplicating test code.
-T.J. Maher
Software Engineer in Test
BlueSky | YouTube | LinkedIn | Articles
No comments:
Post a Comment