From 77850269b86064585e46d91a03369858e72c5fe4 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Wed, 23 Sep 2026 18:57:15 +0100 Subject: [PATCH 1/3] Move mouse movement settings to sidebar --- src/App.tsx | 8 ++- src/settings/InputSection.tsx | 26 ++++++++ src/settings/MouseSettingsView.tsx | 11 ++++ src/settings/PointerSection.tsx | 21 +------ src/settings/ScannerPreferences.tsx | 2 +- src/settings/SettingsView.tsx | 8 +-- src/settings/settings.test.tsx | 93 +++++++++++++++++------------ 7 files changed, 106 insertions(+), 63 deletions(-) create mode 100644 src/settings/InputSection.tsx create mode 100644 src/settings/MouseSettingsView.tsx diff --git a/src/App.tsx b/src/App.tsx index 1083db53..7ace2540 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,13 +3,14 @@ import { Demonstration, DemonstrationProvider } from "./help/Demonstration"; import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react"; import { Accessibility, Bluetooth, ChevronRight, CircleHelp, Download, - Copy, Home, Keyboard, Plus, Power, RefreshCw, Save, Settings, + Copy, Home, Keyboard, MousePointer2, Plus, Power, RefreshCw, Save, Settings, SlidersHorizontal, Smartphone, Trash2, Wrench, X, } from "lucide-react"; import { api, type ProfileExitAction } from "./api"; import type { AppSettings, AppState, PendingPairing, SwitchProfile, UpdateState } from "./types"; import { applyLocalSettings, changedSettingKeys } from "./settings/diff"; import { SettingsView } from "./settings/SettingsView"; +import { MouseSettingsView } from "./settings/MouseSettingsView"; import { updateDescription, updateInFlight, updateLiveness, updateProgress, updateStanding, type UpdateAction } from "./settings/UpdatesSection"; import { TabPanel, Tabs } from "./Tabs"; import { useSwitches, type SwitchController } from "./scanning/useSwitches"; @@ -19,7 +20,7 @@ import { areaOptions } from "./scanning/preferences"; import { SwitchesSection } from "./settings/SwitchesSection"; import { ScanningSection } from "./settings/ScanningSection"; -type View = "switches" | "scanning" | "mobile" | "home" | "devices" | "profiles" | "settings" | "support"; +type View = "switches" | "scanning" | "mouse" | "mobile" | "home" | "devices" | "profiles" | "settings" | "support"; const brandIconUrl = new URL("../src-tauri/icons/icon.png", import.meta.url).href; const mobileQrUrl = new URL("./assets/mobile-download-qr.png", import.meta.url).href; @@ -722,7 +723,7 @@ export function App() { useEffect(() => { if (view === "profiles") void api.listProfiles().then(setProfiles).catch((reason) => setError(String(reason))); }, [view]); const nav = useMemo(() => [ - ["home", "Home", ], ["switches", "Switches", ], ["scanning", "Scanning", ], ["mobile", "Mobile", ], ["settings", "Settings", ], + ["home", "Home", ], ["switches", "Switches", ], ["scanning", "Scanning", ], ["mouse", "Mouse", ], ["mobile", "Mobile", ], ["settings", "Settings", ], ["support", "Help", ], ] as const, []); @@ -779,6 +780,7 @@ export function App() { {view === "home" && void perform(api.disconnectAll)} onAccessibility={() => void perform(() => api.checkAccessibility(true))} />} {view === "switches" &&

Switches

} {view === "scanning" &&

Scanning

Choose how your switches control the pointer.

} + {view === "mouse" && } {(view === "mobile" || view === "devices" || view === "profiles") &&

Mobile

{view === "mobile" && void perform(api.disconnectAll)} />} {view === "devices" && void perform(() => api.forgetDevice(id))} />} diff --git a/src/settings/InputSection.tsx b/src/settings/InputSection.tsx new file mode 100644 index 00000000..7579951f --- /dev/null +++ b/src/settings/InputSection.tsx @@ -0,0 +1,26 @@ +import { MoreOptions } from "../ui/controls"; +import type { AppSettings } from "../types"; +import { + OptionGroup, SettingGroup, Toggle, dwellDelayOptions, keyRepeatDelayOptions, + repeatIntervalOptions, secondsOptions, type SettingsUpdate, +} from "./controls"; + +export function InputSection({ settings, update }: { settings: AppSettings; update: SettingsUpdate }) { + return +
+ update("dwellClickEnabled", value)} /> +
+ legend="Wait before clicking" columns="five" disabled={!settings.dwellClickEnabled} options={secondsOptions(dwellDelayOptions)} value={settings.dwellClickDelayMs} onChange={(next) => update("dwellClickDelayMs", next)} + note={{ summary: "After mobile pointer movement stops, a countdown appears and performs one left click. Move again to rearm it." }} /> +
+
+
+ update("keyRepeatEnabled", value)} /> +
+ legend="Delay before repeating" columns="four" disabled={!settings.keyRepeatEnabled} options={keyRepeatDelayOptions} value={settings.keyRepeatInitialDelayMs} onChange={(next) => update("keyRepeatInitialDelayMs", next)} /> + legend="Key interval" columns="four" disabled={!settings.keyRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.keyRepeatIntervalMs} onChange={(next) => update("keyRepeatIntervalMs", next)} + note={{ about: "key repeat", summary: "Held navigation keys repeat, like on a keyboard.", detail: "Applies to the arrow keys, Tab, Backspace, Delete, Page Up, and Page Down." }} /> +
+
+
; +} diff --git a/src/settings/MouseSettingsView.tsx b/src/settings/MouseSettingsView.tsx new file mode 100644 index 00000000..f1073d6b --- /dev/null +++ b/src/settings/MouseSettingsView.tsx @@ -0,0 +1,11 @@ +import type { AppSettings } from "../types"; +import { PointerSection } from "./PointerSection"; +import type { SettingsUpdate } from "./controls"; + +export function MouseSettingsView({ settings, onChange }: { settings: AppSettings; onChange: (next: AppSettings) => void }) { + const update: SettingsUpdate = (key, value) => onChange({ ...settings, [key]: value }); + return
+

Mouse

Adjust pointer movement for Mouse scanning and Remote. Choose the scanning mode and panel timing under Scanning.

+ +
; +} diff --git a/src/settings/PointerSection.tsx b/src/settings/PointerSection.tsx index d3cd2bfc..7ba5a397 100644 --- a/src/settings/PointerSection.tsx +++ b/src/settings/PointerSection.tsx @@ -2,7 +2,7 @@ import { Button, Input, MoreOptions } from "../ui/controls"; import { useEffect, useId, useState } from "react"; import type { AppSettings } from "../types"; import { - Disclosure, OptionGroup, SettingGroup, Toggle, accelerationOptions, dwellDelayOptions, keyRepeatDelayOptions, + Disclosure, OptionGroup, SettingGroup, Toggle, accelerationOptions, movementValue, pointerSpeedOptions, repeatIntervalOptions, secondsOptions, type SettingsUpdate, } from "./controls"; @@ -26,7 +26,7 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up setSpeedDraft(String(next)); if (next !== settings.pointerScalePercent) update("pointerScalePercent", next); }; - return + return
Pointer speed {settings.pointerScalePercent}%

100% is the original speed. Higher speeds help cross the screen faster in Mouse and Remote.

{pointerSpeedOptions.map((value) => )}
setShowExact(!showExact)} controls={exactSpeedId}> @@ -37,14 +37,7 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up
-
- update("dwellClickEnabled", value)} /> -
- legend="Wait before clicking" columns="five" disabled={!settings.dwellClickEnabled} options={secondsOptions(dwellDelayOptions)} value={settings.dwellClickDelayMs} onChange={(next) => update("dwellClickDelayMs", next)} - note={{ summary: "After mobile pointer movement stops, a countdown appears and performs one left click. Move again to rearm it." }} /> -
-
-
+
update("mouseRepeatEnabled", value)} />
legend="Movement interval" columns="four" disabled={!settings.mouseRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.moveRepeatIntervalMs} onChange={(next) => update("moveRepeatIntervalMs", next)} /> @@ -52,14 +45,6 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up legend="Scroll interval" columns="four" disabled={!settings.mouseRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.scrollRepeatIntervalMs} onChange={(next) => update("scrollRepeatIntervalMs", next)} />
-
- update("keyRepeatEnabled", value)} /> -
- legend="Delay before repeating" columns="four" disabled={!settings.keyRepeatEnabled} options={keyRepeatDelayOptions} value={settings.keyRepeatInitialDelayMs} onChange={(next) => update("keyRepeatInitialDelayMs", next)} /> - legend="Key interval" columns="four" disabled={!settings.keyRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.keyRepeatIntervalMs} onChange={(next) => update("keyRepeatIntervalMs", next)} - note={{ about: "key repeat", summary: "Held navigation keys repeat, like on a keyboard.", detail: "Applies to the arrow keys, Tab, Backspace, Delete, Page Up, and Page Down." }} /> -
-
; } diff --git a/src/settings/ScannerPreferences.tsx b/src/settings/ScannerPreferences.tsx index 829ee7fb..f7e6dfa8 100644 --- a/src/settings/ScannerPreferences.tsx +++ b/src/settings/ScannerPreferences.tsx @@ -68,7 +68,7 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl

{names[area]}

{descriptions[area]} Change any value to customise it; other settings follow the shared defaults.

} {area === 'shared' &&
{(['point', 'mouse'] as const).map(key => )}
} - {area === 'mouse' &&

The ring stays visible while Mouse is open. Select a direction to move; with Repeat mouse movement on, the next switch press stops movement. Pointer speed and repeat controls are in Settings → Controls. Switch to Point returns to screen selection.

} + {area === 'mouse' &&

The ring stays visible while Mouse is open. Select a direction to move; with Repeat mouse movement on, the next switch press stops movement. Pointer speed and repeat controls are under Mouse in the sidebar. Switch to Point returns to screen selection.

} {area === 'point' && <> void; chooseTelemetry: (enabled: boolean) => void; updateAction: (action: UpdateAction) => void; cancelUpdate: () => void; busy: boolean; focusUpdates: boolean; onUpdatesFocused: () => void; updateAttention: string | null; onUpdatesShown: (shown: boolean) => void }) { const updatesRef = useRef(null); @@ -27,7 +27,7 @@ export function SettingsView({ state, settings, onChange, chooseTelemetry, updat const tabs = useMemo[]>(() => [ { id: "general" as const, label: "General" }, - { id: "pointer" as const, label: "Controls" }, + { id: "input" as const, label: "Input" }, ...(state.capabilities.cursorOverlay ? [{ id: "cursor" as const, label: "Cursor appearance" }] : []), { id: "privacy" as const, label: "Privacy" }, // On the Updates tab the panel itself shows the reason, so no marker there. @@ -55,7 +55,7 @@ export function SettingsView({ state, settings, onChange, chooseTelemetry, updat {active === "general" && } - {active === "pointer" && } + {active === "input" && } {active === "cursor" && } {active === "privacy" && } {active === "updates" && } diff --git a/src/settings/settings.test.tsx b/src/settings/settings.test.tsx index 264b3727..df4bd623 100644 --- a/src/settings/settings.test.tsx +++ b/src/settings/settings.test.tsx @@ -42,6 +42,26 @@ describe("Switchify PC settings", () => { expect(screen.getByRole("button",{name:"Customise point scanning"})).toBeInTheDocument(); }); + it("links Mouse scanning guidance to the sidebar settings and keeps Input separate", async () => { + render(); + await screen.findByRole("heading", { name: "Switchify PC" }); + const navigation = screen.getByRole("navigation"); + expect(within(navigation).getAllByRole("button").map((button) => button.textContent)) + .toEqual(["Home", "Switches", "Scanning", "Mouse", "Mobile", "Settings", "Help"]); + fireEvent.click(within(navigation).getByRole("button", { name: "Scanning" })); + fireEvent.click(screen.getByRole("button", { name: "Customise mouse scanning" })); + expect(screen.getByText(/Pointer speed and repeat controls are under Mouse in the sidebar/)).toBeInTheDocument(); + fireEvent.click(within(navigation).getByRole("button", { name: "Mouse" })); + expect(screen.getByRole("group", { name: /Pointer speed/ })).toBeInTheDocument(); + expect(screen.queryByRole("checkbox", { name: "Click when I stop" })).not.toBeInTheDocument(); + fireEvent.click(within(navigation).getByRole("button", { name: "Settings" })); + selectTab("Input"); + expect(screen.getByRole("checkbox", { name: "Click when I stop" })).toBeInTheDocument(); + expect(screen.queryByRole("group", { name: /Pointer speed/ })).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "More options" })); + expect(screen.getByRole("checkbox", { name: "Repeat held keys" })).toBeInTheDocument(); + }); + it("shows update progress and exposes cancellation in Settings", async () => { browserState.updater = { status: "downloading", version: "1.0.0-beta.2", downloadedBytes: 50, totalBytes: 200, error: null, retryAction: null }; const cancel = vi.spyOn(api, "cancelUpdateDownload").mockResolvedValue(structuredClone(browserState)); @@ -119,7 +139,7 @@ describe("Switchify PC settings", () => { it("exposes key repeat settings and disables them with the toggle", async () => { render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + selectTab("Input"); fireEvent.click(screen.getByRole("button", { name: "More options" })); const toggle = screen.getByRole("checkbox", { name: "Repeat held keys" }); @@ -147,14 +167,17 @@ describe("Switchify PC settings", () => { expect(screen.getByRole("group", { name: "Key interval" })).toBeDisabled(); }); - it("opens settings with accessible General and Controls settings", async () => { + it("opens Mouse settings from the sidebar and keeps Input settings separate", async () => { render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); expect(screen.getByRole("heading", { name: "Settings" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Save settings" })).not.toBeInTheDocument(); expect(screen.getByRole("checkbox", { name: "Start with system" })).toBeInTheDocument(); - selectTab("Controls"); + fireEvent.click(screen.getByRole("button", { name: "Mouse" })); + expect(screen.getByRole("heading", { name: "Mouse", level: 1 })).toBeInTheDocument(); + expect(screen.getByText(/Adjust pointer movement for Mouse scanning and Remote/)).toBeInTheDocument(); + expect(screen.queryByRole("checkbox", { name: "Click when I stop" })).not.toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "More options" })); expect(screen.getByRole("button", { name: "100% pointer speed" })).toHaveAttribute("aria-pressed", "true"); expect(screen.getByRole("checkbox", { name: "Repeat mouse movement" })).toBeChecked(); @@ -179,7 +202,7 @@ describe("Switchify PC settings", () => { it("exposes the dwell controls and their explanatory note", async () => { render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + selectTab("Input"); const dwell = screen.getByRole("checkbox", { name: "Click when I stop" }); expect(dwell).not.toBeChecked(); @@ -237,7 +260,7 @@ describe("Switchify PC settings", () => { const saveSettings = vi.spyOn(api, "saveSettings").mockImplementation(async (settings) => stateWithSettings(settings)); render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + selectTab("Input"); fireEvent.click(screen.getByRole("checkbox", { name: "Click when I stop" })); await waitFor(() => expect(saveSettings).toHaveBeenCalledWith(expect.objectContaining({ dwellClickEnabled: true, dwellClickDelayMs: 1000 }))); @@ -251,7 +274,7 @@ describe("Switchify PC settings", () => { vi.spyOn(api, "saveSettings").mockRejectedValueOnce(new Error("Settings storage unavailable")); render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + selectTab("Input"); fireEvent.click(screen.getByRole("checkbox", { name: "Click when I stop" })); @@ -286,8 +309,7 @@ describe("Switchify PC settings", () => { saves.push({ settings: structuredClone(settings), resolve }); })); render(); - fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + fireEvent.click(await screen.findByRole("button", { name: "Mouse" })); fireEvent.click(screen.getByRole("button", { name: "50% pointer speed" })); fireEvent.click(screen.getByRole("button", { name: "75% pointer speed" })); @@ -318,8 +340,7 @@ describe("Switchify PC settings", () => { finishSave = resolve; })); render(); - fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + fireEvent.click(await screen.findByRole("button", { name: "Mouse" })); fireEvent.click(screen.getByRole("button", { name: "50% pointer speed" })); const runtimeState = { @@ -352,11 +373,12 @@ describe("Switchify PC settings", () => { selectTab("Privacy"); fireEvent.click(screen.getByRole("checkbox", { name: "Share anonymous diagnostic data" })); - selectTab("Controls"); + fireEvent.click(screen.getByRole("button", { name: "Mouse" })); fireEvent.click(screen.getByRole("button", { name: "50% pointer speed" })); act(() => stateHandler?.(stateWithSettings({ ...defaultBrowserSettings, pointerScalePercent: 150 }))); expect(screen.getByRole("spinbutton", { name: "Exact pointer speed" })).toHaveValue(150); + fireEvent.click(screen.getByRole("button", { name: "Settings" })); selectTab("Privacy"); expect(screen.getByRole("checkbox", { name: "Share anonymous diagnostic data" })).toBeChecked(); @@ -418,7 +440,7 @@ describe("Switchify PC settings", () => { const tablist = screen.getByRole("tablist", { name: "Settings sections" }); expect(within(tablist).getAllByRole("tab").map((tab) => tab.textContent)) - .toEqual(["General", "Controls", "Cursor appearance", "Privacy", "Updates"]); + .toEqual(["General", "Input", "Cursor appearance", "Privacy", "Updates"]); expect(screen.getByRole("tab", { name: "General" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByRole("tabpanel")).toHaveAccessibleName("General"); }); @@ -428,7 +450,7 @@ describe("Switchify PC settings", () => { fireEvent.click(await screen.findByRole("button", { name: "Settings" })); const general = screen.getByRole("tab", { name: "General" }); - const pointer = screen.getByRole("tab", { name: "Controls" }); + const pointer = screen.getByRole("tab", { name: "Input" }); const updates = screen.getByRole("tab", { name: "Updates" }); expect(general).toHaveAttribute("tabindex", "0"); expect(pointer).toHaveAttribute("tabindex", "-1"); @@ -467,8 +489,8 @@ describe("Switchify PC settings", () => { expect(screen.queryByRole("tab", { name: "Cursor appearance" })).not.toBeInTheDocument(); expect(screen.getAllByRole("tab").map((tab) => tab.textContent)) - .toEqual(["General", "Controls", "Privacy", "Updates"]); - selectTab("Controls"); + .toEqual(["General", "Input", "Privacy", "Updates"]); + selectTab("Input"); expect(screen.queryByRole("checkbox", { name: "Show cursor overlay" })).not.toBeInTheDocument(); }); @@ -476,8 +498,8 @@ describe("Switchify PC settings", () => { browserState.updater = { status: "available", version: "1.0.0-beta.2", downloadedBytes: 0, totalBytes: null, error: null, retryAction: null }; render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); - expect(screen.getByRole("tab", { name: "Controls" })).toHaveAttribute("aria-selected", "true"); + selectTab("Input"); + expect(screen.getByRole("tab", { name: "Input" })).toHaveAttribute("aria-selected", "true"); fireEvent.click(within(screen.getByRole("status", { name: "Application update" })).getByRole("button", { name: "View update" })); @@ -485,15 +507,16 @@ describe("Switchify PC settings", () => { expect(screen.getByRole("tab", { name: "Updates" })).toHaveAttribute("aria-selected", "true"); }); - it("keeps a pending edit on an inactive tab and saves it", async () => { + it("keeps a pending Mouse edit across sidebar pages and saves it", async () => { const saveSettings = vi.spyOn(api, "saveSettings").mockImplementation(async (settings) => stateWithSettings(settings)); render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + fireEvent.click(screen.getByRole("button", { name: "Mouse" })); fireEvent.click(screen.getByRole("button", { name: "50% pointer speed" })); + fireEvent.click(screen.getByRole("button", { name: "Settings" })); selectTab("Privacy"); - selectTab("Controls"); + fireEvent.click(screen.getByRole("button", { name: "Mouse" })); expect(screen.getByRole("button", { name: "50% pointer speed" })).toHaveAttribute("aria-pressed", "true"); await waitFor(() => expect(saveSettings).toHaveBeenCalledWith(expect.objectContaining({ pointerScalePercent: 50 }))); @@ -505,7 +528,7 @@ describe("Switchify PC settings", () => { fireEvent.click(await screen.findByRole("button", { name: "Settings" })); const general = screen.getByRole("tab", { name: "General" }); - const pointer = screen.getByRole("tab", { name: "Controls" }); + const pointer = screen.getByRole("tab", { name: "Input" }); general.focus(); fireEvent.keyDown(general, { key: "ArrowRight" }); expect(pointer).toHaveAttribute("tabindex", "0"); @@ -535,7 +558,7 @@ describe("Switchify PC settings", () => { const general = screen.getByRole("tab", { name: "General" }); expect(general).toHaveAttribute("aria-controls", "settings-panel-general"); expect(document.getElementById("settings-panel-general")).toBeInTheDocument(); - for (const name of ["Controls", "Cursor appearance", "Privacy", "Updates"]) { + for (const name of ["Input", "Cursor appearance", "Privacy", "Updates"]) { expect(screen.getByRole("tab", { name })).not.toHaveAttribute("aria-controls"); } // The panel is not a tab stop of its own; its controls are. @@ -548,7 +571,7 @@ describe("Switchify PC settings", () => { render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - const pointer = screen.getByRole("tab", { name: "Controls" }); + const pointer = screen.getByRole("tab", { name: "Input" }); fireEvent.click(pointer); pointer.focus(); expect(pointer).toHaveAttribute("tabindex", "0"); @@ -592,8 +615,7 @@ describe("Switchify PC settings", () => { it("keeps the exact speed disclosure collapsed for a preset value", async () => { render(); - fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + fireEvent.click(await screen.findByRole("button", { name: "Mouse" })); const toggle = screen.getByRole("button", { name: "Set an exact speed" }); expect(toggle).toHaveAttribute("aria-expanded", "false"); @@ -612,8 +634,7 @@ describe("Switchify PC settings", () => { it("expands the exact speed disclosure for a value the presets cannot reach", async () => { browserState.settings = { ...structuredClone(defaultBrowserSettings), pointerScalePercent: 150 }; render(); - fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + fireEvent.click(await screen.findByRole("button", { name: "Mouse" })); expect(screen.getByRole("button", { name: "Hide exact speed" })).toHaveAttribute("aria-expanded", "true"); expect(screen.getByRole("spinbutton", { name: "Exact pointer speed" })).toHaveValue(150); @@ -621,8 +642,7 @@ describe("Switchify PC settings", () => { it("offers fast presets and rounds exact speed to the supported five percent step", async () => { render(); - fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + fireEvent.click(await screen.findByRole("button", { name: "Mouse" })); fireEvent.click(screen.getByRole("button", { name: "1350% pointer speed" })); expect(screen.getByRole("button", { name: "1350% pointer speed" })).toHaveAttribute("aria-pressed", "true"); fireEvent.click(screen.getByRole("button", { name: "Set an exact speed" })); @@ -643,8 +663,7 @@ describe("Switchify PC settings", () => { return () => undefined; }); render(); - fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + fireEvent.click(await screen.findByRole("button", { name: "Mouse" })); expect(screen.getByRole("button", { name: "Set an exact speed" })).toBeInTheDocument(); act(() => stateHandler?.(stateWithSettings({ ...defaultBrowserSettings, pointerScalePercent: 175 }))); @@ -657,7 +676,7 @@ describe("Switchify PC settings", () => { it("keeps the key repeat explanation behind a disclosure", async () => { render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + selectTab("Input"); fireEvent.click(screen.getByRole("button", { name: "More options" })); expect(screen.getByText("Held navigation keys repeat, like on a keyboard.")).toBeInTheDocument(); @@ -706,7 +725,7 @@ describe("Switchify PC settings", () => { it("gives each help disclosure a distinct accessible name", async () => { render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + selectTab("Input"); fireEvent.click(screen.getByRole("button", { name: "More options" })); const pointerNames = screen.getAllByRole("button", { name: /More about/ }).map((button) => button.textContent); expect(pointerNames).toEqual(["More about key repeat"]); @@ -720,7 +739,7 @@ describe("Switchify PC settings", () => { it("links each help disclosure to the detail it reveals and the group to its summary", async () => { render(); fireEvent.click(await screen.findByRole("button", { name: "Settings" })); - selectTab("Controls"); + selectTab("Input"); fireEvent.click(screen.getByRole("button", { name: "More options" })); // The group is described by the always-visible summary. @@ -776,7 +795,7 @@ describe("Switchify PC settings", () => { expect(screen.getByRole("tab", { name: "Updates" })).not.toHaveAttribute("aria-describedby"); // Having been shown, the failure is not spoken again on leaving. - selectTab("Controls"); + selectTab("Input"); expect(updatesNotice()).toBeEmptyDOMElement(); expect(updatesMarker()).toBeInTheDocument(); }); @@ -795,7 +814,7 @@ describe("Switchify PC settings", () => { expect(screen.getAllByRole("alert")).toHaveLength(1); expect(updatesNotice()).toBeEmptyDOMElement(); - selectTab("Controls"); + selectTab("Input"); expect(updatesNotice()).toBeEmptyDOMElement(); act(() => stateHandler?.({ ...structuredClone(browserState), updater: failedUpdater("Signature check failed") })); @@ -932,7 +951,7 @@ describe("Switchify PC settings", () => { // whatever comes back is news, even from another tab. fireEvent.click(screen.getByRole("button", { name: "Retry" })); act(() => stateHandler?.({ ...structuredClone(browserState), updater: checkingUpdater })); - selectTab("Controls"); + selectTab("Input"); expect(updatesMarker()).toBeNull(); await act(async () => { finishCheck?.({ ...structuredClone(browserState), updater: { ...failedUpdater("Update check failed: offline"), retryAction: "check" } }); }); expect(updatesNotice()).toHaveTextContent("Update check failed: offline. Open the Updates tab to retry."); From 69911f39923c98f1366b7970ab6d6b3022d36355 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Wed, 23 Sep 2026 19:02:04 +0100 Subject: [PATCH 2/3] Clarify Mouse page layout and repeat controls --- src/settings/MouseSettingsView.tsx | 2 +- src/settings/PointerSection.tsx | 9 ++++----- src/settings/settings.test.tsx | 5 +++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/settings/MouseSettingsView.tsx b/src/settings/MouseSettingsView.tsx index f1073d6b..8139e1a2 100644 --- a/src/settings/MouseSettingsView.tsx +++ b/src/settings/MouseSettingsView.tsx @@ -5,7 +5,7 @@ import type { SettingsUpdate } from "./controls"; export function MouseSettingsView({ settings, onChange }: { settings: AppSettings; onChange: (next: AppSettings) => void }) { const update: SettingsUpdate = (key, value) => onChange({ ...settings, [key]: value }); return
-

Mouse

Adjust pointer movement for Mouse scanning and Remote. Choose the scanning mode and panel timing under Scanning.

+

Mouse

Pointer speed and repeat for Mouse scanning and Remote. Set panel scan timing under Scanning.

; } diff --git a/src/settings/PointerSection.tsx b/src/settings/PointerSection.tsx index 7ba5a397..93a3e6cf 100644 --- a/src/settings/PointerSection.tsx +++ b/src/settings/PointerSection.tsx @@ -26,7 +26,7 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up setSpeedDraft(String(next)); if (next !== settings.pointerScalePercent) update("pointerScalePercent", next); }; - return + return
Pointer speed {settings.pointerScalePercent}%

100% is the original speed. Higher speeds help cross the screen faster in Mouse and Remote.

{pointerSpeedOptions.map((value) => )}
setShowExact(!showExact)} controls={exactSpeedId}> @@ -37,14 +37,13 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up
-
+
update("mouseRepeatEnabled", value)} /> -
+
legend="Movement interval" columns="four" disabled={!settings.mouseRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.moveRepeatIntervalMs} onChange={(next) => update("moveRepeatIntervalMs", next)} /> legend="Movement acceleration" columns="four" disabled={!settings.mouseRepeatEnabled} options={accelerationOptions} value={settings.mouseRepeatAccelerationDurationMs} onChange={(next) => update("mouseRepeatAccelerationDurationMs", next)} /> legend="Scroll interval" columns="four" disabled={!settings.mouseRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.scrollRepeatIntervalMs} onChange={(next) => update("scrollRepeatIntervalMs", next)} /> -
+
- ; } diff --git a/src/settings/settings.test.tsx b/src/settings/settings.test.tsx index df4bd623..dd9ede36 100644 --- a/src/settings/settings.test.tsx +++ b/src/settings/settings.test.tsx @@ -176,9 +176,10 @@ describe("Switchify PC settings", () => { fireEvent.click(screen.getByRole("button", { name: "Mouse" })); expect(screen.getByRole("heading", { name: "Mouse", level: 1 })).toBeInTheDocument(); - expect(screen.getByText(/Adjust pointer movement for Mouse scanning and Remote/)).toBeInTheDocument(); + expect(screen.getByText(/Pointer speed and repeat for Mouse scanning and Remote/)).toBeInTheDocument(); expect(screen.queryByRole("checkbox", { name: "Click when I stop" })).not.toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "More options" })); + expect(screen.getByRole("checkbox", { name: "Repeat mouse movement" })).toBeChecked(); + fireEvent.click(screen.getByRole("button", { name: "Repeat timing" })); expect(screen.getByRole("button", { name: "100% pointer speed" })).toHaveAttribute("aria-pressed", "true"); expect(screen.getByRole("checkbox", { name: "Repeat mouse movement" })).toBeChecked(); expect(screen.getByRole("group", { name: "Movement acceleration" })).not.toBeDisabled(); From 4f518e06ffd94b6ebdd8d99da7a98ac0c97feba6 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Wed, 23 Sep 2026 19:06:50 +0100 Subject: [PATCH 3/3] Wait for practice focus restoration in CI --- src/scanning/SwitchPractice.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scanning/SwitchPractice.test.tsx b/src/scanning/SwitchPractice.test.tsx index a7ca5e59..6ed2c03c 100644 --- a/src/scanning/SwitchPractice.test.tsx +++ b/src/scanning/SwitchPractice.test.tsx @@ -31,7 +31,7 @@ describe("safe switch practice", () => { fireEvent.click(screen.getByRole("button", { name: "Exit practice" })); await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument()); expect(invoke).toHaveBeenCalledWith("end_switch_practice"); - expect(screen.getByRole("button", { name: "Test switches" })).toHaveFocus(); + await waitFor(() => expect(screen.getByRole("button", { name: "Test switches" })).toHaveFocus()); }); it("shows disconnected remote errors rather than reporting successful input", async () => { invoke.mockRejectedValueOnce(new Error("No remote forwarding session")); @@ -62,7 +62,7 @@ describe("safe switch practice", () => { await screen.findByRole("dialog"); await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument()); expect(invoke).toHaveBeenCalledWith("end_switch_practice"); - expect(screen.getByRole("button", { name: "Test switches" })).toHaveFocus(); + await waitFor(() => expect(screen.getByRole("button", { name: "Test switches" })).toHaveFocus()); }); it("closes after confirmed end when feedback fails, without requiring mouse assistance", async () => { invoke.mockImplementation(async (command: string) => {