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