Add the LinkTagHelper:

- #1580
This commit is contained in:
damianedwards 2015-01-21 17:47:38 -08:00
parent 49294b7d28
commit 3d33418f31
24 changed files with 763 additions and 22 deletions

5
.gitignore vendored
View File

@ -29,4 +29,7 @@ nuget.exe
*.ncrunchsolution
*.*sdf
*.ipch
*.sln.ide
.settings
*.sln.ide
node_modules
**/[Cc]ompiler/[Rr]esources/**/*.js

View File

@ -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

View File

@ -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<IAssemblyProvider, TestAssemblyProvider<Startup>>();
services.AddSingleton<MoviesService>();
});
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(

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link href="~/blank.css" rel="stylesheet" />
<link href="~/site.min.css" rel="stylesheet"
asp-fallback-href="~/site.css"
asp-fallback-test-class="fallback-test"
asp-fallback-test-property="visibility"
asp-fallback-test-value="hidden" />
</head>
<body>
@RenderBody()
</body>
</html>

View File

@ -1 +1,5 @@
@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers"
@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers"
@{
Layout = "_Layout";
}

View File

@ -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-*",

View File

@ -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!";
}

View File

@ -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" ]);
};

View File

@ -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
{
/// <summary>
/// Utility methods for dealing with JavaScript.
/// </summary>
public static class JavaScriptUtility
{
private static readonly Assembly ResourcesAssembly = typeof(JavaScriptUtility).GetTypeInfo().Assembly;
private static readonly ConcurrentDictionary<string, string> Cache =
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
private static readonly IDictionary<char, string> EncodingMap = new Dictionary<char, string>
{
{ '<', @"\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
};
/// <summary>
/// Gets an embedded JavaScript file resource and decodes it for use as a .NET format string.
/// </summary>
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("]]]", "}");
}
/// <summary>
/// Encodes a .NET string for safe use as a JavaScript string literal, including inline in an HTML file.
/// </summary>
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);
}
}
}

View File

@ -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
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;link&gt; elements that supports fallback href paths.
/// </summary>
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 = "<meta name=\"x-stylesheet-fallback-test\" class=\"{0}\" />";
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
};
/// <summary>
/// The URL of a CSS stylesheet to fallback to in the case the primary one fails (as specified in the href
/// attribute).
/// </summary>
[HtmlAttributeName(FallbackHrefAttributeName)]
public string FallbackHref { get; set; }
/// <summary>
/// The class name defined in the stylesheet to use for the fallback test.
/// </summary>
[HtmlAttributeName(FallbackTestClassAttributeName)]
public string FallbackTestClass { get; set; }
/// <summary>
/// The CSS property name to use for the fallback test.
/// </summary>
[HtmlAttributeName(FallbackTestPropertyAttributeName)]
public string FallbackTestProperty { get; set; }
/// <summary>
/// The CSS property value to use for the fallback test.
/// </summary>
[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; }
/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var logger = LoggerFactory.Create<LinkTagHelper>();
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 <link /> tag that loads the primary stylesheet
content.Append("<link ");
foreach (var attribute in output.Attributes)
{
content.AppendFormat(CultureInfo.InvariantCulture, "{0}=\"{1}\" ", attribute.Key, attribute.Value);
}
content.AppendLine("/>");
// Build the <meta /> tag that's used to test for the presence of the stylesheet
content.AppendLine(string.Format(CultureInfo.InvariantCulture, FallbackTestMetaTemplate, FallbackTestClass));
// Build the <script /> tag that checks the effective style of <meta /> tag above and renders the extra
// <link /> tag to load the fallback stylesheet if the test CSS property value is found to be false,
// indicating that the primary stylesheet failed to load.
content.Append("<script>");
content.AppendFormat(CultureInfo.InvariantCulture,
JavaScriptUtility.GetEmbeddedJavaScript(FallbackJavaScriptResourceName),
JavaScriptUtility.JavaScriptStringEncode(FallbackTestProperty),
JavaScriptUtility.JavaScriptStringEncode(FallbackTestValue),
JavaScriptUtility.JavaScriptStringEncode(FallbackHref));
content.Append("</script>");
output.Content = content.ToString();
}
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="ILoggerStructure"/> for log messages regarding <see cref="ITagHelper"/> instances that opt out of
/// processing due to missing required attributes.
/// </summary>
public class MissingAttributeLoggerStructure : ILoggerStructure
{
private readonly string _uniqueId;
private readonly IEnumerable<string> _missingAttributes;
private readonly IEnumerable<KeyValuePair<string, object>> _values;
/// <summary>
/// Creates a new <see cref="MissingAttributeLoggerStructure"/>.
/// </summary>
/// <param name="uniqueId">The unique ID of the HTML element this message applies to.</param>
/// <param name="missingAttributes">The missing required attributes.</param>
public MissingAttributeLoggerStructure(string uniqueId, IEnumerable<string> missingAttributes)
{
_uniqueId = uniqueId;
_missingAttributes = missingAttributes;
_values = new Dictionary<string, object>
{
{ "UniqueId", _uniqueId },
{ "MissingAttributes", _missingAttributes }
};
}
/// <summary>
/// The log message.
/// </summary>
public string Message
{
get
{
return "Tag Helper skipped due to missing required attributes.";
}
}
/// <summary>
/// Gets the values associated with this structured log message.
/// </summary>
/// <returns>The values.</returns>
public IEnumerable<KeyValuePair<string, object>> GetValues()
{
return _values;
}
/// <summary>
/// Generates a human readable string for this structured log message.
/// </summary>
/// <returns>The message.</returns>
public string Format()
{
return string.Format("Tag Helper unique ID: {0}, Missing attributes: {1}",
_uniqueId,
string.Join(",", _missingAttributes));
}
}
}

View File

@ -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
{
/// <summary>
/// Utility related extensions for <see cref="TagHelperContext"/>.
/// </summary>
public static class TagHelperContextExtensions
{
/// <summary>
/// Determines whether a <see cref="ITagHelper" />'s required attributes are present, non null, non empty, and
/// non whitepsace.
/// </summary>
/// <param name="context">The <see cref="TagHelperContext"/>.</param>
/// <param name="requiredAttributes">The attributes the <see cref="ITagHelper" /> requires in order to run.</param>
/// <param name="logger">An optional <see cref="ILogger"/> to log warning details to.</param>
/// <returns>A <see cref="bool"/> indicating whether the <see cref="ITagHelper" /> should run.</returns>
public static bool AllRequiredAttributesArePresent(
[NotNull]this TagHelperContext context,
[NotNull]IEnumerable<string> requiredAttributes,
ILogger logger = null)
{
// Check for all attribute values & log a warning if any required are missing
var atLeastOnePresent = false;
var missingAttrNames = new List<string>();
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;
}
}
}

View File

@ -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('<link rel="stylesheet" href="' + fallbackHref + '"/>');
}
})("[[[0]]]", "[[[1]]]", "[[[2]]]");

View File

@ -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"
}
}

View File

@ -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" ]
}
}

View File

@ -0,0 +1,17 @@

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Link</title>
<link href="/blank.css" rel="stylesheet"></link>
<link rel="stylesheet" href="/link-test.min.css" data-extra="test" />
<meta name="x-stylesheet-fallback-test" class="hidden" />
<script>!function(a,b,c){var d=document,e=d.getElementsByTagName("SCRIPT"),f=e[e.length-1].previousElementSibling;d.defaultView.getComputedStyle(f)[a]!==b&&d.write('<link rel="stylesheet" href="'+c+'"/>')}("visibility","hidden","/link-test.css");</script>
</head>
<body>
<h2>Link Tag Helper Test</h2>
</body>
</html>

View File

@ -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

View File

@ -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<IApplicationBuilder> _app = new TagHelperSample.Web.Startup().Configure;
private readonly Action<IApplicationBuilder, ILoggerFactory> _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<object, Exception, string> formatter)
{
}
}
private class TestDisposable : IDisposable
{
public void Dispose()
{
}
}
}
}

View File

@ -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 <ah /> '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);
}
}
}

View File

@ -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<string, object>
{
{ "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<ILoggerFactory>();
// 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<string, object>
{
{ "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<string, string>
{
{ "rel", "stylesheet"},
{ "data-extra", "something"},
{ "href", "test.css"}
});
var loggerFactory = new Mock<ILoggerFactory>();
// Act
var helper = new LinkTagHelper
{
LoggerFactory = loggerFactory.Object,
FallbackHref = "test.css",
FallbackTestClass = "hidden",
FallbackTestProperty = "visible",
FallbackTestValue = "hidden"
};
helper.Process(context, output);
// Assert
Assert.StartsWith("<link rel=\"stylesheet\" data-extra=\"something\" href=\"test.css\"", output.Content);
}
[Fact]
public void DoesNotRunWhenARequiredAttributeIsMissing()
{
// Arrange
var context = MakeTagHelperContext(
attributes: new Dictionary<string, object>
{
// 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<ILogger>();
var loggerFactory = new Mock<ILoggerFactory>();
loggerFactory.Setup(f => f.Create(It.IsAny<string>())).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<ILogger>();
var loggerFactory = new Mock<ILoggerFactory>();
loggerFactory.Setup(f => f.Create(It.IsAny<string>())).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<string, object> attributes = null,
string content = null)
{
attributes = attributes ?? new Dictionary<string, object>();
return new TagHelperContext(attributes, Guid.NewGuid().ToString("N"), () => Task.FromResult(content));
}
private TagHelperOutput MakeTagHelperOutput(string tagName, IDictionary<string, string> attributes = null)
{
attributes = attributes ?? new Dictionary<string, string>();
return new TagHelperOutput(tagName, attributes);
}
}
}

View File

@ -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": {

View File

@ -155,5 +155,10 @@ namespace MvcTagHelpersWebSite.Controllers
{
return View();
}
public IActionResult Link()
{
return View();
}
}
}

View File

@ -0,0 +1,20 @@
@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers"
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Link</title>
<link href="~/blank.css" rel="stylesheet" />
<link rel="stylesheet" href="~/link-test.min.css" data-extra="test"
asp-fallback-href="~/link-test.css"
asp-fallback-test-class="hidden"
asp-fallback-test-property="visibility"
asp-fallback-test-value="hidden" />
</head>
<body>
<h2>Link Tag Helper Test</h2>
</body>
</html>