From 4277f5346b774d2dfcc62ea8ba2be7d5f2a428be Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 25 May 2026 13:16:47 +0200 Subject: [PATCH] Add shell emoji tests --- AGENTS.md | 2 +- shell/plugins/emojis/EmojiSearch.js | 46 +++++++++++++++++ shell/plugins/emojis/Emojis.qml | 19 ++----- test/shell.sh | 6 ++- test/shell/base-test.sh | 80 +++++++++++++++++++++++++++++ test/shell/emojis-test.sh | 47 +++++++++++++++++ test/shell/launcher-search-test.sh | 38 +++----------- 7 files changed, 188 insertions(+), 50 deletions(-) create mode 100644 shell/plugins/emojis/EmojiSearch.js create mode 100644 test/shell/base-test.sh create mode 100644 test/shell/emojis-test.sh diff --git a/AGENTS.md b/AGENTS.md index 9cb09ab1..b6c2ea26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,7 +104,7 @@ Run focused automated tests for the area you changed. Current test entry points: - `bash test/cli.sh` - CLI routing, command metadata, theme helpers, and safe dispatch coverage - `bash test/shell.sh` - all Omarchy shell tests under `test/shell/` -New Omarchy shell tests should live in `test/shell/*-test.sh` so `test/shell.sh` picks them up automatically. +New Omarchy shell tests should live in `test/shell/*-test.sh` so `test/shell.sh` picks them up automatically. Source `test/shell/base-test.sh` for shared root-path discovery, assertions, and Node test helpers. For visual changes, such as omarchy-shell styling, desktop appearance, screenshots, or screen recording flows, verify with the running UI in addition to automated tests. Take and analyze screenshots with `omarchy capture screenshot fullscreen save`. For animation, transitions, capture, or screen recording behavior, make a short recording with `omarchy screenrecord --fullscreen`, stop it with `omarchy screenrecord --stop-recording`, and review the output before finishing. diff --git a/shell/plugins/emojis/EmojiSearch.js b/shell/plugins/emojis/EmojiSearch.js new file mode 100644 index 00000000..b36fa58c --- /dev/null +++ b/shell/plugins/emojis/EmojiSearch.js @@ -0,0 +1,46 @@ +function parseEmojis(raw) { + try { + var data = JSON.parse(String(raw || "")) + return Array.isArray(data) ? data : [] + } catch (e) { + return [] + } +} + +function normalizedQuery(query) { + return String(query || "").trim().toLowerCase() +} + +function keywordText(item) { + return String((item && item.k) || "").toLowerCase() +} + +function filterEmojis(emojis, query, limit) { + var values = Array.isArray(emojis) ? emojis : [] + var needle = normalizedQuery(query) + var max = limit === undefined || limit === null ? 1000 : Number(limit) + if (isNaN(max)) max = 1000 + max = Math.max(0, max) + if (max === 0) return [] + + var out = [] + + for (var i = 0; i < values.length; i++) { + var item = values[i] + if (!item || !item.e) continue + if (!needle || keywordText(item).indexOf(needle) >= 0) { + out.push(item) + if (out.length >= max) break + } + } + + return out +} + +if (typeof module !== "undefined") { + module.exports = { + parseEmojis: parseEmojis, + normalizedQuery: normalizedQuery, + filterEmojis: filterEmojis + } +} diff --git a/shell/plugins/emojis/Emojis.qml b/shell/plugins/emojis/Emojis.qml index 5cced23e..5040b96a 100644 --- a/shell/plugins/emojis/Emojis.qml +++ b/shell/plugins/emojis/Emojis.qml @@ -3,6 +3,7 @@ import Quickshell.Io import Quickshell.Wayland import QtQuick import qs.Commons +import "EmojiSearch.js" as EmojiSearch Item { id: root @@ -65,26 +66,12 @@ Item { } function loadEmojis(raw) { - try { - var data = JSON.parse(raw) - root.emojis = data || [] - } catch (e) { - console.warn("Failed to parse emojis.json:", e) - root.emojis = [] - } + root.emojis = EmojiSearch.parseEmojis(raw) if (root.opened) root.rebuildDisplay() } function rebuildDisplay() { - var query = root.filterText.trim().toLowerCase() - var out = [] - for (var i = 0; i < root.emojis.length; i++) { - var item = root.emojis[i] - if (!query || item.k.indexOf(query) >= 0) { - out.push(item) - if (out.length >= 1000) break // limit to keep it fast - } - } + var out = EmojiSearch.filterEmojis(root.emojis, root.filterText, 1000) root.filteredEmojis = out displayModel.clear() diff --git a/test/shell.sh b/test/shell.sh index 4899f877..6ff004a8 100644 --- a/test/shell.sh +++ b/test/shell.sh @@ -6,7 +6,11 @@ ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) TEST_DIR="$ROOT/test/shell" shopt -s nullglob -tests=("$TEST_DIR"/*-test.sh) +tests=() +for test in "$TEST_DIR"/*-test.sh; do + [[ $(basename "$test") == "base-test.sh" ]] && continue + tests+=("$test") +done shopt -u nullglob if (( ${#tests[@]} == 0 )); then diff --git a/test/shell/base-test.sh b/test/shell/base-test.sh new file mode 100644 index 00000000..ecc8f8f4 --- /dev/null +++ b/test/shell/base-test.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +if [[ ${BASH_SOURCE[0]} == "$0" ]]; then + echo "source test/shell/base-test.sh from a shell test; do not run it directly" >&2 + exit 1 +fi + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +SHELL_TEST_DIR="$ROOT/test/shell" + +export ROOT + +pass() { + printf 'ok - %s\n' "$1" +} + +fail() { + local description="$1" + local detail="${2:-}" + + [[ -n $detail ]] && printf '%s\n' "$detail" >&2 + printf 'not ok - %s\n' "$description" >&2 + exit 1 +} + +require_command() { + local command="$1" + + command -v "$command" >/dev/null || fail "required command is available: $command" +} + +run_node_test() { + require_command node + + { + cat <<'JS_PRELUDE' +const path = require('path') +const root = process.env.ROOT + +function fail(description, detail) { + if (detail) console.error(detail) + console.error(`not ok - ${description}`) + process.exit(1) +} + +function pass(description) { + console.log(`ok - ${description}`) +} + +function assert(condition, description, detail) { + if (!condition) fail(description, detail) + pass(description) +} + +function assertEqual(actual, expected, description) { + assert( + actual === expected, + description, + `expected: ${expected}\nactual: ${actual}` + ) +} + +function assertDeepEqual(actual, expected, description) { + const actualJson = JSON.stringify(actual) + const expectedJson = JSON.stringify(expected) + assert( + actualJson === expectedJson, + description, + `expected: ${expectedJson}\nactual: ${actualJson}` + ) +} + +function requireFromRoot(relativePath) { + return require(path.join(root, relativePath)) +} + +JS_PRELUDE + cat + } | node +} diff --git a/test/shell/emojis-test.sh b/test/shell/emojis-test.sh new file mode 100644 index 00000000..58981cc7 --- /dev/null +++ b/test/shell/emojis-test.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +run_node_test <<'JS' +const fs = require('fs') +const emojis = requireFromRoot('shell/plugins/emojis/EmojiSearch.js') + +const raw = fs.readFileSync(path.join(root, 'shell/plugins/emojis/emojis.json'), 'utf8') +const data = emojis.parseEmojis(raw) + +assert(data.length > 1000, 'emoji dataset parses') +assertDeepEqual(emojis.parseEmojis('{'), [], 'invalid emoji JSON parses as empty list') +assertDeepEqual(emojis.parseEmojis('{"e":"nope"}'), [], 'non-array emoji JSON parses as empty list') + +const fixture = [ + { e: 'a', k: 'grinning face smile happy' }, + { e: 'b', k: 'face with tears of joy joy tears' }, + { e: 'c', k: 'flag: united states us america' } +] + +assertDeepEqual( + emojis.filterEmojis(fixture, ' JOY ').map(item => item.e), + ['b'], + 'emoji filtering trims and lowercases query' +) + +assertDeepEqual( + emojis.filterEmojis(fixture, '', 2).map(item => item.e), + ['a', 'b'], + 'emoji filtering honors result limit' +) + +assertDeepEqual( + emojis.filterEmojis(fixture, '', 0), + [], + 'emoji filtering supports zero result limit' +) + +assertEqual( + emojis.filterEmojis(data, 'face with tears')[0].e, + '\u{1F602}', + 'emoji filtering finds face with tears of joy' +) +JS diff --git a/test/shell/launcher-search-test.sh b/test/shell/launcher-search-test.sh index 8c832bf1..45b08ef6 100644 --- a/test/shell/launcher-search-test.sh +++ b/test/shell/launcher-search-test.sh @@ -2,26 +2,10 @@ set -euo pipefail -ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) -export ROOT +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" -node <<'JS' -const search = require(`${process.env.ROOT}/shell/plugins/launcher/LauncherSearch.js`) - -function fail(description, detail) { - if (detail) console.error(detail) - console.error(`not ok - ${description}`) - process.exit(1) -} - -function pass(description) { - console.log(`ok - ${description}`) -} - -function assert(condition, description, detail) { - if (!condition) fail(description, detail) - pass(description) -} +run_node_test <<'JS' +const search = requireFromRoot('shell/plugins/launcher/LauncherSearch.js') const entries = [ { @@ -69,11 +53,7 @@ const entries = [ ] const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry)) -assert( - contactMatches.length === 1 && contactMatches[0] === 'Google Contacts', - 'contact search only returns direct contact matches', - `matches: ${contactMatches.join(', ')}` -) +assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches') assert( search.fuzzyScore(entries[1], 'contact') < 0, @@ -81,14 +61,8 @@ assert( ) const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry)) -assert( - acronymMatches[0] === 'Google Contacts', - 'short acronym matching still works' -) +assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works') const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry)) -assert( - directMatches[0] === 'OBS Studio', - 'direct app-name matching still works' -) +assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works') JS