| 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; using Xunit; namespace Microsoft.AspNetCore.Components.Testing.Infrastructure; /// <summary> /// Manages the lifecycle of a Playwright trace (and optionally video) for a /// single browser context. On disposal, saves or discards artifacts based on /// the test outcome reported by xUnit's <see cref="TestContext"/>. /// </summary> /// <remarks> /// <para> /// <see cref="TestContext.Current"/> provides test state availability: /// </para> /// <list type="bullet"> /// <item><description>During test method body (<c>await using</c>): <c>TestState</c> is <c>null</c> → always saves (conservative; wasteful on passing tests)</description></item> /// <item><description>During <c>IAsyncLifetime.DisposeAsync</c>: <c>TestState</c> is populated → conditional save on failure only</description></item> /// </list> /// <para> /// To avoid keeping artifacts for passing tests, dispose the <see cref="TracingSession"/> /// inside <c>IAsyncLifetime.DisposeAsync</c> rather than via <c>await using</c> in the test body. /// </para> /// </remarks> public sealed class TracingSession : IAsyncDisposable { private readonly IBrowserContext _context; private readonly string _artifactDir; private readonly bool _recordVideo; TracingSession(IBrowserContext context, string artifactDir, bool recordVideo) { _context = context; _artifactDir = artifactDir; _recordVideo = recordVideo; } /// <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> /// <returns>A <see cref="TracingSession"/> managing the trace lifecycle.</returns> public static async Task<TracingSession> StartAsync( IBrowserContext context, string artifactDir, bool recordVideo) { Directory.CreateDirectory(artifactDir); await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true }).ConfigureAwait(false); return new TracingSession(context, artifactDir, recordVideo); } /// <inheritdoc/> public async ValueTask DisposeAsync() { // TestState is populated during xUnit's CleaningUp phase (DisposeAsync of the // test class). When disposed via `await using` in the test method body, TestState // is null — we conservatively save artifacts (correct on failure, wasteful on success). var testState = TestContext.Current.TestState; var shouldSave = testState is null || testState.Result == TestResult.Failed; // 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); } 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 if (!shouldSave) { foreach (var page in pages) { if (page.Video is not null) { try { await page.Video.DeleteAsync().ConfigureAwait(false); } catch { /* video file may not exist */ } } } } } // 3. Report or clean up if (shouldSave && Directory.Exists(_artifactDir)) { var files = Directory.GetFiles(_artifactDir); if (files.Length > 0) { Console.WriteLine($"[E2E] Test artifacts saved to: {_artifactDir}"); foreach (var file in files) { Console.WriteLine($"[E2E] {Path.GetFileName(file)}"); } } } else if (!shouldSave && Directory.Exists(_artifactDir)) { try { if (!Directory.EnumerateFileSystemEntries(_artifactDir).Any()) { Directory.Delete(_artifactDir); } } catch { /* best effort cleanup */ } } } }