/ tests / utils / screenshot-storage.test.js
screenshot-storage.test.js
  1  /**
  2   * Tests for src/utils/screenshot-storage.js
  3   *
  4   * Tests all exports: SCREENSHOT_FILES, saveScreenshots, loadScreenshots,
  5   * loadScreenshot, screenshotsExist, croppedScreenshotsExist, allScreenshotsExist,
  6   * deleteScreenshots.
  7   *
  8   * Uses a dedicated temp directory for screenshot files.
  9   */
 10  
 11  import { test, describe, before, after } from 'node:test';
 12  import assert from 'node:assert/strict';
 13  import { mkdtempSync, rmSync } from 'fs';
 14  import { tmpdir } from 'os';
 15  import { join } from 'path';
 16  
 17  // Set SCREENSHOT_BASE_PATH before importing (captured at module load time)
 18  const TEMP_SCREENSHOTS_DIR = mkdtempSync(join(tmpdir(), 'test-screenshots-'));
 19  process.env.SCREENSHOT_BASE_PATH = TEMP_SCREENSHOTS_DIR;
 20  
 21  // Dynamic import so env var is set before module-level code runs
 22  const {
 23    SCREENSHOT_FILES,
 24    saveScreenshots,
 25    loadScreenshots,
 26    loadScreenshot,
 27    screenshotsExist,
 28    croppedScreenshotsExist,
 29    allScreenshotsExist,
 30    deleteScreenshots,
 31  } = await import('../../src/utils/screenshot-storage.js');
 32  
 33  after(() => {
 34    try {
 35      rmSync(TEMP_SCREENSHOTS_DIR, { recursive: true, force: true });
 36    } catch {
 37      /* ignore */
 38    }
 39  });
 40  
 41  // ─── SCREENSHOT_FILES ─────────────────────────────────────────────────────────
 42  
 43  describe('SCREENSHOT_FILES', () => {
 44    test('has 6 screenshot types', () => {
 45      const keys = Object.keys(SCREENSHOT_FILES);
 46      assert.equal(keys.length, 6);
 47    });
 48  
 49    test('includes all expected types', () => {
 50      assert.ok('desktop_above' in SCREENSHOT_FILES);
 51      assert.ok('desktop_below' in SCREENSHOT_FILES);
 52      assert.ok('mobile_above' in SCREENSHOT_FILES);
 53      assert.ok('desktop_above_uncropped' in SCREENSHOT_FILES);
 54      assert.ok('desktop_below_uncropped' in SCREENSHOT_FILES);
 55      assert.ok('mobile_above_uncropped' in SCREENSHOT_FILES);
 56    });
 57  
 58    test('all values end with .jpg', () => {
 59      for (const filename of Object.values(SCREENSHOT_FILES)) {
 60        assert.ok(filename.endsWith('.jpg'), `${filename} should end with .jpg`);
 61      }
 62    });
 63  });
 64  
 65  // ─── saveScreenshots ──────────────────────────────────────────────────────────
 66  
 67  describe('saveScreenshots', () => {
 68    test('saves screenshots and returns path', async () => {
 69      const siteId = 1001;
 70      const fakeBuffer = Buffer.from('fake-image-data');
 71      const screenshots = {
 72        desktop_above: fakeBuffer,
 73        desktop_below: fakeBuffer,
 74        mobile_above: fakeBuffer,
 75      };
 76  
 77      const screenshotPath = await saveScreenshots(siteId, screenshots);
 78      assert.equal(screenshotPath, `screenshots/${siteId}`);
 79    });
 80  
 81    test('only writes provided screenshot types', async () => {
 82      const siteId = 1002;
 83      const fakeBuffer = Buffer.from('only-desktop');
 84      const screenshots = { desktop_above: fakeBuffer };
 85  
 86      const screenshotPath = await saveScreenshots(siteId, screenshots);
 87      assert.ok(screenshotPath.includes(String(siteId)));
 88    });
 89  
 90    test('handles empty screenshots object', async () => {
 91      const siteId = 1003;
 92      const screenshotPath = await saveScreenshots(siteId, {});
 93      assert.equal(screenshotPath, `screenshots/${siteId}`);
 94    });
 95  });
 96  
 97  // ─── loadScreenshots ──────────────────────────────────────────────────────────
 98  
 99  describe('loadScreenshots', () => {
100    const siteId = 2001;
101    const screenshotPath = `screenshots/${siteId}`;
102  
103    before(async () => {
104      const fakeBuffer = Buffer.from('load-test-image');
105      await saveScreenshots(siteId, {
106        desktop_above: fakeBuffer,
107        mobile_above: fakeBuffer,
108        desktop_above_uncropped: fakeBuffer,
109      });
110    });
111  
112    test('throws if screenshotPath is null', async () => {
113      await assert.rejects(() => loadScreenshots(null), /Screenshot path is required/);
114    });
115  
116    test('throws if screenshotPath is empty string', async () => {
117      await assert.rejects(() => loadScreenshots(''), /Screenshot path is required/);
118    });
119  
120    test('loads existing cropped screenshots', async () => {
121      const result = await loadScreenshots(screenshotPath);
122      assert.ok(result.desktop_above instanceof Buffer);
123      assert.ok(result.mobile_above instanceof Buffer);
124    });
125  
126    test('missing cropped files are not included in result', async () => {
127      // desktop_below was not saved for siteId 2001
128      const result = await loadScreenshots(screenshotPath);
129      assert.equal(result.desktop_below, undefined);
130    });
131  
132    test('loads uncropped screenshots when includeUncropped=true', async () => {
133      const result = await loadScreenshots(screenshotPath, { includeUncropped: true });
134      assert.ok(result.desktop_above_uncropped instanceof Buffer);
135    });
136  
137    test('excludes uncropped screenshots by default', async () => {
138      const result = await loadScreenshots(screenshotPath);
139      assert.equal(result.desktop_above_uncropped, undefined);
140    });
141  });
142  
143  // ─── loadScreenshot ───────────────────────────────────────────────────────────
144  
145  describe('loadScreenshot', () => {
146    const siteId = 3001;
147    const screenshotPath = `screenshots/${siteId}`;
148  
149    before(async () => {
150      await saveScreenshots(siteId, { desktop_above: Buffer.from('single-load-test') });
151    });
152  
153    test('throws if screenshotPath is null', () => {
154      assert.throws(() => loadScreenshot(null, 'desktop_above'), /Screenshot path is required/);
155    });
156  
157    test('throws for invalid screenshot type', () => {
158      assert.throws(() => loadScreenshot(screenshotPath, 'invalid_type'), /Invalid screenshot type/);
159    });
160  
161    test('loads a valid screenshot', async () => {
162      const result = await loadScreenshot(screenshotPath, 'desktop_above');
163      assert.ok(result instanceof Buffer);
164      assert.equal(result.toString(), 'single-load-test');
165    });
166  
167    test('throws ENOENT for non-existent screenshot', async () => {
168      await assert.rejects(
169        () => loadScreenshot(screenshotPath, 'mobile_above'),
170        err => err.code === 'ENOENT'
171      );
172    });
173  });
174  
175  // ─── screenshotsExist ────────────────────────────────────────────────────────
176  
177  describe('screenshotsExist', () => {
178    test('returns false when screenshotPath is null', async () => {
179      const result = await screenshotsExist(null);
180      assert.equal(result, false);
181    });
182  
183    test('returns false when screenshotPath is empty string', async () => {
184      const result = await screenshotsExist('');
185      assert.equal(result, false);
186    });
187  
188    test('returns true when desktop_above exists', async () => {
189      const siteId = 4001;
190      await saveScreenshots(siteId, { desktop_above: Buffer.from('exists-test') });
191      const result = await screenshotsExist(`screenshots/${siteId}`);
192      assert.equal(result, true);
193    });
194  
195    test('returns false when screenshots do not exist', async () => {
196      const result = await screenshotsExist('screenshots/99999');
197      assert.equal(result, false);
198    });
199  });
200  
201  // ─── croppedScreenshotsExist ─────────────────────────────────────────────────
202  
203  describe('croppedScreenshotsExist', () => {
204    test('returns exists=false and all 3 missing when path is null', async () => {
205      const result = await croppedScreenshotsExist(null);
206      assert.equal(result.exists, false);
207      assert.equal(result.missing.length, 3);
208    });
209  
210    test('returns exists=true when all 3 cropped screenshots present', async () => {
211      const siteId = 5001;
212      const buf = Buffer.from('all-cropped');
213      await saveScreenshots(siteId, {
214        desktop_above: buf,
215        desktop_below: buf,
216        mobile_above: buf,
217      });
218      const result = await croppedScreenshotsExist(`screenshots/${siteId}`);
219      assert.equal(result.exists, true);
220      assert.equal(result.missing.length, 0);
221    });
222  
223    test('reports which screenshots are missing', async () => {
224      const siteId = 5002;
225      await saveScreenshots(siteId, { desktop_above: Buffer.from('partial') });
226      const result = await croppedScreenshotsExist(`screenshots/${siteId}`);
227      assert.equal(result.exists, false);
228      assert.ok(result.missing.includes('desktop_below'));
229      assert.ok(result.missing.includes('mobile_above'));
230      assert.ok(!result.missing.includes('desktop_above'));
231    });
232  });
233  
234  // ─── allScreenshotsExist ─────────────────────────────────────────────────────
235  
236  describe('allScreenshotsExist', () => {
237    test('returns exists=false and all 6 missing when path is null', async () => {
238      const result = await allScreenshotsExist(null);
239      assert.equal(result.exists, false);
240      assert.equal(result.missing.length, 6);
241    });
242  
243    test('returns exists=true when all 6 screenshots present', async () => {
244      const siteId = 6001;
245      const buf = Buffer.from('all-six');
246      await saveScreenshots(siteId, {
247        desktop_above: buf,
248        desktop_below: buf,
249        mobile_above: buf,
250        desktop_above_uncropped: buf,
251        desktop_below_uncropped: buf,
252        mobile_above_uncropped: buf,
253      });
254      const result = await allScreenshotsExist(`screenshots/${siteId}`);
255      assert.equal(result.exists, true);
256      assert.equal(result.missing.length, 0);
257    });
258  
259    test('reports missing uncropped screenshots', async () => {
260      const siteId = 6002;
261      const buf = Buffer.from('only-cropped');
262      await saveScreenshots(siteId, {
263        desktop_above: buf,
264        desktop_below: buf,
265        mobile_above: buf,
266      });
267      const result = await allScreenshotsExist(`screenshots/${siteId}`);
268      assert.equal(result.exists, false);
269      assert.ok(result.missing.includes('desktop_above_uncropped'));
270      assert.ok(result.missing.includes('mobile_above_uncropped'));
271    });
272  });
273  
274  // ─── deleteScreenshots ────────────────────────────────────────────────────────
275  
276  describe('deleteScreenshots', () => {
277    test('does nothing when screenshotPath is null', async () => {
278      await assert.doesNotReject(() => deleteScreenshots(null));
279    });
280  
281    test('does nothing when screenshotPath is empty string', async () => {
282      await assert.doesNotReject(() => deleteScreenshots(''));
283    });
284  
285    test('deletes existing screenshots directory', async () => {
286      const siteId = 7001;
287      await saveScreenshots(siteId, { desktop_above: Buffer.from('to-delete') });
288  
289      // Verify exists first
290      const before = await screenshotsExist(`screenshots/${siteId}`);
291      assert.equal(before, true);
292  
293      await deleteScreenshots(`screenshots/${siteId}`);
294  
295      const after = await screenshotsExist(`screenshots/${siteId}`);
296      assert.equal(after, false);
297    });
298  
299    test('does not throw for non-existent path', async () => {
300      await assert.doesNotReject(() => deleteScreenshots('screenshots/99999'));
301    });
302  });