File: BlazorWasmTemplateTest.cs
Web Access
Project: src\src\ProjectTemplates\test\Templates.Blazor.Tests\Templates.Blazor.Tests.csproj (Templates.Blazor.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.BrowserTesting;
using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Playwright;
using Templates.Test.Helpers;
 
namespace BlazorTemplates.Tests;
 
#pragma warning disable xUnit1041 // Fixture arguments to test classes must have fixture sources
 
public class BlazorWasmTemplateTest(ProjectFactoryFixture projectFactory) : BlazorTemplateTest(projectFactory)
{
    public override string ProjectType { get; } = "blazorwasm";
 
    [Theory]
    [InlineData(BrowserKind.Chromium)]
    public async Task BlazorWasmStandaloneTemplate_Works(BrowserKind browserKind)
    {
        var project = await CreateBuildPublishAsync();
        var appName = project.ProjectName;
 
        // The service worker assets manifest isn't generated for non-PWA projects
        var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
        Assert.False(File.Exists(Path.Combine(publishDir, "service-worker-assets.js")), "Non-PWA templates should not produce service-worker-assets.js");
 
        // Test the built project
        using (var aspNetProcess = project.StartBuiltProjectAsync())
        {
            Assert.False(
                aspNetProcess.Process.HasExited,
                ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process));
 
            await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
            await TestBasicInteractionInNewPageAsync(browserKind, aspNetProcess.ListeningUri.AbsoluteUri, appName);
        }
 
        // Test the published project
        var (serveProcess, listeningUri) = RunPublishedStandaloneBlazorProject(project);
        using (serveProcess)
        {
            await TestBasicInteractionInNewPageAsync(browserKind, listeningUri, appName);
        }
    }
 
    [Theory]
    [InlineData(BrowserKind.Chromium)]
    public async Task BlazorWasmStandalonePwaTemplate_Works(BrowserKind browserKind)
    {
        var project = await CreateBuildPublishAsync(args: ["--pwa"]);
        var appName = project.ProjectName;
 
        // Test the built project
        using (var aspNetProcess = project.StartBuiltProjectAsync())
        {
            Assert.False(
                aspNetProcess.Process.HasExited,
                ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process));
 
            await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
            await TestBasicInteractionInNewPageAsync(browserKind, aspNetProcess.ListeningUri.AbsoluteUri, appName);
        }
 
        ValidatePublishedServiceWorker(project);
 
        // Test the published project
        if (BrowserManager.IsAvailable(browserKind))
        {
            var (serveProcess, listeningUri) = RunPublishedStandaloneBlazorProject(project);
            await using var browser = await BrowserManager.GetBrowserInstance(browserKind, BrowserContextInfo);
            Output.WriteLine($"Opening browser at {listeningUri}...");
            var page = await browser.NewPageAsync();
            await page.GotoAsync(listeningUri, new() { WaitUntil = WaitUntilState.NetworkIdle });
            using (serveProcess)
            {
                await TestBasicInteractionAsync(page, project.ProjectName);
            }
 
            // The PWA template supports offline use. By now, the browser should have cached everything it needs,
            // so we can continue working even without the server.
            await page.GotoAsync("about:blank");
            await browser.SetOfflineAsync(true);
            await page.GotoAsync(listeningUri);
            await TestBasicInteractionAsync(page, project.ProjectName, pagesToExclude: BlazorTemplatePages.Weather);
            await page.CloseAsync();
        }
        else
        {
            EnsureBrowserAvailable(browserKind);
        }
    }
 
    [Theory]
    [MemberData(nameof(TemplateData))]
    public Task BlazorWasmStandaloneTemplate_AzureActiveDirectoryTemplate_Works(TemplateInstance instance)
        => CreateBuildPublishAsync(args: instance.Arguments, targetFramework: "netstandard2.1");
 
    private static void ValidatePublishedServiceWorker(Project project)
    {
        var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
 
        // When publishing the PWA template, we generate an assets manifest
        // and move service-worker.published.js to overwrite service-worker.js
        Assert.False(File.Exists(Path.Combine(publishDir, "service-worker.published.js")), "service-worker.published.js should not be published");
        Assert.True(File.Exists(Path.Combine(publishDir, "service-worker.js")), "service-worker.js should be published");
        Assert.True(File.Exists(Path.Combine(publishDir, "service-worker-assets.js")), "service-worker-assets.js should be published");
 
        // We automatically append the SWAM version as a comment in the published service worker file
        var serviceWorkerAssetsManifestContents = ReadFile(publishDir, "service-worker-assets.js");
        var serviceWorkerContents = ReadFile(publishDir, "service-worker.js");
 
        // Parse the "version": "..." value from the SWAM, and check it's in the service worker
        var serviceWorkerAssetsManifestVersionMatch = new Regex(@"^\s*\""version\"":\s*(\""[^\""]+\"")", RegexOptions.Multiline)
            .Match(serviceWorkerAssetsManifestContents);
        Assert.True(serviceWorkerAssetsManifestVersionMatch.Success);
        var serviceWorkerAssetsManifestVersionJson = serviceWorkerAssetsManifestVersionMatch.Groups[1].Captures[0].Value;
        var serviceWorkerAssetsManifestVersion = JsonSerializer.Deserialize<string>(serviceWorkerAssetsManifestVersionJson);
        Assert.True(serviceWorkerContents.Contains($"/* Manifest version: {serviceWorkerAssetsManifestVersion} */", StringComparison.Ordinal));
 
        static string ReadFile(string basePath, string path)
        {
            var fullPath = Path.Combine(basePath, path);
            var doesExist = File.Exists(fullPath);
 
            Assert.True(doesExist, $"Expected file to exist, but it doesn't: {path}");
            return File.ReadAllText(Path.Combine(basePath, path));
        }
    }
 
    public static TheoryData<TemplateInstance> TemplateData => new TheoryData<TemplateInstance>
        {
            new TemplateInstance(
                "blazorwasmstandaloneaadb2c",
                "-au", "IndividualB2C",
                "--aad-b2c-instance", "example.b2clogin.com",
                "-ssp", "b2c_1_siupin",
                "--client-id", "clientId",
                "--domain", "my-domain"),
            new TemplateInstance(
                "blazorwasmstandaloneaad",
                "-au", "SingleOrg",
                "--domain", "my-domain",
                "--tenant-id", "tenantId",
                "--client-id", "clientId"),
        };
 
    public class TemplateInstance
    {
        public TemplateInstance(string name, params string[] arguments)
        {
            Name = name;
            Arguments = arguments;
        }
 
        public string Name { get; }
        public string[] Arguments { get; }
    }
 
    private (ProcessEx, string url) RunPublishedStandaloneBlazorProject(Project project)
    {
        var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
 
        Output.WriteLine("Running dotnet serve on published output...");
        var command = DotNetMuxer.MuxerPathOrDefault();
        string args;
        if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HELIX_DIR")))
        {
            args = $"serve ";
        }
        else
        {
            command = "dotnet-serve";
            args = "--roll-forward LatestMajor"; // dotnet-serve targets net5.0 by default
        }
 
        var serveProcess = ProcessEx.Run(TestOutputHelper, publishDir, command, args);
        var listeningUri = ResolveListeningUrl(serveProcess);
        return (serveProcess, listeningUri);
 
        static string ResolveListeningUrl(ProcessEx process)
        {
            var buffer = new List<string>();
            try
            {
                foreach (var line in process.OutputLinesAsEnumerable)
                {
                    if (line != null)
                    {
                        buffer.Add(line);
                        if (line.Trim().Contains("https://", StringComparison.Ordinal) || line.Trim().Contains("http://", StringComparison.Ordinal))
                        {
                            return line.Trim();
                        }
                    }
                }
            }
            catch (OperationCanceledException)
            {
            }
 
            throw new InvalidOperationException(
                $"Couldn't find listening url:\n{string.Join(Environment.NewLine, buffer.Append(process.Error))}");
        }
    }
}