!2746 test(#I55MCV): add unit test for footer aggregate function

* Merge branch 'main' into test-table
* fix: 修复 Footer 移动端无法正常显示 bug
* test: 增加移动端检测单元测试
* test: 增加数据类型单元测试
* test: 增加聚合函数单元测试
* test: 增加对齐单元测试
* test: 增加 Count 单元测试
* test: 增加 Text 单元测试
* refactor: 重构 FooterCell 组件
* test: 更新移动端统计合并功能
* test: 增加 FooterCell 单元测试
* refactor: 更新 Footer 内部变量与语义一致
This commit is contained in:
Argo 2022-05-03 09:43:43 +00:00
parent 3f5b7e6e6b
commit de5aa26b37
3 changed files with 394 additions and 65 deletions

View File

@ -2,12 +2,12 @@
@inherits BootstrapComponentBase
@if (IsMobileMode)
{
<div @attributes="@AdditionalAttributes" class="@ClassString">@GetText()</div>
}
else
{
<td @attributes="@AdditionalAttributes">
<div class="@ClassString">@GetText()</div>
</td>
}
else
{
<div @attributes="@AdditionalAttributes" class="@ClassString">@GetText()</div>
}

View File

@ -39,7 +39,13 @@ public partial class TableFooterCell
public AggregateType Aggregate { get; set; }
/// <summary>
///
/// 获得/设置 自定义统计列回调方法
/// </summary>
[Parameter]
public Func<object?, string?, string>? CustomerAggregateCallback { get; set; }
/// <summary>
/// 获得/设置 统计列名称 默认为 null 不参与统计仅作为显示单元格
/// </summary>
[Parameter]
public string? Field { get; set; }
@ -72,75 +78,105 @@ public partial class TableFooterCell
// 数据源泛型 TModel 类型
var modelType = type.GenericTypeArguments[0];
var mi = GetType().GetMethod(nameof(CreateCountMethod), BindingFlags.NonPublic | BindingFlags.Static)
?.MakeGenericMethod(modelType);
var mi = GetType().GetMethod(nameof(CreateCountMethod), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(modelType);
if (mi != null)
{
v = mi.Invoke(null, new object[] { DataSource })?.ToString();
var obj = mi.Invoke(null, new object[] { DataSource });
if (obj != null)
{
v = obj.ToString();
}
}
}
return v;
}
private string GetAggegateValue()
private string? GetAggegateValue()
{
var v = "";
if (!string.IsNullOrEmpty(Field) && DataSource != null)
return Aggregate == AggregateType.Customer ? AggregateCustomerValue() : AggregateNumberValue();
string? AggregateCustomerValue()
{
// 绑定数据源类型
var type = DataSource.GetType();
// 数据源泛型 TModel 类型
var modelType = type.GenericTypeArguments[0];
// 通过 Field 获取到 TModel 属性
var propertyInfo = modelType.GetProperty(Field);
if (propertyInfo != null)
string? v = null;
if (CustomerAggregateCallback != null)
{
// @context.Sum(i => i.Count)
// Count 属性类型
var propertyType = propertyInfo.PropertyType;
v = CustomerAggregateCallback(DataSource, Field);
}
return v;
}
// 构建 Aggegate
// @context.Sum(i => i.Count)
var aggegateMethod = Aggregate switch
string? AggregateNumberValue()
{
string? v = null;
if (!string.IsNullOrEmpty(Field) && DataSource != null)
{
// 绑定数据源类型
var type = DataSource.GetType();
// 数据源泛型 TModel 类型
var modelType = type.GenericTypeArguments[0];
// 通过 Field 获取到 TModel 属性
var propertyInfo = modelType.GetProperty(Field);
if (propertyInfo != null)
{
AggregateType.Average => propertyType.Name switch
// @context.Sum(i => i.Count)
// Count 属性类型
var propertyType = propertyInfo.PropertyType;
// 构建 Aggegate
// @context.Sum(i => i.Count)
var aggegateMethod = Aggregate switch
{
nameof(Int32) or nameof(Int64) => GetType()
.GetMethod(nameof(CreateAggregateLambda), BindingFlags.NonPublic | BindingFlags.Static)
?.MakeGenericMethod(typeof(Double)),
nameof(Decimal) or nameof(Double) or nameof(Single) => GetType()
.GetMethod(nameof(CreateAggregateLambda), BindingFlags.NonPublic | BindingFlags.Static)
?.MakeGenericMethod(propertyType),
_ => null
},
_ => GetType().GetMethod(nameof(CreateAggregateLambda), BindingFlags.NonPublic | BindingFlags.Static)
?.MakeGenericMethod(propertyType)
};
if (aggegateMethod != null)
{
var invoker = aggegateMethod.Invoke(null, new object[] { Aggregate, type, modelType, propertyType });
if (invoker != null)
{
// 构建 Selector
var methodInfo = GetType().GetMethod(nameof(CreateSelector), BindingFlags.NonPublic | BindingFlags.Static)
?.MakeGenericMethod(modelType, propertyType);
if (methodInfo != null)
AggregateType.Average => propertyType.Name switch
{
var selector = methodInfo.Invoke(null, new object[] { Field });
if (selector != null)
nameof(Int32) or nameof(Int64) or nameof(Double) => GetType()
.GetMethod(nameof(CreateAggregateLambda), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(typeof(Double)),
_ => GetType()
.GetMethod(nameof(CreateAggregateLambda), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(propertyType),
},
_ => GetType().GetMethod(nameof(CreateAggregateLambda), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(propertyType)
};
if (aggegateMethod != null)
{
v = AggregateMethodInvoker(aggegateMethod, type, modelType, propertyType);
}
}
}
return v;
}
string? AggregateMethodInvoker(MethodInfo aggegateMethod, Type type, Type modelType, Type propertyType)
{
string? v = null;
var invoker = aggegateMethod.Invoke(null, new object[] { Aggregate, type, modelType, propertyType });
if (invoker != null)
{
// 构建 Selector
var methodInfo = GetType().GetMethod(nameof(CreateSelector), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(modelType, propertyType);
if (methodInfo != null)
{
var selector = methodInfo.Invoke(null, new object[] { Field });
if (selector != null)
{
// 执行委托
if (invoker is Delegate d)
{
var val = d.DynamicInvoke(DataSource, selector);
if (val != null)
{
// 执行委托
var val = (invoker as Delegate)?.DynamicInvoke(DataSource, selector);
v = val?.ToString() ?? "";
v = val.ToString();
}
}
}
}
}
return v;
}
return v;
}
/// <summary>
@ -155,13 +191,7 @@ public partial class TableFooterCell
var type = typeof(TModel);
var p1 = Expression.Parameter(type);
var propertyInfo = type.GetProperty(field);
if (propertyInfo == null)
{
throw new InvalidOperationException();
}
var fieldExpression = Expression.Property(p1, propertyInfo);
var fieldExpression = Expression.Property(p1, propertyInfo!);
return Expression.Lambda<Func<TModel, TValue>>(fieldExpression, p1).Compile();
}
@ -196,8 +226,18 @@ public partial class TableFooterCell
.FirstOrDefault(m => m.Name == aggregate.ToString() && m.IsGenericMethod
&& m.ReturnType == typeof(Double) && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.GenericTypeArguments[1] == typeof(Int64)),
nameof(Decimal) or nameof(Double) or nameof(Single) => typeof(Enumerable).GetMethods()
.FirstOrDefault(m => m.Name == aggregate.ToString() && m.IsGenericMethod && m.ReturnType == propertyType),
nameof(Double) => typeof(Enumerable).GetMethods()
.FirstOrDefault(m => m.Name == aggregate.ToString() && m.IsGenericMethod
&& m.ReturnType == typeof(Double) && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.GenericTypeArguments[1] == typeof(Double)),
nameof(Decimal) => typeof(Enumerable).GetMethods()
.FirstOrDefault(m => m.Name == aggregate.ToString() && m.IsGenericMethod
&& m.ReturnType == typeof(Decimal) && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.GenericTypeArguments[1] == typeof(Decimal)),
nameof(Single) => typeof(Enumerable).GetMethods()
.FirstOrDefault(m => m.Name == aggregate.ToString() && m.IsGenericMethod
&& m.ReturnType == typeof(Single) && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.GenericTypeArguments[1] == typeof(Single)),
_ => null
},
_ => typeof(Enumerable).GetMethods()
@ -219,13 +259,16 @@ public partial class TableFooterCell
// 数据源泛型 TModel 类型
var modelType = type.GenericTypeArguments[0];
var mi = typeof(TableFooterCell).GetMethod(nameof(CreateCountMethod), BindingFlags.NonPublic | BindingFlags.Static)
?.MakeGenericMethod(modelType);
var mi = typeof(TableFooterCell).GetMethod(nameof(CreateCountMethod), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(modelType);
if (mi != null)
{
var v = mi.Invoke(null, new object[] { source })?.ToString();
_ = int.TryParse(v, out ret);
var obj = mi.Invoke(null, new object[] { source });
if (obj != null)
{
var v = obj.ToString();
_ = int.TryParse(v, out ret);
}
}
}
return ret;

View File

@ -0,0 +1,286 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using BootstrapBlazor.Shared;
namespace UnitTest.Components;
public class TableFooterCellTest : TestBase
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void IsMobileMode_Ok(bool mobile)
{
var ds = new List<Foo>()
{
new Foo() { Count = 1 },
new Foo() { Count = 2 },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<bool>("IsMobileMode", mobile);
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(Foo.Count));
});
cut.Contains("table-cell");
cut.Contains("3");
if (!mobile)
{
cut.Contains("td");
}
else
{
cut.DoesNotContain("td");
}
}
[Fact]
public void Text_Ok()
{
var ds = new List<Foo>()
{
new Foo() { Count = 1 },
new Foo() { Count = 2 },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(Foo.Count));
pb.Add(a => a.Text, "test-Text");
});
cut.Contains("test-Text");
}
[Fact]
public void Count_Empty()
{
var ds = new List<Foo>();
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(Foo.Count));
pb.Add(a => a.Aggregate, AggregateType.Count);
});
cut.Contains("0");
}
[Fact]
public void Count_Ok()
{
var ds = new List<Foo>()
{
new Foo() { Count = 1 },
new Foo() { Count = 2 },
new Foo() { Count = 3 },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(Foo.Count));
pb.Add(a => a.Aggregate, AggregateType.Count);
});
cut.Contains("3");
}
[Fact]
public void Align_Ok()
{
var ds = new List<Foo>()
{
new Foo() { Count = 1 },
new Foo() { Count = 2 },
new Foo() { Count = 3 },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(Foo.Count));
pb.Add(a => a.Align, Alignment.Right);
});
cut.Contains("justify-content-end");
}
[Theory]
[InlineData(AggregateType.Sum, "6")]
[InlineData(AggregateType.Average, "2")]
[InlineData(AggregateType.Count, "3")]
[InlineData(AggregateType.Max, "3")]
[InlineData(AggregateType.Min, "1")]
public void Aggegate_Ok(AggregateType aggregate, string expected)
{
var ds = new List<Foo>()
{
new Foo() { Count = 1 },
new Foo() { Count = 2 },
new Foo() { Count = 3 },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(Foo.Count));
pb.Add(a => a.Aggregate, aggregate);
});
cut.Contains(expected);
}
[Fact]
public void Aggegate_Customer()
{
var ds = new List<MockFoo>()
{
new() { Name = "1" },
new() { Name = "2" },
new() { Name = "3" },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(MockFoo.Name));
pb.Add(a => a.Aggregate, AggregateType.Customer);
pb.Add(a => a.CustomerAggregateCallback, (obj, field) =>
{
return "test-customer-aggregate";
});
});
cut.Contains("test-customer-aggregate");
}
[Fact]
public void Aggegate_Empty()
{
var ds = new List<Foo>()
{
new Foo() { Count = 1 },
new Foo() { Count = 2 },
new Foo() { Count = 3 },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Aggregate, AggregateType.Average);
});
}
[Fact]
public void Double_Ok()
{
var ds = new List<MockFoo>()
{
new() { Count = 1.0 },
new() { Count = 2.0 },
new() { Count = 3.0 },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(MockFoo.Count));
pb.Add(a => a.Aggregate, AggregateType.Average);
});
cut.Contains("2");
}
[Fact]
public void Single_Ok()
{
var ds = new List<MockFoo>()
{
new() { FloatCount = 1.0f },
new() { FloatCount = 2.0f },
new() { FloatCount = 3.0f },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(MockFoo.FloatCount));
pb.Add(a => a.Aggregate, AggregateType.Average);
});
cut.Contains("2");
}
[Fact]
public void Long_Ok()
{
var ds = new List<MockFoo>()
{
new() { LongCount = 1 },
new() { LongCount = 2 },
new() { LongCount = 3 },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(MockFoo.LongCount));
pb.Add(a => a.Aggregate, AggregateType.Average);
});
cut.Contains("2");
}
[Fact]
public void Decimal_Ok()
{
var ds = new List<MockFoo>()
{
new() { DecimalCount = 1.0m },
new() { DecimalCount = 2.0m },
new() { DecimalCount = 3.0m },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(MockFoo.DecimalCount));
pb.Add(a => a.Aggregate, AggregateType.Average);
});
cut.Contains("2");
}
[Fact]
public void Name_Exception()
{
var ds = new List<MockFoo>()
{
new() { Name = "1" },
new() { Name = "2" },
new() { Name = "3" },
};
var cut = Context.RenderComponent<TableFooterCell>(pb =>
{
pb.AddCascadingValue<object>("TableFooterContext", ds);
pb.Add(a => a.Field, nameof(MockFoo.Name));
pb.Add(a => a.Aggregate, AggregateType.Average);
});
}
private class MockFoo
{
public double Count { get; set; }
public long LongCount { get; set; }
public float FloatCount { get; set; }
public decimal DecimalCount { get; set; }
public string? Name { get; set; }
}
}