Static submenus and links now inherit visibility from their descendants, so hardware sections like Touchpad Haptics disappear on machines without the hardware instead of opening empty. Laptop Display and Mirror Display are guarded by omarchy-hw-laptop. The guard evaluator brace-wraps every when/checked condition before silencing it, so conditions in the jsonc no longer need their own >/dev/null plumbing, and parents no longer need to repeat their children's guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
187 lines
7.3 KiB
Bash
187 lines
7.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 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",
|
||
"keywords": "appearance 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',
|
||
target: '',
|
||
keywords: 'appearance colors',
|
||
description: '',
|
||
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 keyword matches')
|
||
|
||
assertDeepEqual(
|
||
menu.displayRow(merged.items, merged.itemOrder, {}, entry, 'Style', 12, 'search'),
|
||
{
|
||
itemId: 'style.theme',
|
||
kind: 'action',
|
||
icon: '',
|
||
iconFont: '',
|
||
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]))
|
||
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-config-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'
|
||
)
|
||
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'
|
||
)
|
||
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\)[\s\S]*root\.disarmPointer\(\)/.test(menuQml),
|
||
'menu route changes disarm pointer selection'
|
||
)
|
||
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'
|
||
)
|
||
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'
|
||
)
|
||
JS
|