mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 11:52:25 +02:00
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).
Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
4.9 KiB
Python
114 lines
4.9 KiB
Python
"""Keyboard-shortcut audit for the image editor.
|
|
|
|
One test per audited key: the single-letter tool keys (V M L W C B E G T Z H)
|
|
plus the chords Ctrl+Z, Ctrl+Shift+Z, Ctrl+J, Ctrl+D, Ctrl+T and the brush-size
|
|
brackets. Each test pins the exact dispatch wiring so a regression in any key
|
|
is caught without launching a browser.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
TOOL_SHORTCUTS_JS = (ROOT / "static/js/editor/tool-shortcuts.js").read_text(encoding="utf-8")
|
|
TOOLBAR_JS = (ROOT / "static/js/editor/build/toolbar.js").read_text(encoding="utf-8")
|
|
KEYBOARD_JS = (ROOT / "static/js/editor/keyboard-shortcuts.js").read_text(encoding="utf-8")
|
|
EDITOR_JS = (ROOT / "static/js/galleryEditor.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _tool_shortcuts():
|
|
body = TOOL_SHORTCUTS_JS[TOOL_SHORTCUTS_JS.index("{"): TOOL_SHORTCUTS_JS.index("}")]
|
|
return dict(re.findall(r"(\w+):\s*'([^']+)'", body))
|
|
|
|
|
|
AUDITED_TOOL_KEYS = {
|
|
"move": "V",
|
|
"marquee": "M",
|
|
"lasso": "L",
|
|
"wand": "W",
|
|
"crop": "C",
|
|
"brush": "B",
|
|
"eraser": "E",
|
|
"gradient": "G",
|
|
"text": "T",
|
|
"zoom": "Z",
|
|
"hand": "H",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("tool,key", sorted(AUDITED_TOOL_KEYS.items()))
|
|
def test_tool_key_selects_its_tool(tool, key):
|
|
# The key is registered in the shared shortcut map …
|
|
assert _tool_shortcuts()[tool] == key
|
|
# … the tool exists in the palette (so a `[data-tool]` button is rendered) …
|
|
assert ("\n " + tool + ": { label: '") in TOOLBAR_JS
|
|
# … and the keyboard handler focuses the palette by key.
|
|
assert "const toolId = toolKeyMap[e.key.toLowerCase()];" in KEYBOARD_JS
|
|
assert 'const toolBtn = toolbar.querySelector(`[data-tool="${toolId}"]`) || toolbar.querySelector(`[data-tool-option="${toolId}"]`);' in KEYBOARD_JS
|
|
assert "if (toolBtn) toolBtn.click();" in KEYBOARD_JS
|
|
|
|
|
|
def test_tool_shortcut_keys_are_unique_and_single_letters():
|
|
mapping = _tool_shortcuts()
|
|
keys = list(mapping.values())
|
|
assert len(keys) == len(set(keys)), "two tools share a shortcut letter"
|
|
assert all(len(k) == 1 and k.isalpha() for k in keys)
|
|
|
|
|
|
def test_zoom_tool_z_is_wired_to_a_real_canvas_behaviour():
|
|
# Z must not be a dead key: it selects the Zoom tool AND the tool must
|
|
# actually zoom on canvas click (Alt-click zooms out), like Photoshop.
|
|
assert "zoom: 'Z'," in TOOL_SHORTCUTS_JS
|
|
assert "function _zoomToolClick(e)" in EDITOR_JS
|
|
assert "if (state.tool === 'zoom') return _zoomToolClick(e);" in EDITOR_JS
|
|
assert "const zoomOut = !!(e && (e.altKey || e.metaKey));" in EDITOR_JS
|
|
assert "state.mainCanvas.style.cursor" in EDITOR_JS
|
|
assert "else if (state.tool === 'zoom') cursor = 'zoom-in';" in EDITOR_JS
|
|
|
|
|
|
def test_ctrl_z_undo_and_ctrl_shift_z_redo():
|
|
assert "if (e.key.toLowerCase() === 'z') {" in KEYBOARD_JS
|
|
assert "if (e.shiftKey) redo(); else undo();" in KEYBOARD_JS
|
|
# Undo/redo must override the browser's native undo inside the editor.
|
|
assert "e.preventDefault(); e.stopPropagation();" in KEYBOARD_JS
|
|
|
|
|
|
def test_ctrl_j_duplicates_selection_pixels_or_the_active_layer():
|
|
assert "if (!e.altKey && e.code === 'KeyJ') {" in KEYBOARD_JS
|
|
assert "if (state.wandMask) wandCopyToNewLayer();" in KEYBOARD_JS
|
|
assert "else if (state.lassoPoints.length >= 3) lassoCopyToLayer();" in KEYBOARD_JS
|
|
assert "else duplicateActiveLayer?.();" in KEYBOARD_JS
|
|
# Ctrl+Alt+J stays the "new empty layer" variant.
|
|
assert "if (e.altKey && e.code === 'KeyJ') {" in KEYBOARD_JS
|
|
assert "addEmptyLayer();" in KEYBOARD_JS
|
|
|
|
|
|
def test_ctrl_d_deselects():
|
|
assert "if (!e.altKey && e.key.toLowerCase() === 'd') {" in KEYBOARD_JS
|
|
assert "deselectSelection?.();" in KEYBOARD_JS
|
|
|
|
|
|
def test_ctrl_t_starts_free_transform_while_shift_and_alt_stay_resize_and_legacy():
|
|
# Plain Ctrl/Cmd+T starts a free transform (Photoshop parity). The branch
|
|
# is keyed on !shiftKey, so Ctrl+Alt+T (the legacy alias) also lands here.
|
|
assert "if (!e.shiftKey && e.code === 'KeyT') { e.preventDefault(); e.stopPropagation(); startTransform(); return; }" in KEYBOARD_JS
|
|
# Ctrl+Shift+T keeps the canvas-resize prompt.
|
|
assert "if (e.shiftKey && e.key === 'T') { e.preventDefault(); e.stopPropagation(); resizeCustomPrompt(); return; }" in KEYBOARD_JS
|
|
|
|
|
|
def test_bracket_keys_scale_the_brush_proportionally():
|
|
assert "if (e.key === '[' || e.key === ']') {" in KEYBOARD_JS
|
|
assert "const factor = e.key === '[' ? 0.9 : 1.1;" in KEYBOARD_JS
|
|
assert "state.brushSize = Math.max(1, Math.min(800, Math.round(state.brushSize * factor)));" in KEYBOARD_JS
|
|
assert "brushSizeSync(null);" in KEYBOARD_JS
|
|
|
|
|
|
def test_shortcuts_are_gated_to_an_open_editor_and_ignore_typing():
|
|
# The whole handler bails when the editor is closed or the event is consumed.
|
|
assert "if (!state.editorOpen || e.defaultPrevented || e.isComposing) return;" in KEYBOARD_JS
|
|
# Tool keys don't hijack text fields.
|
|
assert "if (isTypingTarget(e.target) && !isNonTextInput(e.target)) return;" in KEYBOARD_JS
|