feat: review and reorder mobile evidence (Closes #825)
This commit is contained in:
1 parent
6a9271cabb
commit
6315323a48
10 files changed
+345
-73
No files matched your search
@@ -89,6 +89,33 @@ controller.select(image('replacement.png'));
|
||||
assert len({key for _, key in output["calls"]}) == 5
|
||||
|
||||
|
||||
def test_mobile_evidence_bundle_reorders_selected_image_for_serialization_and_upload():
|
||||
script = f"""
|
||||
const attachment = require({json.dumps(str(ATTACHMENT))});
|
||||
const uploaded=[];
|
||||
const image=name=>{{const blob=new Blob([name],{{type:'image/png'}});blob.name=name;return blob;}};
|
||||
const controller=attachment.create({{
|
||||
maxFiles:5,
|
||||
upload:async payload=>{{uploaded.push(payload.filename);return {{markdown:''}};}},
|
||||
}});
|
||||
['one.png','two.png','three.png'].forEach(name=>controller.select(image(name)));
|
||||
controller.move(2, 0);
|
||||
controller.move(0, 99);
|
||||
controller.move(0, 1);
|
||||
(async()=>{{
|
||||
const serialized=await controller.serialize();
|
||||
const comment=await controller.prepareComment({{repository:'o/r',number:825}},'Reviewed');
|
||||
process.stdout.write(JSON.stringify({{names:serialized.map(x=>x.filename),uploaded,comment}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
output = json.loads(run_node(script))
|
||||
assert output["names"] == ["one.png", "three.png", "two.png"]
|
||||
assert output["uploaded"] == output["names"]
|
||||
assert output["comment"].endswith(
|
||||
"\n\n\n\n"
|
||||
)
|
||||
|
||||
|
||||
def test_mobile_attachment_retry_reuses_operation_key_until_file_changes():
|
||||
script = f"""
|
||||
const attachment = require({json.dumps(str(ATTACHMENT))});
|
||||
@@ -273,6 +300,53 @@ process.stdout.write(JSON.stringify({{restored,src:image.src,meta:meta.textConte
|
||||
|
||||
|
||||
|
||||
def test_evidence_review_tray_selects_reorders_and_removes_any_screenshot():
|
||||
script = f"""
|
||||
const attachment=require({json.dumps(str(ATTACHMENT))});
|
||||
class Element{{
|
||||
constructor(tag='div'){{this.tag=tag;this.listeners={{}};this.children=[];this.hidden=true;this.value='';this.files=[];this.textContent='';this.src='';this.disabled=false;this.attributes={{}};this.dataset={{}};}}
|
||||
addEventListener(type,fn){{this.listeners[type]=fn;}}
|
||||
dispatch(type){{return this.listeners[type]({{target:this}});}}
|
||||
appendChild(child){{this.children.push(child);return child;}}
|
||||
replaceChildren(...children){{this.children=children;}}
|
||||
setAttribute(name,value){{this.attributes[name]=String(value);}}
|
||||
}}
|
||||
const document={{createElement:tag=>new Element(tag)}};
|
||||
const input=new Element('input'),preview=new Element(),image=new Element('img'),meta=new Element(),remove=new Element('button'),status=new Element(),tray=new Element(),earlier=new Element('button'),later=new Element('button');
|
||||
input.multiple=true;
|
||||
const urls=[];
|
||||
const controller=attachment.mount({{input,preview,image,meta,remove,status,tray,earlier,later,document,
|
||||
removedMessage:'Screenshot removed. Your issue draft is unchanged.',
|
||||
createObjectURL:blob=>{{const url='blob:'+blob.name;urls.push(url);return url;}},revokeObjectURL:()=>{{}},upload:async()=>{{}}}});
|
||||
const file=name=>{{const blob=new Blob([name],{{type:'image/png'}});blob.name=name;return blob;}};
|
||||
input.files=['one.png','two.png','three.png'].map(file);
|
||||
(async()=>{{
|
||||
await input.dispatch('change');
|
||||
await tray.children[1].dispatch('click');
|
||||
await earlier.dispatch('click');
|
||||
const reordered=(await controller.serialize()).map(item=>item.filename);
|
||||
await remove.dispatch('click');
|
||||
const remaining=(await controller.serialize()).map(item=>item.filename);
|
||||
process.stdout.write(JSON.stringify({{reordered,remaining,preview:image.src,meta:meta.textContent,
|
||||
selected:tray.children.map(button=>button.attributes['aria-pressed']),
|
||||
labels:tray.children.map(button=>button.attributes['aria-label']),
|
||||
earlierDisabled:earlier.disabled,laterDisabled:later.disabled,status:status.textContent}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
output = json.loads(run_node(script))
|
||||
assert output["reordered"] == ["two.png", "one.png", "three.png"]
|
||||
assert output["remaining"] == ["one.png", "three.png"]
|
||||
assert output["preview"] == "blob:one.png"
|
||||
assert output["meta"] == "Screenshot 1 of 2 · one.png · 1 KB"
|
||||
assert output["selected"] == ["true", "false"]
|
||||
assert output["labels"] == [
|
||||
"Review screenshot 1 of 2: one.png", "Review screenshot 2 of 2: three.png"
|
||||
]
|
||||
assert output["earlierDisabled"] is True
|
||||
assert output["laterDisabled"] is False
|
||||
assert output["status"] == "Screenshot removed. Your issue draft is unchanged."
|
||||
|
||||
|
||||
def test_metadata_only_attachment_cannot_render_as_an_empty_screenshot():
|
||||
script = f"""
|
||||
const attachment=require({json.dumps(str(ATTACHMENT))});
|
||||
@@ -317,11 +391,25 @@ def test_new_issue_sheet_captures_screenshot_into_durable_outbox():
|
||||
assert 'Up to 5' in html
|
||||
assert 'id="create-issue-attachment-preview"' in html
|
||||
assert 'id="remove-create-issue-attachment"' in html
|
||||
assert 'id="create-issue-evidence-tray"' in html
|
||||
assert 'role="toolbar"' in html
|
||||
assert 'aria-label="Evidence screenshots"' in html
|
||||
assert 'id="move-create-issue-attachment-earlier"' in html
|
||||
assert 'id="move-create-issue-attachment-later"' in html
|
||||
assert "tray: qs('#create-issue-evidence-tray')" in source
|
||||
assert "earlier: qs('#move-create-issue-attachment-earlier')" in source
|
||||
assert "later: qs('#move-create-issue-attachment-later')" in source
|
||||
assert "const createIssueAttachmentController = issueAttachment.mount({" in source
|
||||
assert "attachments:evidence" in source
|
||||
assert "hydrated.attachments || hydrated.attachment" in source
|
||||
assert "createIssueAttachmentController.clear();" in source
|
||||
assert ".create-issue-attachment" in css
|
||||
assert ".issue-evidence-tray" in css
|
||||
assert ".issue-evidence-review-actions" in css
|
||||
assert "#create-issue-attachment-preview { grid-template-columns:1fr;" in css
|
||||
assert "#create-issue-attachment-image { grid-row:auto; width:100%;" in css
|
||||
assert "overflow-x:auto" in css
|
||||
assert "min-width:44px" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
||||
|
||||
@@ -461,21 +549,38 @@ def test_pending_comment_admission_locks_attachment_replacement_and_remove_contr
|
||||
script = f"""
|
||||
const attachment=require({json.dumps(str(ATTACHMENT))});
|
||||
class Element {{
|
||||
constructor() {{ this.listeners={{}};this.hidden=true;this.value='';this.files=[];this.textContent='';this.src='';this.disabled=false; }}
|
||||
constructor() {{ this.listeners={{}};this.children=[];this.hidden=true;this.value='';this.files=[];this.textContent='';this.src='';this.disabled=false;this.attributes={{}}; }}
|
||||
addEventListener(type,fn) {{ this.listeners[type]=fn; }}
|
||||
appendChild(child) {{ this.children.push(child); }}
|
||||
replaceChildren(...children) {{ this.children=children; }}
|
||||
setAttribute(name,value) {{ this.attributes[name]=String(value); }}
|
||||
}}
|
||||
const input=new Element(),preview=new Element(),image=new Element(),meta=new Element(),remove=new Element(),status=new Element();
|
||||
const controller=attachment.mount({{input,preview,image,meta,remove,status,
|
||||
const input=new Element(),preview=new Element(),image=new Element(),meta=new Element(),remove=new Element(),status=new Element(),tray=new Element(),earlier=new Element(),later=new Element();
|
||||
input.multiple=true;
|
||||
const document={{createElement:()=>new Element()}};
|
||||
const controller=attachment.mount({{input,preview,image,meta,remove,status,tray,earlier,later,document,
|
||||
createObjectURL:()=> 'blob:preview',revokeObjectURL:()=>{{}},upload:async()=>{{}}}});
|
||||
controller.restore([
|
||||
{{filename:'one.png',contentType:'image/png',blob:new Blob(['one'],{{type:'image/png'}})}},
|
||||
{{filename:'two.png',contentType:'image/png',blob:new Blob(['two'],{{type:'image/png'}})}},
|
||||
]);
|
||||
controller.setBusy(true);
|
||||
const pending={{input:input.disabled,remove:remove.disabled}};
|
||||
const pending={{input:input.disabled,remove:remove.disabled,earlier:earlier.disabled,later:later.disabled,
|
||||
thumbnails:tray.children.map(button=>button.disabled)}};
|
||||
controller.setBusy(false);
|
||||
process.stdout.write(JSON.stringify({{pending,released:{{input:input.disabled,remove:remove.disabled}}}}));
|
||||
process.stdout.write(JSON.stringify({{pending,released:{{input:input.disabled,remove:remove.disabled,
|
||||
earlier:earlier.disabled,later:later.disabled,thumbnails:tray.children.map(button=>button.disabled)}}}}));
|
||||
"""
|
||||
|
||||
assert json.loads(run_node(script)) == {
|
||||
"pending": {"input": True, "remove": True},
|
||||
"released": {"input": False, "remove": False},
|
||||
"pending": {
|
||||
"input": True, "remove": True, "earlier": True, "later": True,
|
||||
"thumbnails": [True, True],
|
||||
},
|
||||
"released": {
|
||||
"input": False, "remove": False, "earlier": False, "later": True,
|
||||
"thumbnails": [False, False],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -513,6 +618,9 @@ def test_readme_documents_mobile_screenshot_limits_and_delivery_order():
|
||||
assert "uploads before the comment is posted" in readme
|
||||
assert "New issue" in readme
|
||||
assert "retry resumes with the confirmed issue" in readme
|
||||
assert "thumbnail tray" in readme
|
||||
assert "Move earlier" in readme
|
||||
assert "Remove selected" in readme
|
||||
|
||||
|
||||
def test_issue_screenshot_comments_queue_serialized_bytes_before_clearing_or_advancing():
|
||||
|
||||
@@ -837,6 +837,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/search-batch-plan.js",
|
||||
"/dashboard/static/conversation.js",
|
||||
"/dashboard/static/comment-actions.js",
|
||||
"/dashboard/static/issue-evidence-review.js",
|
||||
"/dashboard/static/issue-attachment.js",
|
||||
"/dashboard/static/issue-sheet.js",
|
||||
"/dashboard/static/create-issue-sheet.js",
|
||||
|
||||
Reference in new issue
Block a user