diff --git a/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..46ac17a8733c 100644 --- a/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +*REMOVED*~override Microsoft.AspNetCore.Mvc.TagHelpers.ScriptTagHelper.Process(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) -> void +~override Microsoft.AspNetCore.Mvc.TagHelpers.ScriptTagHelper.ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) -> System.Threading.Tasks.Task diff --git a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs index 64cc1be2f694..85952ce0865a 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs @@ -235,15 +235,29 @@ private StringWriter StringWriter } /// - public override void Process(TagHelperContext context, TagHelperOutput output) + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(output); if (string.Equals(Type, "importmap", StringComparison.OrdinalIgnoreCase)) { - // This is an importmap script, we'll write out the import map and - // stop processing. + // Do not update the content if another tag helper targeting this element has already done so. + if (output.IsContentModified) + { + return; + } + + // This is an importmap script, check if there's existing content first. + var childContent = await output.GetChildContentAsync(); + if (!childContent.IsEmptyOrWhiteSpace) + { + // User provided existing content; preserve it. + output.Content.SetHtmlContent(childContent); + return; + } + + // No existing content, so we can apply import map logic. var importMap = ImportMap ?? ViewContext.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); if (importMap == null) { @@ -252,10 +266,10 @@ public override void Process(TagHelperContext context, TagHelperOutput output) return; } + output.Content.SetHtmlContent(importMap.ToString()); output.TagName = "script"; output.TagMode = TagMode.StartTagAndEndTag; output.Attributes.SetAttribute("type", "importmap"); - output.Content.SetHtmlContent(importMap.ToString()); return; } diff --git a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs index 18ffbc3cd10f..11fec3545440 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs @@ -30,7 +30,7 @@ public class ScriptTagHelperTest [InlineData("abcd.js", "test.js", "test.js")] [InlineData(null, "~/test.js", "virtualRoot/test.js")] [InlineData("abcd.js", "~/test.js", "virtualRoot/test.js")] - public void Process_SrcDefaultsToTagHelperOutputSrcAttributeAddedByOtherTagHelper( + public async Task ProcessAsync_SrcDefaultsToTagHelperOutputSrcAttributeAddedByOtherTagHelper( string src, string srcOutput, string expectedSrcPrefix) @@ -66,7 +66,7 @@ public void Process_SrcDefaultsToTagHelperOutputSrcAttributeAddedByOtherTagHelpe helper.Src = src; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal( @@ -77,7 +77,7 @@ public void Process_SrcDefaultsToTagHelperOutputSrcAttributeAddedByOtherTagHelpe [Theory] [MemberData(nameof(LinkTagHelperTest.MultiAttributeSameNameData), MemberType = typeof(LinkTagHelperTest))] - public void HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList outputAttributes) + public async Task HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList outputAttributes) { // Arrange var allAttributes = new TagHelperAttributeList( @@ -107,7 +107,7 @@ public void HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList ou expectedAttributes.Add(new TagHelperAttribute("src", "/blank.js")); // Act - helper.Process(tagHelperContext, output); + await helper.ProcessAsync(tagHelperContext, output); // Assert Assert.Equal(expectedAttributes, output.Attributes); @@ -268,7 +268,7 @@ public static TheoryData> RunsWh [Theory] [MemberData(nameof(RunsWhenRequiredAttributesArePresent_Data))] - public void RunsWhenRequiredAttributesArePresent( + public async Task RunsWhenRequiredAttributesArePresent( TagHelperAttributeList attributes, Action setProperties) { @@ -287,7 +287,7 @@ public void RunsWhenRequiredAttributesArePresent( setProperties(helper); // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.NotNull(output.TagName); @@ -355,7 +355,7 @@ public static TheoryData> RunsWh [Theory] [MemberData(nameof(RunsWhenRequiredAttributesArePresent_NoSrc_Data))] - public void RunsWhenRequiredAttributesArePresent_NoSrc( + public async Task RunsWhenRequiredAttributesArePresent_NoSrc( TagHelperAttributeList attributes, Action setProperties) { @@ -374,7 +374,7 @@ public void RunsWhenRequiredAttributesArePresent_NoSrc( setProperties(helper); // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Null(output.TagName); @@ -449,7 +449,7 @@ public static TheoryData> DoesNo [Theory] [MemberData(nameof(DoesNotRunWhenARequiredAttributeIsMissing_Data))] - public void DoesNotRunWhenARequiredAttributeIsMissing( + public async Task DoesNotRunWhenARequiredAttributeIsMissing( TagHelperAttributeList attributes, Action setProperties) { @@ -462,7 +462,7 @@ public void DoesNotRunWhenARequiredAttributeIsMissing( setProperties(helper); // Act - helper.Process(tagHelperContext, output); + await helper.ProcessAsync(tagHelperContext, output); // Assert Assert.NotNull(output.TagName); @@ -472,7 +472,7 @@ public void DoesNotRunWhenARequiredAttributeIsMissing( } [Fact] - public void DoesNotRunWhenAllRequiredAttributesAreMissing() + public async Task DoesNotRunWhenAllRequiredAttributesAreMissing() { // Arrange var tagHelperContext = MakeTagHelperContext(); @@ -482,7 +482,7 @@ public void DoesNotRunWhenAllRequiredAttributesAreMissing() var helper = GetHelper(); // Act - helper.Process(tagHelperContext, output); + await helper.ProcessAsync(tagHelperContext, output); // Assert Assert.Equal("script", output.TagName); @@ -492,7 +492,7 @@ public void DoesNotRunWhenAllRequiredAttributesAreMissing() } [Fact] - public void PreservesOrderOfNonSrcAttributes() + public async Task PreservesOrderOfNonSrcAttributes() { // Arrange var tagHelperContext = MakeTagHelperContext( @@ -518,7 +518,7 @@ public void PreservesOrderOfNonSrcAttributes() helper.Src = "/blank.js"; // Act - helper.Process(tagHelperContext, output); + await helper.ProcessAsync(tagHelperContext, output); // Assert Assert.Equal("data-extra", output.Attributes[0].Name); @@ -527,7 +527,7 @@ public void PreservesOrderOfNonSrcAttributes() } [Fact] - public void RendersScriptTagsForGlobbedSrcResults() + public async Task RendersScriptTagsForGlobbedSrcResults() { // Arrange var expectedContent = "" + @@ -552,7 +552,7 @@ public void RendersScriptTagsForGlobbedSrcResults() helper.SrcInclude = "**/*.js"; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -562,7 +562,7 @@ public void RendersScriptTagsForGlobbedSrcResults() } [Fact] - public void RendersScriptTagsForGlobbedSrcResults_EncodesAsExpected() + public async Task RendersScriptTagsForGlobbedSrcResults_EncodesAsExpected() { // Arrange var expectedContent = @@ -605,7 +605,7 @@ public void RendersScriptTagsForGlobbedSrcResults_EncodesAsExpected() helper.SrcInclude = "**/*.js"; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -615,7 +615,7 @@ public void RendersScriptTagsForGlobbedSrcResults_EncodesAsExpected() } [Fact] - public void RenderScriptTags_WithFileVersion() + public async Task RenderScriptTags_WithFileVersion() { // Arrange var context = MakeTagHelperContext( @@ -631,7 +631,7 @@ public void RenderScriptTags_WithFileVersion() helper.AppendVersion = true; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -642,7 +642,7 @@ public void RenderScriptTags_WithFileVersion() [InlineData("~/js/site.js", "/js/site.fingerprint.js")] [InlineData("/js/site.js", "/js/site.fingerprint.js")] [InlineData("js/site.js", "js/site.fingerprint.js")] - public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src, string expected) + public async Task RenderScriptTags_WithFileVersion_UsingResourceCollection(string src, string expected) { // Arrange var context = MakeTagHelperContext( @@ -661,7 +661,7 @@ public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src, helper.AppendVersion = true; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -671,7 +671,7 @@ public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src, [Theory] [InlineData("~/js/site.js")] [InlineData("/approot/js/site.js")] - public void RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection(string path) + public async Task RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection(string path) { // Arrange var context = MakeTagHelperContext( @@ -699,7 +699,7 @@ public void RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection(st helper.AppendVersion = true; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -707,7 +707,7 @@ public void RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection(st } [Fact] - public void ScriptTagHelper_RendersProvided_ImportMap() + public async Task ScriptTagHelper_RendersProvided_ImportMap() { // Arrange var importMap = new ImportMapDefinition( @@ -744,7 +744,7 @@ public void ScriptTagHelper_RendersProvided_ImportMap() helper.ImportMap = importMap; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -752,7 +752,7 @@ public void ScriptTagHelper_RendersProvided_ImportMap() } [Fact] - public void ScriptTagHelper_RendersImportMap_FromEndpoint() + public async Task ScriptTagHelper_RendersImportMap_FromEndpoint() { // Arrange var importMap = new ImportMapDefinition( @@ -787,13 +787,66 @@ public void ScriptTagHelper_RendersImportMap_FromEndpoint() helper.Type = "importmap"; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); Assert.Equal(importMap.ToJson(), output.Content.GetContent()); } + [Fact] + public async Task ScriptTagHelper_PreservesExplicitImportMapContent_WhenUserProvidesContent() + { + // Arrange - this simulates the user's scenario where they provide explicit importmap content + var context = MakeTagHelperContext(attributes: [new TagHelperAttribute("type", "importmap")]); + + // Simulate user providing explicit content + var childContent = new DefaultTagHelperContent(); + childContent.SetHtmlContent(@"{""imports"":{""jquery"":""https://code.jquery.com/jquery.js""}}"); + + var output = MakeTagHelperOutput("script", attributes: [new TagHelperAttribute("type", "importmap")], childContent: childContent); + + var helper = GetHelper(); + helper.Type = "importmap"; + + // No endpoint with ImportMapDefinition - this should NOT suppress the output + // since user provided explicit content + + // Act + await helper.ProcessAsync(context, output); + + // Assert + Assert.Equal("script", output.TagName); // Tag should not be suppressed + Assert.Equal("importmap", output.Attributes["type"].Value); + // The user's explicit content should be preserved + Assert.Equal(@"{""imports"":{""jquery"":""https://code.jquery.com/jquery.js""}}", output.Content.GetContent()); + } + + [Fact] + public async Task ScriptTagHelper_SuppressesOutput_WhenNoContentAndNoImportMapDefinition() + { + // Arrange - this simulates an empty importmap script with no definition + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + new TagHelperAttribute("type", "importmap"), + }); + + var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); + // No content provided + + var helper = GetHelper(); + helper.Type = "importmap"; + // No endpoint with ImportMapDefinition and no explicit content + // This should suppress the output since there's nothing to render + + // Act + await helper.ProcessAsync(context, output); + + // Assert - output should be suppressed when no content and no definition + Assert.Null(output.TagName); // Tag should be suppressed + } + private Endpoint CreateEndpoint(ImportMapDefinition importMap = null) { return new Endpoint( @@ -809,7 +862,7 @@ [new ResourceAssetCollection([ } [Fact] - public void RenderScriptTags_WithFileVersion_AndRequestPathBase() + public async Task RenderScriptTags_WithFileVersion_AndRequestPathBase() { // Arrange var context = MakeTagHelperContext( @@ -826,7 +879,7 @@ public void RenderScriptTags_WithFileVersion_AndRequestPathBase() helper.AppendVersion = true; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -834,7 +887,7 @@ public void RenderScriptTags_WithFileVersion_AndRequestPathBase() } [Fact] - public void RenderScriptTags_FallbackSrc_WithFileVersion() + public async Task RenderScriptTags_FallbackSrc_WithFileVersion() { // Arrange var context = MakeTagHelperContext( @@ -854,7 +907,7 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion() helper.Src = "/js/site.js"; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -865,7 +918,7 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion() } [Fact] - public void RenderScriptTags_FallbackSrc_AppendVersion_WithStaticAssets() + public async Task RenderScriptTags_FallbackSrc_AppendVersion_WithStaticAssets() { // Arrange var context = MakeTagHelperContext( @@ -886,7 +939,7 @@ public void RenderScriptTags_FallbackSrc_AppendVersion_WithStaticAssets() helper.Src = "/js/site.js"; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -897,7 +950,7 @@ public void RenderScriptTags_FallbackSrc_AppendVersion_WithStaticAssets() } [Fact] - public void RenderScriptTags_FallbackSrc_WithFileVersion_EncodesAsExpected() + public async Task RenderScriptTags_FallbackSrc_WithFileVersion_EncodesAsExpected() { // Arrange var expectedContent = @@ -939,7 +992,7 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion_EncodesAsExpected() helper.Src = "/js/site.js"; // Act - helper.Process(context, output); + await helper.ProcessAsync(context, output); // Assert Assert.Equal("script", output.TagName); @@ -949,7 +1002,7 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion_EncodesAsExpected() } [Fact] - public void RenderScriptTags_GlobbedSrc_WithFileVersion() + public async Task RenderScriptTags_GlobbedSrc_WithFileVersion() { // Arrange var expectedContent = "