# A fresh browser profile for each test run

> Leftover cookies and storage are where flaky browser tests come from. A one-time profile gives each run a clean jar and trashes itself when its browser closes.

Source: https://mosaicbrowser.com/blog/fresh-profile-per-test-run

## A fresh browser for every test run, and why it should be cheap

Leftover cookies and storage are where flaky browser tests come from. A one-time profile gives each run a clean jar and trashes itself when its browser closes.

![A fresh browser for every test run, beside a screenshot: the Mosaic trash: deleted profiles kept for 30 days, restorable one by one](https://mosaicbrowser.com/covers/fresh-profile-per-test-run.webp)

A fresh browser profile for each test run is the cheapest fix for the flakiest kind of browser test: the one that passes alone and fails after another test has run. The cause is almost always state the previous run left behind, a cookie, a `localStorage` key, a service worker or a cached redirect. Give each run a profile nobody has used, retire it when the run ends, and that class of failure has nowhere to come from.

## Where leftover state hides

Clearing cookies is not a clean start. A browser profile that has visited a site keeps much more than its cookie jar, and each piece can change what the next test sees:

| Left behind | What it does to the next run |
| --- | --- |
| Cookies | a test meant for a new visitor arrives signed in, or with last run's cart |
| `localStorage`, `sessionStorage`, IndexedDB | an onboarding flag is already set, so the tour never shows |
| A registered service worker | answers requests from its own cache, so a deploy you are testing is not what loads |
| HTTP cache and redirects | a cached 301 sends the test past a page that has since changed |
| HSTS and permissions | a site remembered as HTTPS-only, a notification prompt already answered |

Each of these makes a test depend on the order it runs in. Clearing them one by one works until someone adds a storage type the cleanup does not know about. A new profile has none of them, by construction.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: REST API](https://mosaicbrowser.com/features/rest-api)

## Why a fresh profile per test should be cheap

The reason people reuse a profile is that making one is slow or manual. If a fresh one costs a click, or one API call, there is no reason to share.

That is how Mosaic's one-time profile works. In the window, click New profile and choose One-time, which the dialog describes as "Trashed when its browser closes." From code it is one field on the create call. The profile is marked in the list, and the moment its browser stops it moves itself to the trash. Nobody has to remember to delete it, and nobody's cleanup script has to decide which profiles count as test profiles.

That last part is not a small thing. An unscoped empty-trash call in one of our own check scripts once destroyed 586 real profiles, unrecoverably. Since then an empty body on `POST /trash/empty` is a `400`, and every cleanup we write deletes by id. A profile that retires itself is the cleanup that cannot be widened.

## One run, from code

The local REST API listens on `127.0.0.1`, and a run is three calls with an attach in the middle. Create the profile, with a proxy pasted straight in if the test needs a country:

```
ID=$(curl -s -X POST -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
  -d '{"name":"checkout run","oneTime":true,"proxy":"socks5://user:pass@gw.example.com:1080"}' \
  "$BASE/profiles" | jq -r .id)

```

Start it and take the DevTools endpoint it returns:

```
WS=$(curl -s -X POST -H "Authorization: Bearer $KEY" "$BASE/profiles/$ID/start" | jq -r .wsEndpoint)

```

Attach your test runner. Playwright and Puppeteer both connect over CDP:

```
const browser = await chromium.connectOverCDP(process.env.WS)
const page = browser.contexts()[0].pages()[0] ?? await browser.contexts()[0].newPage()

```

When the test ends, stop it with `POST /profiles/$ID/stop`. The stop is a clean shutdown, so cookies and storage are flushed before the browser exits, and the profile then moves to the trash on its own. If the run failed, it is still there for 30 days by default and can be restored to look at what the site set. If you do not want it kept, `DELETE /trash/profiles/$ID` purges that one profile and nothing else.

Two details save time in a suite. A profile with a proxy does not start until that proxy has answered and tunnelled, so a dead proxy fails the run at start with a reason, instead of producing a test that silently browsed from your own address. And the create call measures the proxy's exit to draw the timezone and language for it; `"matchExit": false` skips that lookup when where the profile appears to be does not matter to the test.

## Contexts, profiles and what each gives you

Playwright's own answer to the same problem is `browser.newContext()`, and for many suites it is the right one: a new context starts with an empty jar and costs milliseconds. What a context does not change is the browser underneath. Every context in one browser has the same fingerprint, the same User-Agent and the same network path.

That matters when the test is about who the visitor is. Checking a checkout as a first-time customer in Germany, a cookie banner that only shows in some countries, or affiliate tracking that must not see a previous click all need a visitor that differs in more than cookies. A one-time profile has its own empty jar, its own fingerprint drawn from real hardware, and its own proxy exit with a timezone and language to match. The checkout case, step by step, is in [testing your own checkout as a customer who has never visited](https://mosaicbrowser.com/blog/test-checkout-as-customer). The affiliate case, where one stale click cookie credits the wrong partner, is in [testing affiliate link tracking with a clean jar](https://mosaicbrowser.com/blog/testing-affiliate-link-tracking).

Not every test should be fresh. A suite that tests the signed-in experience needs a user who stays signed in, and making twenty long-lived test accounts easy to tell apart is its own job ([keeping twenty QA test accounts straight](https://mosaicbrowser.com/blog/qa-test-accounts-naming)). The rule we use is plain: if the test is about a new visitor, the profile is new too, and it is gone when the test is.

## Questions

### Should every test get a fresh browser profile?

Every test that depends on being a new visitor should, such as sign-up, first-visit banners, checkout as a guest or onboarding. Tests that need a signed-in user can share a profile on purpose, but then the shared state is part of the test and should be set up by it.

### Is a Playwright browser context the same as a fresh profile?

For cookies and storage, nearly. A new context starts with an empty jar. It still shares the browser's fingerprint and network path with every other context, so a test that needs a different machine or a different country needs a different profile.

### What happens to a one-time profile in Mosaic after the test?

The moment its browser stops, it moves itself to the trash. The trash keeps it for 30 days by default, so a failed run can still be restored and inspected, or you can purge it by id straight away.

### How do I create a fresh profile per test from code?

Call POST /api/v1/profiles with oneTime set to true, start it with POST /profiles/:id/start, attach Playwright or Puppeteer to the wsEndpoint it returns, and stop it when the test ends.

## Related

[Testing your own checkout as a customer who has never visited](https://mosaicbrowser.com/blog/test-checkout-as-customer): To test a Shopify checkout as a customer, pay with a test method from a browser that has never seen your store: no admin login, no saved address, no cart.

[Testing whether your affiliate links track, with a clean jar](https://mosaicbrowser.com/blog/testing-affiliate-link-tracking): To test whether an affiliate link tracks, click it from a browser with an empty cookie jar and read what it set. An old click's cookie can spoil the test.

[Keeping twenty QA test accounts straight](https://mosaicbrowser.com/blog/qa-test-accounts-naming): Managing test accounts for QA goes wrong when the state lives in people's heads. Give each persona a profile with its login, role and state beside it.

[Checking your cookie banner from the countries that require it](https://mosaicbrowser.com/blog/cookie-banner-by-country): Consent tools pick the banner by the visitor's IP location, so your office sees one version. Test each region from a fresh profile with an exit there.

[What is an anti-detect browser?](https://mosaicbrowser.com/guides/what-is-an-anti-detect-browser): An anti-detect browser gives each profile its own fingerprint, proxy and cookies, so sites see separate visitors. What it changes, how, and its limits.

[Seeing both sides of your own A/B test](https://mosaicbrowser.com/blog/seeing-both-ab-variants): Testing tools put each visitor in one variant by a cookie or id and keep them there. To see every variant, keep one browser profile per variant for the test.

[How to test a metered paywall as a new visitor](https://mosaicbrowser.com/blog/metered-paywall-new-visitor): A meter can count by cookie, by login, by address or by fingerprint. To test yours as a new visitor you need a browser that is new to every one of them.

[A retired bot test can still answer 200 and look like a pass](https://mosaicbrowser.com/blog/dead-detector-returns-200): Antoine Vastel's fp-scanner demo now serves his homepage with HTTP 200, which a challenge reader scores as a pass. His tests moved; here is where.

## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
