Extract QML-embedded logic into testable JavaScript modules #35
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The widget's business logic is embedded directly inside
TailscaleWidget.qmlas inline QML/JavaScript. QML is a UI framework — it cannot be unit tested outside a running Quickshell session. This means any logic living in the QML file is untestable, unverifiable by agents, and requires manual inspection in the live environment to validate.Issue #12 is a concrete example: clipboard detection and command construction live inline in the QML, with no way to write tests for the detection strategy, whitelist validation, or command-building logic.
Where
Currently,
TailscaleWidget.qmlcontains inline logic for:Meanwhile,
lib.jsalready has a pattern for testable pure-JS functions (parsePeers,makeExitNodeCommand,findActiveExitNode,errorMessage) with a CommonJS export block and a correspondingtest/lib.test.js.Goal
Move as much logic as possible out of QML and into
lib.js(or other.jsmodules), so it can be covered by automated tests. The QML file should be a thin UI layer that calls into testable functions.Criteria
lib.js, not inline in QML.test/for both happy path and edge cases (malicious input, empty input, unexpected types).libfunctions — no inline business logic.lib.jsCommonJS export block is updated to export new functions for testing.Benefits
node --testto verify correctness after any changePlan: Extract QML-embedded logic into testable JavaScript modules
Tracer Bullet 1 — Clipboard whitelist validation + safe command builder
Scope: Add
validateClipboardCmd(cmd)andbuildCopyCommand(text, clipboardCmd)tolib.js.validateClipboardCmdchecks a string against the known-safe whitelist:"dms cl copy","wl-copy","clipmanctl copy","none". Returns boolean.buildCopyCommandtakestextandclipboardCmd, validates via the whitelist, and returns a safe command array. Returnsnullon invalid input.Tests:
"rm -rf /","echo hi; malicious","$(whoami)") are rejectednull,undefinedare rejectedWhy first: Directly addresses the security concern in #12 and establishes the extract-then-test pattern.
Tracer Bullet 2 — Clipboard detection result parser
Scope: Add
parseClipboardDetection(stdout)tolib.js.Takes the raw stdout string from the detection Process, trims it, validates it against the whitelist, and returns the validated command string or
"none"for anything unrecognized.Tests:
which) falls back to"none""none""none"Tracer Bullet 3 — Toggle command builder
Scope: Add
buildToggleCommand(isConnected)tolib.js.Returns
["tailscale", "up"]or["tailscale", "down"]based on connection state.Tests:
truereturns down commandfalsereturns up commandnull,undefinedtreated as disconnected (safe default)Tracer Bullet 4 — Status result parser
Scope: Add
parseStatusResult(jsonText)tolib.js.Takes the raw JSON string from
tailscale status --json, parses it, and returns a state object:{ isConnected, tailscaleIP, currentExitNode, peers }. Uses existingparsePeersandfindActiveExitNodeinternally.Tests:
Self, missingPeer, emptyPeerall handled gracefullyBackendStateother than"Running"setsisConnectedto falseTracer Bullet 5 — Refactor QML to use extracted lib.js functions
Scope: Update
TailscaleWidget.qmlto call the newlib.jsfunctions instead of inline logic.Changes:
copyToClipboard()callsbuildCopyCommand()instead of string interpolationdetectClipboardstdout handler callsparseClipboardDetection()instead of raw assignmenttoggleTailscale()callsbuildToggleCommand()instead of inline conditionalstatusCheckstdout handler callsparseStatusResult()instead of inline parsingVerification: Existing behavior is preserved. No new functionality, just delegation. Tests from bullets 1-4 still pass.
Summary
validateClipboardCmd,buildCopyCommandtest/clipboard.test.jsparseClipboardDetectiontest/clipboard.test.jsbuildToggleCommandtest/toggle.test.jsparseStatusResulttest/status.test.jsBullets 1-4 are pure JS, fully testable with
node --test. Bullet 5 is the wiring change that ties it together. Each bullet is independently mergeable.