6600bfaa99
新增纯 TS 预览裁剪 parse-preview:把解析 Vo 裁剪为摘要字段 (taskId/行数统计/开关/prompt),items/groups 整包剔除,可按 previewRowLimit/previewGroupLimit 有界保留样本行供表格展示。输入 对象永不修改,taskId 非法 fail-fast,属性读取抛错时零部分结果。 BrandSimilarAsinTab 的 parseFiles 改为只接收裁剪后的预览(保留 200 行样本),数千行 items 不再进入 Vue 响应式 ref。8 个测试覆盖 默认、批量、幂等、空、单元素、截断溢出、非法输入与 getter 故障。
163 lines
6.2 KiB
TypeScript
163 lines
6.2 KiB
TypeScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { toParsePreview } from '../src/shared/parse-preview.ts'
|
|
|
|
interface Row {
|
|
rowIndex: number
|
|
sourceId: string
|
|
asin: string
|
|
}
|
|
|
|
interface ParseVo {
|
|
taskId: number
|
|
sourceFilename?: string
|
|
sourceFileCount?: number
|
|
totalRows: number
|
|
acceptedRows: number
|
|
droppedRows: number
|
|
groupCount?: number
|
|
aiPrompt?: string
|
|
imgSwitch?: boolean
|
|
categorySwitch?: boolean
|
|
items: Row[]
|
|
groups?: unknown[]
|
|
}
|
|
|
|
const row = (n: number): Row => ({ rowIndex: n, sourceId: `s${n}`, asin: `B00${n}` })
|
|
const vo = (over: Partial<ParseVo> = {}): ParseVo => ({
|
|
taskId: 1,
|
|
sourceFilename: 'a.xlsx',
|
|
sourceFileCount: 2,
|
|
totalRows: 100,
|
|
acceptedRows: 90,
|
|
droppedRows: 10,
|
|
groupCount: 5,
|
|
aiPrompt: 'prompt',
|
|
imgSwitch: true,
|
|
categorySwitch: false,
|
|
items: [row(1), row(2), row(3)],
|
|
groups: [{ g: 1 }, { g: 2 }],
|
|
...over,
|
|
})
|
|
|
|
test('test_task_088_payload_preview_frontend_normal_default_path', () => {
|
|
const input = vo()
|
|
const preview = toParsePreview(input)
|
|
assert.equal(preview.taskId, 1)
|
|
assert.equal(preview.sourceFilename, 'a.xlsx')
|
|
assert.equal(preview.sourceFileCount, 2)
|
|
assert.equal(preview.totalRows, 100)
|
|
assert.equal(preview.acceptedRows, 90)
|
|
assert.equal(preview.droppedRows, 10)
|
|
assert.equal(preview.groupCount, 5)
|
|
assert.equal(preview.aiPrompt, 'prompt')
|
|
assert.equal(preview.imgSwitch, true)
|
|
assert.equal(preview.categorySwitch, false)
|
|
assert.equal('items' in preview, false, 'items 不得进入预览')
|
|
assert.equal('groups' in preview, false, 'groups 不得进入预览')
|
|
assert.equal('previewItems' in preview, false, '默认不保留预览行')
|
|
// 输入对象不被修改
|
|
assert.equal(input.items.length, 3)
|
|
assert.ok(Array.isArray(input.groups) && input.groups.length === 2)
|
|
})
|
|
|
|
test('test_task_088_payload_preview_frontend_normal_multiple_items', () => {
|
|
const input = vo({ items: [row(1), row(2), row(3), row(4), row(5)], groups: [{ g: 1 }, { g: 2 }, { g: 3 }] })
|
|
const preview = toParsePreview(input, { previewRowLimit: 3, previewGroupLimit: 2 })
|
|
assert.equal(preview.totalRows, 100)
|
|
assert.equal(preview.acceptedRows, 90)
|
|
assert.ok(preview.previewItems, '有界保留预览行')
|
|
assert.equal(preview.previewItems.length, 3)
|
|
assert.deepEqual(
|
|
preview.previewItems.map((r) => r.rowIndex),
|
|
[1, 2, 3],
|
|
)
|
|
assert.equal(preview.previewGroups?.length, 2)
|
|
// 摘要字段不受截断影响,顺序稳定
|
|
assert.equal(preview.previewItems[0].asin, 'B001')
|
|
})
|
|
|
|
test('test_task_088_payload_preview_frontend_normal_repeated_operation_is_idempotent', () => {
|
|
const input = vo()
|
|
const once = toParsePreview(input)
|
|
const twice = toParsePreview(input)
|
|
assert.deepEqual(once, twice)
|
|
assert.deepEqual(toParsePreview(once), twice, '对预览再裁剪结果不变')
|
|
assert.deepEqual(vo(), input, '输入对象不被修改')
|
|
const withLimit = toParsePreview(input, { previewRowLimit: 2 })
|
|
assert.deepEqual(toParsePreview(input, { previewRowLimit: 2 }), withLimit)
|
|
})
|
|
|
|
test('test_task_088_payload_preview_frontend_boundary_empty_input', () => {
|
|
const empty = toParsePreview(vo({ items: [], groups: [] }))
|
|
assert.equal(empty.totalRows, 100)
|
|
assert.equal('items' in empty, false)
|
|
assert.equal('groups' in empty, false)
|
|
assert.equal('previewItems' in empty, false)
|
|
const limited = toParsePreview(vo({ items: [] }), { previewRowLimit: 5 })
|
|
assert.deepEqual(limited.previewItems, [])
|
|
assert.equal(limited.acceptedRows, 90)
|
|
})
|
|
|
|
test('test_task_088_payload_preview_frontend_boundary_single_item', () => {
|
|
const input = vo({ items: [row(7)] })
|
|
const preview = toParsePreview(input, { previewRowLimit: 1 })
|
|
assert.equal(preview.previewItems?.length, 1)
|
|
assert.equal(preview.previewItems?.[0].rowIndex, 7)
|
|
assert.equal(preview.taskId, 1)
|
|
})
|
|
|
|
test('test_task_088_payload_preview_frontend_boundary_limit_and_overflow', () => {
|
|
const input = vo({ items: Array.from({ length: 10 }, (_, i) => row(i + 1)) })
|
|
// limit=0 不保留任何行
|
|
const none = toParsePreview(input, { previewRowLimit: 0 })
|
|
assert.equal('previewItems' in none, false)
|
|
// 行数超 limit 截断前 N
|
|
const some = toParsePreview(input, { previewRowLimit: 3 })
|
|
assert.equal(some.previewItems?.length, 3)
|
|
assert.deepEqual(
|
|
some.previewItems?.map((r) => r.rowIndex),
|
|
[1, 2, 3],
|
|
)
|
|
// 非法 limit 抛错
|
|
assert.throws(() => toParsePreview(input, { previewRowLimit: -1 }), /previewRowLimit 不能为负数/)
|
|
assert.throws(() => toParsePreview(input, { previewGroupLimit: -1 }), /previewGroupLimit 不能为负数/)
|
|
assert.throws(() => toParsePreview(input, { previewRowLimit: NaN }), /previewRowLimit 不能为负数/)
|
|
})
|
|
|
|
test('test_task_088_payload_preview_frontend_invalid_input_rejected', () => {
|
|
assert.throws(() => toParsePreview(null as never), /解析结果必须是对象/)
|
|
assert.throws(() => toParsePreview(undefined as never), /解析结果必须是对象/)
|
|
assert.throws(() => toParsePreview('x' as never), /解析结果必须是对象/)
|
|
assert.throws(() => toParsePreview(vo({ taskId: 0 })), /taskId 必须是正整数/)
|
|
assert.throws(() => toParsePreview(vo({ taskId: -1 })), /taskId 必须是正整数/)
|
|
assert.throws(() => toParsePreview(vo({ taskId: NaN })), /taskId 必须是正整数/)
|
|
})
|
|
|
|
test('test_task_088_payload_preview_frontend_dependency_failure_releases_resources', () => {
|
|
const input = vo()
|
|
let broken = true
|
|
const poisoned = new Proxy(input, {
|
|
get(target, prop, receiver) {
|
|
if (broken && prop === 'totalRows') throw new Error('getter down')
|
|
return Reflect.get(target, prop, receiver)
|
|
},
|
|
})
|
|
assert.throws(() => toParsePreview(poisoned), /getter down/)
|
|
// 输入未被修改
|
|
assert.deepEqual(input, vo())
|
|
// 错误可恢复:修复后同一输入成功
|
|
broken = false
|
|
const preview = toParsePreview(poisoned)
|
|
assert.equal(preview.totalRows, 100)
|
|
assert.equal(preview.acceptedRows, 90)
|
|
// 提取行数据失败(items 读取抛错)不影响其余摘要字段的读取路径
|
|
const badItems = new Proxy([row(1)], {
|
|
get(target, prop) {
|
|
if (prop === 'length') throw new Error('items getter down')
|
|
return Reflect.get(target, prop)
|
|
},
|
|
})
|
|
assert.throws(() => toParsePreview(vo({ items: badItems }), { previewRowLimit: 3 }), /items getter down/)
|
|
})
|