diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 818676f..e447763 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,3 +34,9 @@ jobs: - name: Build run: npm run build + + - name: Build native cursor overlay helper + run: npm run native:build-overlay + + - name: Smoke native cursor overlay helper + run: npm run native:smoke-overlay diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ecbd197..672016b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,9 @@ jobs: - name: Build native cursor overlay helper run: npm run native:build-overlay + - name: Smoke native cursor overlay helper + run: npm run native:smoke-overlay + - name: Package Windows installer run: npm run package:win diff --git a/native/cursor-overlay-helper/Program.cs b/native/cursor-overlay-helper/Program.cs index b11d5c6..bbf801c 100644 --- a/native/cursor-overlay-helper/Program.cs +++ b/native/cursor-overlay-helper/Program.cs @@ -78,9 +78,9 @@ private static void HandleCommandLine(OverlayForm overlayForm, SynchronizationCo switch (command.Type) { case "show": - if (command.Event is not ("move" or "click")) + if (command.Event is not ("move" or "click" or "drag")) { - WriteError("invalid_command", "Show command event must be move or click."); + WriteError("invalid_command", "Show command event must be move, click, or drag."); return; } diff --git a/package.json b/package.json index 412d3d6..b9b01c1 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "electron-vite build", "native:build": "node scripts/build-cursor-overlay-helper.cjs", "native:build-overlay": "npm run native:build", + "native:smoke-overlay": "node scripts/smoke-cursor-overlay-helper.cjs", "package:win": "npm run build && npm run native:build && electron-builder --win --x64 --publish never && node scripts/sign-win-artifacts.cjs --update-latest-yml", "package:win:verify-uiaccess": "node scripts/verify-win-uiaccess-package.cjs", "signing:create-dev-cert": "node scripts/create-dev-signing-cert.cjs", diff --git a/scripts/smoke-cursor-overlay-helper.cjs b/scripts/smoke-cursor-overlay-helper.cjs new file mode 100644 index 0000000..2915867 --- /dev/null +++ b/scripts/smoke-cursor-overlay-helper.cjs @@ -0,0 +1,136 @@ +const { spawn } = require('node:child_process'); +const { existsSync } = require('node:fs'); +const { join, resolve } = require('node:path'); + +const projectRoot = resolve(__dirname, '..'); +const helperPath = join( + projectRoot, + 'build', + 'native', + 'cursor-overlay-helper', + 'win-x64', + 'SwitchifyCursorOverlay.exe' +); + +const timeoutMs = 5_000; +let stdoutBuffer = ''; +let stderrBuffer = ''; +let settled = false; +let sawReady = false; +let shutdownSent = false; +let timeout = null; +let helper = null; + +if (!existsSync(helperPath)) { + fail(`Cursor overlay helper was not found: ${helperPath}`); +} + +helper = spawn(helperPath, [], { + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true +}); + +timeout = setTimeout(() => { + fail(`Cursor overlay helper smoke test timed out.${stderrBuffer ? ` stderr: ${stderrBuffer.trim()}` : ''}`); +}, timeoutMs); + +helper.once('error', (error) => { + fail(`Cursor overlay helper failed to start: ${error.message}`); +}); + +helper.once('exit', (code, signal) => { + if (!settled && !shutdownSent) { + fail(`Cursor overlay helper exited before shutdown: ${signal ?? code ?? 'unknown'}`); + } +}); + +helper.stdout.on('data', (chunk) => { + stdoutBuffer += String(chunk); + readStdoutLines(); +}); + +helper.stderr.on('data', (chunk) => { + stderrBuffer += String(chunk); +}); + +function readStdoutLines() { + while (stdoutBuffer.includes('\n')) { + const newlineIndex = stdoutBuffer.indexOf('\n'); + const line = stdoutBuffer.slice(0, newlineIndex).trim(); + stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1); + if (line) { + handleMessage(line); + } + } +} + +function handleMessage(line) { + let message; + try { + message = JSON.parse(line); + } catch (error) { + fail(`Cursor overlay helper returned malformed output: ${line}`); + return; + } + + if (message.type === 'error') { + fail(`Cursor overlay helper reported ${message.code ?? 'error'}: ${message.message ?? 'unknown error'}`); + return; + } + + if (message.type === 'ready' && !sawReady) { + sawReady = true; + smokeEvents(); + } +} + +function smokeEvents() { + for (const event of ['move', 'click', 'drag']) { + writeCommand({ + type: 'show', + event, + x: 100, + y: 100, + size: 96, + durationMs: 50, + crosshairs: false, + persistent: false, + color: { + red: 211, + green: 47, + blue: 47 + } + }); + } + + setTimeout(() => { + shutdownSent = true; + writeCommand({ type: 'shutdown' }); + helper.stdin.end(); + clearTimeout(timeout); + settled = true; + console.log('Cursor overlay helper smoke test passed.'); + setTimeout(() => { + if (helper && !helper.killed) { + helper.kill(); + } + }, 1_000).unref(); + }, 100); +} + +function writeCommand(command) { + helper.stdin.write(`${JSON.stringify(command)}\n`); +} + +function fail(message) { + if (settled) return; + settled = true; + if (timeout) { + clearTimeout(timeout); + } + if (helper && !helper.killed) { + helper.kill(); + } + console.error(message); + process.exitCode = 1; +}