feat: save photo-only captures as drafts (Closes #939)
This commit is contained in:
1 parent
8a85b344f8
commit
caa0fbc2fd
7 files changed
+118
-14
No files matched your search
@@ -186,6 +186,12 @@ def test_release_artifact_files_one_mobile_issue_exactly_once_after_offline_relo
|
||||
assert page.evaluate("() => navigator.serviceWorker.controller !== null")
|
||||
|
||||
new_action.click()
|
||||
expect(page.locator("#create-issue-sheet")).to_have_class("create-issue-sheet open")
|
||||
assert page.evaluate("document.activeElement?.id") != "create-issue-title"
|
||||
for control in page.locator(".photo-evidence-actions .issue-attachment-trigger").all():
|
||||
bounds = control.bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||
expect(page.locator("#voice-issue-capture")).to_be_visible()
|
||||
page.locator("#start-voice-issue-capture").click()
|
||||
page.evaluate("""([title, body]) => {
|
||||
|
||||
@@ -68,7 +68,11 @@ const planned=createUnfiledCaptures.summary({{
|
||||
attachmentCount:2,
|
||||
}});
|
||||
const minimal=createUnfiledCaptures.summary({{}});
|
||||
process.stdout.write(JSON.stringify({{planned,minimal}}));
|
||||
const titles=[
|
||||
createUnfiledCaptures.displayTitle({{title:'Pump failure',hasAttachment:true}}),
|
||||
createUnfiledCaptures.displayTitle({{title:'',hasAttachment:true}}),
|
||||
];
|
||||
process.stdout.write(JSON.stringify({{planned,minimal,titles}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
@@ -86,6 +90,24 @@ process.stdout.write(JSON.stringify({{planned,minimal}}));
|
||||
"action": "Choose repository",
|
||||
"details": "Create issue · No screenshots",
|
||||
},
|
||||
"titles": ["Pump failure", "Untitled photo draft"],
|
||||
}
|
||||
|
||||
|
||||
def test_capture_title_focus_defers_phone_keyboard_until_draft_continuation():
|
||||
script = f"""
|
||||
const createUnfiledCaptures=require({json.dumps(str(UNFILED))});
|
||||
const shouldFocus=createUnfiledCaptures.shouldFocusTitle;
|
||||
process.stdout.write(JSON.stringify({{
|
||||
freshPhone:shouldFocus({{mobile:true,continuing:false}}),
|
||||
resumedPhoto:shouldFocus({{mobile:true,continuing:true}}),
|
||||
freshDesktop:shouldFocus({{mobile:false,continuing:false}}),
|
||||
}}));
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"freshPhone": False,
|
||||
"resumedPhoto": True,
|
||||
"freshDesktop": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +220,41 @@ process.stdout.write(JSON.stringify({{listed:captures.list()[0],names:resumed.at
|
||||
assert output["stored"] == output["notes"]
|
||||
|
||||
|
||||
def test_unfiled_capture_saves_untitled_photo_only_draft_but_rejects_empty_capture():
|
||||
script = f"""
|
||||
const createUnfiledCaptures=require({json.dumps(str(UNFILED))});
|
||||
const values=new Map(),blobs=new Map();
|
||||
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const attachmentStore={{put:async(id,value)=>blobs.set(id,value),get:async id=>blobs.get(id),delete:async id=>blobs.delete(id)}};
|
||||
const photo={{filename:'field.jpg',contentType:'image/jpeg',blob:new Blob(['photo'],{{type:'image/jpeg'}}),note:'Pump label'}};
|
||||
(async()=>{{
|
||||
const captures=createUnfiledCaptures({{storage,attachmentStore,getCaptureLogin:()=>'timmy',getCurrentLogin:()=>'timmy',createId:()=>'photo-only'}});
|
||||
const saved=await captures.save({{title:' ',body:'',attachments:[photo]}});
|
||||
const listed=captures.list()[0];
|
||||
const resumed=await captures.resume(saved.id,'timmy');
|
||||
const remoteValues=new Map(),remoteBlobs=new Map();
|
||||
const remote=createUnfiledCaptures({{
|
||||
storage:{{getItem:k=>remoteValues.get(k)||null,setItem:(k,v)=>remoteValues.set(k,v)}},
|
||||
attachmentStore:{{put:async(id,value)=>remoteBlobs.set(id,value),get:async id=>remoteBlobs.get(id),delete:async id=>remoteBlobs.delete(id)}},
|
||||
getCaptureLogin:()=>'timmy',getCurrentLogin:()=>'timmy',createId:()=>'unused'
|
||||
}});
|
||||
await remote.mergeRemote(await captures.exportOwned('timmy'),'timmy');
|
||||
const remoteResumed=await remote.resume(saved.id,'timmy');
|
||||
let emptyError='';
|
||||
try {{ await captures.save({{title:' ',body:''}}); }} catch(error) {{ emptyError=error.message; }}
|
||||
process.stdout.write(JSON.stringify({{saved,listed,resumedTitle:resumed.title,resumedEvidence:resumed.attachments.map(item=>item.filename),remoteTitle:remoteResumed.title,remoteEvidence:remoteResumed.attachments.map(item=>item.filename),emptyError}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1)}});
|
||||
"""
|
||||
output = run_node(script)
|
||||
assert output["saved"]["title"] == ""
|
||||
assert output["listed"]["title"] == ""
|
||||
assert output["resumedTitle"] == ""
|
||||
assert output["resumedEvidence"] == ["field.jpg"]
|
||||
assert output["remoteTitle"] == ""
|
||||
assert output["remoteEvidence"] == ["field.jpg"]
|
||||
assert output["emptyError"] == "Add a title or photo before saving."
|
||||
|
||||
|
||||
def test_unfiled_captures_export_and_import_ordered_evidence_between_devices():
|
||||
script = f"""
|
||||
const createUnfiledCaptures=require({json.dumps(str(UNFILED))});
|
||||
@@ -643,7 +700,7 @@ process.stdout.write(JSON.stringify({{errors,size:values.size}}));
|
||||
output = run_node(script)
|
||||
|
||||
assert output == {
|
||||
"errors": ["Add a title before saving.", "Offline identity is unavailable. Reconnect once before saving private work."],
|
||||
"errors": ["Add a title or photo before saving.", "Offline identity is unavailable. Reconnect once before saving private work."],
|
||||
"size": 0,
|
||||
}
|
||||
|
||||
@@ -670,7 +727,8 @@ async def test_mobile_composer_exposes_cold_offline_save_and_account_safe_resume
|
||||
assert "dependencies.attachment[evidence ? 'restore' : 'clear'](evidence)" in DRAFT_SESSION.read_text()
|
||||
assert "await unfiledCaptures.completeResume(rUC)" in html
|
||||
assert "...(rUC ? { sourceCaptureId: rUC } : {})" in html
|
||||
assert "item.hasAttachment ? ' · Screenshot attached' : ''" in html
|
||||
assert "title:unfiledDraftDisplayTitle(item)" in html
|
||||
assert "item.hasAttachment ? 'Photo evidence attached' : ''" in html
|
||||
assert "issueCapture.saveDraft(resumed)" in DRAFT_SESSION.read_text()
|
||||
assert "item.kind === 'unfiled-issue'" in html
|
||||
assert '.create-issue-actions button { min-height:44px;' in html
|
||||
@@ -759,7 +817,8 @@ async def test_mobile_new_opens_capture_first_and_progressively_reveals_filing_f
|
||||
assert 'id="file-new-issue" type="button">File now' in html
|
||||
assert "function setIssueFilingMode(enabled)" in html
|
||||
assert "setIssueFilingMode(Boolean(captureDraft.repository))" in html
|
||||
assert "qs('#create-issue-title').focus();" in html
|
||||
assert "window.matchMedia?.('(max-width: 600px)').matches === true" in html
|
||||
assert "unfiledShouldFocusTitle({mobile:mobileCapture, continuing:continuingCapture})" in html
|
||||
assert "qs('#file-new-issue').addEventListener('click'" in html
|
||||
assert '.create-issue-capture-actions button { min-height:44px;' in html
|
||||
assert '.create-issue-capture-actions[hidden] { display:none;' in html
|
||||
|
||||
@@ -51,6 +51,20 @@ def test_unfiled_drafts_bound_collection_and_decoded_evidence(tmp_path):
|
||||
}])])
|
||||
|
||||
|
||||
def test_unfiled_drafts_allow_untitled_photo_evidence_but_reject_empty_records(tmp_path):
|
||||
store = UnfiledDraftStore(tmp_path / "unfiled.sqlite3")
|
||||
photo_only = draft(title="")
|
||||
|
||||
validated = main.UnfiledDraft.model_validate(photo_only).model_dump()
|
||||
assert store.replace("timmy", 0, [validated])["drafts"][0]["title"] == ""
|
||||
|
||||
empty = draft(title="", evidence=[])
|
||||
with pytest.raises(ValueError, match="title or evidence is required"):
|
||||
main.UnfiledDraft.model_validate(empty)
|
||||
with pytest.raises(ValueError, match="title or evidence is required"):
|
||||
store.replace("timmy", 1, [empty])
|
||||
|
||||
|
||||
def test_unfiled_drafts_validate_and_round_trip_complete_filing_plan(tmp_path):
|
||||
store = UnfiledDraftStore(tmp_path / "unfiled.sqlite3")
|
||||
planned = draft(evidence=[])
|
||||
|
||||
Reference in new issue
Block a user