File: Components\ComponentMarkupBlockPassTest.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 System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.AspNetCore.Razor.Language.Components;
 
public class ComponentMarkupBlockPassTest
{
    public ComponentMarkupBlockPassTest()
    {
        ProjectEngine = RazorProjectEngine.Create(
            RazorConfiguration.Default,
            RazorProjectFileSystem.Create(Environment.CurrentDirectory),
            b =>
            {
                if (b.Features.OfType<ComponentMarkupBlockPass>().Any())
                {
                    b.Features.Remove(b.Features.OfType<ComponentMarkupBlockPass>().Single());
                }
            });
        Engine = ProjectEngine.Engine;
 
        Pass = new ComponentMarkupBlockPass(RazorLanguageVersion.Latest)
        {
            Engine = Engine
        };
    }
 
    private RazorProjectEngine ProjectEngine { get; }
 
    private RazorEngine Engine { get; }
 
    private ComponentMarkupBlockPass Pass { get; }
 
    [Fact]
    public void Execute_RewritesHtml_Basic()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  <head cool=""beans"">
    Hello, World!
  </head>
</html>");
 
        var expected = NormalizeContent(@"
<html><head cool=""beans"">
    Hello, World!
  </head></html>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact]
    public void Execute_RewritesHtml_WithComment()
    {
        // Arrange
        var document = CreateDocument(@"Start<!-- -->End");
 
        var expected = NormalizeContent(@"StartEnd");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/aspnetcore/issues/6480")]
    public void Execute_RewritesHtml_HtmlAttributePrefix()
    {
        // Arrange
        var document = CreateDocument(@"<div class=""one two"">Hi</div>");
 
        var expected = NormalizeContent(@"<div class=""one two"">Hi</div>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact]
    public void Execute_RewritesHtml_MergesSiblings()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  @(""Hi"")<div></div>
  <div></div>
  <div>@(""Hi"")</div>
</html>");
 
        var expected = NormalizeContent(@"
<div></div>
  <div></div>
  ");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact]
    public void Execute_RewritesHtml_MergesSiblings_LeftEdge()
    {
        // Arrange
        var document = CreateDocument(@"
<html><div></div>
  <div></div>
  <div>@(""Hi"")</div>
</html>");
 
        var expected = NormalizeContent(@"
<div></div>
  <div></div>
  ");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
 
    [Fact]
    public void Execute_RewritesHtml_CSharpInAttributes()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  <head cool=""beans"" csharp=""@yes"" mixed=""hi @there"">
    <div>foo</div>
  </head>
</html>");
 
        var expected = NormalizeContent("<div>foo</div>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact]
    public void Execute_RewritesHtml_CSharpInBody()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  <head cool=""beans"">
    <div>@foo</div>
    <div>rewriteme</div>
    <div>@bar</div>
  </head>
</html>");
 
        var expected = NormalizeContent("<div>rewriteme</div>\n    ");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact]
    public void Execute_RewritesHtml_EncodesHtmlEntities()
    {
        // Arrange
        var document = CreateDocument(@"
<div>
    &lt;span&gt;Hi&lt;/span&gt;
</div>");
 
        var expected = NormalizeContent(@"
<div>
    &lt;span&gt;Hi&lt;/span&gt;
</div>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact]
    public void Execute_RewritesHtml_EmptyNonvoid()
    {
        // Arrange
        var document = CreateDocument(@"<a href=""...""></a>");
 
        var expected = NormalizeContent(@"<a href=""...""></a>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact]
    public void Execute_RewritesHtml_Void()
    {
        // Arrange
        var document = CreateDocument(@"<link rel=""..."" href=""...""/>");
 
        var expected = NormalizeContent(@"<link rel=""..."" href=""..."">");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    [Fact]
    public void Execute_CannotRewriteHtml_CSharpInCode()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  @if (some_bool)
  {
  <head cool=""beans"">
    @hello
  </head>
  }
</html>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        Assert.Empty(documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>());
    }
 
    [Fact]
    public void Execute_CannotRewriteHtml_Script()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  @if (some_bool)
  {
  <head cool=""beans"">
    <script>...</script>
  </head>
  }
</html>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal("""<head cool="beans"><script>...</script></head>""", block.Content);
    }
 
    [Fact]
    public void Execute_CannotRewriteHtml_SelectOption()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  @if (some_bool)
  {
  <head cool=""beans"">
    <select>
        <option value='1'>One</option>
        <option selected value='2'>Two</option>
        <option value='3'>Three</option>
    </select>
  </head>
  }
</html>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        Assert.Empty(documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>());
    }
 
    [Fact]
    public void Execute_CanRewriteHtml_OptionWithNoSelectAncestor()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  @if (some_bool)
  {
  <head cool=""beans"">
    <option value='1'>One</option>
    <option selected value='2'>Two</option>
  </head>
  }
</html>");
 
        var expected = NormalizeContent(@"
<head cool=""beans""><option value='1'>One</option>
    <option selected value='2'>Two</option></head>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    // The unclosed tag will have errors, so we won't rewrite it or its parent.
    [Fact]
    public void Execute_CannotRewriteHtml_Errors()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  <a href=""..."">
</html>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        Assert.Empty(documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>());
    }
 
    // We want duplicate attributes to result in an error and prevent rewriting.
    //
    // This is because Blazor de-duplicates attributes differently from browsers, so we don't
    // want to allow any markup blocks to exist with duplicate attributes or else they will have
    // the browser's behavior.
    [Fact]
    public void Execute_CannotRewriteHtml_DuplicateAttribute()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  <a href=""test1"" href=""test2""></a>
</html>");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        Assert.Empty(documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>());
 
        var diagnostic = Assert.Single(documentNode.GetAllDiagnostics());
        Assert.Same(ComponentDiagnosticFactory.DuplicateMarkupAttribute.Id, diagnostic.Id);
    }
 
    [Fact]
    public void Execute_RewritesHtml_MismatchedClosingTag()
    {
        // Arrange
        var document = CreateDocument(@"
<html>
  <div>
    <div>rewriteme</div>
  </span>
</html>");
 
        var expected = NormalizeContent("<div>rewriteme</div>\n  ");
 
        var documentNode = Lower(document);
 
        // Act
        Pass.Execute(document, documentNode);
 
        // Assert
        var block = documentNode.FindDescendantNodes<MarkupBlockIntermediateNode>().Single();
        Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
    }
 
    private string NormalizeContent(string content)
    {
        // Test inputs frequently have leading space for readability.
        content = content.TrimStart();
 
        // Normalize newlines since we are testing lengths of things.
        content = content.Replace("\r", "");
        content = content.Replace("\n", "\r\n");
 
        return content;
    }
 
    private RazorCodeDocument CreateDocument(string content)
    {
        // Normalize newlines since we are testing lengths of things.
        content = content.Replace("\r", "");
        content = content.Replace("\n", "\r\n");
 
        var source = RazorSourceDocument.Create(content, "test.cshtml");
        return ProjectEngine.CreateCodeDocument(source, RazorFileKind.Component);
    }
 
    private DocumentIntermediateNode Lower(RazorCodeDocument codeDocument)
    {
        foreach (var phase in Engine.Phases)
        {
            if (phase is IRazorCSharpLoweringPhase)
            {
                break;
            }
 
            codeDocument = phase.Execute(codeDocument);
        }
 
        var document = codeDocument.GetRequiredDocumentNode();
        Engine.GetFeatures<ComponentDocumentClassifierPass>().Single().Execute(codeDocument, document);
        Engine.GetFeatures<ComponentMarkupDiagnosticPass>().Single().Execute(codeDocument, document);
        return document;
    }
}