/ tests / utils / backfill-screenshots.test.js
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  });