Files
crawler-plugin/frontend-vue/tests/responsive-shot-plan.test.ts

176 lines
6.3 KiB
TypeScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createResponsiveShotPlan } from '../src/shared/responsive-shot-plan.ts'
test('test_task_094_task_normal_default_path', () => {
const plan = createResponsiveShotPlan({
entries: ['publish', 'dedupe'],
viewports: [
{ label: 'mobile', width: 375, height: 812 },
{ label: 'desktop', width: 1280, height: 800 },
],
})
assert.deepEqual(
plan.shots.map((s) => `${s.entry}@${s.viewport.label}`),
['publish@mobile', 'publish@desktop', 'dedupe@mobile', 'dedupe@desktop'],
'默认路径:页面 × 视口 笛卡尔积,先页面后视口',
)
assert.equal(plan.shotCount, 4)
assert.equal(plan.shots[0].viewport.width, 375)
assert.equal(plan.shots[0].viewport.height, 812)
assert.equal(plan.shots[3].viewport.height, 800)
assert.deepEqual(plan.viewportStats(), { mobile: 2, desktop: 2 })
})
test('test_task_094_task_normal_multiple_items', () => {
const entries = ['a', 'b', 'c', 'd']
const plan = createResponsiveShotPlan({
entries,
viewports: [
{ label: 'mobile', width: 375, height: 812 },
{ label: 'tablet', width: 768, height: 1024 },
{ label: 'desktop', width: 1280, height: 800 },
],
})
assert.equal(plan.shotCount, 12)
assert.equal(plan.shots.length, 12)
// 批量结果不丢失、顺序稳定
assert.deepEqual(
plan.shots.map((s) => s.entry),
['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd'],
)
assert.deepEqual(plan.viewportStats(), { mobile: 4, tablet: 4, desktop: 4 })
// 文件名唯一且可预期
assert.deepEqual(
new Set(plan.shots.map((s) => s.filename)).size,
12,
)
assert.equal(plan.shots[0].filename, 'a-mobile.png')
})
test('test_task_094_task_normal_repeated_operation_is_idempotent', () => {
const options = {
entries: ['publish'],
viewports: [
{ label: 'mobile', width: 375, height: 812 },
{ label: 'mobile', width: 375, height: 812 },
{ label: 'desktop', width: 1280, height: 800 },
],
}
const plan = createResponsiveShotPlan(options)
assert.equal(plan.shotCount, 2, '重复视口去重,不产生重复截图')
// 输入对象不被修改
assert.equal(options.viewports.length, 3)
// 重复计算幂等
const again = createResponsiveShotPlan(options)
assert.deepEqual(plan.shots, again.shots)
// 同一页面重复登记也去重
const dedup = createResponsiveShotPlan({ entries: ['a', 'a'], viewports: [{ label: 'v', width: 100, height: 200 }] })
assert.equal(dedup.shotCount, 1)
})
test('test_task_094_task_boundary_empty_input', () => {
const plan = createResponsiveShotPlan({ entries: [], viewports: [{ label: 'mobile', width: 375, height: 812 }] })
assert.deepEqual(plan.shots, [])
assert.equal(plan.shotCount, 0)
assert.deepEqual(plan.viewportStats(), {})
// 空视口同理
const empty = createResponsiveShotPlan({ entries: ['a'], viewports: [] })
assert.equal(empty.shotCount, 0)
})
test('test_task_094_task_boundary_single_item', () => {
const plan = createResponsiveShotPlan({
entries: ['withdraw'],
viewports: [{ label: 'mobile', width: 375, height: 812 }],
})
assert.equal(plan.shotCount, 1)
assert.equal(plan.shots[0].entry, 'withdraw')
assert.equal(plan.shots[0].viewport.label, 'mobile')
assert.equal(plan.shots[0].filename, 'withdraw-mobile.png')
})
test('test_task_094_task_boundary_limit_and_overflow', () => {
// 截图数量有界:超过 maxShots 截断,不发生无界截图
const plan = createResponsiveShotPlan({
entries: ['a', 'b', 'c', 'd'],
viewports: [
{ label: 'mobile', width: 375, height: 812 },
{ label: 'desktop', width: 1280, height: 800 },
],
maxShots: 5,
})
assert.equal(plan.shotCount, 5)
assert.equal(plan.droppedCount, 3)
assert.deepEqual(
plan.shots.map((s) => s.filename),
['a-mobile.png', 'a-desktop.png', 'b-mobile.png', 'b-desktop.png', 'c-mobile.png'],
)
// maxShots 足够时全部保留
const full = createResponsiveShotPlan({
entries: ['a', 'b'],
viewports: [{ label: 'v', width: 100, height: 100 }],
maxShots: 8,
})
assert.equal(full.shotCount, 2)
assert.equal(full.droppedCount, 0)
})
test('test_task_094_task_invalid_input_rejected', () => {
assert.throws(() => createResponsiveShotPlan({} as never), /entries 必须是数组/)
assert.throws(
() => createResponsiveShotPlan({ entries: 'a' as never, viewports: [] }),
/entries 必须是数组/,
)
assert.throws(
() => createResponsiveShotPlan({ entries: ['a'], viewports: 'x' as never }),
/viewports 必须是数组/,
)
assert.throws(
() => createResponsiveShotPlan({ entries: ['a'], viewports: [{ label: '', width: 1, height: 1 }] }),
/label 不能为空/,
)
assert.throws(
() => createResponsiveShotPlan({ entries: ['a'], viewports: [{ label: 'v', width: 0, height: 100 }] }),
/width 必须为正数/,
)
assert.throws(
() => createResponsiveShotPlan({ entries: ['a'], viewports: [{ label: 'v', width: 100, height: -1 }] }),
/height 必须为正数/,
)
assert.throws(
() => createResponsiveShotPlan({ entries: ['a'], viewports: [], maxShots: 0 }),
/maxShots 必须为正数/,
)
assert.throws(
() => createResponsiveShotPlan({ entries: ['a'], viewports: [], maxShots: -5 }),
/maxShots 必须为正数/,
)
})
test('test_task_094_task_dependency_failure_releases_resources', () => {
// 视口列表读取失败:抛错,不产生部分截图计划;恢复后可重算
let broken = true
const poisoned = new Proxy([{ label: 'mobile', width: 375, height: 812 }], {
get(target, prop, receiver) {
if (broken && prop === '0') throw new Error('viewport getter down')
return Reflect.get(target, prop, receiver)
},
ownKeys(target) {
if (broken) throw new Error('viewport ownKeys down')
return Reflect.ownKeys(target)
},
})
assert.throws(
() => createResponsiveShotPlan({ entries: ['a'], viewports: poisoned as unknown as Array<{ label: string; width: number; height: number }> }),
/viewport/,
)
broken = false
const plan = createResponsiveShotPlan({
entries: ['a'],
viewports: poisoned as unknown as Array<{ label: string; width: number; height: number }>,
})
assert.equal(plan.shotCount, 1)
assert.equal(plan.shots[0].filename, 'a-mobile.png')
})