|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.NET.Sdk.Razor.SourceGenerators;
public sealed class RazorSourceGeneratorTagHelperTests : RazorSourceGeneratorTestsBase
{
[Fact]
public async Task CustomTagHelper()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<email>
custom tag helper
<email>nested tag helper</email>
</email>
"""
}, new()
{
["EmailTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
public class EmailTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Contains("EmailTagHelper", result.GeneratedSources.Single().SourceText.ToString());
result.VerifyOutputsMatchBaseline();
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8460")]
public async Task VoidTagName()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<col>markup</col>
@{ <col>code</col> }
""",
}, new()
{
["ColTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
public class ColTagHelper : TagHelper { }
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out _, verify: static compilation =>
{
// Malformed C# is generated due to everything after the <col> tag being considered C#.
Assert.Contains(compilation.GetDiagnostics(), static d => d.Severity == DiagnosticSeverity.Error);
});
// Assert
result.Diagnostics.Verify(
// Views/Home/Index.cshtml(3,5): error RZ1042: Component or tag helper 'col' must be self-closing because it's named like an HTML void element. To use child content, qualify such components with their namespace or use prefixes with such tag helpers.
Diagnostic("RZ1042").WithLocation(3, 5));
result.VerifyOutputsMatchBaseline();
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8460")]
public async Task VoidTagName_Prefixed()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@tagHelperPrefix th:
@addTagHelper *, TestProject
<th:col>markup</th:col>
@{ <th:col>code</th:col> }
""",
}, new()
{
["ColTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
public class ColTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "my-col";
}
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
result.Diagnostics.Verify();
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8460")]
public async Task VoidTagName_SelfClosing()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<col />
@{ <col /> }
""",
}, new()
{
["ColTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
public class ColTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "my-col";
}
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
result.Diagnostics.Verify();
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8460")]
public async Task VoidTagName_NoMatchingTagHelper()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<col>markup</col>
@{ <col>code</col> }
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out _, verify: static compilation =>
{
// Malformed C# is generated due to everything after the <col> tag being considered C#.
Assert.Contains(compilation.GetDiagnostics(), static d => d.Severity == DiagnosticSeverity.Error);
});
// Assert
result.Diagnostics.Verify();
result.VerifyOutputsMatchBaseline();
}
[Theory]
[InlineData("Index")]
[InlineData("CustomEncoder")]
[InlineData("NullEncoder")]
[InlineData("TwoEncoders")]
[InlineData("ThreeEncoders")]
public async Task Encoders(string name)
{
// Arrange
var project = CreateTestProject(new()
{
// https://github.com/dotnet/aspnetcore/tree/b40cc0b/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Encoders
["Views/Home/Index.cshtml"] = """
@addTagHelper TestProject.TagHelpers.DefaultEncoderTagHelper, TestProject
@{
var count = 0;
var innerCount = 0;
}
<pre>@($"Outer knows <b>{ ++count } < 4</b>")
<inner>@($"Inner knows <b>{ ++innerCount } < 4</b>")</inner></pre>
""",
["Views/Home/CustomEncoder.cshtml"] = """
@addTagHelper TestProject.TagHelpers.CustomEncoderTagHelper, TestProject
@{
var count = 0;
var innerCount = 0;
}
<pre>@($"Outer knows <b>{ ++count } < 4</b>")
<inner>@($"Inner knows <b>{ ++innerCount } < 4</b>")</inner></pre>
""",
["Views/Home/NullEncoder.cshtml"] = """
@addTagHelper TestProject.TagHelpers.NullEncoderTagHelper, TestProject
@{
var count = 0;
var innerCount = 0;
}
<pre>@($"Outer knows <b>{ ++count } < 4</b>")
<inner>@($"Inner knows <b>{ ++innerCount } < 4</b>")</inner></pre>
""",
["Views/Home/TwoEncoders.cshtml"] = """
@addTagHelper TestProject.TagHelpers.CustomEncoderTagHelper, TestProject
@addTagHelper TestProject.TagHelpers.NullEncoderTagHelper, TestProject
@{
var count = 0;
var innerCount = 0;
}
<pre>@($"Outer knows <b>{ ++count } < 4</b>")
<inner>@($"Inner knows <b>{ ++innerCount } < 4</b>")</inner></pre>
""",
["Views/Home/ThreeEncoders.cshtml"] = """
@addTagHelper TestProject.TagHelpers.CustomEncoderTagHelper, TestProject
@addTagHelper TestProject.TagHelpers.DefaultEncoderTagHelper, TestProject
@addTagHelper TestProject.TagHelpers.NullEncoderTagHelper, TestProject
@{
var count = 0;
var innerCount = 0;
}
<pre>@($"Outer knows <b>{ ++count } < 4</b>")
<inner>@($"Inner knows <b>{ ++innerCount } < 4</b>")</inner></pre>
""",
}, new()
{
["GlobalUsings.g.cs"] = """
global using System.IO;
global using System.Threading.Tasks;
""",
["DefaultEncoderTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement("pre")]
[HtmlTargetElement("inner")]
[OutputElementHint("pre")]
public class DefaultEncoderTagHelper : TagHelper
{
public override int Order => 2;
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var defaultContent = await output.GetChildContentAsync();
output.Content
.SetHtmlContent("Default encoder: ")
.AppendHtml(defaultContent);
output.TagName = "pre";
}
}
""",
["CustomEncoderTagHelper.cs"] = """
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement("pre")]
public class CustomEncoderTagHelper : TagHelper
{
public override int Order => 1;
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var encoder = new CustomEncoder();
var customContent = await output.GetChildContentAsync(encoder);
// Note this is very unsafe. Should always post-process content that may not be fully HTML encoded before
// writing it into a response. Here for example, could pass SetContent() a string and that would be
// HTML encoded later.
output.PreContent
.SetHtmlContent("Custom encoder: ")
.AppendHtml(customContent)
.AppendHtml("<br />");
}
// Simple encoder that just wraps "string" as "Custom[[string]]". Note: Lacks all parameter checks.
private class CustomEncoder : HtmlEncoder
{
public CustomEncoder()
{
}
public override int MaxOutputCharactersPerInputCharacter => 1;
public override string Encode(string value) => $"Custom[[{ value }]]";
public override void Encode(TextWriter output, char[] value, int startIndex, int characterCount)
{
if (characterCount == 0)
{
return;
}
output.Write("Custom[[");
output.Write(value, startIndex, characterCount);
output.Write("]]");
}
public override void Encode(TextWriter output, string value, int startIndex, int characterCount)
{
if (characterCount == 0)
{
return;
}
output.Write("Custom[[");
output.Write(value.Substring(startIndex, characterCount));
output.Write("]]");
}
public override unsafe int FindFirstCharacterToEncode(char* text, int textLength) => -1;
public override unsafe bool TryEncodeUnicodeScalar(
int unicodeScalar,
char* buffer,
int bufferLength,
out int numberOfCharactersWritten)
{
numberOfCharactersWritten = 0;
return false;
}
public override bool WillEncode(int unicodeScalar) => false;
}
}
""",
["NullEncoderTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement("pre")]
public class NullEncoderTagHelper : TagHelper
{
public override int Order => 3;
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var nullContent = await output.GetChildContentAsync(NullHtmlEncoder.Default);
// Note this is very unsafe. Should always post-process content that may not be fully HTML encoded before
// writing it into a response. Here for example, could pass SetContent() a string and that would be
// HTML encoded later.
output.PostContent
.SetHtmlContent("<br />Null encoder: ")
.AppendHtml(nullContent);
}
}
""",
});
project = project.WithCompilationOptions(((CSharpCompilationOptions)project.CompilationOptions!)
.WithAllowUnsafe(true));
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
RunGenerator(compilation!, ref driver, out compilation);
// Assert
await VerifyRazorPageMatchesBaselineAsync(compilation, $"Views_Home_{name}");
}
[Fact]
public async Task Form_Employee()
{
// Arrange
var project = CreateTestProject(new()
{
// https://github.com/dotnet/aspnetcore/blob/b40cc0b/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Employee/Create.cshtml
["Views/Home/Index.cshtml"] = """
@model TestProject.Models.Employee
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Html.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Create</title>
</head>
<body>
<form asp-antiforgery="false">
<div class="form-horizontal">
<h4>Employee</h4>
<hr />
<div asp-validation-summary="All" class="text-danger">
</div>
<div class="form-group">
<label asp-for="Age" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="Age" class="form-control" />
<span asp-validation-for="Age"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Email" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email"></span>
</div>
</div>
<input type="hidden" asp-for="EmployeeId" value="0" />
<div class="form-group">
<label asp-for="FullName" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="FullName" class="form-control" />
<span asp-validation-for="FullName"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Gender" class="control-label col-md-2"></label>
<div class="col-md-10">
<input type="radio" asp-for="Gender" value="M" /> Male
<input type="radio" asp-for="Gender" value="F" /> Female
<span asp-validation-for="Gender"></span>
</div>
</div>
<div class="form-group">
<label asp-for="JoinDate" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="JoinDate" class="form-control" />
<span asp-validation-for="JoinDate"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Salary" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="Salary" class="form-control" />
<span asp-validation-for="Salary"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
</body>
</html>
""",
}, new()
{
["Models/Employee.cs"] = """
using System;
using System.ComponentModel.DataAnnotations;
namespace TestProject.Models;
public class Employee
{
public int EmployeeId { get; set; }
[Display(Name = "Full Name", ShortName = "FN")]
public string? FullName { get; set; }
[DisplayFormat(NullDisplayText = "Not specified")]
public string? Gender { get; set; }
[Range(10, 100)]
public int Age { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTimeOffset? JoinDate { get; set; }
[DataType(DataType.EmailAddress)]
public string? Email { get; set; }
[DisplayFormat(NullDisplayText = "Not specified")]
public int? Salary { get; set; }
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
RunGenerator(compilation!, ref driver, out compilation);
// Assert
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact]
public async Task TagHelpersWebSite()
{
// Arrange
var project = CreateTestProject(new()
{
// https://github.com/dotnet/aspnetcore/blob/b40cc0b/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Home/Index.cshtml
["Views/Home/Index.cshtml"] = """
@using TestProject.Models
@using Microsoft.AspNetCore.Mvc.Razor
@model WebsiteContext
@{
ViewBag.Title = "Home Page";
ViewData.Model = new()
{
Approved = false,
CopyrightYear = 2015,
Version = new Version(1, 3, 3, 7),
TagsToShow = 20
};
}
@addTagHelper *, TestProject
@section css {
<style condition="!Model.Approved">
h1 {
color:red;
font-size:2em;
}
</style>
}
@functions {
public void RenderTemplate(string title, Func<string, HelperResult> template)
{
Output.WriteLine("<br /><p><em>Rendering Template:</em></p>");
var helperResult = template(title);
helperResult.WriteTo(Output, HtmlEncoder);
}
}
<div condition="!Model.Approved">
<p>This website has <strong surround="em">not</strong> been approved yet. Visit www.contoso.com for <strong make-pretty="false">more</strong> information.</p>
</div>
<div>
<h3>Current Tag Cloud from Tag Helper</h3>
<tag-cloud count="Model.TagsToShow" surround="div" />
<h3>Current Tag Cloud from ViewComponentHelper:</h3>
<section bold>@await Component.InvokeAsync("Tags", new { count = 15 })</section>
@{
RenderTemplate(
"Tag Cloud from Template: ",
@<div condition="true"><h3>@item</h3><tag-cloud count="Model.TagsToShow"></tag-cloud></div>);
}
</div>
<div>
<h3>Dictionary Valued Model Expression</h3>
<div prefix-test1="@Model.TagsToShow" prefix-test2="@Model.Version.Build"></div>
</div>
@section footerContent {
<p condition="Model.Approved" bold surround="section">© @Model.CopyrightYear - My ASP.NET Application</p>
}
""",
["Views/_ViewStart.cshtml"] = """
@{
Layout = "/Views/Shared/_Layout.cshtml";
}
""",
["Views/Shared/_Layout.cshtml"] = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewBag.Title - My ASP.NET Application</title>
@RenderSection("css", required: false)
</head>
<body>
<h1>ASP.NET vNext - @ViewBag.Title</h1>
<div>
@RenderBody()
<hr />
<footer>
@RenderSection("footerContent", required: false)
</footer>
</div>
</body>
</html>
"""
}, new()
{
["GlobalUsings.g.cs"] = """
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Threading.Tasks;
""",
["Models/WebsiteContext.cs"] = """
namespace TestProject.Models;
public class WebsiteContext
{
public Version Version { get; set; }
public int CopyrightYear { get; set; }
public bool Approved { get; set; }
public int TagsToShow { get; set; }
}
""",
["AutoLinkerTagHelper.cs"] = """
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement("p")]
public class AutoLinkerTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.AppendHtml(Regex.Replace(
childContent.GetContent(),
@"\b(?:https?://|www\.)(\S+)\b",
"<strong><a target=\"_blank\" href=\"http://$0\">$0</a></strong>"));
}
}
""",
["BoldTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override int Order
{
get
{
return int.MinValue;
}
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.AppendHtml("<b>");
output.PostContent.AppendHtml("</b>");
}
}
""",
["ConditionTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement("div")]
[HtmlTargetElement("style")]
[HtmlTargetElement("p")]
public class ConditionTagHelper : TagHelper
{
public bool? Condition { get; set; }
public override int Order
{
get
{
// Run after other tag helpers targeting the same element. Other tag helpers have Order <= 0.
return 1000;
}
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
// If a condition is set and evaluates to false, don't render the tag.
if (Condition.HasValue && !Condition.Value)
{
output.SuppressOutput();
}
}
}
""",
["DictionaryPrefixTestTagHelper.cs"] = """
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement(Attributes = "prefix-*")]
public class DictionaryPrefixTestTagHelper : TagHelper
{
[HtmlAttributeName(DictionaryAttributePrefix = "prefix-")]
public IDictionary<string, ModelExpression> PrefixValues { get; set; } = new Dictionary<string, ModelExpression>();
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var ulTag = new TagBuilder("ul");
foreach (var item in PrefixValues)
{
var liTag = new TagBuilder("li");
liTag.InnerHtml.Append(item.Value.Name);
ulTag.InnerHtml.AppendHtml(liTag);
}
output.Content.SetHtmlContent(ulTag);
}
}
""",
["PrettyTagHelper.cs"] = """
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement("*")]
public class PrettyTagHelper : TagHelper
{
private static readonly Dictionary<string, string> PrettyTagStyles =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "a", "background-color: gray;color: white;border-radius: 3px;"
+ "border: 1px solid black;padding: 3px;font-family: cursive;" },
{ "strong", "font-size: 1.25em;text-decoration: underline;" },
{ "h1", "font-family: cursive;" },
{ "h3", "font-family: cursive;" }
};
public bool? MakePretty { get; set; }
public string Style { get; set; }
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (MakePretty.HasValue && !MakePretty.Value)
{
return;
}
if (output.TagName == null)
{
// Another tag helper e.g. TagCloudViewComponentTagHelper has suppressed the start and end tags.
return;
}
string prettyStyle;
if (PrettyTagStyles.TryGetValue(output.TagName, out prettyStyle))
{
var style = Style ?? string.Empty;
if (!string.IsNullOrEmpty(style))
{
style += ";";
}
output.Attributes.SetAttribute("style", style + prettyStyle);
}
}
}
""",
["SurroundTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement(Attributes = nameof(Surround))]
public class SurroundTagHelper : TagHelper
{
public override int Order
{
get
{
// Run first
return int.MinValue;
}
}
public string Surround { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var surroundingTagName = Surround.ToLowerInvariant();
output.PreElement.AppendHtml($"<{surroundingTagName}>");
output.PostElement.AppendHtml($"</{surroundingTagName}>");
}
}
""",
["TagCloudViewComponentTagHelper.cs"] = """
using System.Reflection;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MvcSample.Web.Components;
[HtmlTargetElement("tag-cloud")]
[ViewComponent(Name = "Tags")]
public class TagCloudViewComponentTagHelper : ITagHelper
{
private static readonly string[] Tags =
("Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" +
"Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat Duis aute irure " +
"dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat" +
"non proident, sunt in culpa qui officia deserunt mollit anim id est laborum")
.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.ToArray();
private readonly HtmlEncoder _htmlEncoder;
public TagCloudViewComponentTagHelper(HtmlEncoder htmlEncoder)
{
_htmlEncoder = htmlEncoder;
}
public int Count { get; set; }
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
public int Order { get; } = 0;
public void Init(TagHelperContext context)
{
}
public async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var result = await InvokeAsync(Count);
var writer = new StringWriter();
var viewComponentDescriptor = new ViewComponentDescriptor()
{
TypeInfo = typeof(TagCloudViewComponentTagHelper).GetTypeInfo(),
ShortName = "TagCloudViewComponentTagHelper",
FullName = "TagCloudViewComponentTagHelper",
};
await result.ExecuteAsync(new ViewComponentContext(
viewComponentDescriptor,
new Dictionary<string, object>(),
_htmlEncoder,
ViewContext,
writer));
output.TagName = null;
output.Content.AppendHtml(writer.ToString());
}
public async Task<IViewComponentResult> InvokeAsync(int count)
{
var tags = await GetTagsAsync(count);
return new ContentViewComponentResult(string.Join(",", tags));
}
private Task<string[]> GetTagsAsync(int count)
{
return Task.FromResult(GetTags(count));
}
private string[] GetTags(int count)
{
return Tags.Take(count).ToArray();
}
}
""",
});
project = project.WithCompilationOptions(((CSharpCompilationOptions)project.CompilationOptions!)
.WithNullableContextOptions(CodeAnalysis.NullableContextOptions.Disable));
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
RunGenerator(compilation!, ref driver, out compilation);
// Assert
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact]
public async Task UnboundDynamicAttributes()
{
// Arrange
var project = CreateTestProject(new()
{
// https://github.com/dotnet/aspnetcore/blob/b40cc0b/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Home/UnboundDynamicAttributes.cshtml
["Views/Home/Index.cshtml"] = """
@addTagHelper AddProcessedAttributeTagHelper, TestProject
@{
var trueVar = true;
var falseVar = false;
var stringVar = "value";
string? nullVar = null;
}
@functions {
public Task DoSomething()
{
return Task.FromResult(true);
}
}
<input checked="@true" />
<input checked="@trueVar" />
<input checked="@false" />
<input checked="@falseVar" />
<input checked=" @true " />
<input checked=" @falseVar " />
<input checked=" @stringVar: @trueVar " />
<input checked=" value: @false " />
<input checked="@true @trueVar" />
<input checked=" @falseVar @true" />
<input checked="@null" />
<input checked=" @nullVar" />
<input checked="@nullVar " />
<input checked=" @null @stringVar @trueVar" />
<input checked=" @if (trueVar) { <text>True</text> } else { await DoSomething(); <text>False</text> } " />
"""
}, new()
{
["AddProcessedAttributeTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
[HtmlTargetElement("input")]
public class AddProcessedAttributeTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.Add(new TagHelperAttribute("processed"));
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
RunGenerator(compilation!, ref driver, out compilation);
// Assert
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact]
public async Task ViewComponent()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
@{
var num = 42;
}
<vc:test text="Razor" number="@num" flag />
""",
}, new()
{
["TestViewComponent.cs"] = """
public class TestViewComponent
{
public string Invoke(string text, int number, bool flag)
{
return text;
}
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Contains("HtmlTargetElementAttribute(\"vc:test\")", result.GeneratedSources.Single().SourceText.ToString());
result.VerifyOutputsMatchBaseline();
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact]
public async Task ViewComponentTagHelpers()
{
// Arrange
var project = CreateTestProject(new()
{
// https://github.com/dotnet/aspnetcore/blob/b40cc0b/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Home/ViewComponentTagHelpers.cshtml
["Views/Home/Index.cshtml"] = """
@addTagHelper "*, TestProject"
@{
var year = 2016;
var dict = new Dictionary<string, List<string>>();
var items = new List<string>() { "One", "Two", "Three" };
dict.Add("Foo", items);
}
<vc:generic items="dict"></vc:generic>
<!-- <vc:generic items-foo="items"></vc:generic> -->
<vc:duck beak-color="Green" /><br />
<div>
<vc:copyright website="example.com" year="@year" bold></vc:copyright>
</div>
""",
["Views/Shared/Components/Generic/Default.cshtml"] = """
@model Dictionary<string, List<string>>
<div>Items: </div>
<div>
@foreach (var item in Model)
{
<strong>@item.Key</strong><br/>
@foreach (var value in Model[item.Key])
{
<span>@value</span>
}
}
</div>
""",
["Views/Shared/Components/Duck/Default.cshtml"] = """
@model string
<div id="ascii" style="font-family:Courier New, Courier, monospace; font-size: 6px">
@Html.Raw(Model)
</div>
""",
["Views/Shared/Components/Copyright/Default.cshtml"] = """
@model Dictionary<string, object>
<footer>Copyright @Model["year"] @Model["website"]</footer>
""",
}, new()
{
["GlobalUsings.g.cs"] = """
global using System;
global using System.Collections.Generic;
""",
["BoldTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override int Order
{
get
{
return int.MinValue;
}
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.AppendHtml("<b>");
output.PostContent.AppendHtml("</b>");
}
}
""",
["GenericViewComponent.cs"] = """
using Microsoft.AspNetCore.Mvc;
public class GenericViewComponent : ViewComponent
{
public IViewComponentResult Invoke(Dictionary<string, List<string>> items)
{
return View(items);
}
}
""",
["CopyrightViewComponent.cs"] = """
using Microsoft.AspNetCore.Mvc;
public class CopyrightViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string website, int year)
{
var dict = new Dictionary<string, object>
{
["website"] = website,
["year"] = year
};
return View(dict);
}
}
""",
["BeakColor.cs"] = """
public enum BeakColor
{
Red,
Blue,
Green,
Navy,
Brown,
Purple
}
""",
["DuckViewComponent.cs"] = """
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
public class DuckViewComponent : ViewComponent
{
public IViewComponentResult Invoke(BeakColor beakColor)
{
var colorReplacement = string.Format(CultureInfo.InvariantCulture, "<span style='color:{0}'><</span>", beakColor);
var resultString = DuckString
.Replace("<", colorReplacement)
.Replace(Environment.NewLine, "<br>")
.Replace("\n", "<br>");
return View<string>(resultString);
}
private const string DuckString = @"
__
<(o )___
( ._> /
`---'
";
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
RunGenerator(compilation!, ref driver, out compilation);
// Assert
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Theory]
[InlineData("Views_Home_NestedViewImportsTagHelper")]
[InlineData("Views_Shared_ViewWithLayoutAndNestedTagHelper")]
[InlineData("Views_RemoveInheritedTagHelpers_ViewWithInheritedRemoveTagHelper")]
[InlineData("Views_InheritedTagHelperPrefix_InheritedTagHelperPrefix")]
[InlineData("Views_InheritedTagHelperPrefix_OverriddenTagHelperPrefix")]
[InlineData("Views_InheritedTagHelperPrefix_NestedInheritedTagHelperPrefix_NestedInheritedTagHelperPrefix")]
[InlineData("Views_InheritedTagHelperPrefix_NestedInheritedTagHelperPrefix_NestedOverriddenTagHelperPrefix")]
[InlineData("Views_RemoveDefaultInheritedTagHelpers_Index")]
public async Task ViewImports(string name)
{
// Arrange
// https://github.com/dotnet/aspnetcore/blob/b40cc0b/src/Mvc/test/Mvc.FunctionalTests/TagHelpersTest.cs#L120
var project = CreateTestProject(new()
{
["Views/_ViewImports.cshtml"] = """
@addTagHelper TestProject.TagHelpers.RootViewStartTagHelper, TestProject
""",
["Views/Home/_ViewImports.cshtml"] = """
@addTagHelper TestProject.TagHelpers.NestedViewImportsTagHelper, TestProject
""",
["Views/Home/NestedViewImportsTagHelper.cshtml"] = """
<root></root>
<nested>some-content</nested>
""",
["Views/Shared/_LayoutWithRootTagHelper.cshtml"] = """
layout:<root></root>
@RenderBody()
""",
["Views/Shared/ViewWithLayoutAndNestedTagHelper.cshtml"] = """
@{
Layout = "~/Views/Shared/_LayoutWithRootTagHelper.cshtml";
}
@addTagHelper TestProject.TagHelpers.NestedViewImportsTagHelper, TestProject
<nested>some-content</nested>
""",
["Views/RemoveInheritedTagHelpers/_ViewStart.cshtml"] = """
@{
Layout = "~/Views/Shared/_LayoutWithRootTagHelper.cshtml";
}
""",
["Views/RemoveInheritedTagHelpers/_ViewImports.cshtml"] = """
@removeTagHelper TestProject.TagHelpers.RootViewStartTagHelper, TestProject
@addTagHelper TestProject.TagHelpers.NestedViewImportsTagHelper, TestProject
""",
["Views/RemoveInheritedTagHelpers/ViewWithInheritedRemoveTagHelper.cshtml"] = """
page:<root/>
<nested>some-content</nested>
""",
["Views/InheritedTagHelperPrefix/_ViewStart.cshtml"] = """
@{
Layout = "~/Views/Shared/_LayoutWithRootTagHelper.cshtml";
}
""",
["Views/InheritedTagHelperPrefix/_ViewImports.cshtml"] = """
@tagHelperPrefix inherited:
""",
["Views/InheritedTagHelperPrefix/InheritedTagHelperPrefix.cshtml"] = """
page:<inherited:root></inherited:root>
""",
["Views/InheritedTagHelperPrefix/OverriddenTagHelperPrefix.cshtml"] = """
@tagHelperPrefix overridden
page:<overriddenroot></overriddenroot>
""",
["Views/InheritedTagHelperPrefix/NestedInheritedTagHelperPrefix/_ViewImports.cshtml"] = """
@tagHelperPrefix nested-
""",
["Views/InheritedTagHelperPrefix/NestedInheritedTagHelperPrefix/NestedInheritedTagHelperPrefix.cshtml"] = """
page:<nested-root></nested-root>
""",
["Views/InheritedTagHelperPrefix/NestedInheritedTagHelperPrefix/NestedOverriddenTagHelperPrefix.cshtml"] = """
@tagHelperPrefix nested-overridden
page:<nested-overriddenroot></nested-overriddenroot>
""",
["Views/RemoveDefaultInheritedTagHelpers/_ViewImports.cshtml"] = """
@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor
""",
["Views/RemoveDefaultInheritedTagHelpers/Index.cshtml"] = """
<a href="~/VirtualPath">Virtual path</a>
""",
}, new()
{
["RootViewStartTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement("root")]
public class RootViewStartTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Content.AppendHtml("root-content");
}
}
""",
["NestedViewImportsTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
[HtmlTargetElement("nested")]
public class NestedViewImportsTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Content.AppendHtml("nested-content");
}
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
RunGenerator(compilation!, ref driver, out compilation);
// Assert
await VerifyRazorPageMatchesBaselineAsync(compilation, name);
}
[Fact]
public async Task WebsiteInformationTagHelper()
{
// Arrange
var project = CreateTestProject(new()
{
// https://github.com/dotnet/aspnetcore/blob/b40cc0b/src/Mvc/test/WebSites/TagHelpersWebSite/Views/Home/About.cshtml
["Views/Home/Index.cshtml"] = """
@using TestProject.Models
@{
ViewBag.Title = "About";
}
@addTagHelper ATagHelper, TestProject
@addTagHelper WebsiteInformationTagHelper, TestProject
<div>
<p>Hello, you've reached the about page.</p>
<h3>Information about our website (outdated):</h3>
<website-information info="new WebsiteContext {
Version = new Version(1, 1),
CopyrightYear = 1990,
Approved = true,
TagsToShow = 30 }"/>
</div>
"""
}, new()
{
["Models/WebsiteContext.cs"] = """
using System;
namespace TestProject.Models;
public class WebsiteContext
{
public required Version Version { get; set; }
public int CopyrightYear { get; set; }
public bool Approved { get; set; }
public int TagsToShow { get; set; }
}
""",
["WebsiteInformationTagHelper.cs"] = """
using System;
using System.Globalization;
using Microsoft.AspNetCore.Razor.TagHelpers;
using TestProject.Models;
public class WebsiteInformationTagHelper : TagHelper
{
public required WebsiteContext Info { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "section";
output.PostContent.AppendHtml(string.Format(
CultureInfo.InvariantCulture,
"<p><strong>Version:</strong> {0}</p>" + Environment.NewLine +
"<p><strong>Copyright Year:</strong> {1}</p>" + Environment.NewLine +
"<p><strong>Approved:</strong> {2}</p>" + Environment.NewLine +
"<p><strong>Number of tags to show:</strong> {3}</p>" + Environment.NewLine,
Info.Version,
Info.CopyrightYear,
Info.Approved,
Info.TagsToShow));
output.TagMode = TagMode.StartTagAndEndTag;
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
RunGenerator(compilation!, ref driver, out compilation);
// Assert
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task ComponentAndTagHelper()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<email mail="example">custom tag helper</email>
""",
["Shared/EmailTagHelper.razor"] = """
@inherits ComponentAndTagHelper
@code {
public string? Mail { get; set; }
}
""",
}, new()
{
["EmailTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyApp.Shared;
public abstract class ComponentAndTagHelper : TagHelper
{
protected abstract void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder);
}
public partial class EmailTagHelper : ComponentAndTagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", $"mailto:{Mail}");
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(2, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task ComponentAndTagHelper_HtmlTargetElement()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<email mail="example1">inside email</email>
<mail mail="example2">inside mail</mail>
""",
["Shared/EmailTagHelper.razor"] = """
@using Microsoft.AspNetCore.Razor.TagHelpers;
@attribute [HtmlTargetElement("mail")]
@inherits ComponentAndTagHelper
@code {
public string? Mail { get; set; }
}
""",
}, new()
{
["EmailTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyApp.Shared;
public abstract class ComponentAndTagHelper : TagHelper
{
protected abstract void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder);
}
public partial class EmailTagHelper : ComponentAndTagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", $"mailto:{Mail}");
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(2, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/aspnetcore/pull/49034#issuecomment-1608571858")]
public async Task UrlResolutionTagHelper()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@page
<img src="~/images/red.png" alt="Red block" title="<the title>" id="1">
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Single(result.GeneratedSources);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/aspnetcore/pull/49034#issuecomment-1608571858")]
public async Task GlobalPrefixedDirective()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<vc:component1 />
<vc:component2 />
<vc:component3 />
<vc:component4 />
<vc:component5 />
<vc:component6 />
""",
["Views/Shared/Components/Component1/Default.cshtml"] = """
<email>no directive</email>
""",
["Views/Shared/Components/Component2/Default.cshtml"] = """
@addTagHelper global::TestProject.TagHelpers.EmailTagHelper, TestProject
<email>full name</email>
""",
["Views/Shared/Components/Component3/Default.cshtml"] = """
@addTagHelper global::TestProject.TagHelpers.*, TestProject
<email>namespace prefix</email>
""",
["Views/Shared/Components/Component4/Default.cshtml"] = """
@addTagHelper global::*, TestProject
<email>global prefix</email>
""",
["Views/Shared/Components/Component5/Default.cshtml"] = """
@addTagHelper global::TestProject.TagHelpers.EmailTagHelper, TestProject
@removeTagHelper TestProject.TagHelpers.EmailTagHelper, TestProject
<email>add global, remove simple</email>
""",
["Views/Shared/Components/Component6/Default.cshtml"] = """
@addTagHelper TestProject.TagHelpers.EmailTagHelper, TestProject
@removeTagHelper global::TestProject.TagHelpers.EmailTagHelper, TestProject
<email>add simple, remove global</email>
""",
}, new()
{
["ViewComponents.cs"] = """
using Microsoft.AspNetCore.Mvc;
public class BaseComponent : ViewComponent
{
public IViewComponentResult Invoke() => View();
}
public class Component1 : BaseComponent { }
public class Component2 : BaseComponent { }
public class Component3 : BaseComponent { }
public class Component4 : BaseComponent { }
public class Component5 : BaseComponent { }
public class Component6 : BaseComponent { }
""",
["EmailTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TestProject.TagHelpers;
public class EmailTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(7, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/aspnetcore/issues/49728")]
public async Task Suppression()
{
// Arrange
var project = CreateTestProject(new()
{
["Pages/Index.cshtml"] = """
@addTagHelper *, TestProject
<email />
<!email />
<!email></!email>
< email />
<@("email") />
@(await Html.RenderComponentAsync<MyApp.Shared.Component1>(RenderMode.Static))
""",
["Shared/Component1.razor"] = """
Component1:
<Component2 />
<!Component2 />
<!Component2></!Component2>
< Component2 />
<@("Component2") />
""",
["Shared/Component2.razor"] = """
Component2
""",
}, new()
{
["EmailTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
public class EmailTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
}
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project, options =>
{
options.AdditionalTextOptions["Pages/Index.cshtml"]["build_metadata.AdditionalFiles.CssScope"] = "cshtml-scope";
options.AdditionalTextOptions["Shared/Component1.razor"]["build_metadata.AdditionalFiles.CssScope"] = "razor-scope";
});
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
result.Diagnostics.Verify();
Assert.Equal(3, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Pages_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/10186")]
public async Task EscapedIdentifierInRazorBlock()
{
var project = CreateTestProject(new(){
["Views/Home/Index.cshtml"] = """
@using Demo
@addTagHelper *, TestProject
@{
ViewData["Title"] = "Home page";
int count = 0;
}
<div>
<mytag myattr='Convert.ToInt32(@count)'></mytag>
</div>
"""
}, new()
{
["MyTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
namespace Demo
{
[HtmlTargetElement("mytag", Attributes = "myattr")]
public class MyTagHelper : TagHelper
{
[HtmlAttributeName("myattr")]
public int MyAttr { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.SetAttribute("out", MyAttr.ToString());
}
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
result.Diagnostics.Verify();
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/10426")]
public async Task EscapedExpressions()
{
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@using Demo
@addTagHelper *, TestProject
@{
string x(string s) => "x:" + s;
}
<div>
<mytag myattr='@new string("a, b")'></mytag>
<mytag myattr='new string("a, b")'></mytag>
<mytag myattr="@new string(@x("c, d"))"></mytag>
<mytag myattr="new string(@x("c, d"))"></mytag>
</div>
""",
}, new()
{
["MyTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
namespace Demo
{
[HtmlTargetElement("mytag", Attributes = "myattr")]
public class MyTagHelper : TagHelper
{
[HtmlAttributeName("myattr")]
public object? MyAttr { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.SetAttribute("out", MyAttr?.ToString());
}
}
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
result.Diagnostics.Verify();
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
}
|