File: DefaultRazorIntermediateNodeLoweringPhaseIntegrationTest.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.AspNetCore.Razor.Language\test\Microsoft.AspNetCore.Razor.Language.UnitTests.csproj (Microsoft.AspNetCore.Razor.Language.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Xunit;
using static Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNodeAssert;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
public class DefaultRazorIntermediateNodeLoweringPhaseIntegrationTest : RazorProjectEngineTestBase
{
    protected override RazorLanguageVersion Version => RazorLanguageVersion.Latest;
 
    protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder)
    {
        SectionDirective.Register(builder);
    }
 
    protected override void ConfigureCodeDocumentProcessor(RazorCodeDocumentProcessor processor)
    {
        processor.ExecutePhasesThrough<DefaultTagHelperResolutionPhase>();
    }
 
    [Fact]
    public void Lower_SetsOptions_Defaults()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Assert.NotNull(documentNode.Options);
        Assert.False(documentNode.Options.DesignTime);
        Assert.Equal(4, documentNode.Options.IndentSize);
        Assert.False(documentNode.Options.IndentWithTabs);
    }
 
    [Fact]
    public void Lower_SetsOptions_RunsConfigureCallbacks()
    {
        // Arrange
        var projectEngine = CreateProjectEngine(builder =>
        {
            builder.ConfigureCodeGenerationOptions(builder =>
            {
                builder.IndentSize = 17;
                builder.IndentWithTabs = true;
                builder.SuppressChecksum = true;
            });
        });
 
        var codeDocument = projectEngine.CreateEmptyDesignTimeCodeDocument();
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Assert.NotNull(documentNode.Options);
        Assert.True(documentNode.Options.DesignTime);
        Assert.Equal(17, documentNode.Options.IndentSize);
        Assert.True(documentNode.Options.IndentWithTabs);
        Assert.True(documentNode.Options.SuppressChecksum);
    }
 
    [Fact]
    public void Lower_HelloWorld()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateCodeDocument("Hello, World!");
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(documentNode,
            n => Html("Hello, World!", n));
    }
 
    [Fact]
    public void Lower_HtmlWithDataDashAttributes()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateCodeDocument(@"
<html>
    <body>
        <span data-val=""@Hello"" />
    </body>
</html>");
 
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(documentNode,
            n => Html(
@"
<html>
    <body>
        <span data-val=""", n),
            n => CSharpExpression("Hello", n),
            n => Html(@""" />
    </body>
</html>", n));
    }
 
    [Fact]
    public void Lower_HtmlWithConditionalAttributes()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateCodeDocument(@"
<html>
    <body>
        <span val=""@Hello World"" />
    </body>
</html>");
 
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(documentNode,
            n => Html(
@"
<html>
    <body>
        <span", n),
 
            n => ConditionalAttribute(
                prefix: " val=\"",
                name: "val",
                suffix: "\"",
                node: n,
                valueValidators: [
                    value => CSharpExpressionAttributeValue(string.Empty, "Hello", value),
                    value => LiteralAttributeValue(" ",  "World", value)
                ]),
            n => Html(@" />
    </body>
</html>", n));
    }
 
    [Fact]
    public void Lower_WithFunctions()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateCodeDocument(@"@functions { public int Foo { get; set; }}");
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(documentNode,
            n => Directive(
                "functions",
                n,
                c => Assert.IsType<CSharpCodeIntermediateNode>(c)));
    }
 
    [Fact]
    public void Lower_WithUsing()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateCodeDocument(@"@using System");
        var processor = CreateCodeDocumentProcessor(codeDocument);
        var expectedSourceLocation = new SourceSpan(codeDocument.Source.FilePath, 1, 0, 1, 12);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(documentNode,
            n =>
            {
                Using("System", n);
                Assert.Equal(expectedSourceLocation, n.Source);
            });
    }
 
    [Fact]
    public void Lower_TagHelpers()
    {
        // Arrange
        var tagHelper = CreateTagHelperDescriptor(
            tagName: "span",
            typeName: "SpanTagHelper",
            assemblyName: "TestAssembly");
 
        var codeDocument = ProjectEngine.CreateCodeDocument(@"@addTagHelper *, TestAssembly
<span val=""@Hello World""></span>",
            [tagHelper]);
 
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(documentNode,
            n => Directive(
                SyntaxConstants.CSharp.AddTagHelperKeyword,
                n,
                v => DirectiveToken(DirectiveTokenKind.String, "*, TestAssembly", v)),
            n => TagHelper(
                "span",
                TagMode.StartTagAndEndTag,
                [tagHelper],
                n,
                c => Assert.IsType<TagHelperBodyIntermediateNode>(c),
                c => TagHelperHtmlAttribute(
                    "val",
                    AttributeStructure.DoubleQuotes,
                    c,
                    v => CSharpExpressionAttributeValue(string.Empty, "Hello", v),
                    v => LiteralAttributeValue(" ", "World", v))));
    }
 
    [Fact]
    public void Lower_TagHelpers_WithPrefix()
    {
        // Arrange
        var tagHelper = CreateTagHelperDescriptor(
            tagName: "span",
            typeName: "SpanTagHelper",
            assemblyName: "TestAssembly");
 
        var codeDocument = ProjectEngine.CreateCodeDocument(@"@addTagHelper *, TestAssembly
@tagHelperPrefix cool:
<cool:span val=""@Hello World""></cool:span>",
            [tagHelper]);
 
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(documentNode,
            n => Directive(
                SyntaxConstants.CSharp.AddTagHelperKeyword,
                n,
                v => DirectiveToken(DirectiveTokenKind.String, "*, TestAssembly", v)),
            n => Directive(
                SyntaxConstants.CSharp.TagHelperPrefixKeyword,
                n,
                v => DirectiveToken(DirectiveTokenKind.String, "cool:", v)),
            n => TagHelper(
                "span",  // Note: this is span not cool:span
                TagMode.StartTagAndEndTag,
                [tagHelper],
                n,
                c => Assert.IsType<TagHelperBodyIntermediateNode>(c),
                c => TagHelperHtmlAttribute(
                    "val",
                    AttributeStructure.DoubleQuotes,
                    c,
                    v => CSharpExpressionAttributeValue(string.Empty, "Hello", v),
                    v => LiteralAttributeValue(" ", "World", v))));
    }
 
    [Fact]
    public void Lower_TagHelper_InSection()
    {
        // Arrange
        var tagHelper = CreateTagHelperDescriptor(
            tagName: "span",
            typeName: "SpanTagHelper",
            assemblyName: "TestAssembly");
 
        var codeDocument = ProjectEngine.CreateCodeDocument(@"@addTagHelper *, TestAssembly
@section test {
<span val=""@Hello World""></span>
}",
            [tagHelper]);
 
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(
            documentNode,
            n => Directive(
                SyntaxConstants.CSharp.AddTagHelperKeyword,
                n,
                v => DirectiveToken(DirectiveTokenKind.String, "*, TestAssembly", v)),
            n => Directive(
                "section",
                n,
                c1 => DirectiveToken(DirectiveTokenKind.Member, "test", c1),
                c1 => Html(Environment.NewLine, c1),
                c1 => TagHelper(
                    "span",
                    TagMode.StartTagAndEndTag,
                    [tagHelper],
                    c1,
                    c2 => Assert.IsType<TagHelperBodyIntermediateNode>(c2),
                    c2 => TagHelperHtmlAttribute(
                        "val",
                        AttributeStructure.DoubleQuotes,
                        c2,
                        v => CSharpExpressionAttributeValue(string.Empty, "Hello", v),
                        v => LiteralAttributeValue(" ", "World", v))),
                c1 => Html(Environment.NewLine, c1)));
    }
 
    [Fact]
    public void Lower_TagHelpersWithBoundAttribute()
    {
        // Arrange
        var tagHelper = CreateTagHelperDescriptor(
            tagName: "input",
            typeName: "InputTagHelper",
            assemblyName: "TestAssembly",
            attributes: [builder => builder
                .Name("bound")
                .PropertyName("FooProp")
                .TypeName("System.String")]);
 
        var codeDocument = ProjectEngine.CreateCodeDocument(@"@addTagHelper *, TestAssembly
<input bound='foo' />",
            [tagHelper]);
 
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(
            documentNode,
            n => Directive(
                SyntaxConstants.CSharp.AddTagHelperKeyword,
                n,
                v => DirectiveToken(DirectiveTokenKind.String, "*, TestAssembly", v)),
            n => TagHelper(
                "input",
                TagMode.SelfClosing,
                [tagHelper],
                n,
                c => Assert.IsType<TagHelperBodyIntermediateNode>(c),
                c => SetTagHelperProperty(
                    "bound",
                    "FooProp",
                    AttributeStructure.SingleQuotes,
                    c,
                    v => Html("foo", v))));
    }
 
    [Fact]
    public void Lower_WithImports_Using()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(@"@using System.Threading.Tasks
<p>Hi!</p>");
        var importSource1 = TestRazorSourceDocument.Create("@using System.Globalization");
        var importSource2 = TestRazorSourceDocument.Create("@using System.Text");
        var codeDocument = ProjectEngine.CreateCodeDocument(source, [importSource1, importSource2]);
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(
            documentNode,
            n => Using("System.Globalization", n),
            n => Using("System.Text", n),
            n => Using("System.Threading.Tasks", n),
            n => Html("<p>Hi!</p>", n));
    }
 
    [Fact]
    public void Lower_WithImports_AllowsIdenticalNamespacesInPrimaryDocument()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(@"@using System.Threading.Tasks
@using System.Threading.Tasks");
        var importSource = TestRazorSourceDocument.Create("@using System.Threading.Tasks");
        var codeDocument = ProjectEngine.CreateCodeDocument(source, [importSource]);
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(
            documentNode,
            n => Using("System.Threading.Tasks", n),
            n => Using("System.Threading.Tasks", n));
    }
 
    [Fact]
    public void Lower_WithMultipleImports_SingleLineFileScopedSinglyOccurring()
    {
        // Arrange
        var projectEngine = CreateProjectEngine(builder =>
        {
            var directive = DirectiveDescriptor.CreateDirective(
                "test",
                DirectiveKind.SingleLine,
                builder =>
                {
                    builder.AddMemberToken();
                    builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
                });
 
            builder.AddDirective(directive);
        });
 
        var source = TestRazorSourceDocument.Create("<p>Hi!</p>");
        var importSource1 = TestRazorSourceDocument.Create("@test value1");
        var importSource2 = TestRazorSourceDocument.Create("@test value2");
        var codeDocument = projectEngine.CreateCodeDocument(source, [importSource1, importSource2]);
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(
            documentNode,
            n => Directive("test", n, c => DirectiveToken(DirectiveTokenKind.Member, "value2", c)),
            n => Html("<p>Hi!</p>", n));
    }
 
    [Fact]
    public void Lower_WithImports_IgnoresBlockDirective()
    {
        // Arrange
        var projectEngine = CreateProjectEngine(builder =>
        {
            var directive = DirectiveDescriptor.CreateDirective("block", DirectiveKind.RazorBlock, d => d.AddMemberToken());
 
            builder.AddDirective(directive);
        });
 
        var source = TestRazorSourceDocument.Create("<p>Hi!</p>");
        var importSource = TestRazorSourceDocument.Create("@block token { }");
        var codeDocument = projectEngine.CreateCodeDocument(source, [importSource]);
        var processor = CreateCodeDocumentProcessor(codeDocument);
 
        // Act
        var documentNode = processor.GetDocumentNode();
 
        // Assert
        Children(
            documentNode,
            n => Html("<p>Hi!</p>", n));
    }
 
    private static TagHelperDescriptor CreateTagHelperDescriptor(
        string tagName,
        string typeName,
        string assemblyName,
        params ReadOnlySpan<Action<BoundAttributeDescriptorBuilder>> attributes)
    {
        var builder = TagHelperDescriptorBuilder.CreateTagHelper(typeName, assemblyName);
        builder.SetTypeName(typeName, typeNamespace: null, typeNameIdentifier: null);
 
        foreach (var attributeBuilder in attributes)
        {
            builder.BoundAttributeDescriptor(attributeBuilder);
        }
 
        builder.TagMatchingRuleDescriptor(ruleBuilder => ruleBuilder.RequireTagName(tagName));
 
        return builder.Build();
    }
}