Files
workspace/code/one-designer/packages/report-expression/tests/aggregate.test.ts
T
2026-09-20 17:34:40 +08:00

181 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 聚合函数测试(对应设计文档 §16 阶段 3 的"跨页合计"能力基础)。
*
* 语义约定:
* - 空数组:sum → 0,avg/min/max → null,count → 0
* - "合计为 0" 与 "没有数据" 是两种不同语义,前者显示 0,后者显示空白
*/
import { describe, expect, it } from "vitest"
import { evaluateValue } from "../src/evaluator.js"
import type { EvalScope } from "../src/scope.js"
const scope: EvalScope = {
row: {
amounts: [10, 20, 30],
prices: [1.5, 2.5],
mixed: [1, "abc", 3, null],
empty: [],
names: ["甲", "乙", "丙"],
},
params: {},
}
describe("sum", () => {
it("数组求和", () => {
expect(evaluateValue("sum(row.amounts)", scope)).toBe(60)
})
it("空数组返回 0(合计为零应显示 0)", () => {
expect(evaluateValue("sum(row.empty)", scope)).toBe(0)
})
it("忽略非数字元素", () => {
expect(evaluateValue("sum(row.mixed)", scope)).toBe(4)
})
it("标量视为单元素", () => {
expect(evaluateValue("sum(5)", scope)).toBe(5)
})
it("null 返回 0", () => {
expect(evaluateValue("sum(row.nothing)", scope)).toBe(0)
})
it("小数求和", () => {
expect(evaluateValue("sum(row.prices)", scope)).toBe(4)
})
it("可与其他运算组合", () => {
expect(evaluateValue("sum(row.amounts) * 2", scope)).toBe(120)
})
})
describe("avg", () => {
it("平均值为数字", () => {
expect(evaluateValue("avg(row.amounts)", scope)).toBe(20)
})
it("空数组返回 null(无数据应显示空白)", () => {
expect(evaluateValue("avg(row.empty)", scope)).toBeNull()
})
it("忽略非数字元素后再平均", () => {
// 1 和 3 是有效数字,'abc' 与 null 被忽略
expect(evaluateValue("avg(row.mixed)", scope)).toBe(2)
})
it("小数的平均值", () => {
expect(evaluateValue("avg(row.prices)", scope)).toBe(2)
})
})
describe("count", () => {
it("统计元素个数", () => {
expect(evaluateValue("count(row.amounts)", scope)).toBe(3)
})
it("空数组返回 0", () => {
expect(evaluateValue("count(row.empty)", scope)).toBe(0)
})
it("统计非空元素个数", () => {
// mixed = [1, 'abc', 3, null],null 不计入
expect(evaluateValue("count(row.mixed)", scope)).toBe(3)
})
it("无参数返回 0", () => {
expect(evaluateValue("count()", scope)).toBe(0)
})
it("标量计为 1", () => {
expect(evaluateValue("count(5)", scope)).toBe(1)
})
it("null 计为 0", () => {
expect(evaluateValue("count(row.nothing)", scope)).toBe(0)
})
})
describe("min / max 双形态", () => {
it("多参数形态(标量比较)", () => {
expect(evaluateValue("min(3, 1, 2)", scope)).toBe(1)
expect(evaluateValue("max(3, 1, 2)", scope)).toBe(3)
})
it("单参数数组形态(聚合)", () => {
expect(evaluateValue("min(row.amounts)", scope)).toBe(10)
expect(evaluateValue("max(row.amounts)", scope)).toBe(30)
})
it("空数组返回 null", () => {
expect(evaluateValue("min(row.empty)", scope)).toBeNull()
expect(evaluateValue("max(row.empty)", scope)).toBeNull()
})
it("忽略非数字", () => {
expect(evaluateValue("max(row.mixed)", scope)).toBe(3)
})
it("单元素数组与标量结果一致", () => {
expect(evaluateValue("min(5)", scope)).toBe(5)
expect(evaluateValue("min(row.prices)", scope)).toBe(1.5)
})
})
describe("聚合与分组作用域协作", () => {
it("可通过 group 作用域聚合", () => {
const groupScope: EvalScope = {
row: { name: "甲" },
group: {
key: "华北",
amounts: [100, 200, 300],
count: 3,
},
params: {},
}
expect(evaluateValue("sum(group.amounts)", groupScope)).toBe(600)
expect(evaluateValue("avg(group.amounts)", groupScope)).toBe(200)
expect(evaluateValue("group.count", groupScope)).toBe(3)
})
it("可通过 dataset 作用域做全表合计", () => {
const datasetScope: EvalScope = {
row: {},
dataset: { amounts: [1, 2, 3, 4], count: 4 },
params: {},
}
expect(evaluateValue("sum(dataset.amounts)", datasetScope)).toBe(10)
})
it("聚合结果可被格式化", () => {
const groupScope: EvalScope = {
row: {},
group: { amounts: [1234.5, 2.25] },
params: {},
}
expect(evaluateValue("format(sum(group.amounts), '#,##0.00')", groupScope)).toBe("1,236.75")
})
it("空组不产生 NaN", () => {
const groupScope: EvalScope = { row: {}, group: { amounts: [] }, params: {} }
const sum = evaluateValue("sum(group.amounts)", groupScope)
expect(sum).toBe(0)
expect(Number.isNaN(sum as number)).toBe(false)
})
})
describe("聚合函数仍在安全白名单内", () => {
it("sum/avg/count 可调用", () => {
expect(evaluateValue("sum(1)", scope)).toBe(1)
expect(evaluateValue("avg(1)", scope)).toBe(1)
expect(evaluateValue("count(1)", scope)).toBe(1)
})
it("未登记的聚合名仍被拒绝", () => {
// median 等未实现,应在解析期就被拒绝
const scope2: EvalScope = { row: {} }
const result = evaluateValue("median(row.x)", scope2)
expect(result).toBeNull()
})
})