File: Hosting\WebAssemblyHostTest.cs
Web Access
Project: src\src\Components\WebAssembly\WebAssembly\test\Microsoft.AspNetCore.Components.WebAssembly.Tests.csproj (Microsoft.AspNetCore.Components.WebAssembly.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.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.JSInterop;
using Moq;
 
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
 
public class WebAssemblyHostTest
{
    // This won't happen in the product code, but we need to be able to safely call RunAsync
    // to be able to test a few of the other details.
    [Fact]
    public async Task RunAsync_CanExitBasedOnCancellationToken()
    {
        // Arrange
        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
        builder.Services.AddSingleton(Mock.Of<IJSRuntime>());
        var host = builder.Build();
        var cultureProvider = new TestSatelliteResourcesLoader();
 
        var cts = new CancellationTokenSource();
 
        // Act
        var task = host.RunAsyncCore(cts.Token, cultureProvider);
 
        cts.Cancel();
        await task.TimeoutAfter(TimeSpan.FromSeconds(3));
 
        // Assert (does not throw)
    }
 
    [Fact]
    public async Task RunAsync_CallingTwiceCausesException()
    {
        // Arrange
        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
        builder.Services.AddSingleton(Mock.Of<IJSRuntime>());
        var host = builder.Build();
        var cultureProvider = new TestSatelliteResourcesLoader();
 
        var cts = new CancellationTokenSource();
        var task = host.RunAsyncCore(cts.Token, cultureProvider);
 
        // Act
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => host.RunAsyncCore(cts.Token));
 
        cts.Cancel();
        await task.TimeoutAfter(TimeSpan.FromSeconds(3));
 
        // Assert
        Assert.Equal("The host has already started.", ex.Message);
    }
 
    [Fact]
    public async Task DisposeAsync_CanDisposeAfterCallingRunAsync()
    {
        // Arrange
        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
        builder.Services.AddSingleton(Mock.Of<IJSRuntime>());
        builder.Services.AddSingleton<DisposableService>();
        var host = builder.Build();
        var cultureProvider = new TestSatelliteResourcesLoader();
 
        var disposable = host.Services.GetRequiredService<DisposableService>();
 
        var cts = new CancellationTokenSource();
 
        // Act
        await using (host)
        {
            var task = host.RunAsyncCore(cts.Token, cultureProvider);
 
            cts.Cancel();
            await task.TimeoutAfter(TimeSpan.FromSeconds(3));
        }
 
        // Assert
        Assert.Equal(1, disposable.DisposeCount);
    }
 
    [Fact]
    public async Task RunAsync_StartsHostedServices()
    {
        // Arrange
        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
        builder.Services.AddSingleton(Mock.Of<IJSRuntime>());
        
        var testHostedService = new TestHostedService();
        builder.Services.AddSingleton<IHostedService>(testHostedService);
        
        var host = builder.Build();
        var cultureProvider = new TestSatelliteResourcesLoader();
 
        var cts = new CancellationTokenSource();
 
        // Act
        var task = host.RunAsyncCore(cts.Token, cultureProvider);
        
        // Give hosted services time to start
        await Task.Delay(100);
        cts.Cancel();
        await task.TimeoutAfter(TimeSpan.FromSeconds(3));
 
        // Assert
        Assert.True(testHostedService.StartCalled);
        Assert.Equal(cts.Token, testHostedService.StartToken);
    }
 
    [Fact]
    public async Task DisposeAsync_StopsHostedServices()
    {
        // Arrange
        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
        builder.Services.AddSingleton(Mock.Of<IJSRuntime>());
        
        var testHostedService1 = new TestHostedService();
        var testHostedService2 = new TestHostedService();
        builder.Services.AddSingleton<IHostedService>(testHostedService1);
        builder.Services.AddSingleton<IHostedService>(testHostedService2);
        
        var host = builder.Build();
        var cultureProvider = new TestSatelliteResourcesLoader();
 
        var cts = new CancellationTokenSource();
 
        // Start the host to initialize hosted services
        var runTask = host.RunAsyncCore(cts.Token, cultureProvider);
        await Task.Delay(100);
 
        // Act - dispose the host
        await host.DisposeAsync();
        cts.Cancel();
        await runTask.TimeoutAfter(TimeSpan.FromSeconds(3));
 
        // Assert
        Assert.True(testHostedService1.StartCalled);
        Assert.True(testHostedService1.StopCalled);
        Assert.True(testHostedService2.StartCalled);
        Assert.True(testHostedService2.StopCalled);
    }
 
    [Fact]
    public async Task DisposeAsync_HandlesHostedServiceStopErrors()
    {
        // Arrange
        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
        builder.Services.AddSingleton(Mock.Of<IJSRuntime>());
        
        var goodService = new TestHostedService();
        var faultyService = new FaultyHostedService();
        builder.Services.AddSingleton<IHostedService>(goodService);
        builder.Services.AddSingleton<IHostedService>(faultyService);
        
        var host = builder.Build();
        var cultureProvider = new TestSatelliteResourcesLoader();
 
        var cts = new CancellationTokenSource();
 
        // Start the host to initialize hosted services
        var runTask = host.RunAsyncCore(cts.Token, cultureProvider);
        await Task.Delay(100);
 
        // Act & Assert - dispose should not throw even if hosted service fails
        await host.DisposeAsync();
        cts.Cancel();
        await runTask.TimeoutAfter(TimeSpan.FromSeconds(3));
 
        Assert.True(goodService.StartCalled);
        Assert.True(goodService.StopCalled);
        Assert.True(faultyService.StartCalled);
        Assert.True(faultyService.StopCalled);
    }
 
    [Fact]
    public async Task RunAsync_SupportsAddHostedServiceExtension()
    {
        // Arrange
        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
        builder.Services.AddSingleton(Mock.Of<IJSRuntime>());
        
        // Test manual hosted service registration (equivalent to AddHostedService)
        builder.Services.AddSingleton<TestHostedService>();
        builder.Services.AddSingleton<IHostedService>(serviceProvider => serviceProvider.GetRequiredService<TestHostedService>());
        
        var host = builder.Build();
        var cultureProvider = new TestSatelliteResourcesLoader();
 
        var cts = new CancellationTokenSource();
 
        // Act
        var task = host.RunAsyncCore(cts.Token, cultureProvider);
        
        // Give hosted services time to start
        await Task.Delay(100);
        cts.Cancel();
        await task.TimeoutAfter(TimeSpan.FromSeconds(3));
 
        // Assert - verify the hosted service was started via service collection
        var hostedServices = host.Services.GetServices<IHostedService>();
        Assert.Single(hostedServices);
        
        var testService = hostedServices.First();
        Assert.IsType<TestHostedService>(testService);
        Assert.True(((TestHostedService)testService).StartCalled);
    }
 
    private class TestHostedService : IHostedService
    {
        public bool StartCalled { get; private set; }
        public bool StopCalled { get; private set; }
        public CancellationToken StartToken { get; private set; }
        public CancellationToken StopToken { get; private set; }
 
        public Task StartAsync(CancellationToken cancellationToken)
        {
            StartCalled = true;
            StartToken = cancellationToken;
            return Task.CompletedTask;
        }
 
        public Task StopAsync(CancellationToken cancellationToken)
        {
            StopCalled = true;
            StopToken = cancellationToken;
            return Task.CompletedTask;
        }
    }
 
    private class FaultyHostedService : IHostedService
    {
        public bool StartCalled { get; private set; }
        public bool StopCalled { get; private set; }
 
        public Task StartAsync(CancellationToken cancellationToken)
        {
            StartCalled = true;
            return Task.CompletedTask;
        }
 
        public Task StopAsync(CancellationToken cancellationToken)
        {
            StopCalled = true;
            throw new InvalidOperationException("Simulated hosted service stop error");
        }
    }
 
    private class DisposableService : IAsyncDisposable
    {
        public int DisposeCount { get; private set; }
 
        public ValueTask DisposeAsync()
        {
            DisposeCount++;
            return new ValueTask(Task.CompletedTask);
        }
    }
 
    private class TestSatelliteResourcesLoader : WebAssemblyCultureProvider
    {
        internal TestSatelliteResourcesLoader()
            : base(CultureInfo.CurrentCulture)
        {
        }
 
        public override ValueTask LoadCurrentCultureResourcesAsync() => default;
    }
}