|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor.Infrastructure;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
namespace Microsoft.AspNetCore.Mvc.TagHelpers;
public class ScriptTagHelperTest
{
[Theory]
[InlineData(null, "test.js", "test.js")]
[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(
string src,
string srcOutput,
string expectedSrcPrefix)
{
// Arrange
var allAttributes = new TagHelperAttributeList(
new TagHelperAttributeList
{
{ "type", new HtmlString("text/javascript") },
{ "asp-append-version", true },
});
var context = MakeTagHelperContext(allAttributes);
var outputAttributes = new TagHelperAttributeList
{
{ "type", new HtmlString("text/javascript") },
{ "src", srcOutput },
};
var output = MakeTagHelperOutput("script", outputAttributes);
var urlHelper = new Mock<IUrlHelper>();
// Ensure expanded path does not look like an absolute path on Linux, avoiding
// https://github.com/aspnet/External/issues/21
urlHelper
.Setup(urlhelper => urlhelper.Content(It.IsAny<string>()))
.Returns(new Func<string, string>(url => url.Replace("~/", "virtualRoot/")));
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var helper = GetHelper(urlHelperFactory: urlHelperFactory.Object);
helper.AppendVersion = true;
helper.Src = src;
// Act
helper.Process(context, output);
// Assert
Assert.Equal(
expectedSrcPrefix + "?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk",
(string)output.Attributes["src"].Value,
StringComparer.Ordinal);
}
[Theory]
[MemberData(nameof(LinkTagHelperTest.MultiAttributeSameNameData), MemberType = typeof(LinkTagHelperTest))]
public void HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList outputAttributes)
{
// Arrange
var allAttributes = new TagHelperAttributeList(
outputAttributes.Concat(
new TagHelperAttributeList
{
new TagHelperAttribute("data-extra", "something"),
new TagHelperAttribute("src", "/blank.js"),
new TagHelperAttribute("asp-fallback-src", "http://www.example.com/blank.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
}));
var tagHelperContext = MakeTagHelperContext(allAttributes);
var combinedOutputAttributes = new TagHelperAttributeList(
outputAttributes.Concat(
new[]
{
new TagHelperAttribute("data-extra", new HtmlString("something"))
}));
var output = MakeTagHelperOutput("script", combinedOutputAttributes);
var helper = GetHelper();
helper.FallbackSrc = "~/blank.js";
helper.FallbackTestExpression = "http://www.example.com/blank.js";
helper.Src = "/blank.js";
var expectedAttributes = new TagHelperAttributeList(output.Attributes);
expectedAttributes.Add(new TagHelperAttribute("src", "/blank.js"));
// Act
helper.Process(tagHelperContext, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
}
public static TheoryData<TagHelperAttributeList, Action<ScriptTagHelper>> RunsWhenRequiredAttributesArePresent_Data
{
get
{
return new TheoryData<TagHelperAttributeList, Action<ScriptTagHelper>>
{
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src", "test.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()")
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src", "test.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
new TagHelperAttribute("asp-suppress-fallback-integrity", "false")
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackTestExpression = "isavailable()";
tagHelper.SuppressFallbackIntegrity = false;
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src-include", "*.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()")
},
tagHelper =>
{
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src", "test.js"),
new TagHelperAttribute("asp-fallback-src-include", "*.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()")
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src", "test.js"),
new TagHelperAttribute("asp-fallback-src-include", "*.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
new TagHelperAttribute("asp-suppress-fallback-integrity", "false")
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackTestExpression = "isavailable()";
tagHelper.SuppressFallbackIntegrity = false;
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src-include", "*.js"),
new TagHelperAttribute("asp-fallback-src-exclude", "*.min.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()")
},
tagHelper =>
{
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackSrcExclude = "*.min.css";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
// File Version
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src", "test.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
new TagHelperAttribute("asp-append-version", "true")
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackTestExpression = "isavailable()";
tagHelper.AppendVersion = true;
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src-include", "*.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
new TagHelperAttribute("asp-append-version", "true")
},
tagHelper =>
{
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackTestExpression = "isavailable()";
tagHelper.AppendVersion = true;
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src", "test.js"),
new TagHelperAttribute("asp-fallback-src-include", "*.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
new TagHelperAttribute("asp-append-version", "true")
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackTestExpression = "isavailable()";
tagHelper.AppendVersion = true;
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src-include", "*.js"),
new TagHelperAttribute("asp-fallback-src-exclude", "*.min.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
new TagHelperAttribute("asp-append-version", "true")
},
tagHelper =>
{
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackSrcExclude = "*.min.css";
tagHelper.FallbackTestExpression = "isavailable()";
tagHelper.AppendVersion = true;
}
}
};
}
}
[Theory]
[MemberData(nameof(RunsWhenRequiredAttributesArePresent_Data))]
public void RunsWhenRequiredAttributesArePresent(
TagHelperAttributeList attributes,
Action<ScriptTagHelper> setProperties)
{
// Arrange
var context = MakeTagHelperContext(attributes);
var output = MakeTagHelperOutput("script");
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
new TestFileProvider(),
Mock.Of<IMemoryCache>(),
PathString.Empty);
globbingUrlBuilder.Setup(g => g.BuildUrlList(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new[] { "/common.js" });
var helper = GetHelper();
helper.GlobbingUrlBuilder = globbingUrlBuilder.Object;
setProperties(helper);
// Act
helper.Process(context, output);
// Assert
Assert.NotNull(output.TagName);
Assert.False(output.IsContentModified);
Assert.True(output.PostElement.IsModified);
}
public static TheoryData<TagHelperAttributeList, Action<ScriptTagHelper>> RunsWhenRequiredAttributesArePresent_NoSrc_Data
{
get
{
return new TheoryData<TagHelperAttributeList, Action<ScriptTagHelper>>
{
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-src-include", "*.js")
},
tagHelper =>
{
tagHelper.SrcInclude = "*.js";
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-src-include", "*.js"),
new TagHelperAttribute("asp-src-exclude", "*.min.js")
},
tagHelper =>
{
tagHelper.SrcInclude = "*.js";
tagHelper.SrcExclude = "*.min.js";
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-src-include", "*.js"),
new TagHelperAttribute("asp-append-version", "true")
},
tagHelper =>
{
tagHelper.SrcInclude = "*.js";
tagHelper.AppendVersion = true;
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-src-include", "*.js"),
new TagHelperAttribute("asp-src-exclude", "*.min.js"),
new TagHelperAttribute("asp-append-version", "true")
},
tagHelper =>
{
tagHelper.SrcInclude = "*.js";
tagHelper.SrcExclude = "*.min.js";
tagHelper.AppendVersion = true;
}
}
};
}
}
[Theory]
[MemberData(nameof(RunsWhenRequiredAttributesArePresent_NoSrc_Data))]
public void RunsWhenRequiredAttributesArePresent_NoSrc(
TagHelperAttributeList attributes,
Action<ScriptTagHelper> setProperties)
{
// Arrange
var context = MakeTagHelperContext(attributes);
var output = MakeTagHelperOutput("script");
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
new TestFileProvider(),
Mock.Of<IMemoryCache>(),
PathString.Empty);
globbingUrlBuilder.Setup(g => g.BuildUrlList(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new[] { "/common.js" });
var helper = GetHelper();
helper.GlobbingUrlBuilder = globbingUrlBuilder.Object;
setProperties(helper);
// Act
helper.Process(context, output);
// Assert
Assert.Null(output.TagName);
Assert.True(output.IsContentModified);
Assert.True(output.Content.GetContent().Length == 0);
Assert.True(output.PostElement.IsModified);
}
public static TheoryData<TagHelperAttributeList, Action<ScriptTagHelper>> DoesNotRunWhenARequiredAttributeIsMissing_Data
{
get
{
return new TheoryData<TagHelperAttributeList, Action<ScriptTagHelper>>
{
{
new TagHelperAttributeList
{
// This is commented out on purpose: new TagHelperAttribute("asp-src-include", "*.js"),
// Note asp-src-include attribute isn't included.
new TagHelperAttribute("asp-src-exclude", "*.min.js")
},
tagHelper =>
{
// This is commented out on purpose: tagHelper.SrcInclude = "*.js";
tagHelper.SrcExclude = "*.min.js";
}
},
{
new TagHelperAttributeList
{
// This is commented out on purpose: new TagHelperAttribute("asp-fallback-src", "test.js"),
// Note asp-src-include attribute isn't included.
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
},
tagHelper =>
{
// This is commented out on purpose: tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new TagHelperAttributeList
{
new TagHelperAttribute("asp-fallback-src", "test.js"),
// This is commented out on purpose: new TagHelperAttribute("asp-fallback-test", "isavailable()")
// Note asp-src-include attribute isn't included.
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
// This is commented out on purpose: tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new TagHelperAttributeList
{
// This is commented out on purpose: new TagHelperAttribute("asp-fallback-src-include", "test.js"),
// Note asp-src-include attribute isn't included.
new TagHelperAttribute("asp-fallback-src-exclude", "**/*.min.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
},
tagHelper =>
{
// This is commented out on purpose: tagHelper.FallbackSrcInclude = "test.js";
tagHelper.FallbackSrcExclude = "**/*.min.js";
tagHelper.FallbackTestExpression = "isavailable()";
}
}
};
}
}
[Theory]
[MemberData(nameof(DoesNotRunWhenARequiredAttributeIsMissing_Data))]
public void DoesNotRunWhenARequiredAttributeIsMissing(
TagHelperAttributeList attributes,
Action<ScriptTagHelper> setProperties)
{
// Arrange
var tagHelperContext = MakeTagHelperContext(attributes);
var output = MakeTagHelperOutput("script");
var logger = new Mock<ILogger<ScriptTagHelper>>();
var helper = GetHelper();
setProperties(helper);
// Act
helper.Process(tagHelperContext, output);
// Assert
Assert.NotNull(output.TagName);
Assert.False(output.IsContentModified);
Assert.Empty(output.Attributes);
Assert.True(output.PostElement.GetContent().Length == 0);
}
[Fact]
public void DoesNotRunWhenAllRequiredAttributesAreMissing()
{
// Arrange
var tagHelperContext = MakeTagHelperContext();
var viewContext = MakeViewContext();
var output = MakeTagHelperOutput("script");
var helper = GetHelper();
// Act
helper.Process(tagHelperContext, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.False(output.IsContentModified);
Assert.Empty(output.Attributes);
Assert.True(output.PostElement.GetContent().Length == 0);
}
[Fact]
public void PreservesOrderOfNonSrcAttributes()
{
// Arrange
var tagHelperContext = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("data-extra", "something"),
new TagHelperAttribute("src", "/blank.js"),
new TagHelperAttribute("data-more", "else"),
new TagHelperAttribute("asp-fallback-src", "http://www.example.com/blank.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
});
var output = MakeTagHelperOutput("src",
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("data-extra", "something"),
new TagHelperAttribute("data-more", "else"),
});
var helper = GetHelper();
helper.FallbackSrc = "~/blank.js";
helper.FallbackTestExpression = "http://www.example.com/blank.js";
helper.Src = "/blank.js";
// Act
helper.Process(tagHelperContext, output);
// Assert
Assert.Equal("data-extra", output.Attributes[0].Name);
Assert.Equal("src", output.Attributes[1].Name);
Assert.Equal("data-more", output.Attributes[2].Name);
}
[Fact]
public void RendersScriptTagsForGlobbedSrcResults()
{
// Arrange
var expectedContent = "<script src=\"HtmlEncode[[/js/site.js]]\"></script>" +
"<script src=\"HtmlEncode[[/common.js]]\"></script>";
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", "/js/site.js"),
new TagHelperAttribute("asp-src-include", "**/*.js")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
new TestFileProvider(),
Mock.Of<IMemoryCache>(),
PathString.Empty);
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.js", null))
.Returns(new[] { "/common.js" });
var helper = GetHelper();
helper.GlobbingUrlBuilder = globbingUrlBuilder.Object;
helper.Src = "/js/site.js";
helper.SrcInclude = "**/*.js";
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/js/site.js", output.Attributes["src"].Value);
var content = HtmlContentUtilities.HtmlContentToString(output, new HtmlTestEncoder());
Assert.Equal(expectedContent, content);
}
[Fact]
public void RendersScriptTagsForGlobbedSrcResults_EncodesAsExpected()
{
// Arrange
var expectedContent =
"<script encoded='contains \"quotes\"' literal=\"HtmlEncode[[all HTML encoded]]\" " +
"mixed='HtmlEncode[[HTML encoded]] and contains \"quotes\"' " +
"src=\"HtmlEncode[[/js/site.js]]\"></script>" +
"<script encoded='contains \"quotes\"' literal=\"HtmlEncode[[all HTML encoded]]\" " +
"mixed='HtmlEncode[[HTML encoded]] and contains \"quotes\"' " +
"src=\"HtmlEncode[[/common.js]]\"></script>";
var mixed = new DefaultTagHelperContent();
mixed.Append("HTML encoded");
mixed.AppendHtml(" and contains \"quotes\"");
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
{ "asp-src-include", "**/*.js" },
{ new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) },
{ "literal", "all HTML encoded" },
{ new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) },
{ "src", "/js/site.js" },
});
var output = MakeTagHelperOutput(
"script",
attributes: new TagHelperAttributeList
{
{ new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) },
{ "literal", "all HTML encoded"},
{ new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) },
});
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
new TestFileProvider(),
Mock.Of<IMemoryCache>(),
PathString.Empty);
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.js", null))
.Returns(new[] { "/common.js" });
var helper = GetHelper();
helper.GlobbingUrlBuilder = globbingUrlBuilder.Object;
helper.Src = "/js/site.js";
helper.SrcInclude = "**/*.js";
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/js/site.js", output.Attributes["src"].Value);
var content = HtmlContentUtilities.HtmlContentToString(output, new HtmlTestEncoder());
Assert.Equal(expectedContent, content);
}
[Fact]
public void RenderScriptTags_WithFileVersion()
{
// Arrange
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", "/js/site.js"),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var helper = GetHelper();
helper.Src = "/js/site.js";
helper.AppendVersion = true;
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value);
}
[Theory]
[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)
{
// Arrange
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", src),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var helper = GetHelper(urlHelperFactory: MakeUrlHelperFactory(value =>
value.StartsWith("~/", StringComparison.Ordinal) ? value[1..] : value));
helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint());
helper.Src = src;
helper.AppendVersion = true;
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal(expected, output.Attributes["src"].Value);
}
[Theory]
[InlineData("~/js/site.js")]
[InlineData("/approot/js/site.js")]
public void RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection(string path)
{
// Arrange
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", path),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var urlHelperFactory = MakeUrlHelperFactory(value =>
{
if (value.StartsWith("~/", StringComparison.Ordinal))
{
return value.Replace("~/", "/approot/");
}
return value;
});
var helper = GetHelper(urlHelperFactory: urlHelperFactory);
helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint());
helper.ViewContext.HttpContext.Request.PathBase = "/approot";
helper.Src = path;
helper.AppendVersion = true;
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/approot/js/site.fingerprint.js", output.Attributes["src"].Value);
}
[Fact]
public void ScriptTagHelper_RendersProvided_ImportMap()
{
// Arrange
var importMap = new ImportMapDefinition(
new Dictionary<string, string>
{
{ "jquery", "https://code.jquery.com/jquery-3.5.1.min.js" },
{ "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" }
},
new Dictionary<string, IReadOnlyDictionary<string, string>>
{
["development"] = new Dictionary<string, string>
{
{ "jquery", "https://code.jquery.com/jquery-3.5.1.js" },
{ "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js" }
}.AsReadOnly()
},
new Dictionary<string, string>
{
{ "https://code.jquery.com/jquery-3.5.1.js", "sha384-jquery" },
{ "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js", "sha256-bootstrap" }
});
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("type", "importmap"),
new TagHelperAttribute("asp-importmap", importMap)
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var helper = GetHelper();
helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint());
helper.Type = "importmap";
helper.ImportMap = importMap;
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal(importMap.ToJson(), output.Content.GetContent());
}
[Fact]
public void ScriptTagHelper_RendersImportMap_FromEndpoint()
{
// Arrange
var importMap = new ImportMapDefinition(
new Dictionary<string, string>
{
{ "jquery", "https://code.jquery.com/jquery-3.5.1.min.js" },
{ "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" }
},
new Dictionary<string, IReadOnlyDictionary<string, string>>
{
["development"] = new Dictionary<string, string>
{
{ "jquery", "https://code.jquery.com/jquery-3.5.1.js" },
{ "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js" }
}.AsReadOnly()
},
new Dictionary<string, string>
{
{ "https://code.jquery.com/jquery-3.5.1.js", "sha384-jquery" },
{ "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js", "sha256-bootstrap" }
});
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("type", "importmap"),
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var helper = GetHelper();
helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint(importMap));
helper.Type = "importmap";
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal(importMap.ToJson(), output.Content.GetContent());
}
private Endpoint CreateEndpoint(ImportMapDefinition importMap = null)
{
return new Endpoint(
(context) => Task.CompletedTask,
new EndpointMetadataCollection(
[new ResourceAssetCollection([
new("js/site.fingerprint.js", [new ResourceAssetProperty("label", "js/site.js")]),
new("common.fingerprint.js", [new ResourceAssetProperty("label", "common.js")]),
new("fallback.fingerprint.js", [new ResourceAssetProperty("label", "fallback.js")]),
]),
importMap ?? new ImportMapDefinition(null, null, null)]),
"Test");
}
[Fact]
public void RenderScriptTags_WithFileVersion_AndRequestPathBase()
{
// Arrange
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", "/bar/js/site.js"),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var viewContext = MakeViewContext("/bar");
var helper = GetHelper(viewContext: viewContext);
helper.Src = "/bar/js/site.js";
helper.AppendVersion = true;
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/bar/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value);
}
[Fact]
public void RenderScriptTags_FallbackSrc_WithFileVersion()
{
// Arrange
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", "/js/site.js"),
new TagHelperAttribute("asp-fallback-src-include", "fallback.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var helper = GetHelper();
helper.FallbackSrc = "fallback.js";
helper.FallbackTestExpression = "isavailable()";
helper.AppendVersion = true;
helper.Src = "/js/site.js";
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value);
Assert.Equal(Environment.NewLine + "<script>(isavailable()||document.write(\"JavaScriptEncode[[<script " +
"src=\"HtmlEncode[[fallback.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\">" +
"</script>]]\"));</script>", output.PostElement.GetContent());
}
[Fact]
public void RenderScriptTags_FallbackSrc_AppendVersion_WithStaticAssets()
{
// Arrange
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", "/js/site.js"),
new TagHelperAttribute("asp-fallback-src-include", "fallback.js"),
new TagHelperAttribute("asp-fallback-test", "isavailable()"),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var helper = GetHelper();
helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint());
helper.FallbackSrc = "fallback.js";
helper.FallbackTestExpression = "isavailable()";
helper.AppendVersion = true;
helper.Src = "/js/site.js";
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/js/site.fingerprint.js", output.Attributes["src"].Value);
Assert.Equal(Environment.NewLine + "<script>(isavailable()||document.write(\"JavaScriptEncode[[<script " +
"src=\"HtmlEncode[[fallback.fingerprint.js]]\">" +
"</script>]]\"));</script>", output.PostElement.GetContent());
}
[Fact]
public void RenderScriptTags_FallbackSrc_WithFileVersion_EncodesAsExpected()
{
// Arrange
var expectedContent =
"<script encoded='contains \"quotes\"' literal=\"HtmlEncode[[all HTML encoded]]\" " +
"mixed='HtmlEncode[[HTML encoded]] and contains \"quotes\"' " +
"src=\"HtmlEncode[[/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\"></script>" +
Environment.NewLine +
"<script>(isavailable()||document.write(\"JavaScriptEncode[[<script encoded=\'contains \"quotes\"\' " +
"literal=\"HtmlEncode[[all HTML encoded]]\" mixed=\'HtmlEncode[[HTML encoded]] and contains " +
"\"quotes\"' src=\"HtmlEncode[[fallback.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\">" +
"</script>]]\"));</script>";
var mixed = new DefaultTagHelperContent();
mixed.Append("HTML encoded");
mixed.AppendHtml(" and contains \"quotes\"");
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
{ "asp-append-version", "true" },
{ "asp-fallback-src-include", "fallback.js" },
{ "asp-fallback-test", "isavailable()" },
{ new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) },
{ "literal", "all HTML encoded" },
{ new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) },
{ "src", "/js/site.js" },
});
var output = MakeTagHelperOutput(
"script",
attributes: new TagHelperAttributeList
{
{ new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) },
{ "literal", "all HTML encoded" },
{ new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) },
});
var helper = GetHelper();
helper.AppendVersion = true;
helper.FallbackSrc = "fallback.js";
helper.FallbackTestExpression = "isavailable()";
helper.Src = "/js/site.js";
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value);
var content = HtmlContentUtilities.HtmlContentToString(output, new HtmlTestEncoder());
Assert.Equal(expectedContent, content);
}
[Fact]
public void RenderScriptTags_GlobbedSrc_WithFileVersion()
{
// Arrange
var expectedContent = "<script " +
"src=\"HtmlEncode[[/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\"></script>" +
"<script src=\"HtmlEncode[[/common.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\"></script>";
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", "/js/site.js"),
new TagHelperAttribute("asp-src-include", "*.js"),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
new TestFileProvider(),
Mock.Of<IMemoryCache>(),
PathString.Empty);
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "*.js", null))
.Returns(new[] { "/common.js" });
var helper = GetHelper();
helper.GlobbingUrlBuilder = globbingUrlBuilder.Object;
helper.SrcInclude = "*.js";
helper.AppendVersion = true;
helper.Src = "/js/site.js";
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value);
var content = HtmlContentUtilities.HtmlContentToString(output, new HtmlTestEncoder());
Assert.Equal(expectedContent, content);
}
[Fact]
public void RenderScriptTags_GlobbedSrc_WithFileVersion_WithStaticAssets()
{
// Arrange
var expectedContent = "<script " +
"src=\"HtmlEncode[[/js/site.fingerprint.js]]\"></script>" +
"<script src=\"HtmlEncode[[/common.fingerprint.js]]\"></script>";
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", "/js/site.js"),
new TagHelperAttribute("asp-src-include", "*.js"),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
new TestFileProvider(),
Mock.Of<IMemoryCache>(),
PathString.Empty);
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "*.js", null))
.Returns(new[] { "/common.js" });
var helper = GetHelper();
helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint());
helper.GlobbingUrlBuilder = globbingUrlBuilder.Object;
helper.SrcInclude = "*.js";
helper.AppendVersion = true;
helper.Src = "/js/site.js";
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("/js/site.fingerprint.js", output.Attributes["src"].Value);
var content = HtmlContentUtilities.HtmlContentToString(output, new HtmlTestEncoder());
Assert.Equal(expectedContent, content);
}
[Theory]
[InlineData("~/js/site.js")]
[InlineData("/approot/js/site.js")]
public void RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection_PreservesModule(string path)
{
// Arrange
var context = MakeTagHelperContext(
attributes: new TagHelperAttributeList
{
new TagHelperAttribute("src", path),
new TagHelperAttribute("type", "module"),
new TagHelperAttribute("asp-append-version", "true")
});
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
var urlHelperFactory = MakeUrlHelperFactory(value =>
{
return value.StartsWith("~/", StringComparison.Ordinal) ?
value.Replace("~/", "/approot/") :
value;
});
var helper = GetHelper(urlHelperFactory: urlHelperFactory);
helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint());
helper.ViewContext.HttpContext.Request.PathBase = "/approot";
helper.Src = path;
helper.Type = "module";
helper.AppendVersion = true;
// Act
helper.Process(context, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.Equal("module", output.Attributes["type"].Value);
Assert.Equal("/approot/js/site.fingerprint.js", output.Attributes["src"].Value);
}
private static ScriptTagHelper GetHelper(
IWebHostEnvironment hostingEnvironment = null,
IUrlHelperFactory urlHelperFactory = null,
ViewContext viewContext = null)
{
hostingEnvironment = hostingEnvironment ?? MakeHostingEnvironment();
urlHelperFactory = urlHelperFactory ?? MakeUrlHelperFactory();
viewContext = viewContext ?? MakeViewContext();
var memoryCacheProvider = new TagHelperMemoryCacheProvider();
var fileVersionProvider = new DefaultFileVersionProvider(hostingEnvironment, memoryCacheProvider);
return new ScriptTagHelper(
hostingEnvironment,
memoryCacheProvider,
fileVersionProvider,
new HtmlTestEncoder(),
new JavaScriptTestEncoder(),
urlHelperFactory)
{
ViewContext = viewContext,
};
}
private TagHelperContext MakeTagHelperContext(
TagHelperAttributeList attributes = null,
string content = null)
{
attributes = attributes ?? new TagHelperAttributeList();
return new TagHelperContext(
tagName: "script",
allAttributes: attributes,
items: new Dictionary<object, object>(),
uniqueId: Guid.NewGuid().ToString("N"));
}
private static ViewContext MakeViewContext(string requestPathBase = null)
{
var actionContext = new ActionContext(new DefaultHttpContext(), new AspNetCore.Routing.RouteData(), new ActionDescriptor());
if (requestPathBase != null)
{
actionContext.HttpContext.Request.PathBase = new PathString(requestPathBase);
}
var metadataProvider = new EmptyModelMetadataProvider();
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
var viewContext = new ViewContext(
actionContext,
Mock.Of<IView>(),
viewData,
Mock.Of<ITempDataDictionary>(),
TextWriter.Null,
new HtmlHelperOptions());
return viewContext;
}
private TagHelperOutput MakeTagHelperOutput(string tagName, TagHelperAttributeList attributes = null)
{
attributes = attributes ?? new TagHelperAttributeList();
return new TagHelperOutput(
tagName,
attributes,
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(
new DefaultTagHelperContent()));
}
private static IWebHostEnvironment MakeHostingEnvironment()
{
var emptyDirectoryContents = new Mock<IDirectoryContents>();
emptyDirectoryContents.Setup(dc => dc.GetEnumerator())
.Returns(Enumerable.Empty<IFileInfo>().GetEnumerator());
var mockFile = new Mock<IFileInfo>();
mockFile.SetupGet(f => f.Exists).Returns(true);
mockFile
.Setup(m => m.CreateReadStream())
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")));
var mockFileProvider = new Mock<IFileProvider>();
mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
.Returns(emptyDirectoryContents.Object);
mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny<string>()))
.Returns(mockFile.Object);
mockFileProvider.Setup(fp => fp.Watch(It.IsAny<string>()))
.Returns(new TestFileChangeToken());
var hostingEnvironment = new Mock<IWebHostEnvironment>();
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object);
return hostingEnvironment.Object;
}
private static IUrlHelperFactory MakeUrlHelperFactory(Func<string, string> urlResolver = null)
{
var urlHelper = new Mock<IUrlHelper>();
urlResolver ??= (url) => url;
urlHelper
.Setup(helper => helper.Content(It.IsAny<string>()))
.Returns(new Func<string, string>(urlResolver));
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
return urlHelperFactory.Object;
}
}
|