From 40a408e029101ec5e6d91b7d8e9d15d8e490ab43 Mon Sep 17 00:00:00 2001 From: vybe Date: Fri, 22 May 2026 06:33:33 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20extract=20shouldShowClearExitNode?= =?UTF-8?q?=20and=20isActiveExitNode=20to=20lib.js=20(TDD)=20=E2=80=94=20l?= =?UTF-8?q?ight=20progress=20on=20#43?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Two small pure UI predicates moved out of QML delegate - QML now calls TailscaleLib.* (thinner glue) - New tests + all existing tests pass - No new state, no architecture change Written by AI agent working for @jtmorris. Model: Grok build-0.1. --- tailscalectl/TailscaleWidget.qml | 4 ++-- tailscalectl/lib.js | 11 ++++++++++- test/lib.test.js | 13 ++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tailscalectl/TailscaleWidget.qml b/tailscalectl/TailscaleWidget.qml index 63f9df2..e9d5f27 100644 --- a/tailscalectl/TailscaleWidget.qml +++ b/tailscalectl/TailscaleWidget.qml @@ -202,7 +202,7 @@ PluginComponent { } MouseArea { - visible: root.currentExitNode !== "" + visible: TailscaleLib.shouldShowClearExitNode(root.currentExitNode) cursorShape: Qt.PointingHandCursor hoverEnabled: true anchors.verticalCenter: parent.verticalCenter @@ -292,7 +292,7 @@ PluginComponent { id: exitNodeButton text: "↗" font.pixelSize: Theme.fontSizeSmall - color: root.currentExitNode === modelData.hostname ? Theme.primary : Theme.surfaceVariantText + color: TailscaleLib.isActiveExitNode(root.currentExitNode, modelData.hostname) ? Theme.primary : Theme.surfaceVariantText } } } diff --git a/tailscalectl/lib.js b/tailscalectl/lib.js index 7c9df54..e367975 100644 --- a/tailscalectl/lib.js +++ b/tailscalectl/lib.js @@ -76,6 +76,15 @@ function getStrings() { } } +// Light UI predicates extracted from QML (advances #43 — keeps view thin) +function shouldShowClearExitNode(currentExitNode) { + return currentExitNode !== "" +} + +function isActiveExitNode(currentExitNode, hostname) { + return currentExitNode === hostname +} + function parseStatusResult(jsonText) { try { const data = JSON.parse(jsonText) @@ -108,5 +117,5 @@ function errorMessage(cmd) { // CommonJS export for Node.js tests (ignored by QML) if (typeof module !== "undefined" && module.exports) { - module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult, getStrings } + module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult, getStrings, shouldShowClearExitNode, isActiveExitNode } } diff --git a/test/lib.test.js b/test/lib.test.js index 4d91ff9..dada9e4 100644 --- a/test/lib.test.js +++ b/test/lib.test.js @@ -1,7 +1,7 @@ import { test } from "node:test" import assert from "node:assert" import lib from "../tailscalectl/lib.js" -const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult, getStrings } = lib +const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult, getStrings, shouldShowClearExitNode, isActiveExitNode } = lib test("parsePeers extracts exitNode from ExitNodeOption", () => { const peerMap = { @@ -157,6 +157,17 @@ test("getStrings.copied interpolates the text", () => { assert.ok(msg.includes("100.64.0.5")) }) +test("shouldShowClearExitNode returns true only when there is a current exit node", () => { + assert.strictEqual(shouldShowClearExitNode("router"), true) + assert.strictEqual(shouldShowClearExitNode(""), false) +}) + +test("isActiveExitNode correctly identifies the active exit node button", () => { + assert.strictEqual(isActiveExitNode("gluetun-sjc", "gluetun-sjc"), true) + assert.strictEqual(isActiveExitNode("gluetun-sjc", "gluetun-den"), false) + assert.strictEqual(isActiveExitNode("", "router"), false) +}) + // --- buildToggleCommand --- test("buildToggleCommand returns down command when connected", () => {