Files
omarchycn/test/shell.d/menu-test.sh
T
Scott JonesandClaude Opus 5 ab798d80b2 Rank installed apps above equally matching menu entries
Searching the menu for an installed app buried it: "brave" listed Setup >
Defaults > Browser, Install > Browser and Remove > Browser ahead of the
Brave app itself. All four are exact label matches scoring 0, so the
tiebreak falls to declaration order, and mergeAppRows appends app rows
after every static item.

Bias app rows ahead of menu entries that match equally well. The bias is
smaller than the gap between match tiers, so a menu entry that matches the
query better still sorts first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:25:31 -04:00

338 lines
14 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
const defaultMenuJsonc = fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.jsonc'), 'utf8')
const parsed = menu.parseMenuJsonc(`
{
// comment
"items": {
"root": { "label": "Go" },
"style": { "label": "Style" },
"style.theme": {
"label": "Themes",
"aliases": "theme",
"description": "appearance colors",
"action": "omarchy-theme-set"
},
},
}
`)
assertEqual(parsed.length, 3, 'menu parses JSONC with comments and trailing commas')
assertDeepEqual(
parsed.find(item => item.id === 'style.theme'),
{
id: 'style.theme',
parent: 'style',
kind: 'action',
icon: '',
iconFont: '',
label: 'Themes',
title: '',
target: '',
description: 'appearance colors',
action: 'omarchy-theme-set',
provider: '',
aliases: ['theme'],
when: '',
checked: ''
},
'menu normalizes parsed items'
)
const user = [
menu.normalizeItem('style.theme', { label: 'Theme picker', aliases: ['theme', 'colors'], action: 'custom-theme' }),
menu.normalizeItem('tools', { label: 'Tools' })
]
const merged = menu.mergeMenuSources(parsed, user)
assertEqual(merged.items['style.theme'].label, 'Theme picker', 'menu user entries override default entries')
assertEqual(merged.items['style.theme'].order, 2, 'menu preserves original order on override')
assert(merged.items.root, 'menu injects root when merging sources')
assertEqual(menu.slugify('Power Saver!'), 'power-saver', 'menu slugifies provider rows')
assertEqual(menu.pathFor(merged.items, 'style.theme'), 'Style Theme picker', 'menu builds item paths')
assertEqual(menu.parentPathFor(merged.items, 'style.theme'), 'Style', 'menu builds parent paths')
assert(menu.isDescendantOf(merged.items, 'style.theme', 'style'), 'menu detects descendants')
assertEqual(menu.childCount(merged.items, merged.itemOrder, 'style'), 1, 'menu counts children')
assertEqual(menu.labelFor({ id: 'style.theme', label: 'Theme', checked: 'cmd' }, { 'style.theme': true }), 'Theme ✓', 'menu appends checked marker')
const visibilityItems = {
hardware: menu.normalizeItem('hardware', { label: 'Hardware' }),
laptop: menu.normalizeItem('hardware.laptop', { label: 'Laptop', when: 'is-laptop', action: 'toggle-laptop' }),
nested: menu.normalizeItem('nested', { label: 'Nested' }),
branch: menu.normalizeItem('nested.branch', { label: 'Branch' }),
leaf: menu.normalizeItem('nested.branch.leaf', { label: 'Leaf', when: 'has-leaf', action: 'run-leaf' }),
dynamic: menu.normalizeItem('dynamic', { label: 'Dynamic', provider: 'items' })
}
const visibilityOrder = Object.keys(visibilityItems)
assert(!menu.isVisible(visibilityItems, visibilityOrder, { 'hardware.laptop': false }, visibilityItems.hardware), 'menu hides a submenu with no visible children')
assert(menu.isVisible(visibilityItems, visibilityOrder, { 'hardware.laptop': true }, visibilityItems.hardware), 'menu shows a submenu with a visible child')
assert(!menu.isVisible(visibilityItems, visibilityOrder, { 'nested.branch.leaf': false }, visibilityItems.nested), 'menu hides recursively empty submenus')
assert(menu.isVisible(visibilityItems, visibilityOrder, {}, visibilityItems.dynamic), 'menu keeps provider-backed submenus visible')
const entry = merged.items['style.theme']
assert(menu.matchesQuery(entry, 'theme', true), 'menu matches labels and aliases')
assert(menu.matchesQuery(entry, 'colors', true), 'menu matches aliases')
assert(!menu.matchesQuery(entry, 'missing', true), 'menu rejects missing terms')
assert(!menu.matchesQuery(entry, 'theme', false), 'menu hides invisible matches')
assert(menu.searchScore(merged.items, entry, 'theme') < menu.searchScore(merged.items, entry, 'appearance'), 'menu scores name matches above description matches')
assertDeepEqual(
menu.displayRow(merged.items, merged.itemOrder, {}, entry, 'Style', 12, 'search'),
{
itemId: 'style.theme',
kind: 'action',
icon: '',
iconFont: '',
appIcon: '',
appId: '',
label: 'Theme picker',
target: 'style.theme',
detail: 'Style',
path: 'Style Theme picker',
childCount: 0,
action: 'custom-theme',
provider: '',
score: 12,
section: 'search'
},
'menu builds display rows'
)
const defaultItems = menu.parseMenuJsonc(defaultMenuJsonc)
const defaultById = Object.fromEntries(defaultItems.map(item => [item.id, item]))
// App rows land after every static item, so ranking has to survive the real
// menu's item count: with hundreds of entries ahead of them, the order
// tiebreak alone buries an installed app under Install and Remove.
const rankBase = menu.mergeMenuSources(defaultItems, [])
const ranked = menu.mergeAppRows(rankBase.items, rankBase.itemOrder, [
{ id: 'apps.brave', parent: 'apps', kind: 'app', label: 'Brave', description: '', aliases: [] },
{ id: 'apps.fontforge', parent: 'apps', kind: 'app', label: 'FontForge', description: '', aliases: [] }
])
const rankScore = (id, query) => menu.searchScore(ranked.items, ranked.items[id], query)
assert(
['install.browser.brave', 'remove.browser.brave', 'setup.default.browser.brave'].every(
id => rankScore('apps.brave', 'brave') < rankScore(id, 'brave')
),
'menu ranks an installed app above menu entries matching the query equally well'
)
assert(
rankScore('style.font', 'font') < rankScore('apps.fontforge', 'font'),
'menu keeps a better-matching menu entry above a weaker app match'
)
const triggerItems = defaultItems.filter(item => item.parent === 'trigger')
assertEqual(
triggerItems[0].id,
'trigger.emoji',
'menu lists Emoji first under Trigger'
)
assertEqual(
defaultById['trigger.emoji'].action,
'omarchy-menu-emoji',
'menu opens the emoji picker from Trigger'
)
assert(
defaultById['update.omarchy'].icon === '\ue900',
'menu update Omarchy entry uses the Omarchy glyph'
)
assert(
defaultById['update.omarchy'].iconFont === 'omarchy',
'menu update Omarchy entry renders the private glyph with the Omarchy font'
)
assert(
defaultById['setup.input'].action.includes('input.lua'),
'menu keeps Input as a direct config action'
)
assert(
defaultById['setup.direct-boot'].action.includes('omarchy-setup-direct-boot'),
'menu places Direct Boot directly under Setup'
)
assertEqual(
defaultItems.findIndex(item => item.id === 'setup.direct-boot'),
defaultItems.findIndex(item => item.id === 'setup.input') + 1,
'menu lists Direct Boot immediately below Input'
)
assert(
defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'),
'menu places Passwordless Sudo under Setup > Security'
)
assert(
!defaultById['trigger.toggle.direct-boot'] && !defaultById['trigger.toggle.passwordless-sudo'],
'menu removes the relocated toggles from Trigger > Toggle'
)
assert(
defaultById['style.bar.position'].kind === 'menu',
'menu groups Menu Bar positions in a submenu'
)
assert(
['top', 'bottom', 'left', 'right'].every(position => defaultById[`style.bar.position.${position}`].action === `omarchy-bar position ${position}`),
'menu lists all Menu Bar positions under Position'
)
assertEqual(
defaultById['style.bar.transparency'].action,
'omarchy-bar transparent toggle',
'menu exposes Menu Bar transparency as a toggle'
)
assertEqual(
defaultById['trigger.hardware.laptop-display'].when,
'omarchy-hw-laptop',
'menu only shows Laptop Display on laptops'
)
assertEqual(
defaultById['trigger.hardware.mirror-display'].when,
'omarchy-hw-laptop',
'menu only shows Mirror Display on laptops'
)
assertEqual(
defaultById['trigger.capture.screenrecord.webcam'].when,
'omarchy-hw-webcam',
'menu only shows webcam screen recording when a webcam is available'
)
assert(
/font\.family: row\.iconFont\.length > 0 \? row\.iconFont : root\.fontFamily/.test(menuQml),
'menu rows support per-icon font families'
)
assert(
/function select\(delta\)[\s\S]*root\.disarmPointer\(\)[\s\S]*selectedIndex =/.test(menuQml),
'menu keyboard navigation disarms pointer selection'
)
assert(
/function setFilter\(nextFilter\)[\s\S]*root\.disarmPointer\(\)/.test(menuQml),
'menu filter changes disarm pointer selection'
)
assert(
/function setActiveMenu\(id, pushHistory, fromPointer\)[\s\S]*if \(fromPointer\) pointerGate\.allowInitialSample\(\)\s*else root\.disarmPointer\(\)/.test(menuQml),
'menu route changes only accept an initial pointer sample for mouse activation'
)
assert(
/\(event\.key === Qt\.Key_Backspace \|\| event\.key === Qt\.Key_Left\) && !root\.filterText[\s\S]*root\.goBack\(\)/.test(menuQml),
'menu Left key follows empty-filter Backspace navigation'
)
assert(
/PointerMoveGate\s*\{[\s\S]*id: pointerGate[\s\S]*referenceItem: card[\s\S]*\}/.test(menuQml),
'menu uses shared pointer movement gate in card coordinates'
)
assert(
/function disarmPointer\(\)[\s\S]*pointerGate\.reset\(\)/.test(menuQml),
'menu resets pointer movement gate when pointer selection is disarmed'
)
// App rows are rebuilt from scratch on every desktop-entry rescan. The merge
// must be idempotent and must never carry an orphan id forward, or a single
// lost write turns into an app listed twice (and thrice, and so on).
const nonAppItems = {
root: { id: 'root', kind: 'menu', label: 'Go' },
apps: { id: 'apps', kind: 'menu', label: 'Apps', provider: 'apps' }
}
const nonAppOrder = ['root', 'apps']
const appRowsFor = ids => ids.map(id => ({ id: `apps.${id}`, kind: 'app', parent: 'apps', label: id, appId: id }))
const firstMerge = menu.mergeAppRows(nonAppItems, nonAppOrder, appRowsFor(['alacritty', 'youtube']))
assert(
firstMerge.itemOrder.join(',') === 'root,apps,apps.alacritty,apps.youtube',
'app merge appends app rows after the static menu items'
)
const secondMerge = menu.mergeAppRows(firstMerge.items, firstMerge.itemOrder, appRowsFor(['alacritty', 'youtube']))
assert(
secondMerge.itemOrder.join(',') === 'root,apps,apps.alacritty,apps.youtube',
'repeating the app merge with the same entries does not duplicate rows'
)
assert(
menu.mergeAppRows(secondMerge.items, secondMerge.itemOrder, appRowsFor(['alacritty'])).itemOrder.join(',')
=== 'root,apps,apps.alacritty',
'app merge drops rows for entries that went away'
)
assert(
menu.mergeAppRows(nonAppItems, nonAppOrder, appRowsFor(['youtube', 'youtube'])).itemOrder.join(',')
=== 'root,apps,apps.youtube',
'app merge lists an app once even when two desktop entries share an id'
)
const orphanedItems = {}
for (const key in firstMerge.items) orphanedItems[key] = firstMerge.items[key]
delete orphanedItems['apps.youtube']
const healed = menu.mergeAppRows(orphanedItems, firstMerge.itemOrder, appRowsFor(['alacritty', 'youtube']))
assert(
healed.itemOrder.join(',') === 'root,apps,apps.alacritty,apps.youtube'
&& !!healed.items['apps.youtube'],
'app merge heals an order entry whose item went missing instead of duplicating it'
)
assert(
!firstMerge.items['apps.youtube'].hasOwnProperty('__probe')
&& (() => {
const before = Object.keys(nonAppItems).length
menu.mergeAppRows(nonAppItems, nonAppOrder, appRowsFor(['gimp']))
return Object.keys(nonAppItems).length === before
})(),
'app merge leaves the map it was handed untouched'
)
const providerRowsFor = values => values.map(value => ({ id: `style.font.${value}`, kind: 'action', parent: 'style.font', label: value }))
const firstProviderMerge = menu.mergeRowsById(nonAppItems, nonAppOrder, providerRowsFor(['mono', 'serif']))
assert(
firstProviderMerge.itemOrder.join(',') === 'root,apps,style.font.mono,style.font.serif',
'provider merge appends its rows'
)
assert(
menu.mergeRowsById(firstProviderMerge.items, firstProviderMerge.itemOrder, providerRowsFor(['mono', 'serif']))
.itemOrder.join(',') === 'root,apps,style.font.mono,style.font.serif',
'repeating a provider merge does not duplicate rows'
)
// The maps live in QML `var` properties, where an in-place write is
// occasionally dropped by the engine, so both merges must hand back fresh
// objects for the caller to assign in one shot.
assert(
/var merged = MenuModel\.mergeAppRows\(root\.items, root\.itemOrder, appRows\)\s*\n\s*root\.items = merged\.items\s*\n\s*root\.itemOrder = merged\.itemOrder/.test(menuQml),
'menu assigns the rebuilt app item map instead of mutating it in place'
)
assert(
/var merged = MenuModel\.mergeRowsById\(root\.items, root\.itemOrder, providerRows\)\s*\n\s*root\.items = merged\.items\s*\n\s*root\.itemOrder = merged\.itemOrder/.test(menuQml),
'menu assigns the rebuilt provider item map instead of mutating it in place'
)
assert(
!/root\.items\[[^\]]+\] =/.test(menuQml) && !/delete root\.items\[/.test(menuQml),
'menu never writes into the item map held by the var property'
)
for (const functionName of ['openExistingMenu', 'openDmenu']) {
const openMatch = menuQml.match(new RegExp(`function ${functionName}\\([^)]*\\) \\{([\\s\\S]*?)\\n \\}`))
assert(openMatch, `menu ${functionName} function exists`)
assert(
openMatch[1].indexOf('root.disarmPointer()') < openMatch[1].indexOf('opened = true')
&& !openMatch[1].includes('pointerGate.allowInitialSample()'),
`menu ${functionName} 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(menuQml),
'menu only selects from pointer after real movement'
)
assert(
/onPositionChanged: function\(mouse\) \{\s*root\.selectFromPointer\(row\.index, row, mouse\)\s*\}/.test(menuQml),
'menu row hover routes through pointer movement gate'
)
assert(
/onEntered: root\.selectFromPointer\(row\.index, row, \{\s*x: mouseArea\.mouseX,\s*y: mouseArea\.mouseY\s*\}\)/.test(menuQml),
'menu samples pointer movement immediately when entering a row'
)
assert(
/function activateIndex\(index, fromPointer\)[\s\S]*root\.setActiveMenu\(row\.target \|\| row\.itemId, true, fromPointer\)/.test(menuQml)
&& /onClicked:[\s\S]*root\.activateIndex\(row\.index, true\)/.test(menuQml),
'mouse activation carries pointer intent into subordinate menus'
)
JS