mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 10:36:35 +00:00
feat(web): Start Display on Boot toggle on the General tab
Turns ledmatrix.service's autostart on or off at once through the existing enable_autostart / disable_autostart system actions (sudo grants already in lib_sudoers.sh). The checkbox shows systemd's own state, read with `systemctl is-enabled`, so nothing is stored in config.json and the default stays on. Hidden when the state cannot be read; a refused change puts the box back and shows the route's message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -32,7 +32,8 @@ const UNIT = ['unit/test_list_filter.js', 'unit/test_render_cards.js',
|
||||
'unit/test_page_registry.js', 'unit/test_core_modules.js',
|
||||
'unit/test_overview_reconciliation_poll.js',
|
||||
'unit/test_display_partial_ids.js',
|
||||
'unit/test_general_web_login_token.js'];
|
||||
'unit/test_general_web_login_token.js',
|
||||
'unit/test_general_display_autostart.js'];
|
||||
const DOM = ['dom/test_installed_dom.js', 'dom/test_store_dom.js', 'dom/test_no_double_fetch.js',
|
||||
'dom/test_tools_sections.js', 'dom/test_cache_page.js',
|
||||
'dom/test_durations_page.js', 'dom/test_operation_history_page.js',
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// The General tab's "Start Display on Boot" checkbox (setDisplayAutostart in
|
||||
// static/v3/js/pages/general.js) posts enable_autostart / disable_autostart
|
||||
// to /api/v3/system/action and puts the box back when that is refused --
|
||||
// including the route's HTTP-200 {status: "error"} answer for a failed
|
||||
// systemctl (e.g. the sudo rule is missing).
|
||||
//
|
||||
// Fake fetch, no jsdom and no server needed.
|
||||
|
||||
const path = require('path');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
const JS = path.resolve(__dirname, '../../../web_interface/static/v3/js');
|
||||
const load = f => import(pathToFileURL(path.join(JS, f)).href);
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const ok = (label, cond, extra) => cond
|
||||
? (pass++, console.log(' ok ' + label))
|
||||
: (fail++, console.log(' FAIL ' + label + (extra !== undefined ? ' ' + JSON.stringify(extra) : '')));
|
||||
|
||||
async function run(checked, answer) {
|
||||
const { createApi } = await load('core/api.js');
|
||||
const general = await load('pages/general.js');
|
||||
const sent = [];
|
||||
const notes = [];
|
||||
const fetch = (url, init) => {
|
||||
sent.push([url, JSON.parse(init.body)]);
|
||||
if (answer === 'network') return Promise.reject(new TypeError('Failed to fetch'));
|
||||
return Promise.resolve({
|
||||
ok: answer.ok, status: answer.ok ? 200 : 500,
|
||||
headers: { get: () => null },
|
||||
text: () => Promise.resolve(JSON.stringify(answer.body)),
|
||||
});
|
||||
};
|
||||
const ctx = { root: {}, state: {}, signal: { aborted: false },
|
||||
api: createApi({ fetch }), notify: (m, t) => notes.push([m, t]) };
|
||||
const box = { checked, disabled: false };
|
||||
await general.setDisplayAutostart(ctx, box);
|
||||
return { box, sent, notes };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log('\n── General tab: Start Display on Boot ──');
|
||||
|
||||
{
|
||||
const t = await run(false, { ok: true, body: { status: 'success', message: 'ok' } });
|
||||
ok('unchecking posts disable_autostart',
|
||||
t.sent.length === 1 && t.sent[0][0] === '/api/v3/system/action' && t.sent[0][1].action === 'disable_autostart', t.sent);
|
||||
ok('...stays unchecked and re-enabled', t.box.checked === false && t.box.disabled === false);
|
||||
ok('...and reports success', t.notes.some(([, type]) => type === 'success'), t.notes);
|
||||
}
|
||||
|
||||
{
|
||||
const t = await run(true, { ok: true, body: { status: 'success' } });
|
||||
ok('checking posts enable_autostart', t.sent[0][1].action === 'enable_autostart', t.sent);
|
||||
}
|
||||
|
||||
{
|
||||
const t = await run(false, { ok: true, body: { status: 'error', message: 'sudo: a password is required' } });
|
||||
ok('a failed systemctl (HTTP 200, status error) puts the box back', t.box.checked === true && !t.box.disabled);
|
||||
ok('...and shows the route\'s message',
|
||||
t.notes.some(([m, type]) => type === 'error' && /sudo/.test(m)), t.notes);
|
||||
}
|
||||
|
||||
{
|
||||
const t = await run(true, { ok: false, body: { status: 'error', message: 'boom' } });
|
||||
ok('an HTTP error puts the box back', t.box.checked === false && !t.box.disabled);
|
||||
}
|
||||
|
||||
{
|
||||
const t = await run(true, 'network');
|
||||
ok('a network failure puts the box back', t.box.checked === false && !t.box.disabled);
|
||||
ok('...and reports it', t.notes.some(([, type]) => type === 'error'), t.notes);
|
||||
}
|
||||
|
||||
console.log(`\n${pass} passed, ${fail} failed\n`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
})().catch(e => { console.log('HARNESS ERROR: ' + e.stack); process.exit(1); });
|
||||
@@ -0,0 +1,60 @@
|
||||
"""The General tab's "Start Display on Boot" toggle reads systemd's state.
|
||||
|
||||
ledmatrix.service enabled -> checked, disabled -> unchecked, unreadable (no
|
||||
systemctl, unit not installed) -> no toggle. The toggle itself posts the
|
||||
existing enable_autostart / disable_autostart system actions, whose sudo
|
||||
grants test_systemctl_sudoers_alignment.py already checks.
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from test.test_web_settings_ui import client # noqa: F401,E402
|
||||
from web_interface.blueprints import pages_v3 # noqa: E402
|
||||
|
||||
|
||||
REAL_RUN = subprocess.run
|
||||
UNIT_QUERY = ['systemctl', 'is-enabled', 'ledmatrix.service']
|
||||
|
||||
|
||||
def _fake_systemctl(monkeypatch, answer):
|
||||
"""Answer the unit query with `answer` (stdout, or an exception to
|
||||
raise); every other command (the update channel's git) runs for real."""
|
||||
calls = []
|
||||
|
||||
def run(cmd, *args, **kwargs):
|
||||
if cmd != UNIT_QUERY:
|
||||
return REAL_RUN(cmd, *args, **kwargs)
|
||||
calls.append(cmd)
|
||||
if isinstance(answer, Exception):
|
||||
raise answer
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout=answer, stderr='')
|
||||
monkeypatch.setattr(pages_v3.subprocess, 'run', run)
|
||||
return calls
|
||||
|
||||
|
||||
def _toggle(body):
|
||||
start = body.find('data-action="display-autostart"')
|
||||
return None if start < 0 else body[start:body.find('>', start)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('stdout, checked', [('enabled\n', True), ('disabled\n', False)])
|
||||
def test_toggle_reflects_the_unit(client, monkeypatch, stdout, checked):
|
||||
calls = _fake_systemctl(monkeypatch, stdout)
|
||||
tag = _toggle(client.get('/v3/partials/general').get_data(as_text=True))
|
||||
assert calls == [UNIT_QUERY]
|
||||
assert tag is not None
|
||||
assert ('checked' in tag) is checked
|
||||
|
||||
|
||||
@pytest.mark.parametrize('outcome', [FileNotFoundError(), 'not-found\n'])
|
||||
def test_toggle_hidden_when_state_unreadable(client, monkeypatch, outcome):
|
||||
_fake_systemctl(monkeypatch, outcome)
|
||||
body = client.get('/v3/partials/general').get_data(as_text=True)
|
||||
assert _toggle(body) is None
|
||||
assert 'name="plugins_directory"' in body # rest of the tab still renders
|
||||
Reference in New Issue
Block a user