File: Infrastructure\TracingSession.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 System.Linq;
using Microsoft.Playwright;
 
namespace Microsoft.AspNetCore.Components.Testing.Infrastructure;
 
/// <summary>
/// Manages the lifecycle of a Playwright trace (and optionally video) for a single
/// browser context. On disposal the session asks the supplied
/// <see cref="ITestArtifactManager"/> whether to keep the artifacts, then either saves and
/// publishes them or discards them.
/// </summary>
/// <remarks>
/// <para>
/// This type is intentionally decoupled from any test framework: it does not read a
/// test outcome or attach files itself. The save decision and the handling of saved
/// artifacts are provided by <see cref="ITestArtifactManager"/>, so a source-generated adapter in the
/// consumer test assembly can bridge the test framework's outcome/attachment APIs
/// (for example MSTest's <c>TestContext.CurrentTestOutcome</c> and
/// <c>TestContext.AddResultFile</c>) without the library taking a dependency on them.
/// </para>
/// <para>
/// <see cref="ITestArtifactManager.ShouldSaveArtifacts"/> is evaluated at disposal time,
/// after the test framework has finalized the test outcome.
/// </para>
/// </remarks>
internal sealed class TracingSession : IAsyncDisposable
{
    private readonly IBrowserContext _context;
    private readonly string _artifactDir;
    private readonly bool _recordVideo;
    private readonly ITestArtifactManager _artifactManager;
 
    TracingSession(
        IBrowserContext context,
        string artifactDir,
        bool recordVideo,
        ITestArtifactManager artifactManager)
    {
        _context = context;
        _artifactDir = artifactDir;
        _recordVideo = recordVideo;
        _artifactManager = artifactManager;
    }
 
    /// <summary>
    /// Starts tracing on the given browser context with screenshots, snapshots, and sources enabled.
    /// </summary>
    /// <param name="context">The browser context to trace.</param>
    /// <param name="artifactDir">The directory to store trace artifacts in.</param>
    /// <param name="recordVideo">Whether video recording is enabled.</param>
    /// <param name="artifactManager">Determines whether artifacts are retained and publishes retained files.</param>
    /// <returns>A <see cref="TracingSession"/> managing the trace lifecycle.</returns>
    internal static async Task<TracingSession> StartAsync(
        IBrowserContext context,
        string artifactDir,
        bool recordVideo,
        ITestArtifactManager artifactManager)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(artifactDir);
        ArgumentNullException.ThrowIfNull(artifactManager);
 
        Directory.CreateDirectory(artifactDir);
 
        await context.Tracing.StartAsync(new()
        {
            Screenshots = true,
            Snapshots = true,
            Sources = true
        }).ConfigureAwait(false);
 
        return new TracingSession(context, artifactDir, recordVideo, artifactManager);
    }
 
    /// <inheritdoc/>
    public async ValueTask DisposeAsync()
    {
        var shouldSave = _artifactManager.ShouldSaveArtifacts();
        var savedFiles = new List<string>();
 
        // 1. Stop tracing — save to file or discard
        var tracePath = Path.Combine(_artifactDir, "trace.zip");
        if (shouldSave)
        {
            await _context.Tracing.StopAsync(new() { Path = tracePath }).ConfigureAwait(false);
            savedFiles.Add(tracePath);
        }
        else
        {
            await _context.Tracing.StopAsync().ConfigureAwait(false); // discard
        }
 
        // 2. Handle video: close context to flush video files, then keep or delete
        if (_recordVideo)
        {
            var pages = _context.Pages.ToList();
            await _context.CloseAsync().ConfigureAwait(false); // flushes video to disk
 
            foreach (var page in pages)
            {
                if (page.Video is null)
                {
                    continue;
                }
 
                if (shouldSave)
                {
                    try
                    {
                        var videoPath = await page.Video.PathAsync().ConfigureAwait(false);
                        if (!string.IsNullOrEmpty(videoPath))
                        {
                            savedFiles.Add(videoPath);
                        }
                    }
                    catch
                    {
                        // video file may not be available
                    }
                }
                else
                {
                    try { await page.Video.DeleteAsync().ConfigureAwait(false); }
                    catch { /* video file may not exist */ }
                }
            }
        }
 
        // 3. Report saved artifacts to the caller (attach + log happen in the adapter),
        //    or best-effort remove an empty discarded directory.
        if (shouldSave)
        {
            if (savedFiles.Count > 0)
            {
                _artifactManager.AddArtifacts(savedFiles);
            }
        }
        else if (Directory.Exists(_artifactDir))
        {
            try
            {
                if (!Directory.EnumerateFileSystemEntries(_artifactDir).Any())
                {
                    Directory.Delete(_artifactDir);
                }
            }
            catch { /* best effort cleanup */ }
        }
    }
}