#!/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 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: '', 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 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: '', 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' ) 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( /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