feat: crop and redact mobile evidence (Closes #829)
This commit is contained in:
1 parent
7dd396154d
commit
87ff83bd2d
10 files changed
+398
-6
No files matched your search
@@ -0,0 +1,95 @@
|
||||
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
|
||||
@@ -838,6 +838,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/conversation.js",
|
||||
"/dashboard/static/comment-actions.js",
|
||||
"/dashboard/static/issue-evidence-review.js",
|
||||
"/dashboard/static/issue-evidence-editor.js",
|
||||
"/dashboard/static/issue-attachment.js",
|
||||
"/dashboard/static/issue-sheet.js",
|
||||
"/dashboard/static/create-issue-sheet.js",
|
||||
|
||||
Reference in new issue
Block a user