What We’re Testing
Input from the browser viewer is carried over a dedicated WebRTC DataChannel named "input". This channel is created by the viewer (browser-creates, ordered: true) so keystrokes always arrive in sequence. The "video" channel and "input" channel are independent SCTP streams — video drops are transparent to keyboard delivery.
Browser side (RemoteDesktopViewer.tsx):
The canvas element captures all mouse and keyboard DOM events via React event handlers:
onMouseMove→ sends{ type: "mousemove", x, y }where x/y are scaled from canvas coordinates to host pixel coordinates viascaleCoordinates()onMouseDown/onMouseUp→ sends{ type: "mousedown"/"mouseup", x, y, button: "left"/"right"/"middle" }onWheel→ sends{ type: "scroll", deltaX, deltaY }(raw pixel delta fromWheelEvent)onKeyDown/onKeyUp→ sends{ type: "keydown"/"keyup", key, code, modifiers: ["ctrl","shift","alt","meta"] }onContextMenu→e.preventDefault()to suppress the browser context menu on right-click
The canvas has tabIndex={0} so it receives keyboard focus. All events call e.preventDefault() to block browser defaults (e.g., browser scrolling, tab focus changes).
Coordinate scaling uses scaleCoordinates():
scaleX = hostResolution.width / canvas.getBoundingClientRect().width
scaleY = hostResolution.height / canvas.getBoundingClientRect().height
x = Math.round((clientX - rect.left) * scaleX)
y = Math.round((clientY - rect.top) * scaleY)
Agent side (input_windows.go): The windowsInputInjector calls Win32 SendInput via syscall. The inputUnion struct is carefully sized to 40 bytes to match the Win32 INPUT struct on 64-bit Windows (DWORD type + 4 padding + 32-byte union). This struct alignment was a known bug fixed on 2026-03-13 — before the fix, all SendInput calls returned “The parameter is incorrect”.
Mouse coordinates are converted to the Win32 absolute coordinate system (0-65535 range):
absX = x * 65535 / screenW
absY = y * 65535 / screenH
Keyboard events use codeToVK() which maps browser KeyboardEvent.code values (e.g., "KeyA", "Enter", "ArrowUp") to Windows Virtual Key codes. The mapping priority is: code-based (reliable) over key-based (fallback for single characters). Modifier keys (Ctrl, Shift, Alt, Meta/Win) are sent as separate SendInput calls before the main key on keydown, and released after on keyup.
handleInputMessage in session.go processes the JSON, dispatches to the appropriate InputInjector method, and logs errors at debug level (so individual injection failures do not crash the session).
Your Test Setup
| Machine | Role |
|---|---|
| ⊞ Win-A | Viewer — browser sends input events over DataChannel |
| ⊞ Win-B | Target — Win32 SendInput injects mouse/keyboard events |
Prerequisites:
- Remote desktop session active (status
"active") between Win-A and Win-B - Win-B’s desktop is visible and unlocked
- Notepad or a text editor open on Win-B’s desktop for keyboard testing
- No screen saver or UAC prompt active on Win-B (UAC elevates to a different desktop where
SendInputdoes not reach)
ST1 — Mouse Movement and Position Accuracy
What it verifies: Mouse mousemove events from the canvas are scaled correctly to Win-B’s resolution and the cursor on Win-B moves to the corresponding position.
Steps:
-
On ⊞ Win-A , establish an active remote desktop session to Win-B.
-
Click the canvas to give it keyboard focus.
-
Move the mouse to the top-left corner of the canvas. Observe the cursor on Win-B (visible in the canvas) moving to the top-left of Win-B’s desktop.
-
Move the mouse to the bottom-right corner of the canvas. The cursor on Win-B should move to the bottom-right of its display.
-
Test coordinate accuracy at the canvas center. The canvas center (50%, 50%) should map to Win-B’s display center. Open the Windows “Magnifier” app on Win-B and zoom in to verify cursor precision.
-
Open browser DevTools console on ⊞ Win-A and verify
mousemoveevents are being sent. Add a temporary log:
// In console — intercept canvas events (read-only inspection)
document.querySelector('canvas').addEventListener('mousemove', (e) => {
const canvas = e.target;
const rect = canvas.getBoundingClientRect();
const rawX = e.clientX - rect.left;
const rawY = e.clientY - rect.top;
// hostResolution is in component state; approximate from canvas natural dimensions:
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
console.log('Canvas raw:', rawX.toFixed(0), rawY.toFixed(0),
'=> Host:', Math.round(rawX * scaleX), Math.round(rawY * scaleY));
}, { passive: true });
Move the mouse to a known position (e.g., top-left corner of Win-B’s taskbar) and verify the “Host” coordinates match the expected pixel position.
- On ⊞ Win-B , enable cursor position display (optional). Open PowerShell:
Add-Type -AssemblyName System.Windows.Forms
while ($true) {
$pos = [System.Windows.Forms.Cursor]::Position
Write-Host "Cursor: $($pos.X), $($pos.Y)" -NoNewline
Write-Host "`r" -NoNewline
Start-Sleep -Milliseconds 100
}
Move the mouse in the viewer and verify Win-B’s reported cursor position matches.
Pass: Win-B cursor moves in sync with the viewer mouse; corner positions map correctly; coordinate scaling is proportional to canvas size.
Fail / Common issues:
- Cursor moves in the wrong direction or to wrong position —
scaleCoordinates()relies onhostResolutionbeing set from the"resolution"control message. If the control message was not received,hostResolutiondefaults to1280x720, causing incorrect scaling on a 1920x1080 display. - No cursor movement on Win-B — the input DataChannel may not be open. Check that
connState === "connected"in the viewer. ThesendInputfunction returns early ifinputChannelRef.current?.readyState !== "open". - Cursor moves but position drifts — the canvas is zoomed/scaled by CSS (
max-w-full max-h-full object-contain).getBoundingClientRect()returns the CSS-rendered size, not the canvas pixel size.scaleCoordinatesaccounts for this; if position is off, verifycanvas.getBoundingClientRect()returns the visual size.
ST2 — Mouse Button Clicks (Left, Right, Middle)
What it verifies: Mouse button down/up events are injected via SendInput with the correct Win32 mouse flags.
Steps:
-
On ⊞ Win-A , click the canvas to focus it.
-
Left click test: Single left-click on Win-B’s desktop (visible in canvas). A file or icon should become selected on Win-B.
-
Double-click test: Double-click an item on Win-B’s desktop. It should open (e.g., double-clicking the Recycle Bin should open it). Note: double-click is two
mousedown/mouseuppairs in rapid succession — no special handling is needed in the viewer. -
Right-click test: Right-click anywhere on Win-B’s desktop. Win-B’s context menu should appear (visible in the canvas). Note that
onContextMenuon the canvas callse.preventDefault()to suppress the viewer’s own context menu — only Win-B’s context menu should appear. -
Middle-click test (if supported): Middle-click on an open browser tab on Win-B to close it. On Win32,
mousefMiddleDown/mousefMiddleUpflags are used inMouseDown/MouseUp. -
Dismiss any open menus on Win-B by left-clicking an empty area of the desktop.
-
Verify via agent log on Win-B that
SendInputcalls are succeeding (no debug errors):
Get-Content "$env:LOCALAPPDATA\Programs\QuickZTNA\quickztna-svc.log" -Tail 50 |
Select-String "input injection error|SendInput"
If the struct alignment bug were present, you would see "SendInput mouse: The parameter is incorrect" for every click. The fix (40-byte inputUnion struct) means this should be absent.
Pass: Left-click selects, double-click opens, right-click shows Win-B’s context menu; no “SendInput error” lines in agent log.
Fail / Common issues:
- Right-click shows the viewer’s context menu instead of Win-B’s — the
onContextMenuhandler on the canvas is not callinge.preventDefault(). This would be a regression. - Middle-click not working — some mice do not fire
onMouseDownwithbutton: 1. Verify the browser is correctly reporting the middle button. "input injection error"in agent log — ifSendInputis returning an error, verify theinputUnionstruct size. The correct size is 40 bytes. Check that the service binary was built from the fixedinput_windows.go.
ST3 — Keyboard Keys: Alphanumeric and Special Keys
What it verifies: Letter keys, digit keys, Enter, Tab, Escape, Backspace, Delete, arrow keys, and function keys are mapped correctly by codeToVK() and injected via Win32 SendInput with inputKeyboard.
Steps:
-
On ⊞ Win-B , open Notepad (
Win + R→notepad). This gives a clear text field for keyboard testing. -
On ⊞ Win-A , click the canvas to focus it.
-
Alphanumeric test: Type a phrase:
Hello World 123. Verify on Win-B that the text appears in Notepad. Note that thecodeToVK()mapping handles:- Letter keys via
code[3]char from"KeyA"-"KeyZ"mapping to VK codes 0x41-0x5A - Digit keys via
code[5]char from"Digit0"-"Digit9"mapping to VK codes 0x30-0x39
- Letter keys via
-
Enter key: Press Enter in the canvas. A new line should appear in Notepad on Win-B.
codeToVK("Enter", "Enter")returns0x0D(VK_RETURN). -
Backspace: Press Backspace. The last character typed should be deleted.
codeToVK("Backspace", ...)returns0x08(VK_BACK). -
Arrow keys: Press the left, right, up, down arrow keys. The Notepad cursor should move. Codes
"ArrowLeft","ArrowRight","ArrowUp","ArrowDown"map to0x25,0x27,0x26,0x28. -
Function keys: Press F5 in Notepad (this inserts the current time/date in Notepad).
codeToVK("F5", "F5")returns0x74(VK_F5 = 0x70 + 5 - 1). -
Escape: Press Escape. If a menu is open on Win-B, it should close. Code
"Escape"maps to0x1B. -
Tab: In Notepad, Tab should insert a tab character. Code
"Tab"maps to0x09.
Pass: All typed characters appear in Notepad exactly as typed; Enter creates a new line; Backspace deletes; arrow keys move the cursor; F5 inserts timestamp; Escape dismisses menus.
Fail / Common issues:
- Uppercase letters appear as lowercase (or vice versa) — the VK code for letters is the uppercase ASCII code (0x41-0x5A). Win32 uses Shift state to determine case. Without Shift, VK_A produces lowercase ‘a’ in most apps. Type with Shift to get uppercase.
- Digits not working —
"Digit1"through"Digit9"are mapped;"Digit0"is also mapped. If digits from the numpad are used, thecodeis"Numpad0"etc., which is not in the mapping — only the main keyboard row digits are supported. codeToVKreturns 0 for a key — the key code is not in the mapping. Checkinput_windows.go:codeToVK. Special characters (punctuation, brackets, etc.) are not explicitly mapped and fall through to the single-charkeyfallback only for A-Z and 0-9.
ST4 — Modifier Key Combinations
What it verifies: Ctrl, Shift, Alt, and Meta (Win) key combinations are correctly sent with the main key and produce the expected system behavior on Win-B.
Steps:
-
On ⊞ Win-A , with focus on the canvas and Notepad open on Win-B:
-
Ctrl+A (Select All): Press Ctrl+A. All text in Notepad on Win-B should be selected.
The viewer sends:
keydown: { key: "Control", code: "ControlLeft", modifiers: [] }— not incodeToVK, returns 0, ignoredkeydown: { key: "a", code: "KeyA", modifiers: ["ctrl"] }—codeToVKreturns 0x41 (VK_A)
The agent
KeyDown("a", "KeyA", ["ctrl"])callsmodifierToVK("ctrl")→ 0x11, sendsSendInput(VK_CONTROL)first, thenSendInput(VK_A). -
Ctrl+C (Copy): With text selected, press Ctrl+C. Win-B’s clipboard should now contain the selected text.
-
Ctrl+V (Paste): Click at the end of the text in Notepad and press Ctrl+V. The copied text should be pasted.
-
Ctrl+Z (Undo): Press Ctrl+Z. The paste should be undone.
-
Shift+Letter (Uppercase): Type
Shift+Hto produce uppercase ‘H’. The viewer sends modifiers["shift"], agent sendsSendInput(VK_SHIFT)thenSendInput(VK_H). -
Alt+F4 (Close Window): Press Alt+F4. Notepad’s window should close on Win-B.
Warning: Alt+F4 will close the front-most application on Win-B. Make sure you close Notepad intentionally (save or discard when prompted).
-
Win key (Meta): Press the Meta/Windows key on Win-A’s keyboard (the key labeled with the Windows logo).
modifierToVK("meta")→0x5B(VK_LWIN). Win-B’s Start menu should open.Press Escape to close it (or Meta again).
Pass: Ctrl+A selects all; Ctrl+C/V copies and pastes; Shift produces uppercase; Alt+F4 closes the window; Win key opens Start menu.
Fail / Common issues:
- Ctrl+A does not select all — browser may be consuming Ctrl+A as a browser shortcut before the
keydownhandler fires. Ensure the canvas has focus (tabIndex={0}and user clicked it). TheonKeyDownhandler callse.preventDefault()to block browser defaults. - Meta (Win) key not working — some browsers block
KeyboardEventfor the Win/Meta key at the OS level. The key event may not reach the viewer’sonKeyDownhandler. This is a browser limitation, not a code bug. - Modifier released too early — if the user releases Ctrl before ‘a’, the viewer sends
keyupfor Ctrl beforekeydownfor ‘a’. The agent processes these in order (ordered DataChannel), so the modifier is released before the key press reaches Win32. Type Ctrl+A in a single chord (hold Ctrl, press A, release both).
ST5 — Scroll Wheel
What it verifies: Scroll wheel events (WheelEvent) are transmitted as type: "scroll" with deltaX/deltaY values and injected via Win32 mousefWheel flag.
Steps:
-
On ⊞ Win-B , open a document or webpage with scrollable content (e.g., a long text file in Notepad, or a webpage in a browser).
-
On ⊞ Win-A , hover the mouse over the canvas (no click needed).
-
Scroll up and down using the mouse wheel. Win-B’s content should scroll accordingly.
-
The viewer’s
handleWheelcallback:- Calls
e.preventDefault()to stop the viewer’s page from scrolling - Sends
{ type: "scroll", deltaX: Math.round(e.deltaX), deltaY: Math.round(e.deltaY) }
- Calls
-
The agent’s
Scroll(deltaX, deltaY)implementation ininput_windows.go:- Only sends a
mousefWheelevent ifdeltaY != 0(horizontal scrolldeltaXis not forwarded) - The Win32 wheel delta is
mouseData: uint32(int32(deltaY))— positive is scroll down, negative is scroll up
- Only sends a
-
Verify horizontal scroll is not implemented — scroll left/right on Win-A (if using a trackpad or horizontal scroll wheel). No horizontal scrolling should occur on Win-B (deltaX is ignored).
-
Test fast scroll — spin the wheel rapidly. Win-B should scroll smoothly, not stutter. Since the input DataChannel is
ordered: true, scrolls arrive in sequence. -
Verify via agent log that scroll events are being processed (no errors):
Get-Content "$env:LOCALAPPDATA\Programs\QuickZTNA\quickztna-svc.log" -Tail 30 |
Select-String "input injection error"
Pass: Win-B content scrolls vertically in sync with the viewer’s mouse wheel; horizontal scroll is a no-op; no injection errors in agent log.
Fail / Common issues:
- Win-B scrolls but the viewer page also scrolls —
e.preventDefault()inhandleWheelis not being called, or the event listener ispassive: true. Check thatonWheelon the canvas is not marked passive. - Scroll direction reversed —
WheelEvent.deltaYis positive when scrolling down in browsers, and Win32MOUSEEVENTF_WHEELpositive value scrolls up. The agent passesuint32(int32(deltaY))which does a sign-preserving cast. If the scroll direction is wrong on Win-B, the deltaY sign convention may differ between the browser and Win32. - No scroll on Win-B — the focused application on Win-B may not be under the mouse cursor. Win32
SendInputwithmousefWheelsends the scroll to the foreground window, not necessarily the one under the cursor. Move the cursor over the scrollable content on Win-B first.
Summary
| Sub-test | What it proves | Key assertion |
|---|---|---|
| ST1 | Mouse position accuracy | Win-B cursor tracks canvas mouse with correct coordinate scaling |
| ST2 | Mouse button clicks | Left/right/middle click works; no “SendInput error” in agent log |
| ST3 | Keyboard keys | Alphanumeric, Enter, Backspace, arrows, function keys all injected correctly |
| ST4 | Modifier combinations | Ctrl+A/C/V/Z, Shift+letter, Alt+F4, Win key all work as expected |
| ST5 | Scroll wheel | Vertical scroll injected via mousefWheel; horizontal scroll is no-op |