Files
odysseus/tests/test_editor_autosave_recovery.py
T
pewdiepie-archdaemonandClaude Opus 5.5 5fac693c1c feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)
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>
2026-10-15 11:57:35 +00:00

93 lines
4.5 KiB
Python

from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _editor():
return (ROOT / "static/js/galleryEditor.js").read_text()
def _slice(source, start, end):
return source[source.index(start):source.index(end)]
def test_every_edit_schedules_a_debounced_autosave():
source = _editor()
# state mutations funnel through _saveState, which schedules a persist.
save_state = _slice(source, "function _saveState", "function closeEditor")
assert "_schedulePersist()" in save_state
schedule = _slice(source, "function _schedulePersist", "function _buildDraftPayload")
assert "const PERSIST_DEBOUNCE_MS = 800;" in source
assert "if (_draftSavingDisabled()) return;" in schedule
assert "if (!state.editorOpen || !state.layers.length) return;" in schedule
# The first save (no server draft yet) establishes the recovery anchor
# immediately; later edits stay debounced.
assert "const delay = state.draftId ? PERSIST_DEBOUNCE_MS : 0;" in schedule
assert "state.persistTimer = setTimeout(() => { state.persistTimer = null; _persistDraft(); }, delay);" in schedule
def test_autosave_creates_then_updates_the_server_draft_and_coalesces():
persist = _slice(_editor(), "async function _persistDraft", "async function _loadDraftById")
# Concurrent saves coalesce instead of racing.
assert "if (state.persistInFlight) { state.persistDirty = true; _setDraftStatus('Saving…', 'saving'); return; }" in persist
assert "if (isCurrentSession()) state.persistInFlight = null;" in persist
assert "if (state.persistDirty) {" in persist
# Create on the first save, update thereafter, and recreate if the row
# was deleted (404) while the editor was open.
assert "return fetch('/api/editor-drafts', {" in persist
assert "method: 'PUT', credentials: 'same-origin'," in persist
assert "if (res.status === 404) {" in persist
assert "method: 'POST', credentials: 'same-origin'," in persist
# Oversized layered documents pause autosave instead of silently failing.
assert "if (serializedBody.length > _EDITOR_PROJECT_MAX_BYTES) {" in persist
assert "_setDraftStatus('Save paused', 'error');" in persist
def test_autosave_writes_a_session_recovery_anchor():
source = _editor()
write = _slice(source, "function _writeActiveEditorSession", "function _setDraftStatus")
assert "if (!state.editorOpen) return;" in write
assert "sessionStorage.setItem(ACTIVE_EDITOR_SESSION_KEY" in write
assert "draftId: state.draftId || null," in write
assert "draftName: state.draftName || 'Untitled'," in write
assert "width: Number(state.imgWidth) || 0," in write
assert "height: Number(state.imgHeight) || 0," in write
# The anchor is refreshed once a fresh draft id comes back from the server.
assert "state.draftId = out.id;\n _writeActiveEditorSession();" in source
def test_closing_the_editor_flushes_and_clears_the_recovery_anchor():
source = _editor()
close = source[source.index("export function closeEditor"):]
assert "if (state.persistTimer) { clearTimeout(state.persistTimer); state.persistTimer = null; }" in close
assert "try { _persistDraft(); } catch {}" in close
assert "state.editorSessionToken += 1;" in close
assert "_clearActiveEditorSession();" in close
def test_open_editor_resumes_a_draft_by_server_id_with_a_liveness_guard():
source = _editor()
open_fn = source[source.index("export function openEditor"):]
assert "if (!imageUrl && draftId) {" in open_fn
assert "return _loadDraftById(draftId)" in open_fn
# A late draft load must not draw into a torn-down editor.
assert "if (!state.editorOpen) return;" in open_fn
def test_gallery_restores_the_active_edit_after_refresh():
gallery = (ROOT / "static/js/gallery.js").read_text()
restore = _slice(gallery, "async function _restoreActiveEditorAfterRefresh", "if (document.readyState")
assert "const saved = _readActiveEditorSession();" in restore
assert "if (!saved) return;" in restore
# It reopens the same draft (or source image) at the same canvas size.
assert "saved.draftId ? null : (saved.imageUrl || null)," in restore
assert "saved.draftId || null," in restore
assert "saved.width > 0 && saved.height > 0 ? { w: saved.width, h: saved.height } : null," in restore
# Wiring: run the restore once the shell has mounted after a reload.
assert "document.addEventListener('DOMContentLoaded', () => setTimeout(_restoreActiveEditorAfterRefresh, 0), { once: true });" in gallery