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') }) })