Add `SelectTagHelper` tests
This commit is contained in:
parent
2fd51c82f9
commit
d31e107619
|
|
@ -0,0 +1,520 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
public class SelectTagHelperTest
|
||||
{
|
||||
// Model (List<Model> or Model instance), container type (Model or NestModel), model accessor,
|
||||
// property path / id, expected content.
|
||||
public static TheoryData<object, Type, Func<object>, NameAndId, string> GeneratesExpectedDataSet
|
||||
{
|
||||
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<Model>
|
||||
{
|
||||
modelWithNull,
|
||||
modelWithText,
|
||||
};
|
||||
var noneSelected = "<option></option>" + Environment.NewLine +
|
||||
"<option>outer text</option>" + Environment.NewLine +
|
||||
"<option>inner text</option>" + Environment.NewLine +
|
||||
"<option>other text</option>" + Environment.NewLine;
|
||||
var innerSelected = "<option></option>" + Environment.NewLine +
|
||||
"<option>outer text</option>" + Environment.NewLine +
|
||||
"<option selected=\"selected\">inner text</option>" + Environment.NewLine +
|
||||
"<option>other text</option>" + Environment.NewLine;
|
||||
var outerSelected = "<option></option>" + Environment.NewLine +
|
||||
"<option selected=\"selected\">outer text</option>" + Environment.NewLine +
|
||||
"<option>inner text</option>" + Environment.NewLine +
|
||||
"<option>other text</option>" + Environment.NewLine;
|
||||
|
||||
return new TheoryData<object, Type, Func<object>, NameAndId, string>
|
||||
{
|
||||
{ null, typeof(Model), () => null, new NameAndId("Text", "Text"), noneSelected },
|
||||
|
||||
{ modelWithNull, typeof(Model), () => modelWithNull.Text,
|
||||
new NameAndId("Text", "Text"), noneSelected },
|
||||
{ modelWithText, typeof(Model), () => modelWithText.Text,
|
||||
new NameAndId("Text", "Text"), outerSelected },
|
||||
|
||||
{ modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text,
|
||||
new NameAndId("NestedModel.Text", "NestedModel_Text"), noneSelected },
|
||||
{ modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text,
|
||||
new NameAndId("NestedModel.Text", "NestedModel_Text"), innerSelected },
|
||||
|
||||
// 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,
|
||||
new NameAndId("[0].Text", "z0__Text"), noneSelected },
|
||||
{ models, typeof(NestedModel), () => models[0].NestedModel.Text,
|
||||
new NameAndId("[0].NestedModel.Text", "z0__NestedModel_Text"), noneSelected },
|
||||
|
||||
// Skip last two test cases because DefaultHtmlGenerator evaluates expression name against
|
||||
// ViewData, not using ModelMetadata.Model. ViewData.Eval() handles simple property paths and some
|
||||
// dictionary lookups, but not indexing into an array or list. Will file a follow-up bug on this...
|
||||
////{ models, typeof(Model), () => models[1].Text,
|
||||
//// new NameAndId("[1].Text", "z1__Text"), outerSelected },
|
||||
////{ models, typeof(NestedModel), () => models[1].NestedModel.Text,
|
||||
//// new NameAndId("[1].NestedModel.Text", "z1__NestedModel_Text"), innerSelected },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Items value, Multiple value, expected items value (passed to generator), expected allowMultiple.
|
||||
// Provides cross product of Items and Multiple values. These attribute values should not interact.
|
||||
public static TheoryData<IEnumerable<SelectListItem>, string, IEnumerable<SelectListItem>, bool>
|
||||
ItemsAndMultipleDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
var arrayItems = new[] { new SelectListItem() };
|
||||
var listItems = new List<SelectListItem>();
|
||||
var multiItems = new MultiSelectList(Enumerable.Range(0, 5));
|
||||
var selectItems = new SelectList(Enumerable.Range(0, 5));
|
||||
var itemsData = new[]
|
||||
{
|
||||
new[] { (IEnumerable<SelectListItem>)null, Enumerable.Empty<SelectListItem>() },
|
||||
new[] { arrayItems, arrayItems },
|
||||
new[] { listItems, listItems },
|
||||
new[] { multiItems, multiItems },
|
||||
new[] { selectItems, selectItems },
|
||||
};
|
||||
var mutlipleData = new[]
|
||||
{
|
||||
new Tuple<string, bool>(null, false), // allowMultiple determined by string datatype.
|
||||
new Tuple<string, bool>("", false), // allowMultiple determined by string datatype.
|
||||
new Tuple<string, bool>("true", true),
|
||||
new Tuple<string, bool>("false", false),
|
||||
new Tuple<string, bool>("multiple", true),
|
||||
new Tuple<string, bool>("Multiple", true),
|
||||
new Tuple<string, bool>("MULTIPLE", true),
|
||||
};
|
||||
|
||||
var theoryData =
|
||||
new TheoryData<IEnumerable<SelectListItem>, string, IEnumerable<SelectListItem>, bool>();
|
||||
foreach (var items in itemsData)
|
||||
{
|
||||
foreach (var multiples in mutlipleData)
|
||||
{
|
||||
theoryData.Add(items[0], multiples.Item1, items[1], multiples.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
return theoryData;
|
||||
}
|
||||
}
|
||||
|
||||
// Model type, model, allowMultiple expected value
|
||||
public static TheoryData<Type, object, bool> RealModelTypeDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<Type, object, bool>
|
||||
{
|
||||
{ typeof(object), string.Empty, false },
|
||||
{ typeof(string), null, false },
|
||||
{ typeof(int?), null, false },
|
||||
{ typeof(int), 23, false },
|
||||
{ typeof(IEnumerable), null, true },
|
||||
{ typeof(IEnumerable<string>), null, true },
|
||||
{ typeof(List<int>), null, true },
|
||||
{ typeof(object), new[] { "", "", "" }, true },
|
||||
{ typeof(object), new List<string>(), true },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GeneratesExpectedDataSet))]
|
||||
public async Task ProcessAsync_GeneratesExpectedOutput(
|
||||
object model,
|
||||
Type containerType,
|
||||
Func<object> modelAccessor,
|
||||
NameAndId nameAndId,
|
||||
string ignored)
|
||||
{
|
||||
// Arrange
|
||||
var originalAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "class", "form-control" },
|
||||
};
|
||||
var originalContent = "original content";
|
||||
var originalTagName = "not-select";
|
||||
|
||||
var expectedAttributes = new Dictionary<string, string>(originalAttributes)
|
||||
{
|
||||
{ "id", nameAndId.Id },
|
||||
{ "name", nameAndId.Name },
|
||||
{ "valid", "from validation attributes" },
|
||||
};
|
||||
var expectedContent = originalContent;
|
||||
var expectedTagName = "select";
|
||||
|
||||
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(nameAndId.Name, metadata);
|
||||
|
||||
var tagHelperContext = new TagHelperContext(new Dictionary<string, object>());
|
||||
var output = new TagHelperOutput(originalTagName, originalAttributes, expectedContent)
|
||||
{
|
||||
SelfClosing = true,
|
||||
};
|
||||
|
||||
var htmlGenerator = new TestableHtmlGenerator(metadataProvider)
|
||||
{
|
||||
ValidationAttributes =
|
||||
{
|
||||
{ "valid", "from validation attributes" },
|
||||
}
|
||||
};
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
|
||||
var tagHelper = new SelectTagHelper
|
||||
{
|
||||
For = modelExpression,
|
||||
Generator = htmlGenerator,
|
||||
ViewContext = 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);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GeneratesExpectedDataSet))]
|
||||
public async Task ProcessAsync_GeneratesExpectedOutput_WithItems(
|
||||
object model,
|
||||
Type containerType,
|
||||
Func<object> modelAccessor,
|
||||
NameAndId nameAndId,
|
||||
string expectedOptions)
|
||||
{
|
||||
// Arrange
|
||||
var originalAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "class", "form-control" },
|
||||
};
|
||||
var originalContent = "original content";
|
||||
var originalTagName = "not-select";
|
||||
|
||||
var expectedAttributes = new Dictionary<string, string>(originalAttributes)
|
||||
{
|
||||
{ "id", nameAndId.Id },
|
||||
{ "name", nameAndId.Name },
|
||||
{ "valid", "from validation attributes" },
|
||||
};
|
||||
var expectedContent = originalContent + expectedOptions;
|
||||
var expectedTagName = "select";
|
||||
|
||||
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(nameAndId.Name, metadata);
|
||||
|
||||
var tagHelperContext = new TagHelperContext(new Dictionary<string, object>());
|
||||
var output = new TagHelperOutput(originalTagName, originalAttributes, originalContent)
|
||||
{
|
||||
SelfClosing = true,
|
||||
};
|
||||
|
||||
var items = new SelectList(new[] { "", "outer text", "inner text", "other text" });
|
||||
var htmlGenerator = new TestableHtmlGenerator(metadataProvider)
|
||||
{
|
||||
ValidationAttributes =
|
||||
{
|
||||
{ "valid", "from validation attributes" },
|
||||
}
|
||||
};
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
|
||||
var tagHelper = new SelectTagHelper
|
||||
{
|
||||
For = modelExpression,
|
||||
Generator = htmlGenerator,
|
||||
Items = items,
|
||||
ViewContext = 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);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ItemsAndMultipleDataSet))]
|
||||
public async Task ProcessAsync_CallsGeneratorWithExpectedValues_ItemsAndMultiple(
|
||||
IEnumerable<SelectListItem> inputItems,
|
||||
string multiple,
|
||||
IEnumerable<SelectListItem> expectedItems,
|
||||
bool expectedAllowMultiple)
|
||||
{
|
||||
// Arrange
|
||||
var contextAttributes = new Dictionary<string, object>
|
||||
{
|
||||
// Attribute will be restored if value matches "multiple".
|
||||
{ "multiple", multiple },
|
||||
};
|
||||
var originalAttributes = new Dictionary<string, string>();
|
||||
var content = "original content";
|
||||
var propertyName = "Property1";
|
||||
var tagName = "not-select";
|
||||
|
||||
var tagHelperContext = new TagHelperContext(contextAttributes);
|
||||
var output = new TagHelperOutput(tagName, originalAttributes, content);
|
||||
|
||||
// TODO: In real (model => model) scenario, ModelExpression should have name "" and
|
||||
// TemplateInfo.HtmlFieldPrefix should be "Property1" but empty ModelExpression name is not currently
|
||||
// supported, see #1408.
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
string model = null;
|
||||
var metadata = metadataProvider.GetMetadataForType(() => model, typeof(string));
|
||||
var modelExpression = new ModelExpression(propertyName, metadata);
|
||||
|
||||
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
|
||||
htmlGenerator
|
||||
.Setup(real => real.GenerateSelect(
|
||||
viewContext,
|
||||
metadata,
|
||||
null, // optionLabel
|
||||
propertyName, // name
|
||||
expectedItems,
|
||||
expectedAllowMultiple,
|
||||
null)) // htmlAttributes
|
||||
.Returns((TagBuilder)null)
|
||||
.Verifiable();
|
||||
|
||||
var tagHelper = new SelectTagHelper
|
||||
{
|
||||
For = modelExpression,
|
||||
Items = inputItems,
|
||||
Generator = htmlGenerator.Object,
|
||||
Multiple = multiple,
|
||||
ViewContext = viewContext,
|
||||
};
|
||||
|
||||
// Act
|
||||
await tagHelper.ProcessAsync(tagHelperContext, output);
|
||||
|
||||
// Assert
|
||||
htmlGenerator.Verify();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RealModelTypeDataSet))]
|
||||
public async Task TagHelper_CallsGeneratorWithExpectedValues_RealModelType(
|
||||
Type modelType,
|
||||
object model,
|
||||
bool allowMultiple)
|
||||
{
|
||||
// Arrange
|
||||
var contextAttributes = new Dictionary<string, object>();
|
||||
var originalAttributes = new Dictionary<string, string>();
|
||||
var content = "original content";
|
||||
var propertyName = "Property1";
|
||||
var tagName = "not-select";
|
||||
|
||||
var tagHelperContext = new TagHelperContext(contextAttributes);
|
||||
var output = new TagHelperOutput(tagName, originalAttributes, content);
|
||||
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var metadata = metadataProvider.GetMetadataForType(() => model, modelType);
|
||||
var modelExpression = new ModelExpression(propertyName, metadata);
|
||||
|
||||
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
|
||||
htmlGenerator
|
||||
.Setup(real => real.GenerateSelect(
|
||||
viewContext,
|
||||
metadata,
|
||||
null, // optionLabel
|
||||
propertyName, // name
|
||||
It.IsAny<IEnumerable<SelectListItem>>(),
|
||||
allowMultiple,
|
||||
null)) // htmlAttributes
|
||||
.Returns((TagBuilder)null)
|
||||
.Verifiable();
|
||||
|
||||
var tagHelper = new SelectTagHelper
|
||||
{
|
||||
For = modelExpression,
|
||||
Generator = htmlGenerator.Object,
|
||||
ViewContext = viewContext,
|
||||
};
|
||||
|
||||
// Act
|
||||
await tagHelper.ProcessAsync(tagHelperContext, output);
|
||||
|
||||
// Assert
|
||||
htmlGenerator.Verify();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("multiple")]
|
||||
[InlineData("mUlTiPlE")]
|
||||
[InlineData("MULTIPLE")]
|
||||
public async Task ProcessAsync_RestoresMultiple_IfForNotBound(string attributeName)
|
||||
{
|
||||
// Arrange
|
||||
var contextAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ attributeName, "I'm more than one" },
|
||||
};
|
||||
var originalAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "class", "form-control" },
|
||||
{ "size", "2" },
|
||||
};
|
||||
var expectedAttributes = new Dictionary<string, string>(originalAttributes);
|
||||
expectedAttributes[attributeName] = (string)contextAttributes[attributeName];
|
||||
var expectedContent = "original content";
|
||||
var expectedTagName = "not-select";
|
||||
|
||||
var tagHelperContext = new TagHelperContext(contextAttributes);
|
||||
var output = new TagHelperOutput(expectedTagName, originalAttributes, expectedContent)
|
||||
{
|
||||
SelfClosing = true,
|
||||
};
|
||||
|
||||
var tagHelper = new SelectTagHelper
|
||||
{
|
||||
Multiple = "I'm more than one",
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_Throws_IfForNotBoundButItemsIs()
|
||||
{
|
||||
// Arrange
|
||||
var contextAttributes = new Dictionary<string, object>();
|
||||
var originalAttributes = new Dictionary<string, string>();
|
||||
var content = "original content";
|
||||
var tagName = "not-select";
|
||||
var expectedMessage = "Cannot determine body for <select>. 'items' must be null if 'for' is null.";
|
||||
|
||||
var tagHelperContext = new TagHelperContext(contextAttributes);
|
||||
var output = new TagHelperOutput(tagName, originalAttributes, content);
|
||||
var tagHelper = new SelectTagHelper
|
||||
{
|
||||
Items = Enumerable.Empty<SelectListItem>(),
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => tagHelper.ProcessAsync(tagHelperContext, output));
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Invalid")]
|
||||
[InlineData("0")]
|
||||
[InlineData("1")]
|
||||
[InlineData("__true")]
|
||||
[InlineData("false__")]
|
||||
[InlineData("__Multiple")]
|
||||
[InlineData("Multiple__")]
|
||||
public async Task ProcessAsync_Throws_IfMultipleInvalid(string multiple)
|
||||
{
|
||||
// Arrange
|
||||
var contextAttributes = new Dictionary<string, object>();
|
||||
var originalAttributes = new Dictionary<string, string>();
|
||||
var content = "original content";
|
||||
var tagName = "not-select";
|
||||
var expectedMessage = "Cannot parse 'multiple' value '" + multiple +
|
||||
"' for <select>. Acceptable values are 'false', 'true' and 'multiple'.";
|
||||
|
||||
var tagHelperContext = new TagHelperContext(contextAttributes);
|
||||
var output = new TagHelperOutput(tagName, originalAttributes, content);
|
||||
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
string model = null;
|
||||
var metadata = metadataProvider.GetMetadataForType(() => model, typeof(string));
|
||||
var modelExpression = new ModelExpression("Property1", metadata);
|
||||
|
||||
var tagHelper = new SelectTagHelper
|
||||
{
|
||||
For = modelExpression,
|
||||
Multiple = multiple,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => tagHelper.ProcessAsync(tagHelperContext, output));
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
|
||||
public class NameAndId
|
||||
{
|
||||
public NameAndId(string name, string id)
|
||||
{
|
||||
Name = name;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public string Id { get; private set; }
|
||||
}
|
||||
|
||||
private class Model
|
||||
{
|
||||
public string Text { get; set; }
|
||||
|
||||
public NestedModel NestedModel { get; set; }
|
||||
}
|
||||
|
||||
private class NestedModel
|
||||
{
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue