Fix new-capture auto-hide, library capture bar, step delete, and click-capture timing
Template tests / tests (push) Successful in 1m49s
Template tests / tests (push) Successful in 1m49s
- New Capture sessions now start paused; the window only tucks away once the user clicks "Start recording" in the capture bar instead of hiding ~1.2s after starting. - The capture status bar is shown only in the editor view, not over the library. - Fix openModal/confirmDialog resolving as cancelled when an action button is clicked, which made the step "Delete" button (and other modal actions) silently no-op. - Click-triggered captures now use the click-time cursor position for the marker and arm the capture cache as soon as recording starts, so the first click is captured instantly and accurately placed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,7 @@ test('click-triggered session capture uses the low-latency hide pause', async ()
|
||||
delayMs: 0,
|
||||
hideWindowDelayMs: 25,
|
||||
refocus: false,
|
||||
clickPos: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,3 +96,125 @@ test('click-triggered session capture prefers the cached frame when ready', asyn
|
||||
assert.equal(added[0].fields.annotations.length, 1);
|
||||
assert.equal(added[0].fields.annotations[0].type, 'oval');
|
||||
});
|
||||
|
||||
test('click-triggered capture marks the click-time cursor position, not the cached frame\'s (possibly stale) cursor', async () => {
|
||||
const service = makeService();
|
||||
service.settings.get = (key) => {
|
||||
if (key === 'capture.mode') return 'fullscreen';
|
||||
if (key === 'capture.delayMs') return 0;
|
||||
if (key === 'capture.clickMarker') return true;
|
||||
if (key === 'capture.clickMarkerColor') return '#E5484D';
|
||||
if (key === 'editor.focusedViewDefaultForNewSteps') return false;
|
||||
return null;
|
||||
};
|
||||
service.session = { guideId: 'guide-3', paused: false, count: 0, intervalSec: 0 };
|
||||
service.captureCache = {
|
||||
mode: 'fullscreen',
|
||||
png: Buffer.from('cached-png'),
|
||||
size: { width: 120, height: 80 },
|
||||
display: { bounds: { x: 0, y: 0, width: 120, height: 80 } },
|
||||
// Stale cursor position from the cache-refresh loop, well outside the
|
||||
// display — if this were used for the marker, no annotation would be
|
||||
// placed at all.
|
||||
cursor: { x: 9999, y: 9999 },
|
||||
capturedAt: Date.now(),
|
||||
};
|
||||
|
||||
service.shoot = async () => {
|
||||
throw new Error('fresh shot should not run when cache is ready');
|
||||
};
|
||||
|
||||
let added = null;
|
||||
service.store.addStep = (guideId, fields, png, size) => {
|
||||
added = { guideId, fields, png, size };
|
||||
return { stepId: 'step-3', ...fields };
|
||||
};
|
||||
service.notify = () => {};
|
||||
|
||||
// The user clicked dead center of the display.
|
||||
const result = await service.sessionCapture('click', { x: 60, y: 40 });
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(added.fields.annotations.length, 1);
|
||||
const marker = added.fields.annotations[0];
|
||||
assert.equal(marker.type, 'oval');
|
||||
const d = 0.035;
|
||||
assert.ok(Math.abs(marker.x - (0.5 - d / 2)) < 1e-9);
|
||||
assert.ok(Math.abs(marker.y - (0.5 - (d * 120 / 80) / 2)) < 1e-9);
|
||||
});
|
||||
|
||||
test('live-shot click capture also marks the click-time cursor position', async () => {
|
||||
const service = makeService();
|
||||
service.settings.get = (key) => {
|
||||
if (key === 'capture.mode') return 'fullscreen';
|
||||
if (key === 'capture.delayMs') return 0;
|
||||
if (key === 'capture.clickMarker') return true;
|
||||
if (key === 'capture.clickMarkerColor') return '#E5484D';
|
||||
if (key === 'editor.focusedViewDefaultForNewSteps') return false;
|
||||
return null;
|
||||
};
|
||||
service.session = { guideId: 'guide-4', paused: false, count: 0, intervalSec: 0 };
|
||||
// No capture cache, so sessionCapture falls back to a fresh shoot().
|
||||
service.captureCurrentFrame = async () => ({
|
||||
mode: 'fullscreen',
|
||||
png: Buffer.from('live-png'),
|
||||
size: { width: 100, height: 100 },
|
||||
display: { bounds: { x: 0, y: 0, width: 100, height: 100 } },
|
||||
// Grab-time cursor, well outside the display — must not be used.
|
||||
cursor: { x: -1, y: -1 },
|
||||
capturedAt: Date.now(),
|
||||
});
|
||||
|
||||
let added = null;
|
||||
service.store.addStep = (guideId, fields, png, size) => {
|
||||
added = { guideId, fields, png, size };
|
||||
return { stepId: 'step-4', ...fields };
|
||||
};
|
||||
service.notify = () => {};
|
||||
|
||||
const result = await service.sessionCapture('click', { x: 50, y: 50 });
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(added.fields.annotations.length, 1);
|
||||
assert.equal(added.fields.annotations[0].type, 'oval');
|
||||
});
|
||||
|
||||
test('a new session starts paused and does not hide the window or arm the click cache until "Start recording" is pressed', async () => {
|
||||
const service = makeService();
|
||||
const win = {
|
||||
destroyed: false, visible: true, minimized: false, hidden: 0, shown: 0,
|
||||
isDestroyed() { return this.destroyed; },
|
||||
isVisible() { return this.visible; },
|
||||
isMinimized() { return this.minimized; },
|
||||
hide() { this.visible = false; this.hidden += 1; },
|
||||
show() { this.visible = true; this.shown += 1; },
|
||||
showInactive() { this.visible = true; this.shown += 1; },
|
||||
focus() {},
|
||||
getTitle() { return 'StepForge'; },
|
||||
getBounds() { return { x: 0, y: 0, width: 800, height: 600 }; },
|
||||
};
|
||||
service.getWindow = () => win;
|
||||
service.clickCaptureAvailable = () => true;
|
||||
let cacheStarted = 0;
|
||||
service.startClickCaptureCache = () => { cacheStarted += 1; };
|
||||
|
||||
try {
|
||||
service.startSession('guide-5');
|
||||
|
||||
assert.equal(service.session.paused, true, 'sessions start paused');
|
||||
assert.equal(service.state().paused, true);
|
||||
assert.equal(win.hidden, 0, 'window must stay visible until recording starts');
|
||||
assert.equal(cacheStarted, 0, 'click-capture cache must not start before recording starts');
|
||||
|
||||
// User clicks "Start recording" (the resume action).
|
||||
service.togglePause(false);
|
||||
assert.equal(service.session.paused, false);
|
||||
assert.equal(win.hidden, 0, 'hide is deferred briefly so the user sees it happen');
|
||||
|
||||
await new Promise((r) => setTimeout(r, 450));
|
||||
assert.equal(win.hidden, 1, 'window hides once recording actually starts');
|
||||
assert.equal(cacheStarted, 1, 'click-capture cache is armed once recording starts');
|
||||
} finally {
|
||||
service.finishSession();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user