diff --git a/samples/TagHelperSample.Web/Views/Home/Index.cshtml b/samples/TagHelperSample.Web/Views/Home/Index.cshtml index 17b3f00d1b..a976d12163 100644 --- a/samples/TagHelperSample.Web/Views/Home/Index.cshtml +++ b/samples/TagHelperSample.Web/Views/Home/Index.cshtml @@ -1,4 +1,7 @@ - +@* + 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 TagHelperSample.Web.Models @model IList @@ -6,6 +9,20 @@ Hello, you're in Development + + + +

Index

@if (Model != null && Model.Count() != 0) { diff --git a/samples/TagHelperSample.Web/wwwroot/fallback.js b/samples/TagHelperSample.Web/wwwroot/fallback.js new file mode 100644 index 0000000000..9de5a6a3e5 --- /dev/null +++ b/samples/TagHelperSample.Web/wwwroot/fallback.js @@ -0,0 +1,6 @@ +// 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. + +function bar() { + document.write("

foo is from fallback

"); +} \ No newline at end of file diff --git a/samples/TagHelperSample.Web/wwwroot/original.js b/samples/TagHelperSample.Web/wwwroot/original.js new file mode 100644 index 0000000000..3097a25b25 --- /dev/null +++ b/samples/TagHelperSample.Web/wwwroot/original.js @@ -0,0 +1,10 @@ +// 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. + +function foo() { + return 1; +} + +function bar() { + document.write("

foo is available

"); +} \ 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 b857040dd2..0f09635b80 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs @@ -1,7 +1,6 @@ // 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; using System.Globalization; using System.Text; using Microsoft.AspNet.Razor.Runtime.TagHelpers; diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs index df89a021de..228e05e144 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs @@ -1,7 +1,6 @@ // 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; using System.Collections.Generic; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.Framework.Logging; @@ -9,15 +8,17 @@ using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Mvc.TagHelpers { /// - /// An for log messages regarding instances that opt out of + /// 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 IEnumerable _missingAttributes; private readonly IEnumerable> _values; + // internal for unit testing. + internal IEnumerable MissingAttributes { get; } + /// /// Creates a new . /// @@ -26,11 +27,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers public MissingAttributeLoggerStructure(string uniqueId, IEnumerable missingAttributes) { _uniqueId = uniqueId; - _missingAttributes = missingAttributes; + MissingAttributes = missingAttributes; _values = new Dictionary { { "UniqueId", _uniqueId }, - { "MissingAttributes", _missingAttributes } + { "MissingAttributes", MissingAttributes } }; } @@ -62,7 +63,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { return string.Format("Tag Helper unique ID: {0}, Missing attributes: {1}", _uniqueId, - string.Join(",", _missingAttributes)); + string.Join(",", MissingAttributes)); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs new file mode 100644 index 0000000000..eac3a38d69 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs @@ -0,0 +1,125 @@ +// 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; +using System.Globalization; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// implementation targeting <script> elements that supports fallback src paths. + /// + /// + /// and are required to not be + /// null or empty to process. + /// + public class ScriptTagHelper : TagHelper + { + private const string FallbackSrcAttributeName = "asp-fallback-src"; + 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, + }; + + /// + /// The URL of a Script tag to fallback to in the case the primary one fails (as specified in the src + /// attribute). + /// + [HtmlAttributeName(FallbackSrcAttributeName)] + public string FallbackSrc { get; set; } + + /// + /// The script method defined in the primary script to use for the fallback test. + /// + [HtmlAttributeName(FallbackTestExpressionAttributeName)] + public string FallbackTestExpression { get; set; } + + // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing. + [Activate] + protected internal ILogger Logger { get; set; } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (!context.AllRequiredAttributesArePresent(RequiredAttributes, Logger)) + { + if (Logger.IsEnabled(LogLevel.Verbose)) + { + Logger.WriteVerbose("Skipping processing for {0} {1}", nameof(ScriptTagHelper), context.UniqueId); + } + + 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(); + } + + private void AppendSrc(StringBuilder content, string srcKey) + { + // 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("\\\""); + } + } +} 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 new file mode 100644 index 0000000000..bec2f8b480 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html @@ -0,0 +1,27 @@ + + + + + Script + + +

Script tag helper test

+ + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs index 73942af871..b937fe5d8b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs @@ -41,6 +41,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests [InlineData("Environment", null)] // Testing the LinkTagHelper [InlineData("Link", null)] + // Testing the ScriptTagHelper + [InlineData("Script", null)] public async Task MvcTagHelpers_GeneratesExpectedResults(string action, string antiForgeryPath) { // Arrange @@ -67,6 +69,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var forgeryToken = AntiForgeryTestHelper.RetrieveAntiForgeryToken(responseContent, antiForgeryPath); expectedContent = string.Format(expectedContent, forgeryToken); } + Assert.Equal(expectedContent.Trim(), responseContent.Trim()); } diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs index ed3bc509f5..0a9999a49c 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs @@ -9,7 +9,7 @@ using Microsoft.Framework.Logging; using Moq; using Xunit; -namespace Microsoft.AspNet.Mvc.TagHelpers.Test +namespace Microsoft.AspNet.Mvc.TagHelpers { public class LinkTagHelperTest { diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs new file mode 100644 index 0000000000..ff05eda726 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs @@ -0,0 +1,261 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.Logging; +using Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + public class ScriptTagHelperTest + { + [Theory] + [InlineData("~/blank.js")] + [InlineData(null)] + public async Task RunsWhenRequiredAttributesArePresent(string srcValue) + { + // 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 context = MakeTagHelperContext(attributes); + + var output = MakeTagHelperOutput("script"); + var logger = CreateLogger(); + + var helper = new ScriptTagHelper() + { + Logger = logger, + FallbackSrc = "http://www.example.com/blank.js", + FallbackTestExpression = "isavailable()", + }; + + // Act + await helper.ProcessAsync(context, output); + + // Assert + Assert.Null(output.TagName); + Assert.NotNull(output.Content); + Assert.True(output.ContentSet); + Assert.Empty(logger.Logged); + } + + public static TheoryData MissingAttributeDataSet + { + get + { + return new TheoryData, ScriptTagHelper, string> + { + { + new Dictionary // the attributes provided + { + { "asp-fallback-src", "http://www.example.com/blank.js" }, + }, + new ScriptTagHelper() // the tag helper + { + FallbackTestExpression = "isavailable()", + }, + "asp-fallback-test" // missing attribute + }, + + { + new Dictionary // the attributes provided + { + { "asp-fallback-test", "isavailable()" }, + }, + new ScriptTagHelper() // the tag helper + { + FallbackTestExpression = "http://www.example.com/blank.js", + }, + "asp-fallback-src" // missing attribute + }, + }; + } + } + + [Theory] + [MemberData(nameof(MissingAttributeDataSet))] + public async Task DoesNotRunWhenARequiredAttributeIsMissing( + Dictionary attributes, + ScriptTagHelper helper, + string attributeMissing) + { + // Arrange + Assert.Single(attributes); + + var context = MakeTagHelperContext(attributes); + + var output = MakeTagHelperOutput("script"); + var logger = CreateLogger(); + + helper.Logger = logger; + + // Act + await helper.ProcessAsync(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.False(output.ContentSet); + } + + [Fact] + public async Task DoesNotRunWhenAllRequiredAttributesAreMissing() + { + // Arrange + var context = MakeTagHelperContext(); + var output = MakeTagHelperOutput("script"); + var logger = CreateLogger(); + + var helper = new ScriptTagHelper + { + Logger = logger, + }; + + // Act + await helper.ProcessAsync(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.False(output.ContentSet); + } + + [Theory] + [MemberData(nameof(MissingAttributeDataSet))] + public async Task LogsWhenARequiredAttributeIsMissing( + Dictionary attributes, + ScriptTagHelper helper, + string attributeMissing) + { + // Arrange + Assert.Single(attributes); + + var context = MakeTagHelperContext(attributes); + + var output = MakeTagHelperOutput("script"); + var logger = CreateLogger(); + + helper.Logger = logger; + + // Act + await helper.ProcessAsync(context, 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() + { + // Arrange + var context = MakeTagHelperContext(); + var output = MakeTagHelperOutput("script"); + var logger = CreateLogger(); + + var helper = new ScriptTagHelper + { + Logger = logger, + }; + + // Act + await helper.ProcessAsync(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.False(output.ContentSet); + + Assert.Single(logger.Logged); + + Assert.Equal(LogLevel.Verbose, logger.Logged[0].LogLevel); + Assert.IsAssignableFrom(logger.Logged[0].State); + Assert.StartsWith("Skipping processing for ScriptTagHelper", + ((ILoggerStructure)logger.Logged[0].State).Format()); + } + + [Fact] + public async Task PreservesOrderOfSourceAttributesWhenRun() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + { "data-extra", "something"}, + { "src", "/blank.js"}, + { "data-more", "else"}, + { "asp-fallback-src", "http://www.example.com/blank.js" }, + { "asp-fallback-test", "isavailable()" }, + }); + + var output = MakeTagHelperOutput("link", + attributes: new Dictionary + { + { "data-extra", "something"}, + { "src", "/blank.js"}, + { "data-more", "else"}, + }); + + var logger = CreateLogger(); + + var helper = new ScriptTagHelper + { + Logger = logger, + FallbackSrc = "~/blank.js", + FallbackTestExpression = "http://www.example.com/blank.js", + }; + + // Act + await helper.ProcessAsync(context, output); + + // Assert + Assert.StartsWith(" + + + + + + + + \ No newline at end of file