Skip to content

Commit 4277f53

Browse files
committed
Add shell emoji tests
1 parent 81f4896 commit 4277f53

7 files changed

Lines changed: 188 additions & 50 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Run focused automated tests for the area you changed. Current test entry points:
104104
- `bash test/cli.sh` - CLI routing, command metadata, theme helpers, and safe dispatch coverage
105105
- `bash test/shell.sh` - all Omarchy shell tests under `test/shell/`
106106

107-
New Omarchy shell tests should live in `test/shell/*-test.sh` so `test/shell.sh` picks them up automatically.
107+
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.
108108

109109
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.
110110

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
function parseEmojis(raw) {
2+
try {
3+
var data = JSON.parse(String(raw || ""))
4+
return Array.isArray(data) ? data : []
5+
} catch (e) {
6+
return []
7+
}
8+
}
9+
10+
function normalizedQuery(query) {
11+
return String(query || "").trim().toLowerCase()
12+
}
13+
14+
function keywordText(item) {
15+
return String((item && item.k) || "").toLowerCase()
16+
}
17+
18+
function filterEmojis(emojis, query, limit) {
19+
var values = Array.isArray(emojis) ? emojis : []
20+
var needle = normalizedQuery(query)
21+
var max = limit === undefined || limit === null ? 1000 : Number(limit)
22+
if (isNaN(max)) max = 1000
23+
max = Math.max(0, max)
24+
if (max === 0) return []
25+
26+
var out = []
27+
28+
for (var i = 0; i < values.length; i++) {
29+
var item = values[i]
30+
if (!item || !item.e) continue
31+
if (!needle || keywordText(item).indexOf(needle) >= 0) {
32+
out.push(item)
33+
if (out.length >= max) break
34+
}
35+
}
36+
37+
return out
38+
}
39+
40+
if (typeof module !== "undefined") {
41+
module.exports = {
42+
parseEmojis: parseEmojis,
43+
normalizedQuery: normalizedQuery,
44+
filterEmojis: filterEmojis
45+
}
46+
}

‎shell/plugins/emojis/Emojis.qml‎

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Quickshell.Io
33
import Quickshell.Wayland
44
import QtQuick
55
import qs.Commons
6+
import "EmojiSearch.js" as EmojiSearch
67

78
Item {
89
id: root
@@ -65,26 +66,12 @@ Item {
6566
}
6667

6768
function loadEmojis(raw) {
68-
try {
69-
var data = JSON.parse(raw)
70-
root.emojis = data || []
71-
} catch (e) {
72-
console.warn("Failed to parse emojis.json:", e)
73-
root.emojis = []
74-
}
69+
root.emojis = EmojiSearch.parseEmojis(raw)
7570
if (root.opened) root.rebuildDisplay()
7671
}
7772

7873
function rebuildDisplay() {
79-
var query = root.filterText.trim().toLowerCase()
80-
var out = []
81-
for (var i = 0; i < root.emojis.length; i++) {
82-
var item = root.emojis[i]
83-
if (!query || item.k.indexOf(query) >= 0) {
84-
out.push(item)
85-
if (out.length >= 1000) break // limit to keep it fast
86-
}
87-
}
74+
var out = EmojiSearch.filterEmojis(root.emojis, root.filterText, 1000)
8875
root.filteredEmojis = out
8976

9077
displayModel.clear()

‎test/shell.sh‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
66
TEST_DIR="$ROOT/test/shell"
77

88
shopt -s nullglob
9-
tests=("$TEST_DIR"/*-test.sh)
9+
tests=()
10+
for test in "$TEST_DIR"/*-test.sh; do
11+
[[ $(basename "$test") == "base-test.sh" ]] && continue
12+
tests+=("$test")
13+
done
1014
shopt -u nullglob
1115

1216
if (( ${#tests[@]} == 0 )); then

‎test/shell/base-test.sh‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/bin/bash
2+
3+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
4+
echo "source test/shell/base-test.sh from a shell test; do not run it directly" >&2
5+
exit 1
6+
fi
7+
8+
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
9+
SHELL_TEST_DIR="$ROOT/test/shell"
10+
11+
export ROOT
12+
13+
pass() {
14+
printf 'ok - %s\n' "$1"
15+
}
16+
17+
fail() {
18+
local description="$1"
19+
local detail="${2:-}"
20+
21+
[[ -n $detail ]] && printf '%s\n' "$detail" >&2
22+
printf 'not ok - %s\n' "$description" >&2
23+
exit 1
24+
}
25+
26+
require_command() {
27+
local command="$1"
28+
29+
command -v "$command" >/dev/null || fail "required command is available: $command"
30+
}
31+
32+
run_node_test() {
33+
require_command node
34+
35+
{
36+
cat <<'JS_PRELUDE'
37+
const path = require('path')
38+
const root = process.env.ROOT
39+
40+
function fail(description, detail) {
41+
if (detail) console.error(detail)
42+
console.error(`not ok - ${description}`)
43+
process.exit(1)
44+
}
45+
46+
function pass(description) {
47+
console.log(`ok - ${description}`)
48+
}
49+
50+
function assert(condition, description, detail) {
51+
if (!condition) fail(description, detail)
52+
pass(description)
53+
}
54+
55+
function assertEqual(actual, expected, description) {
56+
assert(
57+
actual === expected,
58+
description,
59+
`expected: ${expected}\nactual: ${actual}`
60+
)
61+
}
62+
63+
function assertDeepEqual(actual, expected, description) {
64+
const actualJson = JSON.stringify(actual)
65+
const expectedJson = JSON.stringify(expected)
66+
assert(
67+
actualJson === expectedJson,
68+
description,
69+
`expected: ${expectedJson}\nactual: ${actualJson}`
70+
)
71+
}
72+
73+
function requireFromRoot(relativePath) {
74+
return require(path.join(root, relativePath))
75+
}
76+
77+
JS_PRELUDE
78+
cat
79+
} | node
80+
}

‎test/shell/emojis-test.sh‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
6+
7+
run_node_test <<'JS'
8+
const fs = require('fs')
9+
const emojis = requireFromRoot('shell/plugins/emojis/EmojiSearch.js')
10+
11+
const raw = fs.readFileSync(path.join(root, 'shell/plugins/emojis/emojis.json'), 'utf8')
12+
const data = emojis.parseEmojis(raw)
13+
14+
assert(data.length > 1000, 'emoji dataset parses')
15+
assertDeepEqual(emojis.parseEmojis('{'), [], 'invalid emoji JSON parses as empty list')
16+
assertDeepEqual(emojis.parseEmojis('{"e":"nope"}'), [], 'non-array emoji JSON parses as empty list')
17+
18+
const fixture = [
19+
{ e: 'a', k: 'grinning face smile happy' },
20+
{ e: 'b', k: 'face with tears of joy joy tears' },
21+
{ e: 'c', k: 'flag: united states us america' }
22+
]
23+
24+
assertDeepEqual(
25+
emojis.filterEmojis(fixture, ' JOY ').map(item => item.e),
26+
['b'],
27+
'emoji filtering trims and lowercases query'
28+
)
29+
30+
assertDeepEqual(
31+
emojis.filterEmojis(fixture, '', 2).map(item => item.e),
32+
['a', 'b'],
33+
'emoji filtering honors result limit'
34+
)
35+
36+
assertDeepEqual(
37+
emojis.filterEmojis(fixture, '', 0),
38+
[],
39+
'emoji filtering supports zero result limit'
40+
)
41+
42+
assertEqual(
43+
emojis.filterEmojis(data, 'face with tears')[0].e,
44+
'\u{1F602}',
45+
'emoji filtering finds face with tears of joy'
46+
)
47+
JS

‎test/shell/launcher-search-test.sh‎

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,10 @@
22

33
set -euo pipefail
44

5-
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
6-
export ROOT
5+
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
76

8-
node <<'JS'
9-
const search = require(`${process.env.ROOT}/shell/plugins/launcher/LauncherSearch.js`)
10-
11-
function fail(description, detail) {
12-
if (detail) console.error(detail)
13-
console.error(`not ok - ${description}`)
14-
process.exit(1)
15-
}
16-
17-
function pass(description) {
18-
console.log(`ok - ${description}`)
19-
}
20-
21-
function assert(condition, description, detail) {
22-
if (!condition) fail(description, detail)
23-
pass(description)
24-
}
7+
run_node_test <<'JS'
8+
const search = requireFromRoot('shell/plugins/launcher/LauncherSearch.js')
259
2610
const entries = [
2711
{
@@ -69,26 +53,16 @@ const entries = [
6953
]
7054
7155
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
72-
assert(
73-
contactMatches.length === 1 && contactMatches[0] === 'Google Contacts',
74-
'contact search only returns direct contact matches',
75-
`matches: ${contactMatches.join(', ')}`
76-
)
56+
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
7757
7858
assert(
7959
search.fuzzyScore(entries[1], 'contact') < 0,
8060
'calculator does not match contact as a loose subsequence'
8161
)
8262
8363
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
84-
assert(
85-
acronymMatches[0] === 'Google Contacts',
86-
'short acronym matching still works'
87-
)
64+
assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works')
8865
8966
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
90-
assert(
91-
directMatches[0] === 'OBS Studio',
92-
'direct app-name matching still works'
93-
)
67+
assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works')
9468
JS

0 commit comments

Comments
 (0)