backfill-screenshots.test.js
1 /** 2 * Tests for src/utils/backfill-screenshots.js 3 * 4 * Tests the synchronous DB-query exports: 5 * - findSitesWithMissingScreenshots 6 * - updateSiteScreenshots 7 * 8 * backfillScreenshots() requires Playwright (browser) — not tested here. 9 * Source functions accept db directly — no pg-mock needed. 10 */ 11 12 import { test, describe, mock } from 'node:test'; 13 import assert from 'node:assert/strict'; 14 import Database from 'better-sqlite3'; 15 import { createPgMock } from '../helpers/pg-mock.js'; 16 17 // backfill-screenshots.js receives db as a parameter — mock db.js so any 18 // transitive imports resolve cleanly against the same in-memory instance. 19 const _sharedDb = new Database(':memory:'); 20 mock.module('../../src/utils/db.js', { namedExports: createPgMock(_sharedDb) }); 21 22 const { 23 findSitesWithMissingScreenshots, 24 updateSiteScreenshots, 25 } = await import('../../src/utils/backfill-screenshots.js'); 26 27 function createDb() { 28 const db = new Database(':memory:'); 29 db.exec(` 30 CREATE TABLE IF NOT EXISTS sites ( 31 id INTEGER PRIMARY KEY AUTOINCREMENT, 32 domain TEXT NOT NULL, 33 landing_page_url TEXT, 34 keyword TEXT, 35 status TEXT DEFAULT 'prog_scored', 36 screenshot_above_desktop BLOB, 37 screenshot_below_desktop BLOB, 38 screenshot_above_mobile BLOB, 39 screenshot_above_desktop_uncropped BLOB, 40 screenshot_below_desktop_uncropped BLOB, 41 screenshot_above_mobile_uncropped BLOB, 42 created_at TEXT DEFAULT CURRENT_TIMESTAMP, 43 updated_at TEXT DEFAULT CURRENT_TIMESTAMP, 44 rescored_at DATETIME 45 ) 46 `); 47 return db; 48 } 49 50 function insertSite(db, { 51 domain, 52 hasDesktopAbove = false, 53 hasDesktopBelow = false, 54 hasMobileAbove = false, 55 }) { 56 const fakeBlob = Buffer.from('fake-screenshot'); 57 db.prepare( 58 `INSERT INTO sites (domain, landing_page_url, keyword, 59 screenshot_above_desktop, screenshot_below_desktop, screenshot_above_mobile, 60 screenshot_above_desktop_uncropped, screenshot_below_desktop_uncropped, screenshot_above_mobile_uncropped) 61 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` 62 ).run( 63 domain, 64 `https://${domain}`, 65 'plumber', 66 hasDesktopAbove ? fakeBlob : null, 67 hasDesktopBelow ? fakeBlob : null, 68 hasMobileAbove ? fakeBlob : null, 69 hasDesktopAbove ? fakeBlob : null, 70 hasDesktopBelow ? fakeBlob : null, 71 hasMobileAbove ? fakeBlob : null 72 ); 73 } 74 75 // ─── findSitesWithMissingScreenshots ───────────────────────────────────────── 76 77 describe('findSitesWithMissingScreenshots', () => { 78 test('returns empty array when all sites have screenshots', () => { 79 const db = createDb(); 80 insertSite(db, { 81 domain: 'complete.com', 82 hasDesktopAbove: true, 83 hasDesktopBelow: true, 84 hasMobileAbove: true, 85 }); 86 87 const result = findSitesWithMissingScreenshots(db); 88 assert.equal(result.length, 0); 89 db.close(); 90 }); 91 92 test('returns sites missing desktop above screenshot', () => { 93 const db = createDb(); 94 insertSite(db, { domain: 'missing-desktop.com', hasDesktopBelow: true, hasMobileAbove: true }); 95 96 const result = findSitesWithMissingScreenshots(db); 97 assert.equal(result.length, 1); 98 assert.equal(result[0].domain, 'missing-desktop.com'); 99 db.close(); 100 }); 101 102 test('returns sites with all screenshots missing', () => { 103 const db = createDb(); 104 insertSite(db, { domain: 'no-screenshots.com' }); 105 106 const result = findSitesWithMissingScreenshots(db); 107 assert.equal(result.length, 1); 108 db.close(); 109 }); 110 111 test('returns multiple sites with missing screenshots', () => { 112 const db = createDb(); 113 insertSite(db, { domain: 'missing1.com' }); 114 insertSite(db, { domain: 'missing2.com' }); 115 insertSite(db, { 116 domain: 'complete.com', 117 hasDesktopAbove: true, 118 hasDesktopBelow: true, 119 hasMobileAbove: true, 120 }); 121 122 const result = findSitesWithMissingScreenshots(db); 123 assert.equal(result.length, 2); 124 db.close(); 125 }); 126 127 test('respects the limit parameter', () => { 128 const db = createDb(); 129 insertSite(db, { domain: 'site1.com' }); 130 insertSite(db, { domain: 'site2.com' }); 131 insertSite(db, { domain: 'site3.com' }); 132 133 const result = findSitesWithMissingScreenshots(db, 2); 134 assert.equal(result.length, 2); 135 db.close(); 136 }); 137 138 test('result includes missing_desktop_above field', () => { 139 const db = createDb(); 140 insertSite(db, { domain: 'partial.com', hasDesktopBelow: true }); 141 142 const result = findSitesWithMissingScreenshots(db); 143 assert.equal(result.length, 1); 144 assert.ok('missing_desktop_above' in result[0]); 145 db.close(); 146 }); 147 }); 148 149 // ─── updateSiteScreenshots ──────────────────────────────────────────────────── 150 151 describe('updateSiteScreenshots', () => { 152 test('updates screenshots for a site', () => { 153 const db = createDb(); 154 insertSite(db, { domain: 'update-target.com' }); 155 156 const site = db.prepare('SELECT id FROM sites WHERE domain = ?').get('update-target.com'); 157 const fakeShot = Buffer.from('screenshot-data'); 158 159 updateSiteScreenshots( 160 db, 161 site.id, 162 { desktop_above: fakeShot, desktop_below: fakeShot, mobile_above: fakeShot }, 163 { desktop_above: fakeShot, desktop_below: fakeShot, mobile_above: fakeShot } 164 ); 165 166 const updated = db.prepare('SELECT * FROM sites WHERE id = ?').get(site.id); 167 assert.ok(updated.screenshot_above_desktop !== null, 'desktop_above should be set'); 168 assert.ok(updated.screenshot_below_desktop !== null, 'desktop_below should be set'); 169 assert.ok(updated.screenshot_above_mobile !== null, 'mobile_above should be set'); 170 db.close(); 171 }); 172 173 test('sets null for missing screenshot fields', () => { 174 const db = createDb(); 175 insertSite(db, { domain: 'null-target.com' }); 176 177 const site = db.prepare('SELECT id FROM sites WHERE domain = ?').get('null-target.com'); 178 179 updateSiteScreenshots( 180 db, 181 site.id, 182 { desktop_above: null, desktop_below: null, mobile_above: null }, 183 { desktop_above: null, desktop_below: null, mobile_above: null } 184 ); 185 186 const updated = db.prepare('SELECT * FROM sites WHERE id = ?').get(site.id); 187 assert.equal(updated.screenshot_above_desktop, null); 188 db.close(); 189 }); 190 191 test('does not throw for non-existent site ID', () => { 192 const db = createDb(); 193 assert.doesNotThrow(() => { 194 updateSiteScreenshots(db, 999999, { desktop_above: null }, { desktop_above: null }); 195 }); 196 db.close(); 197 }); 198 });