mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-14 02:56:36 +00:00
feat(element-style): adopt hand-written customization blocks, and widen the font list
Nineteen plugins spell their style elements out longhand instead of declaring them -- football's block is 701 lines for seven elements -- and predate this system entirely. Core now recognises that shape, so they pick up the row-per-element editor and the real font picker on a core update rather than on a plugin release. Checked against every published schema: 21 plugins adopt, and the defaults of each still validate against the schema generated for it. Detection requires *every* field in a block to be one this system understands. A looser "has at least one style field" rule sweeps in baseball's `count`, which carries a text_color beside geometry that means nothing here. That distinction took three attempts to test: the first two assertions passed under both rules, because an over-eager rule leaves a fontless block looking untouched and only surfaces as an extra row in the editor. The hardcoded font enum is replaced rather than extended. Football lists five of the thirty-five installed fonts, which is why a font a user uploads can never appear in one. It is not a curated safe set -- it omits some twenty other faces that fit the declared size cap just as well -- it is the fonts that happened to exist when it was written. Widening it does need a guard, though, and not the one the schema already has: a bitmap font ignores font_size and renders at its size baked into the file, so `maximum: 16` cannot stop a 27px face. The picker now filters out fixed-size fonts taller than the element's own declared ceiling, which drops exactly the four that would overflow a 32px panel and keeps the other thirty. Per-mode overrides stay opt-in: core cannot invent a plugin's display modes, so `x-style-modes` remains the one line that unlocks them. Their layout half covers every positionable element rather than only those with a style block -- the two namespaces do not line up in a hand-written schema, and football positions six things (logos, timeouts, possession) that have no style block at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+186
-2
@@ -177,10 +177,25 @@ class TestExpandStyleElements:
|
||||
expand_style_elements(STYLE_ELEMENTS_SCHEMA)
|
||||
assert json.dumps(STYLE_ELEMENTS_SCHEMA, sort_keys=True) == before
|
||||
|
||||
def test_no_declaration_returns_same_object(self):
|
||||
assert expand_style_elements(MANUAL_SCHEMA) is MANUAL_SCHEMA
|
||||
def test_a_schema_with_nothing_to_expand_is_returned_as_is(self):
|
||||
"""No needless deep copy when there is nothing to do."""
|
||||
empty = {"properties": {}}
|
||||
assert expand_style_elements(empty) is empty
|
||||
no_style = {"properties": {"customization": {"type": "object",
|
||||
"properties": {"favorite_result_colors": {"type": "object",
|
||||
"properties": {"win_color": {"type": "array"}}}}}}}
|
||||
assert expand_style_elements(no_style) is no_style
|
||||
|
||||
def test_a_hand_written_block_is_adopted(self):
|
||||
"""Nineteen plugins spell their style elements out longhand rather
|
||||
than declaring them, and predate this system entirely. They pick up
|
||||
the editor and the font picker on a core update rather than on a
|
||||
plugin release."""
|
||||
expanded = expand_style_elements(MANUAL_SCHEMA)
|
||||
assert expanded is not MANUAL_SCHEMA
|
||||
customization = expanded["properties"]["customization"]
|
||||
assert customization["x-widget"] == "style-editor"
|
||||
assert "x-style-elements" not in MANUAL_SCHEMA["properties"]["customization"]
|
||||
|
||||
def test_garbage_input_never_raises(self):
|
||||
bad = {"properties": {"customization": {"x-style-elements": "nope"}}}
|
||||
@@ -1073,3 +1088,172 @@ class TestAliasedLookup:
|
||||
config, defaults_from_schema_file(extra_schema_path)
|
||||
).style("score_text")
|
||||
assert st.scale == 2.0
|
||||
|
||||
HANDWRITTEN = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"customization": {
|
||||
"type": "object",
|
||||
"title": "Display Customization",
|
||||
"properties": {
|
||||
"score_text": {
|
||||
"type": "object",
|
||||
"title": "Game Score",
|
||||
"properties": {
|
||||
"font": {
|
||||
"type": "string",
|
||||
"enum": ["PressStart2P-Regular.ttf", "4x6-font.ttf",
|
||||
"5by7.regular.ttf", "5x7.bdf", "4x6.bdf"],
|
||||
"default": "PressStart2P-Regular.ttf",
|
||||
},
|
||||
"font_size": {"type": "integer", "minimum": 4,
|
||||
"maximum": 16, "default": 10},
|
||||
"text_color": {"type": "array", "minItems": 3,
|
||||
"maxItems": 3, "default": [255, 255, 255]},
|
||||
},
|
||||
},
|
||||
"favorite_result_colors": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "default": False},
|
||||
"win_color": {"type": "array", "default": [0, 255, 0]},
|
||||
},
|
||||
},
|
||||
# baseball's 'count' shape: one field this system knows,
|
||||
# next to geometry that means nothing to it.
|
||||
"count": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text_color": {"type": "array", "default": [0, 255, 0]},
|
||||
"y_offset": {"type": "integer", "default": 2},
|
||||
},
|
||||
},
|
||||
"layout": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"score": {"type": "object", "properties": {
|
||||
"x_offset": {"type": "integer", "default": 0},
|
||||
"y_offset": {"type": "integer", "default": 0}}},
|
||||
"home_logo": {"type": "object", "properties": {
|
||||
"x_offset": {"type": "integer", "default": 0}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestHandWrittenAdoption:
|
||||
"""Nineteen plugins spell their style elements out longhand -- football's
|
||||
block is 701 lines for seven elements -- and predate this system. Core
|
||||
recognises that shape so they pick up the editor and the real font picker
|
||||
on a core update rather than on a plugin release.
|
||||
"""
|
||||
|
||||
def _customization(self, schema=None):
|
||||
return expand_style_elements(
|
||||
schema or HANDWRITTEN)["properties"]["customization"]
|
||||
|
||||
def test_the_block_gets_the_composite_editor(self):
|
||||
assert self._customization()["x-widget"] == "style-editor"
|
||||
|
||||
def test_the_declared_order_is_stated_explicitly(self):
|
||||
"""Flask's JSON provider sorts keys, so without this the elements
|
||||
reach the browser alphabetised."""
|
||||
order = self._customization()["x-propertyOrder"]
|
||||
assert order[0] == "score_text"
|
||||
|
||||
def test_a_block_that_is_not_styling_is_left_alone(self):
|
||||
"""favorite_result_colors, baseball's bases/outs/player_card and the
|
||||
stocks blocks all carry fields this system knows nothing about."""
|
||||
block = self._customization()["properties"]["favorite_result_colors"]
|
||||
assert "x-style-managed" not in block
|
||||
assert block["properties"]["win_color"]["default"] == [0, 255, 0]
|
||||
|
||||
def test_a_block_that_merely_shares_a_field_is_left_alone(self):
|
||||
"""The reason detection requires *every* field to be one this system
|
||||
understands. baseball's 'count' carries a text_color beside a
|
||||
y_offset that means nothing here.
|
||||
|
||||
Asserted on the element list rather than on the block itself: an
|
||||
over-eager rule leaves a fontless block looking untouched, and only
|
||||
shows up as an extra row in the editor and an extra per-mode
|
||||
override group.
|
||||
"""
|
||||
schema = copy.deepcopy(HANDWRITTEN)
|
||||
schema["properties"]["customization"]["x-style-modes"] = ["live"]
|
||||
live = expand_style_elements(schema)["properties"]["customization"][
|
||||
"properties"]["modes"]["properties"]["live"]["properties"]
|
||||
assert "score_text" in live
|
||||
assert "count" not in live, (
|
||||
"'count' is not a style element -- it shares one field and "
|
||||
"carries geometry this system does not understand")
|
||||
assert "favorite_result_colors" not in live
|
||||
|
||||
def test_the_hardcoded_font_list_is_replaced_by_the_picker(self):
|
||||
"""The reason an uploaded font could never appear in one of these."""
|
||||
font = self._customization()["properties"]["score_text"]["properties"]["font"]
|
||||
assert "enum" not in font
|
||||
assert font["x-widget"] == "font-selector"
|
||||
|
||||
def test_the_picker_inherits_the_declared_size_ceiling(self):
|
||||
"""A bitmap font ignores font_size and renders at its own baked-in
|
||||
size, so a field capped at 16 must not offer a 27px face."""
|
||||
font = self._customization()["properties"]["score_text"]["properties"]["font"]
|
||||
assert font["x-options"]["maxFixedSize"] == 16
|
||||
|
||||
def test_the_users_existing_font_choice_stays_valid(self):
|
||||
font = self._customization()["properties"]["score_text"]["properties"]["font"]
|
||||
assert font["default"] == "PressStart2P-Regular.ttf"
|
||||
assert font["type"] == "string"
|
||||
|
||||
def test_the_input_schema_is_not_mutated(self):
|
||||
expand_style_elements(HANDWRITTEN)
|
||||
original = HANDWRITTEN["properties"]["customization"]
|
||||
assert "x-widget" not in original
|
||||
assert "enum" in original["properties"]["score_text"]["properties"]["font"]
|
||||
|
||||
def test_no_style_blocks_means_no_expansion(self):
|
||||
schema = {"properties": {"customization": {"type": "object",
|
||||
"properties": {"favorite_result_colors": {"type": "object",
|
||||
"properties": {"win_color": {"type": "array"}}}}}}}
|
||||
assert expand_style_elements(schema) is schema
|
||||
|
||||
|
||||
class TestAdoptedModes:
|
||||
"""Per-mode overrides stay opt-in: core cannot invent a plugin's list of
|
||||
display modes. Declaring x-style-modes is the one line that unlocks them
|
||||
for a hand-written block."""
|
||||
|
||||
def _live(self):
|
||||
schema = copy.deepcopy(HANDWRITTEN)
|
||||
schema["properties"]["customization"]["x-style-modes"] = ["live", "recent"]
|
||||
return expand_style_elements(schema)["properties"]["customization"][
|
||||
"properties"]["modes"]["properties"]["live"]["properties"]
|
||||
|
||||
def test_modes_are_not_invented(self):
|
||||
assert "modes" not in self._customization_props()
|
||||
|
||||
def _customization_props(self):
|
||||
return expand_style_elements(HANDWRITTEN)["properties"]["customization"]["properties"]
|
||||
|
||||
def test_declaring_modes_generates_them(self):
|
||||
assert "score_text" in self._live()
|
||||
|
||||
def test_mode_fields_are_nullable(self):
|
||||
size = self._live()["score_text"]["properties"]["font_size"]
|
||||
assert size["default"] is None and "null" in size["type"]
|
||||
|
||||
def test_every_positionable_element_gets_per_mode_offsets(self):
|
||||
"""The two namespaces do not line up in a hand-written schema: this
|
||||
one styles 'score_text' but positions 'score', and positions a logo
|
||||
that has no style block at all. Keying the layout off the style
|
||||
elements would have left both without a per-mode offset."""
|
||||
layout = self._live()["layout"]["properties"]
|
||||
assert set(layout) == {"score", "home_logo"}
|
||||
|
||||
def test_the_mode_font_picker_keeps_the_ceiling(self):
|
||||
font = self._live()["score_text"]["properties"]["font"]
|
||||
assert font["x-options"]["maxFixedSize"] == 16
|
||||
assert "null" in font["type"]
|
||||
|
||||
Reference in New Issue
Block a user