This commit is contained in:
+36
-1
@@ -103,11 +103,19 @@ function hasBinary(name) {
|
||||
}
|
||||
|
||||
class CaptureService {
|
||||
constructor({ store, settings, getWindow, notify, screenApi = screen }) {
|
||||
constructor({
|
||||
store,
|
||||
settings,
|
||||
getWindow,
|
||||
notify,
|
||||
screenApi = screen,
|
||||
dialogApi = null,
|
||||
}) {
|
||||
this.store = store;
|
||||
this.settings = settings;
|
||||
this.getWindow = getWindow;
|
||||
this.notify = notify;
|
||||
this.dialog = dialogApi;
|
||||
// Injectable for tests; the click/coordinate paths must never reach for
|
||||
// the global `screen` directly so coordinate handling stays testable.
|
||||
this.screen = screenApi;
|
||||
@@ -134,6 +142,7 @@ class CaptureService {
|
||||
// True only while a resume is warming up (window still visible, buffer
|
||||
// not yet primed). Clicks are ignored until it clears — see armRecording.
|
||||
this.warmingUp = false;
|
||||
this.sessionInstructionsShown = false;
|
||||
}
|
||||
|
||||
state() {
|
||||
@@ -185,6 +194,7 @@ class CaptureService {
|
||||
// New Capture never makes the window vanish out from under them.
|
||||
this.session = { guideId, paused: true, count: 0, intervalSec: interval };
|
||||
this.sessionNotificationShown = false;
|
||||
this.sessionInstructionsShown = false;
|
||||
if (this.settings.get('capture.captureOutsideClicks') !== false) this.startClickWatcher();
|
||||
this.applyInterval();
|
||||
this.notify('capture:state', this.state());
|
||||
@@ -375,6 +385,11 @@ class CaptureService {
|
||||
if (!this.session || this.session.paused) { this.warmingUp = false; return; }
|
||||
}
|
||||
if (wantHide && win && !win.isDestroyed() && win.isVisible()) {
|
||||
if (!this.sessionInstructionsShown) {
|
||||
this.sessionInstructionsShown = true;
|
||||
await this.showRecordingInstructions(win);
|
||||
if (!this.session || this.session.paused) { this.warmingUp = false; return; }
|
||||
}
|
||||
win.hide();
|
||||
// Let a couple of frames of the now-unobscured screen land before
|
||||
// the user's first click, so that frame shows their work, not the
|
||||
@@ -396,6 +411,25 @@ class CaptureService {
|
||||
run().catch(() => { this.warmingUp = false; });
|
||||
}
|
||||
|
||||
async showRecordingInstructions(win) {
|
||||
if (!this.dialog || typeof this.dialog.showMessageBox !== 'function') return;
|
||||
try {
|
||||
await this.dialog.showMessageBox(win, {
|
||||
type: 'info',
|
||||
title: 'StepForge recording',
|
||||
message: 'Please go into the tray icon and select the red button to stop recording.',
|
||||
detail: 'Click OK to continue and hide this window.',
|
||||
buttons: ['OK'],
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
} catch {
|
||||
// If the dialog cannot be shown, keep the session moving instead of
|
||||
// leaving the user stuck on the start flow.
|
||||
}
|
||||
}
|
||||
|
||||
finishSession() {
|
||||
if (this.intervalTimer) {
|
||||
clearInterval(this.intervalTimer);
|
||||
@@ -408,6 +442,7 @@ class CaptureService {
|
||||
this.destroySessionTray();
|
||||
this.session = null;
|
||||
this.sessionNotificationShown = false;
|
||||
this.sessionInstructionsShown = false;
|
||||
if (this.hiddenForSession) {
|
||||
this.hiddenForSession = false;
|
||||
this.showWindow();
|
||||
|
||||
@@ -672,6 +672,7 @@ if (!gotLock) {
|
||||
settings,
|
||||
getWindow: () => mainWindow,
|
||||
notify: sendToRenderer,
|
||||
dialogApi: dialog,
|
||||
});
|
||||
|
||||
applyTheme();
|
||||
|
||||
@@ -5,7 +5,7 @@ const assert = require('node:assert/strict');
|
||||
|
||||
const CaptureService = require('../../app/capture');
|
||||
|
||||
function makeService({ settings: settingsOverrides, screenApi } = {}) {
|
||||
function makeService({ settings: settingsOverrides, screenApi, dialogApi } = {}) {
|
||||
const store = {
|
||||
addStep() {
|
||||
throw new Error('not used in this test');
|
||||
@@ -26,6 +26,9 @@ function makeService({ settings: settingsOverrides, screenApi } = {}) {
|
||||
settings,
|
||||
getWindow: () => null,
|
||||
notify: () => {},
|
||||
dialogApi: dialogApi || {
|
||||
showMessageBox: async () => ({ response: 0 }),
|
||||
},
|
||||
screenApi: screenApi || {
|
||||
getCursorScreenPoint: () => ({ x: 0, y: 0 }),
|
||||
getAllDisplays: () => [],
|
||||
@@ -626,6 +629,52 @@ test('armRecording warms while visible, then hides and arms the session', async
|
||||
service.finishSession();
|
||||
});
|
||||
|
||||
test('armRecording shows a blocking instruction dialog before the window hides', async () => {
|
||||
const service = makeService();
|
||||
const win = {
|
||||
destroyed: false, visible: true,
|
||||
isDestroyed() { return this.destroyed; },
|
||||
isVisible() { return this.visible; },
|
||||
isMinimized() { return false; },
|
||||
hide() { this.visible = false; },
|
||||
show() { this.visible = true; },
|
||||
focus() {}, getTitle() { return 'StepForge'; },
|
||||
getBounds() { return { x: 0, y: 0, width: 800, height: 600 }; },
|
||||
};
|
||||
service.getWindow = () => win;
|
||||
service.clickCaptureAvailable = () => false;
|
||||
service.hiddenForSession = true;
|
||||
service.session = { guideId: 'g-prompt', paused: true, count: 0, intervalSec: 0 };
|
||||
|
||||
let releaseDialog;
|
||||
const dialogGate = new Promise((resolve) => { releaseDialog = resolve; });
|
||||
let seenOptions = null;
|
||||
service.dialog = {
|
||||
showMessageBox: async (_win, options) => {
|
||||
seenOptions = options;
|
||||
await dialogGate;
|
||||
return { response: 0 };
|
||||
},
|
||||
};
|
||||
|
||||
service.togglePause(false);
|
||||
|
||||
for (let i = 0; i < 40 && !seenOptions; i++) {
|
||||
await new Promise((r) => setTimeout(r, 25));
|
||||
}
|
||||
assert.ok(seenOptions, 'the instruction dialog must appear before the window hides');
|
||||
assert.equal(seenOptions?.message, 'Please go into the tray icon and select the red button to stop recording.');
|
||||
assert.equal(win.visible, true, 'the window must stay visible until the dialog is acknowledged');
|
||||
|
||||
releaseDialog();
|
||||
for (let i = 0; i < 20 && win.visible; i++) {
|
||||
await new Promise((r) => setTimeout(r, 25));
|
||||
}
|
||||
|
||||
assert.equal(win.visible, false, 'the window hides only after the acknowledgement dialog is dismissed');
|
||||
service.finishSession();
|
||||
});
|
||||
|
||||
test('a slow recorder start still arms within the warmup cap', async () => {
|
||||
// If the backend start hangs (Windows can take seconds), the window must
|
||||
// still hide and recording must still arm — the restart bug was the
|
||||
|
||||
Reference in New Issue
Block a user