import json import subprocess from pathlib import Path ROOT = Path(__file__).parents[1] EDITOR = ROOT / "frontend" / "issue-evidence-editor.js" ATTACHMENT = ROOT / "frontend" / "issue-attachment.js" INDEX = ROOT / "frontend" / "index.html" CSS = ROOT / "frontend" / "dashboard.css" SERVICE_WORKER = ROOT / "frontend" / "service-worker.js" def run_node(script: str) -> dict: return json.loads(subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout) def test_editor_crops_redacts_undoes_and_flattens_derivative(): script = f""" const editor = require({json.dumps(str(EDITOR))}); const model = editor.createModel({{width:400,height:300}}); model.setCrop({{x:40,y:30,width:240,height:180}}); model.addRedaction({{x:70,y:60,width:80,height:40}}); model.addRedaction({{x:180,y:120,width:50,height:30}}); model.undo(); const calls=[]; const canvas={{width:0,height:0,getContext:()=>({{ drawImage:(...args)=>calls.push(['draw',...args.slice(1)]), fillRect:(...args)=>calls.push(['fill',...args]), set fillStyle(value){{calls.push(['color',value]);}} }}),toBlob:(callback,type)=>callback(new Blob(['flattened'],{{type}}))}}; (async()=>{{ const result=await editor.flatten({{source:{{}},model,canvas,name:'private.png',type:'image/png'}}); process.stdout.write(JSON.stringify({{snapshot:model.snapshot(),calls,width:canvas.width,height:canvas.height, name:result.name,type:result.type,text:await result.text()}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ output = run_node(script) assert output["snapshot"] == { "crop": {"x": 40, "y": 30, "width": 240, "height": 180}, "redactions": [{"x": 70, "y": 60, "width": 80, "height": 40}], } assert output["width"] == 240 assert output["height"] == 180 assert output["calls"] == [ ["draw", 40, 30, 240, 180, 0, 0, 240, 180], ["color", "#000000"], ["fill", 30, 30, 80, 40], ] assert output["name"] == "private.png" assert output["type"] == "image/png" assert output["text"] == "flattened" def test_applying_edit_replaces_only_selected_blob_and_preserves_note_and_order(): script = f""" const attachment=require({json.dumps(str(ATTACHMENT))}); let id=0; const image=name=>{{const blob=new Blob([name],{{type:'image/png'}});blob.name=name;return blob;}}; const controller=attachment.create({{maxFiles:5,createOperationId:()=>`edit-${{++id}}`,upload:async()=>({{markdown:'ok'}})}}); ['one.png','private.png','three.png'].forEach(name=>controller.select(image(name))); controller.setNote(1,'Token hidden here'); controller.replace(1,image('private-edited.png')); (async()=>{{ const values=await controller.serialize(); process.stdout.write(JSON.stringify({{values:await Promise.all(values.map(async value=>({{name:value.filename,note:value.note||'',text:await value.blob.text()}}))),state:controller.state()}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ output = run_node(script) assert output["values"] == [ {"name": "one.png", "note": "", "text": "one.png"}, {"name": "private-edited.png", "note": "Token hidden here", "text": "private-edited.png"}, {"name": "three.png", "note": "", "text": "three.png"}, ] assert [item["uploaded"] for item in output["state"]] == [False, False, False] def test_mobile_editor_dialog_is_accessible_touch_sized_and_available_offline(): html = INDEX.read_text() css = CSS.read_text() worker = SERVICE_WORKER.read_text() assert 'id="edit-create-issue-attachment"' in html assert 'id="issue-evidence-editor"' in html assert 'role="dialog"' in html assert 'aria-labelledby="issue-evidence-editor-heading"' in html assert 'id="issue-evidence-editor-canvas"' in html assert 'aria-pressed="true"' in html assert "issue-evidence-editor.js" in worker assert ".issue-evidence-editor" in css assert "min-height:44px" in css assert "max-width:100%" in css assert "overflow-x:hidden" in css