task-88: 解析结果前端只接收预览数据,避免大 payload 进入响应式对象
新增纯 TS 预览裁剪 parse-preview:把解析 Vo 裁剪为摘要字段 (taskId/行数统计/开关/prompt),items/groups 整包剔除,可按 previewRowLimit/previewGroupLimit 有界保留样本行供表格展示。输入 对象永不修改,taskId 非法 fail-fast,属性读取抛错时零部分结果。 BrandSimilarAsinTab 的 parseFiles 改为只接收裁剪后的预览(保留 200 行样本),数千行 items 不再进入 Vue 响应式 ref。8 个测试覆盖 默认、批量、幂等、空、单元素、截断溢出、非法输入与 getter 故障。
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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/)
|
||||
})
|
||||
Reference in New Issue
Block a user