mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 18:46:37 +00:00
container.dataset.ownedKeys round-trips schema property keys through a DOM dataset attribute, and the takeover handoff spliced each one straight into '[data-child-key="' + k + '"]' with no escaping -- inconsistent with this codebase's own convention elsewhere (plugin-file-manager.js, app-shell.js's escapeCssSelector) for building a selector from a dynamic value. A key containing a quote or backslash would break the selector or be steerable; Codacy's static analysis flagged this pattern (1 high ErrorProne finding on PR #569, current head at the time) as a new issue, though its dashboard is unreachable from this sandbox (egress to app.codacy.com is blocked) and the check-run API returned no detail text -- verified and fixed by reading the diff directly rather than the tool's own description. Added a source-assertion regression test alongside this file's existing ones (this behavior lives in an inline script no Python test executes). Full suite: 4888 passed, 62 skipped, 2 failed -- both the pre-existing Europe/Kiev/Asia/Calcutta tzdata-alias gap in this sandbox, identical on origin/main, unrelated to this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
6.2 KiB
Python
138 lines
6.2 KiB
Python
"""When the style editor takes over a customization block, it takes over
|
|
exactly its own part of it -- and it actually gets to take over at all.
|
|
|
|
Two defects this pins, both found by rendering the real partial in a browser
|
|
rather than by reading the code:
|
|
|
|
1. The hand-off guard asked "do any fallback controls differ from their
|
|
server-rendered defaults?" as a proxy for "is someone editing this?". The
|
|
fallback contains this block's own font fields, and the font-selector
|
|
widget populates them on the same 50ms timer -- so a plain page load with
|
|
nobody touching anything raced into "dirty" (seven customization.*.font
|
|
selects, measured ~60ms after injection) and the style editor removed
|
|
itself, leaving the accordion form it exists to replace. The question is
|
|
whether a *person* typed, and isTrusted answers exactly that.
|
|
|
|
2. Taking over used to remove the whole fallback section. A customization
|
|
block can hold more than styling -- football keeps favorite_result_colors
|
|
there -- so that removed the only UI those fields had.
|
|
|
|
These assert on the template source because the behaviour lives in an inline
|
|
script that no Python test executes; the browser check that found both is not
|
|
something CI runs.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
TEMPLATE = (PROJECT_ROOT / "web_interface" / "templates" / "v3" / "partials"
|
|
/ "plugin_config.html")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def source():
|
|
return TEMPLATE.read_text(encoding="utf-8")
|
|
|
|
|
|
class TestTheHandOffGuard:
|
|
def test_it_asks_whether_a_person_typed(self, source):
|
|
assert "isTrusted" in source, (
|
|
"a programmatic change by a sibling widget must not read as a "
|
|
"user edit -- that is what removed the editor on a plain load")
|
|
assert "userEdited" in source
|
|
|
|
def test_it_no_longer_diffs_values_against_defaults(self, source):
|
|
assert "fallbackIsDirty" not in source, (
|
|
"value-diffing cannot distinguish a user's edit from another "
|
|
"widget populating this block's own fields")
|
|
assert "defaultSelected" not in source
|
|
|
|
def test_the_listeners_are_attached_synchronously(self, source):
|
|
"""The edit worth protecting can happen before initWidget runs, so
|
|
the listeners cannot wait for it.
|
|
|
|
Scoped to this branch: the template defines an initWidget for every
|
|
widget it can render, and the first one in the file belongs to a
|
|
different branch entirely.
|
|
"""
|
|
start = source.index("obj_widget == 'style-editor'")
|
|
branch = source[start:source.index("{% elif prop.properties %}", start)]
|
|
assert "watchForRealEdits" in branch
|
|
assert branch.index("watchForRealEdits") < branch.index("function initWidget"), (
|
|
"the watcher must be installed above this branch's initWidget, "
|
|
"and invoked immediately rather than from inside it")
|
|
|
|
def test_the_guard_still_backs_off_for_a_real_edit(self, source):
|
|
assert re.search(r"if \(userEdited\) \{ container\.remove\(\); return; \}",
|
|
source), "a genuine in-progress edit must still win"
|
|
|
|
|
|
class TestItTakesOverOnlyItsOwnBlocks:
|
|
def test_children_are_individually_addressable(self, source):
|
|
assert 'data-child-key="{{ nested_key }}"' in source, (
|
|
"without a handle per child the only options are removing the "
|
|
"whole section or none of it")
|
|
|
|
def test_removal_is_driven_by_what_the_widget_reported(self, source):
|
|
assert "ownedKeys" in source
|
|
assert "fallback.querySelector(" in source
|
|
|
|
def test_an_unowned_child_survives(self, source):
|
|
"""The empty-section case still removes the lot, so a block that is
|
|
entirely styling looks exactly as it did before."""
|
|
assert "!fallback.querySelector('[data-child-key]')" in source
|
|
|
|
def test_the_owned_key_is_escaped_before_it_becomes_a_selector(self, source):
|
|
"""ownedKeys comes back off a dataset attribute, not a literal --
|
|
splicing it into '[data-child-key="' + k + '"]' unescaped breaks (or
|
|
is steerable) on a key containing a quote or backslash. The rest of
|
|
this codebase (plugin-file-manager.js, app-shell.js) always routes a
|
|
dynamic value through CSS.escape() before it lands in a selector."""
|
|
assert "CSS.escape(k)" in source, (
|
|
"an owned key must be CSS.escape()'d before being interpolated "
|
|
"into the data-child-key attribute selector")
|
|
|
|
|
|
class TestTheSchemaSaysWhichBlocksAreStyling:
|
|
def test_adopted_blocks_are_marked_and_others_are_not(self):
|
|
from src.element_style import expand_style_elements
|
|
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"customization": {
|
|
"type": "object",
|
|
"properties": {
|
|
"score_text": {
|
|
"type": "object",
|
|
"properties": {
|
|
"font": {"type": "string",
|
|
"default": "PressStart2P-Regular.ttf"},
|
|
"font_size": {"type": "integer", "default": 10},
|
|
"text_color": {"type": "array",
|
|
"default": [255, 255, 255]},
|
|
},
|
|
},
|
|
# Not styling: a feature that happens to live here.
|
|
"favorite_result_colors": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {"type": "boolean", "default": False},
|
|
"win_color": {"type": "array",
|
|
"default": [0, 255, 0]},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
props = expand_style_elements(schema)["properties"]["customization"]["properties"]
|
|
assert props["score_text"]["x-style-managed"] is True
|
|
assert "x-style-managed" not in props["favorite_result_colors"]
|