From 98cce0dc408d3e1942639e55fab418c8df1f8595 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:30:52 -0700 Subject: [PATCH] fix(e2e): select the selectable input source, not the first match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TISCreateInputSourceList can return several entries for one input source id. A third-party IME publishes a non-selectable parent alongside the selectable mode, and taking sources.first can return the parent — after which TISSelectInputSource fails with paramErr (-50) while the caller reports success from the enable step. Found with Qingg (com.aodaren.inputmethod.Qingg), which exposes exactly that pair under one id. Its mode id equals the bundle id, so filtering by name would not have helped; selectability is the discriminator. Now filters on kTISPropertyInputSourceIsSelectCapable and falls back to the old behaviour when nothing advertises it, so single-entry sources are unaffected. Also enables every entry for the id rather than only the one being selected: selecting a mode whose parent is still disabled fails the same way. Compile-checked, and selecting com.apple.keylayout.ABC still exits 0. Unrelated to the enable path: on macOS 26.5.2 third-party IMEs are gated behind a consent sheet in System Settings. TISEnableInputSource returns noErr immediately regardless, and the enable only lands if that sheet is answered while the requesting process is still alive. --- tests/e2e/select-input-source.swift | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/e2e/select-input-source.swift b/tests/e2e/select-input-source.swift index fe8ecf52258..3012e76e8d3 100644 --- a/tests/e2e/select-input-source.swift +++ b/tests/e2e/select-input-source.swift @@ -6,9 +6,27 @@ guard CommandLine.arguments.count == 2 else { let properties = [kTISPropertyInputSourceID: CommandLine.arguments[1] as CFString] as CFDictionary let sources = TISCreateInputSourceList(properties, true).takeRetainedValue() as! [TISInputSource] -guard let source = sources.first else { +guard !sources.isEmpty else { exit(3) } -TISEnableInputSource(source) +func boolProperty(_ source: TISInputSource, _ key: CFString) -> Bool { + guard let raw = TISGetInputSourceProperty(source, key) else { return false } + return unsafeBitCast(raw, to: CFBoolean.self) == kCFBooleanTrue +} + +// Why: one id can expose several entries — a third-party IME publishes a non-selectable +// parent alongside the selectable mode, and `sources.first` can return the parent, which +// then fails TISSelectInputSource with paramErr (-50). Qingg is the case that found this. +let selectable = sources.filter { boolProperty($0, kTISPropertyInputSourceIsSelectCapable) } +guard let source = selectable.first ?? sources.first else { + exit(3) +} + +// Enable every entry for the id, not just the selected one: selecting a mode whose parent +// is still disabled also fails with -50. +for candidate in sources { + TISEnableInputSource(candidate) +} + exit(TISSelectInputSource(source) == noErr ? 0 : 4)