parent
9f2b534436
commit
2ff6a5c0f8
|
|
@ -1,5 +1,10 @@
|
|||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
@layout MainLayout
|
||||
|
|
@ -1 +1,8 @@
|
|||
<Router AppAssembly=typeof(Program).Assembly />
|
||||
<Router AppAssembly=typeof(Program).Assembly>
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
Sorry, there's nothing here.
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
<!--
|
||||
Configuring this stuff here is temporary. Later we'll move the app config
|
||||
into Program.cs, and it won't be necessary to specify AppAssembly.
|
||||
-->
|
||||
<Router AppAssembly=typeof(StandaloneApp.Program).Assembly />
|
||||
<Router AppAssembly=typeof(StandaloneApp.Program).Assembly>
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<h2>Not found</h2>
|
||||
Sorry, there's nothing at this address.
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
@layout MainLayout
|
||||
|
|
@ -16,6 +16,15 @@ namespace Microsoft.AspNetCore.Components
|
|||
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> GetAuthenticationStateAsync();
|
||||
protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> task) { }
|
||||
}
|
||||
public sealed partial class AuthorizeRouteView : Microsoft.AspNetCore.Components.RouteView
|
||||
{
|
||||
public AuthorizeRouteView() { }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
protected override void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
|
||||
}
|
||||
public partial class AuthorizeView : Microsoft.AspNetCore.Components.AuthorizeViewCore
|
||||
{
|
||||
public AuthorizeView() { }
|
||||
|
|
@ -278,6 +287,16 @@ namespace Microsoft.AspNetCore.Components
|
|||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment Body { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
}
|
||||
public partial class LayoutView : Microsoft.AspNetCore.Components.IComponent
|
||||
{
|
||||
public LayoutView() { }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public System.Type Layout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
|
||||
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
||||
}
|
||||
public sealed partial class LocationChangeException : System.Exception
|
||||
{
|
||||
public LocationChangeException(string message, System.Exception innerException) { }
|
||||
|
|
@ -323,20 +342,6 @@ namespace Microsoft.AspNetCore.Components
|
|||
protected OwningComponentBase() { }
|
||||
protected TService Service { get { throw null; } }
|
||||
}
|
||||
public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent
|
||||
{
|
||||
public PageDisplay() { }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public System.Type Page { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public System.Collections.Generic.IDictionary<string, object> PageParameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
|
||||
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
||||
}
|
||||
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
|
||||
public sealed partial class ParameterAttribute : System.Attribute
|
||||
{
|
||||
|
|
@ -391,6 +396,23 @@ namespace Microsoft.AspNetCore.Components
|
|||
public RouteAttribute(string template) { }
|
||||
public string Template { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
}
|
||||
public sealed partial class RouteData
|
||||
{
|
||||
public RouteData(System.Type pageType, System.Collections.Generic.IReadOnlyDictionary<string, object> routeValues) { }
|
||||
public System.Type PageType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, object> RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
}
|
||||
public partial class RouteView : Microsoft.AspNetCore.Components.IComponent
|
||||
{
|
||||
public RouteView() { }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public System.Type DefaultLayout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RouteData RouteData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
|
||||
protected virtual void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
|
||||
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
||||
}
|
||||
}
|
||||
namespace Microsoft.AspNetCore.Components.CompilerServices
|
||||
{
|
||||
|
|
@ -648,15 +670,12 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public System.Reflection.Assembly AppAssembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.RouteData> Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
|
||||
public void Dispose() { }
|
||||
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
|
||||
protected virtual void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder, System.Type handler, System.Collections.Generic.IDictionary<string, object> parameters) { }
|
||||
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Auth;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Combines the behaviors of <see cref="AuthorizeView"/> and <see cref="RouteView"/>,
|
||||
/// so that it displays the page matching the specified route but only if the user
|
||||
/// is authorized to see it.
|
||||
///
|
||||
/// Additionally, this component supplies a cascading parameter of type <see cref="Task{AuthenticationState}"/>,
|
||||
/// which makes the user's current authentication state available to descendants.
|
||||
/// </summary>
|
||||
public sealed class AuthorizeRouteView : RouteView
|
||||
{
|
||||
// We expect applications to supply their own authorizing/not-authorized content, but
|
||||
// it's better to have defaults than to make the parameters mandatory because in some
|
||||
// cases they will never be used (e.g., "authorizing" in out-of-box server-side Blazor)
|
||||
private static readonly RenderFragment<AuthenticationState> _defaultNotAuthorizedContent
|
||||
= state => builder => builder.AddContent(0, "Not authorized");
|
||||
private static readonly RenderFragment _defaultAuthorizingContent
|
||||
= builder => builder.AddContent(0, "Authorizing...");
|
||||
|
||||
private readonly RenderFragment _renderAuthorizeRouteViewCoreDelegate;
|
||||
private readonly RenderFragment<AuthenticationState> _renderAuthorizedDelegate;
|
||||
private readonly RenderFragment<AuthenticationState> _renderNotAuthorizedDelegate;
|
||||
private readonly RenderFragment _renderAuthorizingDelegate;
|
||||
|
||||
public AuthorizeRouteView()
|
||||
{
|
||||
// Cache the rendering delegates so that we only construct new closure instances
|
||||
// when they are actually used (e.g., we never prepare a RenderFragment bound to
|
||||
// the NotAuthorized content except when you are displaying that particular state)
|
||||
RenderFragment renderBaseRouteViewDelegate = builder => base.Render(builder);
|
||||
_renderAuthorizedDelegate = authenticateState => renderBaseRouteViewDelegate;
|
||||
_renderNotAuthorizedDelegate = authenticationState => builder => RenderNotAuthorizedInDefaultLayout(builder, authenticationState);
|
||||
_renderAuthorizingDelegate = RenderAuthorizingInDefaultLayout;
|
||||
_renderAuthorizeRouteViewCoreDelegate = RenderAuthorizeRouteViewCore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The content that will be displayed if the user is not authorized.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment<AuthenticationState> NotAuthorized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content that will be displayed while asynchronous authorization is in progress.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment Authorizing { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
private Task<AuthenticationState> ExistingCascadedAuthenticationState { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Render(RenderTreeBuilder builder)
|
||||
{
|
||||
if (ExistingCascadedAuthenticationState != null)
|
||||
{
|
||||
// If this component is already wrapped in a <CascadingAuthenticationState> (or another
|
||||
// compatible provider), then don't interfere with the cascaded authentication state.
|
||||
_renderAuthorizeRouteViewCoreDelegate(builder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, implicitly wrap the output in a <CascadingAuthenticationState>
|
||||
builder.OpenComponent<CascadingAuthenticationState>(0);
|
||||
builder.AddAttribute(1, nameof(CascadingAuthenticationState.ChildContent), _renderAuthorizeRouteViewCoreDelegate);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderAuthorizeRouteViewCore(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<AuthorizeRouteViewCore>(0);
|
||||
builder.AddAttribute(1, nameof(AuthorizeRouteViewCore.RouteData), RouteData);
|
||||
builder.AddAttribute(2, nameof(AuthorizeRouteViewCore.Authorized), _renderAuthorizedDelegate);
|
||||
builder.AddAttribute(3, nameof(AuthorizeRouteViewCore.Authorizing), _renderAuthorizingDelegate);
|
||||
builder.AddAttribute(4, nameof(AuthorizeRouteViewCore.NotAuthorized), _renderNotAuthorizedDelegate);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderContentInDefaultLayout(RenderTreeBuilder builder, RenderFragment content)
|
||||
{
|
||||
builder.OpenComponent<LayoutView>(0);
|
||||
builder.AddAttribute(1, nameof(LayoutView.Layout), DefaultLayout);
|
||||
builder.AddAttribute(2, nameof(LayoutView.ChildContent), content);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderNotAuthorizedInDefaultLayout(RenderTreeBuilder builder, AuthenticationState authenticationState)
|
||||
{
|
||||
var content = NotAuthorized ?? _defaultNotAuthorizedContent;
|
||||
RenderContentInDefaultLayout(builder, content(authenticationState));
|
||||
}
|
||||
|
||||
private void RenderAuthorizingInDefaultLayout(RenderTreeBuilder builder)
|
||||
{
|
||||
var content = Authorizing ?? _defaultAuthorizingContent;
|
||||
RenderContentInDefaultLayout(builder, content);
|
||||
}
|
||||
|
||||
private class AuthorizeRouteViewCore : AuthorizeViewCore
|
||||
{
|
||||
[Parameter]
|
||||
public RouteData RouteData { get; set; }
|
||||
|
||||
protected override IAuthorizeData[] GetAuthorizeData()
|
||||
=> AttributeAuthorizeDataCache.GetAuthorizeDataForType(RouteData.PageType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,8 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// <inheritdoc />
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
// We're using the same sequence number for each of the content items here
|
||||
// so that we can update existing instances if they are the same shape
|
||||
if (currentAuthenticationState == null)
|
||||
{
|
||||
builder.AddContent(0, Authorizing);
|
||||
|
|
@ -59,11 +61,11 @@ namespace Microsoft.AspNetCore.Components
|
|||
else if (isAuthorized)
|
||||
{
|
||||
var authorized = Authorized ?? ChildContent;
|
||||
builder.AddContent(1, authorized?.Invoke(currentAuthenticationState));
|
||||
builder.AddContent(0, authorized?.Invoke(currentAuthenticationState));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddContent(2, NotAuthorized?.Invoke(currentAuthenticationState));
|
||||
builder.AddContent(0, NotAuthorized?.Invoke(currentAuthenticationState));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +104,12 @@ namespace Microsoft.AspNetCore.Components
|
|||
private async Task<bool> IsAuthorizedAsync(ClaimsPrincipal user)
|
||||
{
|
||||
var authorizeData = GetAuthorizeData();
|
||||
if (authorizeData == null)
|
||||
{
|
||||
// No authorization applies, so no need to consult the authorization service
|
||||
return true;
|
||||
}
|
||||
|
||||
EnsureNoAuthenticationSchemeSpecified(authorizeData);
|
||||
|
||||
var policy = await AuthorizationPolicy.CombineAsync(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
// 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.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays the specified content inside the specified layout and any further
|
||||
/// nested layouts.
|
||||
/// </summary>
|
||||
public class LayoutView : IComponent
|
||||
{
|
||||
private static readonly RenderFragment EmptyRenderFragment = builder => { };
|
||||
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content to display.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the layout in which to display the content.
|
||||
/// The type must implement <see cref="IComponent"/> and accept a parameter named <see cref="LayoutComponentBase.Body"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Type Layout { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
// In the middle goes the supplied content
|
||||
var fragment = ChildContent ?? EmptyRenderFragment;
|
||||
|
||||
// Then repeatedly wrap that in each layer of nested layout until we get
|
||||
// to a layout that has no parent
|
||||
var layoutType = Layout;
|
||||
while (layoutType != null)
|
||||
{
|
||||
fragment = WrapInLayout(layoutType, fragment);
|
||||
layoutType = GetParentLayoutType(layoutType);
|
||||
}
|
||||
|
||||
_renderHandle.Render(fragment);
|
||||
}
|
||||
|
||||
private static RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam)
|
||||
{
|
||||
return builder =>
|
||||
{
|
||||
builder.OpenComponent(0, layoutType);
|
||||
builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
}
|
||||
|
||||
private static Type GetParentLayoutType(Type type)
|
||||
=> type.GetCustomAttribute<LayoutAttribute>()?.LayoutType;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Auth;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays the specified page component, rendering it inside its layout
|
||||
/// and any further nested layouts, plus applying any authorization rules.
|
||||
/// </summary>
|
||||
public class PageDisplay : IComponent
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the page component to display.
|
||||
/// The type must implement <see cref="IComponent"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Type Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameters to pass to the page.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IDictionary<string, object> PageParameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content that will be displayed if the user is not authorized.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment<AuthenticationState> NotAuthorized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content that will be displayed while asynchronous authorization is in progress.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment Authorizing { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
// In the middle goes the requested page
|
||||
var fragment = (RenderFragment)RenderPageWithParameters;
|
||||
|
||||
// Around that goes an AuthorizeViewCore
|
||||
fragment = WrapInAuthorizeViewCore(fragment);
|
||||
|
||||
// Then repeatedly wrap that in each layer of nested layout until we get
|
||||
// to a layout that has no parent
|
||||
Type layoutType = Page;
|
||||
while ((layoutType = GetLayoutType(layoutType)) != null)
|
||||
{
|
||||
fragment = WrapInLayout(layoutType, fragment);
|
||||
}
|
||||
|
||||
_renderHandle.Render(fragment);
|
||||
}
|
||||
|
||||
private RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) => builder =>
|
||||
{
|
||||
builder.OpenComponent(0, layoutType);
|
||||
builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
|
||||
private void RenderPageWithParameters(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent(0, Page);
|
||||
|
||||
if (PageParameters != null)
|
||||
{
|
||||
foreach (var kvp in PageParameters)
|
||||
{
|
||||
builder.AddAttribute(1, kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private RenderFragment WrapInAuthorizeViewCore(RenderFragment pageFragment)
|
||||
{
|
||||
var authorizeData = AttributeAuthorizeDataCache.GetAuthorizeDataForType(Page);
|
||||
if (authorizeData == null)
|
||||
{
|
||||
// No authorization, so no need to wrap the fragment
|
||||
return pageFragment;
|
||||
}
|
||||
|
||||
// Some authorization data exists, so we do need to wrap the fragment
|
||||
RenderFragment<AuthenticationState> authorized = context => pageFragment;
|
||||
return builder =>
|
||||
{
|
||||
builder.OpenComponent<AuthorizeViewWithSuppliedData>(0);
|
||||
builder.AddAttribute(1, nameof(AuthorizeViewWithSuppliedData.AuthorizeDataParam), authorizeData);
|
||||
builder.AddAttribute(2, nameof(AuthorizeViewWithSuppliedData.Authorized), authorized);
|
||||
builder.AddAttribute(3, nameof(AuthorizeViewWithSuppliedData.NotAuthorized), NotAuthorized ?? DefaultNotAuthorized);
|
||||
builder.AddAttribute(4, nameof(AuthorizeViewWithSuppliedData.Authorizing), Authorizing);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
}
|
||||
|
||||
private static Type GetLayoutType(Type type)
|
||||
=> type.GetCustomAttribute<LayoutAttribute>()?.LayoutType;
|
||||
|
||||
private class AuthorizeViewWithSuppliedData : AuthorizeViewCore
|
||||
{
|
||||
[Parameter] public IAuthorizeData[] AuthorizeDataParam { get; private set; }
|
||||
|
||||
protected override IAuthorizeData[] GetAuthorizeData() => AuthorizeDataParam;
|
||||
}
|
||||
|
||||
// There has to be some default content. If we render blank by default, developers
|
||||
// will find it hard to guess why their UI isn't appearing.
|
||||
private static RenderFragment DefaultNotAuthorized(AuthenticationState authenticationState)
|
||||
=> builder => builder.AddContent(0, "Not authorized");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// 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.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays the specified page component, rendering it inside its layout
|
||||
/// and any further nested layouts.
|
||||
/// </summary>
|
||||
public class RouteView : IComponent
|
||||
{
|
||||
private readonly RenderFragment _renderDelegate;
|
||||
private readonly RenderFragment _renderPageWithParametersDelegate;
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the route data. This determines the page that will be
|
||||
/// displayed and the parameter values that will be supplied to the page.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RouteData RouteData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of a layout to be used if the page does not
|
||||
/// declare any layout. If specified, the type must implement <see cref="IComponent"/>
|
||||
/// and accept a parameter named <see cref="LayoutComponentBase.Body"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Type DefaultLayout { get; set; }
|
||||
|
||||
public RouteView()
|
||||
{
|
||||
// Cache the delegate instances
|
||||
_renderDelegate = Render;
|
||||
_renderPageWithParametersDelegate = RenderPageWithParameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
if (RouteData == null)
|
||||
{
|
||||
throw new InvalidOperationException($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteData)}.");
|
||||
}
|
||||
|
||||
_renderHandle.Render(_renderDelegate);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the component.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="RenderTreeBuilder"/>.</param>
|
||||
protected virtual void Render(RenderTreeBuilder builder)
|
||||
{
|
||||
var pageLayoutType = RouteData.PageType.GetCustomAttribute<LayoutAttribute>()?.LayoutType
|
||||
?? DefaultLayout;
|
||||
|
||||
builder.OpenComponent<LayoutView>(0);
|
||||
builder.AddAttribute(1, nameof(LayoutView.Layout), pageLayoutType);
|
||||
builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderPageWithParametersDelegate);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderPageWithParameters(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent(0, RouteData.PageType);
|
||||
|
||||
foreach (var kvp in RouteData.RouteValues)
|
||||
{
|
||||
builder.AddAttribute(1, kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,6 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
|
||||
public Type Handler { get; set; }
|
||||
|
||||
public IDictionary<string, object> Parameters { get; set; }
|
||||
public IReadOnlyDictionary<string, object> Parameters { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes information determined during routing that specifies
|
||||
/// the page to be displayed.
|
||||
/// </summary>
|
||||
public sealed class RouteData
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="RouteData"/>.
|
||||
/// </summary>
|
||||
/// <param name="pageType">The type of the page matching the route, which must implement <see cref="IComponent"/>.</param>
|
||||
/// <param name="routeValues">The route parameter values extracted from the matched route.</param>
|
||||
public RouteData(Type pageType, IReadOnlyDictionary<string, object> routeValues)
|
||||
{
|
||||
if (pageType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pageType));
|
||||
}
|
||||
|
||||
if (!typeof(IComponent).IsAssignableFrom(pageType))
|
||||
{
|
||||
throw new ArgumentException($"The value must implement {nameof(IComponent)}.", nameof(pageType));
|
||||
}
|
||||
|
||||
PageType = pageType;
|
||||
RouteValues = routeValues ?? throw new ArgumentNullException(nameof(routeValues));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the page matching the route.
|
||||
/// </summary>
|
||||
public Type PageType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets route parameter values extracted from the matched route.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> RouteValues { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,20 +3,21 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// A component that displays whichever other component corresponds to the
|
||||
/// current navigation location.
|
||||
/// A component that supplies route data corresponding to the current navigation state.
|
||||
/// </summary>
|
||||
public class Router : IComponent, IHandleAfterRender, IDisposable
|
||||
{
|
||||
static readonly char[] _queryOrHashStartChar = new[] { '?', '#' };
|
||||
static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
|
||||
= new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
|
||||
|
||||
RenderHandle _renderHandle;
|
||||
string _baseUri;
|
||||
|
|
@ -33,25 +34,19 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
[Inject] private ILoggerFactory LoggerFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the assembly that should be searched, along with its referenced
|
||||
/// assemblies, for components matching the URI.
|
||||
/// Gets or sets the assembly that should be searched for components matching the URI.
|
||||
/// </summary>
|
||||
[Parameter] public Assembly AppAssembly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the component that should be used as a fallback when no match is found for the requested route.
|
||||
/// Gets or sets the content to display when no match is found for the requested route.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment NotFound { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content that will be displayed if the user is not authorized.
|
||||
/// Gets or sets the content to display when a match is found for the requested route.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment<AuthenticationState> NotAuthorized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content that will be displayed while asynchronous authorization is in progress.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment Authorizing { get; set; }
|
||||
[Parameter] public RenderFragment<RouteData> Found { get; set; }
|
||||
|
||||
private RouteTable Routes { get; set; }
|
||||
|
||||
|
|
@ -69,6 +64,22 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
// Found content is mandatory, because even though we could use something like <RouteView ...> as a
|
||||
// reasonable default, if it's not declared explicitly in the template then people will have no way
|
||||
// to discover how to customize this (e.g., to add authorization).
|
||||
if (Found == null)
|
||||
{
|
||||
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}.");
|
||||
}
|
||||
|
||||
// NotFound content is mandatory, because even though we could display a default message like "Not found",
|
||||
// it has to be specified explicitly so that it can also be wrapped in a specific layout
|
||||
if (NotFound == null)
|
||||
{
|
||||
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
|
||||
}
|
||||
|
||||
Routes = RouteTableFactory.Create(AppAssembly);
|
||||
Refresh(isNavigationIntercepted: false);
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -80,7 +91,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
|
||||
private string StringUntilAny(string str, char[] chars)
|
||||
private static string StringUntilAny(string str, char[] chars)
|
||||
{
|
||||
var firstIndex = str.IndexOfAny(chars);
|
||||
return firstIndex < 0
|
||||
|
|
@ -88,17 +99,6 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
: str.Substring(0, firstIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary<string, object> parameters)
|
||||
{
|
||||
builder.OpenComponent(0, typeof(PageDisplay));
|
||||
builder.AddAttribute(1, nameof(PageDisplay.Page), handler);
|
||||
builder.AddAttribute(2, nameof(PageDisplay.PageParameters), parameters);
|
||||
builder.AddAttribute(3, nameof(PageDisplay.NotAuthorized), NotAuthorized);
|
||||
builder.AddAttribute(4, nameof(PageDisplay.Authorizing), Authorizing);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void Refresh(bool isNavigationIntercepted)
|
||||
{
|
||||
var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
|
||||
|
|
@ -116,16 +116,19 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
|
||||
Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri);
|
||||
|
||||
_renderHandle.Render(builder => Render(builder, context.Handler, context.Parameters));
|
||||
var routeData = new RouteData(
|
||||
context.Handler,
|
||||
context.Parameters ?? _emptyParametersDictionary);
|
||||
_renderHandle.Render(Found(routeData));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isNavigationIntercepted && NotFound != null)
|
||||
if (!isNavigationIntercepted)
|
||||
{
|
||||
Log.DisplayingNotFound(_logger, locationPath, _baseUri);
|
||||
|
||||
// We did not find a Component that matches the route.
|
||||
// Only show the NotFound if the application developer programatically got us here i.e we did not
|
||||
// Only show the NotFound content if the application developer programatically got us here i.e we did not
|
||||
// intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content.
|
||||
_renderHandle.Render(NotFound);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,355 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
public class AuthorizeRouteViewTest
|
||||
{
|
||||
private readonly static IReadOnlyDictionary<string, object> EmptyParametersDictionary = new Dictionary<string, object>();
|
||||
private readonly TestAuthenticationStateProvider _authenticationStateProvider;
|
||||
private readonly TestRenderer _renderer;
|
||||
private readonly RouteView _authorizeRouteViewComponent;
|
||||
private readonly int _authorizeRouteViewComponentId;
|
||||
private readonly TestAuthorizationService _testAuthorizationService;
|
||||
|
||||
public AuthorizeRouteViewTest()
|
||||
{
|
||||
_authenticationStateProvider = new TestAuthenticationStateProvider();
|
||||
_authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(
|
||||
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
|
||||
|
||||
_testAuthorizationService = new TestAuthorizationService();
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddSingleton<AuthenticationStateProvider>(_authenticationStateProvider);
|
||||
serviceCollection.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();
|
||||
serviceCollection.AddSingleton<IAuthorizationService>(_testAuthorizationService);
|
||||
|
||||
_renderer = new TestRenderer(serviceCollection.BuildServiceProvider());
|
||||
_authorizeRouteViewComponent = new AuthorizeRouteView();
|
||||
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhenAuthorized_RendersPageInsideLayout()
|
||||
{
|
||||
// Arrange
|
||||
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(TestPageRequiringAuthorization.Message), "Hello, world!" }
|
||||
});
|
||||
_testAuthorizationService.NextResult = AuthorizationResult.Success();
|
||||
|
||||
// Act
|
||||
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(AuthorizeRouteView.RouteData), routeData },
|
||||
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
|
||||
}));
|
||||
|
||||
// Assert: renders layout
|
||||
var batch = _renderer.Batches.Single();
|
||||
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
|
||||
Assert.Collection(layoutDiff.Edits,
|
||||
edit => AssertPrependText(batch, edit, "Layout starts here"),
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
AssertFrame.Component<TestPageRequiringAuthorization>(batch.ReferenceFrames[edit.ReferenceFrameIndex]);
|
||||
},
|
||||
edit => AssertPrependText(batch, edit, "Layout ends here"));
|
||||
|
||||
// Assert: renders page
|
||||
var pageDiff = batch.GetComponentDiffs<TestPageRequiringAuthorization>().Single();
|
||||
Assert.Collection(pageDiff.Edits,
|
||||
edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhenNotAuthorized_RendersDefaultNotAuthorizedContentInsideLayout()
|
||||
{
|
||||
// Arrange
|
||||
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
|
||||
_testAuthorizationService.NextResult = AuthorizationResult.Failed();
|
||||
|
||||
// Act
|
||||
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(AuthorizeRouteView.RouteData), routeData },
|
||||
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
|
||||
}));
|
||||
|
||||
// Assert: renders layout containing "not authorized" message
|
||||
var batch = _renderer.Batches.Single();
|
||||
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
|
||||
Assert.Collection(layoutDiff.Edits,
|
||||
edit => AssertPrependText(batch, edit, "Layout starts here"),
|
||||
edit => AssertPrependText(batch, edit, "Not authorized"),
|
||||
edit => AssertPrependText(batch, edit, "Layout ends here"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhenNotAuthorized_RendersCustomNotAuthorizedContentInsideLayout()
|
||||
{
|
||||
// Arrange
|
||||
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
|
||||
_testAuthorizationService.NextResult = AuthorizationResult.Failed();
|
||||
_authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(new AuthenticationState(
|
||||
new ClaimsPrincipal(new TestIdentity { Name = "Bert" })));
|
||||
|
||||
// Act
|
||||
RenderFragment<AuthenticationState> customNotAuthorized =
|
||||
state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");
|
||||
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(AuthorizeRouteView.RouteData), routeData },
|
||||
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
|
||||
{ nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized },
|
||||
}));
|
||||
|
||||
// Assert: renders layout containing "not authorized" message
|
||||
var batch = _renderer.Batches.Single();
|
||||
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
|
||||
Assert.Collection(layoutDiff.Edits,
|
||||
edit => AssertPrependText(batch, edit, "Layout starts here"),
|
||||
edit => AssertPrependText(batch, edit, "Go away, Bert"),
|
||||
edit => AssertPrependText(batch, edit, "Layout ends here"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenAuthorizing_RendersDefaultAuthorizingContentInsideLayout()
|
||||
{
|
||||
// Arrange
|
||||
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
|
||||
var authStateTcs = new TaskCompletionSource<AuthenticationState>();
|
||||
_authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task;
|
||||
RenderFragment<AuthenticationState> customNotAuthorized =
|
||||
state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");
|
||||
|
||||
// Act
|
||||
var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(AuthorizeRouteView.RouteData), routeData },
|
||||
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
|
||||
{ nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized },
|
||||
}));
|
||||
|
||||
// Assert: renders layout containing "authorizing" message
|
||||
Assert.False(firstRenderTask.IsCompleted);
|
||||
var batch = _renderer.Batches.Single();
|
||||
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
|
||||
Assert.Collection(layoutDiff.Edits,
|
||||
edit => AssertPrependText(batch, edit, "Layout starts here"),
|
||||
edit => AssertPrependText(batch, edit, "Authorizing..."),
|
||||
edit => AssertPrependText(batch, edit, "Layout ends here"));
|
||||
|
||||
// Act 2: updates when authorization completes
|
||||
authStateTcs.SetResult(new AuthenticationState(
|
||||
new ClaimsPrincipal(new TestIdentity { Name = "Bert" })));
|
||||
await firstRenderTask;
|
||||
|
||||
// Assert 2: Only the layout is updated
|
||||
batch = _renderer.Batches.Skip(1).Single();
|
||||
var nonEmptyDiff = batch.DiffsInOrder.Where(d => d.Edits.Any()).Single();
|
||||
Assert.Equal(layoutDiff.ComponentId, nonEmptyDiff.ComponentId);
|
||||
Assert.Collection(nonEmptyDiff.Edits, edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
AssertFrame.Text(batch.ReferenceFrames[edit.ReferenceFrameIndex], "Go away, Bert");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhenAuthorizing_RendersCustomAuthorizingContentInsideLayout()
|
||||
{
|
||||
// Arrange
|
||||
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
|
||||
var authStateTcs = new TaskCompletionSource<AuthenticationState>();
|
||||
_authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task;
|
||||
RenderFragment customAuthorizing =
|
||||
builder => builder.AddContent(0, "Hold on, we're checking your papers.");
|
||||
|
||||
// Act
|
||||
var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(AuthorizeRouteView.RouteData), routeData },
|
||||
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
|
||||
{ nameof(AuthorizeRouteView.Authorizing), customAuthorizing },
|
||||
}));
|
||||
|
||||
// Assert: renders layout containing "authorizing" message
|
||||
Assert.False(firstRenderTask.IsCompleted);
|
||||
var batch = _renderer.Batches.Single();
|
||||
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
|
||||
Assert.Collection(layoutDiff.Edits,
|
||||
edit => AssertPrependText(batch, edit, "Layout starts here"),
|
||||
edit => AssertPrependText(batch, edit, "Hold on, we're checking your papers."),
|
||||
edit => AssertPrependText(batch, edit, "Layout ends here"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithoutCascadedAuthenticationState_WrapsOutputInCascadingAuthenticationState()
|
||||
{
|
||||
// Arrange/Act
|
||||
var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
|
||||
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(AuthorizeRouteView.RouteData), routeData }
|
||||
}));
|
||||
|
||||
// Assert
|
||||
var batch = _renderer.Batches.Single();
|
||||
var componentInstances = batch.ReferenceFrames
|
||||
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
||||
.Select(f => f.Component);
|
||||
|
||||
Assert.Collection(componentInstances,
|
||||
// This is the hierarchy inside the AuthorizeRouteView, which contains its
|
||||
// own CascadingAuthenticationState
|
||||
component => Assert.IsType<CascadingAuthenticationState>(component),
|
||||
component => Assert.IsType<CascadingValue<Task<AuthenticationState>>>(component),
|
||||
component => Assert.IsAssignableFrom<AuthorizeViewCore>(component),
|
||||
component => Assert.IsType<LayoutView>(component),
|
||||
component => Assert.IsType<TestPageWithNoAuthorization>(component));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithCascadedAuthenticationState_DoesNotWrapOutputInCascadingAuthenticationState()
|
||||
{
|
||||
// Arrange
|
||||
var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
|
||||
var rootComponent = new AuthorizeRouteViewWithExistingCascadedAuthenticationState(
|
||||
_authenticationStateProvider.CurrentAuthStateTask,
|
||||
routeData);
|
||||
var rootComponentId = _renderer.AssignRootComponentId(rootComponent);
|
||||
|
||||
// Act
|
||||
_renderer.RenderRootComponent(rootComponentId);
|
||||
|
||||
// Assert
|
||||
var batch = _renderer.Batches.Single();
|
||||
var componentInstances = batch.ReferenceFrames
|
||||
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
||||
.Select(f => f.Component);
|
||||
|
||||
Assert.Collection(componentInstances,
|
||||
// This is the externally-supplied cascading value
|
||||
component => Assert.IsType<CascadingValue<Task<AuthenticationState>>>(component),
|
||||
component => Assert.IsType<AuthorizeRouteView>(component),
|
||||
|
||||
// This is the hierarchy inside the AuthorizeRouteView. It doesn't contain a
|
||||
// further CascadingAuthenticationState
|
||||
component => Assert.IsAssignableFrom<AuthorizeViewCore>(component),
|
||||
component => Assert.IsType<LayoutView>(component),
|
||||
component => Assert.IsType<TestPageWithNoAuthorization>(component));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatesOutputWhenRouteDataChanges()
|
||||
{
|
||||
// Arrange/Act 1: Start on some route
|
||||
// Not asserting about the initial output, as that is covered by other tests
|
||||
var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary);
|
||||
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(AuthorizeRouteView.RouteData), routeData },
|
||||
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
|
||||
}));
|
||||
|
||||
// Act 2: Move to another route
|
||||
var routeData2 = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
|
||||
var render2Task = _renderer.Dispatcher.InvokeAsync(() => _authorizeRouteViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(AuthorizeRouteView.RouteData), routeData2 },
|
||||
})));
|
||||
|
||||
// Assert: we retain the layout instance, and mutate its contents
|
||||
Assert.True(render2Task.IsCompletedSuccessfully);
|
||||
Assert.Equal(2, _renderer.Batches.Count);
|
||||
var batch2 = _renderer.Batches[1];
|
||||
var diff = batch2.DiffsInOrder.Where(d => d.Edits.Any()).Single();
|
||||
Assert.Collection(diff.Edits,
|
||||
edit =>
|
||||
{
|
||||
// Inside the layout, we add the new content
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
AssertFrame.Text(batch2.ReferenceFrames[edit.ReferenceFrameIndex], "Not authorized");
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
// ... and remove the old content
|
||||
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
|
||||
Assert.Equal(2, edit.SiblingIndex);
|
||||
});
|
||||
}
|
||||
|
||||
private static void AssertPrependText(CapturedBatch batch, RenderTreeEdit edit, string text)
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
ref var referenceFrame = ref batch.ReferenceFrames[edit.ReferenceFrameIndex];
|
||||
AssertFrame.Text(referenceFrame, text);
|
||||
}
|
||||
|
||||
class TestPageWithNoAuthorization : ComponentBase { }
|
||||
|
||||
[Authorize]
|
||||
class TestPageRequiringAuthorization : ComponentBase
|
||||
{
|
||||
[Parameter] public string Message { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, $"Hello from the page with message: {Message}");
|
||||
}
|
||||
}
|
||||
|
||||
class TestLayout : LayoutComponentBase
|
||||
{
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, "Layout starts here");
|
||||
builder.AddContent(1, Body);
|
||||
builder.AddContent(2, "Layout ends here");
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorizeRouteViewWithExistingCascadedAuthenticationState : AutoRenderComponent
|
||||
{
|
||||
private readonly Task<AuthenticationState> _authenticationState;
|
||||
private readonly RouteData _routeData;
|
||||
|
||||
public AuthorizeRouteViewWithExistingCascadedAuthenticationState(
|
||||
Task<AuthenticationState> authenticationState,
|
||||
RouteData routeData)
|
||||
{
|
||||
_authenticationState = authenticationState;
|
||||
_routeData = routeData;
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
|
||||
builder.AddAttribute(1, nameof(CascadingValue<object>.Value), _authenticationState);
|
||||
builder.AddAttribute(2, nameof(CascadingValue<object>.ChildContent), (RenderFragment)(builder =>
|
||||
{
|
||||
builder.OpenComponent<AuthorizeRouteView>(0);
|
||||
builder.AddAttribute(1, nameof(AuthorizeRouteView.RouteData), _routeData);
|
||||
builder.CloseComponent();
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
|
|
@ -331,15 +330,9 @@ namespace Microsoft.AspNetCore.Components
|
|||
Assert.Equal(2, renderer.Batches.Count);
|
||||
var batch2 = renderer.Batches[1];
|
||||
var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();
|
||||
Assert.Collection(diff2.Edits,
|
||||
edit =>
|
||||
Assert.Collection(diff2.Edits, edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
|
||||
Assert.Equal(0, edit.SiblingIndex);
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
||||
Assert.Equal(0, edit.SiblingIndex);
|
||||
AssertFrame.Text(
|
||||
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
|
|
@ -513,15 +506,6 @@ namespace Microsoft.AspNetCore.Components
|
|||
=> Task.FromResult(new AuthenticationState(
|
||||
new ClaimsPrincipal(new TestIdentity { Name = username })));
|
||||
|
||||
class TestIdentity : IIdentity
|
||||
{
|
||||
public string AuthenticationType => "Test";
|
||||
|
||||
public bool IsAuthenticated => true;
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public TestRenderer CreateTestRenderer(IAuthorizationService authorizationService)
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
|
|
@ -530,52 +514,6 @@ namespace Microsoft.AspNetCore.Components
|
|||
return new TestRenderer(serviceCollection.BuildServiceProvider());
|
||||
}
|
||||
|
||||
private class TestAuthorizationService : IAuthorizationService
|
||||
{
|
||||
public AuthorizationResult NextResult { get; set; }
|
||||
= AuthorizationResult.Failed();
|
||||
|
||||
public List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)> AuthorizeCalls { get; }
|
||||
= new List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)>();
|
||||
|
||||
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
|
||||
{
|
||||
AuthorizeCalls.Add((user, resource, requirements));
|
||||
|
||||
// The TestAuthorizationService doesn't actually apply any authorization requirements
|
||||
// It just returns the specified NextResult, since we're not trying to test the logic
|
||||
// in DefaultAuthorizationService or similar here. So it's up to tests to set a desired
|
||||
// NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls.
|
||||
return Task.FromResult(NextResult);
|
||||
}
|
||||
|
||||
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider
|
||||
{
|
||||
private readonly AuthorizationOptions options = new AuthorizationOptions();
|
||||
|
||||
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
|
||||
=> Task.FromResult(options.DefaultPolicy);
|
||||
|
||||
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
|
||||
=> Task.FromResult(options.FallbackPolicy);
|
||||
|
||||
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) => Task.FromResult(
|
||||
new AuthorizationPolicy(new[]
|
||||
{
|
||||
new TestPolicyRequirement { PolicyName = policyName }
|
||||
},
|
||||
new[] { $"TestScheme:{policyName}" }));
|
||||
}
|
||||
|
||||
public class TestPolicyRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public string PolicyName { get; set; }
|
||||
}
|
||||
|
||||
public class AuthorizeViewCoreWithScheme : AuthorizeViewCore
|
||||
{
|
||||
protected override IAuthorizeData[] GetAuthorizeData()
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange: Service
|
||||
var services = new ServiceCollection();
|
||||
var authStateProvider = new TestAuthStateProvider()
|
||||
var authStateProvider = new TestAuthenticationStateProvider()
|
||||
{
|
||||
CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState("Bert"))
|
||||
};
|
||||
|
|
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
// Arrange: Service
|
||||
var services = new ServiceCollection();
|
||||
var authStateTaskCompletionSource = new TaskCompletionSource<AuthenticationState>();
|
||||
var authStateProvider = new TestAuthStateProvider()
|
||||
var authStateProvider = new TestAuthenticationStateProvider()
|
||||
{
|
||||
CurrentAuthStateTask = authStateTaskCompletionSource.Task
|
||||
};
|
||||
|
|
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange: Service
|
||||
var services = new ServiceCollection();
|
||||
var authStateProvider = new TestAuthStateProvider()
|
||||
var authStateProvider = new TestAuthenticationStateProvider()
|
||||
{
|
||||
CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState(null))
|
||||
};
|
||||
|
|
@ -189,21 +189,6 @@ namespace Microsoft.AspNetCore.Components
|
|||
}
|
||||
}
|
||||
|
||||
class TestAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
public Task<AuthenticationState> CurrentAuthStateTask { get; set; }
|
||||
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
return CurrentAuthStateTask;
|
||||
}
|
||||
|
||||
internal void TriggerAuthenticationStateChanged(Task<AuthenticationState> authState)
|
||||
{
|
||||
NotifyAuthenticationStateChanged(authState);
|
||||
}
|
||||
}
|
||||
|
||||
public static AuthenticationState CreateAuthenticationState(string username)
|
||||
=> new AuthenticationState(new ClaimsPrincipal(username == null
|
||||
? new ClaimsIdentity()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
// 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.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
public class TestAuthenticationStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
public Task<AuthenticationState> CurrentAuthStateTask { get; set; }
|
||||
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
return CurrentAuthStateTask;
|
||||
}
|
||||
|
||||
internal void TriggerAuthenticationStateChanged(Task<AuthenticationState> authState)
|
||||
{
|
||||
NotifyAuthenticationStateChanged(authState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
public class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider
|
||||
{
|
||||
private readonly AuthorizationOptions options = new AuthorizationOptions();
|
||||
|
||||
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
|
||||
=> Task.FromResult(options.DefaultPolicy);
|
||||
|
||||
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
|
||||
=> Task.FromResult(options.FallbackPolicy);
|
||||
|
||||
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) => Task.FromResult(
|
||||
new AuthorizationPolicy(new[]
|
||||
{
|
||||
new TestPolicyRequirement { PolicyName = policyName }
|
||||
},
|
||||
new[] { $"TestScheme:{policyName}" }));
|
||||
}
|
||||
|
||||
public class TestPolicyRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public string PolicyName { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
public class TestAuthorizationService : IAuthorizationService
|
||||
{
|
||||
public AuthorizationResult NextResult { get; set; }
|
||||
= AuthorizationResult.Failed();
|
||||
|
||||
public List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)> AuthorizeCalls { get; }
|
||||
= new List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)>();
|
||||
|
||||
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
|
||||
{
|
||||
AuthorizeCalls.Add((user, resource, requirements));
|
||||
|
||||
// The TestAuthorizationService doesn't actually apply any authorization requirements
|
||||
// It just returns the specified NextResult, since we're not trying to test the logic
|
||||
// in DefaultAuthorizationService or similar here. So it's up to tests to set a desired
|
||||
// NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls.
|
||||
return Task.FromResult(NextResult);
|
||||
}
|
||||
|
||||
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.Security.Principal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
public class TestIdentity : IIdentity
|
||||
{
|
||||
public string AuthenticationType => "Test";
|
||||
|
||||
public bool IsAuthenticated => true;
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test
|
||||
{
|
||||
public class LayoutViewTest
|
||||
{
|
||||
private readonly TestRenderer _renderer;
|
||||
private readonly LayoutView _layoutViewComponent;
|
||||
private readonly int _layoutViewComponentId;
|
||||
|
||||
public LayoutViewTest()
|
||||
{
|
||||
_renderer = new TestRenderer();
|
||||
_layoutViewComponent = new LayoutView();
|
||||
_layoutViewComponentId = _renderer.AssignRootComponentId(_layoutViewComponent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenNoParameters_RendersNothing()
|
||||
{
|
||||
// Arrange/Act
|
||||
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.Empty));
|
||||
Assert.True(setParametersTask.IsCompletedSuccessfully);
|
||||
var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
|
||||
|
||||
// Assert
|
||||
Assert.Single(_renderer.Batches);
|
||||
Assert.Empty(frames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenContentButNoLayout_RendersContent()
|
||||
{
|
||||
// Arrange/Act
|
||||
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
|
||||
builder.AddContent(123, "Hello");
|
||||
builder.AddContent(456, "Goodbye");
|
||||
})}
|
||||
})));
|
||||
Assert.True(setParametersTask.IsCompletedSuccessfully);
|
||||
var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
|
||||
|
||||
// Assert
|
||||
Assert.Single(_renderer.Batches);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Hello", 123),
|
||||
frame => AssertFrame.Text(frame, "Goodbye", 456));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenLayoutButNoContent_RendersLayoutWithEmptyBody()
|
||||
{
|
||||
// Arrange/Act
|
||||
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutView.Layout), typeof(RootLayout) }
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.True(setParametersTask.IsCompletedSuccessfully);
|
||||
var batch = _renderer.Batches.Single();
|
||||
|
||||
var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
|
||||
Assert.Collection(layoutViewFrames,
|
||||
frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
|
||||
|
||||
var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
|
||||
var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
|
||||
Assert.Collection(rootLayoutFrames,
|
||||
frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
|
||||
frame => AssertFrame.Region(frame, subtreeLength: 1), // i.e., empty region
|
||||
frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersContentInsideLayout()
|
||||
{
|
||||
// Arrange/Act
|
||||
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutView.Layout), typeof(RootLayout) },
|
||||
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
|
||||
builder.AddContent(123, "Hello");
|
||||
builder.AddContent(456, "Goodbye");
|
||||
})}
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.True(setParametersTask.IsCompletedSuccessfully);
|
||||
var batch = _renderer.Batches.Single();
|
||||
|
||||
var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
|
||||
Assert.Collection(layoutViewFrames,
|
||||
frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
|
||||
|
||||
var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
|
||||
var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
|
||||
Assert.Collection(rootLayoutFrames,
|
||||
frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
|
||||
frame => AssertFrame.Region(frame, subtreeLength: 3),
|
||||
frame => AssertFrame.Text(frame, "Hello", sequence: 123),
|
||||
frame => AssertFrame.Text(frame, "Goodbye", sequence: 456),
|
||||
frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersContentInsideNestedLayout()
|
||||
{
|
||||
// Arrange/Act
|
||||
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutView.Layout), typeof(NestedLayout) },
|
||||
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
|
||||
builder.AddContent(123, "Hello");
|
||||
builder.AddContent(456, "Goodbye");
|
||||
})}
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.True(setParametersTask.IsCompletedSuccessfully);
|
||||
var batch = _renderer.Batches.Single();
|
||||
|
||||
var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
|
||||
Assert.Collection(layoutViewFrames,
|
||||
frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
|
||||
|
||||
var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
|
||||
var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
|
||||
Assert.Collection(rootLayoutFrames,
|
||||
frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
|
||||
frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1),
|
||||
frame => AssertFrame.Component<NestedLayout>(frame, subtreeLength: 2, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1),
|
||||
frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
|
||||
|
||||
var nestedLayoutComponentId = batch.GetComponentFrames<NestedLayout>().Single().ComponentId;
|
||||
var nestedLayoutFrames = _renderer.GetCurrentRenderTreeFrames(nestedLayoutComponentId).AsEnumerable();
|
||||
Assert.Collection(nestedLayoutFrames,
|
||||
frame => AssertFrame.Text(frame, "NestedLayout starts here", sequence: 0),
|
||||
frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1),
|
||||
frame => AssertFrame.Text(frame, "Hello", sequence: 123),
|
||||
frame => AssertFrame.Text(frame, "Goodbye", sequence: 456),
|
||||
frame => AssertFrame.Text(frame, "NestedLayout ends here", sequence: 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanChangeContentWithSameLayout()
|
||||
{
|
||||
// Arrange
|
||||
var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutView.Layout), typeof(NestedLayout) },
|
||||
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
|
||||
builder.AddContent(0, "Initial content");
|
||||
})}
|
||||
})));
|
||||
|
||||
// Act
|
||||
Assert.True(setParametersTask.IsCompletedSuccessfully);
|
||||
_renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutView.Layout), typeof(NestedLayout) },
|
||||
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
|
||||
builder.AddContent(0, "Changed content");
|
||||
})}
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, _renderer.Batches.Count);
|
||||
var batch = _renderer.Batches[1];
|
||||
Assert.Equal(0, batch.DisposedComponentIDs.Count);
|
||||
Assert.Collection(batch.DiffsInOrder,
|
||||
diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes
|
||||
diff => Assert.Empty(diff.Edits), // RootLayout rerendered, but with no changes
|
||||
diff =>
|
||||
{
|
||||
// NestedLayout rerendered, patching content in place
|
||||
Assert.Collection(diff.Edits, edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
"Changed content",
|
||||
sequence: 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanChangeLayout()
|
||||
{
|
||||
// Arrange
|
||||
var setParametersTask1 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutView.Layout), typeof(NestedLayout) },
|
||||
{ nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
|
||||
builder.AddContent(0, "Some content");
|
||||
})}
|
||||
})));
|
||||
Assert.True(setParametersTask1.IsCompletedSuccessfully);
|
||||
|
||||
// Act
|
||||
var setParametersTask2 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutView.Layout), typeof(OtherNestedLayout) },
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.True(setParametersTask2.IsCompletedSuccessfully);
|
||||
Assert.Equal(2, _renderer.Batches.Count);
|
||||
var batch = _renderer.Batches[1];
|
||||
Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposes NestedLayout
|
||||
Assert.Collection(batch.DiffsInOrder,
|
||||
diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes
|
||||
diff =>
|
||||
{
|
||||
// RootLayout rerendered, changing child
|
||||
Assert.Collection(diff.Edits,
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
AssertFrame.Component<OtherNestedLayout>(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
sequence: 0);
|
||||
});
|
||||
},
|
||||
diff =>
|
||||
{
|
||||
// Inserts new OtherNestedLayout
|
||||
Assert.Collection(diff.Edits,
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(0, edit.SiblingIndex);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
"OtherNestedLayout starts here");
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
"Some content");
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(2, edit.SiblingIndex);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
"OtherNestedLayout ends here");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private class RootLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
if (Body == null)
|
||||
{
|
||||
// Prove that we don't expect layouts to tolerate null values for Body
|
||||
throw new InvalidOperationException("Got a null body when not expecting it");
|
||||
}
|
||||
|
||||
builder.AddContent(0, "RootLayout starts here");
|
||||
builder.AddContent(1, Body);
|
||||
builder.AddContent(2, "RootLayout ends here");
|
||||
}
|
||||
}
|
||||
|
||||
[Layout(typeof(RootLayout))]
|
||||
private class NestedLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, "NestedLayout starts here");
|
||||
builder.AddContent(1, Body);
|
||||
builder.AddContent(2, "NestedLayout ends here");
|
||||
}
|
||||
}
|
||||
|
||||
[Layout(typeof(RootLayout))]
|
||||
private class OtherNestedLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, "OtherNestedLayout starts here");
|
||||
builder.AddContent(1, Body);
|
||||
builder.AddContent(2, "OtherNestedLayout ends here");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
// 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 Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test
|
||||
{
|
||||
public class PageDisplayTest
|
||||
{
|
||||
private TestRenderer _renderer = new TestRenderer();
|
||||
private PageDisplay _pageDisplayComponent = new PageDisplay();
|
||||
private int _pageDisplayComponentId;
|
||||
|
||||
public PageDisplayTest()
|
||||
{
|
||||
_renderer = new TestRenderer();
|
||||
_pageDisplayComponent = new PageDisplay();
|
||||
_pageDisplayComponentId = _renderer.AssignRootComponentId(_pageDisplayComponent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplaysComponentInsideLayout()
|
||||
{
|
||||
// Arrange/Act
|
||||
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
|
||||
})));
|
||||
|
||||
// Assert
|
||||
var batch = _renderer.Batches.Single();
|
||||
Assert.Collection(batch.DiffsInOrder,
|
||||
diff =>
|
||||
{
|
||||
// First is the LayoutDisplay component, which contains a RootLayout
|
||||
var singleEdit = diff.Edits.Single();
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type);
|
||||
AssertFrame.Component<RootLayout>(
|
||||
batch.ReferenceFrames[singleEdit.ReferenceFrameIndex]);
|
||||
},
|
||||
diff =>
|
||||
{
|
||||
// ... then a RootLayout which contains a ComponentWithLayout
|
||||
// First is the LayoutDisplay component, which contains a RootLayout
|
||||
Assert.Collection(diff.Edits,
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
"RootLayout starts here");
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
AssertFrame.Component<ComponentWithLayout>(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
"RootLayout ends here");
|
||||
});
|
||||
},
|
||||
diff =>
|
||||
{
|
||||
// ... then the ComponentWithLayout
|
||||
var singleEdit = diff.Edits.Single();
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[singleEdit.ReferenceFrameIndex],
|
||||
$"{nameof(ComponentWithLayout)} is here.");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplaysComponentInsideNestedLayout()
|
||||
{
|
||||
// Arrange/Act
|
||||
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) }
|
||||
})));
|
||||
|
||||
// Assert
|
||||
var batch = _renderer.Batches.Single();
|
||||
Assert.Collection(batch.DiffsInOrder,
|
||||
// First, a LayoutDisplay containing a RootLayout
|
||||
diff => AssertFrame.Component<RootLayout>(
|
||||
batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex]),
|
||||
// Then a RootLayout containing a NestedLayout
|
||||
diff => AssertFrame.Component<NestedLayout>(
|
||||
batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]),
|
||||
// Then a NestedLayout containing a ComponentWithNestedLayout
|
||||
diff => AssertFrame.Component<ComponentWithNestedLayout>(
|
||||
batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]),
|
||||
// Then the ComponentWithNestedLayout
|
||||
diff => AssertFrame.Text(
|
||||
batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex],
|
||||
$"{nameof(ComponentWithNestedLayout)} is here."));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanChangeDisplayedPageWithSameLayout()
|
||||
{
|
||||
// Arrange
|
||||
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
|
||||
})));
|
||||
|
||||
// Act
|
||||
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(PageDisplay.Page), typeof(DifferentComponentWithLayout) }
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, _renderer.Batches.Count);
|
||||
var batch = _renderer.Batches[1];
|
||||
Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component
|
||||
Assert.Collection(batch.DiffsInOrder,
|
||||
diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes
|
||||
diff =>
|
||||
{
|
||||
// RootLayout rerendered
|
||||
Assert.Collection(diff.Edits,
|
||||
edit =>
|
||||
{
|
||||
// Removed old page
|
||||
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
// Inserted new one
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
AssertFrame.Component<DifferentComponentWithLayout>(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
|
||||
});
|
||||
},
|
||||
diff =>
|
||||
{
|
||||
// New page rendered
|
||||
var singleEdit = diff.Edits.Single();
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[singleEdit.ReferenceFrameIndex],
|
||||
$"{nameof(DifferentComponentWithLayout)} is here.");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanChangeDisplayedPageWithDifferentLayout()
|
||||
{
|
||||
// Arrange
|
||||
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
|
||||
})));
|
||||
|
||||
// Act
|
||||
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) }
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, _renderer.Batches.Count);
|
||||
var batch = _renderer.Batches[1];
|
||||
Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component
|
||||
Assert.Collection(batch.DiffsInOrder,
|
||||
diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes
|
||||
diff =>
|
||||
{
|
||||
// RootLayout rerendered
|
||||
Assert.Collection(diff.Edits,
|
||||
edit =>
|
||||
{
|
||||
// Removed old page
|
||||
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
// Inserted new nested layout
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(1, edit.SiblingIndex);
|
||||
AssertFrame.Component<NestedLayout>(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
|
||||
});
|
||||
},
|
||||
diff =>
|
||||
{
|
||||
// New nested layout rendered
|
||||
var edit = diff.Edits[1];
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
AssertFrame.Component<ComponentWithNestedLayout>(
|
||||
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
|
||||
},
|
||||
diff =>
|
||||
{
|
||||
// New inner page rendered
|
||||
var singleEdit = diff.Edits.Single();
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type);
|
||||
AssertFrame.Text(
|
||||
batch.ReferenceFrames[singleEdit.ReferenceFrameIndex],
|
||||
$"{nameof(ComponentWithNestedLayout)} is here.");
|
||||
});
|
||||
}
|
||||
|
||||
private class RootLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, "RootLayout starts here");
|
||||
builder.AddContent(1, Body);
|
||||
builder.AddContent(2, "RootLayout ends here");
|
||||
}
|
||||
}
|
||||
|
||||
[Layout(typeof(RootLayout))]
|
||||
private class NestedLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, "NestedLayout starts here");
|
||||
builder.AddContent(1, Body);
|
||||
builder.AddContent(2, "NestedLayout ends here");
|
||||
}
|
||||
}
|
||||
|
||||
[Layout(typeof(RootLayout))]
|
||||
private class ComponentWithLayout : AutoRenderComponent
|
||||
{
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
=> builder.AddContent(0, $"{nameof(ComponentWithLayout)} is here.");
|
||||
}
|
||||
|
||||
[Layout(typeof(RootLayout))]
|
||||
private class DifferentComponentWithLayout : AutoRenderComponent
|
||||
{
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
=> builder.AddContent(0, $"{nameof(DifferentComponentWithLayout)} is here.");
|
||||
}
|
||||
|
||||
[Layout(typeof(NestedLayout))]
|
||||
private class ComponentWithNestedLayout : AutoRenderComponent
|
||||
{
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
=> builder.AddContent(0, $"{nameof(ComponentWithNestedLayout)} is here.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test
|
||||
{
|
||||
public class RouteViewTest
|
||||
{
|
||||
private readonly TestRenderer _renderer;
|
||||
private readonly RouteView _routeViewComponent;
|
||||
private readonly int _routeViewComponentId;
|
||||
|
||||
public RouteViewTest()
|
||||
{
|
||||
_renderer = new TestRenderer();
|
||||
_routeViewComponent = new RouteView();
|
||||
_routeViewComponentId = _renderer.AssignRootComponentId(_routeViewComponent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsIfNoRouteDataSupplied()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
// Throws synchronously, so no need to await
|
||||
_ = _routeViewComponent.SetParametersAsync(ParameterView.Empty);
|
||||
});
|
||||
|
||||
|
||||
Assert.Equal($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteView.RouteData)}.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersPageInsideLayoutView()
|
||||
{
|
||||
// Arrange
|
||||
var routeParams = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(ComponentWithLayout.Message), "Test message" }
|
||||
};
|
||||
var routeData = new RouteData(typeof(ComponentWithLayout), routeParams);
|
||||
|
||||
// Act
|
||||
_renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(RouteView.RouteData), routeData },
|
||||
})));
|
||||
|
||||
// Assert: RouteView renders LayoutView
|
||||
var batch = _renderer.Batches.Single();
|
||||
var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable();
|
||||
Assert.Collection(routeViewFrames,
|
||||
frame => AssertFrame.Component<LayoutView>(frame, subtreeLength: 3, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2));
|
||||
|
||||
// Assert: LayoutView renders TestLayout
|
||||
var layoutViewComponentId = batch.GetComponentFrames<LayoutView>().Single().ComponentId;
|
||||
var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(layoutViewComponentId).AsEnumerable();
|
||||
Assert.Collection(layoutViewFrames,
|
||||
frame => AssertFrame.Component<TestLayout>(frame, subtreeLength: 2, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
|
||||
|
||||
// Assert: TestLayout renders page
|
||||
var testLayoutComponentId = batch.GetComponentFrames<TestLayout>().Single().ComponentId;
|
||||
var testLayoutFrames = _renderer.GetCurrentRenderTreeFrames(testLayoutComponentId).AsEnumerable();
|
||||
Assert.Collection(testLayoutFrames,
|
||||
frame => AssertFrame.Text(frame, "Layout starts here", sequence: 0),
|
||||
frame => AssertFrame.Region(frame, subtreeLength: 3),
|
||||
frame => AssertFrame.Component<ComponentWithLayout>(frame, sequence: 0, subtreeLength: 2),
|
||||
frame => AssertFrame.Attribute(frame, nameof(ComponentWithLayout.Message), "Test message", sequence: 1),
|
||||
frame => AssertFrame.Text(frame, "Layout ends here", sequence: 2));
|
||||
|
||||
// Assert: page itself is rendered, having received parameters from the original route data
|
||||
var pageComponentId = batch.GetComponentFrames<ComponentWithLayout>().Single().ComponentId;
|
||||
var pageFrames = _renderer.GetCurrentRenderTreeFrames(pageComponentId).AsEnumerable();
|
||||
Assert.Collection(pageFrames,
|
||||
frame => AssertFrame.Text(frame, "Hello from the page with message 'Test message'", sequence: 0));
|
||||
|
||||
// Assert: nothing else was rendered
|
||||
Assert.Equal(4, batch.DiffsInOrder.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsesDefaultLayoutIfNoneSetOnPage()
|
||||
{
|
||||
// Arrange
|
||||
var routeParams = new Dictionary<string, object>();
|
||||
var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams);
|
||||
|
||||
// Act
|
||||
_renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(RouteView.RouteData), routeData },
|
||||
{ nameof(RouteView.DefaultLayout), typeof(OtherLayout) },
|
||||
})));
|
||||
|
||||
// Assert: uses default layout
|
||||
// Not asserting about what else gets rendered as that's covered by other tests
|
||||
var batch = _renderer.Batches.Single();
|
||||
var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable();
|
||||
Assert.Collection(routeViewFrames,
|
||||
frame => AssertFrame.Component<LayoutView>(frame, subtreeLength: 3, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(OtherLayout), sequence: 1),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsesNoLayoutIfNoneSetOnPageAndNoDefaultSet()
|
||||
{
|
||||
// Arrange
|
||||
var routeParams = new Dictionary<string, object>();
|
||||
var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams);
|
||||
|
||||
// Act
|
||||
_renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(RouteView.RouteData), routeData },
|
||||
})));
|
||||
|
||||
// Assert: uses no layout
|
||||
// Not asserting about what else gets rendered as that's covered by other tests
|
||||
var batch = _renderer.Batches.Single();
|
||||
var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable();
|
||||
Assert.Collection(routeViewFrames,
|
||||
frame => AssertFrame.Component<LayoutView>(frame, subtreeLength: 3, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)null, sequence: 1),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PageLayoutSupersedesDefaultLayout()
|
||||
{
|
||||
// Arrange
|
||||
var routeParams = new Dictionary<string, object>();
|
||||
var routeData = new RouteData(typeof(ComponentWithLayout), routeParams);
|
||||
|
||||
// Act
|
||||
_renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(RouteView.RouteData), routeData },
|
||||
{ nameof(RouteView.DefaultLayout), typeof(OtherLayout) },
|
||||
})));
|
||||
|
||||
// Assert: uses layout specified by page
|
||||
// Not asserting about what else gets rendered as that's covered by other tests
|
||||
var batch = _renderer.Batches.Single();
|
||||
var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable();
|
||||
Assert.Collection(routeViewFrames,
|
||||
frame => AssertFrame.Component<LayoutView>(frame, subtreeLength: 3, sequence: 0),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1),
|
||||
frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2));
|
||||
}
|
||||
|
||||
private class ComponentWithoutLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter] public string Message { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, $"Hello from the page with message '{Message}'");
|
||||
}
|
||||
}
|
||||
|
||||
[Layout(typeof(TestLayout))]
|
||||
private class ComponentWithLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter] public string Message { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, $"Hello from the page with message '{Message}'");
|
||||
}
|
||||
}
|
||||
|
||||
private class TestLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, "Layout starts here");
|
||||
builder.AddContent(1, Body);
|
||||
builder.AddContent(2, "Layout ends here");
|
||||
}
|
||||
}
|
||||
|
||||
private class OtherLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, "OtherLayout starts here");
|
||||
builder.AddContent(1, Body);
|
||||
builder.AddContent(2, "OtherLayout ends here");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -48,6 +49,9 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
public new int AssignRootComponentId(IComponent component)
|
||||
=> base.AssignRootComponentId(component);
|
||||
|
||||
public new ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId)
|
||||
=> base.GetCurrentRenderTreeFrames(componentId);
|
||||
|
||||
public void RenderRootComponent(int componentId, ParameterView? parameters = default)
|
||||
{
|
||||
var task = Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterView.Empty));
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Equal("False", () => appElement.FindElement(By.Id("identity-authenticated")).Text);
|
||||
Browser.Equal(string.Empty, () => appElement.FindElement(By.Id("identity-name")).Text);
|
||||
Browser.Equal("(none)", () => appElement.FindElement(By.Id("test-claim")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -56,6 +57,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Equal("True", () => appElement.FindElement(By.Id("identity-authenticated")).Text);
|
||||
Browser.Equal("someone cool", () => appElement.FindElement(By.Id("identity-name")).Text);
|
||||
Browser.Equal("Test claim value", () => appElement.FindElement(By.Id("test-claim")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -66,6 +68,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized"));
|
||||
Browser.Equal("You're not authorized, anonymous", () =>
|
||||
appElement.FindElement(By.CssSelector("#no-authorization-rule .not-authorized")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -75,6 +78,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||
Browser.Equal("Welcome, Some User!", () =>
|
||||
appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -84,6 +88,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||
Browser.Equal("Welcome, Some User!", () =>
|
||||
appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -93,6 +98,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||
Browser.Equal("You're not authorized, Some User", () =>
|
||||
appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -102,6 +108,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||
Browser.Equal("Welcome, Bert!", () =>
|
||||
appElement.FindElement(By.CssSelector("#authorize-policy .authorized")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -111,6 +118,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||
Browser.Equal("You're not authorized, Mallory", () =>
|
||||
appElement.FindElement(By.CssSelector("#authorize-policy .not-authorized")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -120,6 +128,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous);
|
||||
Browser.Equal("Welcome to PageAllowingAnonymous!", () =>
|
||||
appElement.FindElement(By.CssSelector("#auth-success")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -129,6 +138,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous);
|
||||
Browser.Equal("Welcome to PageAllowingAnonymous!", () =>
|
||||
appElement.FindElement(By.CssSelector("#auth-success")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -138,6 +148,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization);
|
||||
Browser.Equal("Welcome to PageRequiringAuthorization!", () =>
|
||||
appElement.FindElement(By.CssSelector("#auth-success")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -147,6 +158,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization);
|
||||
Browser.Equal("Sorry, anonymous, you're not authorized.", () =>
|
||||
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -156,6 +168,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy);
|
||||
Browser.Equal("Welcome to PageRequiringPolicy!", () =>
|
||||
appElement.FindElement(By.CssSelector("#auth-success")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -165,6 +178,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy);
|
||||
Browser.Equal("Sorry, Mallory, you're not authorized.", () =>
|
||||
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -174,6 +188,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(PageRequiringRole);
|
||||
Browser.Equal("Welcome to PageRequiringRole!", () =>
|
||||
appElement.FindElement(By.CssSelector("#auth-success")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -183,6 +198,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = MountAndNavigateToAuthTest(PageRequiringRole);
|
||||
Browser.Equal("Sorry, Bert, you're not authorized.", () =>
|
||||
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
|
||||
AssertExpectedLayoutUsed();
|
||||
}
|
||||
|
||||
private void AssertExpectedLayoutUsed()
|
||||
{
|
||||
WaitUntilExists(By.Id("auth-links"));
|
||||
}
|
||||
|
||||
protected IWebElement MountAndNavigateToAuthTest(string authLinkText)
|
||||
|
|
|
|||
|
|
@ -8,19 +8,21 @@
|
|||
and @page authorization rules.
|
||||
*@
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly>
|
||||
<Authorizing>Authorizing...</Authorizing>
|
||||
<NotAuthorized>
|
||||
<div id="auth-failure">
|
||||
Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized.
|
||||
</div>
|
||||
</NotAuthorized>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
|
||||
<hr />
|
||||
<Links />
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(AuthRouterLayout)">
|
||||
<Authorizing>Authorizing...</Authorizing>
|
||||
<NotAuthorized>
|
||||
<div id="auth-failure">
|
||||
Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized.
|
||||
</div>
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<p>There's nothing here</p>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
@inherits LayoutComponentBase
|
||||
|
||||
@Body
|
||||
|
||||
<hr />
|
||||
<ul id="auth-links">
|
||||
<li><a href="AuthHome">Home</a></li>
|
||||
<li><a href="CascadingAuthenticationStateConsumer">Cascading authentication state</a></li>
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
@page "/"
|
||||
@page "/Default.html"
|
||||
<div id="test-info">This is the default page.</div>
|
||||
<Links />
|
||||
|
|
|
|||
|
|
@ -38,4 +38,3 @@
|
|||
|
||||
<a href="/subdir/NotAComponent.html">Not a component</a>
|
||||
<a href="/subdir/routeablecomponentfrompackage.html">Cannot route to me</a>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,5 +9,3 @@
|
|||
<button id="go-to-longpage2" @onclick="@(() => NavigationManager.NavigateTo("LongPage2"))">
|
||||
Navigate programmatically to long page 2
|
||||
</button>
|
||||
|
||||
<Links />
|
||||
|
|
|
|||
|
|
@ -4,5 +4,3 @@
|
|||
<div style="border: 2px dashed blue; margin: 1rem; padding: 1rem; height: 1500px;">
|
||||
Scroll past me to find the links
|
||||
</div>
|
||||
|
||||
<Links />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
@page "/Other"
|
||||
<div id="test-info">This is another page.</div>
|
||||
<Links />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
@using Microsoft.AspNetCore.Components.Routing
|
||||
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly>
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<div id="test-info">Oops, that component wasn't found!</div>
|
||||
<LayoutView Layout="@typeof(RouterTestLayout)">
|
||||
<div id="test-info">Oops, that component wasn't found!</div>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
@page "/WithParameters/Name/{firstName}"
|
||||
@page "/WithParameters/Name/{firstName}/LastName/{lastName}"
|
||||
<div id="test-info">Your full name is @FirstName @LastName.</div>
|
||||
<Links />
|
||||
|
||||
@code
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
@layout RouterTestLayout
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
@using Microsoft.AspNetCore.Components
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@Body
|
||||
|
||||
<BasicTestApp.RouterTest.Links />
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
@using Microsoft.AspNetCore.Components;
|
||||
<!--
|
||||
Configuring this stuff here is temporary. Later we'll move the app config
|
||||
into Startup.cs, and it won't be necessary to specify AppAssembly.
|
||||
-->
|
||||
<CascadingValue Value="Name" Name="Name" IsFixed=true>
|
||||
<Router AppAssembly=typeof(ComponentsApp.App.App).Assembly />
|
||||
<Router AppAssembly="@typeof(ComponentsApp.App.App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<h2>Not found</h2>
|
||||
Sorry, there's nothing at this address.
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingValue>
|
||||
|
||||
@code{
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
@layout MainLayout
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
@using Microsoft.AspNetCore.Components.Routing
|
||||
Router component
|
||||
<Router AppAssembly="System.Reflection.Assembly.GetAssembly(typeof(RouterContainer))">
|
||||
<Found Context="routeData"><RouteView RouteData="@routeData" /></Found>
|
||||
<NotFound>
|
||||
<p>Route not found</p>
|
||||
</NotFound>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="typeof(Startup).Assembly">
|
||||
<NotFound>
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
@*#if (OrganizationalAuth || IndividualAuth)
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
#else
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
#endif*@
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
@layout MainLayout
|
||||
|
|
@ -917,7 +917,6 @@
|
|||
"Pages/FetchData.razor",
|
||||
"Pages/Index.razor",
|
||||
"Pages/_Host.cshtml",
|
||||
"Pages/_Imports.razor",
|
||||
"Properties/launchSettings.json",
|
||||
"Shared/LoginDisplay.razor",
|
||||
"Shared/MainLayout.razor",
|
||||
|
|
@ -954,7 +953,6 @@
|
|||
"Pages/FetchData.razor",
|
||||
"Pages/Index.razor",
|
||||
"Pages/_Host.cshtml",
|
||||
"Pages/_Imports.razor",
|
||||
"Properties/launchSettings.json",
|
||||
"Shared/LoginDisplay.razor",
|
||||
"Shared/MainLayout.razor",
|
||||
|
|
@ -991,7 +989,6 @@
|
|||
"Pages/FetchData.razor",
|
||||
"Pages/Index.razor",
|
||||
"Pages/_Host.cshtml",
|
||||
"Pages/_Imports.razor",
|
||||
"Properties/launchSettings.json",
|
||||
"Shared/LoginDisplay.razor",
|
||||
"Shared/MainLayout.razor",
|
||||
|
|
@ -1028,7 +1025,6 @@
|
|||
"Pages/FetchData.razor",
|
||||
"Pages/Index.razor",
|
||||
"Pages/_Host.cshtml",
|
||||
"Pages/_Imports.razor",
|
||||
"Properties/launchSettings.json",
|
||||
"Shared/MainLayout.razor",
|
||||
"Shared/NavMenu.razor",
|
||||
|
|
@ -1064,7 +1060,6 @@
|
|||
"Pages/FetchData.razor",
|
||||
"Pages/Index.razor",
|
||||
"Pages/_Host.cshtml",
|
||||
"Pages/_Imports.razor",
|
||||
"Properties/launchSettings.json",
|
||||
"Shared/LoginDisplay.razor",
|
||||
"Shared/MainLayout.razor",
|
||||
|
|
@ -1101,7 +1096,6 @@
|
|||
"Pages/FetchData.razor",
|
||||
"Pages/Index.razor",
|
||||
"Pages/_Host.cshtml",
|
||||
"Pages/_Imports.razor",
|
||||
"Properties/launchSettings.json",
|
||||
"Shared/LoginDisplay.razor",
|
||||
"Shared/MainLayout.razor",
|
||||
|
|
|
|||
Loading…
Reference in New Issue