File: Infrastructure\ResourceLock.cs
Web Access
Project: src\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 System.Text.RegularExpressions;
using Microsoft.Playwright;
 
namespace Microsoft.AspNetCore.Components.Testing.Infrastructure;
 
/// <summary>
/// Holds a browser resource (script, stylesheet, fetch, etc.) by intercepting
/// its Playwright route. The resource request is paused until the lock is
/// disposed or explicitly released.
/// </summary>
/// <remarks>
/// <para>
/// Create a lock before the navigation that triggers the resource request,
/// then call <see cref="WaitForRequestAsync"/> after navigating:
/// </para>
/// <code>
/// await using var blazorScript = await ResourceLock.CreateAsync(
///     page, new Regex(@"blazor\.web.*\.js"));
///
/// await page.GotoAsync(url, new() { WaitUntil = WaitUntilState.Commit });
/// await blazorScript.WaitForRequestAsync();
///
/// // Resource is held — Blazor hasn't started
/// await Expect(page.Locator("h1")).ToHaveTextAsync("Hello, world!");
/// </code>
/// </remarks>
public sealed class ResourceLock : IAsyncDisposable
{
    private readonly IPage _page;
    private readonly Regex _urlPattern;
    private readonly TaskCompletionSource _requestedTcs = new();
    private IRoute? _heldRoute;
    private int _captured;
    private bool _released;
 
    ResourceLock(IPage page, Regex urlPattern)
    {
        _page = page;
        _urlPattern = urlPattern;
    }
 
    /// <summary>
    /// Sets up route interception for requests matching the given URL pattern.
    /// Must be called before the navigation that triggers the resource request.
    /// </summary>
    /// <param name="page">The page to intercept routes on.</param>
    /// <param name="urlPattern">A regex pattern matching the URL(s) to intercept.</param>
    /// <returns>A <see cref="ResourceLock"/> that holds the first matching request.</returns>
    public static async Task<ResourceLock> CreateAsync(IPage page, Regex urlPattern)
    {
        var resourceLock = new ResourceLock(page, urlPattern);
        await page.RouteAsync(urlPattern, async route =>
        {
            if (Interlocked.CompareExchange(ref resourceLock._captured, 1, 0) == 0)
            {
                resourceLock._heldRoute = route;
                resourceLock._requestedTcs.TrySetResult();
            }
            else
            {
                await route.ContinueAsync().ConfigureAwait(false);
            }
        }).ConfigureAwait(false);
        return resourceLock;
    }
 
    /// <summary>
    /// Waits until the browser actually requests the intercepted resource.
    /// Call this after the navigation that triggers the request.
    /// </summary>
    public Task WaitForRequestAsync() => _requestedTcs.Task;
 
    /// <summary>
    /// Explicitly releases the held route, allowing the request to continue.
    /// Idempotent — safe to call multiple times.
    /// </summary>
    public async Task ReleaseAsync()
    {
        if (_released)
        {
            return;
        }
 
        _released = true;
        if (_heldRoute is not null)
        {
            await _heldRoute.ContinueAsync().ConfigureAwait(false);
        }
        await _page.UnrouteAsync(_urlPattern).ConfigureAwait(false);
    }
 
    /// <inheritdoc/>
    public async ValueTask DisposeAsync() => await ReleaseAsync().ConfigureAwait(false);
}