Make temporary "layout" and "implements" syntax work with _ViewImports hierarchies
This commit is contained in:
parent
f649de2976
commit
1e0836167d
|
|
@ -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 Microsoft.AspNetCore.Razor.Language;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
// This only exists to support SourceLinesVisitor and can be removed once
|
||||
// we are able to implement proper Blazor-specific directives
|
||||
internal class SourceLinesEnumerable : IEnumerable<string>
|
||||
{
|
||||
private RazorSourceDocument _source;
|
||||
|
||||
public SourceLinesEnumerable(RazorSourceDocument source)
|
||||
=> _source = source;
|
||||
|
||||
public IEnumerator<string> GetEnumerator()
|
||||
=> new SourceLinesEnumerator(_source);
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> new SourceLinesEnumerator(_source);
|
||||
|
||||
private class SourceLinesEnumerator : IEnumerator<string>
|
||||
{
|
||||
private readonly RazorSourceDocument _sourceDocument;
|
||||
private readonly RazorSourceLineCollection _lines;
|
||||
private int _currentLineIndex;
|
||||
private int _cumulativeLengthOfPrecedingLines;
|
||||
private char[] _currentLineBuffer = new char[200]; // Grows if needed
|
||||
private string _currentLineText;
|
||||
|
||||
public SourceLinesEnumerator(RazorSourceDocument sourceDocument)
|
||||
{
|
||||
_sourceDocument = sourceDocument ?? throw new ArgumentNullException(nameof(sourceDocument));
|
||||
_lines = _sourceDocument.Lines;
|
||||
_currentLineIndex = -1;
|
||||
}
|
||||
|
||||
public string Current => _currentLineText;
|
||||
|
||||
object IEnumerator.Current => _currentLineText;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentLineIndex++;
|
||||
if (_currentLineIndex >= _lines.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lineLength = _lines.GetLineLength(_currentLineIndex);
|
||||
if (_currentLineBuffer.Length < lineLength)
|
||||
{
|
||||
_currentLineBuffer = new char[lineLength];
|
||||
}
|
||||
|
||||
_sourceDocument.CopyTo(_cumulativeLengthOfPrecedingLines, _currentLineBuffer, 0, lineLength);
|
||||
_currentLineText = new string(_currentLineBuffer, 0, lineLength);
|
||||
_cumulativeLengthOfPrecedingLines += lineLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentLineIndex = -1;
|
||||
_cumulativeLengthOfPrecedingLines = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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.Razor.Language;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
// This only exists to support the temporary fake Blazor directives and can
|
||||
// be removed once we are able to implement proper Blazor-specific directives
|
||||
internal abstract class SourceLinesVisitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Visits each line in the document's imports (in order), followed by
|
||||
/// each line in the document's primary syntax tree.
|
||||
/// </summary>
|
||||
public void Visit(RazorCodeDocument codeDocument)
|
||||
{
|
||||
foreach (var import in codeDocument.GetImportSyntaxTrees())
|
||||
{
|
||||
VisitSyntaxTree(import);
|
||||
}
|
||||
|
||||
VisitSyntaxTree(codeDocument.GetSyntaxTree());
|
||||
}
|
||||
|
||||
protected abstract void VisitLine(string line);
|
||||
|
||||
private void VisitSyntaxTree(RazorSyntaxTree syntaxTree)
|
||||
{
|
||||
var sourceDocument = syntaxTree.Source;
|
||||
foreach (var line in new SourceLinesEnumerable(sourceDocument))
|
||||
{
|
||||
VisitLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// 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.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
// Until we're able to add real directives, this implements a temporary mechanism whereby we
|
||||
// search for lines of the form "@({regex})" (including in the imports sources) and do something
|
||||
// with the regex matches. Also we remove the corresponding tokens from the intermediate
|
||||
// representation to stop them from interfering with the compiled output on its own.
|
||||
internal abstract class TemporaryFakeDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
private readonly Regex _sourceLineRegex;
|
||||
private readonly Regex _tokenRegex;
|
||||
|
||||
protected TemporaryFakeDirectivePass(string syntaxRegexPattern)
|
||||
{
|
||||
_sourceLineRegex = new Regex($@"^\s*@\({syntaxRegexPattern}\)\s*$");
|
||||
_tokenRegex = new Regex($@"^{syntaxRegexPattern}$");
|
||||
}
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
// First, remove any matching lines from the intermediate representation
|
||||
// in the primary document. Don't need to remove them from imports as they
|
||||
// have no effect there anyway.
|
||||
var methodNode = documentNode.FindPrimaryMethod();
|
||||
var methodNodeChildren = methodNode.Children.ToList();
|
||||
foreach (var node in methodNodeChildren)
|
||||
{
|
||||
if (IsMatchingNode(node))
|
||||
{
|
||||
methodNode.Children.Remove(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Now find the matching lines in the source code (including imports)
|
||||
// Need to do this on source, because the imports aren't in the intermediate representation
|
||||
var linesVisitor = new RegexSourceLinesVisitor(_sourceLineRegex);
|
||||
linesVisitor.Visit(codeDocument);
|
||||
if (linesVisitor.MatchedContent.Any())
|
||||
{
|
||||
HandleMatchedContent(codeDocument, linesVisitor.MatchedContent);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable<string> matchedContent);
|
||||
|
||||
private bool IsMatchingNode(IntermediateNode node)
|
||||
=> node.Children.Count == 1
|
||||
&& node.Children[0] is IntermediateToken intermediateToken
|
||||
&& _tokenRegex.IsMatch(intermediateToken.Content);
|
||||
|
||||
private class RegexSourceLinesVisitor : SourceLinesVisitor
|
||||
{
|
||||
private Regex _searchRegex;
|
||||
private readonly List<string> _matchedContent = new List<string>();
|
||||
|
||||
public IEnumerable<string> MatchedContent => _matchedContent;
|
||||
|
||||
public RegexSourceLinesVisitor(Regex searchRegex)
|
||||
{
|
||||
_searchRegex = searchRegex;
|
||||
}
|
||||
|
||||
protected override void VisitLine(string line)
|
||||
{
|
||||
// Pick the most specific by looking for the final one in the sources
|
||||
var match = _searchRegex.Match(line);
|
||||
if (match.Success)
|
||||
{
|
||||
_matchedContent.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// 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.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// This code is temporary. It finds top-level expressions of the form
|
||||
/// @Implements<SomeInterfaceType>()
|
||||
/// ... and converts them into interface declarations on the class.
|
||||
/// Once we're able to add Blazor-specific directives and have them show up in tooling,
|
||||
/// we'll replace this with a simpler and cleaner "@implements SomeInterfaceType" directive.
|
||||
/// </summary>
|
||||
internal class TemporaryImplementsPass : TemporaryFakeDirectivePass
|
||||
{
|
||||
// Example: "Implements<MyApp.Namespace.ISomeType<T1, T2>>()"
|
||||
// Captures: MyApp.Namespace.ISomeType<T1, T2>
|
||||
private const string ImplementsTokenPattern = @"\s*Implements\s*<(.+)\>\s*\(\s*\)\s*";
|
||||
|
||||
public static void Register(IRazorEngineBuilder configuration)
|
||||
{
|
||||
configuration.Features.Add(new TemporaryImplementsPass());
|
||||
}
|
||||
|
||||
private TemporaryImplementsPass() : base(ImplementsTokenPattern)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable<string> matchedContent)
|
||||
{
|
||||
var classNode = codeDocument.GetDocumentIntermediateNode().FindPrimaryClass();
|
||||
if (classNode.Interfaces == null)
|
||||
{
|
||||
classNode.Interfaces = new List<string>();
|
||||
}
|
||||
|
||||
foreach (var implementsType in matchedContent)
|
||||
{
|
||||
classNode.Interfaces.Add(implementsType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// 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.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// This code is temporary. It finds source code lines of the form
|
||||
/// @Layout<SomeType>()
|
||||
/// ... and converts them into [Layout(typeof(SomeType))] attributes on the class.
|
||||
/// Once we're able to add Blazor-specific directives and have them show up in tooling,
|
||||
/// we'll replace this with a simpler and cleaner "@Layout SomeType" directive.
|
||||
/// </summary>
|
||||
internal class TemporaryLayoutPass : TemporaryFakeDirectivePass
|
||||
{
|
||||
// Example: "Layout<MyApp.Namespace.SomeType<T1, T2>>()"
|
||||
// Captures: MyApp.Namespace.SomeType<T1, T2>
|
||||
private const string LayoutTokenPattern = @"\s*Layout\s*<(.+)\>\s*\(\s*\)\s*";
|
||||
|
||||
private const string LayoutAttributeTypeName
|
||||
= "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute";
|
||||
|
||||
public static void Register(IRazorEngineBuilder configuration)
|
||||
{
|
||||
configuration.Features.Add(new TemporaryLayoutPass());
|
||||
}
|
||||
|
||||
private TemporaryLayoutPass() : base(LayoutTokenPattern)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable<string> matchedContent)
|
||||
{
|
||||
var chosenLayoutType = matchedContent.Last();
|
||||
var attributeNode = new CSharpCodeIntermediateNode();
|
||||
attributeNode.Children.Add(new IntermediateToken()
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = $"[{LayoutAttributeTypeName}(typeof ({chosenLayoutType}))]" + Environment.NewLine,
|
||||
});
|
||||
|
||||
var docNode = codeDocument.GetDocumentIntermediateNode();
|
||||
var namespaceNode = docNode.FindPrimaryNamespace();
|
||||
var classNode = docNode.FindPrimaryClass();
|
||||
var classNodeIndex = namespaceNode
|
||||
.Children
|
||||
.IndexOf(classNode);
|
||||
namespaceNode.Children.Insert(classNodeIndex, attributeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +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.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// This code is temporary. It finds top-level expressions of the form
|
||||
/// @Implements<SomeInterfaceType>()
|
||||
/// ... and converts them into interface declarations on the class.
|
||||
/// Once we're able to add Blazor-specific directives and have them show up in tooling,
|
||||
/// we'll replace this with a simpler and cleaner "@implements SomeInterfaceType" directive.
|
||||
/// </summary>
|
||||
internal class TemporaryImplementsPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
// Example: "Implements<MyApp.Namespace.ISomeType<T1, T2>>()"
|
||||
// Captures: MyApp.Namespace.ISomeType<T1, T2>
|
||||
private static readonly Regex ImplementsSourceRegex
|
||||
= new Regex(@"^\s*Implements\s*<(.+)\>\s*\(\s*\)\s*$");
|
||||
|
||||
public static void Register(IRazorEngineBuilder configuration)
|
||||
{
|
||||
configuration.Features.Add(new TemporaryImplementsPass());
|
||||
}
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var visitor = new Visitor();
|
||||
visitor.Visit(documentNode);
|
||||
|
||||
foreach (var implementsNode in visitor.ImplementsNodes)
|
||||
{
|
||||
visitor.MethodNode.Children.Remove(implementsNode);
|
||||
}
|
||||
|
||||
if (visitor.ClassNode.Interfaces == null)
|
||||
{
|
||||
visitor.ClassNode.Interfaces = new List<string>();
|
||||
}
|
||||
|
||||
foreach (var implementsType in visitor.ImplementsTypes)
|
||||
{
|
||||
visitor.ClassNode.Interfaces.Add(implementsType);
|
||||
}
|
||||
}
|
||||
|
||||
private class Visitor : IntermediateNodeWalker
|
||||
{
|
||||
public ClassDeclarationIntermediateNode ClassNode { get; private set; }
|
||||
public MethodDeclarationIntermediateNode MethodNode { get; private set; }
|
||||
public List<CSharpExpressionIntermediateNode> ImplementsNodes { get; private set; }
|
||||
= new List<CSharpExpressionIntermediateNode>();
|
||||
public List<string> ImplementsTypes { get; private set; }
|
||||
= new List<string>();
|
||||
|
||||
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
|
||||
{
|
||||
ClassNode = node;
|
||||
base.VisitClassDeclaration(node);
|
||||
}
|
||||
|
||||
public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode methodNode)
|
||||
{
|
||||
MethodNode = methodNode;
|
||||
|
||||
var topLevelExpressions = methodNode.Children.OfType<CSharpExpressionIntermediateNode>();
|
||||
foreach (var csharpExpression in topLevelExpressions)
|
||||
{
|
||||
if (csharpExpression.Children.Count == 1)
|
||||
{
|
||||
var child = csharpExpression.Children[0];
|
||||
if (child is IntermediateToken intermediateToken)
|
||||
{
|
||||
if (TryGetImplementsType(intermediateToken.Content, out string implementsType))
|
||||
{
|
||||
ImplementsNodes.Add(csharpExpression);
|
||||
ImplementsTypes.Add(implementsType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.VisitMethodDeclaration(methodNode);
|
||||
}
|
||||
|
||||
private bool TryGetImplementsType(string sourceCode, out string implementsType)
|
||||
{
|
||||
var match = ImplementsSourceRegex.Match(sourceCode);
|
||||
if (match.Success)
|
||||
{
|
||||
implementsType = match.Groups[1].Value;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
implementsType = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +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.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// This code is temporary. It finds top-level expressions of the form
|
||||
/// @Layout<SomeType>()
|
||||
/// ... and converts them into [Layout(typeof(SomeType))] attributes on the class.
|
||||
/// Once we're able to add Blazor-specific directives and have them show up in tooling,
|
||||
/// we'll replace this with a simpler and cleaner "@Layout SomeType" directive.
|
||||
/// </summary>
|
||||
internal class TemporaryLayoutPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||
{
|
||||
// Example: "Layout<MyApp.Namespace.SomeType<T1, T2>>()"
|
||||
// Captures: MyApp.Namespace.SomeType<T1, T2>
|
||||
private static readonly Regex LayoutSourceRegex
|
||||
= new Regex(@"^\s*Layout\s*<(.+)\>\s*\(\s*\)\s*$");
|
||||
private const string LayoutAttributeTypeName
|
||||
= "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute";
|
||||
|
||||
public static void Register(IRazorEngineBuilder configuration)
|
||||
{
|
||||
configuration.Features.Add(new TemporaryLayoutPass());
|
||||
}
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var visitor = new Visitor();
|
||||
visitor.Visit(documentNode);
|
||||
|
||||
if (visitor.DidFindLayoutDeclaration)
|
||||
{
|
||||
visitor.MethodNode.Children.Remove(visitor.LayoutNode);
|
||||
|
||||
var attributeNode = new CSharpCodeIntermediateNode();
|
||||
attributeNode.Children.Add(new IntermediateToken()
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = $"[{LayoutAttributeTypeName}(typeof ({visitor.LayoutType}))]" + Environment.NewLine,
|
||||
});
|
||||
|
||||
var classNodeIndex = visitor
|
||||
.NamespaceNode
|
||||
.Children
|
||||
.IndexOf(visitor.ClassNode);
|
||||
visitor.NamespaceNode.Children.Insert(classNodeIndex, attributeNode);
|
||||
}
|
||||
}
|
||||
|
||||
private class Visitor : IntermediateNodeWalker
|
||||
{
|
||||
public bool DidFindLayoutDeclaration { get; private set; }
|
||||
public NamespaceDeclarationIntermediateNode NamespaceNode { get; private set; }
|
||||
public ClassDeclarationIntermediateNode ClassNode { get; private set; }
|
||||
public MethodDeclarationIntermediateNode MethodNode { get; private set; }
|
||||
public CSharpExpressionIntermediateNode LayoutNode { get; private set; }
|
||||
public string LayoutType { get; private set; }
|
||||
|
||||
public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node)
|
||||
{
|
||||
NamespaceNode = node;
|
||||
base.VisitNamespaceDeclaration(node);
|
||||
}
|
||||
|
||||
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
|
||||
{
|
||||
ClassNode = node;
|
||||
base.VisitClassDeclaration(node);
|
||||
}
|
||||
|
||||
public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode methodNode)
|
||||
{
|
||||
var topLevelExpressions = methodNode.Children.OfType<CSharpExpressionIntermediateNode>();
|
||||
foreach (var csharpExpression in topLevelExpressions)
|
||||
{
|
||||
if (csharpExpression.Children.Count == 1)
|
||||
{
|
||||
var child = csharpExpression.Children[0];
|
||||
if (child is IntermediateToken intermediateToken)
|
||||
{
|
||||
if (TryGetLayoutType(intermediateToken.Content, out string layoutType))
|
||||
{
|
||||
DidFindLayoutDeclaration = true;
|
||||
MethodNode = methodNode;
|
||||
LayoutNode = csharpExpression;
|
||||
LayoutType = layoutType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.VisitMethodDeclaration(methodNode);
|
||||
}
|
||||
|
||||
private bool TryGetLayoutType(string sourceCode, out string layoutType)
|
||||
{
|
||||
var match = LayoutSourceRegex.Match(sourceCode);
|
||||
if (match.Success)
|
||||
{
|
||||
layoutType = match.Groups[1].Value;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
layoutType = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
// Arrange/Act
|
||||
var testComponentTypeName = typeof(TestLayout).FullName.Replace('+', '.');
|
||||
var component = CompileToComponent(
|
||||
$"@(Layout<{testComponentTypeName}>())" +
|
||||
$"@(Layout<{testComponentTypeName}>())\n" +
|
||||
$"Hello");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
|
|
@ -386,7 +386,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
Assert.NotNull(layoutAttribute);
|
||||
Assert.Equal(typeof(TestLayout), layoutAttribute.LayoutType);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Hello"));
|
||||
frame => AssertFrame.Text(frame, "\nHello"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -395,14 +395,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
// Arrange/Act
|
||||
var testInterfaceTypeName = typeof(ITestInterface).FullName.Replace('+', '.');
|
||||
var component = CompileToComponent(
|
||||
$"@(Implements<{testInterfaceTypeName}>())" +
|
||||
$"@(Implements<{testInterfaceTypeName}>())\n" +
|
||||
$"Hello");
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.IsAssignableFrom<ITestInterface>(component);
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Text(frame, "Hello"));
|
||||
frame => AssertFrame.Text(frame, "\nHello"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in New Issue