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 });