"""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('= 6 assert ' {" not in wire def test_lasso_action_row_matches_marquee_and_wand(): # The lasso strip is unified with marquee/wand: same compact action row # (icon-only clear, 14px icons, same labels) plus the visibility eye. i = CONTROLS.index('id="ge-lasso-section"') j = CONTROLS.index('id="ge-wand-section"') lasso = CONTROLS[i:j] assert '
', 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