From 3d33418f315ef73c03e777538360746878be744d Mon Sep 17 00:00:00 2001 From: damianedwards Date: Wed, 21 Jan 2015 17:47:38 -0800 Subject: [PATCH] Add the LinkTagHelper: - #1580 --- .gitignore | 5 +- Mvc.NoFun.sln | 2 +- samples/TagHelperSample.Web/Startup.cs | 17 +- .../Views/Shared/_Layout.cshtml | 14 ++ .../Views/_ViewStart.cshtml | 6 +- samples/TagHelperSample.Web/project.json | 2 + samples/TagHelperSample.Web/wwwroot/blank.css | 0 samples/TagHelperSample.Web/wwwroot/site.css | 13 ++ .../Gruntfile.js | 25 +++ .../JavaScriptUtility.cs | 110 ++++++++++++ .../LinkTagHelper.cs | 108 ++++++++++++ .../MissingAttributeLoggerStructure.cs | 68 ++++++++ .../TagHelperContextExtensions.cs | 66 ++++++++ .../js/LinkTagHelper_FallbackJavaScript.js | 12 ++ .../package.json | 10 ++ .../project.json | 34 ++-- ...HelpersWebSite.MvcTagHelper_Home.Link.html | 17 ++ .../MvcTagHelpersTest.cs | 2 + .../TagHelperSampleTest.cs | 45 ++++- .../JavaScriptUtilityTest.cs | 45 +++++ .../LinkTagHelperTest.cs | 158 ++++++++++++++++++ .../project.json | 1 + .../MvcTagHelper_HomeController.cs | 5 + .../Views/MvcTagHelper_Home/Link.cshtml | 20 +++ 24 files changed, 763 insertions(+), 22 deletions(-) create mode 100644 samples/TagHelperSample.Web/Views/Shared/_Layout.cshtml create mode 100644 samples/TagHelperSample.Web/wwwroot/blank.css create mode 100644 samples/TagHelperSample.Web/wwwroot/site.css create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/Gruntfile.js create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/JavaScriptUtility.cs create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperContextExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/package.json create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html create mode 100644 test/Microsoft.AspNet.Mvc.TagHelpers.Test/JavaScriptUtilityTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml diff --git a/.gitignore b/.gitignore index b0dbe53b45..3dba866cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,7 @@ nuget.exe *.ncrunchsolution *.*sdf *.ipch -*.sln.ide \ No newline at end of file +.settings +*.sln.ide +node_modules +**/[Cc]ompiler/[Rr]esources/**/*.js diff --git a/Mvc.NoFun.sln b/Mvc.NoFun.sln index 1aac1cf088..8f4533fd52 100644 --- a/Mvc.NoFun.sln +++ b/Mvc.NoFun.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22522.0 +VisualStudioVersion = 14.0.22526.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject diff --git a/samples/TagHelperSample.Web/Startup.cs b/samples/TagHelperSample.Web/Startup.cs index f530f45f79..8da8ff2024 100644 --- a/samples/TagHelperSample.Web/Startup.cs +++ b/samples/TagHelperSample.Web/Startup.cs @@ -1,26 +1,39 @@ // 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 Microsoft.AspNet.Builder; using Microsoft.AspNet.Mvc; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; +using Microsoft.Framework.Logging.Console; using TagHelperSample.Web.Services; namespace TagHelperSample.Web { public class Startup { - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { app.UseServices(services => { services.AddMvc(); - + // Setup services with a test AssemblyProvider so that only the sample's assemblies are loaded. This // prevents loading controllers from other assemblies when the sample is used in functional tests. services.AddTransient>(); services.AddSingleton(); }); + + loggerFactory.AddConsole((name, logLevel) => + name.StartsWith("Microsoft.AspNet.Mvc.TagHelpers", StringComparison.OrdinalIgnoreCase) + || (name.StartsWith("Microsoft.Net.Http.Server.WebListener", StringComparison.OrdinalIgnoreCase) + && logLevel >= LogLevel.Information)); + + app.UseErrorPage(); + + app.UseStaticFiles(); + app.UseMvc(routes => { routes.MapRoute( diff --git a/samples/TagHelperSample.Web/Views/Shared/_Layout.cshtml b/samples/TagHelperSample.Web/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000000..fbad8b436c --- /dev/null +++ b/samples/TagHelperSample.Web/Views/Shared/_Layout.cshtml @@ -0,0 +1,14 @@ + + + + + + + + @RenderBody() + + \ No newline at end of file diff --git a/samples/TagHelperSample.Web/Views/_ViewStart.cshtml b/samples/TagHelperSample.Web/Views/_ViewStart.cshtml index 8febe27b2c..db880152dc 100644 --- a/samples/TagHelperSample.Web/Views/_ViewStart.cshtml +++ b/samples/TagHelperSample.Web/Views/_ViewStart.cshtml @@ -1 +1,5 @@ -@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers" \ No newline at end of file +@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers" + +@{ + Layout = "_Layout"; +} \ No newline at end of file diff --git a/samples/TagHelperSample.Web/project.json b/samples/TagHelperSample.Web/project.json index 3b21486dec..628f1f61f2 100644 --- a/samples/TagHelperSample.Web/project.json +++ b/samples/TagHelperSample.Web/project.json @@ -8,6 +8,8 @@ }, "dependencies": { "Kestrel": "1.0.0-*", + "Microsoft.Framework.Logging.Console": "1.0.0-*", + "Microsoft.AspNet.Diagnostics": "1.0.0-*", "Microsoft.AspNet.Mvc": "6.0.0-*", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-*", "Microsoft.AspNet.Server.IIS": "1.0.0-*", diff --git a/samples/TagHelperSample.Web/wwwroot/blank.css b/samples/TagHelperSample.Web/wwwroot/blank.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samples/TagHelperSample.Web/wwwroot/site.css b/samples/TagHelperSample.Web/wwwroot/site.css new file mode 100644 index 0000000000..e69a71c805 --- /dev/null +++ b/samples/TagHelperSample.Web/wwwroot/site.css @@ -0,0 +1,13 @@ +meta.fallback-test { + visibility: hidden; +} + +body::after { + display: block; + color: #0fa912; + font-size: 0.9em; + margin-top: 2.4em; + content: "Stylesheet 'site.css' loaded successfully!"; +} + + diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Gruntfile.js b/src/Microsoft.AspNet.Mvc.TagHelpers/Gruntfile.js new file mode 100644 index 0000000000..b4ac01272e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Gruntfile.js @@ -0,0 +1,25 @@ +// 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. + +module.exports = function (grunt) { + grunt.initConfig({ + jshint: { + scripts: [ "js/**/*.js" ] + }, + uglify: { + scripts: { + files: [{ + expand: true, + cwd: "js", + src: "**/*.js", + dest: "Compiler/Resources" + }] + } + } + }); + + grunt.loadNpmTasks("grunt-contrib-jshint"); + grunt.loadNpmTasks("grunt-contrib-uglify"); + + grunt.registerTask("default", [ "jshint", "uglify" ]); +}; \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/JavaScriptUtility.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/JavaScriptUtility.cs new file mode 100644 index 0000000000..dfeb359a2d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/JavaScriptUtility.cs @@ -0,0 +1,110 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// Utility methods for dealing with JavaScript. + /// + public static class JavaScriptUtility + { + private static readonly Assembly ResourcesAssembly = typeof(JavaScriptUtility).GetTypeInfo().Assembly; + + private static readonly ConcurrentDictionary Cache = + new ConcurrentDictionary(StringComparer.Ordinal); + + private static readonly IDictionary EncodingMap = new Dictionary + { + { '<', @"\u003c" }, // opening angle-bracket + { '>', @"\u003e" }, // closing angle-bracket + { '\'', @"\u0027" }, // single quote + { '"', @"\u0022" }, // double quote + { '\\', @"\\" }, // back slash + { '\r', "\\r" }, // carriage return + { '\n', "\\n" }, // new line + { '\u0085', @"\u0085" }, // next line + { '&', @"\u0026" }, // ampersand + }; + + /// + /// Gets an embedded JavaScript file resource and decodes it for use as a .NET format string. + /// + public static string GetEmbeddedJavaScript(string resourceName) + { + return Cache.GetOrAdd(resourceName, key => + { + // Load the JavaScript from embedded resource + using (var resourceStream = ResourcesAssembly.GetManifestResourceStream(key)) + { + Debug.Assert(resourceStream != null, "Embedded resource missing. Ensure 'prebuild' script has run."); + + using (var streamReader = new StreamReader(resourceStream)) + { + var script = streamReader.ReadToEnd(); + + // Replace unescaped/escaped chars with their equivalent + return PrepareFormatString(script); + } + } + }); + } + + // Internal so we can test this separately + internal static string PrepareFormatString(string input) + { + return input.Replace("{", "{{") + .Replace("}", "}}") + .Replace("[[[", "{") + .Replace("]]]", "}"); + } + + /// + /// Encodes a .NET string for safe use as a JavaScript string literal, including inline in an HTML file. + /// + internal static string JavaScriptStringEncode(string value) + { + var result = new StringBuilder(); + + foreach (var character in value) + { + if (CharRequiresJavaScriptEncoding(character)) + { + EncodeAndAppendChar(result, character); + } + else + { + result.Append(character); + } + } + + return result.ToString(); + } + + private static bool CharRequiresJavaScriptEncoding(char character) + { + return character < 0x20 // Control chars + || EncodingMap.ContainsKey(character); + } + + private static void EncodeAndAppendChar(StringBuilder builder, char character) + { + string mapped; + + if (!EncodingMap.TryGetValue(character, out mapped)) + { + mapped = "\\u" + ((int)character).ToString("x4", CultureInfo.InvariantCulture); + } + + builder.Append(mapped); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs new file mode 100644 index 0000000000..3ea3820e5f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs @@ -0,0 +1,108 @@ +// 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; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// implementation targeting <link> elements that supports fallback href paths. + /// + public class LinkTagHelper : TagHelper + { + private const string FallbackHrefAttributeName = "asp-fallback-href"; + private const string FallbackTestClassAttributeName = "asp-fallback-test-class"; + private const string FallbackTestPropertyAttributeName = "asp-fallback-test-property"; + private const string FallbackTestValueAttributeName = "asp-fallback-test-value"; + private const string FallbackTestMetaTemplate = ""; + private const string FallbackJavaScriptResourceName = "compiler/resources/LinkTagHelper_FallbackJavaScript.js"; + + // NOTE: All attributes are required for the LinkTagHelper to process. + private static readonly string[] RequiredAttributes = new[] + { + FallbackHrefAttributeName, + FallbackTestClassAttributeName, + FallbackTestPropertyAttributeName, + FallbackTestValueAttributeName + }; + + /// + /// The URL of a CSS stylesheet to fallback to in the case the primary one fails (as specified in the href + /// attribute). + /// + [HtmlAttributeName(FallbackHrefAttributeName)] + public string FallbackHref { get; set; } + + /// + /// The class name defined in the stylesheet to use for the fallback test. + /// + [HtmlAttributeName(FallbackTestClassAttributeName)] + public string FallbackTestClass { get; set; } + + /// + /// The CSS property name to use for the fallback test. + /// + [HtmlAttributeName(FallbackTestPropertyAttributeName)] + public string FallbackTestProperty { get; set; } + + /// + /// The CSS property value to use for the fallback test. + /// + [HtmlAttributeName(FallbackTestValueAttributeName)] + public string FallbackTestValue { get; set; } + + // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing. + [Activate] + protected internal ILoggerFactory LoggerFactory { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + var logger = LoggerFactory.Create(); + + if (!context.AllRequiredAttributesArePresent(RequiredAttributes, logger)) + { + if (logger.IsEnabled(LogLevel.Verbose)) + { + logger.WriteVerbose("Skipping processing for {0} {1}", nameof(LinkTagHelper), context.UniqueId); + } + return; + } + + var content = new StringBuilder(); + + // NOTE: Values in TagHelperOutput.Attributes are already HtmlEncoded + + // We've taken over rendering here so prevent the element rendering the outer tag + output.TagName = null; + + // Rebuild the tag that loads the primary stylesheet + content.Append(""); + + // Build the tag that's used to test for the presence of the stylesheet + content.AppendLine(string.Format(CultureInfo.InvariantCulture, FallbackTestMetaTemplate, FallbackTestClass)); + + // Build the "); + + output.Content = content.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs new file mode 100644 index 0000000000..df89a021de --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs @@ -0,0 +1,68 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// 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; + + /// + /// Creates a new . + /// + /// The unique ID of the HTML element this message applies to. + /// The missing required attributes. + public MissingAttributeLoggerStructure(string uniqueId, IEnumerable missingAttributes) + { + _uniqueId = uniqueId; + _missingAttributes = missingAttributes; + _values = new Dictionary + { + { "UniqueId", _uniqueId }, + { "MissingAttributes", _missingAttributes } + }; + } + + /// + /// The log message. + /// + public string Message + { + get + { + return "Tag Helper skipped due to 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 unique ID: {0}, Missing attributes: {1}", + _uniqueId, + string.Join(",", _missingAttributes)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperContextExtensions.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperContextExtensions.cs new file mode 100644 index 0000000000..e1bde2e51b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperContextExtensions.cs @@ -0,0 +1,66 @@ +// 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 Microsoft.Framework.Logging; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.TagHelpers; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Utility related extensions for . + /// + public static class TagHelperContextExtensions + { + /// + /// Determines whether a 's required attributes are present, non null, non empty, and + /// non whitepsace. + /// + /// 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]this TagHelperContext context, + [NotNull]IEnumerable requiredAttributes, + ILogger logger = null) + { + // Check for all attribute values & log a warning if any required are missing + var atLeastOnePresent = false; + var missingAttrNames = new List(); + + foreach (var attr in requiredAttributes) + { + if (!context.AllAttributes.ContainsKey(attr) + || context.AllAttributes[attr] == null + || string.IsNullOrWhiteSpace(context.AllAttributes[attr] as string)) + { + // Missing attribute! + missingAttrNames.Add(attr); + } + else + { + atLeastOnePresent = true; + } + } + + if (missingAttrNames.Any()) + { + if (atLeastOnePresent && 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(context.UniqueId, missingAttrNames)); + } + + return false; + } + + // All required attributes present + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js b/src/Microsoft.AspNet.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js new file mode 100644 index 0000000000..0dc37d59f0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js @@ -0,0 +1,12 @@ +(function (cssTestPropertyName, cssTestPropertyValue, fallbackHref) { + // This function finds the previous element (assumed to be meta) and tests its current CSS style using the passed + // values, to determine if a stylesheet was loaded. If not, this function loads the fallback stylesheet via + // document.write. + var doc = document, + scriptElements = doc.getElementsByTagName("SCRIPT"), + meta = scriptElements[scriptElements.length - 1].previousElementSibling; + + if (doc.defaultView.getComputedStyle(meta)[cssTestPropertyName] !== cssTestPropertyValue) { + doc.write(''); + } +})("[[[0]]]", "[[[1]]]", "[[[2]]]"); \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/package.json b/src/Microsoft.AspNet.Mvc.TagHelpers/package.json new file mode 100644 index 0000000000..f4adf22afc --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/package.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "name": "Microsoft.AspNet.Mvc.TagHelpers", + "private": true, + "devDependencies": { + "grunt": "~0.4.5", + "grunt-contrib-jshint": "~0.11.0", + "grunt-contrib-uglify": "~0.7.0" + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/project.json b/src/Microsoft.AspNet.Mvc.TagHelpers/project.json index 9a0034ef94..929d12436b 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/project.json +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/project.json @@ -1,17 +1,21 @@ { - "description": "Contains a default set of Tag Helpers for building ASP.NET MVC applications.", - "version": "6.0.0-*", - "compilationOptions": { - "warningsAsErrors": true - }, - "dependencies": { - "Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" }, - "Microsoft.AspNet.Mvc.Razor": "6.0.0-*", - "Microsoft.Framework.Cache.Memory": "1.0.0-*", - "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*" - }, - "frameworks": { - "aspnet50": {}, - "aspnetcore50": {} - } + "description": "Contains a default set of Tag Helpers for building ASP.NET MVC applications.", + "version": "6.0.0-*", + "compilationOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" }, + "Microsoft.AspNet.Mvc.Razor": "6.0.0-*", + "Microsoft.Framework.Cache.Memory": "1.0.0-*", + "Microsoft.Framework.Logging.Interfaces": { "version": "1.0.0-*", "type": "build" }, + "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + }, + "scripts": { + "prebuild": [ "npm install", "grunt" ] + } } \ No newline at end of file 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 new file mode 100644 index 0000000000..07e924e710 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html @@ -0,0 +1,17 @@ + + + + + + Link + + + + + + + +

Link 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 ee5cc81641..73942af871 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs @@ -39,6 +39,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests [InlineData("EditWarehouse", null)] // Testing the EnvironmentTagHelper [InlineData("Environment", null)] + // Testing the LinkTagHelper + [InlineData("Link", null)] public async Task MvcTagHelpers_GeneratesExpectedResults(string action, string antiForgeryPath) { // Arrange diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TagHelperSampleTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TagHelperSampleTest.cs index 719e4a1c6b..c4eda3e9dc 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/TagHelperSampleTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TagHelperSampleTest.cs @@ -8,6 +8,7 @@ using System.Net; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; +using Microsoft.Framework.Logging; using Xunit; namespace Microsoft.AspNet.Mvc.FunctionalTests @@ -31,10 +32,11 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests "/Home/Index", }; + private readonly ILoggerFactory _loggerFactory = new TestLoggerFactory(); // Path relative to Mvc\\test\Microsoft.AspNet.Mvc.FunctionalTests private readonly IServiceProvider _services = TestHelper.CreateServices("TagHelperSample.Web", Path.Combine("..", "..", "samples")); - private readonly Action _app = new TagHelperSample.Web.Startup().Configure; + private readonly Action _app = new TagHelperSample.Web.Startup().Configure; [Fact] public async Task Home_Pages_ReturnSuccess() @@ -42,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests using (TestHelper.ReplaceCallContextServiceLocationService(_services)) { // Arrange - var server = TestServer.Create(_services, _app); + var server = TestServer.Create(_services, app => _app(app, _loggerFactory)); var client = server.CreateClient(); for (var index = 0; index < Paths.Count; index++) @@ -57,5 +59,44 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } } } + + private class TestLoggerFactory : ILoggerFactory + { + public void AddProvider(ILoggerProvider provider) + { + + } + + public ILogger Create(string name) + { + return new TestLogger(); + } + } + + private class TestLogger : ILogger + { + public bool IsEnabled(LogLevel level) + { + return false; + } + + public IDisposable BeginScope(object scope) + { + return new TestDisposable(); + } + + public void Write(LogLevel logLevel, int eventId, object state, Exception exception, Func formatter) + { + + } + } + + private class TestDisposable : IDisposable + { + public void Dispose() + { + + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/JavaScriptUtilityTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/JavaScriptUtilityTest.cs new file mode 100644 index 0000000000..8b417156e7 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/JavaScriptUtilityTest.cs @@ -0,0 +1,45 @@ +// 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 Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers.Test +{ + public class JavaScriptUtilityTest + { + [Theory] + [InlineData("Hello World", "Hello World")] + [InlineData("Hello & World", "Hello \\u0026 World")] + [InlineData("Hello \r World", "Hello \\r World")] + [InlineData("Hello \n World", "Hello \\n World")] + [InlineData("Hello < World", "Hello \\u003c World")] + [InlineData("Hello > World", "Hello \\u003e World")] + [InlineData("Hello ' World", "Hello \\u0027 World")] + [InlineData("Hello \" World", "Hello \\u0022 World")] + [InlineData("Hello \\ World", "Hello \\\\ World")] + [InlineData("Hello \u0005 \u001f World", "Hello \\u0005 \\u001f World")] + [InlineData("Hello \r\n 'eep' & \"hey\" World", "Hello \\r\\n \\u003cah /\\u003e \\u0027eep\\u0027 \\u0026 \\u0022hey\\u0022 World")] + public void JavaScriptEncode_EncodesCorrectly(string input, string expectedOutput) + { + // Act + var result = JavaScriptUtility.JavaScriptStringEncode(input); + + // Assert + Assert.Equal(expectedOutput, result); + } + + [Theory] + [InlineData("window.alert(\"[[[0]]]\")", "window.alert(\"{0}\")")] + [InlineData("var test = { a: 1 };", "var test = {{ a: 1 }};")] + [InlineData("var test = { a: 1, b: \"[[[0]]]\" };", "var test = {{ a: 1, b: \"{0}\" }};")] + public void PrepareFormatString_PreparesJavaScriptCorrectly(string input, string expectedOutput) + { + // Act + var result = JavaScriptUtility.PrepareFormatString(input); + + // Assert + Assert.Equal(expectedOutput, result); + } + } +} \ 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 new file mode 100644 index 0000000000..2b8c1f1da4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs @@ -0,0 +1,158 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.Logging; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers.Test +{ + public class LinkTagHelperTest + { + [Fact] + public void RunsWhenRequiredAttributesArePresent() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + { "asp-fallback-href", "test.css" }, + { "asp-fallback-test-class", "hidden" }, + { "asp-fallback-test-property", "visible" }, + { "asp-fallback-test-value", "hidden" }, + }); + var output = MakeTagHelperOutput("link"); + var loggerFactory = new Mock(); + + // Act + var helper = new LinkTagHelper + { + LoggerFactory = loggerFactory.Object, + FallbackHref = "test.css", + FallbackTestClass = "hidden", + FallbackTestProperty = "visible", + FallbackTestValue = "hidden" + }; + helper.Process(context, output); + + // Assert + Assert.Null(output.TagName); + Assert.NotNull(output.Content); + Assert.True(output.ContentSet); + } + + [Fact] + public void PreservesOrderOfSourceAttributesWhenRun() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + { "rel", "stylesheet"}, + { "data-extra", "something"}, + { "href", "test.css"}, + { "asp-fallback-href", "test.css" }, + { "asp-fallback-test-class", "hidden" }, + { "asp-fallback-test-property", "visible" }, + { "asp-fallback-test-value", "hidden" } + }); + var output = MakeTagHelperOutput("link", + attributes: new Dictionary + { + { "rel", "stylesheet"}, + { "data-extra", "something"}, + { "href", "test.css"} + }); + var loggerFactory = new Mock(); + + // Act + var helper = new LinkTagHelper + { + LoggerFactory = loggerFactory.Object, + FallbackHref = "test.css", + FallbackTestClass = "hidden", + FallbackTestProperty = "visible", + FallbackTestValue = "hidden" + }; + helper.Process(context, output); + + // Assert + Assert.StartsWith(" + { + // This is commented out on purpose: { "asp-fallback-href", "test.css" }, + { "asp-fallback-test-class", "hidden" }, + { "asp-fallback-test-property", "visible" }, + { "asp-fallback-test-value", "hidden" }, + }); + var output = MakeTagHelperOutput("link"); + var logger = new Mock(); + var loggerFactory = new Mock(); + loggerFactory.Setup(f => f.Create(It.IsAny())).Returns(logger.Object); + + // Act + var helper = new LinkTagHelper + { + LoggerFactory = loggerFactory.Object, + // This is commented out on purpose: FallbackHref = "test.css", + FallbackTestClass = "hidden", + FallbackTestProperty = "visible", + FallbackTestValue = "hidden" + }; + helper.Process(context, output); + + // Assert + Assert.NotNull(output.TagName); + Assert.False(output.ContentSet); + } + + [Fact] + public void DoesNotRunWhenAllRequiredAttributesAreMissing() + { + // Arrange + var context = MakeTagHelperContext(); + var output = MakeTagHelperOutput("link"); + var logger = new Mock(); + var loggerFactory = new Mock(); + loggerFactory.Setup(f => f.Create(It.IsAny())).Returns(logger.Object); + + // Act + var helper = new LinkTagHelper + { + LoggerFactory = loggerFactory.Object + }; + helper.Process(context, output); + + // Assert + Assert.NotNull(output.TagName); + Assert.False(output.ContentSet); + } + + private TagHelperContext MakeTagHelperContext( + IDictionary attributes = null, + string content = null) + { + attributes = attributes ?? new Dictionary(); + + return new TagHelperContext(attributes, Guid.NewGuid().ToString("N"), () => Task.FromResult(content)); + } + + private TagHelperOutput MakeTagHelperOutput(string tagName, IDictionary attributes = null) + { + attributes = attributes ?? new Dictionary(); + + return new TagHelperOutput(tagName, attributes); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/project.json b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/project.json index a05800beec..9b45d8f7fa 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/project.json @@ -2,6 +2,7 @@ "dependencies": { "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*", + "Microsoft.Framework.Logging.Interfaces": { "version": "1.0.0-*", "type": "build" }, "xunit.runner.kre": "1.0.0-*" }, "commands": { diff --git a/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs b/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs index 83c3d697af..7f5317626c 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs +++ b/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs @@ -155,5 +155,10 @@ namespace MvcTagHelpersWebSite.Controllers { return View(); } + + public IActionResult Link() + { + return View(); + } } } diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml new file mode 100644 index 0000000000..66e6432164 --- /dev/null +++ b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml @@ -0,0 +1,20 @@ +@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers" + + + + + + Link + + + + + +

Link Tag Helper Test

+ + + \ No newline at end of file