522 lines
22 KiB
Python
522 lines
22 KiB
Python
import json
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
ATTACHMENT = Path(__file__).parents[1] / "frontend" / "issue-attachment.js"
|
|
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
|
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
|
SERVICE_WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
|
README = Path(__file__).parents[1] / "README.md"
|
|
|
|
|
|
def run_node(script: str) -> str:
|
|
return subprocess.run(
|
|
["node", "-e", script], check=True, capture_output=True, text=True
|
|
).stdout
|
|
|
|
|
|
def test_mobile_attachment_prepares_comment_and_reuses_confirmed_binary_upload():
|
|
script = f"""
|
|
const attachment = require({json.dumps(str(ATTACHMENT))});
|
|
const calls = [];
|
|
const file = new Blob(['png-bytes'],{{type:'image/png'}}); file.name='checkout.png';
|
|
const controller = attachment.create({{
|
|
createOperationId: () => 'attachment-comment-471',
|
|
readDataUrl: async () => {{ throw new Error('base64 conversion must not run'); }},
|
|
upload: async payload => {{ calls.push({{
|
|
repository:payload.repository,number:payload.number,filename:payload.filename,
|
|
content_type:payload.content_type,isBlob:payload.blob instanceof Blob,
|
|
text:await payload.blob.text(),operation_id:payload.operation_id
|
|
}}); return {{markdown:''}}; }},
|
|
}});
|
|
controller.select(file);
|
|
(async()=>{{
|
|
const first = await controller.prepareComment({{repository:'stackchain/api', number:17}}, 'Layout breaks');
|
|
const second = await controller.prepareComment({{repository:'stackchain/api', number:17}}, 'Layout breaks');
|
|
process.stdout.write(JSON.stringify({{first,second,calls,state:controller.state()}}));
|
|
}})().catch(error=>{{ console.error(error); process.exit(1); }});
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
assert output["first"] == (
|
|
"Layout breaks\n\n"
|
|
)
|
|
assert output["second"] == output["first"]
|
|
assert output["calls"] == [{
|
|
"repository": "stackchain/api",
|
|
"number": 17,
|
|
"filename": "checkout.png",
|
|
"content_type": "image/png",
|
|
"isBlob": True,
|
|
"text": "png-bytes",
|
|
"operation_id": "attachment-comment-471",
|
|
}]
|
|
assert output["state"] == {
|
|
"name": "checkout.png", "size": 9, "uploaded": True
|
|
}
|
|
|
|
|
|
def test_mobile_attachment_retry_reuses_operation_key_until_file_changes():
|
|
script = f"""
|
|
const attachment = require({json.dumps(str(ATTACHMENT))});
|
|
const keys=[];
|
|
let sequence=0;
|
|
let fail=true;
|
|
const controller=attachment.create({{
|
|
createOperationId:()=> 'attachment-' + (++sequence),
|
|
readDataUrl:async file=>'data:'+file.type+';base64,iVBORw0KGgo=',
|
|
upload:async payload=>{{keys.push(payload.operation_id);if(fail){{fail=false;throw new Error('offline');}}return {{markdown:''}};}},
|
|
}});
|
|
controller.select({{name:'first.png',type:'image/png',size:8}});
|
|
(async()=>{{
|
|
try{{await controller.prepareComment({{repository:'stackchain/api',number:17}},'Evidence');}}catch(_error){{}}
|
|
await controller.prepareComment({{repository:'stackchain/api',number:17}},'Evidence');
|
|
controller.select({{name:'replacement.png',type:'image/png',size:8}});
|
|
await controller.prepareComment({{repository:'stackchain/api',number:17}},'Replacement');
|
|
process.stdout.write(JSON.stringify(keys));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
|
|
assert json.loads(run_node(script)) == [
|
|
"attachment-1", "attachment-1", "attachment-2"
|
|
]
|
|
|
|
|
|
def test_selected_screenshot_serializes_as_binary_blob_without_base64_expansion():
|
|
script = f"""
|
|
const attachment = require({json.dumps(str(ATTACHMENT))});
|
|
const calls=[];
|
|
const file=new Blob(['binary-image'],{{type:'image/webp'}});
|
|
file.name='phone.webp';
|
|
const controller=attachment.create({{
|
|
readDataUrl:async()=>{{throw new Error('base64 conversion must not run');}},
|
|
upload:async()=>{{calls.push('upload');}},
|
|
}});
|
|
controller.select(file);
|
|
controller.serialize().then(async value=>process.stdout.write(JSON.stringify({{
|
|
filename:value.filename,contentType:value.contentType,
|
|
isBlob:value.blob instanceof Blob,size:value.blob.size,text:await value.blob.text(),calls
|
|
}})));
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert output == {
|
|
"filename": "phone.webp",
|
|
"contentType": "image/webp",
|
|
"isBlob": True,
|
|
"size": 12,
|
|
"text": "binary-image",
|
|
"calls": [],
|
|
}
|
|
|
|
|
|
def test_oversized_screenshot_is_optimized_before_preview_and_durable_serialization():
|
|
script = f"""
|
|
const attachment = require({json.dumps(str(ATTACHMENT))});
|
|
const original = new Blob([new Uint8Array(attachment.MAX_BYTES + 200)], {{type:'image/png'}});
|
|
original.name = 'phone.png';
|
|
const calls = [];
|
|
const controller = attachment.create({{
|
|
optimizeImage: async file => {{
|
|
calls.push({{name:file.name,type:file.type,size:file.size}});
|
|
const optimized = new Blob(['optimized-png'], {{type:'image/png'}});
|
|
optimized.name = file.name;
|
|
return optimized;
|
|
}},
|
|
upload: async () => {{ throw new Error('must not upload during selection'); }},
|
|
}});
|
|
(async()=>{{
|
|
await controller.select(original);
|
|
const serialized = await controller.serialize();
|
|
process.stdout.write(JSON.stringify({{
|
|
calls,state:controller.state(),name:serialized.filename,
|
|
contentType:serialized.contentType,size:serialized.blob.size,
|
|
text:await serialized.blob.text(),sameBlob:serialized.blob===original,
|
|
}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert output == {
|
|
"calls": [{
|
|
"name": "phone.png",
|
|
"type": "image/png",
|
|
"size": 2 * 1024 * 1024 + 200,
|
|
}],
|
|
"state": {"name": "phone.png", "size": 13, "uploaded": False},
|
|
"name": "phone.png",
|
|
"contentType": "image/png",
|
|
"size": 13,
|
|
"text": "optimized-png",
|
|
"sameBlob": False,
|
|
}
|
|
|
|
|
|
def test_browser_optimizer_downscales_until_encoded_image_fits_limit():
|
|
script = f"""
|
|
const attachment = require({json.dumps(str(ATTACHMENT))});
|
|
const original = new Blob([new Uint8Array(attachment.MAX_BYTES + 1)], {{type:'image/jpeg'}});
|
|
original.name = 'camera.jpg';
|
|
const attempts = [];
|
|
let closed = false;
|
|
const canvas = {{
|
|
width: 0, height: 0,
|
|
getContext: () => ({{drawImage: () => {{}}}}),
|
|
toBlob: callback => {{
|
|
attempts.push([canvas.width, canvas.height]);
|
|
const size = attempts.length === 1 ? attachment.MAX_BYTES + 50 : attachment.MAX_BYTES - 50;
|
|
callback(new Blob([new Uint8Array(size)], {{type:'image/jpeg'}}));
|
|
}},
|
|
}};
|
|
(async()=>{{
|
|
const result = await attachment.optimizeImage(original, {{
|
|
createImageBitmap: async () => ({{width:2000,height:1000,close:()=>{{closed=true;}}}}),
|
|
createCanvas: () => canvas,
|
|
}});
|
|
process.stdout.write(JSON.stringify({{
|
|
attempts,closed,size:result.size,type:result.type,name:result.name,
|
|
}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert len(output["attempts"]) == 2
|
|
assert output["attempts"][1][0] < output["attempts"][0][0]
|
|
assert output["attempts"][1][1] < output["attempts"][0][1]
|
|
assert output["closed"] is True
|
|
assert output["size"] == 2 * 1024 * 1024 - 50
|
|
assert output["type"] == "image/jpeg"
|
|
assert output["name"] == "camera.jpg"
|
|
|
|
|
|
def test_binary_screenshot_builds_multipart_body_with_original_bytes():
|
|
script = f"""
|
|
const attachment=require({json.dumps(str(ATTACHMENT))});
|
|
const blob=new Blob(['original-bytes'],{{type:'image/png'}});
|
|
const form=attachment.multipart({{filename:'phone.png',contentType:'image/png',blob}});
|
|
const file=form.get('file');
|
|
(async()=>process.stdout.write(JSON.stringify({{
|
|
filename:file.name,type:file.type,size:file.size,text:await file.text()
|
|
}})))();
|
|
"""
|
|
|
|
assert json.loads(run_node(script)) == {
|
|
"filename": "phone.png",
|
|
"type": "image/png",
|
|
"size": 14,
|
|
"text": "original-bytes",
|
|
}
|
|
|
|
|
|
def test_serialized_capture_attachment_can_be_restored_or_removed_while_editing():
|
|
script = f"""
|
|
const attachment=require({json.dumps(str(ATTACHMENT))});
|
|
const controller=attachment.create({{readDataUrl:async()=>{{throw new Error('must not reread');}},upload:async()=>{{}}}});
|
|
controller.restore({{filename:'saved.png',contentType:'image/png',data:'iVBORw0KGgo='}});
|
|
(async()=>{{const restored={{state:controller.state(),value:await controller.serialize()}};controller.clear();process.stdout.write(JSON.stringify({{restored,removed:await controller.serialize()}}));}})();
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert output["restored"]["state"]["name"] == "saved.png"
|
|
assert output["restored"]["value"]["data"] == "iVBORw0KGgo="
|
|
assert output["removed"] is None
|
|
|
|
|
|
def test_metadata_only_attachment_cannot_render_as_an_empty_screenshot():
|
|
script = f"""
|
|
const attachment=require({json.dumps(str(ATTACHMENT))});
|
|
const controller=attachment.create({{readDataUrl:async()=>'',upload:async()=>{{}}}});
|
|
try {{
|
|
controller.restore({{filename:'saved.png',contentType:'image/png',stored:true}});
|
|
process.stdout.write(JSON.stringify({{restored:true,state:controller.state()}}));
|
|
}} catch (error) {{
|
|
process.stdout.write(JSON.stringify({{restored:false,message:error.message,state:controller.state()}}));
|
|
}}
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert output["restored"] is False
|
|
assert "saved screenshot" in output["message"].lower()
|
|
assert output["state"] is None
|
|
|
|
|
|
def test_issue_composer_renders_thumb_reachable_screenshot_preview():
|
|
html = INDEX.read_text()
|
|
css = CSS.read_text()
|
|
|
|
assert 'id="issue-attachment"' in html
|
|
assert 'type="file"' in html
|
|
assert 'accept="image/png,image/jpeg,image/webp"' in html
|
|
assert 'for="issue-attachment"' in html
|
|
assert 'id="issue-attachment-preview"' in html
|
|
assert 'id="remove-issue-attachment"' in html
|
|
assert '.issue-attachment-trigger' in css
|
|
assert '.issue-attachment-preview' in css
|
|
assert 'min-height:44px' in css
|
|
|
|
|
|
def test_new_issue_sheet_captures_screenshot_into_durable_outbox():
|
|
html = INDEX.read_text()
|
|
source = DASHBOARD.read_text()
|
|
css = CSS.read_text()
|
|
|
|
assert 'id="create-issue-attachment"' in html
|
|
assert 'accept="image/png,image/jpeg,image/webp"' in html
|
|
assert 'id="create-issue-attachment-preview"' in html
|
|
assert 'id="remove-create-issue-attachment"' in html
|
|
assert "const createIssueAttachmentController = issueAttachment.mount({" in source
|
|
assert "attachment: await createIssueAttachmentController.serialize()" in source
|
|
assert "createIssueAttachmentController.restore(hydrated.attachment);" in source
|
|
assert "createIssueAttachmentController.clear();" in source
|
|
assert ".create-issue-attachment" in css
|
|
assert "overflow-x:hidden" in css
|
|
|
|
|
|
def test_queued_screenshot_is_hydrated_before_the_issue_editor_opens_and_failure_is_retryable():
|
|
source = DASHBOARD.read_text()
|
|
handler = re.search(
|
|
r"list\.querySelectorAll\('\.draft-edit'\).*?addEventListener\('click', async \(\) => \{(?P<body>.*?)\n \}\);",
|
|
source,
|
|
re.DOTALL,
|
|
)
|
|
assert handler is not None
|
|
body = handler.group("body")
|
|
assert "await issueOutbox.hydrateForEdit(queued.id)" in body
|
|
assert body.index("await issueOutbox.hydrateForEdit(queued.id)") < body.index(
|
|
"openCreateIssueSheet()"
|
|
)
|
|
assert "catch (error)" in body
|
|
assert "saved screenshot" in body.lower()
|
|
|
|
|
|
def test_attachment_view_keeps_invalid_draft_and_removes_preview():
|
|
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=''; }}
|
|
addEventListener(type, fn) {{ this.listeners[type]=fn; }}
|
|
dispatch(type) {{ return this.listeners[type]({{target:this}}); }}
|
|
}}
|
|
const input=new Element(), preview=new Element(), image=new Element(), meta=new Element(), remove=new Element(), status=new Element();
|
|
const revoked=[];
|
|
const controller=attachment.mount({{
|
|
input,preview,image,meta,remove,status,
|
|
createObjectURL:()=> 'blob:preview', revokeObjectURL:url=>revoked.push(url),
|
|
readDataUrl:async()=>'', upload:async()=>{{}},
|
|
}});
|
|
input.files=[{{name:'payload.svg',type:'image/svg+xml',size:20}}]; input.dispatch('change');
|
|
const invalid={{message:status.textContent,hidden:preview.hidden}};
|
|
input.files=[{{name:'screen.png',type:'image/png',size:2048}}]; input.dispatch('change');
|
|
const selected={{src:image.src,meta:meta.textContent,hidden:preview.hidden,state:controller.state()}};
|
|
remove.dispatch('click');
|
|
process.stdout.write(JSON.stringify({{invalid,selected,removed:{{hidden:preview.hidden,src:image.src,revoked,state:controller.state()}}}}));
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
assert output["invalid"] == {
|
|
"message": "Choose a PNG, JPEG, or WebP screenshot.", "hidden": True
|
|
}
|
|
assert output["selected"]["src"] == "blob:preview"
|
|
assert output["selected"]["hidden"] is False
|
|
assert "screen.png" in output["selected"]["meta"]
|
|
assert output["removed"] == {
|
|
"hidden": True, "src": "", "revoked": ["blob:preview"], "state": None
|
|
}
|
|
|
|
|
|
def test_oversized_attachment_view_shows_progress_then_previews_optimized_blob():
|
|
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; }}
|
|
addEventListener(type,fn) {{ this.listeners[type]=fn; }}
|
|
dispatch(type) {{ return this.listeners[type]({{target:this}}); }}
|
|
}}
|
|
const input=new Element(),preview=new Element(),image=new Element(),meta=new Element(),remove=new Element(),status=new Element();
|
|
const original=new Blob([new Uint8Array(attachment.MAX_BYTES+10)],{{type:'image/png'}}); original.name='large.png';
|
|
const optimized=new Blob(['small'],{{type:'image/png'}}); optimized.name='large.png';
|
|
let finish;
|
|
const gate=new Promise(resolve=>{{finish=resolve;}});
|
|
const previewed=[];
|
|
attachment.mount({{
|
|
input,preview,image,meta,remove,status,
|
|
optimizeImage:async()=>{{await gate;return optimized;}},
|
|
createObjectURL:blob=>{{previewed.push(blob===optimized);return 'blob:optimized';}},
|
|
revokeObjectURL:()=>{{}},upload:async()=>{{}},
|
|
}});
|
|
input.files=[original];
|
|
const pending=input.dispatch('change');
|
|
const during={{message:status.textContent,disabled:input.disabled,hidden:preview.hidden}};
|
|
finish();
|
|
Promise.resolve(pending).then(()=>process.stdout.write(JSON.stringify({{
|
|
during,after:{{message:status.textContent,disabled:input.disabled,hidden:preview.hidden,
|
|
src:image.src,meta:meta.textContent}},previewed
|
|
}}))).catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert output["during"] == {
|
|
"message": "Optimizing screenshot…", "disabled": True, "hidden": True
|
|
}
|
|
assert output["after"]["message"] == "Screenshot optimized and ready to upload."
|
|
assert output["after"]["disabled"] is False
|
|
assert output["after"]["hidden"] is False
|
|
assert output["after"]["src"] == "blob:optimized"
|
|
assert "large.png" in output["after"]["meta"]
|
|
assert output["previewed"] == [True]
|
|
|
|
|
|
def test_removing_screenshot_during_optimization_cancels_stale_selection():
|
|
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; }}
|
|
addEventListener(type,fn) {{ this.listeners[type]=fn; }}
|
|
dispatch(type) {{ return this.listeners[type]({{target:this}}); }}
|
|
}}
|
|
const input=new Element(),preview=new Element(),image=new Element(),meta=new Element(),remove=new Element(),status=new Element();
|
|
const original=new Blob([new Uint8Array(attachment.MAX_BYTES+10)],{{type:'image/webp'}}); original.name='large.webp';
|
|
const optimized=new Blob(['small'],{{type:'image/webp'}}); optimized.name='large.webp';
|
|
let finish;
|
|
const gate=new Promise(resolve=>{{finish=resolve;}});
|
|
const controller=attachment.mount({{
|
|
input,preview,image,meta,remove,status,
|
|
optimizeImage:async()=>{{await gate;return optimized;}},
|
|
createObjectURL:()=> 'blob:stale',revokeObjectURL:()=>{{}},upload:async()=>{{}},
|
|
}});
|
|
input.files=[original];
|
|
const pending=input.dispatch('change');
|
|
remove.dispatch('click');
|
|
finish();
|
|
Promise.resolve(pending).then(async()=>process.stdout.write(JSON.stringify({{
|
|
state:controller.state(),serialized:await controller.serialize(),disabled:input.disabled,
|
|
hidden:preview.hidden,src:image.src,status:status.textContent,
|
|
}}))).catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert output == {
|
|
"state": None,
|
|
"serialized": None,
|
|
"disabled": False,
|
|
"hidden": True,
|
|
"src": "",
|
|
"status": "Screenshot removed. Your comment is unchanged.",
|
|
}
|
|
|
|
|
|
def test_pending_comment_admission_locks_attachment_replacement_and_remove_controls():
|
|
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; }}
|
|
addEventListener(type,fn) {{ this.listeners[type]=fn; }}
|
|
}}
|
|
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,
|
|
createObjectURL:()=> 'blob:preview',revokeObjectURL:()=>{{}},upload:async()=>{{}}}});
|
|
controller.setBusy(true);
|
|
const pending={{input:input.disabled,remove:remove.disabled}};
|
|
controller.setBusy(false);
|
|
process.stdout.write(JSON.stringify({{pending,released:{{input:input.disabled,remove:remove.disabled}}}}));
|
|
"""
|
|
|
|
assert json.loads(run_node(script)) == {
|
|
"pending": {"input": True, "remove": True},
|
|
"released": {"input": False, "remove": False},
|
|
}
|
|
|
|
|
|
def test_issue_comment_actions_upload_binary_multipart_before_posting_and_clear_after_acceptance():
|
|
source = DASHBOARD.read_text()
|
|
|
|
assert "issueAttachment.mount({" in source
|
|
assert "'/attachments'" in source
|
|
assert "issueAttachmentController.prepareComment(item, body)" in source
|
|
assert "issueAttachmentController.prepareComment(selectedIssue, body)" in source
|
|
assert source.count("issueAttachmentController.clear();") >= 2
|
|
assert "'Idempotency-Key': payload.operation_id" in source
|
|
assert "body: issueAttachment.multipart(payload)" in source
|
|
|
|
|
|
def test_closing_issue_sheet_cannot_carry_a_screenshot_to_another_issue():
|
|
source = DASHBOARD.read_text()
|
|
close_body = re.search(
|
|
r"function closeIssueSheet\(navigate = true\) \{(?P<body>.*?)\n \}",
|
|
source,
|
|
re.DOTALL,
|
|
).group("body")
|
|
assert "issueAttachmentController.clear();" in close_body
|
|
|
|
|
|
def test_attachment_runtime_is_available_in_the_offline_app_shell():
|
|
assert "static/issue-attachment.js" in SERVICE_WORKER.read_text()
|
|
|
|
|
|
def test_readme_documents_mobile_screenshot_limits_and_delivery_order():
|
|
readme = README.read_text()
|
|
assert "PNG, JPEG, or WebP" in readme
|
|
assert "2 MB" in readme
|
|
assert "automatically optimizes oversized screenshots" in readme
|
|
assert "uploads before the comment is posted" in readme
|
|
assert "New issue" in readme
|
|
assert "retry resumes with the confirmed issue" in readme
|
|
|
|
|
|
def test_issue_screenshot_comments_queue_serialized_bytes_before_clearing_or_advancing():
|
|
source = DASHBOARD.read_text()
|
|
|
|
assert "async function queueIssueScreenshotComment" in source
|
|
assert "attachment: await issueAttachmentController.serialize()" in source
|
|
assert "await authoredOutbox.enqueueDurably(message)" in source
|
|
assert "await issueCommentNext.admit(item, message)" in source
|
|
assert "navigator.onLine === false" in source
|
|
|
|
|
|
def test_assigned_pull_composer_offers_screenshot_preview_remove_and_reuses_optimizer():
|
|
html = INDEX.read_text()
|
|
source = DASHBOARD.read_text()
|
|
|
|
assert 'id="pull-attachment"' in html
|
|
assert 'accept="image/png,image/jpeg,image/webp"' in html
|
|
assert 'for="pull-attachment"' in html
|
|
assert 'id="pull-attachment-preview"' in html
|
|
assert 'id="pull-attachment-image"' in html
|
|
assert 'id="remove-pull-attachment"' in html
|
|
assert "const pullAttachmentController = issueAttachment.mount({" in source
|
|
assert "pullAttachmentController.clear();" in source
|
|
|
|
|
|
def test_retrying_same_pull_load_preserves_screenshot_but_target_change_clears_it():
|
|
source = DASHBOARD.read_text()
|
|
open_body = source.split("async function openPullSheet(item, trigger, offlineDetail = null) {", 1)[1].split(
|
|
"\n function closePullSheet", 1
|
|
)[0]
|
|
|
|
assert "if (!sameWorkTarget(selectedPull, item)) pullAttachmentController.clear();" in open_body
|
|
|
|
|
|
def test_pull_screenshot_comment_uploads_or_durably_admits_before_clearing_draft():
|
|
source = DASHBOARD.read_text()
|
|
|
|
assert "async function queuePullScreenshotComment" in source
|
|
assert "attachment: await pullAttachmentController.serialize()" in source
|
|
assert "await authoredOutbox.enqueueDurably(message)" in source
|
|
assert "navigator.onLine === false" in source
|
|
assert "Your comment and screenshot are safe; retry." in source
|
|
|
|
|
|
def test_pull_screenshot_foreground_and_replay_share_the_durable_operation_pipeline():
|
|
source = DASHBOARD.read_text()
|
|
standard = source.split(
|
|
"qs('#send-pull-comment').addEventListener('click', async () => {", 1
|
|
)[1].split("\n });", 1)[0]
|
|
comment_next = source.split("async function submitCommentAndNext(kind) {", 1)[1].split(
|
|
"\n }\n qs('#send-issue-comment-next')", 1
|
|
)[0]
|
|
|
|
assert "await queuePullScreenshotComment(item, body, operationId, false, true)" in standard
|
|
assert "pullAttachmentController.prepareComment" not in standard
|
|
assert "attachmentController.state() && (kind === 'pull' || navigator.onLine === false)" in comment_next
|