import { describe, expect, it, vi } from 'vitest'; import { createWatermarkTemplateRequestSchema } from '@atlas/contracts'; import { WatermarksService } from '../src/watermarks/watermarks.service.js'; const templateId = '46ea1f9c-28d0-4b32-92e1-3d4e74ad7757'; const assetId = '54799515-028d-4320-80a2-f53fd13fb588'; const imageRow = (size: number, radius: number, version = 1) => ({ id: templateId, name: '图片水印', kind: 'image' as const, current_version: version, created_at: new Date('2026-09-26T00:00:00Z'), updated_at: new Date('2026-09-26T00:00:00Z'), revision: version, text_content: null, image_asset_id: assetId, font_code: null, font_size_px: null, opacity: '0.2600', scale_ratio: '0.5556', image_size_px: size, border_radius_px: radius, position: 'bottom_right', position_x: '1.0000', position_y: '1.0000', }); const serviceWith = (query: ReturnType) => { const run = vi.fn(async (_input: unknown, work: (tx: { query: typeof query }) => Promise) => work({ query })); return new WatermarksService( { run } as unknown as ConstructorParameters[0], {} as ConstructorParameters[1], ); }; describe('图片水印像素尺寸和圆角', () => { it('创建契约接收像素尺寸并约束尺寸和圆角范围,同时兼容旧比例', () => { const base = { name: '图片水印', kind: 'image', config: { image_asset_id: assetId, opacity: '0.26', position_x: 1, position_y: 1 }, }; expect(createWatermarkTemplateRequestSchema.safeParse({ ...base, config: { ...base.config, image_size_px: 80, border_radius_px: 12 }, }).success).toBe(true); expect(createWatermarkTemplateRequestSchema.safeParse({ ...base, config: { ...base.config, image_size_px: 10 }, }).success).toBe(true); expect(createWatermarkTemplateRequestSchema.safeParse({ ...base, config: { ...base.config, scale_ratio: '0.25' }, }).success).toBe(true); expect(createWatermarkTemplateRequestSchema.safeParse(base).success).toBe(false); expect(createWatermarkTemplateRequestSchema.safeParse({ ...base, config: { ...base.config, image_size_px: 9 }, }).success).toBe(false); expect(createWatermarkTemplateRequestSchema.safeParse({ ...base, config: { ...base.config, image_size_px: 201 }, }).success).toBe(false); expect(createWatermarkTemplateRequestSchema.safeParse({ ...base, config: { ...base.config, image_size_px: 80, border_radius_px: 101 }, }).success).toBe(false); }); it('创建时保存绝对尺寸和圆角,并保留旧比例字段', async () => { let inserted: unknown[] = []; const query = vi.fn(async (sql: string, args?: unknown[]) => { if (sql.includes('SELECT id FROM atlas.media_assets')) return { rows: [{ id: assetId }] }; if (sql.includes('INSERT INTO atlas.watermark_templates')) return { rows: [{ id: templateId }] }; if (sql.includes('INSERT INTO atlas.watermark_template_versions')) { inserted = args ?? []; return { rows: [] }; } if (sql.includes('SELECT template.id')) return { rows: [imageRow(Number(inserted[13]), Number(inserted[14]))] }; throw new Error(`未知 SQL: ${sql}`); }); const result = await serviceWith(query).create('workspace', 'user', 'key', { name: '图片水印', kind: 'image', config: { image_asset_id: assetId, image_size_px: 80, border_radius_px: 12, opacity: '0.26', position_x: 1, position_y: 1 }, }); expect(inserted[8]).toBe('0.5556'); expect(inserted.slice(13, 15)).toEqual([80, 12]); expect(result).toMatchObject({ body: { data: { config: { image_size_px: 80, border_radius_px: 12, } } } }); }); it('旧版比例输入换算为像素尺寸,圆角默认为零', async () => { let inserted: unknown[] = []; const query = vi.fn(async (sql: string, args?: unknown[]) => { if (sql.includes('SELECT id FROM atlas.media_assets')) return { rows: [{ id: assetId }] }; if (sql.includes('INSERT INTO atlas.watermark_templates')) return { rows: [{ id: templateId }] }; if (sql.includes('INSERT INTO atlas.watermark_template_versions')) { inserted = args ?? []; return { rows: [] }; } if (sql.includes('SELECT template.id')) return { rows: [imageRow(36, 0)] }; throw new Error(`未知 SQL: ${sql}`); }); await serviceWith(query).create('workspace', 'user', 'key', { name: '图片水印', kind: 'image', config: { image_asset_id: assetId, scale_ratio: '0.25', opacity: '0.26', position_x: 1, position_y: 1 }, }); expect(inserted[8]).toBe('0.25'); expect(inserted.slice(13, 15)).toEqual([36, 0]); }); it('10px 尺寸原值保存,兼容比例保持旧版允许的最小值', async () => { let inserted: unknown[] = []; const query = vi.fn(async (sql: string, args?: unknown[]) => { if (sql.includes('SELECT id FROM atlas.media_assets')) return { rows: [{ id: assetId }] }; if (sql.includes('INSERT INTO atlas.watermark_templates')) return { rows: [{ id: templateId }] }; if (sql.includes('INSERT INTO atlas.watermark_template_versions')) { inserted = args ?? []; return { rows: [] }; } if (sql.includes('SELECT template.id')) return { rows: [imageRow(10, 0)] }; throw new Error(`未知 SQL: ${sql}`); }); await serviceWith(query).create('workspace', 'user', 'key', { name: '图片水印', kind: 'image', config: { image_asset_id: assetId, image_size_px: 10, opacity: '0.26', position_x: 1, position_y: 1 }, }); expect(inserted[8]).toBe('0.1'); expect(inserted[13]).toBe(10); }); it('只修改圆角时保留图片尺寸并创建新版本', async () => { let inserted: unknown[] = []; const query = vi.fn(async (sql: string, args?: unknown[]) => { if (sql.includes('SELECT template.id') && sql.includes('FOR UPDATE')) return { rows: [imageRow(80, 0)] }; if (sql.includes('SELECT id FROM atlas.media_assets')) return { rows: [{ id: assetId }] }; if (sql.includes('UPDATE atlas.watermark_templates')) return { rows: [] }; if (sql.includes('INSERT INTO atlas.watermark_template_versions')) { inserted = args ?? []; return { rows: [] }; } if (sql.includes('SELECT template.id')) return { rows: [imageRow(80, 16, 2)] }; throw new Error(`未知 SQL: ${sql}`); }); const result = await serviceWith(query).update( 'workspace', 'user', templateId, 'key', 1, { config: { border_radius_px: 16 } }, ); expect(inserted[13]).toBe(80); expect(inserted[14]).toBe(16); expect(result).toMatchObject({ body: { data: { current_version: 2, config: { image_size_px: 80, border_radius_px: 16, } } } }); }); });