Skip to content

Commit 238f9c0

Browse files
committed
Give the triggers and conditions an icon
Ten triggers and seven conditions shipped without any, because Spook had no `icons.json` at all. Nothing failed and nothing logged it: they simply drew without an icon, and the only way to notice was to open the automation editor and look, which is how Frenck found it. Each one is picked for what it does rather than what it is called, taken from the descriptions in the translations. `watchdog` fires when something expected does not happen in time, so it is a timer with an alert and not a dog. Integrations are a puzzle piece the way Home Assistant draws them, the three repair ones share the wrench family the repairs sensors already use, and `triggered_by_user` and `triggered_by_automation` borrow the account and robot that Home Assistant uses for people and automations. Every name was checked against the icon set the frontend ships, off a real installation's `iconList.json`, because a misspelled icon renders as an empty square and reports nothing. All seventeen exist. A test compares the icons against the translations in both directions, so the next trigger added either gets an icon or fails the build. Five mutations caught: dropping a trigger, dropping a condition, misspelling an icon, naming the wrong kind, and adding an icon for something that does not exist.
1 parent ba25779 commit 238f9c0

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

custom_components/spook/icons.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"conditions": {
3+
"chance": {
4+
"condition": "mdi:dice-multiple"
5+
},
6+
"cooldown": {
7+
"condition": "mdi:timer-sand"
8+
},
9+
"not_triggered_by_user": {
10+
"condition": "mdi:account-off"
11+
},
12+
"quota": {
13+
"condition": "mdi:gauge"
14+
},
15+
"repair_issue_present": {
16+
"condition": "mdi:wrench-clock"
17+
},
18+
"triggered_by_automation": {
19+
"condition": "mdi:robot"
20+
},
21+
"triggered_by_user": {
22+
"condition": "mdi:account"
23+
}
24+
},
25+
"triggers": {
26+
"condition_met": {
27+
"trigger": "mdi:check-circle"
28+
},
29+
"cron": {
30+
"trigger": "mdi:calendar-clock"
31+
},
32+
"flapping": {
33+
"trigger": "mdi:sine-wave"
34+
},
35+
"integration_failed": {
36+
"trigger": "mdi:puzzle-remove"
37+
},
38+
"repair_issue_created": {
39+
"trigger": "mdi:wrench"
40+
},
41+
"repair_issue_removed": {
42+
"trigger": "mdi:wrench-check"
43+
},
44+
"sequence": {
45+
"trigger": "mdi:format-list-numbered"
46+
},
47+
"stale": {
48+
"trigger": "mdi:signal-off"
49+
},
50+
"watchdog": {
51+
"trigger": "mdi:timer-alert"
52+
},
53+
"while": {
54+
"trigger": "mdi:repeat"
55+
}
56+
}
57+
}

tests/test_icons.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Tests that every trigger and condition Spook adds has an icon.
2+
3+
Spook shipped ten triggers and seven conditions with no `icons.json` at all,
4+
so every one of them rendered without an icon. Nothing failed and nothing
5+
logged: the only way to notice was to open the automation editor and look.
6+
"""
7+
8+
# pylint: disable=wrong-import-order
9+
from __future__ import annotations
10+
11+
import json
12+
from pathlib import Path
13+
import re
14+
15+
import pytest
16+
17+
SPOOK = Path(__file__).parents[1] / "custom_components" / "spook"
18+
ICONS = json.loads((SPOOK / "icons.json").read_text())
19+
STRINGS = json.loads((SPOOK / "translations" / "en.json").read_text())
20+
21+
# Home Assistant keys these by kind, and the value names the kind again.
22+
_SECTIONS = (("triggers", "trigger"), ("conditions", "condition"))
23+
24+
# An `mdi:` name, lowercase with single hyphens, which is all Material Design
25+
# Icons uses. A typo here renders as an empty square rather than an error.
26+
_MDI = re.compile(r"\Amdi:[a-z0-9]+(-[a-z0-9]+)*\Z")
27+
28+
29+
def test_there_is_something_to_check() -> None:
30+
"""Guard the lookups, which would pass happily against an empty file."""
31+
assert ICONS
32+
for section, _ in _SECTIONS:
33+
assert STRINGS[section], section
34+
35+
36+
@pytest.mark.parametrize(("section", "kind"), _SECTIONS)
37+
def test_everything_translated_also_has_an_icon(section: str, kind: str) -> None:
38+
"""The translations are the list of what Spook actually adds."""
39+
described = set(STRINGS[section])
40+
iconed = set(ICONS.get(section, {}))
41+
42+
assert not described - iconed, (
43+
f"{section} without an icon: {sorted(described - iconed)}"
44+
)
45+
assert not iconed - described, (
46+
f"{section} with an icon and nothing else: {sorted(iconed - described)}"
47+
)
48+
49+
for key, entry in ICONS[section].items():
50+
assert kind in entry, f"{section}.{key} does not name its {kind}"
51+
52+
53+
@pytest.mark.parametrize(("section", "kind"), _SECTIONS)
54+
def test_every_icon_is_shaped_like_an_mdi_name(section: str, kind: str) -> None:
55+
"""A misspelled icon shows an empty square, which nothing reports."""
56+
for key, entry in ICONS[section].items():
57+
icon = entry[kind]
58+
assert _MDI.match(icon), f"{section}.{key} has {icon!r}"

0 commit comments

Comments
 (0)