gongxiangp2/lib/__tests__/task-detail-preview-runs.test.ts

290 lines
11 KiB
TypeScript
Raw Permalink 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.

/** @jest-environment node */
import {
alignRunsToZipEntriesByOrder,
buildRunsWithLegacyIndex,
buildServerPreviewFileName,
groupPreviewRowsIntoRuns,
matchRunsToZipEntries,
parseLegacyPreviewFileName,
parseTaskDetailPreviewFileName,
} from '@/lib/task-detail-asset-edit'
function row(id: string, fileName: string, sortOrder = 0): any {
return { id, assetType: 'main-preview', fileUrl: `s3://${id}`, fileName, sortOrder, skuCode: null }
}
describe('任务-详情预览名解析', () => {
test('客户端单图命名提取来源基础名', () => {
expect(parseTaskDetailPreviewFileName('IMG_1001.webp')).toEqual({
label: 'IMG_1001',
sliceIndex: null,
})
})
test('客户端切片命名', () => {
expect(parseTaskDetailPreviewFileName('长图-001.webp')).toEqual({
label: '长图',
sliceIndex: 1,
})
})
test('服务端生成命名携带来源锚', () => {
const name = buildServerPreviewFileName('IMG_2002', 3)
expect(parseTaskDetailPreviewFileName(name)).toEqual({
label: 'IMG_2002',
sliceIndex: null,
})
})
test('服务端长图切片命名可解析 sliceIndex', () => {
const name = buildServerPreviewFileName('IMG_2002', 3, 1)
expect(parseTaskDetailPreviewFileName(name)).toEqual({
label: 'IMG_2002',
sliceIndex: 1,
})
})
test('非 webp 返回 null', () => {
expect(parseTaskDetailPreviewFileName('photo.jpg')).toBeNull()
})
})
describe('任务-详情预览行切分', () => {
test('相邻同名但独立的单预览保持独立 run', () => {
const runs = groupPreviewRowsIntoRuns([
row('a', 'cover.webp', 0),
row('b', 'cover.webp', 1),
])
expect(runs).not.toBeNull()
expect(runs!.length).toBe(2)
})
test('连续切片合并为一个 run,序号重置后开启新 run', () => {
const runs = groupPreviewRowsIntoRuns([
row('a', 'detail-001.webp', 0),
row('b', 'detail-002.webp', 1),
row('c', 'detail-001.webp', 2),
row('d', 'detail-002.webp', 3),
])
expect(runs).not.toBeNull()
expect(runs!.length).toBe(2)
expect(runs![0].rows.map((item: any) => item.id)).toEqual(['a', 'b'])
expect(runs![1].rows.map((item: any) => item.id)).toEqual(['c', 'd'])
})
test('历史同锚旧 __srv 无 slice 预览按相邻行兼容归并', () => {
const oldSrv = '1_02__srv-1785466728480-11-abc1234.webp'
const runs = groupPreviewRowsIntoRuns([
row('a', oldSrv, 0),
row('b', oldSrv, 1),
row('c', '1_03-preview.webp', 2),
])
expect(runs).not.toBeNull()
expect(runs!.length).toBe(2)
expect(runs![0].rows.map((item: any) => item.id)).toEqual(['a', 'b'])
})
test('出现无法解析的名字时整体放弃切分', () => {
expect(groupPreviewRowsIntoRuns([row('a', 'unknown.png')])).toBeNull()
})
})
describe('run 与 ZIP 条目的名字匹配', () => {
const entryNames = ['IMG_A.jpg', 'IMG_B.png', 'IMG_C.jpg']
test('按基础名唯一命中(顺序无关)', () => {
const runs = groupPreviewRowsIntoRuns([
row('a', 'IMG_C.webp', 0),
row('b', 'IMG_A.webp', 1),
row('c', 'IMG_B.webp', 2),
])!
expect(matchRunsToZipEntries(runs, entryNames)).toEqual([2, 0, 1])
})
test('单个无锚 run 与剩余条目一一配对', () => {
const srvName = buildServerPreviewFileName('IMG_B', 1)
const runs = groupPreviewRowsIntoRuns([
row('a', 'IMG_A.webp', 0),
row('b', srvName, 1),
row('c', 'IMG_C.webp', 2),
])!
expect(matchRunsToZipEntries(runs, entryNames)).toEqual([0, 1, 2])
})
test('两个以上无锚 run 拒绝编辑', () => {
// 历史服务端兜底命名(旧格式,不带来源锚)无法确认对应关系
const runs = groupPreviewRowsIntoRuns([
row('a', 'main-preview-1736000000000-0-aaa0000.webp', 0),
row('b', 'main-preview-1736000000000-1-bbb1111.webp', 1),
])!
expect(() => matchRunsToZipEntries(runs, ['x.jpg', 'y.png'])).toThrow()
})
test('数量不一致直接拒绝', () => {
const runs = groupPreviewRowsIntoRuns([row('a', 'IMG_A.webp')])!
expect(() => matchRunsToZipEntries(runs, entryNames)).toThrow()
})
test('同名条目导致歧义时拒绝', () => {
// 预览行只有一份 cover,但包里有两个同名条目
const runs = groupPreviewRowsIntoRuns([
row('a', 'cover.webp', 0),
row('b', 'other.webp', 1),
])!
expect(() => matchRunsToZipEntries(runs, ['cover.jpg', 'COVER.png'])).toThrow()
})
test('真实形态:1_02 两个旧 __srv preview 对一个 ZIP 条目', () => {
const oldSrv = '1_02__srv-1785466728480-11-abc1234.webp'
const runs = groupPreviewRowsIntoRuns([
row('a', '1_01-preview.webp', 0),
row('b', oldSrv, 1),
row('c', oldSrv, 2),
row('d', '1_03-preview.webp', 3),
])!
expect(runs.length).toBe(3)
expect(runs[1].rows.map((item: any) => item.id)).toEqual(['b', 'c'])
expect(matchRunsToZipEntries(runs, ['1_01.jpg', '1_02.jpg', '1_03.jpg'])).toEqual([0, 1, 2])
})
})
describe('旧代预览名(detail-preview-{时间戳}-{序号}-...)兼容', () => {
const legacyName = (index: number, slice?: number, salt = 'abc1234') =>
`detail-preview-1785466728480-${index}${slice !== undefined ? `-slice${slice}` : ''}-${salt}.webp`
test('解析旧代命名提取图片序号与切片', () => {
expect(parseLegacyPreviewFileName(legacyName(0, 0))).toEqual({ imageIndex: 0, sliceIndex: 0 })
expect(parseLegacyPreviewFileName(legacyName(2))).toEqual({ imageIndex: 2, sliceIndex: null })
expect(parseLegacyPreviewFileName('main-preview-1785466728480-5-slice1-xyz99.png')).toBeNull()
expect(parseLegacyPreviewFileName('详情_01.jpg')).toBeNull()
})
test('纯旧代命名素材:序号升序对齐 ZIP 条目位置', () => {
const rows = [0, 1, 2, 3].map((index) => row(`r${index}`, legacyName(index), index))
const runs = buildRunsWithLegacyIndex(rows)
expect(runs).not.toBeNull()
const aligned = alignRunsToZipEntriesByOrder(runs!, ['详情_01.jpg', '详情_02.jpg', '详情_03.jpg', '详情_04.jpg'])
expect(aligned).toEqual([0, 1, 2, 3])
})
test('切片行归并进同序号 run', () => {
const rows = [
row('a0', legacyName(0, 0), 0),
row('a1', legacyName(0, 1), 1),
row('b', legacyName(1), 2),
]
const runs = buildRunsWithLegacyIndex(rows)
expect(runs!.length).toBe(2)
expect(runs![0].rows.length).toBe(2)
const aligned = alignRunsToZipEntriesByOrder(runs!, ['详情_01.jpg', '详情_02.jpg'])
expect(aligned).toEqual([0, 1])
})
test('混合命名(编辑后一张变服务端名):锚定 + 旧代序号共存', () => {
const srvName = buildServerPreviewFileName('详情_02', 1)
const rows = [
row('a', legacyName(0), 0),
row('b', srvName, 1),
row('c', legacyName(2), 2),
]
const runs = buildRunsWithLegacyIndex(rows)
const aligned = alignRunsToZipEntriesByOrder(runs!, ['详情_01.jpg', '详情_02.jpg', '详情_03.jpg'])
expect(aligned).not.toBeNull()
// srv 行锚定到条目 1,两条旧代行占 0 和 2
expect(aligned![runs!.findIndex((run) => run.legacyIndex === null)]).toBe(1)
expect(aligned![0]).toBe(0)
expect(aligned![2]).toBe(2)
})
test('混合旧代序号与历史无 slice 的 __srv 双切片时仍可对齐', () => {
const oldSrv = '1_02__srv-1785466728480-11-abc1234.webp'
const rows = [
row('a', legacyName(0), 0),
row('b', oldSrv, 1),
row('c', oldSrv, 2),
row('d', legacyName(2), 3),
]
const runs = buildRunsWithLegacyIndex(rows)
expect(runs).not.toBeNull()
expect(runs!.length).toBe(3)
expect(runs![1].rows.map((item: any) => item.id)).toEqual(['b', 'c'])
const aligned = alignRunsToZipEntriesByOrder(runs!, ['1_01.jpg', '1_02.jpg', '1_03.jpg'])
expect(aligned).toEqual([0, 1, 2])
})
test('删除后再编辑:条目与序号同删,相对顺序对齐依然成立', () => {
// 原 4 张删掉序号 1:剩条目 3 个、旧代序号 {0,2,3}
const rows = [0, 2, 3].map((index) => row(`r${index}`, legacyName(index), index))
const runs = buildRunsWithLegacyIndex(rows)
const aligned = alignRunsToZipEntriesByOrder(runs!, ['详情_01.jpg', '详情_03.jpg', '详情_04.jpg'])
expect(aligned).toEqual([0, 1, 2])
})
test('序号重复直接拒绝', () => {
const rows = [
row('a', legacyName(0, 0), 0),
row('b', legacyName(0, 1), 1),
]
const runs = buildRunsWithLegacyIndex(rows)
expect(runs!.length).toBe(1)
})
test('数量不一致拒绝', () => {
const rows = [0, 1].map((index) => row(`r${index}`, legacyName(index), index))
const runs = buildRunsWithLegacyIndex(rows)
expect(alignRunsToZipEntriesByOrder(runs!, ['详情_01.jpg'])).toBeNull()
})
test('存在无法解析的行时返回 null(保持拒绝)', () => {
const rows = [
row('a', legacyName(0), 0),
row('b', '不是预览图.jpg', 1),
]
expect(buildRunsWithLegacyIndex(rows)).toBeNull()
})
test('新命名素材不受影响:走既有锚定路径', () => {
const rows = [
row('a', 'photo-a.webp', 0),
row('b', 'photo-b.webp', 1),
]
const runs = buildRunsWithLegacyIndex(rows)
expect(runs!.every((run) => run.legacyIndex === null)).toBe(true)
const aligned = alignRunsToZipEntriesByOrder(runs!, ['photo-a.jpg', 'photo-b.jpg'])
expect(aligned).toEqual([0, 1])
})
})
describe('历史客户端 -preview 尾巴命名兼容', () => {
test('剥掉 -preview 尾巴得到来源锚', () => {
expect(parseTaskDetailPreviewFileName('主图3-preview.webp')).toEqual({ label: '主图3', sliceIndex: null })
expect(parseTaskDetailPreviewFileName('4-preview.webp')).toEqual({ label: '4', sliceIndex: null })
expect(parseTaskDetailPreviewFileName('GBSFY-SJZ0030 (1)-preview.webp')).toEqual({ label: 'GBSFY-SJZ0030 (1)', sliceIndex: null })
})
test('真实形态:26 行 -preview 命名主图与 26 条目主锚定直接配对', () => {
const labels = ['主图3 拷贝 3', '主图3 拷贝', '主图3', '主图4', '4', '5', '6', '7',
'主图1 拷贝', '主图1', '主图2', '主图2拷贝', '1', '2', '3 拷贝 2', '3',
'功能主图4', '功能主图5', '功能主图1', '功能主图2', '功能主图3',
'功能主图3-2', '功能主图4-2', '功能主图5-2', '功能主图1-2', '功能主图2-2']
const rows = labels.map((label, index) => row(`r${index}`, `${label}-preview.webp`, index))
const runs = groupPreviewRowsIntoRuns(rows)
expect(runs?.length).toBe(26)
const exts = ['.png', '.png', '.png', '.png', '.png', '.png', '.jpg', '.jpg', '.png', '.png',
'.jpg', '.jpg', '.png', '.png', '.png', '.png', '.png', '.png', '.png', '.png',
'.png', '.png', '.png', '.png', '.png', '.png']
const entries = labels.map((label, i) => label + exts[i])
const matched = matchRunsToZipEntries(runs!, entries)
expect(matched).toEqual(labels.map((_, i) => i))
})
test('新代命名不受 -preview 剥离影响', () => {
// 源文件本身叫 photo-preview.jpg 的新代命名:剥尾后锚对不上条目 → 拒绝而非错配
expect(parseTaskDetailPreviewFileName('photo-preview.webp')).toEqual({ label: 'photo', sliceIndex: null })
const runs = groupPreviewRowsIntoRuns([row('a', 'photo-preview.webp', 0)])
// 单图场景:锚剥错后走「剩余一一配对」兜底,1 对 1 无歧义,仍指向唯一条目
expect(matchRunsToZipEntries(runs!, ['photo-preview.jpg'])).toEqual([0])
})
})