A hands-on browser automation tutorial

Web automation testing: a practical guide with Bruster

Web automation testing uses software to open a browser, interact with a website, and check the result. In this guide, you will turn a recorded demo workflow into repeatable tests for forms, UI components, website navigation, and mobile layouts using Bruster’s .bru DSL.

By Bruster contributors · Examples from the executable website and demo suites.

Want a slower, step-by-step introduction? Start with the twenty-page beginner course, including setup, plain-English explanations, and exercises.

What is web automation testing?

Browser automation performs actions; a test also checks an expected outcome. A script that fills a form and clicks Submit automates a task. A test that then verifies the confirmation text can detect a broken submission. That final check is called an assertion.

A useful first test has three parts: establish a known starting page, perform a short user workflow, and assert its visible result. Start with important journeys such as signing in, searching, or submitting a form. Add smaller checks for individual controls when you need to identify which part broke.

  • Smoke tests check that a few important paths still work after a change.
  • UI component checks exercise a field, checkbox, tab, or menu and observe its state. Here, those components run inside a real page.
  • End-to-end tests exercise a complete journey. This demo’s journey ends at a browser-rendered receipt; it has no backend. A production checkout test would also need to verify the order was actually saved.

Bruster is an open-source browser automation tool written in Rust. Its domain-specific language, or DSL, gives these common steps short commands such as navigate, fill, click, and expect text. You write them in a .bru file and run bruster test.

1. Set up Bruster and the demo website

We will reuse the form in examples/launch-demo/index.html, the fixture used by Bruster’s demo-recording script. It contains name and project fields, a workflow dropdown, a screenshot checkbox, a live JSON preview, and a submission receipt.

Open the interactive browser automation demo → Try submitting an empty form, then enter a name and project. Watch the preview update as you change the controls.

Use a source build containing the test runner; older release binaries may not have bruster test. Install Rust and your platform’s webview development dependencies, then run:

Terminal · source checkout
git clone https://github.com/aliilyas/bruster.git
cd bruster
cargo build --release --locked
target/release/bruster test --help

The commands below run from the repository root. On Windows, use target/release/bruster.exe. Linux needs WebKitGTK 4.1 and a display; install xvfb and xauth on a headless machine. See Linux setup.

Serve the fixture in a separate terminal:

Terminal 1 · keep running
python3 -m http.server 8765 --bind 127.0.0.1 --directory examples/launch-demo

Open http://127.0.0.1:8765/ to inspect it yourself. Python only serves this static page; Bruster executes the tests. The fixture needs no account, API key, or external service.

2. Turn the demo recording into an automated test

The recording fills in Ada Lovelace and Bruster, chooses Form submission, enables screenshots, and submits. This is the actual downloadable recording.bru test; its contents are embedded from the file that the test suite runs. The website and demo suites are in the repository’s example-tests/ directory.

example-tests/demo/recording.bru
# The form workflow from scripts/launch_media.py, expressed as an executable test.
# Serve the fixture: python3 -m http.server 8765 --bind 127.0.0.1 --directory examples/launch-demo
base_url "http://127.0.0.1:8765/"
timeout 5000

test "creates the smoke test from the demo recording"
  navigate "."
  expect title "Bruster browser lab"
  expect hidden "#result"
  screenshot "01-empty-form.png"
  fill "#name" "Ada Lovelace"
  fill "#project" "Bruster"
  select "#workflow" "forms"
  check "#screenshots"
  expect js "JSON.parse(document.querySelector('#payload').textContent).screenshots === true"
  screenshot "02-filled-form.png"
  click "button[type=submit]"
  expect visible "#result"
  expect text "#result h2" "Smoke test created for Bruster"
  expect text "#result p" "Ada Lovelace · Form submission · Screenshots: on"
  eval "document.querySelector('#result').scrollIntoView({block: 'center', behavior: 'instant'})"
  expect js "document.querySelector('#result').getBoundingClientRect().top >= 0 && document.querySelector('#result').getBoundingClientRect().bottom <= innerHeight"
  screenshot "03-receipt.png"
end

Run it in another terminal:

Terminal 2
target/release/bruster test example-tests/demo/recording.bru --output target/demo-results

Add --visible to watch the browser. A successful run reports one passed test and prints the report location. The three screenshots capture the empty form, filled form, and receipt.

How to read the test

  • base_url sets the website address. navigate "." opens that base directory.
  • fill "#name" "Ada Lovelace" replaces the value of the element whose HTML ID is name. #name is a CSS selector.
  • select chooses the dropdown’s option value. check sets a checkbox to checked.
  • expect hidden verifies the initial receipt is hidden or absent. After submitting, expect visible and expect text verify the receipt and its contents.
  • screenshot saves the current viewport for inspection. The final eval step scrolls the receipt into view, and the following assertion checks its position before capturing it. The text assertions decide whether the receipt is correct; capturing an image alone does not compare it with a visual baseline.

Each command is on its own line. Quote selectors and values with double quotes, and close every test block with end. Use # for comments outside quoted strings.

3. Test form components and validation

A happy-path test tells you the workflow works. Smaller component checks make failures easier to locate. The component suite tests empty defaults, text editing, all three dropdown options, checkbox transitions, both required fields, repeat submissions, and rendering user input as text.

Check the checkbox and its observable effect

This test uses before_each to open the demo before each case. Checking twice should leave the checkbox checked; unchecking twice should leave it unchecked. Both the control and its live preview must agree.

Runnable excerpt · components.bru
# Serve examples/launch-demo on port 8765; see example-tests/README.md.
base_url "http://127.0.0.1:8765/"
timeout 5000

before_each
  navigate "."
end


test "checkbox state and preview stay in sync"
  check "#screenshots"
  check "#screenshots"
  expect js "document.querySelector('#screenshots').checked === true && JSON.parse(document.querySelector('#payload').textContent).screenshots === true"
  uncheck "#screenshots"
  uncheck "#screenshots"
  expect js "document.querySelector('#screenshots').checked === false && JSON.parse(document.querySelector('#payload').textContent).screenshots === false"
end

expect js is useful for values that are not plain text. It must return the boolean true. Read the checkbox’s checked property: the HTML checked attribute describes its default, and may not reflect its current state.

Verify an invalid form cannot produce a receipt

Leave the project empty, attempt submission, and check both native validity and the missing receipt. Then fill the required field and verify that the user can recover:

Runnable excerpt · components.bru
# Serve examples/launch-demo on port 8765; see example-tests/README.md.
base_url "http://127.0.0.1:8765/"
timeout 5000

before_each
  navigate "."
end


test "a missing project blocks submission"
  fill "#name" "Ada Lovelace"
  click "button[type=submit]"
  expect js "document.querySelector('#project').validity.valueMissing === true && document.querySelector('#test-form').matches(':invalid')"
  expect hidden "#result"
  fill "#project" "Bruster"
  click "button[type=submit]"
  expect text "#result h2" "Smoke test created for Bruster"
end

Run both files against the local server:

target/release/bruster test example-tests/demo/recording.bru example-tests/demo/components.bru

Each test gets a fresh browser process and profile. One test’s input or cookies cannot establish another test’s starting state. Keep related actions together when they are part of the same journey.

4. Automate website navigation and mobile UI checks

The same approach works on the Bruster website. Its page tests follow real navigation links, check documentation anchors and browser history, and reach the demo from this guide. Here is the navigation test, including its setup:

Runnable excerpt · example-tests/website/pages.bru
# Build and serve the product site first; see example-tests/README.md.
# Relative paths preserve a deployment prefix such as /bruster/.
base_url "http://127.0.0.1:8000/"
timeout 5000

before_each
  navigate "."
end


test "main navigation and browser history reach the documentation"
  click "#site-nav a[href$='docs/']"
  expect url "docs/"
  expect contains "h1" "Bruster documentation."
  expect attr "#site-nav a[href$='docs/']" "aria-current" "page"
  click ".docs-sidebar a[href='#testing']"
  expect url "docs/#testing"
  expect visible "#testing"
  expect contains "#testing" "bruster test"
  back
  expect url "docs/"
  back
  expect url "."
  forward
  expect url "docs/"
end

The website component tests exercise FAQ disclosure, MCP tabs with mouse and keyboard actions, and copy-button feedback. Clipboard tests replace the OS clipboard API with a controlled success or rejection so both UI paths are reproducible; they do not verify a system clipboard integration.

The mobile suite opens the menu, closes it with Escape, checks focus, follows a link, and checks page width at a 375×640 viewport. Viewport checks exercise responsive layout; they do not emulate a phone’s browser or touch hardware.

Run the complete website and demo suites

With Python 3.11+ and your built executable, this command builds the site, starts a temporary localhost server, runs the desktop and mobile suites, and stops the server:

Repository root
python3 scripts/site_test.py --binary target/release/bruster --output target/website-results

For an existing preview, build and serve it using the website instructions, then invoke Bruster directly:

target/release/bruster test example-tests/website/pages.bru example-tests/website/components.bru --base-url http://127.0.0.1:8000/ --viewport 960x640
target/release/bruster test example-tests/website/mobile.bru --base-url http://127.0.0.1:8000/ --viewport 375x640

--base-url overrides the file’s address. Keep a trailing slash when using a deployment prefix: with https://example.com/bruster/, navigate "docs/" stays under /bruster/, while navigate "/docs/" goes to the domain root.

Can browser tests check SEO metadata?

Yes. The page suite checks titles, canonical URLs, indexing directives, and this article’s structured data. The build checks also verify the sitemap and internal links. These catch publishing regressions; search ranking, crawl coverage, and page performance require their own measurements.

5. Write reliable assertions and debug failures

Wait for the outcome you need. Bruster retries assertions until their per-step deadline, set here to 5,000 milliseconds. Use expect text "#result h2" "Smoke test created for Bruster" instead of guessing how long submission might take and adding a fixed sleep. Actions run once; whole tests are not automatically retried.

  • Use stable selectors. Prefer purposeful IDs or test attributes over styling classes and positional selectors such as :nth-child(3).
  • Assert the result, not just the click. Verify the new receipt, selected panel, URL, or saved record.
  • Keep observations read-only. An expect js expression may run multiple times. Put intentional setup mutations in eval, which executes once.
  • Choose the right text check. expect text uses exact, whitespace-normalized text. Use expect contains when a substring is the actual requirement.
  • Control dependencies. The demo and preview run locally. For your own application, seed known test data and wait for the server to be ready before launching tests.

Every run creates a unique folder under --output. Its results.json records test names, source lines, step durations, failures, and artifact paths. Each test has a test-NNN/ directory. On failure, Bruster also attempts to save failure.png, snapshot.json, and console.json, alongside stderr.log.

Read the failing source line first, then inspect the screenshot and DOM snapshot to see what the page actually showed. For practice, save a copy of recording.bru and change the expected receipt to a different project name. Run that copy and follow the reported artifact path. Correct the assertion afterward.

Common causes are a server that is not running, a base URL missing its path prefix, a selector that matches the wrong element, or a test expecting a default HTML attribute instead of a live input property. Increase a timeout only when the application legitimately needs more time.

6. Run browser automation tests in CI

Continuous integration runs the same checks after a code change. Bruster exits with 0 when all tests pass, 1 for test or browser failures, and 2 for invalid CLI arguments, test files, or report setup/write errors. Keep failure artifacts even when the job fails.

This GitHub Actions example runs the suites in the Bruster repository on Ubuntu:

.github/workflows/website-tests.yml
name: website tests
on: [push, pull_request]
jobs:
  website:
    runs-on: ubuntu-24.04
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: sudo apt-get update
      - run: sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev xvfb xauth
      - run: cargo build --release --locked
      - run: xvfb-run -a python3 scripts/site_test.py --binary target/release/bruster --output website-results
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: website-results
          path: website-results/
          retention-days: 14

For your own website, replace the fixture build and server with your application’s setup and pass its address through --base-url. Bruster uses WKWebView on macOS, WebKitGTK on Linux, and WebView2 on Windows. Run on the platforms whose native-webview behavior you need to cover.

Web automation testing FAQ

Do I need JavaScript to write a browser test?

Basic navigation, clicks, input, text checks, and screenshots use Bruster DSL commands. JavaScript is useful for application-specific observations, such as the live checkbox state or the JSON preview in this demo. The test runner itself does not require Node.js.

Is a demo recording already a test?

A recording shows a workflow once. A repeatable test adds a known starting state and explicit assertions that can fail on later runs. Here, the recording’s form actions become recording.bru, while component checks cover invalid input and state changes that the short recording does not show.

Are these isolated component unit tests?

These are browser-level checks of components on a complete page. They exercise real DOM events, validation, and rendering together. Unit tests for a framework component’s internal logic are a separate layer.

When should I choose Bruster or Playwright?

Bruster fits focused smoke tests and native-webview automation with a small, line-oriented DSL. Its input events are synthetic, and cross-origin iframes and OS-level controls are outside its scope. Choose Playwright when you need selectable Chromium, Firefox, and WebKit engines, tracing, or its wider testing ecosystem. See the browser automation comparison and Bruster limitations.