/ tests / utils / image-optimizer-supplement.test.js
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  });