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:
@@ -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')
|
||||
@@ -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__':
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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
@@ -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__':
|
||||
|
||||
@@ -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
@@ -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__':
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user