Files
omarchycn/test/shell.d/launcher-search-test.sh
T
ssuptandGitHub 3481b5614b Fix pointer navigation in Quattro launcher menus (#6343)
Accumulate sub-threshold movement so slow motion with a flat acceleration profile updates selection. Sample pointer state on row entry and carry pointer intent through subordinate menus while preserving predictable keyboard selection.
2026-07-22 15:27:56 -07:00

146 lines
5.3 KiB
Bash

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const search = requireFromRoot('shell/plugins/launcher/LauncherSearch.js')
const launcherQml = fs.readFileSync(path.join(root, 'shell/plugins/launcher/Launcher.qml'), 'utf8')
const entries = [
{
name: 'Google Contacts',
genericName: 'Address Book',
comment: 'Manage contacts',
keywords: ['contacts', 'address book', 'people'],
id: 'google-contacts.desktop'
},
{
name: 'Calculator',
genericName: 'Calculator',
comment: 'Perform arithmetic, scientific or financial calculations',
keywords: ['calculation', 'arithmetic', 'scientific', 'financial'],
id: 'org.gnome.Calculator.desktop'
},
{
name: 'OBS Studio',
genericName: 'Streaming/Recording Software',
comment: 'Free and Open Source Streaming/Recording Software',
keywords: ['streaming', 'recording', 'capture'],
id: 'com.obsproject.Studio.desktop'
},
{
name: 'Aether',
genericName: '',
comment: 'Minimal internet radio player',
keywords: ['audio', 'music', 'radio'],
id: 'io.github.taqi.aether.desktop'
},
{
name: 'Xournal++',
genericName: 'Notetaking',
comment: 'Take handwritten notes',
keywords: ['notes', 'pdf', 'annotation'],
id: 'com.github.xournalpp.xournalpp.desktop'
},
{
name: 'RustDesk',
genericName: 'Remote Desktop',
comment: 'Remote desktop control',
keywords: ['remote', 'desktop', 'control'],
id: 'com.rustdesk.RustDesk.desktop'
}
]
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
assert(
search.fuzzyScore(entries[1], 'contact') < 0,
'calculator does not match contact as a loose subsequence'
)
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works')
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works')
assert(
/function select\(delta\)[\s\S]*root\.disarmHover\(\)[\s\S]*root\.selectedIndex =/.test(launcherQml),
'launcher keyboard navigation disarms stale hover before moving selection'
)
assert(
/PointerMoveGate\s*\{[\s\S]*id: pointerGate[\s\S]*referenceItem: card[\s\S]*\}/.test(launcherQml),
'launcher uses shared pointer movement gate in card coordinates'
)
assert(
/function disarmHover\(\)[\s\S]*pointerGate\.reset\(\)/.test(launcherQml),
'launcher resets pointer movement gate when hover is disarmed'
)
const openMatch = launcherQml.match(/function open\(payloadJson\) \{([\s\S]*?)\n \}/)
assert(openMatch, 'launcher open function exists')
assert(
openMatch[1].indexOf('root.disarmHover()') < openMatch[1].indexOf('root.opened = true')
&& !openMatch[1].includes('pointerGate.allowInitialSample()'),
'launcher ignores a stale hidden-pointer position when becoming visible'
)
assert(
/function selectFromPointer\(index, item, mouse\)[\s\S]*pointerGate\.moved\(item, mouse\)[\s\S]*root\.selectedIndex = index/.test(launcherQml),
'launcher only selects from pointer after real movement'
)
assert(
/onPositionChanged: function\(mouse\) \{\s*root\.selectFromPointer\(row\.index, row, mouse\)\s*\}/.test(launcherQml),
'launcher row hover routes through pointer movement gate'
)
assert(
/onEntered: root\.selectFromPointer\(row\.index, row, \{\s*x: mouseArea\.mouseX,\s*y: mouseArea\.mouseY\s*\}\)/.test(launcherQml),
'launcher samples pointer movement immediately when entering a row'
)
assert(
!/onContainsMouseChanged:[\s\S]*root\.selectedIndex/.test(launcherQml),
'launcher does not select rows from containsMouse'
)
const confirmDeleteMatch = launcherQml.match(/function confirmDelete\(\) \{([\s\S]*?)\n \}/)
assert(confirmDeleteMatch, 'launcher confirmDelete function exists')
assert(
confirmDeleteMatch[1].includes('root.dismiss()'),
'launcher delete closes launcher after confirmation'
)
assert(
confirmDeleteMatch[1].includes('Util.execDetached(command)'),
'launcher delete runs remover through the shell'
)
const activateMatch = launcherQml.match(/function activateIndex\(index\) \{([\s\S]*?)\n \}/)
assert(activateMatch, 'launcher activateIndex function exists')
assert(
!activateMatch[1].includes('entry.execute()'),
'launcher does not execute desktop entries directly'
)
assert(
activateMatch[1].includes('gtk-launch') && activateMatch[1].includes('Util.execDetached'),
'launcher runs desktop entry launch through the shell'
)
assert(
/function iconIndexScanCommand\(\)[\s\S]*-path "\*\/apps\/\*" -o -path "\*\/devices\/\*"/.test(launcherQml),
'launcher fallback icon index includes device icons'
)
const iconSourceMatch = launcherQml.match(/function iconSource\(icon\) \{([\s\S]*?)\n \}/)
assert(iconSourceMatch, 'launcher iconSource function exists')
assert(
iconSourceMatch[1].indexOf('root.iconIndex[value]') < iconSourceMatch[1].indexOf('Quickshell.iconPath(value, true)'),
'launcher prefers indexed app icons over ambiguous themed icons'
)
assert(
openMatch[1].includes('if (!iconIndexScan.running) iconIndexScan.running = true'),
'launcher refreshes its icon index when opened'
)
JS