// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.JSInterop; using Microsoft.Net.Http.Headers; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test { public class HtmlHelperComponentExtensionsTests { [Fact] public async Task CanRender_ParameterlessComponent() { // Arrange var helper = CreateHelper(); var writer = new StringWriter(); // Act var result = await helper.RenderComponentAsync(); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); // Assert Assert.Equal("

Hello world!

", content); } [Fact] public async Task CanRender_ComponentWithParametersObject() { // Arrange var helper = CreateHelper(); var writer = new StringWriter(); // Act var result = await helper.RenderComponentAsync(new { Name = "Steve" }); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); // Assert Assert.Equal("

Hello Steve!

", content); } [Fact] public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() { // Arrange var helper = CreateHelper(); var writer = new StringWriter(); // Act var state = new OnAfterRenderState(); var result = await helper.RenderComponentAsync(new { State = state }); result.WriteTo(writer, HtmlEncoder.Default); // Assert Assert.Equal("

Hello

", writer.ToString()); Assert.False(state.OnAfterRenderRan); } [Fact] public async Task CanCatch_ComponentWithSynchronousException() { // Arrange var helper = CreateHelper(); // Act & Assert var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(new { IsAsync = false })); // Assert Assert.Equal("Threw an exception synchronously", exception.Message); } [Fact] public async Task CanCatch_ComponentWithAsynchronousException() { // Arrange var helper = CreateHelper(); // Act & Assert var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(new { IsAsync = true })); // Assert Assert.Equal("Threw an exception asynchronously", exception.Message); } [Fact] public async Task Rendering_ComponentWithJsInteropThrows() { // Arrange var helper = CreateHelper(); // Act & Assert var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(new { JsInterop = true })); // Assert Assert.Equal("JavaScript interop calls cannot be issued during server-side prerendering, " + "because the page has not yet loaded in the browser. Prerendered components must wrap any JavaScript " + "interop calls in conditional logic to ensure those interop calls are not attempted during prerendering.", exception.Message); } [Fact] public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponseHasAlreadyStarted() { // Arrange var ctx = new DefaultHttpContext(); ctx.Request.Scheme = "http"; ctx.Request.Host = new HostString("localhost"); ctx.Request.PathBase = "/base"; ctx.Request.Path = "/path"; ctx.Request.QueryString = new QueryString("?query=value"); var responseMock = new Mock(); responseMock.Setup(r => r.HasStarted).Returns(true); ctx.Features.Set(responseMock.Object); var helper = CreateHelper(ctx); var writer = new StringWriter(); // Act var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(new { RedirectUri = "http://localhost/redirect" })); Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " + "Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" + "reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", exception.Message); } [Fact] public async Task HtmlHelper_Redirects_WhenComponentNavigates() { // Arrange var ctx = new DefaultHttpContext(); ctx.Request.Scheme = "http"; ctx.Request.Host = new HostString("localhost"); ctx.Request.PathBase = "/base"; ctx.Request.Path = "/path"; ctx.Request.QueryString = new QueryString("?query=value"); var helper = CreateHelper(ctx); // Act await helper.RenderComponentAsync(new { RedirectUri = "http://localhost/redirect" }); // Assert Assert.Equal(302, ctx.Response.StatusCode); Assert.Equal("http://localhost/redirect", ctx.Response.Headers[HeaderNames.Location]); } [Fact] public async Task CanRender_AsyncComponent() { // Arrange var helper = CreateHelper(); var writer = new StringWriter(); var expectedContent = @"
Date Summary F C
06/05/2018 Freezing 33 33
07/05/2018 Bracing 57 57
08/05/2018 Freezing 9 9
09/05/2018 Balmy 4 4
10/05/2018 Chilly 29 29
"; // Act var result = await helper.RenderComponentAsync(); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); // Assert Assert.Equal(expectedContent.Replace("\r\n", "\n"), content); } private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action configureServices = null) { var services = new ServiceCollection(); services.AddSingleton(HtmlEncoder.Default); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); configureServices?.Invoke(services); var helper = new Mock(); var context = ctx ?? new DefaultHttpContext(); context.RequestServices = services.BuildServiceProvider(); context.Request.Scheme = "http"; context.Request.Host = new HostString("localhost"); context.Request.PathBase = "/base"; context.Request.Path = "/path"; context.Request.QueryString = QueryString.FromUriComponent("?query=value"); helper.Setup(h => h.ViewContext) .Returns(new ViewContext() { HttpContext = context }); return helper.Object; } private class TestComponent : IComponent { private RenderHandle _renderHandle; public void Attach(RenderHandle renderHandle) { _renderHandle = renderHandle; } public Task SetParametersAsync(ParameterView parameters) { _renderHandle.Render(builder => { var s = 0; builder.OpenElement(s++, "h1"); builder.AddContent(s++, "Hello world!"); builder.CloseElement(); }); return Task.CompletedTask; } } private class RedirectComponent : ComponentBase { [Inject] NavigationManager NavigationManager { get; set; } [Parameter] public string RedirectUri { get; set; } [Parameter] public bool Force { get; set; } protected override void OnInitialized() { NavigationManager.NavigateTo(RedirectUri, Force); } } private class ExceptionComponent : ComponentBase { [Parameter] public bool IsAsync { get; set; } [Parameter] public bool JsInterop { get; set; } [Inject] IJSRuntime JsRuntime { get; set; } protected override async Task OnParametersSetAsync() { if (JsInterop) { await JsRuntime.InvokeAsync("window.alert", "Interop!"); } if (!IsAsync) { throw new InvalidOperationException("Threw an exception synchronously"); } else { await Task.Yield(); throw new InvalidOperationException("Threw an exception asynchronously"); } } } private class OnAfterRenderComponent : ComponentBase { [Parameter] public OnAfterRenderState State { get; set; } protected override void OnAfterRender() { State.OnAfterRenderRan = true; } protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.AddMarkupContent(0, "

Hello

"); } } private class OnAfterRenderState { public bool OnAfterRenderRan { get; set; } } private class GreetingComponent : ComponentBase { [Parameter] public string Name { get; set; } protected override void OnParametersSet() { base.OnParametersSet(); } protected override void BuildRenderTree(RenderTreeBuilder builder) { var s = 0; base.BuildRenderTree(builder); builder.OpenElement(s++, "p"); builder.AddContent(s++, $"Hello {Name}!"); builder.CloseElement(); } } private class AsyncComponent : ComponentBase { private static WeatherRow[] _weatherData = new[] { new WeatherRow { DateFormatted = "06/05/2018", TemperatureC = 1, Summary = "Freezing", TemperatureF = 33 }, new WeatherRow { DateFormatted = "07/05/2018", TemperatureC = 14, Summary = "Bracing", TemperatureF = 57 }, new WeatherRow { DateFormatted = "08/05/2018", TemperatureC = -13, Summary = "Freezing", TemperatureF = 9 }, new WeatherRow { DateFormatted = "09/05/2018", TemperatureC = -16, Summary = "Balmy", TemperatureF = 4 }, new WeatherRow { DateFormatted = "10/05/2018", TemperatureC = 2, Summary = "Chilly", TemperatureF = 29 } }; public class WeatherRow { public string DateFormatted { get; set; } public int TemperatureC { get; set; } public string Summary { get; set; } public int TemperatureF { get; set; } } public WeatherRow[] RowsToDisplay { get; set; } protected override async Task OnParametersSetAsync() { // Simulate an async workflow. await Task.Yield(); RowsToDisplay = _weatherData; } protected override void BuildRenderTree(RenderTreeBuilder builder) { base.BuildRenderTree(builder); var s = 0; builder.OpenElement(s++, "table"); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "thead"); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "tr"); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "th"); builder.AddContent(s++, "Date"); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "th"); builder.AddContent(s++, "Summary"); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "th"); builder.AddContent(s++, "F"); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "th"); builder.AddContent(s++, "C"); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "tbody"); builder.AddMarkupContent(s++, "\n"); if (RowsToDisplay != null) { var s2 = s; foreach (var element in RowsToDisplay) { s = s2; builder.OpenElement(s++, "tr"); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "td"); builder.AddContent(s++, element.DateFormatted); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "td"); builder.AddContent(s++, element.Summary); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "td"); builder.AddContent(s++, element.TemperatureF); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.OpenElement(s++, "td"); builder.AddContent(s++, element.TemperatureF); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); } } builder.CloseElement(); builder.AddMarkupContent(s++, "\n"); builder.CloseElement(); } } } }