File: UITestGenerator.cs
Web Access
Project: src\aspnetcore\src\Components\Testing\gen\Microsoft.AspNetCore.Components.Testing.Generators.csproj (Microsoft.AspNetCore.Components.Testing.Generators)
// 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;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
 
namespace Microsoft.AspNetCore.Components.Testing.Generators;
 
/// <summary>
/// Emits the MSTest binding for E2E UI tests into the consumer test assembly so the shipping
/// library itself needs no test-framework dependency.
/// </summary>
[Generator]
internal sealed class UITestGenerator : IIncrementalGenerator
{
    private const string UITestAttributeMetadataName =
        "Microsoft.AspNetCore.Components.Testing.Playwright.UITestAttribute";
 
    private const string TestContextMetadataName =
        "Microsoft.VisualStudio.TestTools.UnitTesting.TestContext";
 
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // True only when the consuming compilation references MSTest. All MSTest-specific
        // output is gated on this so referencing the package from a non-test project is inert.
        var hasMSTest = context.CompilationProvider.Select(
            static (compilation, _) => compilation.GetTypeByMetadataName(TestContextMetadataName) is not null);
 
        var classes = context.SyntaxProvider.ForAttributeWithMetadataName(
                UITestAttributeMetadataName,
                predicate: static (node, _) => node is ClassDeclarationSyntax,
                transform: static (ctx, _) => GetUITestClass(ctx))
            .Where(static c => c is not null);
 
        context.RegisterSourceOutput(classes.Combine(hasMSTest), static (spc, pair) =>
        {
            if (pair.Right && pair.Left is UITestClass target)
            {
                spc.AddSource($"{target.HintName}.g.cs", EmitClassBinding(target));
            }
        });
    }
 
    private static UITestClass? GetUITestClass(GeneratorAttributeSyntaxContext context)
    {
        if (context.TargetSymbol is not INamedTypeSymbol type)
        {
            return null;
        }
 
        var ns = type.ContainingNamespace is { IsGlobalNamespace: false } n
            ? n.ToDisplayString()
            : null;
 
        var hintName = (ns is null ? type.Name : ns + "." + type.Name) + ".UITest";
        return new UITestClass(ns, type.Name, hintName);
    }
 
    private static string EmitClassBinding(UITestClass target)
    {
        var sb = new StringBuilder();
        sb.AppendLine("// <auto-generated/>");
        sb.AppendLine("#nullable enable");
        sb.AppendLine("using System;");
        sb.AppendLine("using System.Collections.Generic;");
        sb.AppendLine("using System.IO;");
        sb.AppendLine("using Microsoft.AspNetCore.Components.Testing.Infrastructure;");
        sb.AppendLine("using Microsoft.VisualStudio.TestTools.UnitTesting;");
        sb.AppendLine();
 
        if (target.Namespace is not null)
        {
            sb.Append("namespace ").Append(target.Namespace).AppendLine(";");
            sb.AppendLine();
        }
 
        sb.Append("[TestClass]").AppendLine();
        sb.Append("partial class ").Append(target.Name).AppendLine(" : ITestArtifactManager");
        sb.AppendLine("{");
        sb.AppendLine("    private bool __uiTestCleanupFailed;");
        sb.AppendLine();
        sb.AppendLine("    /// <summary>The MSTest test context for the current test (injected by MSTest).</summary>");
        sb.AppendLine("    public TestContext TestContext { get; set; } = null!;");
        sb.AppendLine();
        sb.AppendLine("    bool ITestArtifactManager.ShouldSaveArtifacts()");
        sb.AppendLine("        => __uiTestCleanupFailed ||");
        sb.AppendLine("            TestContext.CurrentTestOutcome is not (UnitTestOutcome.Passed or UnitTestOutcome.Inconclusive);");
        sb.AppendLine();
        sb.AppendLine("    string ITestArtifactManager.CreateArtifactDirectory(string category)");
        sb.AppendLine("        => Path.Combine(");
        sb.AppendLine("            TestArtifactDirectory.GetPath(TestContext.TestName ?? \"unknown\"),");
        sb.AppendLine("            category,");
        sb.AppendLine("            Guid.NewGuid().ToString(\"N\"));");
        sb.AppendLine();
        sb.AppendLine("    void ITestArtifactManager.AddArtifacts(IReadOnlyList<string> paths)");
        sb.AppendLine("    {");
        sb.AppendLine("        foreach (var path in paths)");
        sb.AppendLine("        {");
        sb.AppendLine("            TestContext.AddResultFile(path);");
        sb.AppendLine("        }");
        sb.AppendLine("    }");
        sb.AppendLine();
        sb.AppendLine("    [TestInitialize]");
        sb.AppendLine("    public global::System.Threading.Tasks.Task __UITestInitializeAsync()");
        sb.AppendLine("        => InitializeCoreAsync();");
        sb.AppendLine();
        sb.AppendLine("    [TestCleanup]");
        sb.AppendLine("    public async global::System.Threading.Tasks.Task __UITestCleanupAsync()");
        sb.AppendLine("    {");
        sb.AppendLine("        try");
        sb.AppendLine("        {");
        sb.AppendLine("            await CleanupCoreAsync().ConfigureAwait(false);");
        sb.AppendLine("        }");
        sb.AppendLine("        catch");
        sb.AppendLine("        {");
        sb.AppendLine("            __uiTestCleanupFailed = true;");
        sb.AppendLine("            throw;");
        sb.AppendLine("        }");
        sb.AppendLine("    }");
        sb.AppendLine("}");
 
        return sb.ToString();
    }
 
    private sealed record UITestClass
    {
        public UITestClass(string? @namespace, string name, string hintName)
        {
            Namespace = @namespace;
            Name = name;
            HintName = hintName;
        }
 
        public string? Namespace { get; }
 
        public string Name { get; }
 
        public string HintName { get; }
 
    }
}