diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs index 7608782758..331cffc9cf 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.Framework.Internal; -using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Mvc.TagHelpers.Internal { @@ -15,44 +14,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal /// public static class AttributeMatcher { - /// - /// Determines whether a 's required attributes are present, non null, non empty, and - /// non whitepsace. - /// - /// The . - /// The . - /// - /// The attributes the requires in order to run. - /// - /// An optional to log warning details to. - /// A indicating whether the should run. - public static bool AllRequiredAttributesArePresent( - [NotNull] TagHelperContext tagHelperContext, - [NotNull] ViewContext viewContext, - [NotNull] IEnumerable requiredAttributes, - ILogger logger) - { - var attributes = GetPresentMissingAttributes(tagHelperContext, requiredAttributes); - - if (attributes.Missing.Any()) - { - if (attributes.Present.Any() && logger != null && logger.IsEnabled(LogLevel.Warning)) - { - // At least 1 attribute was present indicating the user intended to use the tag helper, - // but at least 1 was missing too, so log a warning with the details. - logger.WriteWarning(new MissingAttributeLoggerStructure( - tagHelperContext.UniqueId, - viewContext.View.Path, - attributes.Missing)); - } - - return false; - } - - // All required attributes present - return true; - } - /// /// Determines the modes a can run in based on which modes have all their required /// attributes present, non null, non empty, and non whitepsace. diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/MissingAttributeLoggerStructure.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/MissingAttributeLoggerStructure.cs deleted file mode 100644 index 988741f583..0000000000 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/MissingAttributeLoggerStructure.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.AspNet.Razor.Runtime.TagHelpers; -using Microsoft.Framework.Logging; - -namespace Microsoft.AspNet.Mvc.TagHelpers.Internal -{ - /// - /// An for log messages regarding instances that opt out of - /// processing due to missing required attributes. - /// - public class MissingAttributeLoggerStructure : ILoggerStructure - { - private readonly string _uniqueId; - private readonly string _viewPath; - private readonly IEnumerable> _values; - - // Internal for unit testing - internal IEnumerable MissingAttributes { get; } - - /// - /// Creates a new . - /// - /// The unique ID of the HTML element this message applies to. - /// The path to the view. - /// The missing required attributes. - public MissingAttributeLoggerStructure(string uniqueId, string viewPath, IEnumerable missingAttributes) - { - _uniqueId = uniqueId; - _viewPath = viewPath; - MissingAttributes = missingAttributes; - _values = new Dictionary - { - ["UniqueId"] = _uniqueId, - ["ViewPath"] = _viewPath, - ["MissingAttributes"] = MissingAttributes - }; - } - - /// - /// The log message. - /// - public string Message - { - get - { - return "Tag Helper has one or more missing required attributes."; - } - } - - /// - /// Gets the values associated with this structured log message. - /// - /// The values. - public IEnumerable> GetValues() - { - return _values; - } - - /// - /// Generates a human readable string for this structured log message. - /// - /// The message. - public string Format() - { - return string.Format("Tag Helper with ID {0} in view '{1}' is missing attributes: {2}", - _uniqueId, - _viewPath, - string.Join(",", MissingAttributes)); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructure.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructureOfT.cs similarity index 72% rename from src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructure.cs rename to src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructureOfT.cs index 12f1cd8f05..fd7d0c1075 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructure.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructureOfT.cs @@ -14,12 +14,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal /// An for log messages regarding instances that opt out of /// processing due to missing attributes for one of several possible modes. /// - public class PartialModeMatchLoggerStructure : ILoggerStructure + public class PartialModeMatchLoggerStructure : PartialModeMatchLoggerStructure { private readonly string _uniqueId; private readonly string _viewPath; private readonly IEnumerable> _partialMatches; - private readonly IEnumerable> _values; /// /// Creates a new . @@ -31,43 +30,23 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal string uniqueId, string viewPath, [NotNull] IEnumerable> partialMatches) + : base(values: new Dictionary + { + ["UniqueId"] = uniqueId, + ["ViewPath"] = viewPath, + ["PartialMatches"] = partialMatches + }) { _uniqueId = uniqueId; _viewPath = viewPath; _partialMatches = partialMatches; - _values = new Dictionary - { - ["UniqueId"] = _uniqueId, - ["ViewPath"] = _viewPath, - ["PartialMatches"] = partialMatches - }; } - - /// - /// The log message. - /// - public string Message - { - get - { - return "Tag Helper has missing required attributes."; - } - } - - /// - /// Gets the values associated with this structured log message. - /// - /// The values. - public IEnumerable> GetValues() - { - return _values; - } - + /// /// Generates a human readable string for this structured log message. /// /// The message. - public string Format() + public override string Format() { var newLine = Environment.NewLine; return string.Format( diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialModeMatchLoggerStructure.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialModeMatchLoggerStructure.cs new file mode 100644 index 0000000000..f5759e4539 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialModeMatchLoggerStructure.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Mvc.TagHelpers.Internal +{ + /// + /// An for log messages regarding instances that opt out of + /// processing due to missing attributes for one of several possible modes. + /// + public abstract class PartialModeMatchLoggerStructure : ILoggerStructure + { + private readonly IEnumerable> _values; + + protected PartialModeMatchLoggerStructure(IEnumerable> values) + { + _values = values; + } + + /// + /// The log message. + /// + public string Message + { + get + { + return "Tag Helper has missing required attributes."; + } + } + + /// + /// Returns a human-readable string of the structured data. + /// + public abstract string Format(); + + /// + /// Gets the values associated with this structured log message. + /// + /// The values. + public IEnumerable> GetValues() + { + return _values; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs index 91cc7707cb..f130e075e1 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs @@ -66,15 +66,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers private enum Mode { - /// - /// Rendering a fallback block if primary stylesheet fails to load. Will also do globbing if the appropriate - /// properties are set. - /// - Fallback, /// /// Just performing file globbing search for the href, rendering a separate <link> for each match. /// - GlobbedHref + GlobbedHref = 0, + /// + /// Rendering a fallback block if primary stylesheet fails to load. Will also do globbing for both the + /// primary and fallback hrefs if the appropriate properties are set. + /// + Fallback = 1, } /// @@ -93,15 +93,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers public string HrefExclude { get; set; } /// - /// The URL of a CSS stylesheet to fallback to in the case the primary one fails (as specified in the href - /// attribute). + /// The URL of a CSS stylesheet to fallback to in the case the primary one fails. /// [HtmlAttributeName(FallbackHrefAttributeName)] public string FallbackHref { get; set; } /// /// A comma separated list of globbed file patterns of CSS stylesheets to fallback to in the case the primary - /// one fails (as specified in the href attribute). + /// one fails. /// The glob patterns are assessed relative to the application's 'webroot' setting. /// [HtmlAttributeName(FallbackHrefIncludeAttributeName)] @@ -109,7 +108,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers /// /// A comma separated list of globbed file patterns of CSS stylesheets to exclude from the fallback list, in - /// the case the primary one fails (as specified in the href attribute). + /// the case the primary one fails. /// The glob patterns are assessed relative to the application's 'webroot' setting. /// Must be used in conjunction with . /// @@ -161,9 +160,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails); - Debug.Assert(modeResult.FullMatches.Select(match => match.Mode).Distinct().Count() <= 1, - $"There should only be one mode match, check the {nameof(ModeDetails)}"); - modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path); if (!modeResult.FullMatches.Any()) @@ -172,7 +168,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers return; } - var mode = modeResult.FullMatches.First().Mode; + // Get the highest matched mode + var mode = modeResult.FullMatches.Select(match => match.Mode).Max(); // NOTE: Values in TagHelperOutput.Attributes are already HtmlEncoded var attributes = new Dictionary(output.Attributes); @@ -206,11 +203,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers attributes.TryGetValue("href", out staticHref); EnsureGlobbingUrlBuilder(); - var hrefs = GlobbingUrlBuilder.BuildUrlList(staticHref, HrefInclude, HrefExclude); + var urls = GlobbingUrlBuilder.BuildUrlList(staticHref, HrefInclude, HrefExclude); - foreach (var href in hrefs) + foreach (var url in urls) { - attributes["href"] = WebUtility.HtmlEncode(href); + attributes["href"] = WebUtility.HtmlEncode(url); BuildLinkTag(attributes, builder); } } diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs index 975209caba..fe2017d7b2 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs @@ -2,12 +2,17 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; +using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Mvc.TagHelpers.Internal; using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.Cache.Memory; using Microsoft.Framework.Logging; using Microsoft.Framework.WebEncoders; @@ -22,24 +27,92 @@ namespace Microsoft.AspNet.Mvc.TagHelpers /// public class ScriptTagHelper : TagHelper { + private const string SrcIncludeAttributeName = "asp-src-include"; + private const string SrcExcludeAttributeName = "asp-src-exclude"; private const string FallbackSrcAttributeName = "asp-fallback-src"; + private const string FallbackSrcIncludeAttributeName = "asp-fallback-src-include"; + private const string FallbackSrcExcludeAttributeName = "asp-fallback-src-exclude"; private const string FallbackTestExpressionAttributeName = "asp-fallback-test"; private const string SrcAttributeName = "src"; - // NOTE: All attributes are required for the ScriptTagHelper to process. - private static readonly string[] RequiredAttributes = new[] - { - FallbackSrcAttributeName, - FallbackTestExpressionAttributeName, + private static readonly ModeAttributes[] ModeDetails = new[] { + // Globbed src (include only) + ModeAttributes.Create(Mode.GlobbedSrc, new [] { SrcIncludeAttributeName }), + // Globbed src (include & exclude) + ModeAttributes.Create(Mode.GlobbedSrc, new [] { SrcIncludeAttributeName, SrcExcludeAttributeName }), + // Fallback with static src + ModeAttributes.Create( + Mode.Fallback, new[] + { + FallbackSrcAttributeName, + FallbackTestExpressionAttributeName + }), + // Fallback with globbed src (include only) + ModeAttributes.Create( + Mode.Fallback, new[] { + FallbackSrcIncludeAttributeName, + FallbackTestExpressionAttributeName + }), + // Fallback with globbed src (include & exclude) + ModeAttributes.Create( + Mode.Fallback, new[] { + FallbackSrcIncludeAttributeName, + FallbackSrcExcludeAttributeName, + FallbackTestExpressionAttributeName + }), }; + private enum Mode + { + /// + /// Just performing file globbing search for the src, rendering a separate <script> for each match. + /// + GlobbedSrc = 0, + /// + /// Rendering a fallback block if primary javascript fails to load. Will also do globbing for both the + /// primary and fallback srcs if the appropriate properties are set. + /// + Fallback = 1 + } + /// - /// The URL of a Script tag to fallback to in the case the primary one fails (as specified in the src - /// attribute). + /// A comma separated list of globbed file patterns of JavaScript scripts to load. + /// The glob patterns are assessed relative to the application's 'webroot' setting. + /// + [HtmlAttributeName(SrcIncludeAttributeName)] + public string SrcInclude { get; set; } + + /// + /// A comma separated list of globbed file patterns of JavaScript scripts to exclude from loading. + /// The glob patterns are assessed relative to the application's 'webroot' setting. + /// Must be used in conjunction with . + /// + [HtmlAttributeName(SrcExcludeAttributeName)] + public string SrcExclude { get; set; } + + /// + /// The URL of a Script tag to fallback to in the case the primary one fails. /// [HtmlAttributeName(FallbackSrcAttributeName)] public string FallbackSrc { get; set; } + /// + /// A comma separated list of globbed file patterns of JavaScript scripts to fallback to in the case the + /// primary one fails. + /// The glob patterns are assessed relative to the application's 'webroot' setting. + /// + [HtmlAttributeName(FallbackSrcIncludeAttributeName)] + public string FallbackSrcInclude { get; set; } + + /// + /// A comma separated list of globbed file patterns of JavaScript scripts to exclude from the fallback list, in + /// the case the primary one fails. + /// The glob patterns are assessed relative to the application's 'webroot' setting. + /// Must be used in conjunction with . + /// + [HtmlAttributeName(FallbackSrcExcludeAttributeName)] + public string FallbackSrcExclude { get; set; } + /// /// The script method defined in the primary script to use for the fallback test. /// @@ -50,84 +123,167 @@ namespace Microsoft.AspNet.Mvc.TagHelpers [Activate] protected internal ILogger Logger { get; set; } + [Activate] + protected internal IHostingEnvironment HostingEnvironment { get; set; } + [Activate] protected internal ViewContext ViewContext { get; set; } + [Activate] + protected internal IMemoryCache Cache { get; set; } + + // Internal for ease of use when testing. + protected internal GlobbingUrlBuilder GlobbingUrlBuilder { get; set; } + /// public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - if (!AttributeMatcher.AllRequiredAttributesArePresent(context, ViewContext, RequiredAttributes, Logger)) - { - if (Logger.IsEnabled(LogLevel.Verbose)) - { - Logger.WriteVerbose( - "Skipping processing for {0} with ID {1} on view '{2}'", - nameof(ScriptTagHelper), - context.UniqueId, - ViewContext.View.Path); - } + var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails); + modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path); + + if (!modeResult.FullMatches.Any()) + { + // No attributes matched so we have nothing to do return; } - var content = new StringBuilder(); - - // NOTE: Values in Output.Attributes are already HtmlEncoded - - // We've taken over rendering here so prevent the element rendering the outer tag - output.TagName = null; - - // Rebuild the "); - // Build the "); - - output.Content = content.ToString(); + // We've taken over tag rendering, so prevent rendering the outer tag + output.TagName = null; + output.Content = builder.ToString(); } - private void AppendSrc(StringBuilder content, string srcKey) + private void BuildGlobbedScriptTags( + string originalContent, + IDictionary attributes, + StringBuilder builder) + { + // Build a "); + } + } + + private void EnsureGlobbingUrlBuilder() + { + if (GlobbingUrlBuilder == null) + { + GlobbingUrlBuilder = new GlobbingUrlBuilder( + HostingEnvironment.WebRootFileProvider, + Cache, + ViewContext.HttpContext.Request.PathBase); + } + } + + private static void BuildScriptTag( + string content, + IDictionary attributes, + StringBuilder builder) + { + builder.Append("") + .Append(content) + .Append(""); + } + + private void AppendSrc(StringBuilder content, string srcKey, string srcValue) { // Append src attribute in the original place and replace the content the fallback content // No need to encode the key because we know it is "src". content.Append(" ") .Append(srcKey) .Append("=\\\"") - .Append(WebUtility.HtmlEncode(FallbackSrc)) + .Append(WebUtility.HtmlEncode(srcValue)) .Append("\\\""); } } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html index 76fee781c2..85f40d625f 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html @@ -39,6 +39,22 @@ + + + + + + + + + + + + + + + + @@ -59,6 +75,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html index a09eef5de3..70102a9ac7 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html @@ -1,4 +1,5 @@ - + + @@ -6,22 +7,90 @@

Script tag helper test

- - + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs index eb405d9dd5..cea73a981b 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs @@ -351,20 +351,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers return new TagHelperOutput(tagName, attributes); } - private static IHostingEnvironment MakeHostingEnvironment(IFileProvider webRootFileProvider = null) + private static IHostingEnvironment MakeHostingEnvironment() { var emptyDirectoryContents = new Mock(); emptyDirectoryContents.Setup(dc => dc.GetEnumerator()) .Returns(Enumerable.Empty().GetEnumerator()); - if (webRootFileProvider == null) - { - var mockFileProvider = new Mock(); - mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny())) - .Returns(emptyDirectoryContents.Object); - webRootFileProvider = mockFileProvider.Object; - } + var mockFileProvider = new Mock(); + mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny())) + .Returns(emptyDirectoryContents.Object); var hostingEnvironment = new Mock(); - hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(webRootFileProvider); + hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object); return hostingEnvironment.Object; } diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs index ce0d41f4e0..35461a3d80 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNet.FileProviders; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering; @@ -20,39 +22,112 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { public class ScriptTagHelperTest { + public static TheoryData RunsWhenRequiredAttributesArePresent_Data + { + get + { + return new TheoryData, Action> + { + { + new Dictionary + { + ["asp-src-include"] = "*.js" + }, + tagHelper => + { + tagHelper.SrcInclude = "*.js"; + } + }, + { + new Dictionary + { + ["asp-src-include"] = "*.js", + ["asp-src-exclude"] = "*.min.js" + }, + tagHelper => + { + tagHelper.SrcInclude = "*.js"; + tagHelper.SrcExclude = "*.min.js"; + } + }, + { + new Dictionary + { + ["asp-fallback-src"] = "test.js", + ["asp-fallback-test"] = "isavailable()" + }, + tagHelper => + { + tagHelper.FallbackSrc = "test.js"; + tagHelper.FallbackTestExpression = "isavailable()"; + } + }, + { + new Dictionary + { + ["asp-fallback-src-include"] = "*.js", + ["asp-fallback-test"] = "isavailable()" + }, + tagHelper => + { + tagHelper.FallbackSrcInclude = "*.css"; + tagHelper.FallbackTestExpression = "isavailable()"; + } + }, + { + new Dictionary + { + ["asp-fallback-src"] = "test.js", + ["asp-fallback-src-include"] = "*.js", + ["asp-fallback-test"] = "isavailable()" + }, + tagHelper => + { + tagHelper.FallbackSrc = "test.js"; + tagHelper.FallbackSrcInclude = "*.css"; + tagHelper.FallbackTestExpression = "isavailable()"; + } + }, + { + new Dictionary + { + ["asp-fallback-src-include"] = "*.js", + ["asp-fallback-src-exclude"] = "*.min.js", + ["asp-fallback-test"] = "isavailable()" + }, + tagHelper => + { + tagHelper.FallbackSrcInclude = "*.css"; + tagHelper.FallbackSrcExclude = "*.min.css"; + tagHelper.FallbackTestExpression = "isavailable()"; + } + } + }; + } + } + [Theory] - [InlineData("~/blank.js")] - [InlineData(null)] - public async Task RunsWhenRequiredAttributesArePresent(string srcValue) + [MemberData(nameof(RunsWhenRequiredAttributesArePresent_Data))] + public async Task RunsWhenRequiredAttributesArePresent( + IDictionary attributes, + Action setProperties) { // Arrange - var attributes = new Dictionary - { - ["asp-fallback-src"] = "http://www.example.com/blank.js", - ["asp-fallback-test"] = "isavailable()", - }; - - if (srcValue != null) - { - attributes.Add("src", srcValue); - } - - var tagHelperContext = MakeTagHelperContext(attributes); - var viewContext = MakeViewContext(); - + var context = MakeTagHelperContext(attributes); var output = MakeTagHelperOutput("script"); var logger = CreateLogger(); - - var helper = new ScriptTagHelper() + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var helper = new ScriptTagHelper { Logger = logger, + HostingEnvironment = hostingEnvironment, ViewContext = viewContext, - FallbackSrc = "http://www.example.com/blank.js", - FallbackTestExpression = "isavailable()", }; + setProperties(helper); // Act - await helper.ProcessAsync(tagHelperContext, output); + await helper.ProcessAsync(context, output); // Assert Assert.Null(output.TagName); @@ -61,57 +136,113 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.Empty(logger.Logged); } - public static TheoryData MissingAttributeDataSet + public static TheoryData DoesNotRunWhenARequiredAttributeIsMissing_Data { get { - return new TheoryData, ScriptTagHelper, string> + return new TheoryData, Action> { { - new Dictionary // the attributes provided + new Dictionary { - ["asp-fallback-src"] = "http://www.example.com/blank.js", + // This is commented out on purpose: ["asp-src-include"] = "*.js", + ["asp-src-exclude"] = "*.min.js" }, - new ScriptTagHelper() // the tag helper + tagHelper => { - FallbackTestExpression = "isavailable()", - }, - "asp-fallback-test" // missing attribute + // This is commented out on purpose: tagHelper.SrcInclude = "*.js"; + tagHelper.SrcExclude = "*.min.js"; + } }, - { - new Dictionary // the attributes provided + new Dictionary { + // This is commented out on purpose: ["asp-fallback-src"] = "test.js", ["asp-fallback-test"] = "isavailable()", }, - new ScriptTagHelper() // the tag helper + tagHelper => { - FallbackTestExpression = "http://www.example.com/blank.js", - }, - "asp-fallback-src" // missing attribute + // This is commented out on purpose: tagHelper.FallbackSrc = "test.js"; + tagHelper.FallbackTestExpression = "isavailable()"; + } }, + { + new Dictionary + { + ["asp-fallback-src"] = "test.js", + // This is commented out on purpose: ["asp-fallback-test"] = "isavailable()" + }, + tagHelper => + { + tagHelper.FallbackSrc = "test.js"; + // This is commented out on purpose: tagHelper.FallbackTestExpression = "isavailable()"; + } + }, + { + new Dictionary + { + // This is commented out on purpose: ["asp-fallback-src-include"] = "test.js", + ["asp-fallback-src-exclude"] = "**/*.min.js", + ["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(MissingAttributeDataSet))] - public async Task DoesNotRunWhenARequiredAttributeIsMissing( - Dictionary attributes, - ScriptTagHelper helper, - string attributeMissing) + [MemberData(nameof(DoesNotRunWhenARequiredAttributeIsMissing_Data))] + public void DoesNotRunWhenARequiredAttributeIsMissing( + IDictionary attributes, + Action setProperties) { // Arrange - Assert.Single(attributes); - var tagHelperContext = MakeTagHelperContext(attributes); + var output = MakeTagHelperOutput("script"); + var logger = new Mock>(); + var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); + var helper = new ScriptTagHelper + { + Logger = logger.Object, + HostingEnvironment = hostingEnvironment, + ViewContext = viewContext + }; + setProperties(helper); + // Act + helper.Process(tagHelperContext, output); + + // Assert + Assert.NotNull(output.TagName); + Assert.False(output.ContentSet); + } + + [Theory] + [MemberData(nameof(DoesNotRunWhenARequiredAttributeIsMissing_Data))] + public async Task LogsWhenARequiredAttributeIsMissing( + IDictionary attributes, + Action setProperties) + { + // Arrange + var tagHelperContext = MakeTagHelperContext(attributes); var output = MakeTagHelperOutput("script"); var logger = CreateLogger(); - - helper.Logger = logger; - helper.ViewContext = viewContext; + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var helper = new ScriptTagHelper + { + Logger = logger, + HostingEnvironment = hostingEnvironment, + ViewContext = viewContext + }; + setProperties(helper); // Act await helper.ProcessAsync(tagHelperContext, output); @@ -119,6 +250,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Assert Assert.Equal("script", output.TagName); Assert.False(output.ContentSet); + + Assert.Equal(2, logger.Logged.Count); + + Assert.Equal(LogLevel.Warning, logger.Logged[0].LogLevel); + Assert.IsAssignableFrom(logger.Logged[0].State); + + var loggerData0 = (PartialModeMatchLoggerStructure)logger.Logged[0].State; + + Assert.Equal(LogLevel.Verbose, logger.Logged[1].LogLevel); + Assert.IsAssignableFrom(logger.Logged[1].State); + Assert.StartsWith("Skipping processing for Microsoft.AspNet.Mvc.TagHelpers.ScriptTagHelper", + ((ILoggerStructure)logger.Logged[1].State).Format()); } [Fact] @@ -144,47 +287,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.False(output.ContentSet); } - [Theory] - [MemberData(nameof(MissingAttributeDataSet))] - public async Task LogsWhenARequiredAttributeIsMissing( - Dictionary attributes, - ScriptTagHelper helper, - string attributeMissing) - { - // Arrange - Assert.Single(attributes); - - var tagHelperContext = MakeTagHelperContext(attributes); - var viewContext = MakeViewContext(); - - var output = MakeTagHelperOutput("script"); - var logger = CreateLogger(); - - helper.Logger = logger; - helper.ViewContext = viewContext; - - // Act - await helper.ProcessAsync(tagHelperContext, output); - - // Assert - Assert.Equal("script", output.TagName); - Assert.False(output.ContentSet); - - Assert.Equal(2, logger.Logged.Count); - - Assert.Equal(LogLevel.Warning, logger.Logged[0].LogLevel); - Assert.IsType(logger.Logged[0].State); - - var loggerData0 = (MissingAttributeLoggerStructure)logger.Logged[0].State; - Assert.Single(loggerData0.MissingAttributes); - Assert.Equal(attributeMissing, loggerData0.MissingAttributes.Single()); - - Assert.Equal(LogLevel.Verbose, logger.Logged[1].LogLevel); - Assert.IsAssignableFrom(logger.Logged[1].State); - Assert.StartsWith("Skipping processing for ScriptTagHelper", - ((ILoggerStructure)logger.Logged[1].State).Format()); - } - [Fact] public async Task LogsWhenAllRequiredAttributesAreMissing() { @@ -211,7 +313,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.Equal(LogLevel.Verbose, logger.Logged[0].LogLevel); Assert.IsAssignableFrom(logger.Logged[0].State); - Assert.StartsWith("Skipping processing for ScriptTagHelper", + Assert.StartsWith("Skipping processing for Microsoft.AspNet.Mvc.TagHelpers.ScriptTagHelper", ((ILoggerStructure)logger.Logged[0].State).Format()); } @@ -231,7 +333,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var viewContext = MakeViewContext(); - var output = MakeTagHelperOutput("link", + var output = MakeTagHelperOutput("src", attributes: new Dictionary { ["data-extra"] = "something", @@ -240,11 +342,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers }); var logger = CreateLogger(); + var hostingEnvironment = MakeHostingEnvironment(); var helper = new ScriptTagHelper { Logger = logger, ViewContext = viewContext, + HostingEnvironment = hostingEnvironment, FallbackSrc = "~/blank.js", FallbackTestExpression = "http://www.example.com/blank.js", }; @@ -257,6 +361,42 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.Empty(logger.Logged); } + [Fact] + public async Task RendersScriptTagsForGlobbedSrcResults() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + ["src"] = "/js/site.js", + ["asp-src-include"] = "**/*.js" + }); + var output = MakeTagHelperOutput("script", attributes: new Dictionary + { + ["src"] = "/js/site.js" + }); + var logger = new Mock>(); + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var globbingUrlBuilder = new Mock(); + globbingUrlBuilder.Setup(g => g.BuildUrlList("/js/site.js", "**/*.js", null)) + .Returns(new[] { "/js/site.js", "/common.js" }); + var helper = new ScriptTagHelper + { + GlobbingUrlBuilder = globbingUrlBuilder.Object, + Logger = logger.Object, + HostingEnvironment = hostingEnvironment, + ViewContext = viewContext, + SrcInclude = "**/*.js" + }; + + // Act + await helper.ProcessAsync(context, output); + + // Assert + Assert.Equal("", output.Content); + } + private TagHelperContext MakeTagHelperContext( IDictionary attributes = null, string content = null) @@ -291,5 +431,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { return new TagHelperLogger(); } + + private static IHostingEnvironment MakeHostingEnvironment() + { + var emptyDirectoryContents = new Mock(); + emptyDirectoryContents.Setup(dc => dc.GetEnumerator()) + .Returns(Enumerable.Empty().GetEnumerator()); + var mockFileProvider = new Mock(); + mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny())) + .Returns(emptyDirectoryContents.Object); + var hostingEnvironment = new Mock(); + hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object); + + return hostingEnvironment.Object; + } } } \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml index 8553cf89b9..f003ff1967 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml +++ b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml @@ -13,7 +13,7 @@ - + @@ -43,6 +43,35 @@ asp-fallback-test-property="visibility" asp-fallback-test-value="hidden" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Script.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Script.cshtml index dafcdfdc89..3f7c98c7cb 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Script.cshtml +++ b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Script.cshtml @@ -11,20 +11,99 @@

Script tag helper test

- - - + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/appRoot.js b/test/WebSites/MvcTagHelpersWebSite/appRoot.js new file mode 100644 index 0000000000..8845d572ba --- /dev/null +++ b/test/WebSites/MvcTagHelpersWebSite/appRoot.js @@ -0,0 +1 @@ +alert("ERROR!! This should never be loaded"); \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/wwwroot/site.js b/test/WebSites/MvcTagHelpersWebSite/wwwroot/site.js new file mode 100644 index 0000000000..f2d918ec95 --- /dev/null +++ b/test/WebSites/MvcTagHelpersWebSite/wwwroot/site.js @@ -0,0 +1,6 @@ +(function () { + var p = document.createElement("P"); + p.appendChild(document.createTextNode("site.js loaded successfully!")); + p.style.color = "#0fa912"; + document.body.appendChild(p); +})(); \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site2.js b/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site2.js new file mode 100644 index 0000000000..48b6411f60 --- /dev/null +++ b/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site2.js @@ -0,0 +1,6 @@ +(function () { + var p = document.createElement("P"); + p.appendChild(document.createTextNode("site2.js loaded successfully!")); + p.style.color = "#0fa912"; + document.body.appendChild(p); +})(); \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site3.js b/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site3.js new file mode 100644 index 0000000000..8845d572ba --- /dev/null +++ b/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site3.js @@ -0,0 +1 @@ +alert("ERROR!! This should never be loaded"); \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site3.min.css b/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site3.min.css new file mode 100644 index 0000000000..93bd141bdd --- /dev/null +++ b/test/WebSites/MvcTagHelpersWebSite/wwwroot/sub/site3.min.css @@ -0,0 +1,7 @@ +body::after { + display: block; + background-color: #ff0000; + color: #fff; + margin-top: 2.4em; + content: "ERROR: Stylesheet 'site3.min.css' was loaded despite being excluded!"; +} \ No newline at end of file