[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M79Y91FVPJ2PNBXMSFSTPSAH":3,"skill-same:01M79Y91FVPJ2PNBXMSFSTPSAH":47},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":6,"ownerHtmlUrl":29,"ownerBlogUrl":30,"ownerDescription":31,"updatedAt":32,"install":33,"resources":46},"01M79Y91FVPJ2PNBXMSFSTPSAH","playwright-blazor-testing",null,"Write UI tests for Blazor applications (Server or WebAssembly) using Playwright. Covers navigation, interaction, authentication, selectors, and common Blazor-specific patterns.","Blind-Striker\u002Fagent-skills-and-plugins","Blind-Striker","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1965259?v=4","01M79WFXTE5PH0C1VB7B54ANMN","Deniz İrgin","01M79WFXV5KZ5GST1K8G035DM5",1,"MIT","https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins\u002Fblob\u002Fmaster\u002Fplugins\u002Fdeniz-dotnet-general\u002Fskills\u002Fplaywright-blazor-testing\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: playwright-blazor-testing\ndescription: Write UI tests for Blazor applications (Server or WebAssembly)\n  using Playwright. Covers navigation, interaction, authentication, selectors,\n  and common Blazor-specific patterns.\nuser-invocable: false\n---\n\n# Testing Blazor Applications with Playwright\n\n## When to Use This Skill\n\nUse this skill when:\n- Writing end-to-end UI tests for Blazor Server or WebAssembly applications\n- Testing interactive components, forms, and user workflows\n- Verifying authentication and authorization flows\n- Testing SignalR-based real-time updates in Blazor Server\n- Capturing screenshots for visual regression testing\n- Testing responsive designs and mobile emulation\n- Debugging UI issues with browser developer tools\n\n## Core Principles\n\n1. **Wait for Rendering** - Blazor renders asynchronously; use proper wait strategies\n2. **Test Attributes** - Use `data-test` or `data-testid` attributes for stable selectors\n3. **Headless by Default** - Run tests headless in CI, headed for local debugging\n4. **Handle Error UI** - Always check for `#blazor-error-ui` to catch unhandled exceptions\n5. **Avoid Network Wait States** - Blazor navigation doesn't trigger network loads; wait for DOM changes\n6. **Pin Browser Channels** - Use specific browser channels (msedge, chrome) for reproducibility\n\n## Required NuGet Packages\n\nIn a TUnit project, add none of the xUnit packages below and no `Microsoft.NET.Test.Sdk`: xUnit is a second test framework, and the VSTest SDK breaks TUnit's test discovery. Use TUnit's equivalents instead, such as `[Test]` and its lifecycle hooks in place of `IAsyncLifetime`.\n\n```xml\n\u003CItemGroup>\n  \u003CPackageReference Include=\"Microsoft.Playwright\" Version=\"*\" \u002F>\n  \u003CPackageReference Include=\"Microsoft.Playwright.MSTest\" Version=\"*\" \u002F>\n  \u003C!-- OR for xUnit -->\n  \u003CPackageReference Include=\"xunit\" Version=\"*\" \u002F>\n  \u003CPackageReference Include=\"xunit.runner.visualstudio\" Version=\"*\" \u002F>\n\u003C\u002FItemGroup>\n```\n\n## Installation\n\nBefore running tests, install Playwright browsers:\n\n```bash\npwsh -Command \"playwright install --with-deps\"\n```\n\n## Pattern 1: Basic Playwright Setup\n\n```csharp\nusing Microsoft.Playwright;\n\npublic class PlaywrightFixture : IAsyncLifetime\n{\n    private IPlaywright? _playwright;\n    private IBrowser? _browser;\n\n    public IBrowser Browser => _browser\n        ?? throw new InvalidOperationException(\"Browser not initialized\");\n\n    public async Task InitializeAsync()\n    {\n        _playwright = await Playwright.CreateAsync();\n\n        _browser = await _playwright.Chromium.LaunchAsync(new()\n        {\n            Headless = true,\n            \u002F\u002F For CI\u002Fdebugging, you might want:\n            \u002F\u002F Headless = Environment.GetEnvironmentVariable(\"CI\") != null,\n            \u002F\u002F SlowMo = 100 \u002F\u002F Slow down actions for debugging\n        });\n    }\n\n    public async Task DisposeAsync()\n    {\n        if (_browser is not null)\n            await _browser.DisposeAsync();\n\n        _playwright?.Dispose();\n    }\n}\n```\n\n## Pattern 2: Navigation in Blazor Apps\n\n### Initial Page Load (Classic Navigation)\n\n```csharp\n[Fact]\npublic async Task InitialPageLoad()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n\n    \u002F\u002F First load is classic HTTP navigation\n    await page.GotoAsync(\"https:\u002F\u002Flocalhost:5001\");\n\n    \u002F\u002F Wait for Blazor to initialize\n    await page.WaitForSelectorAsync(\"h1:has-text('Welcome')\");\n\n    Assert.True(await page.IsVisibleAsync(\"h1:has-text('Welcome')\"));\n}\n```\n\n### In-App Navigation (No Page Reload)\n\nBlazor uses client-side routing, so subsequent navigations don't trigger page reloads:\n\n```csharp\n[Fact]\npublic async Task InternalNavigation()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(\"https:\u002F\u002Flocalhost:5001\");\n\n    \u002F\u002F Method 1: Click a navigation link\n    await page.GetByRole(AriaRole.Link, new() { Name = \"Counter\" })\n        .ClickAsync();\n\n    \u002F\u002F Wait for the new page content (NOT network idle!)\n    await page.WaitForSelectorAsync(\"h1:has-text('Counter')\");\n\n    \u002F\u002F Method 2: Programmatic navigation (Blazor 8+)\n    await page.EvaluateAsync(\"window.Blazor.navigateTo('\u002Ffetchdata')\");\n    await page.WaitForSelectorAsync(\"h1:has-text('Weather')\");\n\n    \u002F\u002F Method 3: Direct URL navigation (causes full reload)\n    await page.GotoAsync(\"https:\u002F\u002Flocalhost:5001\u002Fcounter\");\n    await page.WaitForSelectorAsync(\"h1:has-text('Counter')\");\n}\n```\n\n### Wait Strategies for Blazor\n\n```csharp\n\u002F\u002F ❌ DON'T: Wait for network idle (Blazor doesn't reload pages)\nawait page.WaitForLoadStateAsync(LoadState.NetworkIdle);\n\n\u002F\u002F ✅ DO: Wait for specific DOM elements\nawait page.WaitForSelectorAsync(\"h1:has-text('My Page')\");\n\n\u002F\u002F ✅ DO: Wait for element visibility\nawait page.Locator(\"[data-test='content']\").WaitForAsync();\n\n\u002F\u002F ✅ DO: Wait for URL change\nawait page.WaitForURLAsync(\"**\u002Fcounter\");\n```\n\n## Pattern 3: Stable Selectors with Test Attributes\n\n### In Your Blazor Components\n\n```razor\n\u003C!-- Add data-test attributes for stable selectors -->\n\u003Cbutton data-test=\"submit-button\" @onclick=\"HandleSubmit\">\n    Submit\n\u003C\u002Fbutton>\n\n\u003Cinput data-test=\"username-input\" @bind=\"Username\" \u002F>\n\n\u003Cdiv data-test=\"result-container\">\n    @Result\n\u003C\u002Fdiv>\n```\n\n### In Your Tests\n\n```csharp\n[Fact]\npublic async Task FormSubmission()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(baseUrl);\n\n    \u002F\u002F Use GetByTestId for elements with data-test attributes\n    await page.GetByTestId(\"username-input\").FillAsync(\"testuser\");\n    await page.GetByTestId(\"password-input\").FillAsync(\"password123\");\n    await page.GetByTestId(\"submit-button\").ClickAsync();\n\n    \u002F\u002F Verify result\n    var result = await page.GetByTestId(\"result-container\").TextContentAsync();\n    Assert.Contains(\"Success\", result);\n}\n```\n\n## Pattern 4: Handling Authentication\n\n### Interactive Login\n\n```csharp\n[Fact]\npublic async Task LoginFlow()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync($\"{baseUrl}\u002Flogin\");\n\n    \u002F\u002F Fill login form\n    await page.FillAsync(\"input[name='username']\", \"alice\");\n    await page.FillAsync(\"input[name='password']\", \"P@ssw0rd\");\n    await page.ClickAsync(\"button[type='submit']\");\n\n    \u002F\u002F Wait for redirect to dashboard\n    await page.WaitForURLAsync(\"**\u002Fdashboard\");\n\n    \u002F\u002F Verify logged in\n    var username = await page.TextContentAsync(\"[data-test='user-name']\");\n    Assert.Equal(\"alice\", username);\n}\n```\n\n### Cookie Injection (Faster)\n\n```csharp\n[Fact]\npublic async Task AuthenticatedAccess_ViaCookie()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n\n    \u002F\u002F Inject authentication cookie\n    await page.Context.AddCookiesAsync(new[]\n    {\n        new Cookie\n        {\n            Name = \".AspNetCore.Cookies\",\n            Value = GenerateAuthCookie(\"alice\"),\n            Url = baseUrl,\n            Secure = true,\n            HttpOnly = true\n        }\n    });\n\n    \u002F\u002F Navigate directly to protected page\n    await page.GotoAsync($\"{baseUrl}\u002Fdashboard\");\n\n    \u002F\u002F Already authenticated!\n    var username = await page.TextContentAsync(\"[data-test='user-name']\");\n    Assert.Equal(\"alice\", username);\n}\n\nprivate string GenerateAuthCookie(string username)\n{\n    \u002F\u002F Generate a valid authentication cookie\n    \u002F\u002F This requires access to your app's cookie encryption keys\n    \u002F\u002F OR use a test endpoint that generates valid cookies\n    \u002F\u002F OR perform actual login once and reuse the cookie\n}\n```\n\n### OAuth\u002FExternal Provider Mocking\n\n```csharp\n\u002F\u002F Use route interception to mock OAuth redirects\nawait page.RouteAsync(\"**\u002Fsignin-microsoft\", async route =>\n{\n    \u002F\u002F Intercept OAuth redirect and return mock response\n    await route.FulfillAsync(new()\n    {\n        Status = 302,\n        Headers = new Dictionary\u003Cstring, string>\n        {\n            [\"Location\"] = $\"{baseUrl}\u002Fsignin-callback?code=mock_auth_code\"\n        }\n    });\n});\n```\n\n## Pattern 5: Click Events and Touch Interactions\n\n```csharp\n[Fact]\npublic async Task ClickInteractions()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(baseUrl);\n\n    \u002F\u002F Standard click\n    await page.GetByText(\"Click Me\").ClickAsync();\n\n    \u002F\u002F Right-click\n    await page.ClickAsync(\"[data-test='context-menu']\", new()\n    {\n        Button = MouseButton.Right\n    });\n\n    \u002F\u002F Double-click\n    await page.DblClickAsync(\"[data-test='item']\");\n\n    \u002F\u002F Hover then click dropdown\n    var menu = page.Locator(\"#profile-menu\");\n    await menu.HoverAsync();\n    await menu.GetByText(\"Sign out\").ClickAsync();\n\n    \u002F\u002F Touch events (mobile emulation)\n    await page.EmulateMediaAsync(new() { Media = Media.Screen });\n    await page.Touchscreen.TapAsync(150, 300);\n}\n```\n\n## Pattern 6: Form Handling\n\n```csharp\n[Fact]\npublic async Task ComplexForm()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync($\"{baseUrl}\u002Fform\");\n\n    \u002F\u002F Text input\n    await page.FillAsync(\"[data-test='name']\", \"John Doe\");\n\n    \u002F\u002F Select dropdown\n    await page.SelectOptionAsync(\"[data-test='country']\", \"US\");\n\n    \u002F\u002F Checkbox\n    await page.CheckAsync(\"[data-test='terms']\");\n\n    \u002F\u002F Radio button\n    await page.CheckAsync(\"[data-test='option-a']\");\n\n    \u002F\u002F File upload\n    await page.SetInputFilesAsync(\"[data-test='file-input']\",\n        \"\u002Fpath\u002Fto\u002Ftest-file.pdf\");\n\n    \u002F\u002F Submit\n    await page.ClickAsync(\"[data-test='submit']\");\n\n    \u002F\u002F Wait for success message\n    await page.WaitForSelectorAsync(\"[data-test='success-message']\");\n}\n```\n\n## Pattern 7: Handling Blazor Error UI\n\nBlazor shows an error overlay when unhandled exceptions occur. Always check for this:\n\n```csharp\npublic static async Task AssertNoBlazorErrors(this IPage page)\n{\n    var errorUi = page.Locator(\"#blazor-error-ui\");\n\n    if (await errorUi.IsVisibleAsync())\n    {\n        var errorText = await errorUi.InnerTextAsync();\n        Assert.Fail($\"Blazor error occurred: {errorText}\");\n    }\n}\n\n[Fact]\npublic async Task Page_ShouldNotHaveErrors()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(baseUrl);\n\n    \u002F\u002F Perform some actions\n    await page.ClickAsync(\"[data-test='action-button']\");\n\n    \u002F\u002F Verify no errors occurred\n    await page.AssertNoBlazorErrors();\n}\n```\n\n## Pattern 8: Testing Real-Time Updates (SignalR)\n\nBlazor Server uses SignalR for real-time communication:\n\n```csharp\n[Fact]\npublic async Task RealTimeUpdates()\n{\n    \u002F\u002F Open two browser contexts (simulating two users)\n    var page1 = await _fixture.Browser.NewPageAsync();\n    var page2 = await _fixture.Browser.NewPageAsync();\n\n    await page1.GotoAsync($\"{baseUrl}\u002Fdrawing\");\n    await page2.GotoAsync($\"{baseUrl}\u002Fdrawing\");\n\n    \u002F\u002F User 1 draws something\n    await page1.ClickAsync(\"[data-test='draw-button']\");\n    await page1.Mouse.ClickAsync(100, 100);\n\n    \u002F\u002F User 2 should see the update\n    await page2.WaitForSelectorAsync(\"[data-test='drawing-canvas']\");\n\n    \u002F\u002F Verify both pages show the same content\n    var canvas1 = await page1.GetByTestId(\"drawing-canvas\")\n        .GetAttributeAsync(\"data-strokes\");\n    var canvas2 = await page2.GetByTestId(\"drawing-canvas\")\n        .GetAttributeAsync(\"data-strokes\");\n\n    Assert.Equal(canvas1, canvas2);\n}\n```\n\n## Pattern 9: Screenshot and Visual Testing\n\n```csharp\n[Fact]\npublic async Task CaptureScreenshots()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(baseUrl);\n\n    \u002F\u002F Full page screenshot\n    await page.ScreenshotAsync(new()\n    {\n        Path = \"screenshots\u002Fhomepage.png\",\n        FullPage = true\n    });\n\n    \u002F\u002F Element screenshot\n    var header = page.Locator(\"header\");\n    await header.ScreenshotAsync(new()\n    {\n        Path = \"screenshots\u002Fheader.png\"\n    });\n\n    \u002F\u002F Screenshot with viewport size\n    await page.SetViewportSizeAsync(1920, 1080);\n    await page.ScreenshotAsync(new()\n    {\n        Path = \"screenshots\u002Fdesktop.png\"\n    });\n\n    \u002F\u002F Mobile viewport\n    await page.SetViewportSizeAsync(375, 667);\n    await page.ScreenshotAsync(new()\n    {\n        Path = \"screenshots\u002Fmobile.png\"\n    });\n}\n```\n\n## Pattern 10: Running Against HTTPS with Dev Certs\n\n```csharp\npublic async Task InitializeAsync()\n{\n    _playwright = await Playwright.CreateAsync();\n\n    _browser = await _playwright.Chromium.LaunchAsync(new()\n    {\n        Headless = true,\n        \u002F\u002F Ignore certificate errors for local dev certs\n        Args = new[] { \"--ignore-certificate-errors\" }\n    });\n}\n```\n\nFor stricter setups, export and trust the dev certificate:\n\n```bash\ndotnet dev-certs https --export-path cert.pfx -p YourPassword\n```\n\n## Common Selectors for Blazor Components\n\n```csharp\n\u002F\u002F By role (best for accessibility)\nawait page.GetByRole(AriaRole.Button, new() { Name = \"Submit\" });\nawait page.GetByRole(AriaRole.Link, new() { Name = \"Home\" });\nawait page.GetByRole(AriaRole.Heading, new() { Name = \"Welcome\" });\n\n\u002F\u002F By test ID\nawait page.GetByTestId(\"user-profile\");\n\n\u002F\u002F By text content\nawait page.GetByText(\"Hello, World!\");\n\n\u002F\u002F By label (for inputs)\nawait page.GetByLabel(\"Email Address\");\n\n\u002F\u002F By placeholder\nawait page.GetByPlaceholder(\"Enter your name\");\n\n\u002F\u002F CSS selectors (use sparingly)\nawait page.Locator(\".mud-button-primary\");\nawait page.Locator(\"#login-form\");\n\n\u002F\u002F XPath (use as last resort)\nawait page.Locator(\"xpath=\u002F\u002Fbutton[contains(text(), 'Submit')]\");\n```\n\n## Parallelization Considerations\n\nBlazor Server uses SignalR websockets. Multiple Playwright tests can saturate connections:\n\n```csharp\n\u002F\u002F Limit parallel execution for Blazor Server tests\n[Collection(\"Blazor Server\")]\npublic class BlazorServerTests { }\n\n\u002F\u002F In AssemblyInfo.cs or test startup\n[assembly: CollectionBehavior(MaxParallelThreads = 2)]\n```\n\nBlazor WebAssembly doesn't have this limitation and can run fully parallel.\n\n## CI\u002FCD Integration\n\n### GitHub Actions\n\n```yaml\nname: Playwright Tests\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions\u002Fcheckout@v3\n\n    - name: Setup .NET\n      uses: actions\u002Fsetup-dotnet@v3\n      with:\n        dotnet-version: 9.0.x\n\n    - name: Install Playwright Browsers\n      run: pwsh -Command \"playwright install --with-deps\"\n\n    - name: Build\n      run: dotnet build -c Release\n\n    - name: Run Playwright Tests\n      run: |\n        dotnet test tests\u002FYourApp.UITests \\\n          --no-build \\\n          -c Release \\\n          --logger trx\n\n    - name: Upload Screenshots\n      uses: actions\u002Fupload-artifact@v3\n      if: failure()\n      with:\n        name: playwright-screenshots\n        path: \"**\u002Fscreenshots\u002F\"\n\n    - name: Upload Test Results\n      uses: actions\u002Fupload-artifact@v3\n      if: always()\n      with:\n        name: test-results\n        path: \"**\u002FTestResults\u002F*.trx\"\n```\n\n## Debugging Tips\n\n1. **Run Headed** - Set `Headless = false` to watch tests execute\n2. **Slow Motion** - Add `SlowMo = 500` to slow down actions\n3. **Pause Execution** - Call `await page.PauseAsync()` to open Playwright Inspector\n4. **Console Logs** - Capture browser console: `page.Console += (_, msg) => Console.WriteLine(msg.Text);`\n5. **Network Traffic** - Monitor requests: `page.Request += (_, req) => Console.WriteLine(req.Url);`\n6. **Screenshots on Failure** - Always capture screenshots in catch blocks\n\n## Best Practices\n\n1. **Use data-test attributes** - More stable than CSS classes or IDs\n2. **Prefer semantic selectors** - Use roles, labels, and text content\n3. **Wait for specific elements** - Don't use blanket delays\n4. **Check for Blazor errors** - Always verify `#blazor-error-ui` is not visible\n5. **Test with multiple viewports** - Verify responsive design\n6. **Reuse browser contexts** - Faster than creating new browsers\n7. **Clean up resources** - Always dispose pages and browsers\n8. **Use collections for Blazor Server** - Avoid SignalR connection saturation\n9. **Capture screenshots on failure** - Essential for debugging CI failures\n10. **Pin browser channels** - Use specific channels for reproducibility\n\n## Advanced: Custom Wait Helpers\n\n```csharp\npublic static class PlaywrightExtensions\n{\n    public static async Task WaitForBlazorAsync(this IPage page)\n    {\n        \u002F\u002F Wait for Blazor to finish rendering\n        await page.EvaluateAsync(@\"\n            () => new Promise(resolve => {\n                if (typeof Blazor !== 'undefined') {\n                    resolve();\n                } else {\n                    const interval = setInterval(() => {\n                        if (typeof Blazor !== 'undefined') {\n                            clearInterval(interval);\n                            resolve();\n                        }\n                    }, 100);\n                }\n            })\n        \");\n    }\n\n    public static async Task WaitForNoSpinnersAsync(\n        this IPage page,\n        int timeout = 5000)\n    {\n        var locator = page.Locator(\".spinner, .loading\");\n        await locator.WaitForAsync(new()\n        {\n            State = WaitForSelectorState.Hidden,\n            Timeout = timeout\n        });\n    }\n\n    public static async Task FillWithValidationAsync(\n        this IPage page,\n        string selector,\n        string value)\n    {\n        await page.FillAsync(selector, value);\n\n        \u002F\u002F Trigger blur to activate validation\n        await page.Locator(selector).BlurAsync();\n\n        \u002F\u002F Wait a bit for validation to complete\n        await Task.Delay(100);\n    }\n}\n```\n","# Testing Blazor Applications with Playwright\n\n## When to Use This Skill\n\nUse this skill when:\n- Writing end-to-end UI tests for Blazor Server or WebAssembly applications\n- Testing interactive components, forms, and user workflows\n- Verifying authentication and authorization flows\n- Testing SignalR-based real-time updates in Blazor Server\n- Capturing screenshots for visual regression testing\n- Testing responsive designs and mobile emulation\n- Debugging UI issues with browser developer tools\n\n## Core Principles\n\n1. **Wait for Rendering** - Blazor renders asynchronously; use proper wait strategies\n2. **Test Attributes** - Use `data-test` or `data-testid` attributes for stable selectors\n3. **Headless by Default** - Run tests headless in CI, headed for local debugging\n4. **Handle Error UI** - Always check for `#blazor-error-ui` to catch unhandled exceptions\n5. **Avoid Network Wait States** - Blazor navigation doesn't trigger network loads; wait for DOM changes\n6. **Pin Browser Channels** - Use specific browser channels (msedge, chrome) for reproducibility\n\n## Required NuGet Packages\n\nIn a TUnit project, add none of the xUnit packages below and no `Microsoft.NET.Test.Sdk`: xUnit is a second test framework, and the VSTest SDK breaks TUnit's test discovery. Use TUnit's equivalents instead, such as `[Test]` and its lifecycle hooks in place of `IAsyncLifetime`.\n\n```xml\n\u003CItemGroup>\n  \u003CPackageReference Include=\"Microsoft.Playwright\" Version=\"*\" \u002F>\n  \u003CPackageReference Include=\"Microsoft.Playwright.MSTest\" Version=\"*\" \u002F>\n  \u003C!-- OR for xUnit -->\n  \u003CPackageReference Include=\"xunit\" Version=\"*\" \u002F>\n  \u003CPackageReference Include=\"xunit.runner.visualstudio\" Version=\"*\" \u002F>\n\u003C\u002FItemGroup>\n```\n\n## Installation\n\nBefore running tests, install Playwright browsers:\n\n```bash\npwsh -Command \"playwright install --with-deps\"\n```\n\n## Pattern 1: Basic Playwright Setup\n\n```csharp\nusing Microsoft.Playwright;\n\npublic class PlaywrightFixture : IAsyncLifetime\n{\n    private IPlaywright? _playwright;\n    private IBrowser? _browser;\n\n    public IBrowser Browser => _browser\n        ?? throw new InvalidOperationException(\"Browser not initialized\");\n\n    public async Task InitializeAsync()\n    {\n        _playwright = await Playwright.CreateAsync();\n\n        _browser = await _playwright.Chromium.LaunchAsync(new()\n        {\n            Headless = true,\n            \u002F\u002F For CI\u002Fdebugging, you might want:\n            \u002F\u002F Headless = Environment.GetEnvironmentVariable(\"CI\") != null,\n            \u002F\u002F SlowMo = 100 \u002F\u002F Slow down actions for debugging\n        });\n    }\n\n    public async Task DisposeAsync()\n    {\n        if (_browser is not null)\n            await _browser.DisposeAsync();\n\n        _playwright?.Dispose();\n    }\n}\n```\n\n## Pattern 2: Navigation in Blazor Apps\n\n### Initial Page Load (Classic Navigation)\n\n```csharp\n[Fact]\npublic async Task InitialPageLoad()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n\n    \u002F\u002F First load is classic HTTP navigation\n    await page.GotoAsync(\"https:\u002F\u002Flocalhost:5001\");\n\n    \u002F\u002F Wait for Blazor to initialize\n    await page.WaitForSelectorAsync(\"h1:has-text('Welcome')\");\n\n    Assert.True(await page.IsVisibleAsync(\"h1:has-text('Welcome')\"));\n}\n```\n\n### In-App Navigation (No Page Reload)\n\nBlazor uses client-side routing, so subsequent navigations don't trigger page reloads:\n\n```csharp\n[Fact]\npublic async Task InternalNavigation()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(\"https:\u002F\u002Flocalhost:5001\");\n\n    \u002F\u002F Method 1: Click a navigation link\n    await page.GetByRole(AriaRole.Link, new() { Name = \"Counter\" })\n        .ClickAsync();\n\n    \u002F\u002F Wait for the new page content (NOT network idle!)\n    await page.WaitForSelectorAsync(\"h1:has-text('Counter')\");\n\n    \u002F\u002F Method 2: Programmatic navigation (Blazor 8+)\n    await page.EvaluateAsync(\"window.Blazor.navigateTo('\u002Ffetchdata')\");\n    await page.WaitForSelectorAsync(\"h1:has-text('Weather')\");\n\n    \u002F\u002F Method 3: Direct URL navigation (causes full reload)\n    await page.GotoAsync(\"https:\u002F\u002Flocalhost:5001\u002Fcounter\");\n    await page.WaitForSelectorAsync(\"h1:has-text('Counter')\");\n}\n```\n\n### Wait Strategies for Blazor\n\n```csharp\n\u002F\u002F ❌ DON'T: Wait for network idle (Blazor doesn't reload pages)\nawait page.WaitForLoadStateAsync(LoadState.NetworkIdle);\n\n\u002F\u002F ✅ DO: Wait for specific DOM elements\nawait page.WaitForSelectorAsync(\"h1:has-text('My Page')\");\n\n\u002F\u002F ✅ DO: Wait for element visibility\nawait page.Locator(\"[data-test='content']\").WaitForAsync();\n\n\u002F\u002F ✅ DO: Wait for URL change\nawait page.WaitForURLAsync(\"**\u002Fcounter\");\n```\n\n## Pattern 3: Stable Selectors with Test Attributes\n\n### In Your Blazor Components\n\n```razor\n\u003C!-- Add data-test attributes for stable selectors -->\n\u003Cbutton data-test=\"submit-button\" @onclick=\"HandleSubmit\">\n    Submit\n\u003C\u002Fbutton>\n\n\u003Cinput data-test=\"username-input\" @bind=\"Username\" \u002F>\n\n\u003Cdiv data-test=\"result-container\">\n    @Result\n\u003C\u002Fdiv>\n```\n\n### In Your Tests\n\n```csharp\n[Fact]\npublic async Task FormSubmission()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(baseUrl);\n\n    \u002F\u002F Use GetByTestId for elements with data-test attributes\n    await page.GetByTestId(\"username-input\").FillAsync(\"testuser\");\n    await page.GetByTestId(\"password-input\").FillAsync(\"password123\");\n    await page.GetByTestId(\"submit-button\").ClickAsync();\n\n    \u002F\u002F Verify result\n    var result = await page.GetByTestId(\"result-container\").TextContentAsync();\n    Assert.Contains(\"Success\", result);\n}\n```\n\n## Pattern 4: Handling Authentication\n\n### Interactive Login\n\n```csharp\n[Fact]\npublic async Task LoginFlow()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync($\"{baseUrl}\u002Flogin\");\n\n    \u002F\u002F Fill login form\n    await page.FillAsync(\"input[name='username']\", \"alice\");\n    await page.FillAsync(\"input[name='password']\", \"P@ssw0rd\");\n    await page.ClickAsync(\"button[type='submit']\");\n\n    \u002F\u002F Wait for redirect to dashboard\n    await page.WaitForURLAsync(\"**\u002Fdashboard\");\n\n    \u002F\u002F Verify logged in\n    var username = await page.TextContentAsync(\"[data-test='user-name']\");\n    Assert.Equal(\"alice\", username);\n}\n```\n\n### Cookie Injection (Faster)\n\n```csharp\n[Fact]\npublic async Task AuthenticatedAccess_ViaCookie()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n\n    \u002F\u002F Inject authentication cookie\n    await page.Context.AddCookiesAsync(new[]\n    {\n        new Cookie\n        {\n            Name = \".AspNetCore.Cookies\",\n            Value = GenerateAuthCookie(\"alice\"),\n            Url = baseUrl,\n            Secure = true,\n            HttpOnly = true\n        }\n    });\n\n    \u002F\u002F Navigate directly to protected page\n    await page.GotoAsync($\"{baseUrl}\u002Fdashboard\");\n\n    \u002F\u002F Already authenticated!\n    var username = await page.TextContentAsync(\"[data-test='user-name']\");\n    Assert.Equal(\"alice\", username);\n}\n\nprivate string GenerateAuthCookie(string username)\n{\n    \u002F\u002F Generate a valid authentication cookie\n    \u002F\u002F This requires access to your app's cookie encryption keys\n    \u002F\u002F OR use a test endpoint that generates valid cookies\n    \u002F\u002F OR perform actual login once and reuse the cookie\n}\n```\n\n### OAuth\u002FExternal Provider Mocking\n\n```csharp\n\u002F\u002F Use route interception to mock OAuth redirects\nawait page.RouteAsync(\"**\u002Fsignin-microsoft\", async route =>\n{\n    \u002F\u002F Intercept OAuth redirect and return mock response\n    await route.FulfillAsync(new()\n    {\n        Status = 302,\n        Headers = new Dictionary\u003Cstring, string>\n        {\n            [\"Location\"] = $\"{baseUrl}\u002Fsignin-callback?code=mock_auth_code\"\n        }\n    });\n});\n```\n\n## Pattern 5: Click Events and Touch Interactions\n\n```csharp\n[Fact]\npublic async Task ClickInteractions()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(baseUrl);\n\n    \u002F\u002F Standard click\n    await page.GetByText(\"Click Me\").ClickAsync();\n\n    \u002F\u002F Right-click\n    await page.ClickAsync(\"[data-test='context-menu']\", new()\n    {\n        Button = MouseButton.Right\n    });\n\n    \u002F\u002F Double-click\n    await page.DblClickAsync(\"[data-test='item']\");\n\n    \u002F\u002F Hover then click dropdown\n    var menu = page.Locator(\"#profile-menu\");\n    await menu.HoverAsync();\n    await menu.GetByText(\"Sign out\").ClickAsync();\n\n    \u002F\u002F Touch events (mobile emulation)\n    await page.EmulateMediaAsync(new() { Media = Media.Screen });\n    await page.Touchscreen.TapAsync(150, 300);\n}\n```\n\n## Pattern 6: Form Handling\n\n```csharp\n[Fact]\npublic async Task ComplexForm()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync($\"{baseUrl}\u002Fform\");\n\n    \u002F\u002F Text input\n    await page.FillAsync(\"[data-test='name']\", \"John Doe\");\n\n    \u002F\u002F Select dropdown\n    await page.SelectOptionAsync(\"[data-test='country']\", \"US\");\n\n    \u002F\u002F Checkbox\n    await page.CheckAsync(\"[data-test='terms']\");\n\n    \u002F\u002F Radio button\n    await page.CheckAsync(\"[data-test='option-a']\");\n\n    \u002F\u002F File upload\n    await page.SetInputFilesAsync(\"[data-test='file-input']\",\n        \"\u002Fpath\u002Fto\u002Ftest-file.pdf\");\n\n    \u002F\u002F Submit\n    await page.ClickAsync(\"[data-test='submit']\");\n\n    \u002F\u002F Wait for success message\n    await page.WaitForSelectorAsync(\"[data-test='success-message']\");\n}\n```\n\n## Pattern 7: Handling Blazor Error UI\n\nBlazor shows an error overlay when unhandled exceptions occur. Always check for this:\n\n```csharp\npublic static async Task AssertNoBlazorErrors(this IPage page)\n{\n    var errorUi = page.Locator(\"#blazor-error-ui\");\n\n    if (await errorUi.IsVisibleAsync())\n    {\n        var errorText = await errorUi.InnerTextAsync();\n        Assert.Fail($\"Blazor error occurred: {errorText}\");\n    }\n}\n\n[Fact]\npublic async Task Page_ShouldNotHaveErrors()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(baseUrl);\n\n    \u002F\u002F Perform some actions\n    await page.ClickAsync(\"[data-test='action-button']\");\n\n    \u002F\u002F Verify no errors occurred\n    await page.AssertNoBlazorErrors();\n}\n```\n\n## Pattern 8: Testing Real-Time Updates (SignalR)\n\nBlazor Server uses SignalR for real-time communication:\n\n```csharp\n[Fact]\npublic async Task RealTimeUpdates()\n{\n    \u002F\u002F Open two browser contexts (simulating two users)\n    var page1 = await _fixture.Browser.NewPageAsync();\n    var page2 = await _fixture.Browser.NewPageAsync();\n\n    await page1.GotoAsync($\"{baseUrl}\u002Fdrawing\");\n    await page2.GotoAsync($\"{baseUrl}\u002Fdrawing\");\n\n    \u002F\u002F User 1 draws something\n    await page1.ClickAsync(\"[data-test='draw-button']\");\n    await page1.Mouse.ClickAsync(100, 100);\n\n    \u002F\u002F User 2 should see the update\n    await page2.WaitForSelectorAsync(\"[data-test='drawing-canvas']\");\n\n    \u002F\u002F Verify both pages show the same content\n    var canvas1 = await page1.GetByTestId(\"drawing-canvas\")\n        .GetAttributeAsync(\"data-strokes\");\n    var canvas2 = await page2.GetByTestId(\"drawing-canvas\")\n        .GetAttributeAsync(\"data-strokes\");\n\n    Assert.Equal(canvas1, canvas2);\n}\n```\n\n## Pattern 9: Screenshot and Visual Testing\n\n```csharp\n[Fact]\npublic async Task CaptureScreenshots()\n{\n    var page = await _fixture.Browser.NewPageAsync();\n    await page.GotoAsync(baseUrl);\n\n    \u002F\u002F Full page screenshot\n    await page.ScreenshotAsync(new()\n    {\n        Path = \"screenshots\u002Fhomepage.png\",\n        FullPage = true\n    });\n\n    \u002F\u002F Element screenshot\n    var header = page.Locator(\"header\");\n    await header.ScreenshotAsync(new()\n    {\n        Path = \"screenshots\u002Fheader.png\"\n    });\n\n    \u002F\u002F Screenshot with viewport size\n    await page.SetViewportSizeAsync(1920, 1080);\n    await page.ScreenshotAsync(new()\n    {\n        Path = \"screenshots\u002Fdesktop.png\"\n    });\n\n    \u002F\u002F Mobile viewport\n    await page.SetViewportSizeAsync(375, 667);\n    await page.ScreenshotAsync(new()\n    {\n        Path = \"screenshots\u002Fmobile.png\"\n    });\n}\n```\n\n## Pattern 10: Running Against HTTPS with Dev Certs\n\n```csharp\npublic async Task InitializeAsync()\n{\n    _playwright = await Playwright.CreateAsync();\n\n    _browser = await _playwright.Chromium.LaunchAsync(new()\n    {\n        Headless = true,\n        \u002F\u002F Ignore certificate errors for local dev certs\n        Args = new[] { \"--ignore-certificate-errors\" }\n    });\n}\n```\n\nFor stricter setups, export and trust the dev certificate:\n\n```bash\ndotnet dev-certs https --export-path cert.pfx -p YourPassword\n```\n\n## Common Selectors for Blazor Components\n\n```csharp\n\u002F\u002F By role (best for accessibility)\nawait page.GetByRole(AriaRole.Button, new() { Name = \"Submit\" });\nawait page.GetByRole(AriaRole.Link, new() { Name = \"Home\" });\nawait page.GetByRole(AriaRole.Heading, new() { Name = \"Welcome\" });\n\n\u002F\u002F By test ID\nawait page.GetByTestId(\"user-profile\");\n\n\u002F\u002F By text content\nawait page.GetByText(\"Hello, World!\");\n\n\u002F\u002F By label (for inputs)\nawait page.GetByLabel(\"Email Address\");\n\n\u002F\u002F By placeholder\nawait page.GetByPlaceholder(\"Enter your name\");\n\n\u002F\u002F CSS selectors (use sparingly)\nawait page.Locator(\".mud-button-primary\");\nawait page.Locator(\"#login-form\");\n\n\u002F\u002F XPath (use as last resort)\nawait page.Locator(\"xpath=\u002F\u002Fbutton[contains(text(), 'Submit')]\");\n```\n\n## Parallelization Considerations\n\nBlazor Server uses SignalR websockets. Multiple Playwright tests can saturate connections:\n\n```csharp\n\u002F\u002F Limit parallel execution for Blazor Server tests\n[Collection(\"Blazor Server\")]\npublic class BlazorServerTests { }\n\n\u002F\u002F In AssemblyInfo.cs or test startup\n[assembly: CollectionBehavior(MaxParallelThreads = 2)]\n```\n\nBlazor WebAssembly doesn't have this limitation and can run fully parallel.\n\n## CI\u002FCD Integration\n\n### GitHub Actions\n\n```yaml\nname: Playwright Tests\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions\u002Fcheckout@v3\n\n    - name: Setup .NET\n      uses: actions\u002Fsetup-dotnet@v3\n      with:\n        dotnet-version: 9.0.x\n\n    - name: Install Playwright Browsers\n      run: pwsh -Command \"playwright install --with-deps\"\n\n    - name: Build\n      run: dotnet build -c Release\n\n    - name: Run Playwright Tests\n      run: |\n        dotnet test tests\u002FYourApp.UITests \\\n          --no-build \\\n          -c Release \\\n          --logger trx\n\n    - name: Upload Screenshots\n      uses: actions\u002Fupload-artifact@v3\n      if: failure()\n      with:\n        name: playwright-screenshots\n        path: \"**\u002Fscreenshots\u002F\"\n\n    - name: Upload Test Results\n      uses: actions\u002Fupload-artifact@v3\n      if: always()\n      with:\n        name: test-results\n        path: \"**\u002FTestResults\u002F*.trx\"\n```\n\n## Debugging Tips\n\n1. **Run Headed** - Set `Headless = false` to watch tests execute\n2. **Slow Motion** - Add `SlowMo = 500` to slow down actions\n3. **Pause Execution** - Call `await page.PauseAsync()` to open Playwright Inspector\n4. **Console Logs** - Capture browser console: `page.Console += (_, msg) => Console.WriteLine(msg.Text);`\n5. **Network Traffic** - Monitor requests: `page.Request += (_, req) => Console.WriteLine(req.Url);`\n6. **Screenshots on Failure** - Always capture screenshots in catch blocks\n\n## Best Practices\n\n1. **Use data-test attributes** - More stable than CSS classes or IDs\n2. **Prefer semantic selectors** - Use roles, labels, and text content\n3. **Wait for specific elements** - Don't use blanket delays\n4. **Check for Blazor errors** - Always verify `#blazor-error-ui` is not visible\n5. **Test with multiple viewports** - Verify responsive design\n6. **Reuse browser contexts** - Faster than creating new browsers\n7. **Clean up resources** - Always dispose pages and browsers\n8. **Use collections for Blazor Server** - Avoid SignalR connection saturation\n9. **Capture screenshots on failure** - Essential for debugging CI failures\n10. **Pin browser channels** - Use specific channels for reproducibility\n\n## Advanced: Custom Wait Helpers\n\n```csharp\npublic static class PlaywrightExtensions\n{\n    public static async Task WaitForBlazorAsync(this IPage page)\n    {\n        \u002F\u002F Wait for Blazor to finish rendering\n        await page.EvaluateAsync(@\"\n            () => new Promise(resolve => {\n                if (typeof Blazor !== 'undefined') {\n                    resolve();\n                } else {\n                    const interval = setInterval(() => {\n                        if (typeof Blazor !== 'undefined') {\n                            clearInterval(interval);\n                            resolve();\n                        }\n                    }, 100);\n                }\n            })\n        \");\n    }\n\n    public static async Task WaitForNoSpinnersAsync(\n        this IPage page,\n        int timeout = 5000)\n    {\n        var locator = page.Locator(\".spinner, .loading\");\n        await locator.WaitForAsync(new()\n        {\n            State = WaitForSelectorState.Hidden,\n            Timeout = timeout\n        });\n    }\n\n    public static async Task FillWithValidationAsync(\n        this IPage page,\n        string selector,\n        string value)\n    {\n        await page.FillAsync(selector, value);\n\n        \u002F\u002F Trigger blur to activate validation\n        await page.Locator(selector).BlurAsync();\n\n        \u002F\u002F Wait a bit for validation to complete\n        await Task.Delay(100);\n    }\n}\n```\n",{"name":5,"description":7,"user-invocable":20},"master","b30b1c1908dbc56a8184128f18803053d762ccb5","plugins\u002Fdeniz-dotnet-general\u002Fskills\u002Fplaywright-blazor-testing",0,"https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins","https:\u002F\u002Fgithub.com\u002FBlind-Striker","denizirgin.com","VP of Engineering - \r\n@armutcom @homeruntech @prontopro\r\n |  Speaker \r\n@CodeFiction\r\n, http:\u002F\u002Fcodefiction.tech | Developer, gamer, blogger, maiden iron fan","2026-11-13 00:23:18",{"source":8,"skillName":5,"commands":34,"prompt":44,"zipAvailable":45},[35,38,41],{"tool":36,"command":37},"npx","npx skills add Blind-Striker\u002Fagent-skills-and-plugins --skill playwright-blazor-testing",{"tool":39,"command":40},"bunx","bunx skills add Blind-Striker\u002Fagent-skills-and-plugins --skill playwright-blazor-testing",{"tool":42,"command":43},"pnpm","pnpm dlx skills add Blind-Striker\u002Fagent-skills-and-plugins --skill playwright-blazor-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins\u002Fblob\u002Fmaster\u002Fplugins\u002Fdeniz-dotnet-general\u002Fskills\u002Fplaywright-blazor-testing\u002FSKILL.md",true,[],[48,55,62,69,76,83],{"skillNo":49,"name":50,"nameZh":6,"description":51,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":52,"categories":53,"keywords":54,"publishedAt":19},"01M79YGWBJ52PHMSTKJ3RJ3AWR","writing-tunit-tests","Write tests in TUnit — the attributes, the async assertion model, data-driven cases, lifecycle hooks, and the parallelism rules. USE FOR writing or fixing a test in a TUnit project, translating a test idea into TUnit syntax, or reviewing TUnit test code for API mistakes. DO NOT USE FOR deciding what to test, judging whether a test is any good, or running tests.","https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins\u002Fblob\u002Fmaster\u002Fskills\u002Fdeniz-dotnet-general\u002Fwriting-tunit-tests\u002FSKILL.md",[],[],{"skillNo":56,"name":57,"nameZh":6,"description":58,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":59,"categories":60,"keywords":61,"publishedAt":19},"01M79YGR3Y9RCRS6TD71D0KKP8","dotnet-walls","Walls for a .NET change in a repository without its own quality gates — pinned, ephemeral analyzer, format, clone and scanner commands for the verify step, their identity ratchet, and the build traps. Use during a walls preflight, or when a worker's verify step needs .NET quality gates.","https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins\u002Fblob\u002Fmaster\u002Fskills\u002Fdeniz-dotnet-general\u002Fdotnet-walls\u002FSKILL.md",[],[],{"skillNo":63,"name":64,"nameZh":6,"description":65,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":66,"categories":67,"keywords":68,"publishedAt":19},"01M79YGFNJBST7D803NQW3SBYM","csharp-15","Write, review and modernize C# 15 (.NET 11) code correctly. Covers every C# 15 feature - collection expression arguments (`[with(...), ...]`), union types (`union`, `[Union]`, `IUnion`, custom unions), closed class hierarchies (`closed`), extension indexers, labeled `break`\u002F`continue`, non-virtual static interface members on runtimes without default interface methods, and the preview memory-safety model (`unsafe(expr)`, `safe`, requires-unsafe members). Use when code targets net11.0 or LangVersion 15\u002Fpreview, when the user mentions any of these features, when replacing marker interfaces, OneOf-style results, `goto`\u002Fflag loop exits or capacity\u002Fcomparer boilerplate, or when the build reports CS8121, CS8509, CS8655, CS8510, CS0121\u002FCS0411 on a `with(...)` element, CS9370, CS9380 or CS9363.","https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins\u002Fblob\u002Fmaster\u002Fskills\u002Fdeniz-dotnet-general\u002Fcsharp-15\u002FSKILL.md",[],[],{"skillNo":70,"name":71,"nameZh":6,"description":72,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":75,"publishedAt":19},"01M79YGAV031473HSX6FN87ENH","verified-waves","Run one task as verified multi-agent waves. Plans the fan-out, sends findings to fresh skeptics, keeps a ledger, and reports N of M done; small tasks stay solo.","https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins\u002Fblob\u002Fmaster\u002Fskills\u002Fdeniz-coordination\u002Fverified-waves\u002FSKILL.md",[],[],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":82,"publishedAt":19},"01M79YG6QXNND4WRPP3GATSY8J","harness","The bundled dispatch harness, which runs a task file on worker models through OpenCode or Codex, fences their writes, escalates their asks to the operator, settles each result from git and its verify commands, and records every step in a ledger. Its commands, task file, modes and write fence, configuration, runs and their recovery, ledger and exit codes.","https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins\u002Fblob\u002Fmaster\u002Fskills\u002Fdeniz-coordination\u002Fharness\u002FSKILL.md",[],[],{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":19},"01M79YG2P5S91SDTA5912XJ4FV","delegating-to-worker-models","Use when you hand off implementation, a plan, research a decision will rely on, or a review, and you brief the worker and check what comes back. A subagent that searches, reads in parallel, or keeps your context small is ordinary tool use. Covers what stays with you, the mechanism, assigning work by strength, the walls preflight, prompts without contradictions, the worker contract and model profiles, evidence from your own run, review, and the fix-up loop.","https:\u002F\u002Fgithub.com\u002FBlind-Striker\u002Fagent-skills-and-plugins\u002Fblob\u002Fmaster\u002Fskills\u002Fdeniz-coordination\u002Fdelegating-to-worker-models\u002FSKILL.md",[],[]]