Rebuild the editor as a Vue app: sidebar, pages side by side, tile settings in a drawer (0.2.74)

The page is now web/ (Vue 3 + Vite + TypeScript), built into screen_manager/app/static
with hashed file names; server.py serves index.html as built and /assets/. The Python
API, the data and the firmware are unchanged. Your screens sit in a sidebar, the pages
of a screen stand next to each other at the screen's own shape, the library sits on the
right, and a tile's settings slide in as a drawer while the mockup stays in view. New
screen, Firmware & USB, Alerts, Override YAML and Settings are pages of their own.
Fonts moved to web/src/assets; the tests read the source through tests/editor_sources.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 07:48:45 +02:00
parent 9f5694b9eb
commit 510ebdf885
72 changed files with 5486 additions and 5409 deletions
+25
View File
@@ -0,0 +1,25 @@
"""The editor's source, for the tests that read it.
The page is a Vue app in web/ (app 0.2.74); `npm run build` writes it to screen_manager/app/static. Tests look at
the source, not the build: every .ts and .vue file under web/src as one text (SCRIPT, which is also the markup:
a .vue file carries its template), and the stylesheets as another (CSS).
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WEB = ROOT / 'web/src'
STATIC = ROOT / 'screen_manager/app/static'
FILES = sorted(p for p in WEB.rglob('*') if p.suffix in ('.ts', '.vue'))
SCRIPT = '\n'.join(p.read_text() for p in FILES)
PAGE = SCRIPT
CSS = '\n'.join(p.read_text() for p in sorted(WEB.glob('styles/*.css')))
def source(name):
"""One file by name, e.g. 'store.ts' or 'components/Sidebar.vue'."""
return (WEB / name).read_text()
def component(name):
return source(f'components/{name}.vue')
+5 -6
View File
@@ -182,14 +182,13 @@ class Broadcast(unittest.IsolatedAsyncioTestCase):
class Page(unittest.TestCase):
def test_cheatsheet_explains_the_event(self):
html = (STATIC / 'index.html').read_text()
cheatsheet = html.split('id="alerts-dialog"', 1)[1]
for marker in ('id="alerts-all"', 'data-jump="alerts-all"', 'id="alerts-all-example"', 'id="alerts-all-copy"',
import editor_sources
cheatsheet = editor_sources.component('AlertsView')
for marker in ('id="alerts-all"', '["alerts-all", "All screens"]', 'id="alerts-all-example"', 'id="alerts-all-copy"',
'esp_screens_show_alert', 'esp_screens_dismiss_alert', 'Settings → Claude'):
self.assertIn(marker, cheatsheet, marker)
script = (STATIC / 'app.js').read_text()
for marker in ('function alertAllYaml', 'broadcast?.show', 'renderAlertBroadcast();', '#alerts-all-copy").onclick'):
self.assertIn(marker, script, marker)
for marker in ('const allYaml = computed', 'broadcast?.show', 'copyText(allYaml, undefined, \'YAML\')'):
self.assertIn(marker, cheatsheet, marker)
if __name__ == '__main__':
+5 -5
View File
@@ -55,13 +55,13 @@ class ReferenceTests(unittest.TestCase):
self.assertLessEqual(parse_version(ALERT_MIN_FIRMWARE), parse_version(FIRMWARE_VERSION))
def test_page_carries_the_cheatsheet(self):
html = (STATIC / 'index.html').read_text()
import editor_sources
html = editor_sources.PAGE
for marker in ('id="open-alerts"', 'id="alerts-dialog"', 'id="close-alerts"', 'id="alerts-screens"', 'id="alerts-icons"', 'id="alerts-colors"', 'id="alerts-fields"', 'id="alerts-example"'):
self.assertIn(marker, html, marker)
script = (STATIC / 'app.js').read_text()
for marker in ('inventory.alerts', 'alert_action', 'dismiss_action', 'function renderAlerts', '#open-alerts'):
self.assertIn(marker, script, marker)
self.assertIn('#alerts-dialog', (STATIC / 'style.css').read_text())
for marker in ('inventory.alerts', 'alert_action', 'dismiss_action', 'AlertsView', 'go(\'#alerts\')'):
self.assertIn(marker, editor_sources.SCRIPT, marker)
self.assertIn('.alert-icon-grid', editor_sources.CSS)
HAS_AIOHTTP = importlib.util.find_spec('aiohttp') is not None
if HAS_AIOHTTP:
+27 -33
View File
@@ -1,26 +1,25 @@
"""Opening ESP Screens.
App 0.2.58: the page asks for the full inventory and opens the live stream at the same time. An add-on that is
building firmware answers the stream first, whose light payload has screens but no entities, so a screen opened
then had an empty catalogue: the entity list said "No entities found" and the tiles showed entity ids until a
filter chip was pressed. The first full inventory now draws those parts again.
building firmware answers the stream first, whose light payload has screens but no entities. The page keeps the
catalogue it has on a light poll and every name on the page follows the inventory, so the first full inventory
fills in what was missing.
App 0.2.65: the page no longer opens the first screen by itself; the owner picks one. Until then a card asks for
that, or offers the install when there are no screens yet. A screen picked before the full inventory arrives
still gets its entity names from the redraw above.
that, or offers the install when there are no screens yet.
App 0.2.74: the page is the Vue app in web/; these tests read its source (tests/editor_sources.py).
"""
import re
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = (ROOT / 'screen_manager/app/static/app.js').read_text()
PAGE = (ROOT / 'screen_manager/app/static/index.html').read_text()
sys.path.insert(0, str(Path(__file__).resolve().parent))
import editor_sources # noqa: E402
def function(name):
start = SCRIPT.index(f'function {name}(')
return SCRIPT[start:SCRIPT.index('\n}\n', start)]
SCRIPT = editor_sources.SCRIPT
STORE = editor_sources.source('store.ts')
class Startup(unittest.TestCase):
@@ -28,33 +27,28 @@ class Startup(unittest.TestCase):
self.assertNotIn('inventory.screens[0]', SCRIPT)
# The one way into a screen is its button in the list.
self.assertEqual(re.findall(r'(?<![\w.])(?<!function )select\(([^)]*)\)', SCRIPT), ['screen.id'])
self.assertIn('@click="select(screen.id)"', editor_sources.component('Sidebar'))
for name in ('refresh', 'applyLive'):
self.assertNotRegex(function(name), r'(?<![\w.])select\(', name)
body = STORE[STORE.index(f'function {name}('):]
self.assertNotRegex(body[:body.index('\n}\n')], r'(?<![\w.])select\(', name)
def test_the_right_side_asks_for_a_screen_until_one_is_chosen(self):
choose = re.search(r'<section id="choose" class="empty" hidden>(.*?)</section>', PAGE, re.S)
self.assertTrue(choose, 'hidden until the first inventory')
empty = editor_sources.component('EmptyState')
choose = re.search(r'<section v-if="state.inventory.screens.length" id="choose" class="empty">(.*?)</section>', empty, re.S)
self.assertTrue(choose, 'the card asks for a screen while there are screens')
self.assertIn('<h2>Choose a screen</h2>', choose[1])
self.assertNotIn('<button', choose[1], 'the list beside it is the choice')
self.assertIn('<section id="empty" class="empty" hidden>', PAGE, 'no install card flashing while the page loads')
self.assertIn('<section id="editor" hidden>', PAGE)
body = function('renderScreens')
self.assertIn('if (!selected) {\n $("#choose").hidden = !inventory.screens.length;\n'
' $("#empty").hidden = !!inventory.screens.length;\n }', body)
self.assertLess(body.index('$("#choose").hidden'), body.index('if ($("#screens .screen-host"))'),
'also while an address form keeps the list')
opened = function('select')
for part in ('$("#editor").hidden = false;', '$("#empty").hidden = true;', '$("#choose").hidden = true;'):
self.assertIn(part, opened)
self.assertIn('id="empty" class="empty"', empty, 'the install card when there are no screens yet')
self.assertIn('id="start"', empty)
app = editor_sources.source('App.vue')
self.assertIn('currentScreen.value && state.layout ? ScreenView : EmptyState', app)
def test_the_first_full_inventory_draws_what_needs_entities_again(self):
body = function('refresh')
first = body.index('const firstCatalogue = full && !inventory.entities?.length;')
self.assertLess(first, body.index('inventory = full ? data'), 'decided before the catalogue is replaced')
redraw = re.search(r'if \(firstCatalogue && selected && layout && !drag\.active && !chipDrag\.active\) '
r'\{ renderTopbar\(\); renderTiles\(\); renderResults\(\); \}', body)
self.assertTrue(redraw, 'tiles, top bar and entity list are drawn again, never in the middle of a drag')
self.assertRegex(function('select'), r'renderTopbar\(\);\s+renderTiles\(\);\s+renderResults\(\);')
def test_a_light_poll_keeps_the_catalogue_and_names_follow_the_inventory(self):
self.assertIn('state.inventory = full ? data : { ...state.inventory, ...data };', STORE)
self.assertIn('inventory?light=1', STORE)
# Names come from the inventory at render time, so they appear as soon as the full inventory does.
self.assertIn('state.inventory.entities.find((e) => e.id === id)?.name', STORE)
self.assertIn('entityName(props.tile.entity)', editor_sources.component('TileCard'))
if __name__ == '__main__':
+1 -1
View File
@@ -17,7 +17,7 @@ from core import FIRMWARE_VERSION, HEADER_MIN_FIRMWARE, discover, header_items,
FIRMWARE = (ROOT / 'components/smart_display/header_bar.h').read_text()
TILES = (ROOT / 'components/smart_display/runtime_tiles.h').read_text()
EDITOR = (ROOT / 'screen_manager/app/static/app.js').read_text()
EDITOR = (ROOT / 'web/src/model/topbar.ts').read_text()
PROFILES = ('guition-4848s040.yaml', 'home-like-2432s028.yaml')
def state(value, last_changed='2026-09-13T14:00:00+00:00', **attributes):
+23 -11
View File
@@ -1,11 +1,11 @@
"""The page loads its own script and styles by content stamp (app 0.2.58).
"""The page loads its own script and styles by content hash (app 0.2.58, Vite build since 0.2.74).
After updating to 0.2.57, Safari showed an empty Screen settings panel: it kept a cached script from before
the update. The page now asks for app.js and style.css with a stamp of their content, so a new version is
always fetched and an old one is never combined with a new page.
the update. Vite names every built file after a hash of its content and index.html asks for those names, so a
new version is always fetched and an old one is never combined with a new page.
"""
import hashlib
import importlib.util
import re
import tempfile
import unittest
from pathlib import Path
@@ -15,28 +15,40 @@ STATIC = ROOT / 'screen_manager/app/static'
HAS_AIOHTTP = importlib.util.find_spec('aiohttp') is not None
class Build(unittest.TestCase):
def test_the_build_is_committed_and_names_its_files_by_hash(self):
page = (STATIC / 'index.html').read_text()
script = re.search(r'<script type="module" crossorigin src="\./(assets/index-[\w-]+\.js)"></script>', page)
styles = re.search(r'<link rel="stylesheet" crossorigin href="\./(assets/index-[\w-]+\.css)">', page)
self.assertTrue(script and styles, 'index.html is the Vite build of web/ (cd web && npm run build)')
for name in (script[1], styles[1]):
self.assertTrue((STATIC / name).exists(), name)
self.assertFalse((STATIC / 'app.js').exists(), 'the old script is gone')
self.assertEqual(len(list((STATIC / 'assets').glob('index-*.js'))), 1, 'one build at a time: npm run build clears assets/')
@unittest.skipUnless(HAS_AIOHTTP, 'Run using .venv-portal/bin/python for server tests')
class Assets(unittest.IsolatedAsyncioTestCase):
async def test_script_and_styles_carry_a_stamp_of_their_content(self):
async def test_the_server_serves_the_page_and_its_hashed_assets(self):
import sys
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
sys.path.insert(0, str(ROOT / 'tests'))
import test_scaling
from aiohttp.test_utils import TestClient, TestServer
from server import Manager, create_app
stamp = hashlib.sha1((STATIC / 'app.js').read_bytes() + (STATIC / 'style.css').read_bytes()).hexdigest()[:12]
page = (STATIC / 'index.html').read_text()
script = re.search(r'src="\./(assets/index-[\w-]+\.js)"', page)[1]
with tempfile.TemporaryDirectory() as tmp:
manager = Manager(test_scaling.fake_ha(), Path(tmp) / 'screens.json')
async with TestClient(TestServer(create_app(manager, True))) as client:
response = await client.get('/')
page = await response.text()
self.assertEqual(response.status, 200)
self.assertIn(f'<script src="static/app.js?v={stamp}" defer></script>', page)
self.assertIn(f'<link rel="stylesheet" href="static/style.css?v={stamp}" />', page)
self.assertEqual(await response.text(), page)
self.assertEqual(response.headers['Cache-Control'], 'no-store')
self.assertIn('text/html', response.headers['Content-Type'])
script = await client.get(f'/static/app.js?v={stamp}')
self.assertEqual((script.status, await script.read()), (200, (STATIC / 'app.js').read_bytes()))
asset = await client.get(f'/{script}')
self.assertEqual((asset.status, await asset.read()), (200, (STATIC / script).read_bytes()))
self.assertEqual((await client.get('/static/app.js')).status, 404)
if __name__ == '__main__':
+13 -11
View File
@@ -545,21 +545,23 @@ class SettingsRoute(unittest.IsolatedAsyncioTestCase):
class Editor(unittest.TestCase):
def setUp(self):
self.html = (STATIC / 'index.html').read_text()
self.script = (STATIC / 'app.js').read_text()
self.css = (STATIC / 'style.css').read_text()
import editor_sources
self.html = editor_sources.PAGE
self.script = editor_sources.SCRIPT
self.css = editor_sources.CSS
self.tab = editor_sources.component('SettingsTab')
def test_the_panel_replaces_the_form(self):
self.assertIn('<section class="screen-settings" id="general-settings"', self.html)
self.assertIn('<div class="settings" id="general-settings"', self.tab)
for element in ('settings-groups', 'settings-status'):
self.assertEqual(self.html.count(f'id="{element}"'), 1)
self.assertNotIn('settings-fields', self.html + self.script)
self.assertNotIn('settingDefinitions', self.script)
for rule in ('.settings-card', '.setting-row', '.switch[aria-checked="true"]', '.switch.unknown::after', '.step', '.setting-value'):
for rule in ('.set-card', '.srow', '.switch[aria-checked="true"]', '.switch.unknown::after', '.step', '.step output'):
self.assertIn(rule, self.css)
# A value Home Assistant does not have (offline, entity off) shows as unknown, not as a default.
self.assertIn('if (value === null || value === undefined) return "—";', self.script)
self.assertIn('refs.toggle.classList.toggle("unknown", values[key] === null || values[key] === undefined);', self.script)
self.assertIn(':class="{ unknown: values[row.key] === null || values[row.key] === undefined }"', self.tab)
def test_the_rows_are_the_settings_of_the_screen_page(self):
keys = re.findall(r'\{ key: "(\w+)", label: "[^"]+", kind: "(\w+)"', self.script)
@@ -567,21 +569,21 @@ class Editor(unittest.TestCase):
self.assertEqual({key for key, _ in keys}, set(SETTING_RULES) - {'show_clock'})
# Labels and steps as on the screen: the same rows, the same -/+ steps, the same duration ladder.
page_labels = set(re.findall(r'(?:number|toggle|duration|moment|choice)\("([^"]+)"', SCREEN_PAGE))
groups = self.script[self.script.index('const SETTING_GROUPS = ['):self.script.index('const SETTING_ROWS =')]
groups = self.script[self.script.index('const SETTING_GROUPS = ['):self.script.index('export type SettingRow')]
script_labels = set(re.findall(r'label: "([^"]+)"', groups))
self.assertEqual(page_labels, script_labels)
for key, step in (('brightness', 5), ('standby_brightness', 5), ('night_brightness', 5)):
self.assertRegex(self.script, rf'key: "{key}", [^}}]*step: {step}')
ladder = re.search(r'if \(seconds < 300\) return 30;\s+if \(seconds < 900\) return 60;\s+if \(seconds < 3600\) return 300;\s+'
r'if \(seconds < 7200\) return 900;\s+return 1800;', SCREEN_PAGE)
self.assertTrue(ladder, 'the ladder in settings_screen.h changed: change ladderStep in app.js with it')
self.assertTrue(ladder, 'the ladder in settings_screen.h changed: change ladderStep in web/src/store.ts with it')
self.assertIn('seconds < 300 ? 30 : seconds < 900 ? 60 : seconds < 3600 ? 300 : seconds < 7200 ? 900 : 1800', self.script)
def test_settings_have_their_own_call_and_save_leaves_them_out(self):
self.assertIn('api(`screens/${encodeURIComponent(screen)}/settings`', self.script)
self.assertIn('const { settings: _settings, ...tiles } = layout;', self.script)
self.assertIn('body: JSON.stringify(tiles)', self.script)
self.assertIn('setSetting("clock_24h", v === "24", 150);', self.script, 'the top bar clock format is the same setting')
self.assertIn('const { settings: _settings, ...tiles } = state.layout;', self.script)
self.assertIn('await send(`screens/${encodeURIComponent(state.selected)}`, "PUT", tiles);', self.script)
self.assertIn("setSetting('clock_24h', v === '24', 150)", self.script, 'the top bar clock format is the same setting')
class Firmware(unittest.TestCase):
+34 -32
View File
@@ -1,51 +1,53 @@
"""Settings (app 0.2.45): New screen, Firmware & USB, firmware updates, Alerts and Claude moved off the main
page into a view of their own, so the header keeps one button and My screens keeps the editor."""
from pathlib import Path
page into a view of their own. App 0.2.74: the sidebar carries the way in (New screen, Firmware & USB, Alerts,
Settings), each once; the Settings page keeps the updates and Claude."""
import re
import sys
import unittest
from pathlib import Path
STATIC = Path(__file__).resolve().parents[1] / 'screen_manager/app/static'
def between(text, start, end):
return text.split(start, 1)[1].split(end, 1)[0]
sys.path.insert(0, str(Path(__file__).resolve().parent))
import editor_sources # noqa: E402
class SettingsView(unittest.TestCase):
def setUp(self):
self.html = (STATIC / 'index.html').read_text()
self.script = (STATIC / 'app.js').read_text()
self.css = (STATIC / 'style.css').read_text()
self.sidebar = editor_sources.component('Sidebar')
self.settings = editor_sources.component('AppSettingsView')
self.script = editor_sources.SCRIPT
self.css = editor_sources.CSS
def test_header_keeps_only_the_settings_button(self):
header = between(self.html, '<header>', '</header>')
self.assertEqual(re.findall(r'<button id="([\w-]+)"', header), ['open-settings'])
def test_the_sidebar_is_the_one_way_to_the_tools(self):
more = self.sidebar.split('<div class="more">', 1)[1]
self.assertEqual(re.findall(r'<button id="([\w-]+)"', more), ['open-firmware', 'open-alerts', 'open-settings', 'refresh'])
self.assertEqual(self.sidebar.count('id="new-screen"'), 1)
for element in ('new-screen', 'open-firmware', 'open-alerts', 'open-settings'):
self.assertEqual(editor_sources.PAGE.count(f'id="{element}"'), 1, element)
def test_every_tool_lives_in_the_settings_view_once(self):
settings = between(self.html, '<section id="settings-view"', '</main>')
home = between(self.html, '<div id="home-view">', '<section id="settings-view"')
for element in ('new-screen', 'open-firmware', 'updates', 'update-all', 'auto-update', 'open-alerts',
'claude-install', 'claude-status', 'claude-path', 'claude-download', 'close-settings'):
self.assertEqual(self.html.count(f'id="{element}"'), 1, element)
self.assertIn(f'id="{element}"', settings, element)
self.assertNotIn(f'id="{element}"', home, element)
for element in ('updates', 'update-all', 'auto-update', 'claude-install', 'claude-status', 'claude-path', 'claude-download', 'close-settings'):
self.assertEqual(editor_sources.PAGE.count(f'id="{element}"'), 1, element)
self.assertIn(f'id="{element}"', self.settings, element)
# The first-run button stays where a new user starts.
self.assertIn('id="start"', home)
self.assertIn('href="api/claude-skill.zip"', settings)
self.assertIn('id="start"', editor_sources.component('EmptyState'))
self.assertIn('href="api/claude-skill.zip"', self.settings)
def test_the_view_switches_by_hash_and_renders_what_it_shows(self):
for marker in ('function showView', 'location.hash === "#settings"', 'window.addEventListener("hashchange", showView)',
'$("#home-view").hidden = settings', 'function renderClaude', 'api("claude-skill", { method: "POST" })',
'if (full) renderClaude();', '$("#new-screen").onclick = $("#start").onclick = openInstaller'):
self.assertIn(marker, self.script, marker)
self.assertNotIn('Not flashed yet? Firmware & USB', self.script, 'texts point to Settings')
for marker in ('.settings-groups', '.settings-group', '.claude-skill', '.screen:hover:not(.selected)'):
store = editor_sources.source('store.ts')
for marker in ('export const routes = ["", "#settings", "#new-screen", "#firmware", "#alerts", "#override"] as const;',
'window.addEventListener("hashchange"', 'export async function installClaudeSkill', 'send("claude-skill", "POST")'):
self.assertIn(marker, store, marker)
app = editor_sources.source('App.vue')
for marker in ('route.value === "#settings") return AppSettingsView', 'route.value === "#new-screen") return InstallerView',
'route.value === "#firmware") return FirmwareView', 'route.value === "#alerts") return AlertsView'):
self.assertIn(marker, app, marker)
for marker in ('.side', '.nav-item[aria-current="true"]', '.panel', '.card'):
self.assertIn(marker, self.css, marker)
def test_unselected_screens_get_a_grey_card(self):
rule = between(self.css, '\n.screen {', '}')
self.assertIn('background: #e9ecf1', rule)
self.assertIn('background: white', between(self.css, '\n.screen.selected {', '}'))
def test_the_chosen_screen_is_marked_in_the_list(self):
self.assertIn(':aria-current="screen.id === state.selected && route === \'\' ? \'true\' : \'false\'"', self.sidebar)
rule = self.css.split('\n.nav-item[aria-current="true"] {', 1)[1].split('}', 1)[0]
self.assertIn('background: var(--seg)', rule)
if __name__ == '__main__':
+10 -7
View File
@@ -220,16 +220,19 @@ class SavingAndTheEditor(unittest.IsolatedAsyncioTestCase):
class Editor(unittest.TestCase):
SCRIPT = (ROOT / 'screen_manager/app/static/app.js').read_text()
def setUp(self):
import editor_sources
self.SCRIPT = editor_sources.SCRIPT
self.PICKER = editor_sources.component('ActionPicker')
def test_perform_action_is_a_tap_choice_with_home_assistants_list(self):
self.assertIn('taps.push(["action", "Perform action"]);', self.SCRIPT)
self.assertIn('api(`entity-actions?entity=${encodeURIComponent(entity)}`)', self.SCRIPT)
self.assertIn('if (tap === "action") body.append(actionPicker(tile));', self.SCRIPT)
self.assertIn('list.push(["action", "Perform action"]);', self.SCRIPT)
self.assertIn('getJson(`entity-actions?entity=${encodeURIComponent(entity)}`)', self.SCRIPT)
self.assertIn('<ActionPicker v-if="domain !== \'screen\' && tap === \'action\'" :tile="tile" />', self.SCRIPT)
# Fields follow Home Assistant's selectors; an empty field is left out of the data.
for marker in ('kind === "boolean"', 'kind === "number" || kind === "color_temp"', 'config.options', 'delete data[field.key]'):
self.assertIn(marker, self.SCRIPT)
self.assertIn('supportsFirmware(0, 2, 58)', self.SCRIPT)
for marker in ("kindOf(field) === 'boolean'", "kindOf(field) === 'number' || kindOf(field) === 'color_temp'", 'config.options', 'delete data[key]'):
self.assertIn(marker, self.PICKER, marker)
self.assertIn('supports(0, 2, 58)', self.PICKER)
if __name__ == '__main__':
+1 -1
View File
@@ -151,7 +151,7 @@ class Setting(unittest.TestCase):
page = (COMPONENT / 'settings_screen.h').read_text()
self.assertIn('toggle("Dark mode", []() -> int32_t { return dark_mode; },', page)
self.assertIn('else if (key == "dark_mode") reported = dark_mode = flag(value);', page)
editor = (ROOT / 'screen_manager/app/static/app.js').read_text()
editor = (ROOT / 'web/src/store.ts').read_text()
self.assertIn('{ key: "dark_mode", label: "Dark mode", kind: "toggle" },', editor)
brightness = editor.split('{ title: "Brightness"', 1)[1].split('] },', 1)[0]
self.assertLess(brightness.index('"brightness"'), brightness.index('"dark_mode"'), 'right under Brightness, as on the screen')
+1 -1
View File
@@ -28,7 +28,7 @@ class IconSetTests(unittest.TestCase):
block = text.split('glyphs: &tile_icons', 1)[1].split('\n\n', 1)[0]
self.assertEqual(re.findall(r'- "(\\U000F[0-9A-F]{4})"', block), wanted, name)
self.assertNotIn('MDI_GLYPH', text, name)
self.assertTrue((ROOT / 'screen_manager/app/static/tile-icons.woff').exists())
self.assertTrue((ROOT / 'web/src/assets/tile-icons.woff').exists())
def test_firmware_name_table_mirrors_the_set(self):
header = (ROOT / 'components/smart_display/tile_icon_names.h').read_text()