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.
146 lines
5.3 KiB
Bash
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
|