{"slug": "how-i-used-ai-agents-to-migrate-reka-ui-s-tests-to-vitest-browser-mode", "title": "How I Used AI Agents to Migrate Reka UI's Tests to Vitest Browser Mode", "summary": "Alexander Opalic created a fork of Reka UI and used AI coding agents to migrate its 97-file test suite from jsdom to Vitest Browser Mode, keeping the original jsdom tests as a comparison baseline. The migration classified files by DOM dependency, assigned one implementer and one reviewer per file, and used targeted mutations to verify tests catch real breakage. The fork, available at github.com/alexanderop/reka-ui-bench-mark, is a research project, not an upstream change.", "body_md": "I wanted to learn Vitest Browser Mode properly.\n\nNot with a counter component. Not with five tests written specifically for a demo. I wanted a real component library with years of testing history and enough sharp edges to punish bad assumptions.\n\nSo I created a working fork of [Reka UI](https://github.com/unovue/reka-ui) and used AI coding agents to migrate its test strategy away from jsdom.\n\nThe original suite had 97 test files. Eighty-seven touched the DOM and received Browser Mode counterparts. The remaining ten contained 571 DOM-free tests, so they moved to a plain Node project instead.\n\nI kept every original jsdom file as a comparison corpus. This gave the agents a live baseline rather than a memory of what the old tests used to do.\n\nThe AI didn’t only translate tests. It reviewed ports, introduced deliberate bugs, ran both environments, recorded findings, and improved its own prompts after each batch.\n\nThat process is the interesting part.\n\nA research fork, not an upstream migration\n\nThis work lives in my [Reka UI Browser Mode fork](https://github.com/alexanderop/reka-ui-bench-mark/tree/browserMode). It isn’t an upstream Reka UI change. The fork exists to study the migration and preserve the comparison.\n\n## ✨TLDR\n\n- →Build the migration oracle before asking agents to port files\n- →Give one file to one implementer and a fresh reviewer\n- →Keep the old suite alive so every port has a comparison target\n- →Use targeted mutations to prove that tests can catch real breakage\n- →Store prompts in Git and fix the prompt when a batch goes wrong\n- →Run only a few browser agents concurrently because each one launches Chromium\n\n## The Wrong Way to Use AI for a Migration\n\nThe tempting prompt is:\n\nPort all tests from jsdom to Vitest Browser Mode. Make the suite green.\n\nThat prompt optimizes for green tests.\n\nAn agent can reach green by deleting an awkward assertion. It can replace an exact query with a broader one. It can add `force: true`\n\nto every click or quarantine failures without understanding them.\n\nAll of those ports compile. All of them can pass.\n\nThe problem gets worse with parallel agents. One weak port is reviewable. Eighty-seven weak ports become a new baseline before anyone notices.\n\nSo the first job wasn’t porting tests.\n\nIt was building a machine that could reject bad ports.\n\n## The Core Idea: Agents Propose, Machines Decide\n\nThe migration became a feedback-controlled loop:\n\nThe AI handled judgment-heavy work inside each step. Deterministic scripts controlled the boundaries.\n\nThat distinction mattered. The agents could propose translations and investigate differences. They couldn’t redefine what “complete” meant.\n\n## Step 1: Keep Both Environments Running\n\nThe first commit created three Vitest projects over the same source tree:\n\n| Project | Purpose |\n|---|---|\n`unit` | Original jsdom tests, kept unchanged |\n`browser` | New `*.browser.test.ts` files in Chromium |\n`node` | Tests that never needed a DOM |\n\nRunning the environments side by side changed the migration from a rewrite into an experiment.\n\nFor every file, I could ask:\n\n- Did the Browser Mode port keep the same test structure?\n- Did it preserve the assertions?\n- Did it reach at least the same production code?\n- Which suite noticed when we broke the component?\n\nWithout the retained jsdom suite, those questions would become opinions.\n\n## Step 2: Build an Inventory Before Assigning Work\n\nI scanned all 97 files for DOM signals such as:\n\n- Vue Test Utils and Testing Library imports\n`document`\n\n,`window`\n\n, and DOM constructors- accessibility audits\n- browser API mocks\n- story fixture mounts\n\nThe script then classified every file by migration risk:\n\n| Tier | Meaning |\n|---|---|\n| T0 | No DOM. Move it to Node |\n| T1 | DOM-dependent but expected to be boring |\n| T2 | Mostly mechanical component tests |\n| T3 | Mocks browser APIs or depends on geometry |\n| T4 | Special patterns such as fake timers, module mocks, snapshots, or virtualization |\n\nThis inventory became the progress bar and the work queue.\n\nIt also prevented a wasteful mistake. Ten files didn’t need Chromium or jsdom. Moving them to Node removed the fake browser environment from 28% of the original tests.\n\n## Step 3: Calibrate the System on Three Files\n\nBefore the fan-out, I chose three deliberately different files.\n\n### Slider: the difficult port\n\nSlider mocked `ResizeObserver`\n\n, scrolling, and pointer capture. It forced the first agent to solve real geometry and input problems.\n\n### useForwardExpose: the boring port\n\nThis composable installed no compensating mocks. Its Browser Mode version was almost identical and bought little new information.\n\nThat negative result was useful. The process needed to report “this port gained nothing” without inventing a victory.\n\n### Label: the mechanical port\n\nLabel was small and looked easy. It still exposed several traps around click semantics, zero-width elements, and missing positive assertions.\n\nThese three files produced the first version of the implementer and reviewer prompts. They gave later agents concrete precedents for difficult, boring, and mechanical work.\n\n## Step 4: Give Each Implementer One File\n\nEach agent received one original test file and one output path.\n\nThe assignment looked roughly like this:\n\n```\nPort packages/core/src/{FILE}\nto packages/core/src/{COMPONENT}.browser.test.ts.\n\nKeep every describe and it name verbatim.\nNever reduce the assertion count.\nDon't change production source or fixtures.\nKeep the original jsdom file.\nRun only focused tests for your file.\nRecord at least one evidence-backed finding.\n```\n\nThe narrow ownership reduced conflicts. It also made failures attributable. If a batch failed, I knew which file, prompt, and agent decision produced it.\n\nAgents weren’t allowed to fix product bugs during the port. A faithful Browser Mode test failing was considered a successful finding.\n\nThe agent had to quarantine that test with `it.fails`\n\nand link it to a findings key:\n\n```\n// @finding Slider/Slider.test.ts#axe\nit.fails(\"should pass axe accessibility tests\", async () => {\n  // Keep the original assertion strong.\n})\n```\n\nThis kept the suite runnable without hiding the discovery. When the bug gets fixed, `it.fails`\n\nbecomes red because the expected failure disappears.\n\n## Step 5: Review with a Fresh Context\n\nThe implementer never reviewed its own port.\n\nA second agent received the original and the Browser Mode version. It didn’t initially receive the implementer’s reasoning or findings.\n\nIts task was adversarial:\n\nAssume this port is weaker than the original. Find out how.\n\nThe reviewer searched for:\n\n- exact assertions replaced by broad ones\n- lazy locators that were never resolved\n- retrying assertions that widened timing contracts\n- new sleeps hiding synchronization problems\n- role queries replacing tag assertions\n- forced interactions that bypassed the behavior under test\n- quarantines swallowing unrelated assertions\n\nThis separation worked because implementation and review reward different behavior. The implementer wants completion. The reviewer wants a counterexample.\n\nThe machine checked structure. The reviewer checked meaning.\n\n## Step 6: Make Parity Machine-Enforced\n\nEvery port had to pass five focused commands:\n\n```\npnpm --filter reka-ui exec vitest run \\\n  --project=browser <browser-file>\n\npnpm --filter reka-ui port:checklist \\\n  <component> --complete\n\npnpm --filter reka-ui port:parity \\\n  <component> --complete\n\npnpm --filter reka-ui port:coverage \\\n  <component>\n\npnpm --filter reka-ui exec vitest run \\\n  --project=unit <original-file>\n```\n\nEach command answered a different question.\n\n`port:checklist`\n\nDid the port keep every `describe`\n\nand `it`\n\nnode in source order?\n\n`port:parity`\n\nDid it preserve test names, assertion counts, and legitimate quarantines?\n\n`port:coverage`\n\nDid it still reach the same production lines?\n\nA lost line couldn’t be waived for an entire file. The agent had to explain that exact line and connect the exception to a recorded finding.\n\nCoverage gains received the same scrutiny. Automatic Browser Mode cleanup reached teardown code that many jsdom originals never exercised. That was a harness improvement, not proof that Chromium earned the line.\n\n## Step 7: Treat Findings as the Main Output\n\nEvery agent appended evidence to `FINDINGS.tsv`\n\n.\n\nThe ledger recorded:\n\n- the original file\n- the verdict\n- deleted and retained mocks\n- coverage differences\n- measurements, mutations, or source evidence\n- unresolved questions marked as unverified\n\n“Ported cleanly” wasn’t enough.\n\nIf the browser added nothing, the agent had to say what it checked before reaching that conclusion. If it found a bug, the finding had to explain how it reproduced. If it kept a mock, it had to explain why that mock constructed the scenario rather than compensated for jsdom.\n\nThis changed the incentive. The ported test was no longer the only deliverable. The knowledge produced during the port mattered just as much.\n\n## Step 8: Let AI Perform Targeted Mutation Testing\n\nCoverage parity proves that a port reaches the same code. It doesn’t prove that either test would detect broken behavior.\n\nNormally, I’d use Stryker for mutation testing. However, StrykerJS’s official Vitest runner currently [doesn’t support Browser Mode](https://stryker-mutator.io/docs/stryker-js/vitest-runner/).\n\nSo the agents performed targeted mutations themselves:\n\n- Read the behavior named by the test.\n- Introduce one deliberate production defect.\n- Run the Browser Mode test.\n- Run the matching jsdom test.\n- Record whether each suite killed the mutation.\n- Restore the source immediately.\n- Verify a clean diff and green baseline.\n\nFor Slider, the agent removed the call to `setPointerCapture()`\n\n.\n\nThe Browser Mode test failed. The jsdom test stayed green because its own mock claimed pointer capture had succeeded.\n\nThis wasn’t exhaustive mutation testing. It didn’t produce a mutation score. It was hypothesis-driven testing aimed at the exact contract each file claimed to protect.\n\nI previously wrote about this approach in [Mutation Testing with AI Agents When Stryker Doesn’t Work](/posts/mutation-testing-ai-agents-vitest-browser-mode/)Mutation Testing with AI Agents When Stryker Doesn't WorkWhen Stryker doesn't support your test stack, AI agents can execute mutation testing manually. A practical approach for Vitest browser mode and Playwright..\n\n## Step 9: Put the Prompts Under Version Control\n\nThe implementer and reviewer prompts lived in `PORT-PROMPTS.md`\n\n.\n\nThat file had a changelog. When a batch produced a weak port or a false belief, I updated the prompt before running the next batch.\n\nOne early rule claimed that `screen.getBy*`\n\ncouldn’t see portalled content. A later Teleport port disproved it. The agent measured that `screen`\n\nand `page`\n\nreturned the same portalled node.\n\nWe corrected the shared rule before eight overlay components inherited it.\n\nThis became the most transferable idea from the migration:\n\nFix the prompt, not only the generated code.\n\nHand-fixing one port removes one symptom. Updating the prompt removes a class of future mistakes.\n\n## Step 10: Use Bounded Parallelism\n\nOne file per agent doesn’t mean 87 agents at once.\n\nEvery Browser Mode worker starts Chromium. During the first mechanical batch, concurrent coverage runs competed for the browser. One in four identical runs failed before writing a report.\n\nThat failure was dangerous. An agent could interpret missing coverage as a broken port and start “fixing” correct code.\n\nI limited the fan-out to roughly three concurrent agents. Each agent ran only its focused file. The coordinating session ran global validation between batches.\n\nMore agents would’ve increased contention without increasing throughput.\n\n## The Exact Batch Loop\n\nAfter calibration, each batch followed the same process:\n\n- Select files from the inventory.\n- Assign one file to each implementer.\n- Run focused browser, parity, checklist, and coverage checks.\n- Assign independent reviewers with fresh context.\n- Run targeted mutations for important claims.\n- Merge findings into the shared ledger.\n- Update the prompts and migration guide.\n- Regenerate the inventory.\n- Run global parity and both retained suites.\n- Start the next batch only from a green baseline.\n\nThe Git history shows that progression:\n\n| Commit | What changed |\n|---|---|\n`bd93d9b1` |\n\n`f050ec36`\n\n`cb8fd28c`\n\n`675792e3`\n\n`1a02ac8c`\n\n`12d75a3b`\n\nThe setup began on August 16. The completion commit landed on August 18.\n\n## How to Reuse This in Your Project\n\nYou don’t need Reka UI or Vitest to reuse the process.\n\nYou need six things:\n\n**A live baseline.** Keep the original system runnable during the migration.**An inventory.** Give every item an owner, risk tier, and completion state.**Machine oracles.** Compare structure, behavior, and coverage automatically.**Narrow agent ownership.** One file or one coherent unit per implementer.**Independent review.** Don’t let the generating context approve itself.**A feedback loop.** Store prompts in Git and improve them after every failure class.\n\nThe tool is secondary. I used Claude Code sessions, but the pattern works with any coding agent that can edit files and run commands.\n\nFor larger deterministic agent workflows, see [Claude Code Workflows: Deterministic Multi-Agent Orchestration](/posts/claude-code-workflows-deterministic-orchestration/)Claude Code Workflows: Deterministic Multi-Agent OrchestrationWorkflows let you script how Claude Code fans out across dozens of subagents, then synthesizes the results. Here's how they work, the primitives, and the small example I built to understand them..\n\n## What Comes Next\n\nThis post explains how the AI migration worked. It doesn’t answer whether Vitest Browser Mode was worth the effort.\n\nThat deserves a separate post.\n\nIn Part 2, I’ll cover what the paired suites actually proved: which mocks disappeared, which bugs only Chromium caught, where jsdom was already good enough, and what Browser Mode cost.\n\nThe core lesson from Part 1 is simpler:\n\nDon’t use AI as a mass code translator. Build a system where agents propose changes, independent agents challenge them, mutations test their claims, and machines enforce the boundary.\n\nThat’s how I turned 97 test files into a controlled migration instead of 97 opportunities for confidently green mistakes.", "url": "https://wpnews.pro/news/how-i-used-ai-agents-to-migrate-reka-ui-s-tests-to-vitest-browser-mode", "canonical_source": "https://alexop.dev/posts/how-i-used-ai-agents-to-migrate-reka-ui-tests/", "published_at": "2026-08-22 00:00:00+00:00", "updated_at": "2026-08-22 11:14:07.983421+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["Alexander Opalic", "Reka UI", "Vitest", "jsdom", "Chromium", "Vue Test Utils", "Testing Library", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/how-i-used-ai-agents-to-migrate-reka-ui-s-tests-to-vitest-browser-mode", "markdown": "https://wpnews.pro/news/how-i-used-ai-agents-to-migrate-reka-ui-s-tests-to-vitest-browser-mode.md", "text": "https://wpnews.pro/news/how-i-used-ai-agents-to-migrate-reka-ui-s-tests-to-vitest-browser-mode.txt", "jsonld": "https://wpnews.pro/news/how-i-used-ai-agents-to-migrate-reka-ui-s-tests-to-vitest-browser-mode.jsonld"}}