File: Playwright\BrowserTest.cs
Web Access
Project: src\aspnetcore\src\Components\Testing\src\Microsoft.AspNetCore.Components.Testing.csproj (Microsoft.AspNetCore.Components.Testing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Components.Testing.Infrastructure;
using Microsoft.Playwright;
using Microsoft.Playwright.TestAdapter;
 
namespace Microsoft.AspNetCore.Components.Testing.Playwright;
 
/// <summary>
/// Base class for MSTest tests that need a Playwright <see cref="IBrowser"/>. Mirrors the
/// shape of <c>Microsoft.Playwright.MSTest.BrowserTest</c> without taking a dependency on
/// that package.
/// </summary>
/// <remarks>
/// <para>
/// One <see cref="IBrowser"/> is created per test assembly (lazily, on first use) and
/// shared across every test. Browser type and launch options are configured using
/// Playwright's environment variables and runsettings support.
/// </para>
/// <para>
/// Use <see cref="NewContext"/> to create a per-test <see cref="IBrowserContext"/>; every
/// context obtained that way is tracked and closed automatically when the per-test instance
/// is disposed. MSTest runs tests sequentially by default, and contexts remain isolated
/// per test if the consumer opts into parallel execution.
/// </para>
/// </remarks>
public abstract class BrowserTest : PlaywrightTest
{
    private static BrowserState? s_browserState;
    private static readonly SemaphoreSlim s_browserInitLock = new(1, 1);
 
    /// <summary>The shared <see cref="IBrowser"/>. Initialized on first use by <see cref="EnsureBrowserAsync"/>.</summary>
    public IBrowser Browser =>
        s_browserState?.Browser ?? throw new InvalidOperationException(
            $"Browser has not been initialized. {nameof(EnsureBrowserAsync)} is called automatically " +
            "by the BrowserTest initialization hook; ensure your derived class calls base.InitializeCoreAsync().");
 
    /// <summary>Gets the configured browser type shared by all tests in the test assembly.</summary>
    public string BrowserName { get; private set; } = null!;
 
    /// <summary>
    /// Returns the shared <see cref="IBrowser"/>, creating it on first call. Safe to
    /// invoke concurrently — initialization is serialized.
    /// </summary>
    public async Task<IBrowser> EnsureBrowserAsync()
    {
        var state = s_browserState;
        if (state is not null)
        {
            BrowserName = state.BrowserName;
            return state.Browser;
        }
 
        var pw = await EnsurePlaywrightAsync().ConfigureAwait(false);
 
        await s_browserInitLock.WaitAsync().ConfigureAwait(false);
        try
        {
            state = s_browserState;
            if (state is null)
            {
                PlaywrightSettingsProvider.LoadViaEnvIfNeeded();
                var browserName = PlaywrightSettingsProvider.BrowserName;
                var browser = await pw[browserName]
                    .LaunchAsync(PlaywrightSettingsProvider.LaunchOptions)
                    .ConfigureAwait(false);
                state = new(browser, browserName);
                s_browserState = state;
            }
 
            BrowserName = state.BrowserName;
            return state.Browser;
        }
        finally
        {
            s_browserInitLock.Release();
        }
    }
 
    /// <summary>
    /// Creates a new <see cref="IBrowserContext"/> on the shared browser and tracks it for
    /// automatic disposal after the current test outcome has been finalized.
    /// </summary>
    /// <param name="options">Optional browser-context options.</param>
    public async Task<IBrowserContext> NewContext(BrowserNewContextOptions? options = null)
    {
        await EnsureBrowserAsync().ConfigureAwait(false);
        var ctx = await Browser.NewContextAsync(options).ConfigureAwait(false);
        return RegisterForDisposal(ctx);
    }
 
    /// <summary>
    /// Creates a routed browser context with tracing and optional video capture. The context is
    /// disposed automatically after the test framework finalizes the test outcome.
    /// </summary>
    /// <param name="server">The application server to route requests to.</param>
    /// <param name="options">Optional browser-context options.</param>
    /// <returns>The traced browser context.</returns>
    protected async Task<IBrowserContext> NewTracedContextAsync(
        ServerInstance server,
        BrowserNewContextOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(server);
 
        var directory = ArtifactManager.CreateArtifactDirectory("browser");
        var tracedContext = await Infrastructure.PlaywrightExtensions.NewTracedContextAsync(
            Browser, server, directory, ArtifactManager, options).ConfigureAwait(false);
        RegisterForDisposal(tracedContext);
        return tracedContext.Context;
    }
 
    /// <summary>Ensures the shared browser is initialized before the test runs.</summary>
    protected internal override async Task InitializeCoreAsync()
    {
        await base.InitializeCoreAsync().ConfigureAwait(false);
        await EnsureBrowserAsync().ConfigureAwait(false);
    }
 
    private sealed record BrowserState(IBrowser Browser, string BrowserName);
}