← Blog
Partnerships

Sign in once per worker: TesterArmy e2e tests on smol machines

smol machines is integrated with TesterArmy’s open-source testing framework, e2e, as a browser provider. @e2e-dev/smol allows every test attempt to run in its own branch of a warm, signed-in Chromium machine.

npm install --save-dev @e2e-dev/smol smolmachines
import type { E2EConfig } from 'e2e';
import { web } from '@e2e-dev/web';
import { smol } from '@e2e-dev/smol';

export default {
  targets: [
    {
      name: 'web',
      engine: web({ browser: smol({ hostPorts: [3000] }) }),
      app: {
        url: 'http://localhost:3000',
        command: { executable: 'npm', args: ['run', 'dev'] },
      },
    },
  ],
  workers: 4,
} satisfies E2EConfig;

Shared state impacts end-to-end test results

Each attempt starts in a fresh browser context, but the app under test still remembers what earlier tests wrote to its database and server, and that carries over to the next test.

This property causes two problems. A test can pass or fail depending on what ran before it. And when a test fails and the runner retries it, the retry starts from whatever the failed attempt left behind. So a test can also fail for a perfectly working subject.

The usual fix is to start the new test in a fresh browser with a fresh login, which is correct but repeats the same steps, and the app’s server state stays shared. Signing in through a real login page is a series of steps, and they repeat for every attempt as the test suite grows. A session saves the cookies and storage of one sign-in for other tests to restore, but not the open pages, the browser’s memory or the app’s server state.

With prepare, the sign-in steps run once per worker instead of once per attempt, retries included.

Branch of a signed-in browser

smol machines’ virtual machines resolve this inconvenience through branching. A branch becomes a copy-on-write clone of the machine at that instant, with its memory, its running processes and its disk intact, which removes the need to log in repeatedly.

The e2e testing framework integrates with smol machines and builds on top of this branching feature. Each worker slot boots one Chromium machine and keeps it warm. A prepare step drives that browser once. Then every test attempt runs in a branch of the signed-in browser, and the branch is deleted after the attempt ends. Nothing an attempt does inside the machine reaches another, and a retry starts exactly from the beginning.

Fresh browser and loginBranch of a signed-in browser
Each attempt starts ina new browsera copy-on-write branch of the warm machine
The sign-in steps runevery attemptonce per worker, in prepare
A retry starts froman empty browser that signs in againthe same signed-in state as the first try
What an attempt leaves behindthe app’s server state and databasenothing inside the machine, and with app, the app’s data is the attempt’s own too

Add the prepare configuration so that every attempt starts with the signed-in state. It only needs to receive the browser’s DevTools endpoint, and it runs once per warm machine:

import { chromium } from 'playwright-core';

smol({
  hostPorts: [3000],
  prepare: async (cdpEndpoint) => {
    const browser = await chromium.connectOverCDP(cdpEndpoint);
    const page = browser.contexts()[0]!.pages()[0]!;
    await page.goto('http://localhost:3000/login');
    await page.getByLabel('Email').fill('[email protected]');
    await page.getByLabel('Password').fill(process.env.QA_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('**/dashboard');
    await browser.close();
  },
})

The tests stay the same across attempts. If a test uses sessions, prepare replaces them:

import { test } from '@e2e-dev/web';
import { expect } from 'e2e';

test('a signed-in member sees the billing page', async ({ app, screen }) => {
  await app.open('/settings/billing');

  await expect(screen.getByRole('heading', 'Billing')).toBeVisible();
});

Give every attempt its own app

For apps that run on your computer, you can move the app into the machine to prevent sharing a local database between attempts:

smol({
  app: {
    source: '.',
    setup: 'npm ci && npm run db:seed',
    start: 'npm start',
    port: 3000,
    env: { DATABASE_URL: 'file:/app/dev.db' },
  },
})

The provider copies the project, runs setup and start inside the machine, and waits for the port to answer. Each attempt’s branch now holds its own copy of the running app and its local database, and two attempts that write the same row will never see each other’s writes.

With app, set the target’s app.url to http://localhost:<port>, leave out app.command, and keep that port out of hostPorts.

The collaboration

e2e owns the test: the runner, the API, agentic steps like agent.act and the cache that replays them, and the reporters.

smol machines owns the machine: the microVM, the warm Chromium image, the branch, and cleaning up.

Notes before testing


The full option list is on e2e's smol machines page, and the setup is on ours. The provider's code is in tester-army/e2e.

RSS feed