/ tests / capture / screenshot-storage-supplement.test.js
screenshot-storage-supplement.test.js
  1  /**
  2   * Screenshot Storage Supplement - Coverage for uncovered paths
  3   *
  4   * Targets:
  5   *   - Lines 94-95: non-ENOENT error in loadScreenshots (cropped files)
  6   *   - Lines 114-117: non-ENOENT error in loadScreenshots (uncropped files)
  7   *   - Lines 176-202: croppedScreenshotsExist function (entirely uncovered)
  8   *   - Lines 251-255: non-ENOENT error in deleteScreenshots
  9   *
 10   * CRITICAL: Do NOT use mock.module() - it causes V8 coverage interference.
 11   * Use SCREENSHOT_BASE_PATH env var set BEFORE import to control the base dir.
 12   */
 13  
 14  import { test, describe } from 'node:test';
 15  import assert from 'node:assert/strict';
 16  import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
 17  import { tmpdir } from 'os';
 18  import { join } from 'path';
 19  
 20  // Create a temp base directory for screenshots
 21  const tempBase = mkdtempSync(join(tmpdir(), 'screenshot-storage-sup-'));
 22  
 23  // Must set before import so module picks it up
 24  process.env.SCREENSHOT_BASE_PATH = tempBase;
 25  
 26  const { loadScreenshots, croppedScreenshotsExist, deleteScreenshots } =
 27    await import('../../src/utils/screenshot-storage.js');
 28  
 29  // Helper to create a fake screenshot directory with real files
 30  function createFakeSite(siteId, files = []) {
 31    const siteDir = join(tempBase, siteId.toString());
 32    mkdirSync(siteDir, { recursive: true });
 33    for (const filename of files) {
 34      writeFileSync(join(siteDir, filename), Buffer.from('fake-img-data'));
 35    }
 36    return `screenshots/${siteId}`;
 37  }
 38  
 39  // ======================================================
 40  // loadScreenshots - non-ENOENT error path (lines 94-95)
 41  // ======================================================
 42  
 43  describe('loadScreenshots - non-ENOENT error in cropped files', () => {
 44    test('throws non-ENOENT errors from cropped file reads', async () => {
 45      // Create a siteDir where desktop_above.jpg is a directory, not a file
 46      // This causes EISDIR when readFile is called on it
 47      const siteId = 'err-cropped-1';
 48      const siteDir = join(tempBase, siteId);
 49      mkdirSync(siteDir, { recursive: true });
 50      // Create desktop_above.jpg as a directory to force a non-ENOENT error
 51      mkdirSync(join(siteDir, 'desktop_above.jpg'), { recursive: true });
 52  
 53      const screenshotPath = `screenshots/${siteId}`;
 54      await assert.rejects(
 55        async () => await loadScreenshots(screenshotPath),
 56        err => {
 57          // Should throw EISDIR (or similar non-ENOENT FS error)
 58          assert.ok(err.code !== 'ENOENT', `Expected non-ENOENT error but got: ${err.code}`);
 59          return true;
 60        }
 61      );
 62    });
 63  });
 64  
 65  // ======================================================
 66  // loadScreenshots - non-ENOENT error in uncropped files (lines 114-117)
 67  // ======================================================
 68  
 69  describe('loadScreenshots - non-ENOENT error in uncropped files', () => {
 70    test('throws non-ENOENT errors from uncropped file reads', async () => {
 71      // Create a siteDir where a cropped file exists, but uncropped file is a directory
 72      const siteId = 'err-uncropped-1';
 73      const siteDir = join(tempBase, siteId);
 74      mkdirSync(siteDir, { recursive: true });
 75      // Provide valid cropped files
 76      writeFileSync(join(siteDir, 'desktop_above.jpg'), Buffer.from('img'));
 77      writeFileSync(join(siteDir, 'desktop_below.jpg'), Buffer.from('img'));
 78      writeFileSync(join(siteDir, 'mobile_above.jpg'), Buffer.from('img'));
 79      // Make uncropped file a directory (forces EISDIR read error)
 80      mkdirSync(join(siteDir, 'desktop_above_uncropped.jpg'), { recursive: true });
 81  
 82      const screenshotPath = `screenshots/${siteId}`;
 83      await assert.rejects(
 84        async () => await loadScreenshots(screenshotPath, { includeUncropped: true }),
 85        err => {
 86          assert.ok(err.code !== 'ENOENT', `Expected non-ENOENT error but got: ${err.code}`);
 87          return true;
 88        }
 89      );
 90    });
 91  
 92    test('ENOENT in uncropped files is silently ignored', async () => {
 93      // Cropped files present, uncropped files simply missing (ENOENT) - should not throw
 94      const siteId = 'uncropped-enoent-1';
 95      const siteDir = join(tempBase, siteId);
 96      mkdirSync(siteDir, { recursive: true });
 97      writeFileSync(join(siteDir, 'desktop_above.jpg'), Buffer.from('img'));
 98      writeFileSync(join(siteDir, 'desktop_below.jpg'), Buffer.from('img'));
 99      writeFileSync(join(siteDir, 'mobile_above.jpg'), Buffer.from('img'));
100      // No uncropped files - should just silently skip them
101  
102      const screenshotPath = `screenshots/${siteId}`;
103      const result = await loadScreenshots(screenshotPath, { includeUncropped: true });
104      assert.ok(result.desktop_above, 'Should have desktop_above');
105      assert.equal(
106        result.desktop_above_uncropped,
107        undefined,
108        'Uncropped should be undefined when missing'
109      );
110    });
111  });
112  
113  // ======================================================
114  // croppedScreenshotsExist - entirely uncovered (lines 176-202)
115  // ======================================================
116  
117  describe('croppedScreenshotsExist', () => {
118    test('returns exists=false with all three missing when screenshotPath is null', async () => {
119      const result = await croppedScreenshotsExist(null);
120      assert.equal(result.exists, false);
121      assert.deepEqual(result.missing, ['desktop_above', 'desktop_below', 'mobile_above']);
122    });
123  
124    test('returns exists=false with all three missing when screenshotPath is empty string', async () => {
125      const result = await croppedScreenshotsExist('');
126      assert.equal(result.exists, false);
127      assert.deepEqual(result.missing, ['desktop_above', 'desktop_below', 'mobile_above']);
128    });
129  
130    test('returns exists=true when all three cropped screenshots are present', async () => {
131      const screenshotPath = createFakeSite('cropped-exist-1', [
132        'desktop_above.jpg',
133        'desktop_below.jpg',
134        'mobile_above.jpg',
135      ]);
136      const result = await croppedScreenshotsExist(screenshotPath);
137      assert.equal(result.exists, true);
138      assert.deepEqual(result.missing, []);
139    });
140  
141    test('returns exists=false when desktop_below is missing', async () => {
142      const screenshotPath = createFakeSite('cropped-exist-2', [
143        'desktop_above.jpg',
144        'mobile_above.jpg',
145        // No desktop_below.jpg
146      ]);
147      const result = await croppedScreenshotsExist(screenshotPath);
148      assert.equal(result.exists, false);
149      assert.ok(result.missing.includes('desktop_below'));
150    });
151  
152    test('returns exists=false when all three cropped files are missing', async () => {
153      const screenshotPath = createFakeSite('cropped-exist-3', [
154        // Only uncropped files present, no cropped
155        'desktop_above_uncropped.jpg',
156      ]);
157      const result = await croppedScreenshotsExist(screenshotPath);
158      assert.equal(result.exists, false);
159      assert.equal(result.missing.length, 3);
160    });
161  
162    test('returns exists=false when only mobile_above is missing', async () => {
163      const screenshotPath = createFakeSite('cropped-exist-4', [
164        'desktop_above.jpg',
165        'desktop_below.jpg',
166        // No mobile_above.jpg
167      ]);
168      const result = await croppedScreenshotsExist(screenshotPath);
169      assert.equal(result.exists, false);
170      assert.deepEqual(result.missing, ['mobile_above']);
171    });
172  
173    test('returns exists=false when only desktop_above is missing', async () => {
174      const screenshotPath = createFakeSite('cropped-exist-5', [
175        'desktop_below.jpg',
176        'mobile_above.jpg',
177        // No desktop_above.jpg
178      ]);
179      const result = await croppedScreenshotsExist(screenshotPath);
180      assert.equal(result.exists, false);
181      assert.deepEqual(result.missing, ['desktop_above']);
182    });
183  
184    test('ignores uncropped files - only checks 3 essential cropped', async () => {
185      // Only uncropped files present, none of the 3 cropped ones
186      const screenshotPath = createFakeSite('cropped-exist-6', [
187        'desktop_above_uncropped.jpg',
188        'desktop_below_uncropped.jpg',
189        'mobile_above_uncropped.jpg',
190      ]);
191      const result = await croppedScreenshotsExist(screenshotPath);
192      assert.equal(result.exists, false);
193      assert.equal(result.missing.length, 3);
194    });
195  
196    test('extracts site ID from path correctly for nested-style paths', async () => {
197      // screenshotPath like "screenshots/99" -> siteId "99"
198      const screenshotPath = createFakeSite('99', [
199        'desktop_above.jpg',
200        'desktop_below.jpg',
201        'mobile_above.jpg',
202      ]);
203      assert.equal(screenshotPath, 'screenshots/99');
204      const result = await croppedScreenshotsExist(screenshotPath);
205      assert.equal(result.exists, true);
206    });
207  });
208  
209  // ======================================================
210  // deleteScreenshots - non-ENOENT throw path (lines 251-255)
211  // ======================================================
212  
213  describe('deleteScreenshots - non-ENOENT error path', () => {
214    test('does not throw when ENOENT (directory does not exist)', async () => {
215      // A path pointing to a non-existent directory - should not throw
216      const screenshotPath = 'screenshots/nonexistent-999';
217      await assert.doesNotReject(async () => await deleteScreenshots(screenshotPath));
218    });
219  
220    test('returns early for null screenshotPath', async () => {
221      await assert.doesNotReject(async () => await deleteScreenshots(null));
222    });
223  
224    test('returns early for empty screenshotPath', async () => {
225      await assert.doesNotReject(async () => await deleteScreenshots(''));
226    });
227  
228    test('successfully deletes existing directory', async () => {
229      const screenshotPath = createFakeSite('delete-test-1', ['desktop_above.jpg']);
230      // Directory should exist before
231      const siteDir = join(tempBase, 'delete-test-1');
232      const { existsSync } = await import('fs');
233      assert.ok(existsSync(siteDir), 'Directory should exist before delete');
234  
235      await deleteScreenshots(screenshotPath);
236  
237      assert.ok(!existsSync(siteDir), 'Directory should not exist after delete');
238    });
239  });
240  
241  // Cleanup temp directory at end
242  process.on('exit', () => {
243    try {
244      rmSync(tempBase, { recursive: true, force: true });
245    } catch (_) {
246      // ignore
247    }
248  });