QA automation, beginner edition

QA automation for beginners, starting from the login problem no other guide solves.

Open the top five search results for this keyword. They all spend three sections picking a framework and a language, then quietly skip the moment a beginner actually stalls: how do I keep my test logged in. Assrt's answer is the default behavior of the tool. The browser profile lives on disk at ~/.assrt/browser-profile and the same Chromium instance is reused across every #Case in a run.

M
Matthew Diakonov
10 min read
4.8from Assrt MCP users
Persistent Chromium profile on disk by default
One browser shared across every #Case in a run
Opt out per run with --isolated

The whole idea, one sentence

Assrt mounts ~/.assrt/browser-profile as a real Chromium user-data dir, so your first login is your last login.

Cookies, localStorage, IndexedDB, service workers all live on disk and survive between runs and across reboots. Inside one run, every #Case shares the same browser. Beginner-friendly default. Opt out with --isolated when you genuinely need a clean session.

Where every beginner's tutorial stalls

The top SERP guides for this keyword follow the same arc. Manual vs automated. The testing pyramid. Selenium and Cypress and Playwright. Then a section called something like "writing your first test" that uses an unauthenticated public page as the example. Then the article ends. The reader closes the tab and tries to test the part of their own app behind a login, and the tutorial provided no path. Below is the curriculum that fills that gap, and none of it has to be learned to ship the first useful test.

storageState JSONbeforeEach hooktest fixturesauth.setup.ts fileprocess.env.DEMO_PASSWORDtest.use({ storageState })browserContext.storageState()page.context().cookies()cy.session()Cypress.session.clearAllSavedSessions()page object patternglobal setup scriptglobal teardownENV file rotationsession token refreshdescribe.beforeAll()

Every chip on that scroller is a real concept that real QA automation tutorials require their reader to learn before they can write a logged-in test. The Assrt default replaces all of them with one on-disk directory and a one-line CLI flag.

The anchor: the actual on-disk directory

This is the verifiable, uncopyable part of the page. Run any Assrt test once without the --isolated flag, then list the directory. You will see a normal Chromium profile layout, because that is what it is.

bash

That directory is the persistent state. SQLite cookie store, localStorage shards under Local Storage/, IndexedDB folders for any web app that uses it, service worker registrations, and the standard Preferences JSON. Treat it the same way you would treat a real Chrome profile: useful on your laptop, not a thing you commit to git or copy to a teammate.

The code that creates that directory

Two branches inside the launch wiring. With isolated: true the upstream Playwright MCP gets --isolated and a fresh ephemeral profile. With the default, --user-data-dir is set to the on-disk path under your home directory.

assrt-mcp/src/core/browser.ts:285-322

Multi-case runs share one browser

The persistent profile is half the story. The other half is that within a single Assrt run, every #Case block runs in the same Chromium instance, in order. Case 1 sets the cookies. Case 2 reads them. Case 3 reads them too. Nothing is torn down between scenarios.

assrt-mcp/src/core/agent.ts:445-456

The practical effect: a beginner's plan file can be structured the same way a manual QA pass is structured. Sign in. Then do the thing. Then do the next thing. No setup block, no fixtures, no shared user object. The browser is the shared state, exactly as it is when a real human tests an app.

What the round-trip looks like

Five inputs land on the persistent profile. Three outputs come back out of it. The hub in the middle is the Chromium instance Assrt launches once per run.

Persistent profile, in and out

#Case 1 sign-in
#Case 2 actions
Extension token
localStorage writes
IndexedDB writes
~/.assrt/browser-profile
Next #Case
Tomorrow's run
--isolated run

Side by side: the Playwright way vs. the Assrt default

Both code samples below sign a beginner's test in and then create a project. The left tab is the standard Playwright recipe a beginner would copy from the docs. The right tab is the Assrt plan that gets you the same result. Toggle and read.

Authenticated tests, beginner edition

// Playwright, the storageState approach a beginner has to learn // 1) Run a one-off "auth.setup.ts" file that signs in and saves the state. import { test as setup } from "@playwright/test"; setup("authenticate", async ({ page }) => { await page.goto("https://app.example.com/login"); await page.getByLabel("Email").fill(process.env.DEMO_EMAIL!); await page.getByLabel("Password").fill(process.env.DEMO_PASSWORD!); await page.getByRole("button", { name: "Continue" }).click(); await page.waitForURL(/dashboard/); await page.context().storageState({ path: "playwright/.auth/user.json" }); }); // 2) Tell every other test to load that file as its initial state. import { test, expect } from "@playwright/test"; test.use({ storageState: "playwright/.auth/user.json" }); test("create a project", async ({ page }) => { await page.goto("https://app.example.com"); await page.getByRole("button", { name: "New project" }).click(); // ... });

  • Two separate spec files
  • auth.setup.ts saves a JSON
  • test.use({ storageState }) on every test
  • Env vars for the credentials
  • Re-runs the entire login on every CI invocation

What a beginner actually types

Below is the full first-run output for a two-case plan. The second case never logs in; it inherits the cookies the first case set, and Assrt logs the fact explicitly so you can read it in the terminal.

claude session

The four steps to your first logged-in test

One install command. One plan file. One assrt_test call. One ls to confirm the profile is on disk.

1

Install the MCP server

npx assrt-mcp inside any Claude Code or Claude Desktop session that has MCP support. The first launch creates ~/.assrt/ and registers the three tools assrt_plan, assrt_test, assrt_diagnose.

2

Write a two-case plan

Open scenario.md in any editor. Write '#Case 1: Sign in' followed by the steps. Then '#Case 2: Do the thing'. No fixtures, no env vars, no test runner config. Just imperative English.

3

Run it

Ask Claude to call assrt_test with the file. Watch the auto-recorded video at /tmp/assrt/<runId>/video/recording.webm. Case 2 will run without a sign-in step because the session from Case 1 carries over.

4

Verify the persistent profile

ls ~/.assrt/browser-profile/Default/ in another terminal. You will see Cookies, Local Storage/, IndexedDB/, Service Worker/. The session is real, on-disk, and survives until the server-side cookie expires or you pass --isolated.

The numbers from the source

All checkable by opening assrt-mcp/src/core/browser.ts and assrt-mcp/src/core/agent.ts.

0Chromium instance per run
0Line where userDataDir is set
0CLI flags that change auth behavior
0JSON storage-state files written
0Subdirectories under Default/
0Built-in test actions you can call
0sDefault OTP poll window
0sDefault wait_for_stable timeout

Where this approach fits, and where it does not

Persistent profile is the right beginner default. It is not the right CI default. The grid below is the honest table.

FeaturestorageState / cy.sessionAssrt persistent profile
Beginner laptop, localhost, exploratory testingWorkable, but requires understanding fixturesBest fit — log in once, every test inherits
Test against a staging environment from your laptopWorkable; you re-run the auth.setup file when staleBest fit — the session lives across days
CI worker, fresh container every runNative fit — write storageState once, pin the JSONUse --isolated and inject credentials per run
Tests that explicitly cover the unauthenticated flowUse a separate project / config without storageStatePass --isolated for that single run
Browser extensions (1Password, Bitwarden) needed for loginPainful — you almost always disable the extensionFirst-class — pass --extension once, token saved
Cross-team session sharingYes — storageState JSON can be checked in (cautiously)No — never commit ~/.assrt/browser-profile
Beginner concepts you must learn firstSessions, fixtures, JSON state, beforeEach lifecycleZero

Anchor fact

The directory at ~/.assrt/browser-profile/Default/ is a normal Chromium profile. List it after one test run and the cookie store, localStorage, IndexedDB, and service workers are right there on disk.

The path comes from assrt-mcp/src/core/browser.ts:290 where the launch flow does join(homedir(), '.assrt', 'browser-profile') and passes it as Chromium's --user-data-dir. The same beginner-friendly default holds across every #Case in a run, which is why your second case never has to repeat the sign-in.

0 directory. 0 JSON files to wire up. 0 beforeEach hooks to write.

What you give up by leaning on the default

Every default is a trade. The persistent profile makes one beginner concern go away and creates a few intermediate concerns you should know about before you scale this up.

The honest list

  • State leaks between tests. Case 1 might create a project that Case 5 still sees. That is the same dynamic any human QA pass has, and it is a feature for exploratory testing. For deterministic CI, pass --isolated.
  • The profile is laptop-local. You cannot ship it to a teammate or commit it. If shared session state matters, write a Playwright storageState file separately.
  • Server-side sessions still expire. If your app sets a 7-day cookie, the persistent profile is good for 7 days. After that, Case 1 has to sign in again and refresh the cookie on disk.
  • Live cookies are sensitive. Treat ~/.assrt/browser-profile the way you treat your real Chrome profile: do not back it up to public storage, do not share it.

Skip the auth-setup chapter every other guide makes you read.

15 minutes with the Assrt maintainer. Bring your app's URL; we will show the persistent profile + multi-case loop running against it live.

Book a call

QA automation for beginners: specific answers

I have never written a test in my life. Why is the login problem the first thing you talk about?

Because every other beginner guide for this keyword spends three sections on framework choice, language choice, and the testing pyramid, then quietly skips the moment a real beginner stalls. That moment is: I wrote my first test, it works on the homepage, now I want to test something behind the login wall, and the tutorial says 'just save a storageState.json and import it in beforeEach.' That sentence assumes you understand sessions, cookies, JSON loading, and the test runner's lifecycle hooks. None of those are obvious to a beginner. The Assrt default avoids the explanation by avoiding the problem: the browser profile lives at ~/.assrt/browser-profile and persists between runs, so you log in inside Case 1 (or even just once, by hand the first time you launch the MCP server) and every test from then on starts already authenticated.

Where exactly does Assrt save the browser profile, and how do I prove it for myself?

It saves to ~/.assrt/browser-profile. The path is constructed at line 290 of /Users/matthewdi/assrt-mcp/src/core/browser.ts as join(homedir(), '.assrt', 'browser-profile'). After running npx assrt-mcp once and signing in inside a test, run ls ~/.assrt/browser-profile/Default/ in a terminal. You will see Cookies (a SQLite file), Local Storage/, IndexedDB/, Service Worker/, and a handful of other Chromium internals. That is the same directory layout you would see under ~/Library/Application Support/Google/Chrome/Default on macOS. Assrt does not invent a custom format; it hands the directory to Chromium as --user-data-dir and lets the browser do its normal thing.

What if I do want a clean session for a particular run?

Pass --isolated when you launch the server. The flag is read at /Users/matthewdi/assrt-mcp/src/cli.ts line 33 and forwarded to Playwright MCP at browser.ts line 285, which causes the upstream MCP to start a fresh ephemeral profile that lives only for the duration of the run. Use it for: tests that must not be affected by previous logins, paywall flows where you want to confirm the unauthenticated experience, or CI runs where every job should start cold. The default is non-isolated because that is the right answer for a beginner doing exploratory testing on localhost; --isolated is the explicit opt-in for the other case.

Across multiple #Case blocks in the same run, does Case 2 start with the cookies Case 1 set?

Yes. The scenario loop at /Users/matthewdi/assrt-mcp/src/core/agent.ts lines 445 through 456 iterates the parsed scenarios sequentially and reuses the same Chromium instance for all of them. The agent passes a summary of previous scenario results to the model so it knows what state Case 1 left the browser in. There is one log line in agent.ts around line 585 that says exactly this: 'Cookies, auth state carry over between scenarios.' That is the property a beginner needs the most: write Case 1 'Sign in with the demo user,' write Case 2 'Open the dashboard and click Settings,' and Case 2 will not need to repeat the sign-in steps.

What is a #Case block? I have not seen that format before.

It is the plain-English test format Assrt accepts in place of a code file. You write a Markdown file (or just a string) with sections that look like '#Case 1: Sign in' followed by 3 to 5 imperative sentences about what to click, type, and verify. The parser at agent.ts line 569 splits the file on the regex /(?:#?\s*(?:Scenario|Test|Case))\s*\d*[:.]\s*/gi so #Case 1, #Test 2, and # Scenario 3 all work. The agent then drives a real Chromium through real Playwright MCP for each section. You never import @playwright/test, never call describe() or it(), never write a CSS selector. If you can write a Jira ticket, you can write a #Case.

Does the persistent profile survive a reboot, or only a session?

It survives reboots. ~/.assrt/browser-profile is a real on-disk directory, not a tmpfs mount. If you log in to your own staging environment on Monday and run a test against it on Friday without --isolated, the cookies are still there as long as the server-side session has not expired. This is the property that turns 'I cannot test the logged-in part of my app' into a one-time setup problem instead of a permanent rebuild-on-every-run problem. The trade-off: do not commit ~/.assrt/browser-profile to a shared repo or copy it to a teammate's laptop; it contains your live cookies.

How does this compare to Cypress or Playwright's storageState approach?

Cypress recommends a custom command (cy.session) that caches cookies and localStorage between tests. Playwright recommends saving the storage state to a JSON file with browserContext.storageState() and reading it back with browser.newContext({ storageState: 'auth.json' }). Both work, both require code, both ask a beginner to understand what storage state is. Assrt's persistent profile is the same idea moved one layer down: instead of serializing the state into JSON and reloading it, the browser keeps its own profile on disk and reuses it directly. There is no JSON to rotate, no path to wire up, no expiry to think about until the server-side session actually dies. For a CI environment you would still typically pass --isolated and use Playwright's storageState idiom, because CI workers are stateless. For a beginner on their laptop running tests against localhost, the persistent profile is a strict upgrade.

Is the underlying browser real Chromium or something custom?

Real Chromium, started by @playwright/mcp. Assrt does not ship its own browser binary; it spawns the official Playwright MCP server which in turn launches a Playwright-managed Chromium. Every action your #Case takes (navigate, click, type, snapshot, press_key) becomes a real Playwright protocol call. That is why the browser-profile directory looks like a normal Chrome profile and behaves like one: it is one. The relevant launch wiring is at browser.ts lines 285 to 322 of /Users/matthewdi/assrt-mcp/src/core/browser.ts.

What about extensions like a 1Password or Bitwarden that I would need for some logins?

There is an --extension flag on the CLI (cli.ts line 28-ish onward) that opts the browser into loading the user's existing Chrome extensions on first run. The first time you use it, Chrome prompts for approval and Assrt saves the resulting token to ~/.assrt/extension-token (browser.ts lines 208 and 223). Subsequent runs read the token and skip the approval step. So if your logged-in flow needs a password manager, the same persistent-profile philosophy applies: approve once, every test from then on has the extension available. This is genuinely uncommon among QA automation tools; most beginner guides simply tell you to disable the password manager.

If I am brand new to QA automation, what is the smallest first thing I can run?

Three commands. One: npx assrt-mcp inside a Claude Code or Claude Desktop session that has the MCP server registered. Two: ask Claude to call assrt_test with a one-line plan like '#Case 1: Open https://example.com and verify the heading contains Example Domain.' Three: open the auto-recorded video at /tmp/assrt/<runId>/video/recording.webm and watch what happened. That run produced ~/.assrt/browser-profile as a side effect, so your second test (something behind a login) inherits whatever cookies and localStorage the first test left behind. There is no fixture file to set up, no test runner to configure, no beforeEach hook to learn.

How did this page land for you?

React to reveal totals

Comments ()

Leave a comment to see what others are saying.

Public and anonymous. No signup.