mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-22 18:30:28 +00:00
143 lines
4.5 KiB
Python
143 lines
4.5 KiB
Python
"""Regression coverage for #1697: multi-image clipboard paste attachments."""
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent.resolve()
|
|
BOOT_JS_PATH = REPO_ROOT / "static" / "boot.js"
|
|
PANELS_JS_PATH = REPO_ROOT / "static" / "panels.js"
|
|
NODE = shutil.which("node")
|
|
|
|
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
|
|
|
|
def _read_js(path: Path) -> str:
|
|
with open(path, encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def _extract_msg_paste_registration() -> str:
|
|
boot = _read_js(BOOT_JS_PATH)
|
|
marker = "$('msg').addEventListener('paste',e=>{"
|
|
start = boot.find(marker)
|
|
assert start >= 0, "boot.js must register the composer paste handler"
|
|
end_marker = "\n});"
|
|
end = boot.find(end_marker, start)
|
|
assert end >= 0, "composer paste handler should end with a listener close"
|
|
return boot[start : end + len(end_marker)]
|
|
|
|
|
|
def _run_node(source: str) -> str:
|
|
result = subprocess.run(
|
|
[NODE],
|
|
input=source,
|
|
text=True,
|
|
capture_output=True,
|
|
cwd=REPO_ROOT,
|
|
timeout=20,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"node driver failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _paste_harness(items_js: str) -> dict:
|
|
paste_registration = json.dumps(_extract_msg_paste_registration())
|
|
source = f"""
|
|
const vm = require('vm');
|
|
const pasteRegistration = {paste_registration};
|
|
const listeners = {{}};
|
|
const S = {{pendingFiles: []}};
|
|
let renderCount = 0;
|
|
let lastStatus = '';
|
|
let preventDefaultCount = 0;
|
|
class File extends Blob {{
|
|
constructor(parts, name, options={{}}) {{
|
|
super(parts, options);
|
|
this.name = name;
|
|
this.lastModified = options.lastModified || 0;
|
|
}}
|
|
}}
|
|
const context = {{
|
|
S,
|
|
File,
|
|
Blob,
|
|
Date: {{now: () => 1700000000000}},
|
|
Array,
|
|
console,
|
|
$: (id) => {{
|
|
if (id !== 'msg') throw new Error('unexpected element id '+id);
|
|
return {{addEventListener: (type, cb) => {{listeners[type] = cb;}}}};
|
|
}},
|
|
addFiles: (files) => {{
|
|
for (const f of files) {{
|
|
if (!S.pendingFiles.find(p => p.name === f.name)) S.pendingFiles.push(f);
|
|
}}
|
|
renderCount += 1;
|
|
}},
|
|
setStatus: (text) => {{ lastStatus = text; }},
|
|
t: (key) => key === 'image_pasted' ? 'Image pasted: ' : key,
|
|
}};
|
|
vm.createContext(context);
|
|
vm.runInContext(pasteRegistration, context);
|
|
listeners.paste({{
|
|
clipboardData: {{items: {items_js}}},
|
|
preventDefault: () => {{ preventDefaultCount += 1; }},
|
|
}});
|
|
console.log(JSON.stringify({{
|
|
pendingNames: S.pendingFiles.map(f => f.name),
|
|
pendingCount: S.pendingFiles.length,
|
|
renderCount,
|
|
lastStatus,
|
|
preventDefaultCount,
|
|
}}));
|
|
"""
|
|
return json.loads(_run_node(source))
|
|
|
|
|
|
def test_one_clipboard_paste_with_two_image_items_adds_two_attachment_chips():
|
|
"""Two image clipboard items from one paste must survive addFiles() filename de-dupe."""
|
|
result = _paste_harness(
|
|
"["
|
|
"{kind:'file', type:'image/png', getAsFile:()=>new Blob(['one'], {type:'image/png'})},"
|
|
"{kind:'file', type:'image/png', getAsFile:()=>new Blob(['two'], {type:'image/png'})}"
|
|
"]"
|
|
)
|
|
|
|
assert result["preventDefaultCount"] == 1
|
|
assert result["renderCount"] == 1
|
|
assert result["pendingCount"] == 2
|
|
assert result["pendingNames"] == [
|
|
"screenshot-1700000000000-1.png",
|
|
"screenshot-1700000000000-2.png",
|
|
]
|
|
assert result["lastStatus"] == (
|
|
"Image pasted: screenshot-1700000000000-1.png, "
|
|
"screenshot-1700000000000-2.png"
|
|
)
|
|
|
|
|
|
def test_single_image_paste_keeps_existing_screenshot_filename_shape():
|
|
"""The one-image path should keep screenshot-<timestamp>.<ext> for compatibility."""
|
|
result = _paste_harness(
|
|
"[{kind:'file', type:'image/png', getAsFile:()=>new Blob(['one'], {type:'image/png'})}]"
|
|
)
|
|
|
|
assert result["pendingNames"] == ["screenshot-1700000000000.png"]
|
|
|
|
|
|
def test_file_picker_and_drop_paths_still_pass_real_file_names_to_addfiles():
|
|
"""Non-clipboard multi-file paths should preserve browser-provided filenames."""
|
|
boot = _read_js(BOOT_JS_PATH)
|
|
panels = _read_js(PANELS_JS_PATH)
|
|
|
|
assert "$('fileInput').onchange=e=>{addFiles(Array.from(e.target.files));e.target.value='';};" in boot
|
|
assert "const files=Array.from(e.dataTransfer.files);" in panels
|
|
assert "if(files.length){addFiles(files);$('msg').focus();}" in panels
|
|
assert "screenshot-" not in panels[panels.find("document.addEventListener('drop'") : panels.find("document.addEventListener('drop'") + 900]
|