Skip to main content
Your app runs behind bot protection, and your end-to-end tests fail when it blocks the test browser. This walkthrough runs pytest tests in Apostate with a fixed seed, on your computer and on GitHub Actions, and shows the same tests in Playwright Test for Node.

What Apostate changes

Bot protection looks for traces of automation, such as HeadlessChrome in the User-Agent, navigator.webdriver set to true, and the 800x600 screen of headless Chrome. Apostate’s browser shows none of them. It presents the machine of a persona, the Windows, macOS or Linux platform you choose. Detection lists the traces and what the browser does about each. A fixed seed, the value that selects the machine, presents the same machine on every run, so a failure reproduces. The example app stands in for your protection. It refuses a signup whose User-Agent contains HeadlessChrome or whose page reports navigator.webdriver.

Set up

apostate fonts install windows is for Linux and macOS hosts (Fonts). The folder holds:

The fixtures

conftest.py starts the app, launches one browser for the test session and gives each test a new page:
The fixtures are named browser and page, so in a project that has the pytest-playwright plugin they replace the plugin’s fixtures of the same names.

The tests

The pages come from Patchright, the package’s default driver, so expect comes from patchright.sync_api. With driver="playwright", import it from playwright.sync_api.

Run the tests

To run the same tests against a site that is already running, such as staging, set APP_URL:

GitHub Actions

e2e.yml runs the tests on every push and pull request. Copy it to .github/workflows/e2e.yml in your repository:
  • The browser download for Linux x64 is 192 MB. The cache step keeps it in ~/.cache/apostate between runs, and apostate install finds it there.
  • The Linux archive is zstd-compressed. Python 3.14 reads it, and older versions need the zstandard package or the zstd command (Installation).
  • The tests run headless, the default, so the runner needs no display.
  • A runner image without Chrome’s shared libraries needs the packages that the Docker example’s Dockerfile installs.

Playwright Test for Node

The Node package needs Node 22 or later. Patchright, the driver it launches with, includes Playwright Test as patchright/test. The fixture file replaces Playwright Test’s page with a page from Apostate:
playwright.config.mjs starts the Python app for the tests, unless APP_URL names a running site:
The tests import test and expect from ./fixtures.mjs instead of patchright/test. Run them with Patchright’s test runner:

Points for this job

  • Fixed seed. Assert on values the seed decides, such as the platform, screen and GPU. The core count and memory are capped at the host’s, so they can differ between your computer and a CI runner (The host cap).
  • Isolation. All tests share one browser and its user data directory, and the fixtures clear cookies before each test. For a fresh user data directory per test, make browser a function-scoped fixture. Each test then launches its own browser, which is slower.
  • New pages only. Open pages with new_page(), not new_context(). Python explains why.
  • Test keys. Some bot protection products offer test keys for automated tests, such as Cloudflare Turnstile’s test sitekeys. Those tests pass whatever the browser. Apostate is for the tests that go through your real protection, such as a staging site with production keys. Test your own defenses covers that.
  • Headed runs. On a Linux runner, headless=False needs Xvfb installed, and the package starts it (Linux servers).