diff --git a/app/capture.js b/app/capture.js index 7100f02..9ef6467 100644 --- a/app/capture.js +++ b/app/capture.js @@ -24,6 +24,7 @@ const { encodePng } = require('../core/png'); const CLICK_DEBOUNCE_MS = 700; const CLICK_CAPTURE_CACHE_MS = 75; +const CLICK_CAPTURE_CACHE_MAX_AGE_MS = 400; const CLICK_CAPTURE_HIDE_DELAY_MS = 25; function hasBinary(name) { @@ -257,9 +258,13 @@ class CaptureService { try { const mode = this.settings.get('capture.mode') || 'fullscreen'; const grabMode = mode === 'region' ? 'fullscreen' : mode; - const cached = trigger === 'click' && this.captureCache && this.captureCache.mode === grabMode - ? this.captureCache - : null; + // The background refresh loop (startClickCaptureCache) keeps this + // updated every ~75ms; if it's gone stale (refresh errors silently and + // stops updating, or the cache was never refreshed after a resume), + // fall back to a fresh shot rather than reusing an old background. + const cacheFresh = this.captureCache && this.captureCache.mode === grabMode + && Date.now() - this.captureCache.capturedAt <= CLICK_CAPTURE_CACHE_MAX_AGE_MS; + const cached = trigger === 'click' && cacheFresh ? this.captureCache : null; const finalResult = cached ? this.storeFrameAsStep(this.session.guideId, grabMode, cached, clickPos) : await this.shoot({ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b2369c7..01a0073 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -92,10 +92,12 @@ Initial release. literal text "undefined" by an old bug); a corrupted file is now treated as empty instead of crashing the dialog, and is overwritten with valid JSON the next time settings are saved. -- Resuming a paused capture session no longer reuses the same stale - background screenshot for every click capture (only the click marker - moved); pausing now fully resets the click-capture cache so resuming - starts a fresh background refresh loop. +- Click captures no longer reuse the same stale background screenshot + for every step (only the click marker moved). Pausing now fully resets + the click-capture cache so resuming starts a fresh background refresh + loop, and a cached frame older than 400ms (e.g. if the background + refresh silently stops working) is now discarded in favor of a fresh + screenshot. ### Added (initial feature set) diff --git a/tests/unit/capture.test.js b/tests/unit/capture.test.js index 2ba3581..6c475e5 100644 --- a/tests/unit/capture.test.js +++ b/tests/unit/capture.test.js @@ -143,6 +143,33 @@ test('click-triggered capture marks the click-time cursor position, not the cach assert.ok(Math.abs(marker.y - (0.5 - (d * 120 / 80) / 2)) < 1e-9); }); +test('click-triggered session capture falls back to a fresh shot when the cached frame is stale', async () => { + const service = makeService(); + service.session = { guideId: 'guide-stale', paused: false, count: 0, intervalSec: 0 }; + // A frame that's well past the cache's max age — e.g. the background + // refresh loop died (errored silently, or never restarted after a + // pause/resume) and left a frozen, increasingly-stale frame behind. + service.captureCache = { + mode: 'fullscreen', + png: Buffer.from('stale-png'), + size: { width: 120, height: 80 }, + display: { bounds: { x: 0, y: 0, width: 120, height: 80 } }, + cursor: { x: 60, y: 40 }, + capturedAt: Date.now() - 10_000, + }; + + let shootCalled = false; + service.shoot = async () => { + shootCalled = true; + return { ok: true, step: { stepId: 'fresh-step' } }; + }; + + const result = await service.sessionCapture('click', { x: 1, y: 1 }); + + assert.equal(result.ok, true); + assert.equal(shootCalled, true, 'a stale cached frame must not be reused'); +}); + test('live-shot click capture also marks the click-time cursor position', async () => { const service = makeService(); service.settings.get = (key) => {