"""Second editor-polish batch: slider chip editor alignment, action-row nudge,
wand caret styling, crop-close reset, hex copy + favorites."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CSS = (ROOT / "static/css/13-image-editor.css").read_text(encoding="utf-8")
SLIDER = (ROOT / "static/js/editor/slider-ux.js").read_text(encoding="utf-8")
EDITOR = (ROOT / "static/js/galleryEditor.js").read_text(encoding="utf-8")
PANELS = (ROOT / "static/js/editor/panels/panels.js").read_text(encoding="utf-8")
CONTROLS = (ROOT / "static/js/editor/build/controls.js").read_text(encoding="utf-8")
def test_slider_edit_input_is_chip_sized():
assert ".ge-eraser-row input.ge-slider-edit," in CSS
assert ".ge-adj-row input.ge-slider-edit {" in CSS
i = CSS.index(".ge-eraser-row input.ge-slider-edit,")
block = CSS[i:i + 220]
assert "height: 16px;" in block
assert "box-sizing: border-box;" in block
def test_slider_edit_input_is_vertically_centred():
assert "inp.style.top = (crect.top - rrect.top + (crect.height - inp.offsetHeight) / 2) + 'px';" in SLIDER
assert "crect.top - rrect.top - 1" not in SLIDER
def test_action_row_buttons_are_nudged_up():
assert ".ge-controls--in-options .ge-control-row.ge-actions .ge-btn {" in CSS
i = CSS.index(".ge-controls--in-options .ge-control-row.ge-actions .ge-btn {")
block = CSS[i:i + 120]
assert "position: relative;" in block
assert "top: -2px;" in block
def test_wand_more_button_has_no_box():
i = CSS.index(".ge-wand-more-btn {")
block = CSS[i:i + 200]
assert "background: transparent;" in block
assert "border-color: transparent;" in block
def test_wand_caret_and_value_are_nudged_left():
i = CSS.index(".ge-wand-more-btn {")
assert "left: -8px;" in CSS[i:i + 120]
assert "top: -4px;" in CSS[i:i + 120]
assert "#ge-wand-section .ge-slider-value { position: relative; left: -6px; }" in CSS
def test_crop_close_x_is_nudged_up():
assert ".ge-crop-close-x { display: block; line-height: 1; transform: translateY(-2px); }" in CSS
def test_crop_close_clears_the_selection():
assert "pop.querySelector('.ge-crop-close').addEventListener('click', () => _cropTool.reset());" in EDITOR
assert "_cancelCrop('dismiss')" not in EDITOR
def test_crop_close_button_and_presets_are_nudged_up():
idx = CSS.index(".ge-crop-close {")
assert "top: -13px;" in CSS[idx:idx + 120]
assert ".ge-crop-preset-row { display: flex; gap: 4px; position: relative; top: -4px; }" in CSS
def test_escape_removes_the_crop_like_the_x():
# Escape routes through _cancelCrop, which now fully resets (not cancel()).
assert "_cancelCrop('escape')" in EDITOR
i = EDITOR.index("function _cancelCrop(reason = 'cancel') {")
block = EDITOR[i:i + 520]
assert "_cropTool.reset();" in block
def test_hex_row_has_copy_and_favorite_buttons():
assert "el('button', 'ge-hex-copy')" in PANELS
assert "el('button', 'ge-hex-copy ge-hex-fav')" in PANELS
assert "hexRow.append(hexHash, hexInput, hexCopy, hexFav);" in PANELS
assert "navigator.clipboard?.writeText" in PANELS
assert ".ge-hex-copy {" in CSS
assert ".ge-hex-fav.active {" in CSS
def test_color_favorites_are_persisted_and_rendered():
assert "const FAV_KEY = 'ge-color-favorites';" in PANELS
assert "const getFavoriteColors = () =>" in PANELS
assert "const isFavorite = (hex) =>" in PANELS
assert "favRow.innerHTML = favs.length" in PANELS
assert "ge-swatch-fav" in PANELS
def test_ctl_range_thumb_is_nudged_up_one_px():
i = CSS.index(".ge-ctl-range::-webkit-slider-thumb {")
block = CSS[i:i + 200]
assert "margin-top: -5px;" in block
def test_wand_tolerance_value_chip_is_always_tinted():
assert "#ge-wand-section #ge-wand-tol-label.ge-slider-value {" in CSS
i = CSS.index("#ge-wand-section #ge-wand-tol-label.ge-slider-value {")
block = CSS[i:i + 160]
assert "background: color-mix(in srgb, var(--fg) 10%, transparent);" in block
assert "opacity: 1;" in block
def test_selection_action_row_is_pinned_to_the_right_edge():
assert ".ge-options-bar .ge-controls.ge-controls--in-options {" in CSS
i = CSS.index(".ge-options-bar .ge-controls.ge-controls--in-options {")
assert "flex: 1 1 auto;" in CSS[i:i + 260]
assert ".ge-controls--in-options .ge-control-row.ge-actions { position: relative; top: -2px; margin-left: auto; }" in CSS
j = CSS.index(".ge-controls--in-options > div:not([style*=\"display:none\"])")
assert "flex: 1 1 auto;" in CSS[j:j + 500]
def test_crop_can_expand_beyond_the_canvas():
# normalizeCropRect must not clamp back inside the image anymore.
GEO = (ROOT / "static/js/editor/document-geometry.js").read_text(encoding="utf-8")
i = GEO.index("function normalizeCropRect(")
block = GEO[i:i + 900]
assert "editorState.imgWidth - 1" not in block
assert "Math.max(-MAX, Math.min(MAX, Math.round(rect.x)))" in block
assert "const MAX = 20000;" in block
def test_crop_tool_allows_dragging_past_the_edge_and_syncs_a_frame():
CROP = (ROOT / "static/js/editor/tools/crop.js").read_text(encoding="utf-8")
assert "createCropTool({ composite, showCropApply, snapFrame, syncCropFrame })" in CROP
assert "x: Math.max(-MAX, Math.min(MAX, end.x))," in CROP
assert "syncCropFrame?.();" in CROP
def test_crop_frame_outline_is_wired_and_styled():
assert "function _syncCropFrame()" in EDITOR
assert "el.className = 'ge-crop-frame';" in EDITOR
assert "syncCropFrame: () => _syncCropFrame()," in EDITOR
assert ".ge-crop-frame {" in CSS
i = CSS.index(".ge-crop-frame {")
block = CSS[i:i + 260]
assert "position: absolute;" in block
assert "pointer-events: none;" in block
def test_crop_can_start_outside_the_canvas_without_panning():
# The crop tool must be able to begin a rect in the empty area around the
# image (so it can be pulled out past the edge) instead of the canvas-area
# pan grabbing the pointer and switching to hand mode.
CE = (ROOT / "static/js/editor/canvas-events.js").read_text(encoding="utf-8")
assert "if (state.tool !== 'lasso' && state.tool !== 'crop') return;" in CE
assert "(state.tool === 'lasso' || state.tool === 'crop') && !directPan" in CE
# Pressing editor chrome inside the area (e.g. the crop Apply panel) must
# not start a fresh crop.
assert ".ge-crop-apply, .ge-adj-popup, .ge-transform-popup, .ge-fx-popup, .ge-inpaint-popup" in CE
def test_crop_apply_has_a_standard_size_dropdown():
# The crop Apply box (W × H pixel inputs) carries a dropdown of standard
# crop sizes that set the rect to a fixed pixel size (or the original).
assert 'class="ge-crop-size-preset"' in EDITOR
i = EDITOR.index('class="ge-crop-size-preset"')
block = EDITOR[i:i + 900]
for v in ('custom', '1080x1080', '1080x1350', '1080x1920', '1200x630',
'1280x720', '1920x1080', '2048x1536', '3000x2400', '4000x3000', 'original'):
assert f'value="{v}"' in block
assert '' in block
assert '' in block
# change handler applies the chosen pixel size / original image size
assert "sizeSel?.addEventListener('change'" in EDITOR
assert "if (v === 'original') { w = state.imgWidth; h = state.imgHeight; }" in EDITOR
assert "const m = /^(\\d+)x(\\d+)$/.exec(v);" in EDITOR
# manual W/H edits reset the dropdown to Custom
assert "if (sizeSel) sizeSel.value = 'custom';" in EDITOR
# CSS styles the select like the number inputs and leaves a gap to its right
assert ".ge-crop-apply select.ge-crop-size-preset {" in CSS
ci = CSS.index(".ge-crop-apply select.ge-crop-size-preset {")
assert "margin-right: 8px;" in CSS[ci:ci + 260]
def test_clear_x_is_nudged_down_and_lit_when_a_selection_is_active():
# The Clear ✕ in the marquee/wand action rows: the glyph sits 1px lower,
# and the button turns accent while a selection is active.
i = CSS.index("#ge-marquee-clear svg,")
block = CSS[i:i + 900]
assert "#ge-wand-clear svg," in block
assert "#ge-objectselect-clear svg { position: relative; top: 1px; }" in block
assert "#ge-marquee-clear.has-selection," in block
assert "#ge-objectselect-clear.has-selection { color: var(--accent, var(--red));" in block
assert "querySelectorAll('#ge-marquee-clear, #ge-wand-clear, #ge-lasso-clear, #ge-sam-clear, #ge-quickselect-clear, #ge-objectselect-clear').forEach((btn) => {" in EDITOR
assert "btn.classList.toggle('has-selection', !!state.wandMask);" in EDITOR
def test_transform_cancel_and_apply_are_right_aligned():
# The Transform popup's Cancel + Apply share the right edge of the body.
i = CSS.index(".ge-transform-popup-body #ge-transform-cancel-btn {")
assert "margin-left: auto;" in CSS[i:i + 120]
def test_clone_source_clear_matches_the_wand_clear_button():
# The Clone "clear sampled source" ✕ now uses the same SVG cross and
# button classes as the wand/marquee Clear ✕, and lights up (accent)
# while a source is sampled.
assert 'class="ge-btn ge-btn-sm" id="ge-clone-source-clear"' in CONTROLS
assert '' in CONTROLS
assert "ge-icon-btn\" id=\"ge-clone-source-clear\"" not in CONTROLS
assert "#ge-clone-source-clear svg { position: relative; top: -1px; }" in CSS
assert "#ge-clone-source-clear.has-selection { color: var(--accent, var(--red));" in CSS
assert "clear.classList.toggle('has-selection', hasSource);" in EDITOR
def test_clone_sample_mode_lives_in_the_clone_section():
# The Sample select was moved out of the brush "More" menu (the custom
# stroke group) into the dedicated Clone section, sharing the standard
# control-select style.
assert 'id="ge-clone-sample-mode" class="ge-ctl-select"' in CONTROLS
assert '