"""Marquee options simplify to Free / Fixed ratio, and no duplicate mode row.
The options strip used to render a second copy of the marquee mode buttons
(New / + / - / intersect via `ge-opt-mode` mirrors) that duplicated the real
`ge-wand-mode-btn` buttons relocated into the strip. Those mirrors were
removed, and the "Fixed size" marquee style was dropped (only Free and Fixed
ratio remain).
"""
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[1]
CONTROLS = (ROOT / "static/js/editor/build/controls.js").read_text(encoding="utf-8")
OPTIONS = (ROOT / "static/js/editor/build/options-bar.js").read_text(encoding="utf-8")
CSS = (ROOT / "static/css/13-image-editor.css").read_text(encoding="utf-8")
DOC_EDITOR_CSS = (ROOT / "static/css/documents-gallery-editor.css").read_text(encoding="utf-8")
SW = (ROOT / "static/sw.js").read_text(encoding="utf-8")
def test_marquee_style_has_no_fixed_size_option():
assert 'id="ge-marquee-constraint"' in CONTROLS
i = CONTROLS.index('id="ge-marquee-constraint"')
block = CONTROLS[i:i + 240]
assert '' in block
assert '' in block
assert 'value="size"' not in block
assert "Fixed size" not in CONTROLS
def test_marquee_has_no_duplicate_mode_mirror_row():
assert "marquee: { modes: null, feather: null }," in OPTIONS
assert "if (src.modes) parts.push(makeModes(src.modes));" in OPTIONS
def test_no_selection_tool_renders_a_mode_mirror_row():
# The real wand-mode buttons are relocated into the strip, so a mirror row
# duplicates (and collides with) them for every selection tool.
block = OPTIONS[OPTIONS.index("const SELECTION_TOOLS = {"):]
block = block[:block.index("};")]
assert "modes: '#ge-" not in block
assert "modes: null" in block
def test_sw_cache_name_bumped():
assert re.search(r"odysseus-v\d+-pr40-editor-precache", SW)
def test_options_body_shrinks_to_keep_controls_left_aligned():
# An empty mirror body must not stretch and push the relocated controls
# (and the marquee action row) away from the left edge.
i = CSS.index(".ge-options-body {")
block = CSS[i:i + 420]
assert "flex: 0 1 auto;" in block
assert "flex: 1 1 auto;" not in block
def test_marquee_action_row_is_nudged_up():
assert ".ge-controls--in-options .ge-control-row.ge-actions { position: relative; top: -2px; margin-left: auto; }" in CSS
def test_auto_select_checkbox_is_accent_when_checked():
assert 'class="ge-ctl-check"' in OPTIONS # the auto-select input uses the shared control
i = DOC_EDITOR_CSS.index('body.ge-editor-open .ge-opt input[type="checkbox"]:checked,')
block = DOC_EDITOR_CSS[i:i + 220]
assert "background: var(--accent, var(--red));" in block
assert "background: var(--fg);" not in block
def test_wand_live_button_sits_next_to_the_intersect_mode_and_is_nudged_up():
# Live now sits in the mode row, immediately after the intersect (∩) button.
i = CONTROLS.index('id="ge-wand-section"')
j = CONTROLS.index('id="ge-wand-tol-preview"')
mode_row = CONTROLS[i:j]
assert mode_row.index('data-wand-mode="intersect"') < mode_row.index('id="ge-wand-live"')
# the tolerance row no longer carries the Live button
k = CONTROLS.index('id="ge-wand-tol-preview"')
tol_row = CONTROLS[k:k + 400]
assert 'id="ge-wand-live"' not in tol_row
c = CSS.index(".ge-wand-live-btn {")
live_block = CSS[c:CSS.index("}", c)]
assert "top: 0;" in live_block
def test_wand_slider_label_hugs_its_text():
assert "#ge-wand-section .ge-eraser-row label { flex: 0 0 auto; }" in CSS
def test_wand_action_row_is_nudged_up_like_marquee():
# The wand action row (eye/clear/invert/erase/...) is raised 2px, matching
# the marquee row, by dropping its inline top margin from 4px to 2px.
i = CONTROLS.index('id="ge-wand-section"')
j = CONTROLS.index('id="ge-sam-section"')
wand = CONTROLS[i:j]
assert '
' in wand
assert '
' not in wand
def test_wand_action_row_matches_marquee():
# Wand and marquee share the same selection actions; the wand row must use
# the same compact buttons as the marquee row (icon-only clear, 14px icons,
# same labels/modes) instead of its own older 12px + text variants.
i = CONTROLS.index('id="ge-wand-section"')
j = CONTROLS.index('id="ge-sam-section"')
wand = CONTROLS[i:j]
assert '' in wand
assert 'data-wand-mode="subtract" title="Subtract from selection (Alt)">−' in wand
for label in ("Invert", "Erase", "Copy Layer", "To Mask", "Quick Mask"):
assert label in wand
assert wand.count('
', CONTROLS)
assert m, f"section ge-{tool}-section not found"
return m.group(0)
def test_quickselect_objectselect_sam_match_the_wand_strip():
# The three remaining selection tools were refactored to the wand shape:
# compact mode labels, a caret "More" menu (help moved into it), and the
# unified 7-button action row.
for tool in ("sam", "quickselect", "objectselect"):
sec = _selection_section(tool)
assert 'class="ge-wand-more-wrap"' in sec
assert f'id="ge-{tool}-more"' in sec
assert f'id="ge-{tool}-more-menu"' in sec
assert 'aria-haspopup="true"' in sec
assert 'class="ge-wand-tip"' in sec
# compact mode labels (no verbose "+ Add" / "− Subtract")
assert "+ Add" not in sec
assert "− Subtract" not in sec
assert 'data-wand-mode="add" title="Add to selection">+' in sec
assert 'data-wand-mode="subtract" title="Subtract from selection">−' in sec
# the unified action row
assert '
' in sec
assert f'id="ge-{tool}-clear" title="Clear selection" aria-label="Clear selection"' in sec
for action in ("vis", "clear", "invert", "delete", "copy", "mask"):
assert f'id="ge-{tool}-{action}"' in sec
for label in ("Invert", "Erase", "Copy Layer", "To Mask", "Quick Mask"):
assert label in sec
assert sec.count('= 7
def test_selection_tool_carets_reuse_the_wand_classes():
# CSS: the new menus reuse the wand caret classes and un-clip the strip.
for tool in ("sam", "quickselect", "objectselect"):
assert f".ge-options-bar:has(#ge-{tool}-more-menu:not([hidden])) {{ overflow: visible; }}" in CSS
# the clear ✕ nudge + active-accent rules cover the new buttons too
for tool in ("sam", "quickselect", "objectselect"):
assert f"#ge-{tool}-clear svg" in CSS
assert f"#ge-{tool}-clear.has-selection" in CSS
def test_selection_tool_action_wiring_is_shared():
wire = (ROOT / "static/js/editor/wire-selection-controls.js").read_text(encoding="utf-8")
assert "const wireSelectionCaret = (prefix) => {" in wire
assert "['ge-sam', 'ge-quickselect', 'ge-objectselect'].forEach((prefix) => {" in wire
assert "document.getElementById(`${prefix}-vis`)?.addEventListener('click', toggleSelectionOverlay);" in wire
assert "document.getElementById(`${prefix}-clear`)?.addEventListener('click', wandClear);" in wire
assert "document.getElementById(`${prefix}-delete`)?.addEventListener('click', wandDeleteSelection);" in wire
assert "document.getElementById(`${prefix}-mask`)?.addEventListener('click', wandToMask);" in wire
assert "wireSelectionCaret(prefix);" in wire
# the old one-off SAM bindings were removed (no double toggle / double bind)
editor = (ROOT / "static/js/galleryEditor.js").read_text(encoding="utf-8")
assert "document.getElementById('ge-sam-vis')?.addEventListener('click', () => {" not in editor
assert "document.getElementById('ge-sam-clear')?.addEventListener('click', () => _wandClear());" not in editor
# the clear indicator lights the new buttons too
assert "#ge-marquee-clear, #ge-wand-clear, #ge-lasso-clear, #ge-sam-clear, #ge-quickselect-clear, #ge-objectselect-clear" in editor
def test_quickselect_brush_size_slider_is_wired():
wire = (ROOT / "static/js/editor/wire-selection-controls.js").read_text(encoding="utf-8")
assert "state.quickSelectBrushSize = Math.max(4, Math.min(200, parseInt(quickSize.value, 10) || 24));" in wire
def test_quickselect_brush_size_uses_the_shared_eraser_row_slider_style():
# Quick Select's brush-size slider kept the bare .ge-ctl-range look (14px,
# transparent) instead of the shared .ge-eraser-row slider the wand
# Tolerance row uses. Its row now uses the same pattern (8px track, preview
# dot, value chip) so every tool's slider reads the same.
q = _selection_section("quickselect")
assert 'class="ge-control-row ge-eraser-row"' in q
assert 'id="ge-quickselect-size-preview"' in q
assert 'id="ge-quickselect-size-label"' in q
assert 'class="ge-quickselect-size" id="ge-quickselect-size"' in q
assert "#ge-quickselect-section .ge-eraser-row label { flex: 0 0 auto; }" in CSS
wire = (ROOT / "static/js/editor/wire-selection-controls.js").read_text(encoding="utf-8")
assert "document.getElementById('ge-quickselect-size-label')" in wire