mirror of
https://gitee.com/LongbowEnterprise/BootstrapBlazor.git
synced 2024-12-02 03:59:14 +08:00
!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:
parent
3f5b7e6e6b
commit
de5aa26b37
@ -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>
|
||||
}
|
||||
|
@ -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;
|
||||
|
286
test/UnitTest/Components/TableFooterCellTest.cs
Normal file
286
test/UnitTest/Components/TableFooterCellTest.cs
Normal 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; }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user