290 lines
11 KiB
TypeScript
290 lines
11 KiB
TypeScript
/** @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])
|
||
})
|
||
})
|