Files
workspace/code/fms/fms-vue/tests/unit/module-list-utils.spec.js
T
2026-09-27 21:59:13 +08:00

149 lines
6.1 KiB
JavaScript
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.
import { describe, expect, it } from 'vitest'
import {
buildConditionSql,
defaultQueryValue,
sqlLiteral,
} from '@/components/fms-module-common/queryUtils'
import {
formatDateValue,
parseOptions,
toUtcIsoDateTime,
} from '@/components/fms-module-common/fieldUtils'
import { setMessages, translate } from '@/stores/i18n'
const pad2 = (number) => String(number).padStart(2, '0')
/**
* 本地墙钟串 → UTC 墙钟串(与实现同口径,断言不依赖运行环境时区)。
* 库中 datetime 存 UTC,查询条件里的本地时刻会先折算(见后端 TimeUtils / queryUtils.utcWallTextOf)。
*/
function toUtcWall(localText) {
const date = new Date(String(localText).replace(' ', 'T'))
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`
}
/** buildConditionSql 需要按字段名查到字段定义,b_type 决定字面量形式(数字裸写 / 文本 N'...') */
function fieldMapOf(...fields) {
return new Map(fields.map((field) => [field.b_field, field]))
}
function item(over = {}) {
return {
field: { b_field: 'amount', b_type: 'money', b_options: '' },
config: { b_operator: 'eq', b_component: 'number' },
...over,
}
}
describe('fms-module-list query utilities', () => {
it('uses a standalone translation catalog and fallback text', () => {
setMessages({ 'common.search': '查询' }, 'zh-CN')
expect(translate('common.search', '搜索')).toBe('查询')
expect(translate('common.reset', '重置')).toBe('重置')
setMessages({})
})
it('builds numeric and escaped text conditions', () => {
expect(
buildConditionSql(
{ field: 'amount', operator: 'eq', value: 12.5 },
fieldMapOf({ b_field: 'amount', b_type: 'money' }),
),
).toBe('[amount] = 12.5')
expect(
buildConditionSql(
{ field: 'name', operator: 'like', value: "O'Reilly" },
fieldMapOf({ b_field: 'name', b_type: 'input' }),
),
).toBe("[name] LIKE N'%O''Reilly%'")
})
it('builds range and IN conditions while ignoring empty values', () => {
expect(
buildConditionSql(
{ field: 'created', operator: 'between', value: ['2026-01-01', '2026-01-31'] },
fieldMapOf({ b_field: 'created', b_type: 'date' }),
),
).toBe("[created] BETWEEN N'2026-01-01' AND N'2026-01-31'")
expect(
buildConditionSql(
{ field: 'status', operator: 'in', value: ['a', '', 'b'] },
fieldMapOf({ b_field: 'status', b_type: 'input' }),
),
).toBe("[status] IN (N'a', N'b')")
})
it('日期时间字面量:本地时刻折算成 UTC 后比较,date 字段不折算', () => {
const datetimeField = { b_field: 'created', b_type: 'datetime' }
const dateField = { b_field: 'created', b_type: 'date' }
// datetime:库中存 UTC(见后端 TimeUtils),本地端点先折算再拼字面量;
// 自带时分秒按原时刻、仅日精度才补当天起 / 当天末,再统一折算
expect(sqlLiteral('2026-01-01 09:30:00', datetimeField)).toBe(
`N'${toUtcWall('2026-01-01 09:30:00')}'`,
)
expect(sqlLiteral('2026-01-31 18:00:00', datetimeField, true)).toBe(
`N'${toUtcWall('2026-01-31 18:00:00')}'`,
)
expect(sqlLiteral('2026-01-01', datetimeField)).toBe(`N'${toUtcWall('2026-01-01 00:00:00')}'`)
expect(sqlLiteral('2026-01-31', datetimeField, true)).toBe(
`N'${toUtcWall('2026-01-31 23:59:59')}'`,
)
// date 字段是纯日历值(无时区语义):始终截到日,不折算
expect(sqlLiteral('2026-01-01 09:30:00', dateField)).toBe("N'2026-01-01'")
})
it('parses options and normalizes defaults', () => {
const parsed = parseOptions({ b_options: '{"options":[{"key":"1","name":"启用"},"2"]}' })
expect(parsed.options).toEqual([
{ key: '1', name: '启用', value: '1', label: '启用' },
{ value: '2', label: '2' },
])
expect(
defaultQueryValue(
item({
field: { b_field: 'status', b_type: 'input' },
config: { b_operator: 'in', b_default_value: 'a,b' },
}),
),
).toEqual(['a', 'b'])
})
it('formats ISO dates consistently', () => {
expect(formatDateValue('2026-08-01', false)).toBe('2026-08-01')
expect(formatDateValue(null)).toBe('')
})
/**
* 时间契约(见后端 TimeUtils 与 fieldUtils 注释):
* 库中 datetime 列存 UTC,接口出口是带 Z 的 ISO 串("2026-09-26T02:00:00Z");
* 展示时按浏览器本地时区折算,提交时再由 toUtcIsoDateTime 转回 UTC。
* date / time 与无时区标记的墙钟串没有时区语义,原样透传。
*/
it('展示:带 Z 的 UTC 串折算成本地,墙钟串与日期串原样透传', () => {
// 用往返断言,不依赖运行环境时区
const local = '2026-09-15 21:16:47'
const utc = toUtcIsoDateTime(local)
expect(utc).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/)
expect(formatDateValue(utc, true)).toBe(local)
expect(formatDateValue(utc, false)).toBe('2026-09-15')
// 无时区标记的墙钟串(存量数据、本地值):透传
expect(formatDateValue('2026-09-15 21:16:47', true)).toBe('2026-09-15 21:16:47')
expect(formatDateValue('2026-09-15 21:16:47', false)).toBe('2026-09-15')
expect(formatDateValue('2026-09-15', false)).toBe('2026-09-15')
// 非日期开头的输入(数字时间戳等)仍走 Date 兜底,按本地时间格式化
expect(formatDateValue(1789471007000, true)).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)
})
it('提交:本地时间值转成带 Z 的 UTC 串,已是带标记的串原样返回', () => {
expect(toUtcIsoDateTime('2026-09-15 21:16:47')).toMatch(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/,
)
expect(toUtcIsoDateTime('2026-09-15T13:16:47Z')).toBe('2026-09-15T13:16:47Z')
expect(toUtcIsoDateTime('2026-09-15T21:16:47+08:00')).toBe('2026-09-15T21:16:47+08:00')
// 本地 → UTC → 展示:往返一致
expect(formatDateValue(toUtcIsoDateTime('2026-09-15 21:16:47'), true)).toBe('2026-09-15 21:16:47')
})
})