tamash-playwright-cli 0.1.0

dotnet tool install --global tamash-playwright-cli --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local tamash-playwright-cli --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=tamash-playwright-cli&version=0.1.0
                    
nuke :add-package tamash-playwright-cli --version 0.1.0
                    

tamash-playwright

tamash-playwright is a plug and play self-healing solution for Playwright .NET — works with NUnit, MSTest, or xUnit. Install it, add your AI API key details, and swap one base class.

That's it. No changes needed to your actual test methods if you're following standard Playwright/.NET testing best practices.

Also available for TypeScript (tamash-playwright on npm), Python (tamash-playwright on PyPI), and Java (io.github.qtpsudhakarproducts:tamash-playwright on Maven Central) — same name, same idea, separate package per ecosystem.

Why you need this

Websites change often. A button gets renamed or moved, and your test can't find it anymore — even though the app still works fine for real users. Normally, that just means a broken test.

tamash-playwright fixes this automatically. When a test action can't find an element, it asks an AI model to find it on the current page and tries again. If it succeeds, your test keeps going. If not, it fails normally, just like before.

Here are the detailed steps to use this package.

Step 1: Install it

dotnet add package tamash-playwright
dotnet add package tamash-playwright-nunit

This pulls in Microsoft.Playwright and NUnit as dependencies. If you're starting fresh, you'll also need the Playwright browsers:

pwsh bin/Debug/net10.0/playwright.ps1 install

(run from your test project directory after your first build)

Step 2: Connect an AI model

tamash-playwright needs an AI model to decide where a broken element actually went. Pick one of Ollama, OpenAI, Anthropic (Claude), or Google Gemini, and give it an API key.

Create a file named .env in your project folder:

# Master on/off switch. Leave this as true, or remove the line entirely.
HEALER_ENABLED=true

# Pick one: ollama | openai | anthropic | gemini
HEALER_PROVIDER=ollama

# --- Ollama Cloud (https://ollama.com) ---
OLLAMA_MODEL=gpt-oss:120b
OLLAMA_API_KEY=

# --- OpenAI ---
# OPENAI_MODEL=gpt-4.1-mini
# OPENAI_API_KEY=

# --- Anthropic ---
# ANTHROPIC_MODEL=claude-haiku-4-5
# ANTHROPIC_API_KEY=

# --- Google Gemini ---
# GEMINI_MODEL=
# GEMINI_API_KEY=

Just fill in the API key and model for whichever one you want to use, and leave the rest as-is (or delete them).

.env is found by searching your working directory and then walking up parent directories — this matters more here than in the other ports, since dotnet test runs with its working directory set to the build output folder (bin/Debug/net10.0/), not your project root. Put .env at your solution/project root and it'll be found either way.

Getting a free Ollama key (fastest way to get started)

Ollama Cloud is a quick, free way to get an API key without signing up for OpenAI/Anthropic/Gemini billing.

  1. Go to ollama.com and create an account.
  2. Once signed in, go to ollama.com/settings/keys.
  3. Create a new API key and copy it.
  4. Paste it into your .env file:
HEALER_ENABLED=true
HEALER_PROVIDER=ollama
OLLAMA_MODEL=gpt-oss:120b
OLLAMA_API_KEY=paste_your_key_here

That's all you need — no other variables required.

Step 3: Use it in your tests

Pick whichever test runner you're already using — all three get the same self-healing Page, wired in differently because each runner has its own lifecycle model.

NUnit

dotnet add package tamash-playwright-nunit

Inherit from TamashPageTest instead of Microsoft.Playwright.NUnit.PageTest — everything else about writing the test stays the same:

// Before
using Microsoft.Playwright.NUnit;

public class LoginTest : PageTest { ... }

// After
using Tamash.Playwright.NUnit;

public class LoginTest : TamashPageTest { ... }

Write your tests as normal — Page is available as a property exactly like Playwright's own NUnit integration:

using NUnit.Framework;
using Tamash.Playwright.NUnit;

using static Tamash.Playwright.Bindings;
using static Microsoft.Playwright.Assertions;

[TestFixture]
public class LoginTest : TamashPageTest
{
    [Test]
    public async Task LogsIn()
    {
        await Page.GotoAsync("/");
        await Page.GetByPlaceholder("Username").FillAsync("Admin"); // healed automatically if this breaks
        await Page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
        await Expect(Unwrap(Page.GetByRole(AriaRole.Heading, new() { Name = "Dashboard" }))).ToBeVisibleAsync();
    }
}

Under the hood, TamashPageTest runs its own Playwright/Browser/BrowserContext/Page lifecycle rather than wrapping Microsoft's own PageTest — there's no guarantee its Page property is designed to be overridden by a derived class, so this package manages that lifecycle itself ([OneTimeSetUp]/[OneTimeTearDown] launch the browser once per test class, [SetUp]/[TearDown] create a fresh context per test method), the same reasoning behind the Java port's TamashPlaywrightExtension.

MSTest

dotnet add package tamash-playwright-mstest

Inherit from TamashPageTest<TSelf>, passing your own class as the generic parameter:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tamash.Playwright.MSTest;

using static Tamash.Playwright.Bindings;
using static Microsoft.Playwright.Assertions;

[TestClass]
public class LoginTest : TamashPageTest<LoginTest>
{
    [TestMethod]
    public async Task LogsIn()
    {
        await Page.GotoAsync("/");
        await Page.GetByPlaceholder("Username").FillAsync("Admin"); // healed automatically if this breaks
        await Page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
        await Expect(Unwrap(Page.GetByRole(AriaRole.Heading, new() { Name = "Dashboard" }))).ToBeVisibleAsync();
    }
}

The generic parameter isn't decoration — MSTest requires [ClassInitialize]/[ClassCleanup] to be static methods, unlike NUnit's instance-method equivalents. A plain static field on a shared base class would be stomped on by every derived test class inheriting it. Making the base class generic on the derived type itself (TSelf : TamashPageTest<TSelf>) sidesteps that: static fields are per closed generic type at the CLR level, so TamashPageTest<LoginTest>'s browser is entirely separate storage from TamashPageTest<CheckoutTest>'s — each derived class gets its own isolated instance automatically.

xUnit (v3)

dotnet add package tamash-playwright-xunit

xUnit has no inheritance-based lifecycle at all — composition over inheritance is the whole design philosophy. Inherit from TamashPageTest, forwarding the injected fixture through your constructor:

using Xunit;
using Tamash.Playwright.Xunit;

using static Tamash.Playwright.Bindings;
using static Microsoft.Playwright.Assertions;

public class LoginTest(TamashPlaywrightFixture fixture) : TamashPageTest(fixture)
{
    [Fact]
    public async Task LogsIn()
    {
        await Page.GotoAsync("/");
        await Page.GetByPlaceholder("Username").FillAsync("Admin"); // healed automatically if this breaks
        await Page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
        await Expect(Unwrap(Page.GetByRole(AriaRole.Heading, new() { Name = "Dashboard" }))).ToBeVisibleAsync();
    }
}

TamashPlaywrightFixture (launches Playwright/Browser once per test class) is injected via IClassFixture<T>; the base class itself implements IAsyncLifetime to create a fresh BrowserContext/Page before each test — no [SetUp]-style attributes anywhere, this is xUnit's own idiom.

Run xUnit v3 tests with dotnet run, not dotnet test. xUnit v3 test projects build as self-hosted executables (<OutputType>Exe</OutputType>) under the newer Microsoft Testing Platform model — dotnet test's traditional VSTest flow doesn't pick them up without an extra bridge adapter (xunit.runner.visualstudio), which this setup doesn't include. dotnet run from the test project directory is the native way to execute them.

Important (all three runners): Expect(...) needs Unwrap(...) around any locator/page you pass to it — see "What gets healed (and what doesn't)" below for why this is required, not optional, in .NET specifically.

Step 4: Check your setup

Run the built-in doctor command to confirm everything's wired up correctly:

dotnet tool install -g tamash-playwright-cli
tamash-playwright doctor

It checks three things:

  1. AI connectivity — confirms HEALER_ENABLED/HEALER_PROVIDER are set correctly and actually calls your configured provider to make sure the API key and model work.
  2. Missing .Describe() labels — scans your test files (tests by default, or pass --dir <path>) for locators that don't have a .Describe("...") label, flagging the ones most worth fixing (raw CSS/XPath selectors first).
  3. Locators written directly in test files — flags any locator defined inline in a test rather than inside a Page Object class, a Playwright best practice regardless of self-healing.

If it finds issues, the fastest fix is to open the project in an AI coding assistant (Claude Code, Cursor, GitHub Copilot, etc.) and ask it to address what it flagged. You can also add a standing rule to that assistant's instructions/skill file (e.g. CLAUDE.md, .cursor/rules, .github/copilot-instructions.md) so it follows both practices automatically on any new test code going forward.

A quick tip for better results

If you're using plain CSS selectors (like page.Locator("input[name='username']")) rather than Playwright's more descriptive locators (GetByRole, GetByPlaceholder, etc.), it helps to add a short, human-readable label so the healer knows what it's actually looking for. Chain .Describe("...") right onto the locator:

var username = Page.Locator("input[name='username']").Describe("Username Textbox");
await username.FillAsync("testadmin");

This step is optional, but recommended — without it, the healer has to guess purely from a broken CSS selector, which gives it a lot less to work with.

One .NET-specific limitation worth knowing: for GetByRole(...), the auto-derived description (used when you don't call .Describe()) only captures the role itself (e.g. role:Button), not the accessible name you passed via PageGetByRoleOptions.Name — there's no cheap generic way to read that value back out. If you rely on GetByRole with a name and want the full description quality, add .Describe("...") explicitly.

What gets healed (and what doesn't)

Only real Playwright actions that can be safely retried are healed: Click, Fill, Check, Hover, Press, SelectOption, SetInputFiles, Focus, Blur, DblClick, Tap, Clear, Uncheck (all Async-suffixed). DragTo and anything unlisted is intentionally left alone rather than guessed at.

Expect(...) assertions are not healed — they use Playwright's own built-in auto-retrying assertions, a separate mechanism this package doesn't touch. If a locator only ever appears inside an assertion and never in an action, .Describe() on it is a readability nicety, not something that affects healing.

Expect(...) requires Unwrap(...), and this is .NET-specific. Assertions.Expect(ILocator) casts its argument internally to Playwright's concrete Locator class, not just the ILocator interface. This package's self-healing Page/Locator objects are System.Reflection.DispatchProxy instances — they satisfy ILocator/IPage interface checks fine (which is all normal Playwright calls need), but a DispatchProxy can never satisfy a cast to an unrelated concrete class, so passing one straight into Expect(...) throws InvalidCastException. Wrap it with Unwrap(...) first:

using static Tamash.Playwright.Bindings;

await Expect(Unwrap(Page.Locator("h6"))).ToHaveTextAsync("Dashboard");

This isn't a bug to work around case-by-case — it's a structural difference from the TS/Python versions of this package (where expect() works directly on the wrapped object), and the same structural limitation the Java port has for the same underlying reason. Always unwrap before asserting in .NET.

License

Free to use, including commercially. The source code may not be copied, modified, redistributed, or resold without prior written permission. See the LICENSE file included in this package for the full terms.

Support

For questions or concerns, contact us at support@vibetestq.com.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.1.0 124 8/12/2026