Files
crawler-plugin/frontend-vue/tests/parse-preview.test.ts
T
huangzd1997 6600bfaa99 task-88: 解析结果前端只接收预览数据,避免大 payload 进入响应式对象
新增纯 TS 预览裁剪 parse-preview:把解析 Vo 裁剪为摘要字段
(taskId/行数统计/开关/prompt),items/groups 整包剔除,可按
previewRowLimit/previewGroupLimit 有界保留样本行供表格展示。输入
对象永不修改,taskId 非法 fail-fast,属性读取抛错时零部分结果。
BrandSimilarAsinTab 的 parseFiles 改为只接收裁剪后的预览(保留
200 行样本),数千行 items 不再进入 Vue 响应式 ref。8 个测试覆盖
默认、批量、幂等、空、单元素、截断溢出、非法输入与 getter 故障。
2026-08-30 22:55:27 +08:00

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