diff --git a/Mvc.sln b/Mvc.sln index 8ecd806165..9bd980f531 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22115.0 +VisualStudioVersion = 14.0.22209.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -102,6 +102,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TagHelperSample.Web", "samp EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.TagHelpers", "src\Microsoft.AspNet.Mvc.TagHelpers\Microsoft.AspNet.Mvc.TagHelpers.kproj", "{B2347320-308E-4D2B-AEC8-005DFA68B0C9}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.TagHelpers.Test", "test\Microsoft.AspNet.Mvc.TagHelpers.Test\Microsoft.AspNet.Mvc.TagHelpers.Test.kproj", "{860119ED-3DB1-424D-8D0A-30132A8A7D96}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -542,6 +544,16 @@ Global {B2347320-308E-4D2B-AEC8-005DFA68B0C9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {B2347320-308E-4D2B-AEC8-005DFA68B0C9}.Release|Mixed Platforms.Build.0 = Release|Any CPU {B2347320-308E-4D2B-AEC8-005DFA68B0C9}.Release|x86.ActiveCfg = Release|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Debug|x86.ActiveCfg = Debug|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Release|Any CPU.Build.0 = Release|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {860119ED-3DB1-424D-8D0A-30132A8A7D96}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -591,5 +603,6 @@ Global {5DE8E4D9-AACD-4B5F-819F-F091383FB996} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {2223120F-D675-40DA-8CD8-11DC14A0B2C7} = {DAAE4C74-D06F-4874-A166-33305D2643CE} {B2347320-308E-4D2B-AEC8-005DFA68B0C9} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {860119ED-3DB1-424D-8D0A-30132A8A7D96} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Microsoft.AspNet.Mvc.TagHelpers.Test.kproj b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Microsoft.AspNet.Mvc.TagHelpers.Test.kproj new file mode 100644 index 0000000000..7321c7f047 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Microsoft.AspNet.Mvc.TagHelpers.Test.kproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 860119ed-3db1-424d-8d0a-30132a8a7d96 + Microsoft.AspNet.Mvc.TagHelpers.Test + + + + 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs new file mode 100644 index 0000000000..457e01716e --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TestableHtmlGenerator.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.Generic; +using System.IO; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Security.DataProtection; +using Microsoft.Framework.OptionsModel; +using Moq; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + public class TestableHtmlGenerator : DefaultHtmlGenerator + { + private IDictionary _validationAttributes; + + public TestableHtmlGenerator(IModelMetadataProvider metadataProvider) + : this(metadataProvider, Mock.Of()) + { + } + + public TestableHtmlGenerator(IModelMetadataProvider metadataProvider, IUrlHelper urlHelper) + : this( + metadataProvider, + urlHelper, + validationAttributes: new Dictionary(StringComparer.OrdinalIgnoreCase)) + { + } + + public TestableHtmlGenerator( + IModelMetadataProvider metadataProvider, + IUrlHelper urlHelper, + IDictionary validationAttributes) + : base(Mock.Of(), GetAntiForgery(), metadataProvider, urlHelper) + { + _validationAttributes = validationAttributes; + } + + public IDictionary ValidationAttributes + { + get { return _validationAttributes; } + } + + public static ViewContext GetViewContext( + object model, + IHtmlGenerator htmlGenerator, + IModelMetadataProvider metadataProvider) + { + var serviceProvider = new Mock(); + serviceProvider + .Setup(provider => provider.GetService(typeof(IHtmlGenerator))) + .Returns(htmlGenerator); + + var httpContext = new Mock(); + httpContext + .Setup(context => context.RequestServices) + .Returns(serviceProvider.Object); + + var actionContext = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + var viewData = new ViewDataDictionary(metadataProvider) + { + Model = model, + }; + var viewContext = new ViewContext(actionContext, Mock.Of(), viewData, new StringWriter()); + + return viewContext; + } + + public override TagBuilder GenerateAntiForgery(ViewContext viewContext) + { + return new TagBuilder("input") + { + Attributes = + { + { "name", "__RequestVerificationToken" }, + { "type", "hidden" }, + { "value", "olJlUDjrouRNWLen4tQJhauj1Z1rrvnb3QD65cmQU1Ykqi6S4" }, // 50 chars of a token. + }, + }; + } + + protected override IDictionary GetValidationAttributes( + ViewContext viewContext, + ModelMetadata metadata, + string name) + { + return ValidationAttributes; + } + + private static AntiForgery GetAntiForgery() + { + // AntiForgery must be passed to TestableHtmlGenerator constructor but will never be called. + var optionsAccessor = new Mock>(); + optionsAccessor + .SetupGet(o => o.Options) + .Returns(new MvcOptions()); + var antiForgery = new AntiForgery( + Mock.Of(), + Mock.Of(), + Mock.Of(), + optionsAccessor.Object); + + return antiForgery; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TextAreaTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TextAreaTagHelperTest.cs new file mode 100644 index 0000000000..20faf2cca6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TextAreaTagHelperTest.cs @@ -0,0 +1,194 @@ +// 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.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Razor; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + public class TextAreaTagHelperTest + { + // Model (List or Model instance), container type (Model or NestModel), model accessor, + // property path, expected content. + public static TheoryData, string, string> TestDataSet + { + get + { + var modelWithNull = new Model + { + NestedModel = new NestedModel + { + Text = null, + }, + Text = null, + }; + var modelWithText = new Model + { + NestedModel = new NestedModel + { + Text = "inner text", + }, + Text = "outer text", + }; + var models = new List + { + modelWithNull, + modelWithText, + }; + + return new TheoryData, string, string> + { + { null, typeof(Model), () => null, "Text", + Environment.NewLine }, + + { modelWithNull, typeof(Model), () => modelWithNull.Text, "Text", + Environment.NewLine }, + { modelWithText, typeof(Model), () => modelWithText.Text, "Text", + Environment.NewLine + "outer text" }, + + { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text", + Environment.NewLine }, + { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text", + Environment.NewLine + "inner text" }, + + // Top-level indexing does not work end-to-end due to code generation issue #1345. + // TODO: Remove above comment when #1345 is fixed. + { models, typeof(Model), () => models[0].Text, "[0].Text", + Environment.NewLine }, + { models, typeof(Model), () => models[1].Text, "[1].Text", + Environment.NewLine + "outer text" }, + + { models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text", + Environment.NewLine }, + { models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text", + Environment.NewLine + "inner text" }, + }; + } + } + + [Theory] + [MemberData(nameof(TestDataSet))] + public async Task Process_GeneratesExpectedOutput( + object model, + Type containerType, + Func modelAccessor, + string propertyPath, + string expectedContent) + { + // Arrange + var expectedAttributes = new Dictionary + { + { "class", "form-control" }, + { "id", propertyPath }, + { "name", propertyPath }, + { "valid", "from validation attributes" }, + }; + var expectedTagName = "textarea"; + + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + + // Property name is either nameof(Model.Text) or nameof(NestedModel.Text). + var metadata = metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName: "Text"); + var modelExpression = new ModelExpression(propertyPath, metadata); + var tagHelper = new TextAreaTagHelper + { + For = modelExpression, + }; + + var tagHelperContext = new TagHelperContext(new Dictionary()); + var htmlAttributes = new Dictionary + { + { "class", "form-control" }, + }; + var output = new TagHelperOutput("original tag name", htmlAttributes, "original content") + { + SelfClosing = true, + }; + + var htmlGenerator = new TestableHtmlGenerator(metadataProvider) + { + ValidationAttributes = + { + { "valid", "from validation attributes" }, + } + }; + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + var activator = new DefaultTagHelperActivator(); + activator.Activate(tagHelper, viewContext); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Equal(expectedContent, output.Content); + Assert.False(output.SelfClosing); + Assert.Equal(expectedTagName, output.TagName); + } + + [Fact] + public async Task TagHelper_LeavesOutputUnchanged_IfForNotBound() + { + // Arrange + var expectedAttributes = new Dictionary + { + { "class", "form-control" }, + }; + var expectedContent = "original content"; + var expectedTagName = "original tag name"; + + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + var metadata = metadataProvider.GetMetadataForProperty( + modelAccessor: () => null, + containerType: typeof(Model), + propertyName: nameof(Model.Text)); + var modelExpression = new ModelExpression(nameof(Model.Text), metadata); + var tagHelper = new TextAreaTagHelper(); + + var tagHelperContext = new TagHelperContext(new Dictionary()); + var output = new TagHelperOutput(expectedTagName, expectedAttributes, expectedContent) + { + SelfClosing = true, + }; + + var htmlGenerator = new TestableHtmlGenerator(metadataProvider) + { + ValidationAttributes = + { + { "valid", "from validation attributes" }, + } + }; + Model model = null; + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + var activator = new DefaultTagHelperActivator(); + activator.Activate(tagHelper, viewContext); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Equal(expectedContent, output.Content); + Assert.True(output.SelfClosing); + Assert.Equal(expectedTagName, output.TagName); + } + + private class Model + { + public string Text { get; set; } + + public NestedModel NestedModel { get; set; } + } + + private class NestedModel + { + public string Text { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/project.json b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/project.json new file mode 100644 index 0000000000..549e7651fe --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/project.json @@ -0,0 +1,17 @@ +{ + "commands": { + "test": "Xunit.KRunner" + }, + "dependencies": { + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-*", + "Microsoft.AspNet.Testing": "1.0.0-*", + "Xunit.KRunner": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { + "dependencies": { + "Moq": "4.2.1312.1622" + } + } + } +} \ No newline at end of file