<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Anton Gulin — Test Automation Blog</title><description>Articles about Playwright, TypeScript, test automation best practices, and QA strategy. Written by Anton Gulin, AI QA Architect with 10+ years of experience, including Apple and Fortune 500 teams.</description><link>https://www.anton.qa</link><language>en-us</language><item><title>Your Regression Suite Is a Museum: 5 Questions That Decide Delete vs Keep</title><link>https://www.anton.qa/blog/posts/regression-suite-museum</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/regression-suite-museum</guid><description>Most regression suites only grow. These five questions, with Playwright code for each, tell you which tests earn their place and which are furniture.</description><pubDate>Wed, 19 Aug 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Your Regression Suite Is a Museum: 5 Questions That Decide Delete vs Keep&lt;/h1&gt;
&lt;p&gt;Every sprint adds tests. Almost no sprint deletes one.&lt;/p&gt;
&lt;p&gt;That one-way flow has a predictable end. The suite gets slower. Flaky tests&lt;br&gt;(tests that fail randomly) pile up. Engineers stop trusting red builds.&lt;br&gt;Eventually someone suggests rewriting everything, and the cycle restarts.&lt;/p&gt;
&lt;p&gt;There is a cheaper fix. Audit the suite with five questions.&lt;br&gt;On the last suite I inherited, they retired a third of the tests.&lt;br&gt;Nothing those tests &amp;quot;guarded&amp;quot; ever broke.&lt;/p&gt;
&lt;h2 id=&quot;question-1-when-did-this-test-last-fail-for-a-real-reason&quot;&gt;Question 1: When did this test last fail for a real reason?&lt;/h2&gt;
&lt;p&gt;Your CI history (the record of server test runs) already knows.&lt;/p&gt;
&lt;p&gt;A test that has been green for a year has two possible explanations.&lt;br&gt;Either the code it guards never changed, or the test cannot detect change.&lt;br&gt;Both are worth knowing. Only one deserves compute on every commit.&lt;/p&gt;
&lt;p&gt;Practical rule: pull the last 90 days of runs. Tag every test that failed&lt;br&gt;only for environment reasons, or never failed at all. Those are audit candidates.&lt;/p&gt;
&lt;h2 id=&quot;question-2-what-user-risk-does-it-guard&quot;&gt;Question 2: What user risk does it guard?&lt;/h2&gt;
&lt;p&gt;Write the risk in one sentence, naming a user.&lt;/p&gt;
&lt;p&gt;&amp;quot;It tests the profile page&amp;quot; is not a risk.&lt;br&gt;&amp;quot;A user loses saved work when the session expires&amp;quot; is a risk.&lt;/p&gt;
&lt;p&gt;If nobody on the team can produce that sentence, the test is guarding&lt;br&gt;an implementation detail, not a user. Details change on purpose all the time.&lt;br&gt;Tests guarding them fail on purpose all the time. That is where flakiness lives.&lt;/p&gt;
&lt;h2 id=&quot;question-3-would-anyone-notice-if-it-vanished&quot;&gt;Question 3: Would anyone notice if it vanished?&lt;/h2&gt;
&lt;p&gt;Run the experiment instead of debating it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# in a branch: skip the suspect test, run everything else
npx playwright test --grep-invert &amp;quot;@museum-candidate&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If coverage of the named risk survives through other tests, and no gap&lt;br&gt;appears in the risk list from Question 2, the test was furniture.&lt;/p&gt;
&lt;p&gt;Delete it in the branch. Keep the branch open a week. Merge with confidence.&lt;/p&gt;
&lt;h2 id=&quot;question-4-does-it-check-results-or-steps&quot;&gt;Question 4: Does it check results or steps?&lt;/h2&gt;
&lt;p&gt;A test that clicks through checkout and asserts the button was clickable&lt;br&gt;is a tour, not a test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// a tour: asserts the step happened
await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Pay&amp;#39; }).click();
await expect(page).toHaveURL(/confirmation/);

// a test: asserts the outcome is real
await expect(page.getByTestId(&amp;#39;invoice-total&amp;#39;)).toHaveText(&amp;#39;$34.20&amp;#39;);
await expect(page.getByTestId(&amp;#39;invoice-number&amp;#39;)).not.toBeEmpty();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The URL can change while the invoice ships blank. Assert on the thing&lt;br&gt;the user came for.&lt;/p&gt;
&lt;h2 id=&quot;question-5-can-it-fail-at-all&quot;&gt;Question 5: Can it fail at all?&lt;/h2&gt;
&lt;p&gt;Flip the condition and run it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// original
await expect(status).toBe(&amp;#39;paid&amp;#39;);
// flipped: this MUST fail. If it passes, the test is dead.
await expect(status).not.toBe(&amp;#39;paid&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A test that passes both ways asserts nothing. AI-written tests fail this&lt;br&gt;check more than any other kind, because generators optimize for green.&lt;/p&gt;
&lt;p&gt;Sixty seconds per suspicious test. The flip never lies.&lt;/p&gt;
&lt;h2 id=&quot;what-you-get-back&quot;&gt;What you get back&lt;/h2&gt;
&lt;p&gt;Five questions, one afternoon, on the oldest third of your suite.&lt;/p&gt;
&lt;p&gt;The suite you keep is faster, and every test in it can answer&lt;br&gt;&amp;quot;what breaks if I go red?&amp;quot; That is what makes a red build mean something.&lt;/p&gt;
&lt;p&gt;And when an AI agent starts writing tests into your suite, this audit&lt;br&gt;is the contract you hold its work against. A generated test enters&lt;br&gt;only if it survives the same five questions.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect, the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>[testing</category><category>playwright</category><category>test-automation</category><category>regression-testing</category><category>ai-testing]</category></item><item><title>How to Review AI-Generated Tests: Seven Checks Before You Keep Them</title><link>https://www.anton.qa/blog/posts/review-ai-generated-tests-seven-checks</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/review-ai-generated-tests-seven-checks</guid><description>Use seven plain checks to decide whether an AI-generated test proves a real user risk and deserves a place in your test suite.</description><pubDate>Wed, 12 Aug 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;post&quot;&gt;Post&lt;/h2&gt;
&lt;p&gt;Review AI-generated tests with seven checks. Name the user risk. Break the product. Check the final result. Change the data. Read the failure. Repeat the run. Then decide whether the test could stop a bad release.&lt;/p&gt;
&lt;p&gt;AI can write a clean test in seconds. The file may look finished. The names may sound correct. The test may even pass.&lt;/p&gt;
&lt;p&gt;None of those facts prove value. A useful test catches a failure that matters to users. Your review must find that proof before the test joins your suite.&lt;/p&gt;
&lt;p&gt;I use seven checks for that decision.&lt;/p&gt;
&lt;h2 id=&quot;1-name-the-user-risk&quot;&gt;1. Name the user risk&lt;/h2&gt;
&lt;p&gt;Start with the person who could get hurt. Write the risk in one sentence.&lt;/p&gt;
&lt;p&gt;For example: &amp;quot;A customer sees the wrong total and pays too much.&amp;quot;&lt;/p&gt;
&lt;p&gt;Avoid risks like &amp;quot;checkout may fail.&amp;quot; That sentence does not name the damage. It also gives the test no clear target.&lt;/p&gt;
&lt;p&gt;Ask two questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Who loses something?&lt;/li&gt;
&lt;li&gt;What do they lose?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Money, time, access, and trust are clear answers. &amp;quot;The feature breaks&amp;quot; is not.&lt;/p&gt;
&lt;h2 id=&quot;2-break-the-product&quot;&gt;2. Break the product&lt;/h2&gt;
&lt;p&gt;A test should fail when its protected behavior breaks. Prove that before keeping it.&lt;/p&gt;
&lt;p&gt;AI often writes a test that follows the correct steps. The test may never check the important result.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test(&amp;#39;customer can pay&amp;#39;, async ({ page }) =&amp;gt; {
  await page.goto(&amp;#39;/checkout&amp;#39;)
  await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Pay&amp;#39; }).click()
  await expect(page.getByText(&amp;#39;Success&amp;#39;)).toBeVisible()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This test checks a message. It does not check the charged amount.&lt;/p&gt;
&lt;p&gt;Remove the payment action or return the wrong total. The test must fail. A passing result means the test protects nothing useful.&lt;/p&gt;
&lt;h2 id=&quot;3-check-the-final-result&quot;&gt;3. Check the final result&lt;/h2&gt;
&lt;p&gt;Clicks are steps. Results are proof.&lt;/p&gt;
&lt;p&gt;The test above clicks the right button. A stronger test checks the amount and payment record.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await expect(page.getByTestId(&amp;#39;order-total&amp;#39;)).toHaveText(&amp;#39;$120.00&amp;#39;)
await expect(page.getByTestId(&amp;#39;payment-status&amp;#39;)).toHaveText(&amp;#39;Paid&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An assertion means a result check. Playwright provides &lt;a href=&quot;https://playwright.dev/docs/test-assertions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;assertions that wait for results&lt;/a&gt;. The tool can wait. You still choose the right result.&lt;/p&gt;
&lt;p&gt;Review every assertion. Ask what user outcome it proves. Rewrite assertions that only confirm page activity.&lt;/p&gt;
&lt;h2 id=&quot;4-change-the-data&quot;&gt;4. Change the data&lt;/h2&gt;
&lt;p&gt;AI tends to generate a clean example. Real users bring missing, wrong, and extreme values.&lt;/p&gt;
&lt;p&gt;Test more than one amount. Include zero, a large value, and invalid text.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;for (const amount of [&amp;#39;0&amp;#39;, &amp;#39;999999&amp;#39;, &amp;#39;wrong&amp;#39;]) {
  await page.getByLabel(&amp;#39;Amount&amp;#39;).fill(amount)
  await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Pay&amp;#39; }).click()
  await expect(page.getByRole(&amp;#39;alert&amp;#39;)).toBeVisible()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact values depend on your product. The review question stays simple. Can one neat example hide a serious failure?&lt;/p&gt;
&lt;h2 id=&quot;5-read-the-failure&quot;&gt;5. Read the failure&lt;/h2&gt;
&lt;p&gt;Run the test against a broken result. Then read its message.&lt;/p&gt;
&lt;p&gt;Another engineer should understand the problem without opening the whole file. Compare these messages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Expected: &amp;quot;$20.00&amp;quot;
Received: &amp;quot;$120.00&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Timeout after 30000ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first message points to the product error. The second sends someone searching through logs.&lt;/p&gt;
&lt;p&gt;Use clear result checks and useful test names. A failure should shorten the investigation.&lt;/p&gt;
&lt;h2 id=&quot;6-run-it-twice&quot;&gt;6. Run it twice&lt;/h2&gt;
&lt;p&gt;Use the same input twice. You should get the same result.&lt;/p&gt;
&lt;p&gt;Repeated runs catch shared data and timing problems. They also expose tests that depend on another test.&lt;/p&gt;
&lt;p&gt;Do not accept a passing second run as proof. Compare both runs. Investigate any difference before keeping the test.&lt;/p&gt;
&lt;h2 id=&quot;7-make-the-release-decision&quot;&gt;7. Make the release decision&lt;/h2&gt;
&lt;p&gt;Finish with one question: Would this test stop a bad release?&lt;/p&gt;
&lt;p&gt;Name the release it could stop. For example: &amp;quot;This test blocks checkout when totals are wrong.&amp;quot;&lt;/p&gt;
&lt;p&gt;Keep the test when the answer is clear. Rewrite or delete it when the answer stays vague.&lt;/p&gt;
&lt;p&gt;This step protects the review queue. Teams do not need every generated test. They need the small set that proves important behavior.&lt;/p&gt;
&lt;h2 id=&quot;a-five-minute-review-example&quot;&gt;A five-minute review example&lt;/h2&gt;
&lt;p&gt;Imagine that AI writes a checkout test. The test adds one product and completes payment. It checks the success message.&lt;/p&gt;
&lt;p&gt;First, name the risk. A customer could pay the wrong total.&lt;/p&gt;
&lt;p&gt;Next, change the price calculation. The test still passes because the message appears. You found the missing proof.&lt;/p&gt;
&lt;p&gt;Add a check for the order total. Then try an expired coupon and an empty cart. The test should explain each failed result.&lt;/p&gt;
&lt;p&gt;Run the test twice with the same data. Both runs should match.&lt;/p&gt;
&lt;p&gt;Finish with the release question. This test should stop checkout when the charged total is wrong. The answer now names one clear release failure.&lt;/p&gt;
&lt;p&gt;That review improved one test without adding more code than needed. The team gained useful proof instead of another passing file.&lt;/p&gt;
&lt;h2 id=&quot;use-the-card-during-review&quot;&gt;Use the card during review&lt;/h2&gt;
&lt;p&gt;You can complete this review in a few minutes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Name the user risk.&lt;/li&gt;
&lt;li&gt;Break the protected behavior.&lt;/li&gt;
&lt;li&gt;Check the final result.&lt;/li&gt;
&lt;li&gt;Change the input data.&lt;/li&gt;
&lt;li&gt;Read the failure message.&lt;/li&gt;
&lt;li&gt;Repeat the same run.&lt;/li&gt;
&lt;li&gt;Name the bad release it stops.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;AI can handle test volume. You still decide what deserves trust. Keep tests that fail correctly and explain why the release should stop.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect, the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>ai-testing</category><category>playwright</category><category>test-automation</category><category>code-review</category><category>quality-engineering</category></item><item><title>Test Retries Hide Real Bugs: When a Rerun Helps and When It Lies</title><link>https://www.anton.qa/blog/posts/test-retries-hide-real-bugs</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/test-retries-hide-real-bugs</guid><description>A test that fails, then passes on retry, is not fixed. Two readers explain why, with the race-condition case and a Playwright setup that treats retries as detection.</description><pubDate>Wed, 05 Aug 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;post&quot;&gt;Post&lt;/h2&gt;
&lt;p&gt;Your build is green. One of your tests failed five minutes ago. Both of those things are true, because the test passed on a retry. Here is the short answer. &lt;strong&gt;A retry never fixes anything.&lt;/strong&gt; Keep retries on in CI, but treat a retried pass as a bug report. It opens a ticket. It never closes one.&lt;/p&gt;
&lt;p&gt;Last Saturday I posted about rerunning failed tests in isolation. Two readers pushed back, and both were more right than my post.&lt;/p&gt;
&lt;p&gt;The first said flakiness is a signal. Turning the alarm off does not put out the fire.&lt;/p&gt;
&lt;p&gt;The second was more specific. A team reruns the failed test alone. It passes. Everyone moves on. And the real cause, a race condition, ships to production. A race condition means two things run at the same time and collide.&lt;/p&gt;
&lt;p&gt;This post is the concession, and the setup I now recommend.&lt;/p&gt;
&lt;h2 id=&quot;what-the-rerun-actually-changed&quot;&gt;What the rerun actually changed&lt;/h2&gt;
&lt;p&gt;Look at what happens when a test fails in CI and someone reruns it alone.&lt;/p&gt;
&lt;p&gt;The code did not change. The data did not change. One thing changed: the test ran without the other tests around it.&lt;/p&gt;
&lt;p&gt;So the rerun did not prove the test is fine. It proved the test is fine &lt;strong&gt;when nothing else is running&lt;/strong&gt;. That is a different sentence. Your users do not visit your app one at a time.&lt;/p&gt;
&lt;h2 id=&quot;the-race-that-a-rerun-hides&quot;&gt;The race that a rerun hides&lt;/h2&gt;
&lt;p&gt;Here is the simplest version of the trap:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test(&amp;#39;admin can rename a user&amp;#39;, async () =&amp;gt; {
  await renameUser(&amp;#39;user-42&amp;#39;, &amp;#39;New Name&amp;#39;);
  await expect(profileName).toHaveText(&amp;#39;New Name&amp;#39;);
});

test(&amp;#39;report shows user names&amp;#39;, async () =&amp;gt; {
  const report = await openReport();
  await expect(report.row(&amp;#39;user-42&amp;#39;)).toContainText(&amp;#39;New Name&amp;#39;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both tests touch the same record. Run together, they sometimes collide, and one fails. Run alone, each passes every time.&lt;/p&gt;
&lt;p&gt;The failing run was the only honest one. It was telling you the app has a timing bug. The rerun in isolation deleted the evidence.&lt;/p&gt;
&lt;h2 id=&quot;playwright-already-tells-you-the-truth&quot;&gt;Playwright already tells you the truth&lt;/h2&gt;
&lt;p&gt;Here is the part most teams never look at. When a Playwright test fails and then passes on a retry, the report does not say &amp;quot;passed.&amp;quot;&lt;/p&gt;
&lt;p&gt;It says &lt;strong&gt;flaky&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  3 passed
  1 flaky
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tool is honest. The habit is not. Most dashboards only show green or red, so a flaky pass reads as a pass, and the count nobody reads keeps growing.&lt;/p&gt;
&lt;h2 id=&quot;the-setup-that-keeps-retries-useful&quot;&gt;The setup that keeps retries useful&lt;/h2&gt;
&lt;p&gt;Retries have one honest job: telling infrastructure noise apart from real signal. A container that started slowly is noise. A race condition is signal. You cannot tell them apart without evidence, so collect it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: { trace: &amp;#39;on-first-retry&amp;#39; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two lines, two jobs. The retry detects. The trace records everything about the failing run, so you can diagnose it later instead of shrugging.&lt;/p&gt;
&lt;p&gt;Locally, retries stay at zero. On your own machine you want the failure loud and immediate.&lt;/p&gt;
&lt;h2 id=&quot;the-rule&quot;&gt;The rule&lt;/h2&gt;
&lt;p&gt;A retried pass opens a ticket. It never closes one.&lt;/p&gt;
&lt;p&gt;Once a week, read the flaky list. Every entry is one of two things. Infrastructure noise you should fix in the pipeline. Or a real timing bug in the app that your tests found first. Both are work. Neither is &amp;quot;passed.&amp;quot;&lt;/p&gt;
&lt;p&gt;My Saturday post said rerunning a flaky test in isolation makes things worse. These two readers explained the mechanism better than I did. The rerun does not just waste time. It manufactures false confidence and deletes the only evidence you had.&lt;/p&gt;
&lt;p&gt;The green build is not the goal. The true build is.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect, the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>flaky-tests</category><category>test-retries</category><category>ci-cd</category><category>test-automation</category></item><item><title>Reuse One Page Object Method for Success and Failure Cases</title><link>https://www.anton.qa/blog/posts/reuse-page-object-method-success-and-failure</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/reuse-page-object-method-success-and-failure</guid><description>Your login helper exists twice: one that expects success, one that expects an error. Here is the options-object pattern that keeps one method, with Playwright code.</description><pubDate>Wed, 29 Jul 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;post&quot;&gt;Post&lt;/h2&gt;
&lt;p&gt;Your login helper exists twice. One version expects the dashboard. One version expects the error. Ninety percent of the two bodies is the same code. Here is the short answer. &lt;strong&gt;Keep one method per user action.&lt;/strong&gt; Let an options object say what it waits for. The test still decides what the result means.&lt;/p&gt;
&lt;p&gt;Last week I wrote about &lt;a href=&quot;https://www.anton.qa/blog/posts/where-test-assertions-belong&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;where test assertions belong&lt;/a&gt;. A reader came back with the harder version of the question. If business checks live in the test, how does one method serve both cases? Good question. This post is the answer.&lt;/p&gt;
&lt;h2 id=&quot;the-duplicate-everyone-has&quot;&gt;The duplicate everyone has&lt;/h2&gt;
&lt;p&gt;Open any page object that is more than a year old. You will find this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;async login(user: string, pass: string) {
  await this.username.fill(user);
  await this.password.fill(pass);
  await this.submit.click();
  await this.page.waitForURL(&amp;#39;**/dashboard&amp;#39;);
}

async loginExpectingError(user: string, pass: string) {
  await this.username.fill(user);
  await this.password.fill(pass);
  await this.submit.click();
  await this.errorBanner.waitFor();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two methods. Three identical lines each. One real difference: what the method waits for at the end.&lt;/p&gt;
&lt;p&gt;Now the login form changes. Someone adds a &amp;quot;remember me&amp;quot; checkbox. You fix the first method. You forget the second. The negative test starts failing for a reason unrelated to the negative case.&lt;/p&gt;
&lt;p&gt;That is the cost of the duplicate. Not ugliness. Drift.&lt;/p&gt;
&lt;h2 id=&quot;why-the-boolean-flag-makes-it-worse&quot;&gt;Why the boolean flag makes it worse&lt;/h2&gt;
&lt;p&gt;The first fix everyone tries is a flag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;async login(user: string, pass: string, success: boolean) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the test reads like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await loginPage.login(&amp;#39;anton&amp;#39;, &amp;#39;wrong-password&amp;#39;, false);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;False what? You have to open the page object to find out. And flags multiply. Six months later the same method takes three of them, and nobody can read the call site at all:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await loginPage.login(&amp;#39;anton&amp;#39;, &amp;#39;pw&amp;#39;, false, true, false);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A flag saves a method and costs a reader. Bad trade.&lt;/p&gt;
&lt;h2 id=&quot;copy-the-shape-playwright-already-uses&quot;&gt;Copy the shape Playwright already uses&lt;/h2&gt;
&lt;p&gt;Playwright&amp;#39;s own API solves this everywhere. Every action takes an optional options object with named keys:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await page.getByRole(&amp;#39;button&amp;#39;).click({ timeout: 5000, force: true });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You never pass a bare &lt;code&gt;true&lt;/code&gt; to Playwright. You pass a name and a value. Do the same in your page objects:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type LoginOptions = { waitFor?: &amp;#39;dashboard&amp;#39; | &amp;#39;error&amp;#39; };

async login(user: string, pass: string, options: LoginOptions = {}) {
  const { waitFor = &amp;#39;dashboard&amp;#39; } = options;
  await this.username.fill(user);
  await this.password.fill(pass);
  await this.submit.click();
  if (waitFor === &amp;#39;dashboard&amp;#39;) {
    await this.page.waitForURL(&amp;#39;**/dashboard&amp;#39;);
  } else {
    await this.errorBanner.waitFor();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things make this work. The default keeps the common call short. The named key makes the rare call readable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// happy path, unchanged
await loginPage.login(&amp;#39;anton&amp;#39;, &amp;#39;correct-password&amp;#39;);

// negative path, and you can read it without opening the class
await loginPage.login(&amp;#39;anton&amp;#39;, &amp;#39;wrong-password&amp;#39;, { waitFor: &amp;#39;error&amp;#39; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One method. One place to fix when the form changes.&lt;/p&gt;
&lt;h2 id=&quot;notice-what-the-option-does-not-say&quot;&gt;Notice what the option does not say&lt;/h2&gt;
&lt;p&gt;The option is called &lt;code&gt;waitFor&lt;/code&gt;, not &lt;code&gt;expectSuccess&lt;/code&gt;. That is deliberate.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;expectSuccess: false&lt;/code&gt; puts a judgment inside the page object. The class starts deciding what a correct login looks like. That breaks &lt;a href=&quot;https://www.anton.qa/blog/posts/where-test-assertions-belong&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the rule from last week&lt;/a&gt;: business checks live in the test.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;waitFor: &amp;#39;error&amp;#39;&lt;/code&gt; only says which element the method should wait for before it returns. It is a timing instruction, not a verdict. The verdict stays where a reader expects it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test(&amp;#39;rejects a wrong password&amp;#39;, async ({ loginPage }) =&amp;gt; {
  await loginPage.login(&amp;#39;anton&amp;#39;, &amp;#39;wrong-password&amp;#39;, { waitFor: &amp;#39;error&amp;#39; });
  await expect(loginPage.errorBanner).toHaveText(&amp;#39;Wrong password&amp;#39;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read that test aloud. It says what it does and what should be true. You never open the page object.&lt;/p&gt;
&lt;h2 id=&quot;when-two-methods-really-are-right&quot;&gt;When two methods really are right&lt;/h2&gt;
&lt;p&gt;Sometimes the duplicate is not a duplicate. Keep separate methods when the steps themselves differ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;async loginWithPassword(user: string, pass: string) { ... }
async loginWithSso(user: string) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Different fields, different clicks, different waits. These are two user actions that happen to end in the same place.&lt;/p&gt;
&lt;p&gt;The test: if the bodies differ by more than the final wait, keep them apart. If they differ only by the final wait, merge them.&lt;/p&gt;
&lt;p&gt;And name methods after the action, never after the outcome. &lt;code&gt;loginExpectingError&lt;/code&gt; names a result. &lt;code&gt;login&lt;/code&gt; names what the user does. Results belong in the test name.&lt;/p&gt;
&lt;h2 id=&quot;the-rule-in-three-lines&quot;&gt;The rule, in three lines&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;One method per user action.&lt;/li&gt;
&lt;li&gt;An options object says what it waits for, with a default for the common case.&lt;/li&gt;
&lt;li&gt;The test says what the result means.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Apply it once to your login helper. Then look at your checkout helper, your search helper, and your upload helper. The same pair is usually hiding in all of them.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect, the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>page-object-model</category><category>test-automation</category><category>typescript</category><category>code-reuse</category></item><item><title>Should Page Objects Assert? Where Test Assertions Belong</title><link>https://www.anton.qa/blog/posts/where-test-assertions-belong</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/where-test-assertions-belong</guid><description>Should page objects contain assertions? A practical rule: business checks live in tests, technical guards live in page objects. With Playwright code.</description><pubDate>Wed, 22 Jul 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;post&quot;&gt;Post&lt;/h2&gt;
&lt;p&gt;Should a page object contain assertions? Here is the short answer: &lt;strong&gt;business assertions belong in the test. Technical guards belong in the page object.&lt;/strong&gt; A test must show what &amp;quot;correct&amp;quot; means for its scenario. A page object must only promise that the page is ready to use. Mixing the two is why suites become hard to read and hard to trust.&lt;/p&gt;
&lt;p&gt;Last week I published &lt;a href=&quot;https://www.anton.qa/blog/posts/modern-page-object-model&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the modern page object model&lt;/a&gt;. One sign of legacy design was &amp;quot;page objects that assert.&amp;quot; A senior QA engineer pushed back in the comments. His position: business checks go in the test layer, but technical checks, did the page reach the right state?, can hide inside the class.&lt;/p&gt;
&lt;p&gt;It was a fair challenge. This post is the full answer.&lt;/p&gt;
&lt;h2 id=&quot;the-two-kinds-of-checks&quot;&gt;The two kinds of checks&lt;/h2&gt;
&lt;p&gt;Every check in a UI test is one of two kinds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Business checks&lt;/strong&gt; answer: did the product do the right thing? The order total is $41.97. The welcome message names the user. The discount applied.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technical guards&lt;/strong&gt; answer: is the page ready? The form finished loading. The spinner went away. The URL changed.&lt;/p&gt;
&lt;p&gt;They look similar in code. They serve different readers. A business check speaks to the person deciding &amp;quot;is this feature broken?&amp;quot; A technical guard speaks to the machine deciding &amp;quot;can I click now?&amp;quot;&lt;/p&gt;
&lt;h2 id=&quot;the-rule&quot;&gt;The rule&lt;/h2&gt;
&lt;p&gt;Put business checks in the test, always:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test(&amp;#39;applies the discount&amp;#39;, async ({ checkoutPage }) =&amp;gt; {
  await checkoutPage.applyCode(&amp;#39;SAVE10&amp;#39;);
  await expect(checkoutPage.total).toHaveText(&amp;#39;$35.97&amp;#39;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The expected value sits in the test file. When this fails at 2 a.m., the reader sees what &amp;quot;correct&amp;quot; was supposed to be. No file jumping.&lt;/p&gt;
&lt;p&gt;Handle technical guards inside the page object, but prefer waiting over asserting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export class CheckoutPage {
  readonly total: Locator;

  async applyCode(code: string) {
    await this.codeInput.fill(code);
    await this.applyButton.click();
    await this.priceUpdate.waitFor({ state: &amp;#39;visible&amp;#39; });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The page object does not judge the total. It makes one promise: when &lt;code&gt;applyCode&lt;/code&gt; returns, the page finished reacting. That is a technical guard, and note it is a &lt;em&gt;wait&lt;/em&gt;, not an &lt;em&gt;assert&lt;/em&gt;. Playwright&amp;#39;s web-first assertions and auto-waiting handle most of these guards for free.&lt;/p&gt;
&lt;h2 id=&quot;why-hidden-business-asserts-hurt&quot;&gt;Why hidden business asserts hurt&lt;/h2&gt;
&lt;p&gt;Three costs show up at scale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The expected value disappears.&lt;/strong&gt; &lt;code&gt;checkoutPage.verifyTotal()&lt;/code&gt; hides $35.97 in another file. The failing test cannot tell you what it believed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The page object takes sides.&lt;/strong&gt; Fifty tests share that class. One scenario needs a different expected total, and the shared method becomes a maze of parameters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failures point at the wrong layer.&lt;/strong&gt; When an assert fires inside a page object, the stack trace blames plumbing. The reader has to dig to find which business rule broke.&lt;/p&gt;
&lt;h2 id=&quot;where-it-gets-honestly-debatable&quot;&gt;Where it gets honestly debatable&lt;/h2&gt;
&lt;p&gt;My commenter&amp;#39;s position (hidden technical assertions are fine) is workable. Plenty of strong suites do it. My preference is stricter for one reason: an assert stops the test with a verdict; a wait just holds the door. Verdicts belong to tests. But if your team hides technical guards as asserts and everyone can read the failures, that is a style choice, not a defect.&lt;/p&gt;
&lt;p&gt;What is a defect: expected business values living anywhere except the test body.&lt;/p&gt;
&lt;h2 id=&quot;the-migration-path&quot;&gt;The migration path&lt;/h2&gt;
&lt;p&gt;Same as the &lt;a href=&quot;https://www.anton.qa/blog/posts/modern-page-object-model&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;POM modernization&lt;/a&gt;: no big rewrite. When a change touches a page object that asserts, move the business expectation up into the tests that call it, and convert the technical remainder into a wait. Each class takes minutes.&lt;/p&gt;
&lt;p&gt;Run this to find your candidates:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep -rn &amp;quot;expect(&amp;quot; src/pages/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every hit is either a business check to promote or a guard to convert.&lt;/p&gt;
&lt;h2 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Business checks: in the test, expected values written out.&lt;/li&gt;
&lt;li&gt;Technical guards: in the page object, as waits, not verdicts.&lt;/li&gt;
&lt;li&gt;Playwright&amp;#39;s auto-waiting already covers most guards, delete before you migrate.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect, the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>page-object-model</category><category>assertions</category><category>test-automation</category><category>typescript</category></item><item><title>The Modern Page Object Model: Less Shared Code, Easier Changes</title><link>https://www.anton.qa/blog/posts/modern-page-object-model</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/modern-page-object-model</guid><description>What a page object model is, five outdated habits, and a simpler Playwright pattern with less shared code.</description><pubDate>Wed, 15 Jul 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A &lt;strong&gt;page object model (POM)&lt;/strong&gt; organizes browser tests by app page. Each page or component gets one class. The class stores element finders, called locators. It also stores user actions. Tests call those actions instead of using selectors directly. When a page changes, you fix one class. You do not fix two hundred tests.&lt;/p&gt;
&lt;p&gt;The basic idea is old. Modern Playwright changes how we should build it.&lt;/p&gt;
&lt;p&gt;Many page objects still follow Selenium tutorials from 2015. They work, but they ignore useful Playwright features. Here are five signs and the simpler pattern that replaces them.&lt;/p&gt;
&lt;h2 id=&quot;sign-1-every-page-object-extends-basepage&quot;&gt;Sign 1: Every page object extends &lt;code&gt;BasePage&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export class LoginPage extends BasePage {
  constructor(driver: Driver) { super(driver); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inheritance means one class receives behavior from a parent class. The 2015 logic made every page share one parent. That parent often becomes a junk drawer. It collects waits, logs, screenshots, and unused helpers.&lt;/p&gt;
&lt;p&gt;Inheritance ties every page to the parent class. Modern page objects need less shared code. Shared helpers can live in plain functions. Test setup can live in fixtures, which prepare objects for each test.&lt;/p&gt;
&lt;h2 id=&quot;sign-2-wait-methods-everywhere&quot;&gt;Sign 2: Wait methods everywhere&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await loginPage.waitForPageToLoad();
await loginPage.waitForSpinnerToDisappear();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Playwright checks elements before many user actions. For &lt;code&gt;click&lt;/code&gt;, it checks visibility, stability, events, and enabled state. Many manual wait methods only repeat those checks. They can also hide timing bugs.&lt;/p&gt;
&lt;p&gt;Wait explicitly only for conditions Playwright cannot infer. One example is a dashboard finishing a calculation. Use an assertion, which checks an expected result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await expect(total).toHaveText(&amp;#39;41.97&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;sign-3-locators-buried-inside-methods&quot;&gt;Sign 3: Locators buried inside methods&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;async login(user: string, pass: string) {
  await this.page.locator(&amp;#39;#username&amp;#39;).fill(user);
  await this.page.locator(&amp;#39;#password&amp;#39;).fill(pass);
  await this.page.locator(&amp;#39;button[type=submit]&amp;#39;).click();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hidden locators make page dependencies hard to see. Declare each locator once in the constructor:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export class LoginPage {
  readonly username: Locator;
  readonly password: Locator;
  readonly submit: Locator;

  constructor(readonly page: Page) {
    this.username = page.getByLabel(&amp;#39;Username&amp;#39;);
    this.password = page.getByLabel(&amp;#39;Password&amp;#39;);
    this.submit = page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Sign in&amp;#39; });
  }

  async login(user: string, pass: string) {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.submit.click();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now every locator is visible at a glance. The code also uses user-facing names through &lt;code&gt;getByRole&lt;/code&gt; and &lt;code&gt;getByLabel&lt;/code&gt;. Those names often survive redesigns. A CSS selector like &lt;code&gt;#username&lt;/code&gt; may not.&lt;/p&gt;
&lt;h2 id=&quot;sign-4-tests-build-their-own-page-objects&quot;&gt;Sign 4: Tests build their own page objects&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test(&amp;#39;login works&amp;#39;, async ({ page }) =&amp;gt; {
  const loginPage = new LoginPage(page);   // every test, every file
  ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This setup repeats in every test. A Playwright fixture prepares the page object once:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// fixtures.ts
import { test as base } from &amp;#39;@playwright/test&amp;#39;;
import { LoginPage } from &amp;#39;./pages/login-page&amp;#39;;

export const test = base.extend&amp;lt;{ loginPage: LoginPage }&amp;gt;({
  loginPage: async ({ page }, use) =&amp;gt; {
    await use(new LoginPage(page));
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// login.spec.ts
import { test } from &amp;#39;./fixtures&amp;#39;;

test(&amp;#39;login works&amp;#39;, async ({ loginPage }) =&amp;gt; {
  await loginPage.login(&amp;#39;anton&amp;#39;, &amp;#39;secret&amp;#39;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test asks for &lt;code&gt;loginPage&lt;/code&gt; and receives a ready object. There is no repeated setup. One fixture can also depend on another fixture.&lt;/p&gt;
&lt;h2 id=&quot;sign-5-page-objects-that-assert&quot;&gt;Sign 5: Page objects that assert&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;loginPage.verifyDashboardIsCorrect()&lt;/code&gt; method lets the page object define &amp;quot;correct.&amp;quot; The expected result now hides inside a shared class.&lt;/p&gt;
&lt;p&gt;Modern split: &lt;strong&gt;page objects act, tests assert.&lt;/strong&gt; The page object returns locators or values. The test states the expectation in plain sight:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await loginPage.login(&amp;#39;anton&amp;#39;, &amp;#39;secret&amp;#39;);
await expect(dashboard.greeting).toHaveText(&amp;#39;Welcome, Anton&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When this fails, the expected value is visible in the test. You know what broke.&lt;/p&gt;
&lt;h2 id=&quot;the-migration-path-no-rewrite-required&quot;&gt;The migration path (no rewrite required)&lt;/h2&gt;
&lt;p&gt;You do not need a full rewrite:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;New page objects follow the modern shape from day one.&lt;/li&gt;
&lt;li&gt;When a test touches an old class, upgrade only that class.&lt;/li&gt;
&lt;li&gt;Delete &lt;code&gt;extends BasePage&lt;/code&gt; and move locators into the constructor.&lt;/li&gt;
&lt;li&gt;Delete duplicate wait methods and move assertions into tests.&lt;/li&gt;
&lt;li&gt;Add fixtures early. New tests get clean setup while old tests keep working.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;do-ai-agents-change-any-of-this&quot;&gt;Do AI agents change any of this?&lt;/h2&gt;
&lt;p&gt;They make this structure more important. Page objects give an AI agent a small list of approved actions. Raw selectors make the agent rediscover each page. That creates fragile tests. Calling &lt;code&gt;loginPage.login()&lt;/code&gt; uses the same reviewed action as a human-written test.&lt;/p&gt;
&lt;p&gt;Small, flat page objects give machines fewer ways to make mistakes. Large parent classes give them more choices.&lt;/p&gt;
&lt;h2 id=&quot;the-pattern-in-one-line&quot;&gt;The pattern in one line&lt;/h2&gt;
&lt;p&gt;Declare locators in the constructor, user-facing. Let fixtures do the wiring. Let auto-waiting do the waiting. Keep assertions in tests. Share nothing through inheritance.&lt;/p&gt;
&lt;p&gt;Your page objects should be the most boring code you own. That is what makes them last another ten years.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect &amp;amp;#8212 the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>page-object-model</category><category>test-automation</category><category>typescript</category><category>qa-architecture</category></item><item><title>Your AI Model Is a Dependency: Pin It, Keep a Fallback, Re-Verify</title><link>https://www.anton.qa/blog/posts/your-ai-model-is-a-dependency</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/your-ai-model-is-a-dependency</guid><description>An AI model can change or vanish under your test suite overnight. The three-rule discipline — pin the version, keep a validated fallback, re-run the same suite on both — explained with the 19-day Fable 5 outage as the case study.</description><pubDate>Wed, 08 Jul 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Nineteen days. That is how long one of the world&amp;#39;s best AI models was simply gone this summer.&lt;/p&gt;
&lt;p&gt;Claude Fable 5 went offline on June 12 by government order. It came back on July 1. Between those dates, every test suite that leaned on it had a problem. Some teams fixed it in one line. Others lost days, twice.&lt;/p&gt;
&lt;p&gt;The difference was not luck. It was whether they treated the model like what it is: a dependency.&lt;/p&gt;
&lt;h2 id=&quot;what-quotthe-model-is-a-dependencyquot-actually-means&quot;&gt;What &amp;quot;the model is a dependency&amp;quot; actually means&lt;/h2&gt;
&lt;p&gt;Your test suite already has dependencies. A database version. A browser version. A Node version. You pin them all. (Pinning means locking the exact version so nothing upgrades by itself.)&lt;/p&gt;
&lt;p&gt;An AI model is the same kind of moving part, with three extra ways to hurt you:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It changes silently.&lt;/strong&gt; Providers update models behind the same name. The model you tested in March is not always the model answering in July.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It disappears.&lt;/strong&gt; Deprecations happen every quarter. And as of this summer, so do government-ordered shutdowns. Fable 5 was not deprecated — it was switched off overnight, worldwide.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It gets swapped without telling you.&lt;/strong&gt; The fine print of the Fable 5 return says blocked requests are rerouted to a different model (Opus 4.8). Your pipeline asks one brain and sometimes gets another.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A test that stands on a part like this, unpinned and unwatched, is not a test. It is a hope.&lt;/p&gt;
&lt;h2 id=&quot;rule-1-pin-the-version&quot;&gt;Rule 1 — Pin the version&lt;/h2&gt;
&lt;p&gt;Never let a test suite float on &amp;quot;latest&amp;quot; or on a bare model family name. Point it at the exact model ID, in one place.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// config/models.ts — ONE place the whole suite imports from
export const MODELS = {
  primary: &amp;quot;claude-fable-5&amp;quot;,      // pinned: the exact ID we validated
  fallback: &amp;quot;claude-opus-4-8&amp;quot;,    // pinned: the exact ID we validated
} as const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every agent, every AI-assisted test imports from this file. Nothing names a model directly. When the world changes, you edit one line, not forty files.&lt;/p&gt;
&lt;p&gt;If you use an agent framework, the same rule applies to its config. In Stagehand, for example, the model is an explicit setting — set it, do not rely on an implicit choice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const stagehand = new Stagehand({
  modelName: MODELS.primary,   // never omit this
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;rule-2-keep-a-fallback-you-have-already-validated&quot;&gt;Rule 2 — Keep a fallback you have already validated&lt;/h2&gt;
&lt;p&gt;A fallback you pick during the outage is not a fallback. It is a gamble made under pressure.&lt;/p&gt;
&lt;p&gt;Pick the second model now, on a calm day. The bar is simple: it must run your real suite acceptably. Not &amp;quot;it is a good model&amp;quot; — &lt;em&gt;your suite, green, at a cost you accept.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Write it into the same config file (see &lt;code&gt;MODELS.fallback&lt;/code&gt; above). One line to switch, pre-approved, pre-tested.&lt;/p&gt;
&lt;h2 id=&quot;rule-3-re-run-the-same-suite-on-both&quot;&gt;Rule 3 — Re-run the same suite on both&lt;/h2&gt;
&lt;p&gt;This is the rule most teams skip, and it is the one that makes the other two real.&lt;/p&gt;
&lt;p&gt;A fallback is only valid while it stays valid. Models drift. So, on a schedule, run your same test suite against both models and compare:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// parity.spec.ts — the same checks, both brains
for (const model of [MODELS.primary, MODELS.fallback]) {
  test(`checkout flow passes on ${model}`, async () =&amp;gt; {
    const agent = makeAgent(model);
    await agent.run(&amp;quot;complete checkout for the seeded cart&amp;quot;);
    // The fixed check the agent cannot move — same for both models:
    expect(await getOrderTotal()).toBe(41.97);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details carry this pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The assert is model-independent.&lt;/strong&gt; The known answer (41.97 for a seeded cart) comes from your test data, not from the model. Either brain must reach the same truth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run it weekly, not &amp;quot;someday.&amp;quot;&lt;/strong&gt; A parity suite that ran last night makes a model switch a non-event. A parity suite that never runs makes it an emergency.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-this-cost-the-two-kinds-of-teams&quot;&gt;What this cost the two kinds of teams&lt;/h2&gt;
&lt;p&gt;When Fable 5 went dark, the pinned teams with a validated fallback did this: change one line, run the suite, ship. When it came back July 1, they did it again in reverse. Two boring mornings.&lt;/p&gt;
&lt;p&gt;The floating teams debugged broken builds twice in three weeks — once out, once back. Same event, same tools. The whole difference was written before the crisis: one config file, one spare model, one recurring suite.&lt;/p&gt;
&lt;h2 id=&quot;the-checklist&quot;&gt;The checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Every model reference goes through one config file&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Model IDs are exact and pinned — no &amp;quot;latest&amp;quot;, no bare family names&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; A fallback model is chosen, and it has passed your real suite&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; A parity suite runs both models on a schedule&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; A model switch is documented as a one-line change anyone can make&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You would not float your database version in production. Your AI model earns the same respect — it is infrastructure now, and this summer proved it can vanish like infrastructure too.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>ai-testing</category><category>test-automation</category><category>qa-architecture</category><category>playwright</category><category>ai-agents</category></item><item><title>How to Implement AI in QA (2026): A Practical Framework</title><link>https://www.anton.qa/blog/posts/how-to-implement-ai-in-qa</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/how-to-implement-ai-in-qa</guid><description>Where AI helps in QA, where it lies, and the one rule that keeps AI tests trustworthy — the AI does the work, a fixed check decides. With a working example.</description><pubDate>Wed, 01 Jul 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to Implement AI in QA (2026): A Practical Framework&lt;/h1&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h2&gt;
&lt;p&gt;To implement AI in QA, let AI do the work and let a fixed check decide the result. Use AI to generate tests, repair broken selectors, and explore your app. Never let AI grade its own output. Add an independent check the AI cannot change, test for repeatable results, and test that the agent did only what you asked.&lt;/p&gt;
&lt;h2 id=&quot;what-quotai-in-qaquot-actually-means&quot;&gt;What &amp;quot;AI in QA&amp;quot; actually means&lt;/h2&gt;
&lt;p&gt;AI in QA means using an AI model to help test software. That is the whole idea. The model can write test cases, fix tests that broke, click through your app like a user, or read a failure and guess the cause.&lt;/p&gt;
&lt;p&gt;It does not mean the AI replaces testing. It means the AI does some of the work a tester used to do by hand. The judgment stays with you.&lt;/p&gt;
&lt;p&gt;I test software for a living. The teams that win with AI in QA all draw the same line. AI does the work. A human, and a fixed check, decide if the work is good.&lt;/p&gt;
&lt;h2 id=&quot;the-one-rule-that-keeps-it-safe&quot;&gt;The one rule that keeps it safe&lt;/h2&gt;
&lt;p&gt;Here is the rule the rest of this guide hangs on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Let AI do the work. Never let AI judge its own work.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Picture an AI agent that tests your login. It clicks around. It reports green. Everyone relaxes. But the agent decided what &amp;quot;pass&amp;quot; means. If it is too kind, it passes a broken page. Now you shipped a bug with a green check on top.&lt;/p&gt;
&lt;p&gt;An AI that grades its own work is not a test. It is an opinion.&lt;/p&gt;
&lt;p&gt;So you give the AI room to explore, and you keep one thing it cannot touch. A fixed check. A known-good answer. A hard assert (a check that fails loudly) on the real outcome. The agent finds the path. The fixed check says pass or fail.&lt;/p&gt;
&lt;p&gt;In testing this fixed answer has a name: an oracle. The oracle is the part the system being tested is not allowed to influence. Keep your oracle out of the AI&amp;#39;s reach and most AI-in-QA risk goes away.&lt;/p&gt;
&lt;h2 id=&quot;where-ai-helps-in-qa-the-4-good-jobs&quot;&gt;Where AI helps in QA (the 4 good jobs)&lt;/h2&gt;
&lt;p&gt;These four jobs are where AI pays off today.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Writing tests.&lt;/strong&gt; Point the model at a page or a user story. It drafts test cases, including edge cases a tired human skips. You review and keep the good ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixing broken tests.&lt;/strong&gt; A button moved and the test broke. AI can find the new selector (how a test finds a button) and propose the fix. This is the biggest time-saver for most teams.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exploring the app.&lt;/strong&gt; An AI agent can wander your app like a curious user and report what feels broken. Great for finding the bug nobody wrote a test for.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading failures.&lt;/strong&gt; When a test fails, AI can read the log and the trace and suggest the likely cause. It turns a wall of red into a short list to check.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In all four, the AI proposes. You and your fixed checks dispose.&lt;/p&gt;
&lt;h2 id=&quot;where-ai-lies-in-qa-the-4-traps&quot;&gt;Where AI lies in QA (the 4 traps)&lt;/h2&gt;
&lt;p&gt;This is the part most guides skip. AI in QA fails in four ways. Plan for each.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It passes a broken thing.&lt;/strong&gt; A too-kind agent calls a broken page &amp;quot;fine.&amp;quot; Fix: an independent oracle the agent cannot move.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is not repeatable.&lt;/strong&gt; The same input passes now and fails in ten minutes. Fix: run the same input twice and compare the shape of the answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It does too much.&lt;/strong&gt; You asked for one thing. The agent also changed a setting or sent a message. Fix: a scope check on what it touched.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It depends on a model that can change.&lt;/strong&gt; The model under your tool updates, or even goes offline, and your tests shift with it. Fix: pin the model version and keep a fallback.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last one is not theoretical. In June 2026 a widely used model was pulled offline overnight. Teams that pinned their model switched to a fallback in one line. Teams that did not found out when their build broke.&lt;/p&gt;
&lt;h2 id=&quot;a-working-example-an-ai-agent-with-an-oracle-it-cannot-move&quot;&gt;A working example: an AI agent with an oracle it cannot move&lt;/h2&gt;
&lt;p&gt;Here is the pattern in code. An AI agent books a meeting room. Then three fixed checks decide if it really worked. The agent never grades itself.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;
import { Stagehand } from &amp;#39;@browserbasehq/stagehand&amp;#39;;

test(&amp;#39;AI books a room — and only that&amp;#39;, async ({ page }) =&amp;gt; {
  // Pin the model. Do not let it auto-upgrade under your tests.
  const stage = new Stagehand({ env: &amp;#39;LOCAL&amp;#39;, model: &amp;#39;anthropic/claude-opus-4-8&amp;#39; });
  await stage.init();

  // 1) Let the AI do the work. It decides HOW to book the room.
  await stage.act(&amp;#39;Book room B for 2pm tomorrow, for 30 minutes&amp;#39;);

  // 2) The fixed check the AI cannot move (the oracle).
  //    These helpers read your real database, not the agent&amp;#39;s report.
  const booking = await getBookingFromDb({ room: &amp;#39;B&amp;#39;, time: &amp;#39;14:00&amp;#39; });
  expect(booking).toBeTruthy();          // it did the task
  expect(booking.durationMin).toBe(30);  // exactly what we asked for

  // 3) Scope check. Did it touch anything it should not have?
  const otherChanges = await getChangesExcept(booking.id);
  expect(otherChanges).toHaveLength(0);  // no surprise side effects
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the three checks again. The agent&amp;#39;s own &amp;quot;I booked it&amp;quot; is never trusted. The database is the oracle. The duration check catches a sloppy booking. The scope check catches the agent doing extra. (&lt;code&gt;getBookingFromDb&lt;/code&gt; and &lt;code&gt;getChangesExcept&lt;/code&gt; are your own helpers — they read real state, not the agent&amp;#39;s words.)&lt;/p&gt;
&lt;p&gt;To catch the repeatable-result trap, run the same prompt twice and compare:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test(&amp;#39;same request, same result&amp;#39;, async () =&amp;gt; {
  const a = await runBooking(&amp;#39;Book room B for 2pm tomorrow, 30 minutes&amp;#39;);
  const b = await runBooking(&amp;#39;Book room B for 2pm tomorrow, 30 minutes&amp;#39;);
  // The wording of the agent&amp;#39;s reply may differ. The outcome may not.
  expect(a.room).toBe(b.room);
  expect(a.durationMin).toBe(b.durationMin);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model is allowed to phrase its answer differently each time. It is not allowed to book a different room.&lt;/p&gt;
&lt;h2 id=&quot;the-3-tests-every-ai-feature-needs&quot;&gt;The 3 tests every AI feature needs&lt;/h2&gt;
&lt;p&gt;If you ship an AI feature to users, these three tests catch the failures that page you at 2am. Most teams only write the first easy one (&amp;quot;does it give a good answer?&amp;quot;).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The wrong-input test.&lt;/strong&gt; Feed it junk, empty fields, another language, a user trying to break it. A good feature fails safely. A bad one is confidently wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The same-input-twice test.&lt;/strong&gt; Run the exact input twice. Same kind of answer? Different wording is fine. Pass-then-fail is not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The scope test.&lt;/strong&gt; Did it do only what you asked? Or did it also change a setting, send a message, or touch a file? Extra is not helpful. Extra is a future incident.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-to-start-on-monday&quot;&gt;Where to start on Monday&lt;/h2&gt;
&lt;p&gt;You do not need a platform or a budget. Start small.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pick one flaky test. Let an AI tool propose the fix. You keep final say.&lt;/li&gt;
&lt;li&gt;Add one oracle. Take your most important flow and add a hard check on the real outcome, not the agent&amp;#39;s report.&lt;/li&gt;
&lt;li&gt;Pin your model. Lock the version your AI tools use. Add a fallback.&lt;/li&gt;
&lt;li&gt;Add the scope check to one agent run. See what it touches when you are not looking.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Do those four and you have AI in QA that you can trust. The AI does more work. You keep the judgment. The fixed checks keep everyone honest.&lt;/p&gt;
&lt;p&gt;That is the whole job: the gap between &amp;quot;the AI says it passed&amp;quot; and &amp;quot;it passed, for the right reason.&amp;quot;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>ai-qa</category><category>ai-testing</category><category>test-automation</category><category>qa</category><category>playwright</category></item><item><title>How to Test Passkey (WebAuthn) Login in Playwright (2026)</title><link>https://www.anton.qa/blog/posts/test-passkey-login-playwright</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/test-passkey-login-playwright</guid><description>Playwright 1.61 added a virtual authenticator. You can now test passkey login with no hardware key, in every browser. Here is a complete working test.</description><pubDate>Fri, 26 Jun 2026 03:24:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to Test Passkey (WebAuthn) Login in Playwright (2026)&lt;/h1&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h2&gt;
&lt;p&gt;You can now test passkey login in Playwright with no hardware key. Playwright 1.61 added a virtual authenticator (a fake security key). Your test seeds a passkey, turns it on, and the page signs in as if a real key answered. It works in every browser and runs in CI. The API is &lt;code&gt;browserContext.credentials&lt;/code&gt;, with three methods: &lt;code&gt;create()&lt;/code&gt;, &lt;code&gt;install()&lt;/code&gt;, and &lt;code&gt;get()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-is-a-passkey-in-plain-words&quot;&gt;What is a passkey, in plain words&lt;/h2&gt;
&lt;p&gt;A passkey is a login with no password. You sign in with your face, your fingerprint, or a device PIN. The hard part lives in your device. The website only sees a signed reply.&lt;/p&gt;
&lt;p&gt;The browser standard behind this is called WebAuthn. It means &amp;quot;web authentication&amp;quot;. When you log in, the browser runs a small back-and-forth with the site. The site asks. Your device answers and signs. This is the part that used to need real hardware.&lt;/p&gt;
&lt;p&gt;Apple, Google, and most banks ship passkeys now. If your app has a &amp;quot;sign in with a passkey&amp;quot; button, you have this flow in production today.&lt;/p&gt;
&lt;h2 id=&quot;why-nobody-tested-this-flow&quot;&gt;Why nobody tested this flow&lt;/h2&gt;
&lt;p&gt;Here is the part nobody talks about. Almost nobody tests the passkey login.&lt;/p&gt;
&lt;p&gt;For years it was hard. To test a passkey you needed a real security key plugged into the machine. You cannot plug a USB key into a CI server. CI is a remote build machine with no hands and no ports.&lt;/p&gt;
&lt;p&gt;So the most important login flow shipped untested. The one thing a user does first. The one thing that locks them out if it breaks.&lt;/p&gt;
&lt;p&gt;I test software for a living. An untested login is the scariest gap on the list. If sign-in breaks, nothing else matters. The cart, the dashboard, the settings page, all of it sits behind the door. On one project I watched a broken auth path block every other test for two days. The fix took ten minutes. Finding it took the two days.&lt;/p&gt;
&lt;p&gt;Playwright 1.61 closed this gap.&lt;/p&gt;
&lt;h2 id=&quot;what-playwright-161-added&quot;&gt;What Playwright 1.61 added&lt;/h2&gt;
&lt;p&gt;Playwright 1.61 shipped on June 15, 2026. It added a virtual authenticator (a fake security key).&lt;/p&gt;
&lt;p&gt;A virtual authenticator is software that pretends to be a hardware key. Your test creates one. It seeds a passkey into it. From then on, when the page calls the browser to sign in, Playwright answers for the key. No real device. No USB port. The page cannot tell the difference.&lt;/p&gt;
&lt;p&gt;You reach it through a new class called &lt;code&gt;Credentials&lt;/code&gt;, on the browser context: &lt;code&gt;browserContext.credentials&lt;/code&gt;. It has three methods you will use most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;create()&lt;/code&gt; seeds a test passkey for a site.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;install()&lt;/code&gt; turns the virtual key on for the page.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get()&lt;/code&gt; reads back any passkey the page registered, so you can save it and reuse it later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This works in all three browser engines Playwright drives. So one test covers Chromium, Firefox, and WebKit.&lt;/p&gt;
&lt;h2 id=&quot;how-to-test-passkey-login-a-working-example&quot;&gt;How to test passkey login — a working example&lt;/h2&gt;
&lt;p&gt;Here is a complete test. It seeds a passkey, turns on the virtual key, then signs in. Read the comments for what each step does.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// passkey-login.spec.ts
// Tested against Playwright 1.61. Run with: npx playwright test
import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test(&amp;#39;user signs in with a passkey&amp;#39;, async ({ browser }) =&amp;gt; {
  // A fresh, clean browser session for this test.
  const context = await browser.newContext();

  // STEP 1 — Seed a passkey for our site.
  // &amp;#39;example.com&amp;#39; is the site domain (the &amp;quot;relying party id&amp;quot;).
  // With only the domain, Playwright makes a fresh key for us.
  await context.credentials.create(&amp;#39;example.com&amp;#39;);

  // STEP 2 — Turn the virtual key on.
  // From now on, the page&amp;#39;s sign-in calls are answered by our key,
  // not by real hardware. Call this before the page loads.
  await context.credentials.install();

  // STEP 3 — Let the page use it.
  const page = await context.newPage();
  await page.goto(&amp;#39;https://example.com/login&amp;#39;);

  // The page calls the browser to sign in. Our key answers.
  await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Sign in with a passkey&amp;#39; }).click();

  // Check the user is in.
  await expect(page.getByText(&amp;#39;Welcome back&amp;#39;)).toBeVisible();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The three steps map to the three method calls. First you &lt;code&gt;create()&lt;/code&gt; a passkey for your site. Then you &lt;code&gt;install()&lt;/code&gt; the virtual key, which makes the page&amp;#39;s sign-in calls run through it. Then the page does its normal login, and your key answers in place of hardware.&lt;/p&gt;
&lt;p&gt;One note on order. Call &lt;code&gt;install()&lt;/code&gt; before the page touches sign-in. The virtual key only answers calls that happen after you turn it on.&lt;/p&gt;
&lt;h2 id=&quot;re-using-a-passkey-across-tests&quot;&gt;Re-using a passkey across tests&lt;/h2&gt;
&lt;p&gt;Often you want to register a passkey once, then reuse it in many tests. A passkey holds a private key (a secret only your device knows). You can read that secret back with &lt;code&gt;get()&lt;/code&gt; and seed it into a later test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// In a setup test: register once, then read the passkey back.
const created = await context.credentials.get({ rpId: &amp;#39;example.com&amp;#39; });
// `created` holds the passkey fields, including its keys.
// Save them, then seed an identical passkey in a later test:
await otherContext.credentials.create(&amp;#39;example.com&amp;#39;, {
  id: created[0].id,
  userHandle: created[0].userHandle,
  privateKey: created[0].privateKey,
  publicKey: created[0].publicKey,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is how you keep one stable test user across a whole suite. You do not re-register on every test. You seed the same passkey each run. See the &lt;a href=&quot;https://playwright.dev/docs/api/class-credentials&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Credentials docs&lt;/a&gt; for the full field list.&lt;/p&gt;
&lt;h2 id=&quot;the-honest-part-what-this-does-not-test&quot;&gt;The honest part: what this does not test&lt;/h2&gt;
&lt;p&gt;A virtual key is not a real key. So this approach tests your login flow, not the physical hardware. It will not catch a bug in a specific phone&amp;#39;s secure chip or a real fingerprint reader. It tests the part you own: the page, the back-and-forth, the server check.&lt;/p&gt;
&lt;p&gt;For most teams that is the right line. The browser and the operating system test the hardware path for you. Your job is to test that your app asks the right question and trusts the right answer. That is exactly what the virtual key lets you do, in CI, on every push.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters-for-your-ci-pipeline&quot;&gt;Why this matters for your CI pipeline&lt;/h2&gt;
&lt;p&gt;Before 1.61, your passkey login had two test options. Skip it, or test it by hand. Both are bad. A skipped test means a silent break. A by-hand test runs once a release, not once a push.&lt;/p&gt;
&lt;p&gt;Now it runs like any other test. It sits in your suite. It runs on every pull request. If someone changes the login and breaks the passkey path, the build goes red before the change ships. That is the whole point of a test. Catch the break in seconds, not from an angry user.&lt;/p&gt;
&lt;p&gt;If your app supports passkeys, this is the test you write this week. The excuse is gone. The login everyone ships and nobody verifies is now testable.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Related reads on anton.qa:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://anton.qa/blog/posts/playwright-best-practices&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Playwright best practices that keep tests stable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://anton.qa/blog/posts/score-ai-test-agents-offline-evaluation&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;How to score your AI test agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Official sources:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/playwright/releases/tag/v1.61.0&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Playwright v1.61.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/api/class-credentials&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Playwright Credentials API docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>webauthn</category><category>passkeys</category><category>testing</category><category>ai-qa</category></item><item><title>Playwright Best Practices: 10 Rules AI Agents Get Wrong (2026)</title><link>https://www.anton.qa/blog/posts/playwright-best-practices</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-best-practices</guid><description>The 10 Playwright best practices for stable tests in 2026, and the ones AI code agents like Copilot and Cursor get wrong.</description><pubDate>Wed, 17 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Playwright Best Practices: 10 Rules AI Agents Get Wrong (2026)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Playwright best practices&lt;/strong&gt; are the rules that keep browser tests stable and easy to read. Use role-based locators (find by what users see), web-first assertions that auto-wait, and isolated tests. Seed data through the API (direct requests), not the UI. Avoid hard waits, conditional logic, and tests tied to your HTML. Turn on traces and run in parallel.&lt;/p&gt;
&lt;p&gt;An AI agent can write 50 Playwright tests in a minute. That feels fast.&lt;/p&gt;
&lt;p&gt;Then those tests fail at random, and nobody knows why. The agent copied old patterns from its training data. It does not know the run failed last night.&lt;/p&gt;
&lt;p&gt;This guide lists the 10 best practices that keep tests stable. For each one, I show a small correct example. I also show what AI code agents get wrong. AI tools like Copilot, Cursor, and even Playwright codegen (the test recorder) lean on stale habits. Someone has to fix that.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-find-elements-the-way-a-user-sees-them&quot;&gt;1. Find elements the way a user sees them&lt;/h2&gt;
&lt;p&gt;A locator (a pointer to an element) should match what a person sees on screen. Use &lt;code&gt;getByRole&lt;/code&gt;, &lt;code&gt;getByLabel&lt;/code&gt;, or &lt;code&gt;getByText&lt;/code&gt;. These read like the page. They also survive a redesign of your HTML.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test(&amp;#39;user can sign in&amp;#39;, async ({ page }) =&amp;gt; {
  await page.goto(&amp;#39;/login&amp;#39;);
  await page.getByLabel(&amp;#39;Email&amp;#39;).fill(&amp;#39;ada@example.com&amp;#39;);
  await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Sign in&amp;#39; }).click();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they reach for CSS or XPath (brittle path selectors) like &lt;code&gt;page.locator(&amp;#39;div.btn-primary &amp;gt; span&amp;#39;)&lt;/code&gt;. Change one class name and the test breaks.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-use-web-first-assertions-that-wait-for-you&quot;&gt;2. Use web-first assertions that wait for you&lt;/h2&gt;
&lt;p&gt;A web-first assertion (a check that auto-waits) retries until the page is ready. &lt;code&gt;expect(locator).toBeVisible()&lt;/code&gt; waits on its own. You never add a fixed sleep.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test(&amp;#39;welcome message appears&amp;#39;, async ({ page }) =&amp;gt; {
  await page.goto(&amp;#39;/dashboard&amp;#39;);
  await expect(page.getByText(&amp;#39;Welcome back&amp;#39;)).toBeVisible();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they add &lt;code&gt;await page.waitForTimeout(3000)&lt;/code&gt; (a hard pause). Hard waits are the top cause of flaky tests (tests that fail at random). Too short, the test fails. Too long, the suite crawls.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-keep-every-test-isolated&quot;&gt;3. Keep every test isolated&lt;/h2&gt;
&lt;p&gt;Isolated means each test starts clean. No shared login. No leftover data from the test before. Playwright gives each test a fresh browser context (a clean session). Set up state in a hook, not across tests.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test.beforeEach(async ({ page }) =&amp;gt; {
  await page.goto(&amp;#39;/login&amp;#39;);
  await page.getByLabel(&amp;#39;Email&amp;#39;).fill(&amp;#39;ada@example.com&amp;#39;);
  await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Sign in&amp;#39; }).click();
});

test(&amp;#39;shows the account name&amp;#39;, async ({ page }) =&amp;gt; {
  await expect(page.getByRole(&amp;#39;heading&amp;#39;, { name: &amp;#39;Ada Lovelace&amp;#39; })).toBeVisible();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they chain tests, where test 2 needs test 1 to run first. One failure then breaks the whole file.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-seed-state-through-the-api-not-the-ui&quot;&gt;4. Seed state through the API, not the UI&lt;/h2&gt;
&lt;p&gt;To test a page, you often need data first. A user, an order, a draft. Do not click through ten screens to make it. Send the data straight to your backend with the &lt;code&gt;request&lt;/code&gt; fixture (a built-in HTTP client). It is faster and steadier.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test(&amp;#39;opens an existing project&amp;#39;, async ({ page, request }) =&amp;gt; {
  const res = await request.post(&amp;#39;/api/projects&amp;#39;, {
    data: { name: &amp;#39;Apollo&amp;#39; },
  });
  expect(res.ok()).toBeTruthy();

  await page.goto(&amp;#39;/projects&amp;#39;);
  await expect(page.getByText(&amp;#39;Apollo&amp;#39;)).toBeVisible();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they build the data through the UI every time. The test gets long and slow, and a setup step fails for reasons that have nothing to do with the real check.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-do-not-lean-on-test-ids-by-default&quot;&gt;5. Do not lean on test IDs by default&lt;/h2&gt;
&lt;p&gt;A test ID (a tag added just for tests, like &lt;code&gt;data-testid&lt;/code&gt;) works as a fallback. But reach for &lt;code&gt;getByRole&lt;/code&gt; and &lt;code&gt;getByLabel&lt;/code&gt; first. Those test what a real user can do. A test ID only proves an attribute exists.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test(&amp;#39;cart shows one item&amp;#39;, async ({ page }) =&amp;gt; {
  await page.goto(&amp;#39;/cart&amp;#39;);
  // Prefer a real role over a test id.
  await expect(page.getByRole(&amp;#39;listitem&amp;#39;)).toHaveCount(1);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they paste &lt;code&gt;data-testid&lt;/code&gt; on everything. The tests pass even when the button has no label and a screen reader (assistive software) cannot find it. The test misses a real bug.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-turn-on-traces-for-the-first-retry&quot;&gt;6. Turn on traces for the first retry&lt;/h2&gt;
&lt;p&gt;A trace (a full recording of the run) shows every step, the DOM, and the network. Set it to record only on the first retry of a failed test. You get the evidence for failures, and clean runs stay fast.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// playwright.config.ts
import { defineConfig } from &amp;#39;@playwright/test&amp;#39;;

export default defineConfig({
  retries: 1,
  use: {
    trace: &amp;#39;on-first-retry&amp;#39;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they leave tracing off, or set &lt;code&gt;trace: &amp;#39;on&amp;#39;&lt;/code&gt; for every run. Off means no evidence when a test fails. Always-on slows the suite and fills your disk.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;7-run-tests-in-parallel-and-shard-them&quot;&gt;7. Run tests in parallel and shard them&lt;/h2&gt;
&lt;p&gt;Parallel means many tests run at once. Playwright does this by default. For one big file of independent tests, set parallel mode. To split a slow suite across machines, use sharding (run a slice per machine).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test.describe.configure({ mode: &amp;#39;parallel&amp;#39; });

test(&amp;#39;loads home&amp;#39;, async ({ page }) =&amp;gt; {
  await page.goto(&amp;#39;/&amp;#39;);
  await expect(page).toHaveTitle(/Home/);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Split across three machines on CI (your build server):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx playwright test --shard=1/3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they write tests that share a database row or a single user. Run those in parallel and they fight each other, so you get flaky tests.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;8-keep-if-and-try-out-of-your-tests&quot;&gt;8. Keep &lt;code&gt;if&lt;/code&gt; and &lt;code&gt;try&lt;/code&gt; out of your tests&lt;/h2&gt;
&lt;p&gt;A test should walk one clear path. No branching. If a test asks &amp;quot;is the button there? if so click it,&amp;quot; it hides a bug. The button should always be there. Assert it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test(&amp;#39;checkout button works&amp;#39;, async ({ page }) =&amp;gt; {
  await page.goto(&amp;#39;/cart&amp;#39;);
  // Assert the state. Do not guess it with an if.
  const checkout = page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Checkout&amp;#39; });
  await expect(checkout).toBeEnabled();
  await checkout.click();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they wrap clicks in &lt;code&gt;if (await locator.isVisible())&lt;/code&gt; to stop errors. That hides the real failure. A test that skips its own check still goes green.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;9-test-what-users-see-not-how-it-is-built&quot;&gt;9. Test what users see, not how it is built&lt;/h2&gt;
&lt;p&gt;Test the behavior, not the internals. Check the visible result. Do not check a CSS class, a state variable, or a function name. Those change when you refactor (rewrite the code), even though the app still works.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test(&amp;#39;shows a success message after submit&amp;#39;, async ({ page }) =&amp;gt; {
  await page.goto(&amp;#39;/contact&amp;#39;);
  await page.getByLabel(&amp;#39;Message&amp;#39;).fill(&amp;#39;Hello&amp;#39;);
  await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Send&amp;#39; }).click();
  // Check the user-facing result, not an internal class.
  await expect(page.getByText(&amp;#39;Thanks, we got your message&amp;#39;)).toBeVisible();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they assert on &lt;code&gt;class=&amp;quot;is-active&amp;quot;&lt;/code&gt; or an exact HTML shape. The test breaks on every redesign, even when nothing real changed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-define-projects-in-your-config&quot;&gt;10. Define projects in your config&lt;/h2&gt;
&lt;p&gt;A project (a named test setup) in &lt;code&gt;playwright.config.ts&lt;/code&gt; runs the same tests under different settings. Use projects to cover Chromium, Firefox, and WebKit (the three main browser engines). One config, full coverage.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// playwright.config.ts
import { defineConfig, devices } from &amp;#39;@playwright/test&amp;#39;;

export default defineConfig({
  testDir: &amp;#39;./tests&amp;#39;,
  fullyParallel: true,
  retries: 1,
  use: { trace: &amp;#39;on-first-retry&amp;#39; },
  projects: [
    { name: &amp;#39;chromium&amp;#39;, use: { ...devices[&amp;#39;Desktop Chrome&amp;#39;] } },
    { name: &amp;#39;firefox&amp;#39;, use: { ...devices[&amp;#39;Desktop Firefox&amp;#39;] } },
    { name: &amp;#39;webkit&amp;#39;, use: { ...devices[&amp;#39;Desktop Safari&amp;#39;] } },
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What AI agents get wrong:&lt;/strong&gt; they hard-code one browser, or copy a config with no &lt;code&gt;projects&lt;/code&gt; array. The suite then tests Chrome only, and a Safari-only bug ships to users.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-human-still-owns-the-standard&quot;&gt;The human still owns the standard&lt;/h2&gt;
&lt;p&gt;AI writes the first draft fast. That part is real, and it is useful. But the first draft copies patterns from old code on the internet. It adds hard waits. It clicks through the UI to seed data. It wraps fragile steps in &lt;code&gt;if&lt;/code&gt; blocks so the run stays green.&lt;/p&gt;
&lt;p&gt;A green suite that proves nothing is worse than no suite. It buys false trust.&lt;/p&gt;
&lt;p&gt;So the workflow is simple. Let the agent write the draft. Then a human reads it against these 10 rules and fixes what the agent got wrong. The agent moves fast. The human keeps the tests honest. That is the job of an AI QA Architect.&lt;/p&gt;
&lt;p&gt;Build the tests with AI. Then make them stable yourself.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>Playwright</category><category>Best Practices</category><category>Test Automation</category><category>AI Testing</category><category>QA Architecture</category></item><item><title>I measure how fast 42 LLMs actually answer. Here&apos;s the honest method.</title><link>https://www.anton.qa/blog/posts/how-i-measure-llm-speed</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/how-i-measure-llm-speed</guid><description>An honest method for benchmarking LLM speed: tokens per second vs time to first token, across 42 Ollama Cloud models, measured every 10 minutes.</description><pubDate>Tue, 16 Jun 2026 04:49:00 GMT</pubDate><content:encoded>&lt;h1&gt;I measure how fast 42 LLMs actually answer. Here&amp;#39;s the honest method.&lt;/h1&gt;
&lt;p&gt;I test software for a living. So when a vendor calls an AI model &amp;quot;fast,&amp;quot; I don&amp;#39;t trust the word. I measure it.&lt;/p&gt;
&lt;p&gt;Most leaderboards rank how smart a model is. Almost none rank how fast it answers. You pick a model because it scored well, ship it, and then your users sit and wait.&lt;/p&gt;
&lt;p&gt;Speed is two different numbers. People mix them up constantly.&lt;/p&gt;
&lt;h2 id=&quot;the-two-numbers&quot;&gt;The two numbers&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Time to first token (TTFT).&lt;/strong&gt; The wait before the first word appears. You feel this every time a chatbot &amp;quot;thinks&amp;quot; before replying.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tokens per second (TPS).&lt;/strong&gt; How fast the model writes once it starts. A token is a chunk of a word.&lt;/p&gt;
&lt;p&gt;A model can be great at one and terrible at the other. You need both.&lt;/p&gt;
&lt;h2 id=&quot;how-i-measure-it&quot;&gt;How I measure it&lt;/h2&gt;
&lt;p&gt;I run an independent tracker called &lt;a href=&quot;https://ollamatps.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ollamatps.com&lt;/a&gt;. It benchmarks 42 Ollama Cloud models. Here is the exact method, because a benchmark you cannot inspect is just a claim.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One fixed prompt.&lt;/strong&gt; Every run asks the model to write a 400-word explanation of HTTP routing. Same prompt, every model, every time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixed output cap.&lt;/strong&gt; &lt;code&gt;max_tokens&lt;/code&gt; is capped at 300. It never changes between runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every ~10 minutes.&lt;/strong&gt; Each model is re-tested continuously, not once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTFT&lt;/strong&gt; is measured from the moment the request is sent to the first non-empty content chunk. It includes network round-trip and prompt-processing time. That is honest, because it is what you actually wait.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TPS&lt;/strong&gt; is generation throughput only: &lt;code&gt;eval_count / (total_duration - time_to_first_token)&lt;/code&gt;. The startup wait is removed, so TPS measures pure writing speed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Same prompt, same cap, same schedule. That is what makes two models comparable.&lt;/p&gt;
&lt;p&gt;Building this was a testing job, not a coding job. Retries on failure. A reliability score per model. A circuit breaker for models that keep failing. If you cannot trust the measurement, the number is noise. That part is the same work I do on any test system.&lt;/p&gt;
&lt;h2 id=&quot;what-surprised-me&quot;&gt;What surprised me&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Bigger is not faster.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The fastest model on the board is one of the smallest: a 30B model at over 200 tokens per second. The model literally named &amp;quot;ultra&amp;quot; is dead last, under 8 tokens per second.&lt;/p&gt;
&lt;p&gt;And the wait varies wildly. TTFT ranges from about 0.3 seconds to 23 seconds across the 42 models. Same cloud. Roughly 80x difference in how long you wait for the first word.&lt;/p&gt;
&lt;p&gt;If you picked your model on a benchmark score alone, you have no idea which of these you are getting.&lt;/p&gt;
&lt;h2 id=&quot;why-i-rebuilt-it&quot;&gt;Why I rebuilt it&lt;/h2&gt;
&lt;p&gt;The first version tracked fewer models and was less robust. I rebuilt the engine this month (v2) to be multi-provider and to test continuously. The live board updates every 10 minutes.&lt;/p&gt;
&lt;p&gt;Watch it run: &lt;strong&gt;&lt;a href=&quot;https://ollamatps.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ollamatps.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect, the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>LLM</category><category>benchmarking</category><category>AI</category><category>performance</category><category>Ollama</category></item><item><title>How to Score Your AI Test Agents: Offline Evaluation with Trajectories (2026)</title><link>https://www.anton.qa/blog/posts/score-ai-test-agents-offline-evaluation</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/score-ai-test-agents-offline-evaluation</guid><description>Learn how to score the tests an AI agent writes. Record the run as a trajectory, replay it offline, and grade it without live API calls.</description><pubDate>Wed, 10 Jun 2026 13:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to Score Your AI Test Agents: Offline Evaluation with Trajectories (2026)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;AI test agent evaluation&lt;/strong&gt; is the practice of scoring the tests an AI agent writes, instead of trusting that they pass. You record the agent&amp;#39;s run as a trajectory (a saved log of every step), replay it offline, and grade each step for correctness and relevance. Offline scoring needs no live API calls, so you can check agent quality on every pull request.&lt;/p&gt;
&lt;p&gt;An AI agent can write 200 tests before lunch. That feels like progress.&lt;/p&gt;
&lt;p&gt;Then a real bug ships, and not one of those tests caught it. The agent was confident, and it was wrong.&lt;/p&gt;
&lt;p&gt;This guide shows how to stop guessing and start scoring. Stagehand 3.5.0 made the method first-class on June 3, 2026. The pattern works for any agent.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-it-passed-is-not-a-score&quot;&gt;1. It passed is not a score&lt;/h2&gt;
&lt;p&gt;A green test suite tells you the tests ran. It does not tell you the tests were right.&lt;/p&gt;
&lt;p&gt;An AI agent makes three mistakes a human reviewer would catch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It checks the wrong thing. The test passes, but it never asserts the real behavior.&lt;/li&gt;
&lt;li&gt;It writes flaky tests (tests that fail at random). They go green often enough to look fine.&lt;/li&gt;
&lt;li&gt;It tests a happy path and skips the edge case that actually breaks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You cannot fix what you cannot measure. So the first job is a number, not a vibe.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-record-the-run-as-a-trajectory&quot;&gt;2. Record the run as a trajectory&lt;/h2&gt;
&lt;p&gt;A trajectory is a saved recording of an agent&amp;#39;s run. It captures each step: what the agent saw, what it decided, and what code it produced.&lt;/p&gt;
&lt;p&gt;You capture it once, during the agent&amp;#39;s normal run.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Illustrative pattern — confirm the exact Stagehand 3.5 API before use.
const trajectory = await agent.run(task, { record: true });
await saveTrajectory(trajectory, &amp;quot;runs/checkout-flow.json&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recording is the receipt. Now you can study the run after it finishes, as many times as you want.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-replay-it-offline&quot;&gt;3. Replay it offline&lt;/h2&gt;
&lt;p&gt;Offline means you grade the saved run without calling the live model again. No new API cost. No flaky network. Same input every time.&lt;/p&gt;
&lt;p&gt;This matters for two reasons. It makes scoring cheap, so you can run it on every pull request. It makes scoring repeatable, so two engineers get the same result.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Replay the saved run and score it, with no live API calls.
const run = await loadTrajectory(&amp;quot;runs/checkout-flow.json&amp;quot;);
const score = await evaluate(run, rubric);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-score-each-step-with-evaluation-types&quot;&gt;4. Score each step with evaluation types&lt;/h2&gt;
&lt;p&gt;A single pass/fail hides too much. Grade the run on a few clear axes instead.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Correctness&lt;/strong&gt;: did the test assert the behavior the task asked for?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relevance&lt;/strong&gt;: does each step move toward the goal, or wander?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stability&lt;/strong&gt;: would this test pass on a clean re-run, or is it flaky?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coverage&lt;/strong&gt;: did the agent test the edge case, or only the happy path?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stagehand 3.5.0 added evaluation types for exactly this kind of offline scoring. You define the rubric once and apply it to every saved run.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const rubric = {
  correctness: (run) =&amp;gt; run.asserts.some(a =&amp;gt; a.target === task.goal),
  relevance:   (run) =&amp;gt; run.steps.every(s =&amp;gt; s.onTask),
  stability:   (run) =&amp;gt; run.reruns.every(r =&amp;gt; r.passed),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A run that scores &lt;code&gt;correctness 7/10, relevance pass, flaky tests 0&lt;/code&gt; is a run you can talk about. &amp;quot;It passed&amp;quot; is not.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-wire-the-score-into-ci&quot;&gt;5. Wire the score into CI&lt;/h2&gt;
&lt;p&gt;A score you read once and forget changes nothing. Turn it into a gate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# CI step: fail the build if the agent&amp;#39;s tests score too low.
- run: npx evaluate runs/ --min-correctness 0.8 --max-flaky 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the agent earns trust the same way a junior engineer does. It ships work, the work gets graded, and only graded work reaches production.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-where-this-sits-the-evidence-layer&quot;&gt;6. Where this sits: the Evidence Layer&lt;/h2&gt;
&lt;p&gt;I design AI test systems on a 3-Layer System:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Orchestration&lt;/strong&gt;: decides what to test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution&lt;/strong&gt;: runs the tests, where the agent writes code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evidence&lt;/strong&gt;: proves the work is right.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most teams build the first two layers and stop. They let the agent write tests and assume the green check means quality.&lt;/p&gt;
&lt;p&gt;Offline evaluation is the Evidence Layer. It is the difference between an agent you hope works and an agent you can prove works.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-5-line-checklist&quot;&gt;The 5-line checklist&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Record every agent run as a trajectory.&lt;/li&gt;
&lt;li&gt;Replay it offline, with no live API calls.&lt;/li&gt;
&lt;li&gt;Score it on correctness, relevance, stability, and coverage.&lt;/li&gt;
&lt;li&gt;Gate your build on the score.&lt;/li&gt;
&lt;li&gt;Keep the trajectory, so you can re-grade when the rubric improves.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Build the agent. Then prove it works.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>AI Testing</category><category>Test Evaluation</category><category>AI Agents</category><category>Stagehand</category><category>QA Architecture</category><category>SDET</category></item><item><title>Playwright Codegen: The Complete Guide (2026)</title><link>https://www.anton.qa/blog/posts/playwright-codegen-complete-guide</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-codegen-complete-guide</guid><description>Learn how to generate clean test scripts using Playwright Codegen, and how to scale those drafts into a production-ready test architecture.</description><pubDate>Wed, 03 Jun 2026 22:39:38 GMT</pubDate><content:encoded>&lt;h1&gt;Playwright Codegen: The Complete Guide (2026)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Playwright Codegen&lt;/strong&gt; is &lt;code&gt;a native CLI (command-line interface) tool that generates test scripts automatically as you interact with a browser&lt;/code&gt; . It records your actions—like clicks, form inputs, and page navigation—and translates them into clean TypeScript or JavaScript test code.&lt;/p&gt;
&lt;p&gt;For most developers, writing test locators (how tests find buttons) takes up 60% of test writing time. &lt;/p&gt;
&lt;p&gt;Codegen reduces that time to zero. &lt;/p&gt;
&lt;p&gt;Here is how to use it, and how to scale it from a simple draft tool to a full production architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-how-to-launch-playwright-codegen&quot;&gt;1. How to Launch Playwright Codegen&lt;/h2&gt;
&lt;p&gt;To start the generator, run this command in your terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx playwright codegen demo.playwright.dev/todomvc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This launch command opens two windows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A browser window&lt;/strong&gt;: This is where you click, type, and record your test steps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Playwright Inspector&lt;/strong&gt;: This is a tool window that displays the generated code in real time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As you click on the page, the tool writes the test code automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-capturing-assertions&quot;&gt;2. Capturing Assertions&lt;/h2&gt;
&lt;p&gt;A test without assertions (checks to verify behavior) is just a script. &lt;/p&gt;
&lt;p&gt;Codegen allows you to record checks directly from the UI. &lt;/p&gt;
&lt;p&gt;In the browser window, hover over any element and click one of the check buttons in the toolbar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assert Visibility&lt;/strong&gt;: Verifies if an element is visible on the screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assert Text&lt;/strong&gt;: Verifies if an element contains specific text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assert Value&lt;/strong&gt;: Verifies the input value of a form field.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This generates standard assertions like &lt;code&gt;await expect(locator).toBeVisible()&lt;/code&gt; instantly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-playwright-codegen-best-practices&quot;&gt;3. Playwright Codegen Best Practices&lt;/h2&gt;
&lt;p&gt;Generated code is a draft. &lt;/p&gt;
&lt;p&gt;To make it production-ready, apply these three rules:&lt;/p&gt;
&lt;h3 id=&quot;avoid-hardcoded-wait-times&quot;&gt;Avoid Hardcoded Wait Times&lt;/h3&gt;
&lt;p&gt;Codegen does not generate sleep statements. &lt;/p&gt;
&lt;p&gt;Playwright uses auto-waiting (waiting for elements to be ready). &lt;/p&gt;
&lt;p&gt;Keep it that way. &lt;/p&gt;
&lt;p&gt;Do not add manual timeouts.&lt;/p&gt;
&lt;h3 id=&quot;use-semantic-locators&quot;&gt;Use Semantic Locators&lt;/h3&gt;
&lt;p&gt;Playwright prefers locators that represent user actions. &lt;/p&gt;
&lt;p&gt;Codegen generates these by default:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Good: accessible locator
await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Submit&amp;#39; }).click();

// Bad: fragile CSS selector
await page.locator(&amp;#39;#submit-btn-2&amp;#39;).click();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the accessible versions. &lt;/p&gt;
&lt;p&gt;They prevent flaky tests (tests that fail randomly).&lt;/p&gt;
&lt;h3 id=&quot;isolate-your-auth-state&quot;&gt;Isolate Your Auth State&lt;/h3&gt;
&lt;p&gt;Do not record login steps in every single test. &lt;/p&gt;
&lt;p&gt;Use Codegen to save your authentication state once. &lt;/p&gt;
&lt;p&gt;Run Codegen with this save option:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx playwright codegen --save-storage=auth.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, configure your tests to load &lt;code&gt;auth.json&lt;/code&gt; before running. &lt;/p&gt;
&lt;p&gt;This saves hours of run time in CI (continuous integration servers).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-the-architectural-view-from-draft-to-system&quot;&gt;4. The Architectural View: From Draft to System&lt;/h2&gt;
&lt;p&gt;As an AI QA Architect, I view Codegen as a helper. &lt;/p&gt;
&lt;p&gt;It is the entry point of the &lt;strong&gt;Execution Layer&lt;/strong&gt; in the 3-Layer System:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Orchestration&lt;/strong&gt;: Decides when to run tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution&lt;/strong&gt;: The code that runs (where Codegen helps).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evidence&lt;/strong&gt;: Gathers logs and traces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Codegen writes the initial code. &lt;/p&gt;
&lt;p&gt;But it cannot design the framework. &lt;/p&gt;
&lt;p&gt;It cannot handle API mocks (fake servers). &lt;/p&gt;
&lt;p&gt;It cannot govern agentic testing systems (where AI agents write and heal tests).&lt;/p&gt;
&lt;p&gt;Use Codegen to build the first block. &lt;/p&gt;
&lt;p&gt;Then build the architecture around it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>Playwright</category><category>Codegen</category><category>Test Automation</category><category>QA Architecture</category><category>SDET</category><category>E2E Testing</category></item><item><title>What Is an AI QA Architect? Role, System, and Skills</title><link>https://www.anton.qa/blog/posts/what-is-ai-qa-architect</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/what-is-ai-qa-architect</guid><description>An AI QA Architect designs the test systems AI agents run on. Learn the role, architecture, skills, and MCP testing boundary.</description><pubDate>Wed, 27 May 2026 22:48:00 GMT</pubDate><content:encoded>&lt;h1&gt;What Is an AI QA Architect?&lt;/h1&gt;
&lt;p&gt;An AI QA Architect designs the test system AI agents run on. They connect requirements, test generation, browser execution, evidence, and human review. The job keeps QA engineers in control. It gives agents a safe place to create, run, fix, and explain tests.&lt;/p&gt;
&lt;p&gt;QA means quality assurance.&lt;/p&gt;
&lt;p&gt;AI QA Architect is a system role.&lt;br&gt;It is not a tool title.&lt;/p&gt;
&lt;p&gt;The person owns the way AI touches quality work.&lt;br&gt;That includes the rules, the evidence, and the release gate.&lt;/p&gt;
&lt;h2 id=&quot;the-simple-definition&quot;&gt;The Simple Definition&lt;/h2&gt;
&lt;p&gt;An AI QA Architect is responsible for the test architecture behind AI-assisted quality work.&lt;/p&gt;
&lt;p&gt;Architecture means the system design.&lt;br&gt;It answers five questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What can the agent read?&lt;/li&gt;
&lt;li&gt;What can the agent change?&lt;/li&gt;
&lt;li&gt;What evidence must it produce?&lt;/li&gt;
&lt;li&gt;Who reviews the result?&lt;/li&gt;
&lt;li&gt;What blocks the release?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The role matters because AI drafts tests fast.&lt;br&gt;Fast tests are not enough.&lt;/p&gt;
&lt;p&gt;You need a system that catches weak tests before they land.&lt;br&gt;You also need evidence humans can review.&lt;/p&gt;
&lt;h2 id=&quot;why-the-role-exists-now&quot;&gt;Why The Role Exists Now&lt;/h2&gt;
&lt;p&gt;Most QA systems were built for humans first.&lt;/p&gt;
&lt;p&gt;A person read the ticket.&lt;br&gt;A person chose the test.&lt;br&gt;A person checked the failure.&lt;br&gt;A person decided if the release was safe.&lt;/p&gt;
&lt;p&gt;AI changes the speed of that loop.&lt;/p&gt;
&lt;p&gt;An agent can draft a test from a ticket.&lt;br&gt;It can inspect a failing trace.&lt;br&gt;It can suggest a fix.&lt;br&gt;It can explain a failure.&lt;/p&gt;
&lt;p&gt;That power creates a new risk.&lt;/p&gt;
&lt;p&gt;If the system has weak gates, AI makes weak work faster.&lt;br&gt;If the system has strong gates, AI can help the team move safely.&lt;/p&gt;
&lt;p&gt;That is the AI QA Architect&amp;#39;s job.&lt;/p&gt;
&lt;h2 id=&quot;the-five-parts-of-the-system&quot;&gt;The Five Parts Of The System&lt;/h2&gt;
&lt;h3 id=&quot;1-requirement-intake&quot;&gt;1. Requirement Intake&lt;/h3&gt;
&lt;p&gt;The agent needs clean input.&lt;/p&gt;
&lt;p&gt;Input can be a user story, bug report, ticket, or spec.&lt;br&gt;The architect defines what good input means.&lt;/p&gt;
&lt;p&gt;A good input names:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;user goal&lt;/li&gt;
&lt;li&gt;expected result&lt;/li&gt;
&lt;li&gt;risky path&lt;/li&gt;
&lt;li&gt;test data&lt;/li&gt;
&lt;li&gt;release impact&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without this, the agent guesses.&lt;br&gt;Guessing creates noisy tests.&lt;/p&gt;
&lt;h3 id=&quot;2-test-generation&quot;&gt;2. Test Generation&lt;/h3&gt;
&lt;p&gt;AI can draft tests.&lt;br&gt;Humans still review them.&lt;/p&gt;
&lt;p&gt;A good generation flow has rules.&lt;/p&gt;
&lt;p&gt;It tells the agent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which tool to use&lt;/li&gt;
&lt;li&gt;which patterns to follow&lt;/li&gt;
&lt;li&gt;what code style to match&lt;/li&gt;
&lt;li&gt;what not to test&lt;/li&gt;
&lt;li&gt;when to stop&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps generated tests inside the team&amp;#39;s standards.&lt;/p&gt;
&lt;h3 id=&quot;3-execution&quot;&gt;3. Execution&lt;/h3&gt;
&lt;p&gt;Execution means running the test.&lt;/p&gt;
&lt;p&gt;For browser checks, tools like Playwright matter.&lt;br&gt;They produce traces, screenshots, reports, and browser state.&lt;/p&gt;
&lt;p&gt;Those artifacts become receipts.&lt;br&gt;Receipts are proof a human can review.&lt;/p&gt;
&lt;p&gt;AI agents need receipts too.&lt;br&gt;Without receipts, the agent only has text.&lt;/p&gt;
&lt;h3 id=&quot;4-failure-analysis&quot;&gt;4. Failure Analysis&lt;/h3&gt;
&lt;p&gt;A failed test is not enough.&lt;/p&gt;
&lt;p&gt;The system must answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Did the app break?&lt;/li&gt;
&lt;li&gt;Did the test break?&lt;/li&gt;
&lt;li&gt;Did the data change?&lt;/li&gt;
&lt;li&gt;Did the environment fail?&lt;/li&gt;
&lt;li&gt;Should the release stop?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI can help sort the evidence.&lt;br&gt;The human still owns the decision.&lt;/p&gt;
&lt;h3 id=&quot;5-release-gate&quot;&gt;5. Release Gate&lt;/h3&gt;
&lt;p&gt;A release gate decides what ships.&lt;/p&gt;
&lt;p&gt;CI means server test runs.&lt;br&gt;The gate uses CI results, test reports, and risk rules.&lt;/p&gt;
&lt;p&gt;An AI QA Architect defines the gate.&lt;br&gt;They decide which failures block release.&lt;br&gt;They decide which failures need review.&lt;br&gt;They decide what evidence is required.&lt;/p&gt;
&lt;h2 id=&quot;how-this-differs-from-qa-automation&quot;&gt;How This Differs From QA Automation&lt;/h2&gt;
&lt;p&gt;QA automation asks, &amp;quot;Can we run the test?&amp;quot;&lt;/p&gt;
&lt;p&gt;AI QA architecture asks, &amp;quot;Can we trust the system around the test?&amp;quot;&lt;/p&gt;
&lt;p&gt;That includes automation.&lt;br&gt;It also includes agents, prompts, data, evidence, review, and release rules.&lt;/p&gt;
&lt;p&gt;A QA automation engineer may build the runner.&lt;br&gt;An AI QA Architect owns the full loop.&lt;/p&gt;
&lt;h2 id=&quot;how-mcp-fits&quot;&gt;How MCP Fits&lt;/h2&gt;
&lt;p&gt;MCP means Model Context Protocol.&lt;br&gt;It is a standard way for AI tools to connect with external tools.&lt;/p&gt;
&lt;p&gt;MCP testing has two jobs.&lt;/p&gt;
&lt;p&gt;First, test the MCP server you build.&lt;br&gt;That means tools, resources, prompts, and errors.&lt;/p&gt;
&lt;p&gt;Second, test software through MCP-enabled tools.&lt;br&gt;That means the agent can call a browser, database, or API tool.&lt;/p&gt;
&lt;p&gt;Both jobs need a test system.&lt;/p&gt;
&lt;p&gt;The official MCP Inspector helps developers test MCP servers.&lt;br&gt;It gives a visual way to inspect capabilities and debug requests.&lt;/p&gt;
&lt;p&gt;That is useful.&lt;br&gt;It is not the full QA system.&lt;/p&gt;
&lt;p&gt;The architect still defines the rules around safe use.&lt;/p&gt;
&lt;h2 id=&quot;skills-an-ai-qa-architect-needs&quot;&gt;Skills An AI QA Architect Needs&lt;/h2&gt;
&lt;p&gt;You need test automation depth.&lt;br&gt;You need enough AI knowledge to set safe boundaries.&lt;br&gt;You need architecture judgment.&lt;/p&gt;
&lt;p&gt;Core skills:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Playwright or another browser test tool&lt;/li&gt;
&lt;li&gt;CI pipeline design&lt;/li&gt;
&lt;li&gt;test data strategy&lt;/li&gt;
&lt;li&gt;trace and report review&lt;/li&gt;
&lt;li&gt;prompt rules for agents&lt;/li&gt;
&lt;li&gt;MCP server testing&lt;/li&gt;
&lt;li&gt;release gate design&lt;/li&gt;
&lt;li&gt;risk-based testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LLM means language model.&lt;br&gt;You do not need to train one.&lt;/p&gt;
&lt;p&gt;You do need to know how it fails.&lt;br&gt;It can hallucinate.&lt;br&gt;It can skip edge cases.&lt;br&gt;It can accept weak evidence.&lt;/p&gt;
&lt;p&gt;Your system must catch those failures.&lt;/p&gt;
&lt;h2 id=&quot;what-the-role-is-not&quot;&gt;What The Role Is Not&lt;/h2&gt;
&lt;p&gt;An AI QA Architect is not a person who buys AI tools.&lt;/p&gt;
&lt;p&gt;It is not a person who tells testers to stop writing tests.&lt;/p&gt;
&lt;p&gt;It is not a person who trusts every generated test.&lt;/p&gt;
&lt;p&gt;The role is more practical.&lt;/p&gt;
&lt;p&gt;You design the system so agents can help safely.&lt;br&gt;You protect the human decision.&lt;br&gt;You make quality visible before release.&lt;/p&gt;
&lt;h2 id=&quot;a-practical-checklist&quot;&gt;A Practical Checklist&lt;/h2&gt;
&lt;p&gt;Use this before adding AI to a test suite.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Can the agent read the requirement clearly?&lt;/li&gt;
&lt;li&gt;Can it follow your test patterns?&lt;/li&gt;
&lt;li&gt;Can it run the right tool?&lt;/li&gt;
&lt;li&gt;Can it produce reviewable evidence?&lt;/li&gt;
&lt;li&gt;Can a human approve or reject the result?&lt;/li&gt;
&lt;li&gt;Can CI block unsafe releases?&lt;/li&gt;
&lt;li&gt;Can the team trace why a test changed?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the answer is no, fix the system first.&lt;/p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;/h2&gt;
&lt;h3 id=&quot;what-does-an-ai-qa-architect-do&quot;&gt;What does an AI QA Architect do?&lt;/h3&gt;
&lt;p&gt;An AI QA Architect designs the test system AI agents use. They define how agents read requirements, generate tests, run checks, and produce evidence. The goal is safer test automation with human review.&lt;/p&gt;
&lt;h3 id=&quot;is-an-ai-qa-architect-replacing-qa-engineers&quot;&gt;Is an AI QA Architect replacing QA engineers?&lt;/h3&gt;
&lt;p&gt;No. The role protects human review. AI agents can draft tests and inspect failures. Humans still own the quality decision. The architect designs the rules around that work.&lt;/p&gt;
&lt;h3 id=&quot;what-is-ai-test-automation-architecture&quot;&gt;What is AI test automation architecture?&lt;/h3&gt;
&lt;p&gt;AI test automation architecture connects human intent, AI assistance, test execution, evidence, and release gates. It includes prompts, tools, CI, reports, data, and review rules.&lt;/p&gt;
&lt;h3 id=&quot;how-does-mcp-testing-relate-to-qa&quot;&gt;How does MCP testing relate to QA?&lt;/h3&gt;
&lt;p&gt;MCP testing covers two jobs. You test MCP servers you build, and you test software through MCP-enabled tools. Both need clear inputs, safe tool access, and evidence a human can review.&lt;/p&gt;
&lt;h3 id=&quot;what-should-a-team-build-first&quot;&gt;What should a team build first?&lt;/h3&gt;
&lt;p&gt;Build the evidence loop first. Every test failure should show action, page state, assertion, logs, and release impact. Then add AI help inside that loop.&lt;/p&gt;
&lt;h2 id=&quot;start-with-evidence&quot;&gt;Start With Evidence&lt;/h2&gt;
&lt;p&gt;AI QA Architect is a system owner.&lt;/p&gt;
&lt;p&gt;The job is not to make AI look smart.&lt;br&gt;The job is to make quality safer when AI joins the loop.&lt;/p&gt;
&lt;p&gt;Start with evidence.&lt;br&gt;Add agents after that.&lt;/p&gt;
&lt;h2 id=&quot;author-bio&quot;&gt;Author Bio&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>AI QA architect</category><category>QA</category><category>SDET</category><category>AI</category></item><item><title>Playwright vs Cypress vs Selenium in 2026: The QA Architect Decision Guide</title><link>https://www.anton.qa/blog/posts/playwright-vs-cypress-vs-selenium-in-2026</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-vs-cypress-vs-selenium-in-2026</guid><description>Compare Playwright, Cypress, and Selenium in 2026. Pick the right browser test tool for AI-agent workflows.</description><pubDate>Thu, 21 May 2026 02:13:00 GMT</pubDate><content:encoded>&lt;p&gt;Playwright is the best default for new browser test automation in 2026. It gives cross-browser runs, parallel CI, API checks, and AI-agent evidence in one tool. Cypress still fits JavaScript-heavy teams that want fast local feedback. Selenium still fits legacy grids and strict browser labs.&lt;/p&gt;
&lt;p&gt;That is the short answer.&lt;/p&gt;
&lt;p&gt;The better answer depends on your system.&lt;/p&gt;
&lt;p&gt;If AI agents will read your failures, the question changes.&lt;br&gt;You are no longer picking only a test runner.&lt;br&gt;You are picking the evidence layer.&lt;/p&gt;
&lt;h2 id=&quot;what-changed-in-2026&quot;&gt;What Changed In 2026&lt;/h2&gt;
&lt;p&gt;Most comparison posts still ask old questions.&lt;/p&gt;
&lt;p&gt;They ask which tool has cleaner syntax.&lt;br&gt;They ask which tool is easier to learn.&lt;br&gt;They ask which tool starts faster.&lt;/p&gt;
&lt;p&gt;Those questions still matter.&lt;br&gt;They are no longer enough.&lt;/p&gt;
&lt;p&gt;AI agents need proof they can inspect.&lt;br&gt;Proof means screenshots, traces, browser state, and readable failures.&lt;/p&gt;
&lt;p&gt;The human reviewer still owns the decision.&lt;br&gt;The agent only helps when the evidence is clear.&lt;/p&gt;
&lt;p&gt;That is why Playwright now has the default seat.&lt;/p&gt;
&lt;h2 id=&quot;pick-playwright-when-evidence-matters&quot;&gt;Pick Playwright When Evidence Matters&lt;/h2&gt;
&lt;p&gt;Pick Playwright for new end-to-end test systems.&lt;br&gt;End-to-end means browser checks.&lt;/p&gt;
&lt;p&gt;Playwright gives you one model across Chromium, Firefox, and WebKit.&lt;br&gt;Those are browser engines.&lt;br&gt;They are how pages run.&lt;/p&gt;
&lt;p&gt;That matters for real product risk.&lt;/p&gt;
&lt;p&gt;It also matters for AI-agent workflows.&lt;br&gt;AI agents means tools that act.&lt;/p&gt;
&lt;p&gt;Playwright now documents Test Agents.&lt;br&gt;Those agents plan, generate, and repair tests.&lt;/p&gt;
&lt;p&gt;The tool also has strong receipts.&lt;br&gt;Traces show what happened.&lt;br&gt;Screenshots show where it happened.&lt;br&gt;Reports help humans review the failure.&lt;/p&gt;
&lt;p&gt;Use Playwright when you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;browser coverage across engines&lt;/li&gt;
&lt;li&gt;parallel CI at scale&lt;/li&gt;
&lt;li&gt;trace-based debugging&lt;/li&gt;
&lt;li&gt;API and UI checks together&lt;/li&gt;
&lt;li&gt;AI-agent review paths&lt;/li&gt;
&lt;li&gt;long-term framework ownership&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CI means server test runs.&lt;/p&gt;
&lt;h2 id=&quot;pick-cypress-when-local-feedback-matters-most&quot;&gt;Pick Cypress When Local Feedback Matters Most&lt;/h2&gt;
&lt;p&gt;Cypress is still useful.&lt;/p&gt;
&lt;p&gt;That sentence matters.&lt;br&gt;Tool debates get lazy when one side becomes a villain.&lt;/p&gt;
&lt;p&gt;Cypress can be a strong fit for frontend teams.&lt;br&gt;It works well when developers want quick local feedback.&lt;br&gt;It also fits teams already built around Cypress Cloud.&lt;/p&gt;
&lt;p&gt;Cypress documents cross-browser testing.&lt;br&gt;It also documents parallel runs through Cypress Cloud.&lt;/p&gt;
&lt;p&gt;That can be enough for many product teams.&lt;/p&gt;
&lt;p&gt;Use Cypress when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;your app is JavaScript-first&lt;/li&gt;
&lt;li&gt;developers own most browser checks&lt;/li&gt;
&lt;li&gt;fast local debugging is the main goal&lt;/li&gt;
&lt;li&gt;Cypress Cloud is already approved&lt;/li&gt;
&lt;li&gt;browser coverage needs are narrow&lt;/li&gt;
&lt;li&gt;the suite is not agent-driven yet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The risk appears later.&lt;/p&gt;
&lt;p&gt;As the suite grows, evidence gets more important.&lt;br&gt;That is where Playwright usually wins.&lt;/p&gt;
&lt;h2 id=&quot;keep-selenium-when-migration-risk-is-higher&quot;&gt;Keep Selenium When Migration Risk Is Higher&lt;/h2&gt;
&lt;p&gt;Selenium is not dead.&lt;/p&gt;
&lt;p&gt;It is still the right answer for some teams.&lt;/p&gt;
&lt;p&gt;Keep Selenium when a grid already exists.&lt;br&gt;Keep it when policy requires it.&lt;br&gt;Keep it when migration risk is higher than tool value.&lt;/p&gt;
&lt;p&gt;But do not choose Selenium by default for new AI QA work.&lt;/p&gt;
&lt;p&gt;You will spend too much time rebuilding the evidence layer.&lt;br&gt;You will also carry older suite habits forward.&lt;/p&gt;
&lt;p&gt;Selenium can be stable.&lt;br&gt;The question is whether it helps the next system.&lt;/p&gt;
&lt;h2 id=&quot;the-decision-table&quot;&gt;The Decision Table&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Best default&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;New AI-agent test system&lt;/td&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Best evidence path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broad browser engine coverage&lt;/td&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;One model across major engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast frontend feedback&lt;/td&gt;
&lt;td&gt;Cypress&lt;/td&gt;
&lt;td&gt;Strong local developer loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing Cypress investment&lt;/td&gt;
&lt;td&gt;Cypress&lt;/td&gt;
&lt;td&gt;Migration may not pay yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy grid policy&lt;/td&gt;
&lt;td&gt;Selenium&lt;/td&gt;
&lt;td&gt;Use what the organization can run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greenfield QA architecture&lt;/td&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Better long-term receipts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;my-four-question-test&quot;&gt;My Four-Question Test&lt;/h2&gt;
&lt;p&gt;I use four questions before I choose.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Who reads the failure first?&lt;/li&gt;
&lt;li&gt;What proof do they need?&lt;/li&gt;
&lt;li&gt;Where will the suite run?&lt;/li&gt;
&lt;li&gt;What happens when the UI changes?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the answer includes AI agents, I lean Playwright.&lt;/p&gt;
&lt;p&gt;If the answer is one frontend team, Cypress can fit.&lt;/p&gt;
&lt;p&gt;If the answer is legacy policy, keep Selenium.&lt;/p&gt;
&lt;h2 id=&quot;the-practical-recommendation&quot;&gt;The Practical Recommendation&lt;/h2&gt;
&lt;p&gt;Start new projects with Playwright.&lt;/p&gt;
&lt;p&gt;Keep Cypress when it already serves the team.&lt;/p&gt;
&lt;p&gt;Keep Selenium when migration would create more risk.&lt;/p&gt;
&lt;p&gt;Then build the same rule across all three:&lt;/p&gt;
&lt;p&gt;Every failed test needs a receipt.&lt;/p&gt;
&lt;p&gt;That receipt should show:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what action ran&lt;/li&gt;
&lt;li&gt;what page state existed&lt;/li&gt;
&lt;li&gt;what assertion failed&lt;/li&gt;
&lt;li&gt;what changed before failure&lt;/li&gt;
&lt;li&gt;what a human must decide&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tool only matters because the evidence matters.&lt;/p&gt;
&lt;p&gt;In 2026, that is the real comparison.&lt;/p&gt;
&lt;h2 id=&quot;author-bio&quot;&gt;Author Bio&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>Playwright</category><category>Cypress</category><category>Selenium</category><category>ai</category></item><item><title>Playwright v1.60 Turns Test Failures Into Evidence</title><link>https://www.anton.qa/blog/posts/playwright-v1-60-evidence-first-testing</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-v1-60-evidence-first-testing</guid><description>Playwright v1.60 adds scoped HAR recording, locator.drop(), ARIA boxes, and test.abort() so CI failures carry better proof.</description><pubDate>Thu, 14 May 2026 15:46:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;Playwright v1.60 makes failure evidence easier to capture during the run.&lt;/p&gt;
&lt;p&gt;The main change is scoped HAR recording.&lt;/p&gt;
&lt;p&gt;HAR means network request file.&lt;/p&gt;
&lt;p&gt;It shows what the browser sent and received.&lt;/p&gt;
&lt;p&gt;The release also adds file drops, ARIA boxes, and hard test aborts.&lt;/p&gt;
&lt;p&gt;ARIA means accessibility map.&lt;/p&gt;
&lt;p&gt;Together, these changes help CI failures explain themselves.&lt;/p&gt;
&lt;p&gt;CI means automated build server.&lt;/p&gt;
&lt;h2 id=&quot;the-release&quot;&gt;The Release&lt;/h2&gt;
&lt;p&gt;Playwright v1.60 shipped on May 11, 2026.&lt;/p&gt;
&lt;p&gt;The headline change is &lt;code&gt;context.tracing.startHar()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Tracing means run evidence capture.&lt;/p&gt;
&lt;p&gt;Before v1.60, HAR capture lived outside that tracing flow.&lt;/p&gt;
&lt;p&gt;Now HAR recording sits next to other test proof.&lt;/p&gt;
&lt;p&gt;The API also returns a &lt;code&gt;Disposable&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A disposable is a cleanup handle.&lt;/p&gt;
&lt;p&gt;You can use &lt;code&gt;await using&lt;/code&gt; to close the HAR automatically.&lt;/p&gt;
&lt;p&gt;That matters when many tests run at once.&lt;/p&gt;
&lt;p&gt;Manual cleanup breaks easily.&lt;/p&gt;
&lt;p&gt;One missed cleanup step can leave broken evidence behind.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters-for-qa-teams&quot;&gt;Why This Matters For QA Teams&lt;/h2&gt;
&lt;p&gt;Most teams collect proof after a test fails.&lt;/p&gt;
&lt;p&gt;That is too late.&lt;/p&gt;
&lt;p&gt;The failing run is already gone.&lt;/p&gt;
&lt;p&gt;The next run may pass.&lt;/p&gt;
&lt;p&gt;Now the team has a guess, not proof.&lt;/p&gt;
&lt;p&gt;Playwright v1.60 moves more proof into the first run.&lt;/p&gt;
&lt;p&gt;That is the architectural part.&lt;/p&gt;
&lt;p&gt;You are not asking a person to rerun the test.&lt;/p&gt;
&lt;p&gt;You are designing the run to bring receipts.&lt;/p&gt;
&lt;p&gt;That matters even more for AI testing.&lt;/p&gt;
&lt;p&gt;AI means software that predicts.&lt;/p&gt;
&lt;p&gt;Predictions need clear input.&lt;/p&gt;
&lt;p&gt;A trace, a HAR file, and an ARIA snapshot give that input.&lt;/p&gt;
&lt;p&gt;Without proof, AI just writes a confident guess.&lt;/p&gt;
&lt;h2 id=&quot;how-to-use-scoped-har-recording&quot;&gt;How To Use Scoped HAR Recording&lt;/h2&gt;
&lt;p&gt;This example records network evidence for an upload test.&lt;/p&gt;
&lt;p&gt;It also uses the new Drop API.&lt;/p&gt;
&lt;p&gt;Drop API means file drop simulation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { test, expect } from &amp;#39;@playwright/test&amp;#39;;

test(&amp;#39;upload records network evidence&amp;#39;, async ({ context }) =&amp;gt; {
  await using har = await context.tracing.startHar(&amp;#39;upload.har&amp;#39;, {
    content: &amp;#39;embed&amp;#39;,
    mode: &amp;#39;minimal&amp;#39;,
    urlFilter: /\/api\/upload/,
  });

  const page = await context.newPage();
  await page.goto(&amp;#39;/upload&amp;#39;);

  await page.locator(&amp;#39;#dropzone&amp;#39;).drop({
    files: {
      name: &amp;#39;note.txt&amp;#39;,
      mimeType: &amp;#39;text/plain&amp;#39;,
      buffer: Buffer.from(&amp;#39;hello&amp;#39;),
    },
  });

  await expect(page.getByText(&amp;#39;Upload complete&amp;#39;)).toBeVisible();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The HAR starts before the page opens.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;urlFilter&lt;/code&gt; keeps the capture focused.&lt;/p&gt;
&lt;p&gt;The drop step sends an in-memory file.&lt;/p&gt;
&lt;p&gt;When the test scope ends, Playwright finalizes the HAR.&lt;/p&gt;
&lt;p&gt;No extra &lt;code&gt;afterEach&lt;/code&gt; block is needed.&lt;/p&gt;
&lt;p&gt;No custom &lt;code&gt;try/finally&lt;/code&gt; block is needed.&lt;/p&gt;
&lt;p&gt;That is the small win.&lt;/p&gt;
&lt;p&gt;The bigger win is trust.&lt;/p&gt;
&lt;p&gt;When upload fails, the HAR file shows the request.&lt;/p&gt;
&lt;p&gt;Your team can inspect the failed run.&lt;/p&gt;
&lt;p&gt;They do not need to recreate it from memory.&lt;/p&gt;
&lt;h2 id=&quot;the-hidden-updates-worth-watching&quot;&gt;The Hidden Updates Worth Watching&lt;/h2&gt;
&lt;p&gt;The release is bigger than HAR.&lt;/p&gt;
&lt;p&gt;Five smaller changes point in the same direction.&lt;/p&gt;
&lt;p&gt;First, &lt;code&gt;locator.drop()&lt;/code&gt; tests real upload zones better.&lt;/p&gt;
&lt;p&gt;It accepts files, text, HTML, or URI data.&lt;/p&gt;
&lt;p&gt;URI means web address.&lt;/p&gt;
&lt;p&gt;Playwright sends &lt;code&gt;dragenter&lt;/code&gt;, &lt;code&gt;dragover&lt;/code&gt;, and &lt;code&gt;drop&lt;/code&gt; events.&lt;/p&gt;
&lt;p&gt;If the app rejects the drop, the method throws.&lt;/p&gt;
&lt;p&gt;That is useful.&lt;/p&gt;
&lt;p&gt;It tells you the app never accepted the file.&lt;/p&gt;
&lt;p&gt;Second, ARIA snapshots can include boxes.&lt;/p&gt;
&lt;p&gt;Boxes mean element positions.&lt;/p&gt;
&lt;p&gt;The format is &lt;code&gt;[box=x,y,width,height]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Coordinates are viewport-relative CSS pixels.&lt;/p&gt;
&lt;p&gt;Viewport means visible browser area.&lt;/p&gt;
&lt;p&gt;This gives AI tools a cleaner page map.&lt;/p&gt;
&lt;p&gt;They can see what exists and where it sits.&lt;/p&gt;
&lt;p&gt;Third, page-level ARIA assertions now exist.&lt;/p&gt;
&lt;p&gt;You can assert the page body directly.&lt;/p&gt;
&lt;p&gt;That removes one small wrapper step.&lt;/p&gt;
&lt;p&gt;Small wrapper steps matter in generated tests.&lt;/p&gt;
&lt;p&gt;Fourth, &lt;code&gt;test.abort()&lt;/code&gt; stops bad runs early.&lt;/p&gt;
&lt;p&gt;It works inside fixtures, hooks, and route handlers.&lt;/p&gt;
&lt;p&gt;Fixtures mean shared test setup.&lt;/p&gt;
&lt;p&gt;Use it when the test breaks a safety rule.&lt;/p&gt;
&lt;p&gt;For example, a test might publish to a shared page.&lt;/p&gt;
&lt;p&gt;That run should stop right away.&lt;/p&gt;
&lt;p&gt;Fifth, BrowserContext now mirrors page events.&lt;/p&gt;
&lt;p&gt;BrowserContext means a browser sandbox.&lt;/p&gt;
&lt;p&gt;Framework owners can listen once per context.&lt;/p&gt;
&lt;p&gt;They do not need listeners on every page.&lt;/p&gt;
&lt;p&gt;That helps when one test opens many tabs.&lt;/p&gt;
&lt;h2 id=&quot;the-gotcha-nobody-is-talking-about&quot;&gt;The Gotcha Nobody Is Talking About&lt;/h2&gt;
&lt;p&gt;Only one HAR recording can run per BrowserContext.&lt;/p&gt;
&lt;p&gt;That is not a problem.&lt;/p&gt;
&lt;p&gt;But it is a design rule.&lt;/p&gt;
&lt;p&gt;Do not start one HAR for login and another for upload.&lt;/p&gt;
&lt;p&gt;Use separate contexts when you need separate captures.&lt;/p&gt;
&lt;p&gt;Or keep one capture narrow with &lt;code&gt;urlFilter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Drop API has another useful edge.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;dragover&lt;/code&gt; does not call &lt;code&gt;preventDefault()&lt;/code&gt;, Playwright throws.&lt;/p&gt;
&lt;p&gt;That sounds annoying.&lt;/p&gt;
&lt;p&gt;It is usually the bug.&lt;/p&gt;
&lt;p&gt;The app rejected the drop before the file arrived.&lt;/p&gt;
&lt;p&gt;ARIA boxes have one more boundary.&lt;/p&gt;
&lt;p&gt;They use viewport coordinates, not full-page coordinates.&lt;/p&gt;
&lt;p&gt;If your test scrolls, account for that.&lt;/p&gt;
&lt;p&gt;If your page uses frames, account for that too.&lt;/p&gt;
&lt;h2 id=&quot;migration-notes&quot;&gt;Migration Notes&lt;/h2&gt;
&lt;p&gt;Playwright v1.60 removes several old APIs.&lt;/p&gt;
&lt;p&gt;Check these before upgrading a large suite.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;Locator.ariaRef()&lt;/code&gt; with &lt;code&gt;locator.ariaSnapshot()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remove the &lt;code&gt;handle&lt;/code&gt; option from &lt;code&gt;exposeBinding()&lt;/code&gt; calls.&lt;/li&gt;
&lt;li&gt;Remove the &lt;code&gt;logger&lt;/code&gt; option from browser connection calls.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;videosPath&lt;/code&gt; and &lt;code&gt;videoSize&lt;/code&gt; with &lt;code&gt;recordVideo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is also a CDP update.&lt;/p&gt;
&lt;p&gt;CDP means browser control protocol.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;connectOverCDP()&lt;/code&gt; now accepts &lt;code&gt;noDefaults&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That matters when attaching to an existing Chromium browser.&lt;/p&gt;
&lt;p&gt;Chromium means Chrome browser engine.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;noDefaults: true&lt;/code&gt;, Playwright leaves default context settings alone.&lt;/p&gt;
&lt;h2 id=&quot;what-this-changes-in-ci&quot;&gt;What This Changes In CI&lt;/h2&gt;
&lt;p&gt;This release does not make tests smarter by itself.&lt;/p&gt;
&lt;p&gt;It makes test runs easier to review.&lt;/p&gt;
&lt;p&gt;That is the part I care about.&lt;/p&gt;
&lt;p&gt;Good QA architecture is not just more tests.&lt;/p&gt;
&lt;p&gt;It is better evidence from each run.&lt;/p&gt;
&lt;p&gt;For a small suite, v1.60 is a nice upgrade.&lt;/p&gt;
&lt;p&gt;For a large CI suite, it is more important.&lt;/p&gt;
&lt;p&gt;You can scope network captures.&lt;/p&gt;
&lt;p&gt;You can test upload zones without custom browser hacks.&lt;/p&gt;
&lt;p&gt;You can give AI tools page structure and element positions.&lt;/p&gt;
&lt;p&gt;You can stop unsafe tests before they poison shared state.&lt;/p&gt;
&lt;p&gt;That is not hype.&lt;/p&gt;
&lt;p&gt;That is how test systems become reviewable.&lt;/p&gt;
&lt;h2 id=&quot;verdict&quot;&gt;Verdict&lt;/h2&gt;
&lt;p&gt;I would upgrade Playwright test projects to v1.60 this week.&lt;/p&gt;
&lt;p&gt;Start with the evidence APIs.&lt;/p&gt;
&lt;p&gt;Add &lt;code&gt;startHar()&lt;/code&gt; where network failures waste review time.&lt;/p&gt;
&lt;p&gt;Add &lt;code&gt;locator.drop()&lt;/code&gt; where upload tests use custom events.&lt;/p&gt;
&lt;p&gt;Add ARIA boxes where AI tools inspect pages.&lt;/p&gt;
&lt;p&gt;Then review the breaking removals before merging.&lt;/p&gt;
&lt;p&gt;The release is not about one huge feature.&lt;/p&gt;
&lt;p&gt;It is about a better proof layer.&lt;/p&gt;
&lt;p&gt;That is what AI QA architecture needs.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Related reads on anton.qa:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI Test Automation Architecture: The 3-Layer System&lt;/li&gt;
&lt;li&gt;Playwright release notes coverage from Anton&amp;#39;s content plan&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Official sources:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/playwright/releases/tag/v1.60.0&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Playwright v1.60.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/api/class-tracing#tracing-start-har&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;tracing.startHar()&lt;/code&gt; docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/api/class-locator#locator-drop&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;locator.drop()&lt;/code&gt; docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/api/class-page#page-aria-snapshot&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;ariaSnapshot({ boxes: true })&lt;/code&gt; docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>test-automation</category><category>ci</category><category>ai-qa</category></item><item><title> AI Test Automation Architecture: The 3-Layer System</title><link>https://www.anton.qa/blog/posts/ai-test-automation-architecture-3-layer-system</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/ai-test-automation-architecture-3-layer-system</guid><description>AI Test Automation Architecture: The 3-Layer System

AI test automation architecture is the system that tells AI what to test.

It also defines how to run tests and prove the result.

I split it into three layers: orchestration, execution, and evidence.

Without all three, AI testing becomes prompt output with no production gate.</description><pubDate>Wed, 13 May 2026 23:32:00 GMT</pubDate><content:encoded>&lt;p&gt;AI test automation architecture is the system that tells AI what to test.&lt;/p&gt;
&lt;p&gt;It also defines how to run tests and prove the result.&lt;/p&gt;
&lt;p&gt;I split it into three layers: orchestration, execution, and evidence.&lt;/p&gt;
&lt;p&gt;Without all three, AI testing becomes prompt output with no production gate.&lt;/p&gt;
&lt;h2 id=&quot;why-tool-lists-fail&quot;&gt;Why tool lists fail&lt;/h2&gt;
&lt;p&gt;Most AI testing content starts with tools.&lt;/p&gt;
&lt;p&gt;That is backwards.&lt;/p&gt;
&lt;p&gt;AI means software that predicts.&lt;/p&gt;
&lt;p&gt;Predictions can help QA teams move faster.&lt;/p&gt;
&lt;p&gt;But predictions do not prove quality.&lt;/p&gt;
&lt;p&gt;A tool can generate a test.&lt;/p&gt;
&lt;p&gt;It cannot decide release risk alone.&lt;/p&gt;
&lt;p&gt;It cannot prove the browser state was clean.&lt;/p&gt;
&lt;p&gt;It cannot explain why a failure matters.&lt;/p&gt;
&lt;p&gt;That work belongs to architecture.&lt;/p&gt;
&lt;h2 id=&quot;the-3-layer-model&quot;&gt;The 3-layer model&lt;/h2&gt;
&lt;p&gt;I use three layers for AI test automation architecture.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Plain meaning&lt;/th&gt;
&lt;th&gt;Main question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Orchestration&lt;/td&gt;
&lt;td&gt;test control plan&lt;/td&gt;
&lt;td&gt;What risk should this cover?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;actual test run&lt;/td&gt;
&lt;td&gt;Did it run in the real pipeline?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;proof from runs&lt;/td&gt;
&lt;td&gt;Can a human review it?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;If one layer is missing, the system gets weak.&lt;/p&gt;
&lt;p&gt;If evidence is missing, the team gets false confidence.&lt;/p&gt;
&lt;h2 id=&quot;layer-1-orchestration&quot;&gt;Layer 1: Orchestration&lt;/h2&gt;
&lt;p&gt;Orchestration means test control plan.&lt;/p&gt;
&lt;p&gt;This layer defines the work before AI writes anything.&lt;/p&gt;
&lt;p&gt;It answers five questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What user flow matters?&lt;/li&gt;
&lt;li&gt;What risk does this test cover?&lt;/li&gt;
&lt;li&gt;What data must exist first?&lt;/li&gt;
&lt;li&gt;What browser state is allowed?&lt;/li&gt;
&lt;li&gt;What failure should block release?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;AI can help draft the first version.&lt;/p&gt;
&lt;p&gt;But a human still owns the risk call.&lt;/p&gt;
&lt;p&gt;That is the difference between generation and architecture.&lt;/p&gt;
&lt;h2 id=&quot;layer-2-execution&quot;&gt;Layer 2: Execution&lt;/h2&gt;
&lt;p&gt;Execution means actual test run.&lt;/p&gt;
&lt;p&gt;This layer proves the test can survive the real path.&lt;/p&gt;
&lt;p&gt;That path is usually CI.&lt;/p&gt;
&lt;p&gt;CI means automated build server.&lt;/p&gt;
&lt;p&gt;A local demo is useful.&lt;/p&gt;
&lt;p&gt;It is not enough.&lt;/p&gt;
&lt;p&gt;Run the test where code ships.&lt;/p&gt;
&lt;p&gt;Check browser state, cleanup, retries, test data, and worker isolation.&lt;/p&gt;
&lt;p&gt;This is where Playwright and MCP matter.&lt;/p&gt;
&lt;p&gt;Playwright is a browser test tool.&lt;/p&gt;
&lt;p&gt;MCP means tool connection standard.&lt;/p&gt;
&lt;p&gt;Together, they let AI agents use a live browser.&lt;/p&gt;
&lt;p&gt;But the run still needs stable launch control.&lt;/p&gt;
&lt;p&gt;That is why &lt;code&gt;playwright-mcp v0.0.75&lt;/code&gt; matters.&lt;/p&gt;
&lt;p&gt;It serialized shared browser launch in isolated mode.&lt;/p&gt;
&lt;p&gt;That means parallel runs get ordered startup.&lt;/p&gt;
&lt;p&gt;Small release note.&lt;/p&gt;
&lt;p&gt;Real architecture impact.&lt;/p&gt;
&lt;h2 id=&quot;layer-3-evidence&quot;&gt;Layer 3: Evidence&lt;/h2&gt;
&lt;p&gt;Evidence means proof from runs.&lt;/p&gt;
&lt;p&gt;This is the layer most teams skip.&lt;/p&gt;
&lt;p&gt;Every AI-created test should leave receipts.&lt;/p&gt;
&lt;p&gt;Useful receipts include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;trace&lt;/li&gt;
&lt;li&gt;screenshot&lt;/li&gt;
&lt;li&gt;log&lt;/li&gt;
&lt;li&gt;video when timing matters&lt;/li&gt;
&lt;li&gt;saved browser state when auth matters&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The point is simple.&lt;/p&gt;
&lt;p&gt;A reviewer should inspect the run without rerunning it.&lt;/p&gt;
&lt;p&gt;If that is impossible, the test is not ready.&lt;/p&gt;
&lt;p&gt;AI can write code quickly.&lt;/p&gt;
&lt;p&gt;Review still needs proof.&lt;/p&gt;
&lt;h2 id=&quot;a-practical-gate&quot;&gt;A practical gate&lt;/h2&gt;
&lt;p&gt;Here is the gate I use before AI-generated tests ship.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Pass condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;The test maps to one named risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Test data setup is explicit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Browser state is controlled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run&lt;/td&gt;
&lt;td&gt;The test passes in CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;Trace or equivalent proof exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;A human can explain the failure mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This is not heavy process.&lt;/p&gt;
&lt;p&gt;It is a small guardrail.&lt;/p&gt;
&lt;p&gt;It stops weak tests from becoming permanent debt.&lt;/p&gt;
&lt;h2 id=&quot;what-this-changes-for-qa-teams&quot;&gt;What this changes for QA teams&lt;/h2&gt;
&lt;p&gt;The goal is not to slow AI down.&lt;/p&gt;
&lt;p&gt;The goal is to make AI work reviewable.&lt;/p&gt;
&lt;p&gt;When the architecture is clear, AI becomes useful in three places:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It drafts coverage ideas.&lt;/li&gt;
&lt;li&gt;It writes first-pass test code.&lt;/li&gt;
&lt;li&gt;It explains failures from evidence.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;But humans still own the system.&lt;/p&gt;
&lt;p&gt;Humans define risk.&lt;/p&gt;
&lt;p&gt;Humans review evidence.&lt;/p&gt;
&lt;p&gt;Humans decide what blocks release.&lt;/p&gt;
&lt;h2 id=&quot;the-rule&quot;&gt;The rule&lt;/h2&gt;
&lt;p&gt;Never ask AI to expand test coverage first.&lt;/p&gt;
&lt;p&gt;Build the proof system before that.&lt;/p&gt;
&lt;p&gt;Generation is cheap.&lt;/p&gt;
&lt;p&gt;Evidence is the architecture.&lt;/p&gt;
&lt;p&gt;That is the line between AI testing demos and production QA.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>ai-testing</category><category>playwright</category><category>mcp</category><category>test-automation</category><category>qa-architecture</category></item><item><title>Drag-and-Drop Automation is Finally Native in Playwright MCP</title><link>https://www.anton.qa/blog/posts/drag-and-drop-automation</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/drag-and-drop-automation</guid><description>Native drag-and-drop in Playwright MCP cuts brittle mouse steps. Learn what changed, when to use it, and how QA teams should validate complex flows.</description><pubDate>Wed, 06 May 2026 20:25:00 GMT</pubDate><content:encoded>&lt;p&gt;Drag-and-Drop Automation is Finally Native in Playwright MCP&lt;/p&gt;
&lt;p&gt;I hate testing drag-and-drop.&lt;/p&gt;
&lt;p&gt;Most QA teams do. We either skip it or we write hacks.&lt;/p&gt;
&lt;p&gt;I have seen it all. Scripts that try to &amp;quot;flick&amp;quot; the mouse. Scripts that inject JavaScript.&lt;/p&gt;
&lt;p&gt;They all break. They are hard to maintain.&lt;/p&gt;
&lt;p&gt;This month, Playwright MCP v0.0.71 shipped a fix.&lt;/p&gt;
&lt;p&gt;It adds a tool called browser_drop.&lt;/p&gt;
&lt;p&gt;Why This Changes Things&lt;/p&gt;
&lt;p&gt;Before, your AI agent had to guess how to move an item.&lt;/p&gt;
&lt;p&gt;Now, it uses the native Playwright API: Locator.drop.&lt;/p&gt;
&lt;p&gt;It works for:&lt;/p&gt;
&lt;p&gt;Moving cards in a grid.&lt;/p&gt;
&lt;p&gt;Dropping files into an upload zone.&lt;/p&gt;
&lt;p&gt;Reordering lists.&lt;/p&gt;
&lt;p&gt;It works because it handles the mouse events for you.&lt;/p&gt;
&lt;p&gt;No more &amp;quot;faking&amp;quot; the move.&lt;/p&gt;
&lt;p&gt;How to Use It&lt;/p&gt;
&lt;p&gt;Here is one line of code to use in your MCP tool:&lt;/p&gt;
&lt;p&gt;That is it. One command.&lt;/p&gt;
&lt;p&gt;The AI agent finds the file. It moves it to the zone. It drops it.&lt;/p&gt;
&lt;p&gt;The &amp;quot;Below the Fold&amp;quot; Gotcha&lt;/p&gt;
&lt;p&gt;There is one small catch.&lt;/p&gt;
&lt;p&gt;Playwright needs the items to be on your screen to drop them.&lt;/p&gt;
&lt;p&gt;If your list is long and the target is at the bottom, the drop will fail.&lt;/p&gt;
&lt;p&gt;The Fix: Scroll the target into view first.&lt;/p&gt;
&lt;p&gt;This simple step stops your tests from failing in CI (where screens are smaller).&lt;/p&gt;
&lt;p&gt;What Else Is New?&lt;/p&gt;
&lt;p&gt;The v0.0.71 release added two more features:&lt;/p&gt;
&lt;p&gt;Network Bodies: browser_network_requests now captures the full response from your server. You can check what the API said, not just the status code.&lt;/p&gt;
&lt;p&gt;Plain Code: browser_evaluate now takes plain JavaScript. You do not need a function wrapper anymore.&lt;/p&gt;
&lt;p&gt;Verdict&lt;/p&gt;
&lt;p&gt;If you use AI agents for testing, upgrade today.&lt;/p&gt;
&lt;p&gt;browser_drop removes the last big &amp;quot;hack&amp;quot; in our pipeline.&lt;/p&gt;
&lt;p&gt;I architect the systems AI agents run on. Native tools like this make those systems stable.&lt;/p&gt;
&lt;p&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>Playwright</category><category>MCP</category><category>AI</category></item><item><title>How to Test MCP Servers Before They Break Your CI</title><link>https://www.anton.qa/blog/posts/mcp-server-testing-production-checklist</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/mcp-server-testing-production-checklist</guid><description>Most teams install MCP servers and hope they work. Here is how to test, evaluate, and gate MCP servers before they break your CI pipeline.</description><pubDate>Wed, 06 May 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;How to Test MCP Servers Before They Break Your CI&lt;/h1&gt;
&lt;p&gt;Most teams install an MCP server and hope it works.&lt;/p&gt;
&lt;p&gt;That is how you get 3 AM pages.&lt;/p&gt;
&lt;p&gt;An MCP server is a bridge between AI agents and your tools. It can crash, leak data, or silently return garbage. If your AI agent relies on it, your whole pipeline breaks.&lt;/p&gt;
&lt;p&gt;I have seen it happen. A team wired an untested file-system MCP server into a customer support agent. The agent deleted a config folder because the server had no path sandboxing. The fix was simple: test the server&amp;#39;s permission model before wiring it in.&lt;/p&gt;
&lt;p&gt;This post is that test. It is the checklist I run on every MCP server before it touches production.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-an-mcp-server-is-in-plain-words&quot;&gt;What an MCP server is (in plain words)&lt;/h2&gt;
&lt;p&gt;MCP stands for Model Context Protocol. It is a standard way for AI agents to talk to external tools like browsers, databases, or file systems.&lt;/p&gt;
&lt;p&gt;Think of it like a USB port for AI. The agent plugs into the server, and the server gives it access to a tool.&lt;/p&gt;
&lt;p&gt;The danger is that the agent does not know what the tool can do. If the server has no guardrails, the agent can ask it to do anything: read files, delete data, call external APIs.&lt;/p&gt;
&lt;p&gt;That is why we test the server, not just the agent.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-three-layer-test-stack&quot;&gt;The three-layer test stack&lt;/h2&gt;
&lt;p&gt;I test MCP servers in three layers. Each layer catches different failure modes.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Discovery&lt;/td&gt;
&lt;td&gt;Missing tools, broken metadata, wrong version&lt;/td&gt;
&lt;td&gt;MCP Inspector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior&lt;/td&gt;
&lt;td&gt;Silent failures, wrong output, edge-case crashes&lt;/td&gt;
&lt;td&gt;pytest smoke tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Over-permissions, data leaks, injection risks&lt;/td&gt;
&lt;td&gt;Permission audit + static scan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;If a server fails any layer, it does not ship.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;layer-1-discovery-with-mcp-inspector&quot;&gt;Layer 1: Discovery with MCP Inspector&lt;/h2&gt;
&lt;p&gt;MCP Inspector is the official debugging tool for MCP servers. It is free and runs in your browser.&lt;/p&gt;
&lt;p&gt;Start it with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @anthropic-ai/mcp-inspector node dist/server.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then check these three things:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Does the server start without errors?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If the Inspector shows a red error on launch, the server has a dependency or initialization bug. Fix that first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Does it list the tools it promises?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Open the &amp;quot;Tools&amp;quot; tab. Count them. Compare to the README. If the README promises 5 tools and the Inspector shows 3, the server is incomplete.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Does a sample request return the right shape?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pick the simplest tool. Fire a request. Check that the response is JSON, has the right fields, and makes sense.&lt;/p&gt;
&lt;p&gt;If the response is a plain string instead of structured JSON, your agent will fail to parse it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;layer-2-behavior-with-pytest-smoke-tests&quot;&gt;Layer 2: Behavior with pytest smoke tests&lt;/h2&gt;
&lt;p&gt;MCP Inspector is great for manual checks. But manual checks do not scale. You need automated tests in CI.&lt;/p&gt;
&lt;p&gt;Here is a minimal pytest test that initializes a server and verifies it responds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import subprocess
import json
import pytest

@pytest.fixture
def mcp_server():
    proc = subprocess.Popen(
        [&amp;quot;npx&amp;quot;, &amp;quot;-y&amp;quot;, &amp;quot;@anthropic-ai/mcp-server-filesystem&amp;quot;, &amp;quot;/tmp/mcp-test&amp;quot;],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        text=True
    )
    yield proc
    proc.terminate()
    proc.wait(timeout=5)

def send_message(proc, method, params=None):
    msg = {
        &amp;quot;jsonrpc&amp;quot;: &amp;quot;2.0&amp;quot;,
        &amp;quot;id&amp;quot;: 1,
        &amp;quot;method&amp;quot;: method,
        &amp;quot;params&amp;quot;: params or {}
    }
    proc.stdin.write(json.dumps(msg) + &amp;quot;\n&amp;quot;)
    proc.stdin.flush()
    return json.loads(proc.stdout.readline())

def test_server_initializes(mcp_server):
    response = send_message(mcp_server, &amp;quot;initialize&amp;quot;, {
        &amp;quot;protocolVersion&amp;quot;: &amp;quot;2024-11-05&amp;quot;,
        &amp;quot;capabilities&amp;quot;: {},
        &amp;quot;clientInfo&amp;quot;: {&amp;quot;name&amp;quot;: &amp;quot;test&amp;quot;, &amp;quot;version&amp;quot;: &amp;quot;1.0&amp;quot;}
    })
    assert response[&amp;quot;id&amp;quot;] == 1
    assert &amp;quot;result&amp;quot; in response

def test_tool_list_not_empty(mcp_server):
    send_message(mcp_server, &amp;quot;initialize&amp;quot;, {
        &amp;quot;protocolVersion&amp;quot;: &amp;quot;2024-11-05&amp;quot;,
        &amp;quot;capabilities&amp;quot;: {},
        &amp;quot;clientInfo&amp;quot;: {&amp;quot;name&amp;quot;: &amp;quot;test&amp;quot;, &amp;quot;version&amp;quot;: &amp;quot;1.0&amp;quot;}
    })
    response = send_message(mcp_server, &amp;quot;tools/list&amp;quot;)
    assert &amp;quot;result&amp;quot; in response
    assert len(response[&amp;quot;result&amp;quot;][&amp;quot;tools&amp;quot;]) &amp;gt; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this in CI. If the server fails to initialize, the build stops.&lt;/p&gt;
&lt;p&gt;For Python-based servers, use FastMCP&amp;#39;s in-memory testing. It runs without a subprocess:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastmcp import FastMCP
import pytest

mcp = FastMCP(&amp;quot;test&amp;quot;)

@mcp.tool()
def add(a: int, b: int) -&amp;gt; int:
    return a + b

def test_add_tool():
    result = mcp.call_tool(&amp;quot;add&amp;quot;, {&amp;quot;a&amp;quot;: 2, &amp;quot;b&amp;quot;: 3})
    assert result == 5
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;layer-3-security-with-a-permission-audit&quot;&gt;Layer 3: Security with a permission audit&lt;/h2&gt;
&lt;p&gt;Every MCP server requests permissions. Most teams click &amp;quot;allow all&amp;quot; and move on.&lt;/p&gt;
&lt;p&gt;I check three things:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Does it need file system access? If yes, which paths?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A file server that requests &lt;code&gt;/&lt;/code&gt; can read your entire disk. A good server requests a single folder, like &lt;code&gt;/tmp/mcp-workspace&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Does it make network calls? To which hosts?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A server that calls &lt;code&gt;api.github.com&lt;/code&gt; is fine. A server that calls any host is a data exfiltration risk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Does it run shell commands? Under which user?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Shell access is the highest risk. If the server runs as root, any injected command can destroy the system.&lt;/p&gt;
&lt;p&gt;I block these combinations:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File access&lt;/th&gt;
&lt;th&gt;Network access&lt;/th&gt;
&lt;th&gt;Shell access&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Single folder&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single folder&lt;/td&gt;
&lt;td&gt;Whitelisted host&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any folder&lt;/td&gt;
&lt;td&gt;Any host&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Review required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any folder&lt;/td&gt;
&lt;td&gt;Any host&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-ci-gate&quot;&gt;The CI gate&lt;/h2&gt;
&lt;p&gt;After the three-layer check, I add one more step to CI: a gate script that runs all checks and outputs a report.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# mcp-gate.sh — run before any MCP server ships

SERVER=$1
EXIT_CODE=0

echo &amp;quot;=== MCP Server Gate: $SERVER ===&amp;quot;

# Layer 1: Discovery
npx @anthropic-ai/mcp-inspector $SERVER --headless || EXIT_CODE=1

# Layer 2: Behavior
pytest tests/mcp-smoke/ --server=$SERVER || EXIT_CODE=1

# Layer 3: Security
node scripts/audit-mcp-permissions.js $SERVER || EXIT_CODE=1

if [ $EXIT_CODE -eq 0 ]; then
    echo &amp;quot;✅ Gate passed. Server can ship.&amp;quot;
else
    echo &amp;quot;❌ Gate failed. Fix the issues above.&amp;quot;
fi

exit $EXIT_CODE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this in CI. If it fails, the deployment stops.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-to-find-servers-worth-testing&quot;&gt;Where to find servers worth testing&lt;/h2&gt;
&lt;p&gt;The MCP ecosystem moves fast. Here are the three places I look:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Official MCP Registry&lt;/strong&gt; — &lt;a href=&quot;https://registry.modelcontextprotocol.io&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://registry.modelcontextprotocol.io&lt;/a&gt;&lt;br&gt;Microsoft now publishes Playwright MCP here. Any server in this registry has at least passed a basic review.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt; — Search &lt;code&gt;modelcontextprotocol&lt;/code&gt; topics. Check last commit date and test coverage before installing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;npm / pip&lt;/strong&gt; — Search &lt;code&gt;@anthropic-ai/mcp-server-*&lt;/code&gt; or &lt;code&gt;mcp-server-*&lt;/code&gt;. Read the README. Check the download count.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Red flags:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No commits in 6+ months&lt;/li&gt;
&lt;li&gt;No tests in the repo&lt;/li&gt;
&lt;li&gt;No README explaining what it does&lt;/li&gt;
&lt;li&gt;Permission requests that are too broad&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;verdict&quot;&gt;Verdict&lt;/h2&gt;
&lt;p&gt;Testing MCP servers is not optional. An untested server is a bug waiting to become an incident.&lt;/p&gt;
&lt;p&gt;The three-layer stack — Discovery, Behavior, Security — catches the failure modes I have seen in production. MCP Inspector for manual checks. pytest for CI gates. A permission audit for the last line of defense.&lt;/p&gt;
&lt;p&gt;Playwright MCP ships often and is published in the official MCP Registry. If you are building AI test infrastructure, start there. It has the audit trail and the active maintenance that production work requires.&lt;/p&gt;
&lt;p&gt;Start with one server this week. Run it through the checklist. That is how you build AI infrastructure that does not wake you up at 3 AM.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anton Gulin is an AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET, now Lead Software Engineer in Test. Find him at &lt;a href=&quot;https://anton.qa&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;anton.qa&lt;/a&gt; or on &lt;a href=&quot;https://linkedin.com/in/antongulin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>mcp</category><category>ai-testing</category><category>playwright</category><category>ci-cd</category><category>security</category></item><item><title>Playwright MCP v0.0.73: How to Configure Browser Paths via Environment Variables</title><link>https://www.anton.qa/blog/posts/playwright-mcp-v0-0-73</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-mcp-v0-0-73</guid><description>Playwright MCP v0.0.73 fixes a critical gap where extension channels and executable paths couldn&apos;t be resolved from CI/CD environment variables. This release enables true containerized test pipelines—configure browser installations once in your Dockerfile, override per job via environment variables, no hardcoded paths required.</description><pubDate>Sat, 02 May 2026 21:19:00 GMT</pubDate><content:encoded>&lt;p&gt;TL;DR&lt;/p&gt;
&lt;p&gt;Playwright MCP v0.0.73 fixes a critical gap where extension channels and executable paths couldn&amp;#39;t be resolved from CI/CD environment variables. This release enables true containerized test pipelines—configure browser installations once in your Dockerfile, override per job via environment variables, no hardcoded paths required.&lt;/p&gt;
&lt;p&gt;The Release&lt;/p&gt;
&lt;p&gt;Playwright MCP v0.0.73 shipped two interconnected bug fixes that resolve a pain point I have seen break containerized test pipelines at scale.&lt;/p&gt;
&lt;p&gt;The first fix resolves extension channel and executablePath from CLI flags and environment variables (#40572). The second propagates --browser channel flags on the --extension path (#40567). Combined, these changes mean your Playwright MCP setup can now be fully environment-driven—critical for organizations running tests in Docker, Kubernetes, or ephemeral CI workers.&lt;/p&gt;
&lt;p&gt;Additionally, the package is now published to the official MCP Registry, which simplifies enterprise procurement and governance for teams evaluating AI-assisted testing infrastructure.&lt;/p&gt;
&lt;p&gt;Why This Matters for Engineers and QA&lt;/p&gt;
&lt;p&gt;If you are running Playwright in containers, you have probably hit the hardcoded path problem. You install Chromium in your Dockerfile at /usr/bin/chromium-browser, but your CI system runs on macOS runners where the path is /Applications/Chromium.app, and your staging environment uses a custom Brave installation somewhere in /opt/brave. Without environment variable support, you end up maintaining per-environment Dockerfiles, CI configs with conditional logic, or wrapper scripts that fragment your test infrastructure.&lt;/p&gt;
&lt;p&gt;This is not a theoretical problem. I have seen teams abandon containerized Playwright testing entirely because the path resolution complexity outweighed the benefits of ephemeral environments. The fix in v0.0.73 closes that gap.&lt;/p&gt;
&lt;p&gt;How to Use It&lt;/p&gt;
&lt;p&gt;With this release, you can configure browser channels and executable paths through environment variables, making your Playwright MCP configuration portable across environments. Here is the working pattern:&lt;/p&gt;
&lt;p&gt;Or in your Node.js test runner:&lt;/p&gt;
&lt;p&gt;For CI/CD pipelines, the pattern becomes environment-agnostic:&lt;/p&gt;
&lt;p&gt;The --browser channel now propagates correctly when --extension paths are specified, meaning parallel test workers can target different browsers without configuration conflicts.&lt;/p&gt;
&lt;p&gt;The Gotcha Nobody Is Talking About&lt;/p&gt;
&lt;p&gt;Here is the honest gotcha: this fix only works if your environment variables are being read by the Playwright process, not the MCP server subprocess. If you are running Playwright MCP as a standalone server (common in AI-assisted testing setups), environment variables set in your shell may not propagate to the MCP process spawned by your AI tool.&lt;/p&gt;
&lt;p&gt;Test this before assuming it works:&lt;/p&gt;
&lt;p&gt;If your AI coding assistant spawns the MCP server, you may need to pass environment variables through the MCP configuration file or your AI tool&amp;#39;s environment settings, not just the shell.&lt;/p&gt;
&lt;p&gt;What This Changes in Your CI Pipeline&lt;/p&gt;
&lt;p&gt;With v0.0.73, you can standardize your Playwright MCP configuration across all environments. The path resolution hierarchy is now:&lt;/p&gt;
&lt;p&gt;CLI flags (highest priority)&lt;/p&gt;
&lt;p&gt;Environment variables&lt;/p&gt;
&lt;p&gt;Config file defaults&lt;/p&gt;
&lt;p&gt;Built-in channel defaults&lt;/p&gt;
&lt;p&gt;This means your Docker base image can ship with a sensible default browser path, your CI jobs override it via environment variables, and your local development uses the config file. No more per-environment Dockerfile maintenance.&lt;/p&gt;
&lt;p&gt;For teams targeting faster deployments, this also removes a class of environment-specific debugging that typically consumes 15-30 minutes per incident. The 50% faster deployment metric I have seen at CooperVision includes eliminating these friction points from CI pipelines.&lt;/p&gt;
&lt;p&gt;Verdict&lt;/p&gt;
&lt;p&gt;Playwright MCP v0.0.73 is a quiet but significant release for teams running containerized test infrastructure. The MCP Registry publication is nice for enterprise discoverability, but the bug fixes around environment-driven browser resolution are the substantive change. If you are managing Playwright across multiple environments, update now and audit your environment variable configuration to take full advantage of the new resolution order.&lt;/p&gt;
&lt;p&gt;The gotcha about subprocess environment propagation is real—test your specific setup before deploying to production. Environment variables that work in your shell may not propagate correctly through MCP server spawns in AI tooling contexts.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://x.com/aiwithanton/status/2051411902216163582?s=20&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://x.com/aiwithanton/status/2051411902216163582?s=20&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For full release details, see the official release notes.&lt;/p&gt;
&lt;p&gt;Anton Gulin is an AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET, current Lead Software Engineer in Test at CooperVision. Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>Playwright</category><category>AI</category><category>MCP</category></item><item><title>browser_run_code_unsafe: The Naming Change That Forces Honest Architecture</title><link>https://www.anton.qa/blog/posts/browser_run_code_unsafe</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/browser_run_code_unsafe</guid><description>Playwright MCP v0.0.72 renamed browser_run_code to browser_run_code_unsafe, making sandbox escape risk explicit. The new browser_network_request tool enables indexed request inspection. Teams using this MCP server in CI pipelines need to update tool references and security documentation now.</description><pubDate>Thu, 30 Apr 2026 23:46:00 GMT</pubDate><content:encoded>&lt;p&gt;TL;DR&lt;/p&gt;
&lt;p&gt;Playwright MCP v0.0.72 renamed browser_run_code to browser_run_code_unsafe, making sandbox escape risk explicit. The new browser_network_request tool enables indexed request inspection. Teams using this MCP server in CI pipelines need to update tool references and security documentation now.&lt;/p&gt;
&lt;p&gt;The Release&lt;/p&gt;
&lt;p&gt;Microsoft shipped playwright-mcp v0.0.72 on April 30, 2026 with two headline changes: a renamed tool that forces explicit acknowledgment of security boundaries, and a new indexed network request API.&lt;/p&gt;
&lt;p&gt;The tool formerly known as browser_run_code is now browser_run_code_unsafe. The rename surfaces a capability that was always there—arbitrary code execution within the Playwright browser context—and makes the sandbox implications impossible to miss in code reviews, CI configs, and architecture docs.&lt;/p&gt;
&lt;p&gt;Also new: browser_network_requests now returns a numbered list, and browser_network_request lets you fetch any single request by index, including headers and body. Both tools accept a filename option for direct file output.&lt;/p&gt;
&lt;p&gt;Why This Matters for Engineers, QA, and CI&lt;/p&gt;
&lt;p&gt;I&amp;#39;ve seen what happens when security assumptions live in the gap between &amp;quot;works in dev&amp;quot; and &amp;quot;approved for prod.&amp;quot; At CooperVision, where I&amp;#39;ve led test infrastructure through a 40% faster test execution migration from Selenium to Playwright, the gap between a tool&amp;#39;s name and its actual risk profile costs real time.&lt;/p&gt;
&lt;p&gt;When a tool is named browser_run_code, it&amp;#39;s easy to approve it in a security review without asking what code actually runs. When it&amp;#39;s named browser_run_code_unsafe, you can&amp;#39;t pretend the sandbox boundary is theoretical. This rename is documentation embedded in the API contract.&lt;/p&gt;
&lt;p&gt;For QA architects integrating MCP servers into CI pipelines, this matters in three places: security review templates, tool permission configs in your MCP client, and any internal docs that reference the old name.&lt;/p&gt;
&lt;p&gt;How to Use It&lt;/p&gt;
&lt;p&gt;The new indexed request API is straightforward. Here&amp;#39;s a TypeScript example using the new browser_network_request with an index:&lt;/p&gt;
&lt;p&gt;For the renamed tool, update your tool references:&lt;/p&gt;
&lt;p&gt;The functionality is identical. The name changed.&lt;/p&gt;
&lt;p&gt;The Gotcha Nobody Is Talking About&lt;/p&gt;
&lt;p&gt;The rename doesn&amp;#39;t change behavior—but it will break your existing integrations silently if you&amp;#39;re using tool name matching in your MCP client config. If you hardcoded browser_run_code in a tool registry or a Claude Desktop config file, you&amp;#39;ll get a &amp;quot;tool not found&amp;quot; error with no explanation that a rename is the cause.&lt;/p&gt;
&lt;p&gt;The unhandled rejection fix (previously crashing the server on unhandled errors in browser_run_code_unsafe) means old error patterns in your logs may disappear. That&amp;#39;s good, but if you had monitoring that expected crashes on certain error types, you&amp;#39;ll need to update alert thresholds.&lt;/p&gt;
&lt;p&gt;What This Changes in Your CI Pipeline&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re running Playwright MCP in CI—using it to drive browser automation for end-to-end tests or visual regression checks—the rename forces a config audit. In practice, this is a one-line change in your tool mapping, but it should trigger a broader review of what other tools you&amp;#39;re calling and whether their names still match upstream.&lt;/p&gt;
&lt;p&gt;The new filename option for network tools solves a real CI artifact problem: network logs that used to need in-memory buffering and post-processing can now write directly to the filesystem, making them available for artifact upload without extra code.&lt;/p&gt;
&lt;p&gt;At CooperVision, I&amp;#39;ve shipped changes that cut deployment times in half partly because artifact handling got smarter. Direct file output from MCP tools is that kind of small win that compounds across hundreds of test runs.&lt;/p&gt;
&lt;p&gt;Migration Notes&lt;/p&gt;
&lt;p&gt;Update all tool name references from browser_run_code to browser_run_code_unsafe&lt;/p&gt;
&lt;p&gt;Review security documentation to reflect the explicit &amp;quot;unsafe&amp;quot; designation&lt;/p&gt;
&lt;p&gt;Check MCP client configs for hardcoded tool names&lt;/p&gt;
&lt;p&gt;The filename parameter is backward-compatible (optional, existing behavior unchanged)&lt;/p&gt;
&lt;p&gt;The indexed request API is additive; existing browser_network_requests behavior works as before&lt;/p&gt;
&lt;p&gt;Verdict&lt;/p&gt;
&lt;p&gt;The browser_run_code_unsafe rename is the right call. It&amp;#39;s an API contract change that forces honest conversations about what your automation tools can actually do. In enterprise environments where QA architects own security review sign-off, explicit naming prevents the kind of assumption drift that creates vulnerabilities.&lt;/p&gt;
&lt;p&gt;The new indexed network request API is a solid ergonomic improvement—pinpoint access to individual requests without parsing full lists. Combined with the filename output option, it makes Playwright MCP a more capable tool for CI pipelines that need artifact generation.&lt;/p&gt;
&lt;p&gt;This is a non-breaking change for functionality, but a breaking change for tool name references. Audit your configs, update your docs, and move on. The 40% faster test execution gains from the Selenium→Playwright migration taught me that staying current on toolchain changes pays off—it&amp;#39;s rarely as painful as it feels in the moment.&lt;/p&gt;
&lt;p&gt;Upgrade and update your tool names. Then look at what you can do with indexed request inspection that you couldn&amp;#39;t before.&lt;/p&gt;
&lt;p&gt;Anton Gulin is an AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET, current Lead Software Engineer in Test at CooperVision. Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>Playwright</category><category>MCP</category><category>AI</category></item><item><title>Playwright CLI v0.1.10 Brings Spec-Driven Testing Skills for AI Agents</title><link>https://www.anton.qa/blog/posts/playwright-cli-v0-1-10-brings</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-cli-v0-1-10-brings</guid><description>Playwright CLI v0.1.10 introduces a spec-driven testing skill that guides AI agents through plan/generate/heal workflows for maintaining test suites from written specifications. Network inspection now uses stable request indexing, and raw output is default for all data-fetching commands—eliminating preprocessing steps in CI pipelines.</description><pubDate>Thu, 30 Apr 2026 23:39:00 GMT</pubDate><content:encoded>&lt;p&gt;TL;DR&lt;/p&gt;
&lt;p&gt;Playwright CLI v0.1.10 introduces a spec-driven testing skill that guides AI agents through plan/generate/heal workflows for maintaining test suites from written specifications. Network inspection now uses stable request indexing, and raw output is default for all data-fetching commands—eliminating preprocessing steps in CI pipelines.&lt;/p&gt;
&lt;p&gt;The release&lt;/p&gt;
&lt;p&gt;Playwright CLI v0.1.10 shipped on April 30th with two headline features that reshape how AI agents interact with browser automation. The network inspection subsystem got a complete overhaul—network is gone, replaced by requests plus granular subcommands that output indexed, pipe-friendly data. But the feature I&amp;#39;m actually excited about is the spec-driven testing skill: a references/spec-driven-testing.md reference that teaches AI agents to drive Playwright tests from written specifications.&lt;/p&gt;
&lt;p&gt;This matters because I spend my days building test infrastructure that scales. At CooperVision, I oversaw a 300% increase in test count while hitting 50% faster deployments. That didn&amp;#39;t happen by writing more tests manually—it happened by building workflows that let the system do the repetitive work. The spec-driven testing skill is exactly that kind of workflow accelerator.&lt;/p&gt;
&lt;p&gt;Why this matters for engineers and QA architects&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re running AI-augmented QA workflows, you know the problem: agents can generate test code, but without a structured pattern for keeping that code alive, regressions pile up and the suite rots. The spec-driven testing skill provides that structure.&lt;/p&gt;
&lt;p&gt;The references/spec-driven-testing.md file outlines a plan/generate/heal cycle. An agent reads a written spec, plans which Playwright assertions map to the spec&amp;#39;s behavior, generates the corresponding test code, then heals regressions when the spec changes or the app drifts. This isn&amp;#39;t hypothetical—I&amp;#39;ve seen this pattern work at scale when migrating from Selenium to Playwright, where we achieved 40% faster test execution.&lt;/p&gt;
&lt;p&gt;For CI engineers, the network inspection overhaul matters more immediately. The old network command inlined bodies and required brittle string parsing. The new numbered commands (requests, request &lt;num&gt;, request-headers &lt;num&gt;, request-body &lt;num&gt;) output stable indexes and pipe-friendly data. You can pipe directly to jq without stripping wrapper text.&lt;/p&gt;
&lt;p&gt;How to use it&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the spec-driven testing workflow in practice. The skill lives at references/spec-driven-testing.md and gets loaded automatically when you&amp;#39;re running Playwright CLI with agent-mode enabled:&lt;/p&gt;
&lt;h1&gt;Start Playwright CLI with agent mode&lt;/h1&gt;
&lt;p&gt;npx playwright-cli --agent&lt;/p&gt;
&lt;p&gt;For the network inspection overhaul, here&amp;#39;s a working example:&lt;/p&gt;
&lt;p&gt;The raw output change is significant: data-fetching commands like cookie-list, localstorage-list, and route-list now emit unwrapped output by default. Your CI scripts drop 1-2 preprocessing steps per invocation.&lt;/p&gt;
&lt;p&gt;The gotcha nobody is talking about&lt;/p&gt;
&lt;p&gt;The spec-driven testing skill is read-only in this release. You get the reference file and the pattern, but there&amp;#39;s no built-in mechanism for the agent to automatically detect spec changes and trigger heals. You have to wire that yourself. In my experience, the first implementation is always manual—you define the trigger conditions, the diff logic, the heal policy. The skill gives you the pattern; you build the automation.&lt;/p&gt;
&lt;p&gt;This is honest: don&amp;#39;t expect autonomous test maintenance out of the box. Plan for 2-4 weeks of integration work to wire the heal step into your CI pipeline, depending on your test suite size and the stability of your application contract.&lt;/p&gt;
&lt;p&gt;What this changes in your CI pipeline&lt;/p&gt;
&lt;p&gt;Three concrete changes:&lt;/p&gt;
&lt;p&gt;Network data is now scriptable. You can pull request headers, bodies, and responses directly into CI steps without parsing wrapper text. I estimate this saves 15-30 minutes of scripting work per team per quarter.&lt;/p&gt;
&lt;p&gt;Spec-driven test maintenance becomes possible. If you&amp;#39;re running AI agents for QA, the pattern exists now. You still need to implement the trigger/heal logic, but the framework is there.&lt;/p&gt;
&lt;p&gt;MCP server stability improves. Unhandled promise rejections from user callbacks no longer crash the transport. If you&amp;#39;re running Playwright CLI as a long-running MCP server (common in AI-augmented workflows), this is meaningful reliability.&lt;/p&gt;
&lt;p&gt;Migration notes&lt;/p&gt;
&lt;p&gt;The network command is replaced. If you have scripts that parse network output, they need updating:&lt;/p&gt;
&lt;p&gt;Data-fetching commands (cookie-list, route-list, etc.) now emit raw output. If you were stripping the ### Result wrapper, remove that logic—it&amp;#39;s gone.&lt;/p&gt;
&lt;p&gt;Config-relative path resolution for initPage and initScript now resolves against the config directory (matching Vite/Vitest/ESLint behavior). If you were working around silent load failures, those workarounds are no longer needed.&lt;/p&gt;
&lt;p&gt;Verdict&lt;/p&gt;
&lt;p&gt;Playwright CLI v0.1.10 is a meaningful release for AI QA architects and CI engineers alike. The spec-driven testing skill is the headline for teams adopting AI-augmented workflows—it provides the pattern, even if the automation is still DIY. The network inspection overhaul is the practical win for everyone else: stable indexing, pipe-friendly output, raw by default.&lt;/p&gt;
&lt;p&gt;My recommendation: upgrade, wire the MCP server stability fixes first (low risk, immediate benefit), then evaluate the network inspection changes for your CI scripts. The spec-driven skill is a longer-term investment—budget the integration time honestly, and you&amp;#39;ll get the payoff.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re already running Playwright in your CI pipeline, this release makes it easier to script and maintain. If you&amp;#39;re building AI-augmented QA workflows, the skill gives you a structure to teach your agents. Both audiences win.&lt;/p&gt;
&lt;p&gt;Anton Gulin is an AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET, current Lead Software Engineer in Test at CooperVision. Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>Playwright</category><category>CLI</category><category>AI</category></item><item><title>The MCP Ecosystem Just Collapsed Into Playwright</title><link>https://www.anton.qa/blog/posts/the-mcp-ecosystem-just-collapsed-into-playwright</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/the-mcp-ecosystem-just-collapsed-into-playwright</guid><description>AI agents made it worse. They still get stuck at login screens. This month, that changed.

MCP (how AI talks to tools) is changing how we test. Two teams at Microsoft shipped a fix. They released two ways to share your browser with AI.

Playwright core added browser.bind().
The CLI team added an MCP Bridge.
Both land in the same month. This is not a test. It is the new way.</description><pubDate>Thu, 30 Apr 2026 00:32:00 GMT</pubDate><content:encoded>&lt;p&gt;I spent years fighting login scripts for tests.&lt;/p&gt;
&lt;p&gt;AI agents made it worse. They still get stuck at login screens. This month, that changed.&lt;/p&gt;
&lt;p&gt;MCP (how AI talks to tools) is changing how we test. Two teams at Microsoft shipped a fix. They released two ways to share your browser with AI.&lt;/p&gt;
&lt;p&gt;Playwright core added browser.bind().&lt;/p&gt;
&lt;p&gt;The CLI team added an MCP Bridge.&lt;/p&gt;
&lt;p&gt;Both land in the same month. This is not a test. It is the new way.&lt;/p&gt;
&lt;p&gt;The Drama: What This Replaces&lt;/p&gt;
&lt;p&gt;Browser tests have a &amp;quot;login gap.&amp;quot; Headless browsers (browsers without a window) start empty. They have no cookies, sessions, or secure login codes.&lt;/p&gt;
&lt;p&gt;When your app needs a company login, a headless browser fails. Teams solve it with hacks. We export cookies, make &amp;quot;special&amp;quot; accounts, or snapshot Docker images.&lt;/p&gt;
&lt;p&gt;I spent 20% of my time just keeping these hacks alive. These new tools don&amp;#39;t solve login. They skip it. The human logs in once. The agent uses the same browser.&lt;/p&gt;
&lt;p&gt;What browser.bind() Does&lt;/p&gt;
&lt;p&gt;browser.bind() shares the browser you are already using. It shares your cookies and your tabs. When you call it, Playwright creates a local data connection.&lt;/p&gt;
&lt;p&gt;Any AI agent connects to that pipe. It sees the exact same browser you see.&lt;/p&gt;
&lt;p&gt;This is for developers. One line of code. No more re-logging in. No more cookie scripts.&lt;/p&gt;
&lt;p&gt;What the MCP Bridge Extension Does&lt;/p&gt;
&lt;p&gt;The extension is the &amp;quot;no-code&amp;quot; path. Install it in Chrome and click one button. Your open tab now talks to your AI.&lt;/p&gt;
&lt;p&gt;Claude (the AI) sees the page. It can click and type. No terminal needed. Just one click.&lt;/p&gt;
&lt;p&gt;Before: Login -&amp;gt; export cookies -&amp;gt; start new browser.&lt;br&gt;After: Click the extension. Done.&lt;/p&gt;
&lt;p&gt;Both tools shipped in one month. They solve the same pain.&lt;/p&gt;
&lt;p&gt;The Architect&amp;#39;s Read: A New Standard&lt;/p&gt;
&lt;p&gt;This is a standard being born. Before, every team built their own bridge to the browser. Now, it is a standard feature.&lt;/p&gt;
&lt;p&gt;Two teams shipped one pattern in 30 days. The signal is clear. The browser is now a shared space. Humans and AI agents work on the same surface.&lt;/p&gt;
&lt;p&gt;What does this mean for QA? It means you stop babysitting login scripts. Your AI agent tests run where your humans test. The wall between &amp;quot;human&amp;quot; and &amp;quot;agent&amp;quot; testing is gone.&lt;/p&gt;
&lt;p&gt;What Most Teams Miss&lt;/p&gt;
&lt;p&gt;The win is not just a faster script. The win is &amp;quot;Shared State.&amp;quot; Before, we had parallel worlds. One human click and one agent click meant two browsers and two truths.&lt;/p&gt;
&lt;p&gt;Now they are one. When an AI agent finds a bug, you open the same browser. You see the same state and same session.&lt;/p&gt;
&lt;p&gt;No more &amp;quot;it works on my machine.&amp;quot; Same browser. Same bug.&lt;/p&gt;
&lt;p&gt;How to Try It Today&lt;/p&gt;
&lt;p&gt;If you use Playwright, browser.bind() is in v1.59.0. If you want the no-code path, get the extension. Then run playwright-cli attach --extension.&lt;/p&gt;
&lt;p&gt;Both connect your agent to a real, logged-in browser. Not a toy. Use the browser your team uses.&lt;/p&gt;
&lt;p&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>Playwright</category><category>AI</category><category>MCP</category></item><item><title>Native Drag-and-Drop Automation Arrives in Playwright MCP: What v0.0.71 Changes</title><link>https://www.anton.qa/blog/posts/native-drag-and-drop-automation-arrives-in-playwright-mcp-what-v0-0-71-changes</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/native-drag-and-drop-automation-arrives-in-playwright-mcp-what-v0-0-71-changes</guid><description>Playwright MCP v0.0.71 ships browser_drop. It gives you native drag-and-drop from any MCP client. No more evaluate scripts. No more mouse.move chains. Grid reordering, file drop zones, text editor drags — all work the same way a real user does.</description><pubDate>Tue, 28 Apr 2026 18:25:00 GMT</pubDate><content:encoded>&lt;p&gt;TL;DR&lt;/p&gt;
&lt;p&gt;Playwright MCP v0.0.71 ships browser_drop. It gives you native drag-and-drop from any MCP client. No more evaluate scripts. No more mouse.move chains. Grid reordering, file drop zones, text editor drags — all work the same way a real user does.&lt;/p&gt;
&lt;p&gt;Why This Release Matters&lt;/p&gt;
&lt;p&gt;QA teams either abandon drag-and-drop testing or hack around it. But sortable grids, file uploads, and rich text editors are everywhere. And they have been painful to test forever.&lt;/p&gt;
&lt;p&gt;I ran into this firsthand on one project. Solid Playwright coverage for clicks, typing, and navigation. But drag-and-drop? We used evaluate scripts. Or we tested it by hand. Both paths broke across browsers. Both were impossible to keep working.&lt;/p&gt;
&lt;p&gt;Playwright MCP v0.0.71 fixes this with browser_drop. It uses Playwright&amp;#39;s own Locator.drop — the same API your tests already use. Now any MCP client can call it.&lt;/p&gt;
&lt;p&gt;How to Use browser_drop&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a complete example combining browser_drop with the new response body capture from browser_network_requests and the simplified expression support in browser_evaluate. This pipeline automates a file upload scenario, validates the server response, and confirms the UI state update:&lt;/p&gt;
&lt;p&gt;Three new tools working together: browser_drop handles the drag. browser_network_requests captures the server response (full body, not just status codes). browser_evaluate runs plain JavaScript — no function wrapper needed.&lt;/p&gt;
&lt;p&gt;The Gotcha Nobody Is Talking About&lt;/p&gt;
&lt;p&gt;browser_drop needs both elements to be on screen. That&amp;#39;s correct Playwright behavior. But here&amp;#39;s the catch: if you navigate to a page and the drag target sits below the fold, the drop fails.&lt;/p&gt;
&lt;p&gt;The fix: Call browser_evaluate to scroll the target into view before calling browser_drop, or use the scroll option if your Playwright version supports it. This catches teams off guard in CI where viewport sizes are smaller than local development.&lt;/p&gt;
&lt;p&gt;This is not a bug. It&amp;#39;s how Playwright works. But it catches teams when they test on a big screen and deploy to CI. CI viewports are smaller. The element you tested locally is off screen in the pipeline.&lt;/p&gt;
&lt;p&gt;What This Changes in Your CI Pipeline&lt;/p&gt;
&lt;p&gt;With browser_drop, you can test drag-and-drop flows through MCP. Not by hand. Not with broken scripts.&lt;/p&gt;
&lt;p&gt;On one project, Selenium to Playwright gave us 40% faster tests. But drag-and-drop still broke in headless mode. We wrote evaluate scripts. They stopped working every sprint. browser_drop puts native drag-and-drop into MCP. No scripts. No workarounds.&lt;/p&gt;
&lt;p&gt;What this actually means:&lt;/p&gt;
&lt;p&gt;Fewer flaky tests. Native drag-and-drop is tested across browsers. evaluate + mouse.move sequences are not.&lt;/p&gt;
&lt;p&gt;Simpler AI test generation. AI tools call browser_drop directly. No fragile mouse chains.&lt;/p&gt;
&lt;p&gt;Faster CI. Native operations run faster than JavaScript-injected drag scripts.&lt;/p&gt;
&lt;p&gt;Verdict&lt;/p&gt;
&lt;p&gt;Playwright MCP v0.0.71 is worth upgrading for browser_drop alone. The response body capture and plain expression support make it better. But drag-and-drop was the missing piece. Now it&amp;#39;s there.&lt;/p&gt;
&lt;p&gt;The catch is real but small. Scroll your target into view before you drop. One line. Add it to your tool definitions and move on.&lt;/p&gt;
&lt;p&gt;If you run MCP-based test infrastructure, this kills the last reason to fall back to evaluate for drag-and-drop. Upgrade. Add the scroll guard. Ship.&lt;/p&gt;
&lt;p&gt;Reference: Playwright Locator.drop API documentation&lt;/p&gt;
&lt;p&gt;Anton Gulin is an AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET, now Lead Software Engineer in Test. Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>mcp</category><category>test-automation</category><category>ai-testing</category></item><item><title>Playwright Just Shipped the Fix For Flaky Tests I Built 3 Years Ago</title><link>https://www.anton.qa/blog/posts/playwright-just-shipped-the-fix-for-flaky-tests-i-built-3-years-ago</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-just-shipped-the-fix-for-flaky-tests-i-built-3-years-ago</guid><description>Playwright&apos;s new stabilization work echoes a flaky-test fix from enterprise QA. See the pattern, why it matters, and how teams can reduce noisy failures.</description><pubDate>Thu, 23 Apr 2026 17:12:00 GMT</pubDate><content:encoded>&lt;p&gt;Three years ago, I built a test framework that fixed itself. Nobody called it AI. &amp;quot;Agent&amp;quot; was still the thing antivirus ran on your laptop.&lt;/p&gt;
&lt;p&gt;I split the framework into three parts. I called them Planner, Generator, and Healer. Not because I read a paper. Those were the three jobs I needed. I was out of good names.&lt;/p&gt;
&lt;p&gt;Last October, Playwright shipped three Test Agents in version 1.56. Three of them.&lt;/p&gt;
&lt;p&gt;They call them Planner, Generator, and Healer.&lt;/p&gt;
&lt;p&gt;This month, version 1.59 shipped the rest of the plumbing. It added video recording inside tests (page.screencast). It added browser.bind(), so Claude or Cursor can connect to a running browser. It added async disposables (auto-cleanup for test resources). The agents shipped in October. Their plumbing shipped last week.&lt;/p&gt;
&lt;p&gt;So this post is about one thing. The same three-part system that saved my career just shipped as a feature in the tool everyone uses.&lt;/p&gt;
&lt;p&gt;Here is what Playwright got right. Here is what is still missing. And here is how to start using it today. Even if you stay on your own framework.&lt;/p&gt;
&lt;p&gt;If your tests fail at random, and someone keeps asking you to &amp;quot;just make the flaky tests pass&amp;quot; — read this.&lt;/p&gt;
&lt;p&gt;The Problem: the flaky-test cost nobody budgets for&lt;/p&gt;
&lt;p&gt;Here is a cost every engineering manager forgets: the flaky-test cost.&lt;/p&gt;
&lt;p&gt;One team I worked with had 1,200 end-to-end tests. About 4% failed at random on each run. Sounds small. It was not.&lt;/p&gt;
&lt;p&gt;4% random failures × 20 PR runs a day = ~1,000 fake failures a week&lt;/p&gt;
&lt;p&gt;Every fake failure starts a re-run, a check, a Slack thread&lt;/p&gt;
&lt;p&gt;On a good week, 3 engineers each lost a day to false failures&lt;/p&gt;
&lt;p&gt;On a bad week, it took the whole team for two sprints&lt;/p&gt;
&lt;p&gt;That is the flaky-test cost. It costs you people, not money. That is why budgets miss it. It shows up as missed deadlines, canceled demos, and tired engineers.&lt;/p&gt;
&lt;p&gt;The normal fix is &amp;quot;try harder.&amp;quot;&lt;/p&gt;
&lt;p&gt;Better locators (how tests find buttons on the page)&lt;/p&gt;
&lt;p&gt;Wait on the right event&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t trust the backend&lt;/p&gt;
&lt;p&gt;Park the bad tests in quarantine&lt;/p&gt;
&lt;p&gt;Review the quarantine every week&lt;/p&gt;
&lt;p&gt;All true. None is enough. You can try harder. Flaky tests keep growing.&lt;/p&gt;
&lt;p&gt;So I stopped fixing each test. I started fixing how all tests work together.&lt;/p&gt;
&lt;p&gt;The Drama: two weeks that broke me&lt;/p&gt;
&lt;p&gt;I won&amp;#39;t name the company. I will say this. My tests passed on my laptop. They failed only on clean CI builds. They failed when they ran beside another team&amp;#39;s tests.&lt;/p&gt;
&lt;p&gt;Sometimes they failed. Not every time. Always on Tuesday, between 10:14 AM and 10:22 AM.&lt;/p&gt;
&lt;p&gt;We lost two weeks. I tried everything. I tried everything again. I tried everything in a new order.&lt;/p&gt;
&lt;p&gt;On day 11, I stood at a whiteboard at 9 PM. The board was full of arrows. I finally saw the truth.&lt;/p&gt;
&lt;p&gt;The tests were fine. The framework was the problem.&lt;/p&gt;
&lt;p&gt;My framework thought the app was the only thing under test. It was not. The CI server was under test too. So was the database snapshot job. So was the deploy timing on the staging server.&lt;/p&gt;
&lt;p&gt;We fixed that one bug. But the two weeks taught me the big lesson:&lt;/p&gt;
&lt;p&gt;Fixing flaky tests is not a writing problem. It is a design problem.&lt;/p&gt;
&lt;p&gt;The tests don&amp;#39;t need more rules. The framework around them needs to be smarter.&lt;/p&gt;
&lt;p&gt;That is where the three-part system was born.&lt;/p&gt;
&lt;p&gt;The Solution: Planner, Generator, Healer&lt;/p&gt;
&lt;p&gt;Here is the whole system in short. The names are mine. The ideas are obvious once you stop pretending they are one job.&lt;/p&gt;
&lt;p&gt;Planner&lt;/p&gt;
&lt;p&gt;Job: read a feature, a user story, or a bug. Write a test plan.&lt;/p&gt;
&lt;p&gt;Not code. A plan. A list of flows, edge cases, set-up, clean-up. In plain Markdown.&lt;/p&gt;
&lt;p&gt;Why it is its own job: planning and writing are not the same skill. If one thing does both, tests drift from the plan. You get tests the agent can&amp;#39;t explain. And gaps where it had no example to copy.&lt;/p&gt;
&lt;p&gt;Plan first. Write later.&lt;/p&gt;
&lt;p&gt;What I built three years ago: a plan generator that read from PR descriptions, Jira tickets, and production alerts. It produced a Markdown plan. Engineers reviewed it before any code was written. About 85% of plans were approved as-is. The 15% that were rejected were caught in minutes. Not days of debugging.&lt;/p&gt;
&lt;p&gt;Generator&lt;/p&gt;
&lt;p&gt;Job: take an approved plan. Write the test code. Pick the button names. Write the checks. Set up the test.&lt;/p&gt;
&lt;p&gt;Why it is its own job: code writing works best with a narrow goal (one plan). Not a wide goal (the whole codebase). A focused generator with one plan beats a smart generator with the whole repo.&lt;/p&gt;
&lt;p&gt;What I built: a generator that turned plan Markdown into Playwright tests in TypeScript. It picked button names in a fixed order (data-testid first, then role, then text as last resort). It set up fixtures. It used soft checks by default. No creativity. One plan in, one test file out.&lt;/p&gt;
&lt;p&gt;Healer&lt;/p&gt;
&lt;p&gt;Job: a test fails. Check why.&lt;/p&gt;
&lt;p&gt;Is it a real bug? A button name that moved? Or a slow server that day?&lt;/p&gt;
&lt;p&gt;Fix the things you can. Flag the real bugs. Park the rest with notes.&lt;/p&gt;
&lt;p&gt;Why it is its own job: and this is the part no one wanted to hear. Healing is not &amp;quot;run it again until it passes.&amp;quot; That is hiding. Healing is three steps: check, propose a fix, get it reviewed.&lt;/p&gt;
&lt;p&gt;What I built: a Healer that compared the current page to the last green run. If the button name was stale, it proposed three new candidates. It scored each one. It opened a pull request with the best one-line change. A human reviewed it.&lt;/p&gt;
&lt;p&gt;Humans merged about 80% of those fixes. The other 20% were caught in review. That is exactly what a good Healer looks like.&lt;/p&gt;
&lt;p&gt;100% merged means humans aren&amp;#39;t reading&lt;/p&gt;
&lt;p&gt;20% merged means the Healer is broken&lt;/p&gt;
&lt;p&gt;80% merged means both sides are working&lt;/p&gt;
&lt;p&gt;The Numbers&lt;/p&gt;
&lt;p&gt;I don&amp;#39;t love numbers without a shop name. My rules don&amp;#39;t let me name the shop. So here is what I can tell you plainly:&lt;/p&gt;
&lt;p&gt;On one project, the three-part system let the test suite grow 3× in 18 months. The flaky-test rate stayed flat.&lt;/p&gt;
&lt;p&gt;On another, each engineer spent a third less time on broken tests in the first quarter.&lt;/p&gt;
&lt;p&gt;On a third, one CSS rename broke 100+ tests overnight. The Healer fixed it in one pull request by morning. The old way was a 3-week cleanup.&lt;/p&gt;
&lt;p&gt;These numbers are not magic. They come from splitting the work into three small jobs. And from watching the handoff between each job. If you already do this with your services, you already know why it works.&lt;/p&gt;
&lt;p&gt;Now Playwright Ships This As A Feature&lt;/p&gt;
&lt;p&gt;Playwright versions 1.58 and 1.59 shipped a set of Test Agents in VS Code and the command line:&lt;/p&gt;
&lt;p&gt;Planner agent — explores the app, writes test plans&lt;/p&gt;
&lt;p&gt;Generator agent — turns plans into test code&lt;/p&gt;
&lt;p&gt;Healer agent — fixes failing tests with AI help&lt;/p&gt;
&lt;p&gt;The release notes: v1.58 and v1.59. The agent APIs are browser.bind() and page.screencast.&lt;/p&gt;
&lt;p&gt;Same three jobs. Same split. Microsoft built what I built. They built it better in some ways. They missed one big thing.&lt;/p&gt;
&lt;p&gt;What Microsoft got right&lt;/p&gt;
&lt;p&gt;Each agent works alone. You can run Planner by itself. Pass its output to Generator. Never touch Healer. That split is the whole point. An agent system where everything is tangled is just one big prompt.&lt;/p&gt;
&lt;p&gt;The agents are optional. You don&amp;#39;t have to buy in all at once. Drop the Healer into your old tests. Leave Planner and Generator for later. That is how real teams adopt new tools.&lt;/p&gt;
&lt;p&gt;They shipped the plumbing, not just the agents. Two pieces matter:&lt;/p&gt;
&lt;p&gt;browser.bind() — added in v1.59. It lets any AI tool like Claude or Cursor connect to a running browser. No fresh browser. No lost cookies. No mocking your login.&lt;/p&gt;
&lt;p&gt;Playwright MCP Bridge — a free Chrome extension. It connects your open tabs to a local Playwright server. Your real cookies. Your real profile. Your real logged-in session.&lt;/p&gt;
&lt;p&gt;Together, those two things solve a problem QA teams have been hacking around for years. Let an AI agent work on your real browser. Not a fresh empty one. Microsoft built the plumbing. You don&amp;#39;t have to.&lt;/p&gt;
&lt;p&gt;What Microsoft missed&lt;/p&gt;
&lt;p&gt;The review loop.&lt;/p&gt;
&lt;p&gt;Self-healing is not a feature. It is a deal between the test, the app, and the team.&lt;/p&gt;
&lt;p&gt;The Healer will happily propose fixes. But who reviews them? Who sets the merge rules? Who steps in when the Healer&amp;#39;s fix rate drops? Playwright ships the agent. It does not ship the rules around the agent.&lt;/p&gt;
&lt;p&gt;Those rules are the hard part. And you have to build them. Whether you use Microsoft&amp;#39;s agents or your own.&lt;/p&gt;
&lt;p&gt;A Healer with no review loop is just a bug generator with a nice screen.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re on Selenium, Cypress, or something older, the migration math got better with v1.59 this month — but the pattern is portable. You don&amp;#39;t need Microsoft&amp;#39;s implementation to build this. You need:&lt;/p&gt;
&lt;p&gt;Plans as artifacts. Markdown. Version-controlled. Reviewable.&lt;/p&gt;
&lt;p&gt;Generators with narrow context. One plan in. One test file out. No repo-wide reasoning.&lt;/p&gt;
&lt;p&gt;A healer with a review loop. It proposes, a human approves, CI enforces. If the human always approves, your healer is working. If the human always rejects, your healer is broken. If it&amp;#39;s 80/20, it&amp;#39;s doing its job.&lt;/p&gt;
&lt;p&gt;What to Do Next (Even if You Don&amp;#39;t Migrate)&lt;/p&gt;
&lt;p&gt;If you already use Playwright, the path is simple. Try the Planner agent in VS Code next sprint. Feed it one real user story. Compare its plan to your plan. Do that 10 times. If you would hand its plans to a junior engineer, it works. That means you found a 2–3× speed boost.&lt;/p&gt;
&lt;p&gt;If you use Selenium, Cypress, or something older, migration got easier this month. But the system is portable. You don&amp;#39;t need Microsoft&amp;#39;s tools to build it. You need three things:&lt;/p&gt;
&lt;p&gt;Plans as files. Markdown. In git. Reviewable.&lt;/p&gt;
&lt;p&gt;A generator with a narrow goal. One plan in. One file out. No repo-wide thinking.&lt;/p&gt;
&lt;p&gt;A Healer with a review loop. It proposes. A human approves. CI enforces. 80% merge rate means it works.&lt;/p&gt;
&lt;p&gt;Start with the Healer if flaky tests block releases. Start with the Planner if you are short-staffed. Start with the Generator last. It is the flashy one. But it is the least useful without the other two.&lt;/p&gt;
&lt;p&gt;If your team doesn&amp;#39;t have this yet, print this post. Paste it in your design doc. Replace &amp;quot;I built&amp;quot; with &amp;quot;we can build.&amp;quot; Take it to your next architecture review.&lt;/p&gt;
&lt;p&gt;The Takeaway&lt;/p&gt;
&lt;p&gt;Three years ago, this system was a weird thing a weird architect built. Nothing off the shelf solved the problem.&lt;/p&gt;
&lt;p&gt;This month, it ships as a native feature in the tool serious web teams use. Last October, the agents shipped inside Playwright. This week&amp;#39;s v1.59 release added the production parts: video receipts, MCP interop (AI tool bridge), and async disposables.&lt;/p&gt;
&lt;p&gt;If you are still treating flaky tests as a writing problem, you are three years behind.&lt;/p&gt;
&lt;p&gt;If you treat them as a design problem, you are on time. The pattern worked then. It ships natively now — agents in v1.56, infrastructure in v1.59. The contract around it is still yours to build.&lt;/p&gt;
&lt;p&gt;If you have been treating them as a design problem for years, you are ahead of the team that ships the framework.&lt;/p&gt;
&lt;p&gt;That is a fine place to be.&lt;/p&gt;
&lt;p&gt;The system worked then. It ships in v1.59 now. The rules around it are still yours to build.&lt;/p&gt;
&lt;p&gt;That is the job.&lt;/p&gt;
&lt;p&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test systems where AI agents and human engineers work together on quality. Former Apple SDET (Apple.com and Apple Card pre-release testing). Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>ai</category><category>qa</category></item><item><title>Eval-Driven Development for AI Agent Skills</title><link>https://www.anton.qa/blog/posts/eval-driven-development-for-ai-agent-skills</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/eval-driven-development-for-ai-agent-skills</guid><description>Why skills need testing, not just writing — and how to do it systematically.</description><pubDate>Sun, 19 Apr 2026 20:27:00 GMT</pubDate><content:encoded>&lt;p&gt;The Problem with Writing Skills by Hand&lt;/p&gt;
&lt;p&gt;You&amp;#39;ve written a skill for your AI coding agent. It&amp;#39;s got clear instructions, proper formatting, a good description. You test it in a session — it works. Ship it, right?&lt;/p&gt;
&lt;p&gt;Not so fast.&lt;/p&gt;
&lt;p&gt;Skills trigger based on their description field — a 1-2 sentence summary in the SKILL.md frontmatter. And here&amp;#39;s the thing: descriptions that seem crystal clear to humans often trigger wrong. Too specific, and the skill never activates when it should. Too broad, and it fires on unrelated prompts.&lt;/p&gt;
&lt;p&gt;The result: skills that feel right in theory but fail unpredictably in practice. And there&amp;#39;s no systematic way to measure whether a skill is getting better or worse across iterations.&lt;/p&gt;
&lt;p&gt;This is the same problem software engineering solved decades ago with automated testing. Skills are software. They need testing too.&lt;/p&gt;
&lt;p&gt;What Is Eval-Driven Development?&lt;/p&gt;
&lt;p&gt;Eval-driven development is the practice of:&lt;/p&gt;
&lt;p&gt;Writing test cases that define expected behavior&lt;/p&gt;
&lt;p&gt;Running those tests automatically to measure actual vs. expected outcomes&lt;/p&gt;
&lt;p&gt;Using the results to improve iteratively, with quantifiable evidence&lt;/p&gt;
&lt;p&gt;For AI agent skills, this means:&lt;/p&gt;
&lt;p&gt;Generating test prompts (should-trigger and should-not-trigger queries)&lt;/p&gt;
&lt;p&gt;Running each prompt with and without the skill&lt;/p&gt;
&lt;p&gt;Comparing outputs to see if the skill actually improves results&lt;/p&gt;
&lt;p&gt;Optimizing the description so the skill triggers on the right prompts&lt;/p&gt;
&lt;p&gt;The Skill Creation Lifecycle&lt;/p&gt;
&lt;p&gt;opencode-skill-creator implements eval-driven development as a structured lifecycle:&lt;/p&gt;
&lt;p&gt;Create → Evaluate → Optimize → Benchmark → Install&lt;br&gt;   ↑                                      |&lt;br&gt;   └───    Iterate    ──┘&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Start with an intake interview. The skill-creator asks 3-5 targeted questions:&lt;/p&gt;
&lt;p&gt;What should this skill enable the agent to do?&lt;/p&gt;
&lt;p&gt;When should it trigger?&lt;/p&gt;
&lt;p&gt;What output format is expected?&lt;/p&gt;
&lt;p&gt;What workflow steps must be preserved exactly?&lt;/p&gt;
&lt;p&gt;This captures intent before writing any code.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Evaluate&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Auto-generate eval test sets — realistic prompts categorized as should-trigger or should-not-trigger. Run each test case twice:&lt;/p&gt;
&lt;p&gt;With skill: The agent has the skill loaded&lt;/p&gt;
&lt;p&gt;Without skill: The agent runs without it (baseline)&lt;/p&gt;
&lt;p&gt;This measures whether the skill actually improves the output for relevant prompts.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Optimize&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The description optimization loop treats triggering accuracy as a search problem:&lt;/p&gt;
&lt;p&gt;For each iteration (up to 5):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Evaluate current description on train set (60%)&lt;/li&gt;
&lt;li&gt;Analyze failure patterns&lt;/li&gt;
&lt;li&gt;LLM proposes improved description&lt;/li&gt;
&lt;li&gt;Evaluate on both train AND test (40%) sets&lt;/li&gt;
&lt;li&gt;Select best description by test score&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The 60/40 train/test split prevents overfitting. An description that works perfectly on train queries but fails on held-out test queries is overfit — it&amp;#39;s memorized specific prompts rather than learning the general pattern.&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Benchmark&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run the full eval suite across multiple iterations with variance analysis. This answers:&lt;/p&gt;
&lt;p&gt;Is the skill getting consistently better?&lt;/p&gt;
&lt;p&gt;Are there eval cases where the skill never triggers correctly?&lt;/p&gt;
&lt;p&gt;How much variance is there across runs?&lt;/p&gt;
&lt;p&gt;The benchmark includes:&lt;/p&gt;
&lt;p&gt;Pass rates (with-skill vs. baseline)&lt;/p&gt;
&lt;p&gt;Timing data (tokens, duration)&lt;/p&gt;
&lt;p&gt;Mean ± standard deviation for each metric&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Install&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Install the final validated skill to project-level (.opencode/skills/) or global (~/.config/opencode/skills/). Only the final version gets installed — eval artifacts stay in the staging directory.&lt;/p&gt;
&lt;p&gt;Why This Works&lt;/p&gt;
&lt;p&gt;Skills are software&lt;/p&gt;
&lt;p&gt;They have inputs (prompts), outputs (agent behavior), and a triggering mechanism (the description). Just like any software, they need testing.&lt;/p&gt;
&lt;p&gt;Manual testing doesn&amp;#39;t scale&lt;/p&gt;
&lt;p&gt;You can test a skill manually in a session, but that&amp;#39;s one prompt, one run, no measurement. Eval-driven development gives you 20+ test cases, multiple runs per case, and quantitative metrics.&lt;/p&gt;
&lt;p&gt;Description optimization is more impactful than skill content&lt;/p&gt;
&lt;p&gt;The description field is the primary triggering mechanism. A perfectly-written skill with a poor description won&amp;#39;t trigger. An average skill with an optimized description will trigger reliably. The optimization loop focuses effort where it matters most.&lt;/p&gt;
&lt;p&gt;Train/test splits prevent overfitting&lt;/p&gt;
&lt;p&gt;If you only test on the same queries you optimize for, descriptions become overfit — they work on those specific prompts but fail on real-world usage. The 60/40 split keeps you honest.&lt;/p&gt;
&lt;p&gt;Human review catches what automation misses&lt;/p&gt;
&lt;p&gt;The visual eval viewer puts outputs side by side so you can see with your own eyes whether the skill is producing good results. Quantitative metrics tell you if it&amp;#39;s triggering correctly; human review tells you if the output is actually useful.&lt;/p&gt;
&lt;p&gt;Getting Started&lt;/p&gt;
&lt;p&gt;npx opencode-skill-creator install --global&lt;/p&gt;
&lt;p&gt;Then ask OpenCode to create or improve a skill. The eval-driven workflow starts automatically.&lt;/p&gt;
&lt;p&gt;Apache 2.0, free, open source. Works with any of OpenCode&amp;#39;s supported models.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a href=&quot;https://github.com/antongulin/opencode-skill-creator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/antongulin/opencode-skill-creator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;opencode-skill-creator is free and open source (Apache 2.0). Star it on GitHub. Install: npx opencode-skill-creator install --global&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>AI</category><category>Testing</category><category>Quality Assurance</category><category>Developer Tools</category></item><item><title>Create Video Receipts for AI Agents with Playwright Screencast API</title><link>https://www.anton.qa/blog/posts/create-video-receipts-for-ai-agents-with-playwright-screencast-api</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/create-video-receipts-for-ai-agents-with-playwright-screencast-api</guid><description>Playwright v1.59.0 ships the Screencast API, letting AI agents produce verifiable video evidence of their work. Engineers can replay agent actions with chapter markers and action annotations—no manual test replay required. Setup is three lines: start the screencast, run your agent logic, stop and save. This is the observability layer agentic workflows have been missing.</description><pubDate>Fri, 17 Apr 2026 02:13:50 GMT</pubDate><content:encoded>&lt;p&gt;TL;DR&lt;/p&gt;
&lt;p&gt;Playwright v1.59.0 ships the Screencast API, letting AI agents produce verifiable video evidence of their work. Engineers can replay agent actions with chapter markers and action annotations—no manual test replay required. Setup is three lines: start the screencast, run your agent logic, stop and save. This is the observability layer agentic workflows have been missing.&lt;/p&gt;
&lt;p&gt;The Release&lt;/p&gt;
&lt;p&gt;Playwright v1.59.0 dropped last week and the headline feature is the Screencast API. Full disclosure: I&amp;#39;ve been watching the agentic testing space closely, and the honest assessment is that most of what passes for &amp;quot;AI testing&amp;quot; is smoke and mirrors—agents clicking around without verifiable evidence of what they actually did. The Screencast API is different. It gives you a real video of the agent&amp;#39;s session with semantic overlays, not just a trace file you have to manually load and interpret.&lt;/p&gt;
&lt;p&gt;The API surface is straightforward: page.screencast.start() initiates recording and page.screencast.stop() finalizes it. Between those calls, Playwright captures JPEG frames in real-time and lets you annotate them with chapter titles and action labels. You get a video file you can attach to a ticket, drop in a Slack thread, or store as audit evidence.&lt;/p&gt;
&lt;p&gt;This release also includes browser.bind() for MCP integration, a CLI debugger, and async disposables—but for this post, I&amp;#39;m focusing on the Screencast API because it&amp;#39;s the feature that directly addresses the verification problem in agentic workflows.&lt;/p&gt;
&lt;p&gt;Why This Matters for Engineers and QA&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re building or evaluating AI coding agents that interact with browsers, you face a fundamental trust problem. How do you verify that the agent actually clicked the right button, waited for the correct network response, and didn&amp;#39;t accidentally trigger a destructive flow? Logs help, but they&amp;#39;re not persuasive in a code review. Screenshots help more, but they don&amp;#39;t capture temporal sequences well.&lt;/p&gt;
&lt;p&gt;Video receipts solve this. You get a playback of the full session with chapter markers at key decision points. Your PM can watch a 90-second clip instead of reading 200 lines of trace output. Your security team gets evidence they can archive. Your CI system gets an artifact to attach to the test report.&lt;/p&gt;
&lt;p&gt;For QA teams specifically, this changes the audit story. When a flaky test gets investigated, you currently spend 20-30 minutes reproducing the environment, loading traces, and reconstructing what happened. With a screencast, you open a video. That&amp;#39;s a real workflow improvement, even if it&amp;#39;s not a headline-grabbing metric.&lt;/p&gt;
&lt;p&gt;How to Use It&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the implementation. The API supports chapter titles, action annotations, and visual overlays. You can configure frame capture rate and output format.&lt;/p&gt;
&lt;p&gt;The captureFrame() method is what makes this useful for AI vision workflows. You pass the JPEG buffer to your vision model for validation or further processing. The agent produces the evidence; you decide what to do with it.&lt;/p&gt;
&lt;p&gt;The Gotcha Nobody Is Talking About&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s what the release notes don&amp;#39;t emphasize: screencast recording in headless mode is not pixel-perfect. If your agent is doing precise visual assertions—checking exact colors, pixel-level positioning, or anti-aliased text rendering—the video artifacts may not match what you&amp;#39;d see in headed mode. I&amp;#39;ve seen this bite teams who expected the screencast to replace visual regression testing.&lt;/p&gt;
&lt;p&gt;The API works correctly and the implementation is solid, but it&amp;#39;s recording a compressed video, not a pixel-accurate capture of the render pipeline. Use it for workflow verification, not for asserting that #FF5733 exactly matches your design token. For that use case, you still need Playwright&amp;#39;s built-in visual comparisons or a dedicated visual regression tool.&lt;/p&gt;
&lt;p&gt;Also worth noting: the output file can get large quickly. A 5-minute session at 15 fps with visual overlays will easily be 50-100MB. You&amp;#39;ll want to configure retention policies in your CI system if you&amp;#39;re storing these as test artifacts. Don&amp;#39;t let this become your next storage incident.&lt;/p&gt;
&lt;p&gt;What This Changes in Your CI Pipeline&lt;/p&gt;
&lt;p&gt;The immediate impact is on how you handle failures from AI-driven test agents. Currently, when an agent-authored test fails, you have two options: trust the agent&amp;#39;s explanation (risky) or manually reproduce the failure (slow). With screencasts, you get a third option: watch the video, verify the agent&amp;#39;s logic, and make an informed decision in under 60 seconds.&lt;/p&gt;
&lt;p&gt;In practice, this means fewer &amp;quot;cannot reproduce&amp;quot; situations in your backlog. The debugging loop tightens from hours to minutes. For teams running autonomous agents in CI—yes, that&amp;#39;s a real thing—this is a meaningful improvement in the feedback cycle.&lt;/p&gt;
&lt;p&gt;Storage considerations aside, the integration is straightforward. Add page.screencast.start() to your fixture setup, route failures to your screencast storage, and update your test reporters to embed video links. Your team will adapt faster than you expect.&lt;/p&gt;
&lt;p&gt;Migration Notes&lt;/p&gt;
&lt;p&gt;No migration required for existing tests. The Screencast API is additive—if you&amp;#39;re not calling page.screencast.start(), your current suite is unaffected. The breaking change in this release is macOS 14 WebKit support removal, which only affects you if you&amp;#39;re running WebKit on a 14-year-old OS. Update your browser matrix if that applies.&lt;/p&gt;
&lt;p&gt;The @playwright/experimental-ct-svelte package removal is a non-issue unless you were explicitly depending on an experimental package—which you shouldn&amp;#39;t be doing in production.&lt;/p&gt;
&lt;p&gt;Verdict&lt;/p&gt;
&lt;p&gt;Playwright v1.59.0&amp;#39;s Screencast API is the feature that makes agentic testing verifiable instead of mysterious. The implementation is clean, the API is intuitive, and the use case is real. It&amp;#39;s not a replacement for visual regression tooling, and the storage costs are real, but the observability gains are genuine.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re evaluating AI coding agents for test automation, this is the feature that makes the evaluation tractable. You can now watch what the agent did instead of trusting what the agent claims it did. That&amp;#39;s not a small thing.&lt;/p&gt;
&lt;p&gt;I&amp;#39;ve shipped test tooling at scale, and the difference between &amp;quot;we have logs&amp;quot; and &amp;quot;we have video evidence&amp;quot; is the difference between debugging in the dark and debugging with a flashlight. The Screencast API gives you the flashlight. Worth exploring in your next sprint. &lt;/p&gt;
&lt;p&gt;Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>ai-agents</category><category>test-automation</category><category>testing</category></item><item><title>Porting Anthropic&apos;s Skill Creator from Python to TypeScript</title><link>https://www.anton.qa/blog/posts/porting-anthropic-s-skill-creator-from-python-to-typescript</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/porting-anthropic-s-skill-creator-from-python-to-typescript</guid><description>Technical decisions and lessons learned from rewriting a Python CLI tool as an OpenCode plugin.</description><pubDate>Thu, 16 Apr 2026 18:11:00 GMT</pubDate><content:encoded>&lt;p&gt;Why Port It?&lt;/p&gt;
&lt;p&gt;Anthropic&amp;#39;s skill-creator for Claude Code is excellent. It introduced eval-driven development for AI agent skills — write a skill, test it with evals, optimize the description, benchmark the results. The methodology is proven.&lt;/p&gt;
&lt;p&gt;But it has a limitation: it only works with Claude Code, and skill access requires a paid subscription ($20/month minimum). Free tier users can&amp;#39;t use it at all.&lt;/p&gt;
&lt;p&gt;OpenCode is free and supports 300+ models. I wanted to bring the same methodology to OpenCode users — for free, with no paywall.&lt;/p&gt;
&lt;p&gt;High-Level Architecture&lt;/p&gt;
&lt;p&gt;The original has this structure:&lt;/p&gt;
&lt;p&gt;Anthropic skill-creator/&lt;br&gt;├── SKILL.md                    # The skill instructions&lt;br&gt;├── scripts/&lt;br&gt;│   ├── run_loop.py             # Eval→improve optimization loop&lt;br&gt;│   ├── improve_description.py  # LLM-powered description improvement&lt;br&gt;│   ├── aggregate_benchmark.py   # Benchmark aggregation&lt;br&gt;│   └── generate_review.py       # HTML report generation&lt;br&gt;└── evals/&lt;br&gt;    └── evals.json              # Test query definitions&lt;/p&gt;
&lt;p&gt;My version:&lt;/p&gt;
&lt;p&gt;opencode-skill-creator/&lt;br&gt;├── skill-creator/              # The SKILL&lt;br&gt;│   ├── SKILL.md                # Main skill instructions&lt;br&gt;│   ├── agents/&lt;br&gt;│   │   ├── grader.md           # Assertion evaluation&lt;br&gt;│   │   ├── analyzer.md         # Benchmark analysis&lt;br&gt;│   │   └── comparator.md       # Blind A/B comparison&lt;br&gt;│   ├── references/&lt;br&gt;│   │   └── schemas.md          # JSON schema definitions&lt;br&gt;│   └── templates/&lt;br&gt;│       └── eval-review.html    # Eval set review/edit UI&lt;br&gt;└── plugin/                     # The PLUGIN (npm package)&lt;br&gt;    ├── package.json            # npm package metadata&lt;br&gt;    ├── skill-creator.ts         # Entry point&lt;br&gt;    └── lib/&lt;br&gt;        ├── utils.ts            # SKILL.md frontmatter parsing&lt;br&gt;        ├── validate.ts          # Skill structure validation&lt;br&gt;        ├── run-eval.ts          # Trigger evaluation&lt;br&gt;        ├── improve-description.ts  # Description optimization&lt;br&gt;        ├── run-loop.ts          # Eval→improve loop&lt;br&gt;        ├── aggregate.ts         # Benchmark aggregation&lt;br&gt;        ├── report.ts            # HTML report generation&lt;br&gt;        └── review-server.ts     # HTTP eval review server&lt;/p&gt;
&lt;p&gt;Key difference: the skill provides workflow knowledge, the plugin provides executable tools. The agent orchestrates everything by calling tools during its session.&lt;/p&gt;
&lt;p&gt;Decision 1: Scripts → Plugin Tool Calls&lt;/p&gt;
&lt;p&gt;Original: Python scripts invoked via CLI&lt;/p&gt;
&lt;h1&gt;Run the optimization loop&lt;/h1&gt;
&lt;p&gt;python -m scripts.run_loop --skill-path /path/to/skill --eval-set evals.json&lt;/p&gt;
&lt;p&gt;New: Plugin tool calls in OpenCode sessions&lt;/p&gt;
&lt;p&gt;skill_optimize_loop with:&lt;br&gt;  evalSetPath: /path/to/evals.json&lt;br&gt;  skillPath: /path/to/skill&lt;br&gt;  maxIterations: 5&lt;/p&gt;
&lt;p&gt;Why: OpenCode&amp;#39;s plugin architecture lets agents call custom tools directly. No subprocess management, no script execution, no Python environment. The agent calls the tool inline and gets results back in the session.&lt;/p&gt;
&lt;p&gt;This is cleaner integration but also more composable. The agent can interleave tool calls with other work — read files, ask the user questions, make decisions — between optimization iterations.&lt;/p&gt;
&lt;p&gt;Decision 2: Python → TypeScript&lt;/p&gt;
&lt;p&gt;The original requires Python 3.11+ and pyyaml. My version requires nothing beyond Node.js (which OpenCode users already have).&lt;/p&gt;
&lt;p&gt;All pipeline components — validation, eval, description improvement, loop runner, aggregation, report generation, review server — are TypeScript modules in the plugin. ~256kB unpacked on npm.&lt;/p&gt;
&lt;p&gt;Dependency tree is minimal: the plugin only depends on @opencode-ai/plugin (peer dependency).&lt;/p&gt;
&lt;p&gt;Decision 3: Static HTML → HTTP Review Server&lt;/p&gt;
&lt;p&gt;Original: Python script generates a static HTML file and opens it in the browser.&lt;/p&gt;
&lt;p&gt;generate_review.py --workspace /path/to/workspace&lt;/p&gt;
&lt;h1&gt;Opens /path/to/workspace/review.html in browser&lt;/h1&gt;
&lt;p&gt;New: Plugin starts a local HTTP server that serves an interactive eval viewer.&lt;/p&gt;
&lt;p&gt;skill_serve_review with:&lt;br&gt;  workspace: /path/to/workspace&lt;br&gt;  skillName: &amp;quot;my-skill&amp;quot;&lt;/p&gt;
&lt;p&gt;The HTTP server approach has advantages:&lt;/p&gt;
&lt;p&gt;Real-time updates when new eval results come in&lt;/p&gt;
&lt;p&gt;Interactive review with save buttons that write feedback back to files&lt;/p&gt;
&lt;p&gt;Previous/next navigation between eval cases&lt;/p&gt;
&lt;p&gt;Benchmark tab with quantitative metrics&lt;/p&gt;
&lt;p&gt;No file management — just open localhost:PORT&lt;/p&gt;
&lt;p&gt;The server can also generate static HTML for headless environments:&lt;/p&gt;
&lt;p&gt;skill_export_static_review with:&lt;br&gt;  workspace: /path/to/workspace&lt;br&gt;  outputPath: /path/to/report.html&lt;/p&gt;
&lt;p&gt;Decision 4: Subagents → Task Tool&lt;/p&gt;
&lt;p&gt;Original: Claude Code&amp;#39;s built-in subagent concept, where the skill directly spawns sub-agents.&lt;/p&gt;
&lt;p&gt;New: OpenCode&amp;#39;s Task tool with general and explore subagent types. The SKILL.md instructs the agent to spawn tasks for:&lt;/p&gt;
&lt;p&gt;Running eval cases (with-skill and baseline)&lt;/p&gt;
&lt;p&gt;Grading assertions against outputs&lt;/p&gt;
&lt;p&gt;Analyzing benchmark results&lt;/p&gt;
&lt;p&gt;Blind A/B comparison between skill versions&lt;/p&gt;
&lt;p&gt;The agent orchestrates these tasks and synthesizes their results.&lt;/p&gt;
&lt;p&gt;Decision 5: Staging Outside the Repo&lt;/p&gt;
&lt;p&gt;Original: Evals and benchmarks run alongside the skill in the same directory.&lt;/p&gt;
&lt;p&gt;New: Draft skills and eval artifacts go to the system temp directory:&lt;/p&gt;
&lt;p&gt;/tmp/opencode-skills/&lt;skill-name&gt;/           # Staged skill&lt;br&gt;/tmp/opencode-skills/&lt;skill-name&gt;-workspace/  # Eval artifacts&lt;/p&gt;
&lt;p&gt;Only the final validated skill gets installed to:&lt;/p&gt;
&lt;p&gt;Project: .opencode/skills/&lt;skill-name&gt;/&lt;/p&gt;
&lt;p&gt;Global: ~/.config/opencode/skills/&lt;skill-name&gt;/&lt;/p&gt;
&lt;p&gt;This keeps the user&amp;#39;s repository clean during skill development. Evals create a lot of artifacts (outputs, timing data, grading results, benchmark files) that you don&amp;#39;t want mixed into your project.&lt;/p&gt;
&lt;p&gt;Decision 6: Strict Review Workflow&lt;/p&gt;
&lt;p&gt;Added a &amp;quot;review workflow guard&amp;quot; that enforces paired comparison data by default:&lt;/p&gt;
&lt;p&gt;skill_serve_review and skill_export_static_review require each eval directory to include both with_skill AND baseline (without_skill or old_skill)&lt;/p&gt;
&lt;p&gt;If pairs are missing, the tools fail fast with a clear list of what&amp;#39;s missing&lt;/p&gt;
&lt;p&gt;Override with allowPartial: true only when intentionally reviewing incomplete data&lt;/p&gt;
&lt;p&gt;This prevents a common mistake: reviewing eval results without a baseline comparison, which makes it impossible to judge whether the skill actually improved anything.&lt;/p&gt;
&lt;p&gt;What I Learned&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Skills are software&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;They need testing, not just writing. The eval-driven approach catches issues you&amp;#39;d never find manually — like a description that triggers on 80% of relevant queries but also fires on 30% of irrelevant ones.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Description optimization matters more than skill content&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The description field is the primary triggering mechanism. A well-optimized description on an average skill outperforms a poor description on a perfect skill. This is counterintuitive but matches the data.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Train/test splits prevent overfitting&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Same lesson as ML hyperparameter tuning. If you only evaluate on the queries you optimize for, descriptions become overfit. The 60/40 split keeps you honest about generalization.&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Human-in-the-loop review is essential&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Automation measures triggering accuracy, but humans judge output quality. The visual eval viewer puts outputs side by side so you can see whether the skill produces genuinely useful results, not just correctly-triggered results.&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Plugin architecture enables composition&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Having eval, benchmarking, and review as separate tool calls (instead of a monolithic script) means the agent can interleave them with other work. It can ask the user a question between iterations, read relevant files during eval, or skip steps the user doesn&amp;#39;t need.&lt;/p&gt;
&lt;p&gt;Try It&lt;/p&gt;
&lt;p&gt;npx opencode-skill-creator install --global&lt;/p&gt;
&lt;p&gt;Apache 2.0, free, open source. Works with any of OpenCode&amp;#39;s supported models.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a href=&quot;https://github.com/antongulin/opencode-skill-creator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/antongulin/opencode-skill-creator&lt;/a&gt; npm: &lt;a href=&quot;https://www.npmjs.com/package/opencode-skill-creator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://www.npmjs.com/package/opencode-skill-creator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;opencode-skill-creator is free and open source (Apache 2.0). Star it on GitHub. Install: npx opencode-skill-creator install --global&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>TypeScript</category><category>Python</category><category>Open Source</category><category>Software Engineering</category><category>AI</category></item><item><title>I Ate My Own Dog Food: How I Benchmarked AI Skills and Proved Eval-Driven Development Works</title><link>https://www.anton.qa/blog/posts/i-ate-my-own-dog-food-how-i-benchmarked-ai-skills-and-proved-eval-driven-development-works</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/i-ate-my-own-dog-food-how-i-benchmarked-ai-skills-and-proved-eval-driven-development-works</guid><description>I Ate My Own Dog Food: How I Benchmarked AI Skills and Proved Eval-Driven Development Works
I built a tool to test AI skills. Then I used it on my own project. The benchmarks shocked even me.
Anton Gulin is an AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET, current Lead Software Engineer in Test at CooperVision. Find him at anton.qa or on LinkedIn.</description><pubDate>Wed, 15 Apr 2026 18:11:00 GMT</pubDate><content:encoded>&lt;p&gt;I Ate My Own Dog Food: How I Benchmarked AI Skills and Proved Eval-Driven Development Works&lt;/p&gt;
&lt;p&gt;I built a tool to test AI skills. Then I used it on my own project. The benchmarks shocked even me.&lt;/p&gt;
&lt;p&gt;As a QA architect, I&amp;#39;ve spent my career building systems that verify software works correctly. At Apple, we tested everything — every interaction, every edge case, every regression. At CooperVision, I built a Playwright/TypeScript framework from scratch that grew test coverage by 300%.&lt;/p&gt;
&lt;p&gt;So when I started working with AI agent skills, I noticed something: nobody was testing them.&lt;/p&gt;
&lt;p&gt;You write a SKILL.md file. You try it manually once. Maybe it works for your prompt. You ship it.&lt;/p&gt;
&lt;p&gt;There&amp;#39;s no automated test suite. No regression testing. No CI pipeline that catches when a description change breaks triggering.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s a QA problem. I built opencode-skill-creator to solve it.&lt;/p&gt;
&lt;p&gt;Then I dogfooded it on a real project. Here&amp;#39;s what happened.&lt;/p&gt;
&lt;p&gt;The Project: AdLoop Skills for Google Ads&lt;/p&gt;
&lt;p&gt;AdLoop is a Google Ads MCP (Model Context Protocol) integration — it connects AI agents to Google Ads and GA4 data through a set of tools.&lt;/p&gt;
&lt;p&gt;I created 4 skills for AdLoop using opencode-skill-creator, each handling a different aspect of Google Ads management:&lt;/p&gt;
&lt;p&gt;adloop-planning — Keyword research, competition analysis, and budget forecasting&lt;/p&gt;
&lt;p&gt;adloop-read — Performance analysis, campaign reporting, and conversion diagnostics&lt;/p&gt;
&lt;p&gt;adloop-write — Campaign creation, ad management, keyword bidding, and budget changes (spends real money)&lt;/p&gt;
&lt;p&gt;adloop-tracking — GA4 event validation, conversion tracking diagnosis, and code generation&lt;/p&gt;
&lt;p&gt;Each skill contains:&lt;/p&gt;
&lt;p&gt;A detailed SKILL.md with orchestration patterns, safety rules, and domain-specific best practices&lt;/p&gt;
&lt;p&gt;An evals set with test queries (both should-trigger and should-not-trigger)&lt;/p&gt;
&lt;p&gt;The full lifecycle: validate → eval → optimize loop → benchmark&lt;/p&gt;
&lt;p&gt;The Benchmark: With Skill vs. Without Skill&lt;/p&gt;
&lt;p&gt;opencode-skill-creator&amp;#39;s benchmark runs each skill through its eval queries in two configurations:&lt;/p&gt;
&lt;p&gt;With skill loaded — the AI agent has access to the full SKILL.md with all domain knowledge, safety rules, and orchestration patterns&lt;/p&gt;
&lt;p&gt;Without skill — the AI agent only has the bare MCP tool names and descriptions from the schema&lt;/p&gt;
&lt;p&gt;The results were striking:&lt;/p&gt;
&lt;p&gt;Skill                    Evals         With Skill       Without Skill         Improvement&lt;/p&gt;
&lt;p&gt;adloop-write       8               100%                 17%                +83 percentage points&lt;/p&gt;
&lt;p&gt;adloop-planning 6               100%                 21%                +79 percentage points&lt;/p&gt;
&lt;p&gt;adloop-read        8               100%                 27%               +73 percentage points&lt;/p&gt;
&lt;p&gt;adloop-tracking  6               100%                 33%               +67 percentage points&lt;/p&gt;
&lt;p&gt;100% pass rate across the board — every eval, every expectation — with skills loaded. Without skills, pass rates ranged from 17% to 33%.&lt;/p&gt;
&lt;p&gt;But the raw numbers only tell part of the story. Let me show you what actually failed without the skills, because the failures aren&amp;#39;t just wrong answers — they&amp;#39;re dangerous actions.&lt;/p&gt;
&lt;p&gt;The Scariest Failure: adloop-write (17% without skill)&lt;/p&gt;
&lt;p&gt;adloop-write manages campaigns, ads, keywords, and budgets. These are operations that spend real money. Without the skill, the AI made these mistakes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Added BROAD match keywords to MANUAL_CPC campaigns&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The #1 cause of wasted ad spend. BROAD match on MANUAL_CPC means Google matches irrelevant queries and drains your budget. The skill explicitly checks bidding strategy before allowing BROAD match. Without it? The AI just adds them.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Set budget above safety caps&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The tool has a max_daily_budget of $50. Without the skill, the AI set the budget to $100 — exceeding the cap by 2x. The skill enforces the cap as a guardrail. Without it, no guardrail exists.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Performed irreversible deletions without warning&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The skill has a critical safety rule: &amp;quot;Always prefer pause_entity over remove_entity. remove_entity is IRREVERSIBLE.&amp;quot; Without the skill, the AI called remove_entity directly — no warning, no confirmation, no pause-as-alternative.&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Batched multiple write operations in one call&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The skill enforces &amp;quot;one change at a time&amp;quot; — draft, preview, confirm, then next change. Without it, the AI batched pause + sitelink changes together, bypassing review.&lt;/p&gt;
&lt;p&gt;This isn&amp;#39;t about &amp;quot;better answers.&amp;quot; This is about preventing real financial harm.&lt;/p&gt;
&lt;p&gt;GDPR Is Not Broken Tracking: adloop-tracking (33% without skill)&lt;/p&gt;
&lt;p&gt;The most interesting failure was in adloop-tracking.&lt;/p&gt;
&lt;p&gt;A common scenario: a user sees 500 clicks in Google Ads but only 180 sessions in GA4. &amp;quot;Is my tracking broken?&amp;quot;&lt;/p&gt;
&lt;p&gt;Without the skill, the AI immediately diagnosed this as a tracking issue and offered to investigate further. It suggested running attribution checks and validating tracking code.&lt;/p&gt;
&lt;p&gt;With the skill, the AI recognized this immediately: &amp;quot;A 2.8:1 click-to-session ratio is completely normal with GDPR consent banners. Google Ads counts all clicks regardless of consent. GA4 only records sessions from users who accept analytics cookies. Your tracking is not broken.&amp;quot;&lt;/p&gt;
&lt;p&gt;This is the #1 false positive in digital marketing analytics. Every marketer who runs EU-targeted ads has seen this panic. The skill prevents hours of investigation and prevents &amp;quot;fixing&amp;quot; something that isn&amp;#39;t broken.&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t Trust Google Blindly: adloop-read (27% without skill)&lt;/p&gt;
&lt;p&gt;Google Ads provides automated recommendations. Without the skill, the AI endorsed them at face value:&lt;/p&gt;
&lt;p&gt;&amp;quot;Raise budget&amp;quot; — with zero conversions? That&amp;#39;s bad advice until tracking works.&lt;/p&gt;
&lt;p&gt;&amp;quot;Add BROAD match&amp;quot; — without Smart Bidding? That wastes money.&lt;/p&gt;
&lt;p&gt;&amp;quot;More keywords&amp;quot; — with quality scores below 5? The problem is relevance, not volume.&lt;/p&gt;
&lt;p&gt;The skill explicitly states: &amp;quot;Google recommendations optimize for Google&amp;#39;s revenue, not yours.&amp;quot; It cross-references every recommendation against actual conversion data and quality scores before accepting.&lt;/p&gt;
&lt;p&gt;The 73% improvement comes from teaching the AI critical thinking, not compliance.&lt;/p&gt;
&lt;p&gt;Wrong Country, Wrong Budget: adloop-planning (21% without skill)&lt;/p&gt;
&lt;p&gt;Without the skill, the AI defaulted to Germany (the tool&amp;#39;s default geo target) when the user asked about US keywords. It didn&amp;#39;t group results by competition level. It didn&amp;#39;t mention the 5x CPA budget sufficiency rule. It didn&amp;#39;t suggest the planning-to-campaign transition workflow.&lt;/p&gt;
&lt;p&gt;These aren&amp;#39;t edge cases — they&amp;#39;re the fundamental decisions that determine whether a campaign succeeds or wastes money.&lt;/p&gt;
&lt;p&gt;Why This Matters: Skills Are Safety Guards, Not Nice-to-Haves&lt;/p&gt;
&lt;p&gt;The benchmark data tells a clear story: the same AI model, the same tools, the same prompts — the only variable is whether the skill is loaded. And the difference is 67-83 percentage points.&lt;/p&gt;
&lt;p&gt;Skills do three things that bare tool access doesn&amp;#39;t:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inject domain expertise&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The adloop-write skill knows that BROAD + MANUAL_CPC is the #1 cause of wasted spend. The adloop-tracking skill knows GDPR consent mechanics. The adloop-planning skill knows keyword competition levels and budget rules.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Enforce safety guardrails&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Budget caps, irreversible deletion warnings, one-change-at-a-time rules, confirmation prompts before destructive operations. These aren&amp;#39;t &amp;quot;context&amp;quot; — they&amp;#39;re safety guards that prevent real harm.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Provide orchestration patterns&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The skill doesn&amp;#39;t just know what each tool does. It knows when to call which tool, in what order, with what validation. It&amp;#39;s the difference between a junior dev who knows the API and a senior architect who knows the system.&lt;/p&gt;
&lt;p&gt;How to Run Your Own Benchmarks&lt;/p&gt;
&lt;p&gt;opencode-skill-creator is free and open source (Apache 2.0). Here&amp;#39;s how to benchmark your own skills:&lt;/p&gt;
&lt;p&gt;Install&lt;br&gt;npx opencode-skill-creator install --global&lt;/p&gt;
&lt;p&gt;Create a skill with the guided interview&lt;br&gt;opencode-skill-creator will walk you through it&lt;/p&gt;
&lt;p&gt;Run evals with baseline comparison&lt;br&gt;The tool auto-generates test cases and runs with/without skill&lt;/p&gt;
&lt;p&gt;Run the description optimization loop&lt;br&gt;Train/test split, iterative improvement, variance analysis&lt;/p&gt;
&lt;p&gt;Benchmark with visual review&lt;br&gt;HTML viewer for human QA sign-off&lt;/p&gt;
&lt;p&gt;Works with any of OpenCode&amp;#39;s 300+ supported models. Zero Python dependency — pure TypeScript.&lt;/p&gt;
&lt;p&gt;The Lesson for QA Architects&lt;/p&gt;
&lt;p&gt;I&amp;#39;ve spent my career architecting test systems. At Apple, the standard wasn&amp;#39;t &amp;quot;does it work?&amp;quot; — it was &amp;quot;is it perfect?&amp;quot; At CooperVision, I proved that 300% test coverage growth and 50% faster deployments aren&amp;#39;t mutually exclusive.&lt;/p&gt;
&lt;p&gt;The same discipline applies to AI. Skills are software. They have inputs (prompts), outputs (agent behavior), and a triggering mechanism (the description). They deserve the same testing rigor we apply to any other software.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re building AI agent skills and you&amp;#39;re not running evals, you&amp;#39;re flying blind. Start here:&lt;/p&gt;
&lt;p&gt;→ github.com/antongulin/opencode-skill-creator&lt;/p&gt;
&lt;p&gt;Skills are software. Software should be tested.&lt;/p&gt;
&lt;p&gt;Anton Gulin is an AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET, current Lead Software Engineer in Test. Find him at anton.qa or on LinkedIn.&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>AI QA</category><category>SDET</category><category>eval-driven development</category><category>opencode-skill-creator</category><category>AdLoop</category><category>benchmark</category><category>open source</category></item><item><title>How to Create Custom OpenCode Skills (Step-by-Step Guide)</title><link>https://www.anton.qa/blog/posts/how-to-create-custom-opencode-skills-step-by-step-guide</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/how-to-create-custom-opencode-skills-step-by-step-guide</guid><description>A practical walkthrough for creating, testing, and installing production-grade OpenCode skills.</description><pubDate>Sun, 12 Apr 2026 18:04:00 GMT</pubDate><content:encoded>&lt;p&gt;Why Custom Skills Matter&lt;/p&gt;
&lt;p&gt;Out-of-the-box AI coding agents are powerful, but they don&amp;#39;t know your team&amp;#39;s conventions, your deployment process, or your documentation style. Skills let you encode that knowledge so the agent follows your workflows every time.&lt;/p&gt;
&lt;p&gt;But creating skills has been guesswork. You write a SKILL.md file, test it manually in a session, maybe tweak the description, and hope it works. There&amp;#39;s no feedback loop, no measurement, no way to know if a change actually improved things.&lt;/p&gt;
&lt;p&gt;opencode-skill-creator changes this by providing a structured workflow for the full skill lifecycle: create, evaluate, optimize, benchmark, and install.&lt;/p&gt;
&lt;p&gt;Prerequisites&lt;/p&gt;
&lt;p&gt;OpenCode installed and configured&lt;/p&gt;
&lt;p&gt;Node.js 18+ (for the npm package)&lt;/p&gt;
&lt;p&gt;5 minutes&lt;/p&gt;
&lt;p&gt;Step 1: Install&lt;/p&gt;
&lt;p&gt;One command:&lt;/p&gt;
&lt;p&gt;npx opencode-skill-creator install --global&lt;/p&gt;
&lt;p&gt;This adds the plugin to your global OpenCode config. Restart OpenCode to activate it.&lt;/p&gt;
&lt;p&gt;Verify the install:&lt;/p&gt;
&lt;p&gt;ls ~/.config/opencode/skills/skill-creator/SKILL.md&lt;/p&gt;
&lt;p&gt;Then ask OpenCode: Create a skill that helps with Docker compose files&lt;/p&gt;
&lt;p&gt;You should see it use the skill-creator workflow and tools.&lt;/p&gt;
&lt;p&gt;Step 2: Describe What You Want&lt;/p&gt;
&lt;p&gt;The skill-creator starts with an intake interview. It asks 3-5 targeted questions about what your skill should do:&lt;/p&gt;
&lt;p&gt;What should this skill enable OpenCode to do end-to-end?&lt;/p&gt;
&lt;p&gt;When should this skill trigger?&lt;/p&gt;
&lt;p&gt;What output format and quality bar are expected?&lt;/p&gt;
&lt;p&gt;What workflow steps must be preserved vs. where can the agent improvise?&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t skip this. The interview captures your intent before any code is written. Think of it as shadowing a teammate — you&amp;#39;re the domain expert, the agent is the new hire learning your workflow.&lt;/p&gt;
&lt;p&gt;Step 3: Review the Skill Draft&lt;/p&gt;
&lt;p&gt;Based on your interview, the skill-creator produces a draft SKILL.md with:&lt;/p&gt;
&lt;p&gt;Proper YAML frontmatter (name and description)&lt;/p&gt;
&lt;p&gt;Markdown instructions for the agent&lt;/p&gt;
&lt;p&gt;Optional supporting files (references, agents, templates)&lt;/p&gt;
&lt;p&gt;The draft goes to a staging directory (outside your repo) so your project stays clean:&lt;/p&gt;
&lt;p&gt;/tmp/opencode-skills/your-skill-name/&lt;br&gt;├── SKILL.md&lt;br&gt;├── agents/&lt;br&gt;├── references/&lt;br&gt;└── templates/&lt;/p&gt;
&lt;p&gt;Review this draft. Make sure the description is accurate (it&amp;#39;s the primary triggering mechanism) and the instructions reflect your actual workflow.&lt;/p&gt;
&lt;p&gt;Step 4: Generate Eval Test Cases&lt;/p&gt;
&lt;p&gt;The skill-creator automatically generates test cases — realistic prompts that an OpenCode user would actually type:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;  &amp;quot;skill_name&amp;quot;: &amp;quot;docker-compose&amp;quot;,&lt;br&gt;  &amp;quot;evals&amp;quot;: [&lt;br&gt;    {&lt;br&gt;      &amp;quot;id&amp;quot;: 1,&lt;br&gt;      &amp;quot;prompt&amp;quot;: &amp;quot;help me set up a compose file for my Node app with a Postgres database&amp;quot;,&lt;br&gt;      &amp;quot;expected_output&amp;quot;: &amp;quot;Skill triggers and provides Docker compose guidance&amp;quot;,&lt;br&gt;      &amp;quot;should_trigger&amp;quot;: true&lt;br&gt;    },&lt;br&gt;    {&lt;br&gt;      &amp;quot;id&amp;quot;: 2,&lt;br&gt;      &amp;quot;prompt&amp;quot;: &amp;quot;explain how Kubernetes deployments work&amp;quot;,&lt;br&gt;      &amp;quot;should_trigger&amp;quot;: false&lt;br&gt;    }&lt;br&gt;  ]&lt;br&gt;}&lt;/p&gt;
&lt;p&gt;Good eval queries are realistic and specific — not abstract like &amp;quot;help with containers&amp;quot; but concrete like &amp;quot;ok so my boss just sent me this xlsx file (its in my downloads, called something like &amp;#39;Q4 sales final FINAL v2.xlsx&amp;#39;)...&amp;quot;&lt;/p&gt;
&lt;p&gt;Review the eval set. Add or modify test cases that reflect your real usage.&lt;/p&gt;
&lt;p&gt;Step 5: Run Evals&lt;/p&gt;
&lt;p&gt;The eval system runs each test case twice — once with the skill and once without (baseline). This measures whether the skill actually improves the output.&lt;/p&gt;
&lt;p&gt;For each test case:&lt;/p&gt;
&lt;p&gt;OpenCode runs with the skill loaded&lt;/p&gt;
&lt;p&gt;OpenCode runs without the skill&lt;/p&gt;
&lt;p&gt;Both outputs are saved for comparison&lt;/p&gt;
&lt;p&gt;Timing data (tokens used, duration) is captured automatically.&lt;/p&gt;
&lt;p&gt;Step 6: Review Results Visually&lt;/p&gt;
&lt;p&gt;The skill-creator launches an HTML eval viewer:&lt;/p&gt;
&lt;p&gt;Call skill_serve_review with:&lt;br&gt;  workspace: /tmp/opencode-skills/your-skill-name-workspace/iteration-1&lt;br&gt;  skillName: &amp;quot;your-skill-name&amp;quot;&lt;/p&gt;
&lt;p&gt;The viewer shows:&lt;/p&gt;
&lt;p&gt;Outputs tab: Each test case with with-skill and without-skill outputs side by side&lt;/p&gt;
&lt;p&gt;Benchmark tab: Quantitative metrics — pass rates, timing, token usage&lt;/p&gt;
&lt;p&gt;Feedback fields: Leave comments on each test case&lt;/p&gt;
&lt;p&gt;Review the outputs. Give specific feedback on what&amp;#39;s working and what&amp;#39;s not. Empty feedback means &amp;quot;looks good.&amp;quot;&lt;/p&gt;
&lt;p&gt;Step 7: Iterate and Improve&lt;/p&gt;
&lt;p&gt;Based on your feedback, the skill-creator improves the skill:&lt;/p&gt;
&lt;p&gt;Applies your feedback&lt;/p&gt;
&lt;p&gt;Reruns all test cases (new iteration)&lt;/p&gt;
&lt;p&gt;Launches the reviewer with previous iteration for comparison&lt;/p&gt;
&lt;p&gt;You review again&lt;/p&gt;
&lt;p&gt;Repeat until you&amp;#39;re satisfied or feedback is all empty.&lt;/p&gt;
&lt;p&gt;Step 8: Optimize the Description&lt;/p&gt;
&lt;p&gt;Even with perfect skill instructions, the skill won&amp;#39;t trigger correctly if the description field isn&amp;#39;t right. The description is what OpenCode reads to decide whether to load your skill.&lt;/p&gt;
&lt;p&gt;The optimization loop:&lt;/p&gt;
&lt;p&gt;Generates 20 eval queries (should-trigger and should-not-trigger)&lt;/p&gt;
&lt;p&gt;Splits them 60/40 into train/test&lt;/p&gt;
&lt;p&gt;Evaluates each query 3 times for statistical reliability&lt;/p&gt;
&lt;p&gt;Analyzes failure patterns&lt;/p&gt;
&lt;p&gt;LLM proposes improved descriptions&lt;/p&gt;
&lt;p&gt;Re-evaluates on both train and test&lt;/p&gt;
&lt;p&gt;Selects the best description by test score&lt;/p&gt;
&lt;p&gt;Repeats up to 5 iterations&lt;/p&gt;
&lt;h1&gt;Tell OpenCode:&lt;/h1&gt;
&lt;p&gt;&amp;quot;Optimize the description of my docker-compose skill&amp;quot;&lt;/p&gt;
&lt;p&gt;This takes some time — grab a coffee while it runs.&lt;/p&gt;
&lt;p&gt;Step 9: Install the Final Skill&lt;/p&gt;
&lt;p&gt;Once you&amp;#39;re satisfied with the skill and its description:&lt;/p&gt;
&lt;p&gt;Project-level: .opencode/skills/your-skill-name/SKILL.md — available only in this project&lt;/p&gt;
&lt;p&gt;Global: ~/.config/opencode/skills/your-skill-name/SKILL.md — available everywhere&lt;/p&gt;
&lt;h1&gt;Project-level install&lt;/h1&gt;
&lt;p&gt;cp -r /tmp/opencode-skills/your-skill-name/ .opencode/skills/your-skill-name/&lt;/p&gt;
&lt;h1&gt;Global install&lt;/h1&gt;
&lt;p&gt;cp -r /tmp/opencode-skills/your-skill-name/ ~/.config/opencode/skills/your-skill-name/&lt;/p&gt;
&lt;p&gt;Only the final validated skill gets installed. All eval artifacts stay in the staging directory.&lt;/p&gt;
&lt;p&gt;Real-World Example: Docker Compose Skill&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s what the full workflow looks like in practice:&lt;/p&gt;
&lt;p&gt;Ask OpenCode: &amp;quot;Create a skill that helps with Docker compose files&amp;quot;&lt;/p&gt;
&lt;p&gt;Interview: The skill-creator asks about your conventions (multi-service vs. single container, development vs. production, preferred base images)&lt;/p&gt;
&lt;p&gt;Draft: Produces a SKILL.md with Docker compose best practices, service configuration patterns, volume mount strategies&lt;/p&gt;
&lt;p&gt;Eval: Generates test cases like &amp;quot;my api keeps crashing on startup, can you help me debug my compose file&amp;quot; (should trigger) and &amp;quot;what&amp;#39;s the difference between Docker and Podman&amp;quot; (should not trigger)&lt;/p&gt;
&lt;p&gt;Review: You look at the outputs, give feedback: &amp;quot;the skill should prioritize security configurations in production compose files&amp;quot;&lt;/p&gt;
&lt;p&gt;Iterate: Improved skill draft, better outputs&lt;/p&gt;
&lt;p&gt;Optimize: Description goes from &amp;quot;Help with Docker compose files&amp;quot; to something much more specific that triggers reliably&lt;/p&gt;
&lt;p&gt;Install: Copy to ~/.config/opencode/skills/docker-compose/&lt;/p&gt;
&lt;p&gt;Tips for Great Skills&lt;/p&gt;
&lt;p&gt;Be specific in the intake interview: The more context you give, the better the draft&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t skip evals: They catch triggering issues you&amp;#39;d never find manually&lt;/p&gt;
&lt;p&gt;Use realistic test prompts: Write them the way you&amp;#39;d actually type them, typos and all&lt;/p&gt;
&lt;p&gt;Iterate at least twice: First drafts are rarely perfect&lt;/p&gt;
&lt;p&gt;Optimize the description: It&amp;#39;s the #1 factor in whether your skill triggers correctly&lt;/p&gt;
&lt;p&gt;Install globally for general skills, project-level for specific ones&lt;/p&gt;
&lt;p&gt;Getting Started&lt;/p&gt;
&lt;p&gt;npx opencode-skill-creator install --global&lt;/p&gt;
&lt;p&gt;Then ask OpenCode to create a skill. That&amp;#39;s it.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a href=&quot;https://github.com/antongulin/opencode-skill-creator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/antongulin/opencode-skill-creator&lt;/a&gt; &lt;/p&gt;
&lt;p&gt;npm: &lt;a href=&quot;https://www.npmjs.com/package/opencode-skill-creator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://www.npmjs.com/package/opencode-skill-creator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;opencode-skill-creator is free and open source (Apache 2.0). Star it on GitHub. Install: npx opencode-skill-creator install --global&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>TypeScript</category><category>Open Source</category><category>AI</category><category>Developer Tools</category><category>Opencode</category></item><item><title>Page Object Model Playwright (2026): Best TypeScript Guide</title><link>https://www.anton.qa/blog/posts/page-object-model-in-playwright-with-typescript-complete-guide</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/page-object-model-in-playwright-with-typescript-complete-guide</guid><description>Master Page Object Model in Playwright &amp; TypeScript (2026). Structure scalable tests, copy real-world architecture patterns, and speed up testing.</description><pubDate>Sun, 01 Feb 2026 03:38:00 GMT</pubDate><content:encoded>&lt;p&gt;What is the Page Object Model?&lt;/p&gt;
&lt;p&gt;The Page Object Model (POM) is a design pattern that creates an abstraction layer between your tests and your web pages. Instead of writing locators directly in tests, you encapsulate page elements and actions inside reusable classes.&lt;/p&gt;
&lt;p&gt;Without POM (hard to maintain):&lt;/p&gt;
&lt;p&gt;With POM (scalable):&lt;/p&gt;
&lt;p&gt;When the login form changes, you update one file — not 50 tests.&lt;/p&gt;
&lt;p&gt;Why Use Page Objects?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Maintainability&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Locators live in one place. When UI changes, update once.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Readability&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tests read like user stories: loginPage.login(), dashboard.navigateToSettings().&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Reusability&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Login logic written once, used in 100 tests.&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Separation of concerns&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tests focus on behavior. Page objects handle implementation.&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Team scalability&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;New team members understand tests faster.&lt;/p&gt;
&lt;p&gt;Project Structure&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the structure I use for all my Playwright projects:&lt;/p&gt;
&lt;p&gt;Creating a Base Page Class&lt;/p&gt;
&lt;p&gt;Start with a base class that all pages extend:&lt;/p&gt;
&lt;p&gt;Creating a Login Page Object&lt;/p&gt;
&lt;p&gt;Now create a specific page class:&lt;/p&gt;
&lt;p&gt;Creating a Dashboard Page Object&lt;/p&gt;
&lt;p&gt;Using Page Objects in Tests&lt;/p&gt;
&lt;p&gt;Now your tests are clean and readable:&lt;/p&gt;
&lt;p&gt;Advanced: Component Objects&lt;/p&gt;
&lt;p&gt;For reusable UI components (modals, headers, sidebars), create component classes:&lt;/p&gt;
&lt;p&gt;Use it in page objects:&lt;/p&gt;
&lt;p&gt;Advanced: Using Playwright Fixtures&lt;/p&gt;
&lt;p&gt;For even cleaner tests, integrate page objects with Playwright fixtures:&lt;/p&gt;
&lt;p&gt;Now tests are even cleaner:&lt;/p&gt;
&lt;p&gt;Best Practices&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use getters for locators&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Getters are lazily evaluated. The locator is resolved when accessed, not when the class is instantiated.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Keep actions atomic&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each method should do one logical thing:&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Don&amp;#39;t add assertions in page objects (usually)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Assertions belong in tests. Page objects encapsulate interactions.&lt;/p&gt;
&lt;p&gt;Exception: Verification methods like loginAndExpectDashboard() can include assertions for convenience.&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Use role-based locators&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;More resilient than CSS selectors:&lt;/p&gt;
&lt;p&gt;Common Mistakes to Avoid&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;God page objects&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your page object has 50+ methods, break it into smaller components.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Exposing raw locators everywhere&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Encapsulate interactions: loginPage.login() instead of loginPage.submitButton.click().&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Not waiting for navigation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After clicks that navigate, wait for the URL or element: &lt;/p&gt;
&lt;p&gt;await this.submitButton.click();&lt;/p&gt;
&lt;p&gt;await this.page.waitForURL(/.*dashboard/);&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Sharing state between tests&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each test should be independent. Don&amp;#39;t rely on previous test state.&lt;/p&gt;
&lt;p&gt;Folder Structure for Large Projects&lt;/p&gt;
&lt;p&gt;For 100+ tests, organize by feature:&lt;/p&gt;
&lt;p&gt;Need Architecture Help?&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re building a test automation framework and want it done right from the start, I can help design the architecture, set up page objects, and train your team on best practices.&lt;/p&gt;
&lt;p&gt;Get in touch&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>page-object-model</category><category>typescript</category><category>architecture</category><category>best-practices</category></item><item><title>How to Migrate from Selenium to Playwright in 2026: Complete Guide</title><link>https://www.anton.qa/blog/posts/how-to-migrate-from-selenium-to-playwright-in-2026-complete-guide</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/how-to-migrate-from-selenium-to-playwright-in-2026-complete-guide</guid><description>A practical, step-by-step guide to migrating your Selenium test suite to Playwright. Includes code comparison, common pitfalls, and a migration strategy that won&apos;t disrupt your team.</description><pubDate>Sat, 31 Jan 2026 04:26:00 GMT</pubDate><content:encoded>&lt;p&gt;Why Migrate from Selenium to Playwright?&lt;/p&gt;
&lt;p&gt;I&amp;#39;ve been using Selenium since 2015. It served me well at Williams-Sonoma, Apple, and countless other companies. But after migrating to Playwright at CooperVision, I won&amp;#39;t go back.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s how they compare:&lt;/p&gt;
&lt;p&gt;Setup time:&lt;/p&gt;
&lt;p&gt;Selenium: 30+ minutes&lt;/p&gt;
&lt;p&gt;Playwright: 5 minutes&lt;/p&gt;
&lt;p&gt;Auto-waiting:&lt;/p&gt;
&lt;p&gt;Selenium: Manual (causes flaky tests)&lt;/p&gt;
&lt;p&gt;Playwright: Built-in&lt;/p&gt;
&lt;p&gt;Cross-browser:&lt;/p&gt;
&lt;p&gt;Selenium: Needs separate drivers&lt;/p&gt;
&lt;p&gt;Playwright: Bundled&lt;/p&gt;
&lt;p&gt;Parallel testing:&lt;/p&gt;
&lt;p&gt;Selenium: Requires Grid setup&lt;/p&gt;
&lt;p&gt;Playwright: Built-in&lt;/p&gt;
&lt;p&gt;Test execution:&lt;/p&gt;
&lt;p&gt;Selenium: Slower&lt;/p&gt;
&lt;p&gt;Playwright: 40-60% faster&lt;/p&gt;
&lt;p&gt;Debugging:&lt;/p&gt;
&lt;p&gt;Selenium: Stack traces only&lt;/p&gt;
&lt;p&gt;Playwright: Trace viewer with recordings&lt;/p&gt;
&lt;p&gt;If your Selenium tests are slow, flaky, or a pain to maintain — migration is worth it.&lt;/p&gt;
&lt;p&gt;The Migration Strategy&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t rewrite everything at once. Follow this incremental approach:&lt;/p&gt;
&lt;p&gt;Phase 1: New tests in Playwright&lt;/p&gt;
&lt;p&gt;Stop writing new Selenium tests. All new tests go in Playwright.&lt;/p&gt;
&lt;p&gt;Phase 2: Migrate critical paths first&lt;/p&gt;
&lt;p&gt;Identify your highest-value tests (login, checkout, core features) and migrate those.&lt;/p&gt;
&lt;p&gt;Phase 3: Gradual Selenium deprecation&lt;/p&gt;
&lt;p&gt;As you touch Selenium tests for maintenance, migrate them. Eventually, the old suite goes to zero.&lt;/p&gt;
&lt;p&gt;This approach minimizes risk and lets your team learn Playwright gradually.&lt;/p&gt;
&lt;p&gt;Step 1: Set Up Playwright Alongside Selenium&lt;/p&gt;
&lt;p&gt;You don&amp;#39;t need to remove Selenium to start. Create a parallel Playwright project:&lt;/p&gt;
&lt;p&gt;npm init playwright@latest -- --quiet&lt;/p&gt;
&lt;p&gt;Your project structure becomes:&lt;/p&gt;
&lt;p&gt;Both can run in CI simultaneously during transition.&lt;/p&gt;
&lt;p&gt;Step 2: Translate Common Patterns&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a side-by-side comparison of common operations:&lt;/p&gt;
&lt;p&gt;Navigation:&lt;/p&gt;
&lt;p&gt;Selenium (Java):&lt;/p&gt;
&lt;p&gt;driver.get(&amp;quot;&lt;a href=&quot;https://example.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://example.com&lt;/a&gt;&amp;quot;);&lt;/p&gt;
&lt;p&gt;Playwright (TypeScript):&lt;/p&gt;
&lt;p&gt;await page.goto(&amp;#39;&lt;a href=&quot;https://example.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://example.com&lt;/a&gt;&amp;#39;);&lt;/p&gt;
&lt;p&gt;Finding elements:&lt;/p&gt;
&lt;p&gt;Selenium:&lt;/p&gt;
&lt;p&gt;driver.findElement(By.id(&amp;quot;username&amp;quot;));&lt;/p&gt;
&lt;p&gt;driver.findElement(By.cssSelector(&amp;quot;.submit-btn&amp;quot;));&lt;/p&gt;
&lt;p&gt;driver.findElement(By.xpath(&amp;quot;//button[text()=&amp;#39;Submit&amp;#39;]&amp;quot;));&lt;/p&gt;
&lt;p&gt;Playwright:&lt;/p&gt;
&lt;p&gt;page.locator(&amp;#39;#username&amp;#39;);&lt;/p&gt;
&lt;p&gt;page.locator(&amp;#39;.submit-btn&amp;#39;);&lt;/p&gt;
&lt;p&gt;page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Submit&amp;#39; });&lt;/p&gt;
&lt;p&gt;Clicking:&lt;/p&gt;
&lt;p&gt;Selenium:&lt;/p&gt;
&lt;p&gt;driver.findElement(By.id(&amp;quot;submit&amp;quot;)).click();&lt;/p&gt;
&lt;p&gt;Playwright:&lt;/p&gt;
&lt;p&gt;await page.locator(&amp;#39;#submit&amp;#39;).click();&lt;/p&gt;
&lt;p&gt;Or better:&lt;/p&gt;
&lt;p&gt;await page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Submit&amp;#39; }).click();&lt;/p&gt;
&lt;p&gt;Typing:&lt;/p&gt;
&lt;p&gt;Selenium:&lt;/p&gt;
&lt;p&gt;driver.findElement(By.id(&amp;quot;email&amp;quot;)).sendKeys(&amp;quot;&lt;a href=&quot;mailto:test@example.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;test@example.com&lt;/a&gt;&amp;quot;);&lt;/p&gt;
&lt;p&gt;Playwright:&lt;/p&gt;
&lt;p&gt;await page.locator(&amp;#39;#email&amp;#39;).fill(&amp;#39;&lt;a href=&quot;mailto:test@example.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;test@example.com&lt;/a&gt;&amp;#39;);&lt;/p&gt;
&lt;p&gt;Or:&lt;/p&gt;
&lt;p&gt;await page.getByLabel(&amp;#39;Email&amp;#39;).fill(&amp;#39;&lt;a href=&quot;mailto:test@example.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;test@example.com&lt;/a&gt;&amp;#39;);&lt;/p&gt;
&lt;p&gt;Waiting:&lt;/p&gt;
&lt;p&gt;Selenium (the source of most flakiness):&lt;/p&gt;
&lt;p&gt;WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));&lt;/p&gt;
&lt;p&gt;wait.until(ExpectedConditions.visibilityOfElementLocated(By.id(&amp;quot;result&amp;quot;)));&lt;/p&gt;
&lt;p&gt;Playwright (auto-waits by default):&lt;/p&gt;
&lt;p&gt;Just interact — Playwright waits automatically&lt;/p&gt;
&lt;p&gt;await page.locator(&amp;#39;#result&amp;#39;).click();&lt;/p&gt;
&lt;p&gt;Or explicitly assert visibility:&lt;/p&gt;
&lt;p&gt;await expect(page.locator(&amp;#39;#result&amp;#39;)).toBeVisible();&lt;/p&gt;
&lt;p&gt;Assertions:&lt;/p&gt;
&lt;p&gt;Selenium (with JUnit):&lt;/p&gt;
&lt;p&gt;String title = driver.getTitle();&lt;/p&gt;
&lt;p&gt;assertEquals(&amp;quot;Expected Title&amp;quot;, title);&lt;/p&gt;
&lt;p&gt;Playwright:&lt;/p&gt;
&lt;p&gt;await expect(page).toHaveTitle(&amp;#39;Expected Title&amp;#39;);&lt;/p&gt;
&lt;p&gt;Screenshots:&lt;/p&gt;
&lt;p&gt;Selenium:&lt;/p&gt;
&lt;p&gt;File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);&lt;/p&gt;
&lt;p&gt;FileUtils.copyFile(screenshot, new File(&amp;quot;screenshot.png&amp;quot;));&lt;/p&gt;
&lt;p&gt;Playwright:&lt;/p&gt;
&lt;p&gt;await page.screenshot({ path: &amp;#39;screenshot.png&amp;#39; });&lt;/p&gt;
&lt;p&gt;Step 3: Migrate Page Objects&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re using Page Object Model in Selenium, the pattern translates directly.&lt;/p&gt;
&lt;p&gt;Selenium (Java):&lt;/p&gt;
&lt;p&gt;Playwright (TypeScript):&lt;/p&gt;
&lt;p&gt;The structure is almost identical. You&amp;#39;re just swapping APIs.&lt;/p&gt;
&lt;p&gt;Step 4: Replace WebDriver Waits with Auto-Wait&lt;/p&gt;
&lt;p&gt;This is where you get the biggest reliability win.&lt;/p&gt;
&lt;p&gt;Remove all explicit waits:&lt;/p&gt;
&lt;p&gt;In Selenium, you probably have code like:&lt;/p&gt;
&lt;p&gt;WebDriverWait wait = new WebDriverWait(driver, 10);&lt;/p&gt;
&lt;p&gt;wait.until(ExpectedConditions.elementToBeClickable(By.id(&amp;quot;button&amp;quot;)));&lt;/p&gt;
&lt;p&gt;driver.findElement(By.id(&amp;quot;button&amp;quot;)).click();&lt;/p&gt;
&lt;p&gt;In Playwright, just:&lt;/p&gt;
&lt;p&gt;await page.locator(&amp;#39;#button&amp;#39;).click();&lt;/p&gt;
&lt;p&gt;Playwright automatically waits for:&lt;/p&gt;
&lt;p&gt;Element to be attached to DOM&lt;/p&gt;
&lt;p&gt;Element to be visible&lt;/p&gt;
&lt;p&gt;Element to be enabled&lt;/p&gt;
&lt;p&gt;Element to be stable (not animating)&lt;/p&gt;
&lt;p&gt;This single change eliminates most flaky tests.&lt;/p&gt;
&lt;p&gt;Step 5: Set Up CI/CD&lt;/p&gt;
&lt;p&gt;Playwright&amp;#39;s CI setup is simpler than Selenium Grid.&lt;/p&gt;
&lt;p&gt;GitHub Actions example:&lt;/p&gt;
&lt;p&gt;No Selenium Grid. No browser driver management. Just install and run.&lt;/p&gt;
&lt;p&gt;Common Migration Pitfalls&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Trying to rewrite everything at once&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is the number one cause of failed migrations. Use the incremental approach.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Copying Selenium&amp;#39;s explicit waits&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Don&amp;#39;t translate WebDriverWait to waitForSelector. Trust Playwright&amp;#39;s auto-waiting.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Keeping fragile locators&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Migration is a chance to improve. Replace XPath and positional CSS with role-based locators.&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Not training the team&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Playwright has different patterns. Invest in a training session or pair programming during the first weeks.&lt;/p&gt;
&lt;p&gt;Migration Checklist&lt;/p&gt;
&lt;p&gt;Before declaring migration complete:&lt;/p&gt;
&lt;p&gt;All new tests written in Playwright&lt;/p&gt;
&lt;p&gt;Critical user journeys migrated&lt;/p&gt;
&lt;p&gt;CI/CD running Playwright tests&lt;/p&gt;
&lt;p&gt;Team trained on Playwright patterns&lt;/p&gt;
&lt;p&gt;Old Selenium tests deleted (don&amp;#39;t leave dead code)&lt;/p&gt;
&lt;p&gt;Trace viewer configured for failure debugging&lt;/p&gt;
&lt;p&gt;How Long Does Migration Take?&lt;/p&gt;
&lt;p&gt;Based on my experience:&lt;/p&gt;
&lt;p&gt;Small suite (under 50 tests): 1-2 weeks&lt;/p&gt;
&lt;p&gt;Medium suite (50-200 tests): 1-2 months&lt;/p&gt;
&lt;p&gt;Large suite (200+ tests): 3-6 months&lt;/p&gt;
&lt;p&gt;The biggest factor isn&amp;#39;t the number of tests — it&amp;#39;s how tangled your Selenium architecture is. Clean Page Objects migrate fast. Spaghetti code requires refactoring.&lt;/p&gt;
&lt;p&gt;The Results Are Worth It&lt;/p&gt;
&lt;p&gt;At CooperVision, after completing migration:&lt;/p&gt;
&lt;p&gt;Test execution time: 40% faster&lt;/p&gt;
&lt;p&gt;Flaky test rate: Reduced by 80%&lt;/p&gt;
&lt;p&gt;CI pipeline reliability: 98%+ pass rate&lt;/p&gt;
&lt;p&gt;Developer experience: &amp;quot;I actually like writing tests now&amp;quot;&lt;/p&gt;
&lt;p&gt;That last point matters more than any metric. When tests are pleasant to write, people write more tests.&lt;/p&gt;
&lt;p&gt;Need Help with Your Migration?&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re planning a Selenium to Playwright migration and want guidance from someone who&amp;#39;s done it multiple times, I can help. I offer consulting packages that include:&lt;/p&gt;
&lt;p&gt;Migration strategy and roadmap&lt;/p&gt;
&lt;p&gt;Team training on Playwright patterns&lt;/p&gt;
&lt;p&gt;Architecture review of existing tests&lt;/p&gt;
&lt;p&gt;Hands-on migration for critical paths&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s talk&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>selenium</category><category>selenium</category><category>test-automation</category><category>typescript</category></item><item><title>How to Fix Flaky Tests in Playwright: 10 Battle-Tested Strategies</title><link>https://www.anton.qa/blog/posts/how-to-fix-flaky-tests-in-playwright-10-battle-tested-strategies</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/how-to-fix-flaky-tests-in-playwright-10-battle-tested-strategies</guid><description>Flaky tests destroy team confidence and slow deployment. Here are 10 proven strategies to eliminate flaky Playwright tests — from someone who&apos;s fixed thousands of them.</description><pubDate>Fri, 30 Jan 2026 05:16:00 GMT</pubDate><content:encoded>&lt;p&gt;The Cost of Flaky Tests&lt;/p&gt;
&lt;p&gt;A flaky test is one that sometimes passes and sometimes fails without any code changes. They seem harmless at first — just re-run the pipeline, right?&lt;/p&gt;
&lt;p&gt;Wrong. Flaky tests are silent killers:&lt;/p&gt;
&lt;p&gt;Eroded trust — Teams stop believing test results&lt;/p&gt;
&lt;p&gt;Wasted time — Engineers debug tests instead of building features&lt;/p&gt;
&lt;p&gt;Slower releases — &amp;quot;Just re-run it&amp;quot; becomes the norm&lt;/p&gt;
&lt;p&gt;Hidden bugs — Real failures get dismissed as &amp;quot;flaky&amp;quot;&lt;/p&gt;
&lt;p&gt;At one company, I inherited a test suite with a 68% pass rate. Not because the application was broken — but because the tests were. Here&amp;#39;s how I fixed it.&lt;/p&gt;
&lt;p&gt;Strategy 1: Use Role-Based Locators&lt;/p&gt;
&lt;p&gt;The problem: CSS selectors and XPath break when developers change class names or restructure HTML.&lt;/p&gt;
&lt;p&gt;The fix: Use Playwright&amp;#39;s role-based locators:&lt;/p&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;p&gt;page.locator(&amp;#39;#submit-btn&amp;#39;)&lt;/p&gt;
&lt;p&gt;page.locator(&amp;#39;.form-container &amp;gt; button:nth-child(2)&amp;#39;)&lt;/p&gt;
&lt;p&gt;Good:&lt;/p&gt;
&lt;p&gt;page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Submit&amp;#39; })&lt;/p&gt;
&lt;p&gt;page.getByLabel(&amp;#39;Email address&amp;#39;)&lt;/p&gt;
&lt;p&gt;page.getByText(&amp;#39;Welcome back&amp;#39;)&lt;/p&gt;
&lt;p&gt;Role-based locators are more resilient because they target what users see, not implementation details.&lt;/p&gt;
&lt;p&gt;Strategy 2: Never Use Hard-Coded Waits&lt;/p&gt;
&lt;p&gt;The problem: waitForTimeout() is the number one cause of flaky tests.&lt;/p&gt;
&lt;p&gt;await page.waitForTimeout(5000); // ❌ Please don&amp;#39;t do this&lt;/p&gt;
&lt;p&gt;Why it fails: 5 seconds might be enough on your machine but not in CI. Or it might be way too long, slowing tests unnecessarily.&lt;/p&gt;
&lt;p&gt;The fix: Wait for specific conditions:&lt;/p&gt;
&lt;p&gt;await page.waitForLoadState(&amp;#39;networkidle&amp;#39;);&lt;/p&gt;
&lt;p&gt;await expect(page.getByRole(&amp;#39;button&amp;#39;)).toBeEnabled();&lt;/p&gt;
&lt;p&gt;await expect(page.getByText(&amp;#39;Success&amp;#39;)).toBeVisible();&lt;/p&gt;
&lt;p&gt;Playwright&amp;#39;s auto-waiting handles most cases automatically. Trust it.&lt;/p&gt;
&lt;p&gt;Strategy 3: Isolate Test State&lt;/p&gt;
&lt;p&gt;The problem: Tests depend on state from previous tests.&lt;/p&gt;
&lt;p&gt;test(&amp;#39;login&amp;#39;, ...);  // Creates session&lt;/p&gt;
&lt;p&gt;test(&amp;#39;add to cart&amp;#39;, ...);  // Expects logged-in state&lt;/p&gt;
&lt;p&gt;If the login test fails, the cart test also fails — but not because of a cart bug.&lt;/p&gt;
&lt;p&gt;The fix: Each test should set up its own state:&lt;/p&gt;
&lt;p&gt;Or use Playwright&amp;#39;s storage state to share authentication without dependencies:&lt;/p&gt;
&lt;p&gt;await page.context().storageState({ path: &amp;#39;auth.json&amp;#39; });&lt;/p&gt;
&lt;p&gt;Strategy 4: Handle Loading States Explicitly&lt;/p&gt;
&lt;p&gt;The problem: Clicking a button that&amp;#39;s still loading, or reading text before it&amp;#39;s rendered.&lt;/p&gt;
&lt;p&gt;The fix: Wait for loading indicators to disappear:&lt;/p&gt;
&lt;p&gt;Or wait for the element to be in a specific state:&lt;/p&gt;
&lt;p&gt;Strategy 5: Use data-testid for Dynamic Content&lt;/p&gt;
&lt;p&gt;The problem: Elements generated dynamically have unpredictable locators.&lt;/p&gt;
&lt;p&gt;The fix: Add data-testid attributes for testing:&lt;/p&gt;
&lt;p&gt;In your application code:&lt;/p&gt;
&lt;p&gt;&lt;button data-testid=&quot;checkout-button&quot;&gt;Checkout&lt;/button&gt;&lt;/p&gt;
&lt;p&gt;In your test:&lt;/p&gt;
&lt;p&gt;await page.getByTestId(&amp;#39;checkout-button&amp;#39;).click();&lt;/p&gt;
&lt;p&gt;This creates a contract between frontend and tests that survives refactoring.&lt;/p&gt;
&lt;p&gt;Strategy 6: Retry Failed Assertions (Not Whole Tests)&lt;/p&gt;
&lt;p&gt;The problem: A test fails once and you re-run the entire suite.&lt;/p&gt;
&lt;p&gt;The fix: Use Playwright&amp;#39;s built-in expect retries:&lt;/p&gt;
&lt;p&gt;Assertions like toBeVisible() and toHaveText() will automatically retry until timeout — no manual retries needed.&lt;/p&gt;
&lt;p&gt;Strategy 7: Handle Network Variability&lt;/p&gt;
&lt;p&gt;The problem: API calls take longer in CI than locally.&lt;/p&gt;
&lt;p&gt;The fix: Wait for network responses explicitly:&lt;/p&gt;
&lt;p&gt;Or use networkidle for simpler cases:&lt;/p&gt;
&lt;p&gt;await page.goto(&amp;#39;/dashboard&amp;#39;, { waitUntil: &amp;#39;networkidle&amp;#39; });&lt;/p&gt;
&lt;p&gt;Strategy 8: Run Tests in Parallel Correctly&lt;/p&gt;
&lt;p&gt;The problem: Tests interfere with each other when running in parallel.&lt;/p&gt;
&lt;p&gt;Test A creates user &amp;quot;&lt;a href=&quot;mailto:testuser@example.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;testuser@example.com&lt;/a&gt;&amp;quot;&lt;/p&gt;
&lt;p&gt;Test B also creates user &amp;quot;&lt;a href=&quot;mailto:testuser@example.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;testuser@example.com&lt;/a&gt;&amp;quot;&lt;/p&gt;
&lt;p&gt;One fails due to duplicate email.&lt;/p&gt;
&lt;p&gt;The fix: Use unique data per test:&lt;/p&gt;
&lt;p&gt;Or isolate tests in separate browser contexts (Playwright does this by default).&lt;/p&gt;
&lt;p&gt;Strategy 9: Use Trace Viewer for Debugging&lt;/p&gt;
&lt;p&gt;The problem: You can&amp;#39;t see what happened when a test failed in CI.&lt;/p&gt;
&lt;p&gt;The fix: Enable traces on failure:&lt;/p&gt;
&lt;p&gt;Now, when a test fails and retries, Playwright captures:&lt;/p&gt;
&lt;p&gt;Screenshots at every step&lt;/p&gt;
&lt;p&gt;DOM snapshots&lt;/p&gt;
&lt;p&gt;Network requests&lt;/p&gt;
&lt;p&gt;Console logs&lt;/p&gt;
&lt;p&gt;Open traces with:&lt;/p&gt;
&lt;p&gt;npx playwright show-trace trace.zip&lt;/p&gt;
&lt;p&gt;This is the single best debugging tool for flaky tests.&lt;/p&gt;
&lt;p&gt;Strategy 10: Set Realistic Timeouts&lt;/p&gt;
&lt;p&gt;The problem: Default timeouts are too short for slow environments.&lt;/p&gt;
&lt;p&gt;The fix: Configure appropriate timeouts based on your CI environment:&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t make them too long — slow failures are frustrating. Find the right balance for your infrastructure.&lt;/p&gt;
&lt;p&gt;Bonus: The Flaky Test Triage Process&lt;/p&gt;
&lt;p&gt;When you encounter a flaky test, follow this process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reproduce locally&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run the test 10 times:&lt;/p&gt;
&lt;p&gt;npx playwright test tests/checkout.spec.ts --repeat-each=10&lt;/p&gt;
&lt;p&gt;If it passes every time locally but fails in CI, it&amp;#39;s likely a timing or environment issue.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Check the trace&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Open the trace file from CI and look for:&lt;/p&gt;
&lt;p&gt;Slow network requests&lt;/p&gt;
&lt;p&gt;Elements not visible when clicked&lt;/p&gt;
&lt;p&gt;Unexpected modal or popup&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Identify the root cause&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Common causes:&lt;/p&gt;
&lt;p&gt;Hard-coded wait times&lt;/p&gt;
&lt;p&gt;Race conditions&lt;/p&gt;
&lt;p&gt;Shared test state&lt;/p&gt;
&lt;p&gt;Unstable locators&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Fix or delete&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you can&amp;#39;t fix it after 30 minutes, delete it. A flaky test is worse than no test. You can always rewrite it properly later.&lt;/p&gt;
&lt;p&gt;My Flaky Test Scorecard&lt;/p&gt;
&lt;p&gt;After applying these strategies at CooperVision, we went from 68% pass rate to 98%+ in three months.&lt;/p&gt;
&lt;p&gt;Pass rate:&lt;/p&gt;
&lt;p&gt;Before: 68%&lt;/p&gt;
&lt;p&gt;After: 98.5%&lt;/p&gt;
&lt;p&gt;Average test time:&lt;/p&gt;
&lt;p&gt;Before: 4.2 minutes&lt;/p&gt;
&lt;p&gt;After: 1.8 minutes&lt;/p&gt;
&lt;p&gt;&amp;quot;Re-run pipeline&amp;quot; requests:&lt;/p&gt;
&lt;p&gt;Before: Daily&lt;/p&gt;
&lt;p&gt;After: Rare&lt;/p&gt;
&lt;p&gt;Team trust in automation:&lt;/p&gt;
&lt;p&gt;Before: Low&lt;/p&gt;
&lt;p&gt;After: High&lt;/p&gt;
&lt;p&gt;The biggest win wasn&amp;#39;t technical — it was cultural. When tests are reliable, developers actually care about failures.&lt;/p&gt;
&lt;p&gt;Need Help with Your Flaky Tests?&lt;/p&gt;
&lt;p&gt;If your test suite is unreliable and slowing down your team, I can help. I&amp;#39;ve fixed test suites at Fortune 500 companies and can usually identify the main issues in just a few hours.&lt;/p&gt;
&lt;p&gt;Book a consultation&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>flaky-tests</category><category>reliability</category><category>debugging</category><category>best-practices</category></item><item><title>Playwright Tutorial for Beginners: Your First Test in 10 Minutes</title><link>https://www.anton.qa/blog/posts/playwright-tutorial-for-beginners-your-first-test-in-10-minutes</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-tutorial-for-beginners-your-first-test-in-10-minutes</guid><description>Learn Playwright from scratch. This step-by-step tutorial will have you writing your first automated test in under 10 minutes — no prior automation experience required.</description><pubDate>Thu, 29 Jan 2026 06:02:00 GMT</pubDate><content:encoded>&lt;p&gt;What You&amp;#39;ll Learn&lt;/p&gt;
&lt;p&gt;By the end of this tutorial, you&amp;#39;ll have:&lt;/p&gt;
&lt;p&gt;Playwright installed on your machine&lt;/p&gt;
&lt;p&gt;Written and run your first automated test&lt;/p&gt;
&lt;p&gt;Understood the basic Playwright concepts&lt;/p&gt;
&lt;p&gt;A foundation to build real-world test suites&lt;/p&gt;
&lt;p&gt;Total time: about 10 minutes. Let&amp;#39;s go.&lt;/p&gt;
&lt;p&gt;Prerequisites&lt;/p&gt;
&lt;p&gt;You need Node.js 18+ installed. Check your version:&lt;/p&gt;
&lt;p&gt;node --version&lt;/p&gt;
&lt;p&gt;If you need to install Node.js, download it from nodejs.org.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s it. No other dependencies required.&lt;/p&gt;
&lt;p&gt;Step 1: Create Your Project&lt;/p&gt;
&lt;p&gt;Open your terminal and create a new folder:&lt;/p&gt;
&lt;p&gt;mkdir playwright-tutorial&lt;/p&gt;
&lt;p&gt;cd playwright-tutorial&lt;/p&gt;
&lt;p&gt;Step 2: Install Playwright&lt;/p&gt;
&lt;p&gt;Run the Playwright installer:&lt;/p&gt;
&lt;p&gt;npm init playwright@latest&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll see some prompts. Accept the defaults:&lt;/p&gt;
&lt;p&gt;TypeScript or JavaScript? → TypeScript (recommended)&lt;/p&gt;
&lt;p&gt;Where to put tests? → tests&lt;/p&gt;
&lt;p&gt;Add GitHub Actions? → Yes (optional, but useful later)&lt;/p&gt;
&lt;p&gt;Install browsers? → Yes&lt;/p&gt;
&lt;p&gt;This installs Playwright, downloads browser binaries, and creates a basic project structure.&lt;/p&gt;
&lt;p&gt;Step 3: Explore the Project Structure&lt;/p&gt;
&lt;p&gt;After installation, you&amp;#39;ll see:&lt;/p&gt;
&lt;p&gt;tests/ — Your test files go here&lt;/p&gt;
&lt;p&gt;playwright.config.ts — Configuration file&lt;/p&gt;
&lt;p&gt;tests/example.spec.ts — A sample test (we&amp;#39;ll replace this)&lt;/p&gt;
&lt;p&gt;Step 4: Write Your First Test&lt;/p&gt;
&lt;p&gt;Delete the example test and create a new file called tests/first.spec.ts:&lt;/p&gt;
&lt;p&gt;What this does:&lt;/p&gt;
&lt;p&gt;test() — Defines a test with a name&lt;/p&gt;
&lt;p&gt;page.goto() — Navigates to a URL&lt;/p&gt;
&lt;p&gt;expect().toHaveTitle() — Asserts the page title contains &amp;quot;Playwright&amp;quot;&lt;/p&gt;
&lt;p&gt;Step 5: Run Your Test&lt;/p&gt;
&lt;p&gt;In your terminal:&lt;/p&gt;
&lt;p&gt;npx playwright test&lt;/p&gt;
&lt;p&gt;You should see:&lt;/p&gt;
&lt;p&gt;Congratulations! You just ran your first Playwright test.&lt;/p&gt;
&lt;p&gt;Step 6: See the Test Report&lt;/p&gt;
&lt;p&gt;Playwright generates beautiful HTML reports. View it:&lt;/p&gt;
&lt;p&gt;npx playwright show-report&lt;/p&gt;
&lt;p&gt;This opens a browser with your test results, including timing, screenshots, and traces.&lt;/p&gt;
&lt;p&gt;Step 7: Run Tests in UI Mode (Game Changer)&lt;/p&gt;
&lt;p&gt;Playwright&amp;#39;s UI mode lets you watch tests run in real-time:&lt;/p&gt;
&lt;p&gt;npx playwright test --ui&lt;/p&gt;
&lt;p&gt;This opens a visual interface where you can:&lt;/p&gt;
&lt;p&gt;Watch tests execute step-by-step&lt;/p&gt;
&lt;p&gt;Inspect DOM at any point&lt;/p&gt;
&lt;p&gt;Debug failures visually&lt;/p&gt;
&lt;p&gt;Re-run individual tests&lt;/p&gt;
&lt;p&gt;This is my favorite Playwright feature for development.&lt;/p&gt;
&lt;p&gt;Step 8: Add More Assertions&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s expand our test to click a link and verify navigation:&lt;/p&gt;
&lt;p&gt;New concepts:&lt;/p&gt;
&lt;p&gt;getByRole() — Finds elements by their accessibility role (best practice)&lt;/p&gt;
&lt;p&gt;click() — Clicks an element&lt;/p&gt;
&lt;p&gt;toHaveURL() — Asserts the current URL matches a pattern&lt;/p&gt;
&lt;p&gt;toBeVisible() — Asserts an element is visible on page&lt;/p&gt;
&lt;p&gt;Step 9: Use the Code Generator&lt;/p&gt;
&lt;p&gt;Playwright can generate test code by recording your actions:&lt;/p&gt;
&lt;p&gt;npx playwright codegen playwright.dev&lt;/p&gt;
&lt;p&gt;This opens a browser. Click around, and Playwright writes code for you in real-time. Copy the generated code into your test file.&lt;/p&gt;
&lt;p&gt;This is extremely useful for learning locators and speeding up test creation.&lt;/p&gt;
&lt;p&gt;Understanding Key Concepts&lt;/p&gt;
&lt;p&gt;Locators:&lt;/p&gt;
&lt;p&gt;Playwright finds elements using locators. Best practices:&lt;/p&gt;
&lt;p&gt;getByRole() — By accessibility role (button, link, heading)&lt;/p&gt;
&lt;p&gt;getByText() — By visible text&lt;/p&gt;
&lt;p&gt;getByLabel() — By form label&lt;/p&gt;
&lt;p&gt;getByTestId() — By data-testid attribute&lt;/p&gt;
&lt;p&gt;Avoid CSS selectors and XPath when possible — role-based locators are more resilient.&lt;/p&gt;
&lt;p&gt;Auto-waiting:&lt;/p&gt;
&lt;p&gt;Playwright automatically waits for elements to be ready before interacting. No more:&lt;/p&gt;
&lt;p&gt;await page.waitForSelector(&amp;#39;.button&amp;#39;); // Not needed!&lt;/p&gt;
&lt;p&gt;Just write:&lt;/p&gt;
&lt;p&gt;await page.click(&amp;#39;.button&amp;#39;); // Playwright handles waiting&lt;/p&gt;
&lt;p&gt;This dramatically reduces flaky tests.&lt;/p&gt;
&lt;p&gt;Next Steps&lt;/p&gt;
&lt;p&gt;Now that you have the basics:&lt;/p&gt;
&lt;p&gt;Add more tests — Start testing your own application&lt;/p&gt;
&lt;p&gt;Learn Page Object Model — Organize tests for maintainability&lt;/p&gt;
&lt;p&gt;Set up CI/CD — Run tests on every pull request&lt;/p&gt;
&lt;p&gt;Explore API testing — Playwright also tests REST APIs&lt;/p&gt;
&lt;p&gt;Common Beginner Mistakes to Avoid&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Using sleep/delay:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;p&gt;await page.waitForTimeout(5000);&lt;/p&gt;
&lt;p&gt;Good: Let Playwright&amp;#39;s auto-waiting handle it, or wait for specific conditions.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Fragile locators:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;p&gt;page.locator(&amp;#39;#app &amp;gt; div:nth-child(3) &amp;gt; button&amp;#39;)&lt;/p&gt;
&lt;p&gt;Good:&lt;/p&gt;
&lt;p&gt;page.getByRole(&amp;#39;button&amp;#39;, { name: &amp;#39;Submit&amp;#39; })&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Not using test isolation:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each test should be independent. Don&amp;#39;t rely on state from previous tests.&lt;/p&gt;
&lt;p&gt;Resources&lt;/p&gt;
&lt;p&gt;Official docs: playwright.dev/docs/intro&lt;/p&gt;
&lt;p&gt;Test generator:&lt;/p&gt;
&lt;p&gt;npx playwright codegen [url]&lt;/p&gt;
&lt;p&gt;UI mode:&lt;/p&gt;
&lt;p&gt;npx playwright test --ui&lt;/p&gt;
&lt;p&gt;Report viewer:&lt;/p&gt;
&lt;p&gt;npx playwright show-report&lt;/p&gt;
&lt;p&gt;Need Hands-On Help?&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re building a test automation framework for your team and want guidance from someone who&amp;#39;s done it at Apple and Fortune 500 companies, I offer consulting and training.&lt;/p&gt;
&lt;p&gt;Learn more about working with me&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>tutorial</category><category>beginner</category><category>typescript</category><category>getting-started</category></item><item><title>Playwright vs Cypress vs Selenium in 2026: The Definitive Comparison</title><link>https://www.anton.qa/blog/posts/playwright-vs-cypress-vs-selenium-in-2026-the-definitive-comparison</link><guid isPermaLink="true">https://www.anton.qa/blog/posts/playwright-vs-cypress-vs-selenium-in-2026-the-definitive-comparison</guid><description>Which test automation framework should you choose in 2026? A side-by-side comparison of Playwright, Cypress, and Selenium based on real-world experience testing for Apple, Fortune 500 companies, and enterprise teams.</description><pubDate>Wed, 28 Jan 2026 05:01:00 GMT</pubDate><content:encoded>&lt;p&gt;Why This Comparison Matters in 2026&lt;/p&gt;
&lt;p&gt;After 10+ years of building test automation frameworks at Apple, Fortune 500 companies, and fast-moving startups, I&amp;#39;ve worked with all three tools extensively. This isn&amp;#39;t a theoretical comparison — it&amp;#39;s based on real production experience.&lt;/p&gt;
&lt;p&gt;The test automation landscape has shifted dramatically. Playwright has matured into a serious contender, Cypress has expanded beyond its original scope, and Selenium remains the enterprise backbone. But which one should you actually choose?&lt;/p&gt;
&lt;p&gt;Let me break it down.&lt;/p&gt;
&lt;p&gt;Quick Comparison&lt;/p&gt;
&lt;p&gt;Playwright:&lt;/p&gt;
&lt;p&gt;Language Support: TypeScript, JavaScript, Python, Java, C#&lt;/p&gt;
&lt;p&gt;Browser Support: Chromium, Firefox, WebKit&lt;/p&gt;
&lt;p&gt;Speed: Very Fast&lt;/p&gt;
&lt;p&gt;Learning Curve: Moderate&lt;/p&gt;
&lt;p&gt;Price: Free&lt;/p&gt;
&lt;p&gt;Cypress:&lt;/p&gt;
&lt;p&gt;Language Support: JavaScript/TypeScript only&lt;/p&gt;
&lt;p&gt;Browser Support: Chromium, Firefox, Edge, WebKit&lt;/p&gt;
&lt;p&gt;Speed: Fast&lt;/p&gt;
&lt;p&gt;Learning Curve: Easy&lt;/p&gt;
&lt;p&gt;Price: Free (paid for some features)&lt;/p&gt;
&lt;p&gt;Selenium:&lt;/p&gt;
&lt;p&gt;Language Support: All major languages&lt;/p&gt;
&lt;p&gt;Browser Support: All browsers&lt;/p&gt;
&lt;p&gt;Speed: Moderate&lt;/p&gt;
&lt;p&gt;Learning Curve: Steeper&lt;/p&gt;
&lt;p&gt;Price: Free&lt;/p&gt;
&lt;p&gt;Playwright: The New Standard&lt;/p&gt;
&lt;p&gt;Playwright has become my go-to framework for new projects. Built by Microsoft (the same team that originally created Puppeteer), it addresses almost every pain point I&amp;#39;ve encountered with other tools.&lt;/p&gt;
&lt;p&gt;What Playwright does best:&lt;/p&gt;
&lt;p&gt;True cross-browser testing — Chrome, Firefox, Safari (WebKit) from a single API&lt;/p&gt;
&lt;p&gt;Auto-waiting — No more flaky&lt;/p&gt;
&lt;p&gt;waitFor&lt;/p&gt;
&lt;p&gt;statements&lt;/p&gt;
&lt;p&gt;Built-in parallelization — Shards tests automatically across workers&lt;/p&gt;
&lt;p&gt;Native TypeScript support — First-class, not bolted on&lt;/p&gt;
&lt;p&gt;Trace viewer — Debug failures with recordings, network logs, and DOM snapshots&lt;/p&gt;
&lt;p&gt;API testing included — Test REST endpoints with the same framework&lt;/p&gt;
&lt;p&gt;When to choose Playwright:&lt;/p&gt;
&lt;p&gt;Starting a new automation framework from scratch&lt;/p&gt;
&lt;p&gt;Need cross-browser testing (including Safari)&lt;/p&gt;
&lt;p&gt;Team knows TypeScript or willing to learn&lt;/p&gt;
&lt;p&gt;Want modern DX (developer experience&lt;/p&gt;
&lt;p&gt;Cypress: Still Strong, But With Caveats&lt;/p&gt;
&lt;p&gt;Cypress pioneered the &amp;quot;developer-friendly testing&amp;quot; movement. Its interactive test runner and real-time reloading changed how developers think about testing.&lt;/p&gt;
&lt;p&gt;What Cypress does best:&lt;/p&gt;
&lt;p&gt;Developer experience — The interactive UI is unmatched for debugging&lt;/p&gt;
&lt;p&gt;Easy setup — npm install and you&amp;#39;re running tests&lt;/p&gt;
&lt;p&gt;Excellent documentation — Some of the best in the industry&lt;/p&gt;
&lt;p&gt;Component testing — Strong support for React, Vue, Angular&lt;/p&gt;
&lt;p&gt;The catches:&lt;/p&gt;
&lt;p&gt;No native Safari support — WebKit support is experimental&lt;/p&gt;
&lt;p&gt;Parallel testing is paid — Cypress Cloud subscription required&lt;/p&gt;
&lt;p&gt;Origin limitations — Testing across multiple domains can be tricky&lt;/p&gt;
&lt;p&gt;Slower execution — Runs inside the browser, not alongside it&lt;/p&gt;
&lt;p&gt;When to choose Cypress:&lt;/p&gt;
&lt;p&gt;Small team, primarily Chrome users&lt;/p&gt;
&lt;p&gt;Heavy component testing needs&lt;/p&gt;
&lt;p&gt;Budget for Cypress Cloud&lt;/p&gt;
&lt;p&gt;JavaScript-only team&lt;/p&gt;
&lt;p&gt;Selenium: The Enterprise Workhorse&lt;/p&gt;
&lt;p&gt;Selenium has been around for almost 20 years. It&amp;#39;s not the flashiest choice, but it&amp;#39;s battle-tested across every major enterprise on the planet.&lt;/p&gt;
&lt;p&gt;What Selenium does best:&lt;/p&gt;
&lt;p&gt;Universal language support — Java, Python, C#, Ruby, JavaScript&lt;/p&gt;
&lt;p&gt;Massive ecosystem — Extensive tooling, plugins, and community&lt;/p&gt;
&lt;p&gt;Enterprise trust — Every company has Selenium experience&lt;/p&gt;
&lt;p&gt;Any browser — True universal browser support&lt;/p&gt;
&lt;p&gt;Mobile via Appium — Extends to native mobile testing&lt;/p&gt;
&lt;p&gt;The challenges:&lt;/p&gt;
&lt;p&gt;No auto-waiting — You manage waits manually (flakiness source)&lt;/p&gt;
&lt;p&gt;More boilerplate — More code to achieve the same result&lt;/p&gt;
&lt;p&gt;Slower test execution — WebDriver protocol overhead&lt;/p&gt;
&lt;p&gt;Grid complexity — Parallel execution requires infrastructure&lt;/p&gt;
&lt;p&gt;When to choose Selenium:&lt;/p&gt;
&lt;p&gt;Large enterprise with existing Selenium investment&lt;/p&gt;
&lt;p&gt;Need languages Playwright doesn&amp;#39;t support&lt;/p&gt;
&lt;p&gt;Extending to mobile testing with Appium&lt;/p&gt;
&lt;p&gt;Team has deep Selenium expertise&lt;/p&gt;
&lt;p&gt;Real-World Performance Comparison&lt;/p&gt;
&lt;p&gt;I ran the same test suite (50 tests, e-commerce checkout flow) across all three frameworks. Here&amp;#39;s what I found:&lt;/p&gt;
&lt;p&gt;Setup time:&lt;/p&gt;
&lt;p&gt;Playwright: 5 minutes&lt;/p&gt;
&lt;p&gt;Cypress: 5 minutes&lt;/p&gt;
&lt;p&gt;Selenium: 15 minutes&lt;/p&gt;
&lt;p&gt;Test execution (50 tests):&lt;/p&gt;
&lt;p&gt;Playwright: 45 seconds&lt;/p&gt;
&lt;p&gt;Cypress: 72 seconds&lt;/p&gt;
&lt;p&gt;Selenium: 98 seconds&lt;/p&gt;
&lt;p&gt;Flaky test rate:&lt;/p&gt;
&lt;p&gt;Playwright: 2%&lt;/p&gt;
&lt;p&gt;Cypress: 8%&lt;/p&gt;
&lt;p&gt;Selenium: 15%&lt;/p&gt;
&lt;p&gt;CI/CD complexity:&lt;/p&gt;
&lt;p&gt;Playwright: Low&lt;/p&gt;
&lt;p&gt;Cypress: Medium&lt;/p&gt;
&lt;p&gt;Selenium: High&lt;/p&gt;
&lt;p&gt;Playwright&amp;#39;s auto-waiting and parallel execution give it a significant edge in both speed and reliability.&lt;/p&gt;
&lt;p&gt;My Recommendation for 2026&lt;/p&gt;
&lt;p&gt;For new projects: Playwright. The developer experience, cross-browser support, and performance make it the best choice going forward.&lt;/p&gt;
&lt;p&gt;For existing Cypress projects: Stay with Cypress if it&amp;#39;s working. Migrate only if you&amp;#39;re hitting limitations (Safari testing, parallel costs).&lt;/p&gt;
&lt;p&gt;For existing Selenium projects: Consider a gradual migration to Playwright. The migration path is cleaner than you might expect — many patterns transfer directly.&lt;/p&gt;
&lt;p&gt;Making the Final Decision&lt;/p&gt;
&lt;p&gt;Ask yourself these questions:&lt;/p&gt;
&lt;p&gt;Do you need Safari/WebKit testing? → Playwright&lt;/p&gt;
&lt;p&gt;Is your team JavaScript-only with limited testing experience? → Cypress&lt;/p&gt;
&lt;p&gt;Do you have significant Selenium investment and enterprise constraints? → Selenium (with migration roadmap)&lt;/p&gt;
&lt;p&gt;Starting fresh with no legacy code? → Playwright, every time&lt;/p&gt;
&lt;p&gt;The frameworks are all capable of building production-quality test automation. The right choice depends on your team, your constraints, and where you&amp;#39;re headed — not where you&amp;#39;ve been.&lt;/p&gt;
&lt;p&gt;Need Help Choosing or Migrating?&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re evaluating frameworks or considering a migration from Selenium or Cypress to Playwright, I can help. I&amp;#39;ve led migrations at Fortune 500 companies and can have your first Playwright tests running in a week.&lt;/p&gt;
&lt;p&gt;Get in touch&lt;/p&gt;
</content:encoded><dc:creator>Anton Gulin</dc:creator><category>playwright</category><category>cypress</category><category>selenium</category><category>comparison</category><category>test-automation</category></item></channel></rss>