image-optimizer-supplement.test.js
1 /** 2 * Supplement tests for src/utils/image-optimizer.js 3 * 4 * Existing tests only cover calculateSavings() (the one pure function). 5 * This supplement tests: 6 * - calculateSavings edge cases: negative savings, partial key sets 7 * - optimizeScreenshot + optimizeScreenshots using real sharp buffers 8 * - applyDomAwareCrop logic (via optimizeScreenshot with cropBoundaries) 9 * - includeUncropped mode: identical detection + separate buffer return 10 * - Batch optimizeScreenshots: partial keys, all keys 11 */ 12 13 import { test, describe } from 'node:test'; 14 import assert from 'node:assert/strict'; 15 import sharp from 'sharp'; 16 17 import { 18 calculateSavings, 19 optimizeScreenshot, 20 optimizeScreenshots, 21 } from '../../src/utils/image-optimizer.js'; 22 23 // ── Helpers ────────────────────────────────────────────────────────────────── 24 25 /** Create a real PNG buffer of given dimensions (solid colour) */ 26 async function makePng(width, height, colour = { r: 100, g: 150, b: 200 }) { 27 return sharp({ 28 create: { width, height, channels: 3, background: colour }, 29 }) 30 .png() 31 .toBuffer(); 32 } 33 34 /** Create a PNG with distinct content (gradient-like via two halves) */ 35 async function makeContentPng(width, height) { 36 const top = await sharp({ 37 create: { width, height: Math.floor(height / 2), channels: 3, background: { r: 255, g: 0, b: 0 } }, 38 }).raw().toBuffer(); 39 40 const bottom = await sharp({ 41 create: { width, height: height - Math.floor(height / 2), channels: 3, background: { r: 0, g: 0, b: 255 } }, 42 }).raw().toBuffer(); 43 44 const combined = Buffer.concat([top, bottom]); 45 return sharp(combined, { raw: { width, height, channels: 3 } }).png().toBuffer(); 46 } 47 48 // ── calculateSavings — additional edge cases ───────────────────────────────── 49 50 describe('calculateSavings — supplement', () => { 51 test('handles negative savings (optimized larger than original)', () => { 52 const original = { desktop_above: Buffer.alloc(10 * 1024) }; // 10 KB 53 const optimized = { desktop_above: Buffer.alloc(20 * 1024) }; // 20 KB 54 const result = calculateSavings(original, optimized); 55 assert.ok(result.savingsPercent < 0, 'savings percent should be negative'); 56 assert.ok(result.savingsKB < 0, 'savingsKB should be negative'); 57 }); 58 59 test('handles only desktop_below and mobile_above keys', () => { 60 const original = { 61 desktop_below: Buffer.alloc(50 * 1024), 62 mobile_above: Buffer.alloc(30 * 1024), 63 }; 64 const optimized = { 65 desktop_below: Buffer.alloc(25 * 1024), 66 mobile_above: Buffer.alloc(15 * 1024), 67 }; 68 const result = calculateSavings(original, optimized); 69 assert.equal(result.originalKB, 80); 70 assert.equal(result.optimizedKB, 40); 71 assert.equal(result.savingsPercent, 50); 72 }); 73 74 test('handles mismatched keys (original has key optimized does not)', () => { 75 const original = { 76 desktop_above: Buffer.alloc(100 * 1024), 77 desktop_below: Buffer.alloc(50 * 1024), 78 }; 79 const optimized = { 80 desktop_above: Buffer.alloc(60 * 1024), 81 // desktop_below missing from optimized 82 }; 83 const result = calculateSavings(original, optimized); 84 assert.equal(result.originalKB, 150); 85 assert.equal(result.optimizedKB, 60); 86 }); 87 88 test('ignores keys not in the fixed set', () => { 89 const original = { desktop_above: Buffer.alloc(1024), extra_key: Buffer.alloc(99999) }; 90 const optimized = { desktop_above: Buffer.alloc(512), extra_key: Buffer.alloc(99999) }; 91 const result = calculateSavings(original, optimized); 92 assert.equal(result.originalKB, 1); 93 assert.equal(result.optimizedKB, 1); // rounds 0.5 94 }); 95 }); 96 97 // ── optimizeScreenshot — real sharp processing ────────────────────────────── 98 99 describe('optimizeScreenshot — real sharp buffers', () => { 100 test('returns a JPEG buffer smaller than or equal to original for desktop_above', async () => { 101 const png = await makePng(1440, 900); 102 const result = await optimizeScreenshot(png, { type: 'desktop_above', smartCrop: false }); 103 assert.ok(Buffer.isBuffer(result), 'should return a Buffer'); 104 // JPEG is generally smaller than PNG for photos 105 const meta = await sharp(result).metadata(); 106 assert.equal(meta.format, 'jpeg'); 107 assert.ok(meta.width <= 768, `width should be <= 768, got ${meta.width}`); 108 }); 109 110 test('returns a JPEG buffer for mobile_above type', async () => { 111 const png = await makePng(390, 844); 112 const result = await optimizeScreenshot(png, { type: 'mobile_above', smartCrop: false }); 113 assert.ok(Buffer.isBuffer(result)); 114 const meta = await sharp(result).metadata(); 115 assert.equal(meta.format, 'jpeg'); 116 assert.ok(meta.width <= 208, `width should be <= 208, got ${meta.width}`); 117 }); 118 119 test('returns a JPEG buffer for desktop_below type', async () => { 120 const png = await makePng(1440, 900); 121 const result = await optimizeScreenshot(png, { type: 'desktop_below', smartCrop: false }); 122 assert.ok(Buffer.isBuffer(result)); 123 const meta = await sharp(result).metadata(); 124 assert.equal(meta.format, 'jpeg'); 125 }); 126 127 test('smartCrop=true with no cropBoundaries uses zero-crop fallback', async () => { 128 const png = await makePng(1440, 900); 129 // No cropBoundaries → applyDomAwareCrop uses zero-crop (no extraction) 130 const result = await optimizeScreenshot(png, { type: 'desktop_above', smartCrop: true }); 131 assert.ok(Buffer.isBuffer(result)); 132 const meta = await sharp(result).metadata(); 133 assert.equal(meta.format, 'jpeg'); 134 }); 135 136 test('smartCrop=true with significant cropBoundaries applies DOM crop', async () => { 137 const png = await makeContentPng(1440, 900); 138 const result = await optimizeScreenshot(png, { 139 type: 'desktop_above', 140 smartCrop: true, 141 cropBoundaries: { 142 topCrop: 80, 143 leftCrop: 20, 144 rightCrop: 20, 145 metadata: { navReasoning: 'Test nav detected at 80px' }, 146 }, 147 }); 148 assert.ok(Buffer.isBuffer(result)); 149 const meta = await sharp(result).metadata(); 150 assert.equal(meta.format, 'jpeg'); 151 }); 152 153 test('does not enlarge small images (withoutEnlargement)', async () => { 154 const png = await makePng(200, 100); 155 const result = await optimizeScreenshot(png, { type: 'desktop_above', smartCrop: false }); 156 const meta = await sharp(result).metadata(); 157 assert.ok(meta.width <= 200, `should not enlarge, got ${meta.width}`); 158 }); 159 160 test('defaults type to desktop_above when omitted', async () => { 161 const png = await makePng(1440, 900); 162 const result = await optimizeScreenshot(png); 163 assert.ok(Buffer.isBuffer(result)); 164 const meta = await sharp(result).metadata(); 165 assert.ok(meta.width <= 768); 166 }); 167 }); 168 169 // ── optimizeScreenshot — includeUncropped mode ────────────────────────────── 170 171 describe('optimizeScreenshot — includeUncropped', () => { 172 test('returns object with cropped and uncropped when images differ', async () => { 173 const png = await makeContentPng(1440, 900); 174 const result = await optimizeScreenshot(png, { 175 type: 'desktop_above', 176 smartCrop: true, 177 includeUncropped: true, 178 cropBoundaries: { 179 topCrop: 150, 180 leftCrop: 50, 181 rightCrop: 50, 182 metadata: { navReasoning: 'Large crop to create size diff' }, 183 }, 184 }); 185 186 assert.equal(typeof result, 'object'); 187 assert.ok(Buffer.isBuffer(result.cropped), 'should have cropped buffer'); 188 assert.ok(result.metadata, 'should have metadata'); 189 // uncropped may be null if images are identical, or a Buffer if they differ 190 if (result.uncropped !== null) { 191 assert.ok(Buffer.isBuffer(result.uncropped), 'uncropped should be a buffer when not null'); 192 assert.equal(result.metadata.uncroppedSkipped, false); 193 } 194 }); 195 196 test('returns uncropped=null when crop has no effect (identical images)', async () => { 197 // Solid colour image with zero crop boundaries → crop is identical to uncropped 198 const png = await makePng(1440, 900); 199 const result = await optimizeScreenshot(png, { 200 type: 'desktop_above', 201 smartCrop: true, 202 includeUncropped: true, 203 // No cropBoundaries → zero-crop → no actual crop 204 }); 205 206 assert.equal(typeof result, 'object'); 207 assert.ok(Buffer.isBuffer(result.cropped)); 208 // With zero crop, the images should be identical 209 assert.equal(result.uncropped, null, 'uncropped should be null when identical to cropped'); 210 assert.equal(result.metadata.uncroppedSkipped, true); 211 assert.equal(result.metadata.reason, 'identical_to_cropped'); 212 }); 213 }); 214 215 // ── applyDomAwareCrop — safety bounds via optimizeScreenshot ───────────────── 216 217 describe('applyDomAwareCrop via optimizeScreenshot — safety bounds', () => { 218 test('excessive crop is clamped (crop > 40% of image)', async () => { 219 const png = await makeContentPng(1440, 900); 220 // Requesting crop that would reduce width below 60% of original 221 const result = await optimizeScreenshot(png, { 222 type: 'desktop_above', 223 smartCrop: true, 224 cropBoundaries: { 225 topCrop: 500, // > 40% of 900 226 leftCrop: 400, // > 40% of 1440 227 rightCrop: 400, 228 metadata: { navReasoning: 'Excessive crop test' }, 229 }, 230 }); 231 assert.ok(Buffer.isBuffer(result), 'should still return a valid buffer'); 232 const meta = await sharp(result).metadata(); 233 assert.equal(meta.format, 'jpeg'); 234 }); 235 236 test('handles cropBoundaries with all zeros (no significant crop)', async () => { 237 const png = await makePng(1440, 900); 238 const result = await optimizeScreenshot(png, { 239 type: 'desktop_above', 240 smartCrop: true, 241 cropBoundaries: { 242 topCrop: 0, 243 leftCrop: 0, 244 rightCrop: 0, 245 metadata: { navReasoning: 'No nav found' }, 246 }, 247 }); 248 assert.ok(Buffer.isBuffer(result)); 249 }); 250 251 test('handles small crops below significance threshold (< 10px)', async () => { 252 const png = await makePng(1440, 900); 253 const result = await optimizeScreenshot(png, { 254 type: 'desktop_above', 255 smartCrop: true, 256 cropBoundaries: { 257 topCrop: 5, 258 leftCrop: 3, 259 rightCrop: 2, 260 metadata: { navReasoning: 'Minimal crop' }, 261 }, 262 }); 263 assert.ok(Buffer.isBuffer(result)); 264 }); 265 }); 266 267 // ── optimizeScreenshots — batch processing ─────────────────────────────────── 268 269 describe('optimizeScreenshots — batch', () => { 270 test('optimizes all three screenshot types', async () => { 271 const screenshots = { 272 desktop_above: await makePng(1440, 900), 273 desktop_below: await makePng(1440, 900), 274 mobile_above: await makePng(390, 844), 275 }; 276 const result = await optimizeScreenshots(screenshots); 277 assert.ok(Buffer.isBuffer(result.desktop_above), 'desktop_above should be buffer'); 278 assert.ok(Buffer.isBuffer(result.desktop_below), 'desktop_below should be buffer'); 279 assert.ok(Buffer.isBuffer(result.mobile_above), 'mobile_above should be buffer'); 280 }); 281 282 test('handles partial screenshots (only desktop_above)', async () => { 283 const screenshots = { 284 desktop_above: await makePng(1440, 900), 285 }; 286 const result = await optimizeScreenshots(screenshots); 287 assert.ok(Buffer.isBuffer(result.desktop_above)); 288 assert.equal(result.desktop_below, undefined); 289 assert.equal(result.mobile_above, undefined); 290 }); 291 292 test('handles empty screenshots object', async () => { 293 const result = await optimizeScreenshots({}); 294 assert.deepEqual(result, {}); 295 }); 296 297 test('handles only mobile screenshot', async () => { 298 const screenshots = { 299 mobile_above: await makePng(390, 844), 300 }; 301 const result = await optimizeScreenshots(screenshots); 302 assert.ok(Buffer.isBuffer(result.mobile_above)); 303 assert.equal(result.desktop_above, undefined); 304 }); 305 });