feat: guard mobile Draft capacity (Closes #623)
This commit is contained in:
1 parent
14ba1bcf14
commit
fcb0575e38
9 files changed
+207
-27
No files matched your search
@@ -684,6 +684,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/widgets.js",
|
||||
"/dashboard/static/drafts.js",
|
||||
"/dashboard/static/unfiled-captures.js",
|
||||
"/dashboard/static/draft-capacity-dialog.js",
|
||||
"/dashboard/static/outbox-coordinator.js",
|
||||
"/dashboard/static/issue-outbox.js",
|
||||
"/dashboard/static/authored-outbox.js",
|
||||
|
||||
@@ -15,7 +15,7 @@ def run_node(script: str):
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_unfiled_captures_keep_twenty_newest_account_bound_notes():
|
||||
def test_unfiled_captures_block_at_capacity_until_oldest_is_explicitly_replaced():
|
||||
script = f"""
|
||||
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
|
||||
const values = new Map();
|
||||
@@ -32,25 +32,78 @@ const captures = createUnfiledCaptures({{
|
||||
createId:()=>String(++sequence),
|
||||
now:()=>1000 + sequence,
|
||||
}});
|
||||
for (let index = 1; index <= 22; index += 1) {{
|
||||
for (let index = 1; index <= 20; index += 1) {{
|
||||
captures.save({{title:'Note ' + index, body:'Context ' + index}});
|
||||
}}
|
||||
let fullError = '';
|
||||
try {{ captures.save({{title:'Note 21', body:'Context 21'}}); }} catch (error) {{
|
||||
fullError = error.message;
|
||||
}}
|
||||
const beforeReplace = captures.list();
|
||||
const oldest = captures.capacity().oldest;
|
||||
const replacement = captures.replaceOldest({{title:'Note 21', body:'Context 21'}}, oldest.id);
|
||||
const offline = captures.list();
|
||||
const restored = createUnfiledCaptures({{
|
||||
storage, getCaptureLogin:()=>'timmy', getCurrentLogin:()=>'timmy'
|
||||
}}).list();
|
||||
process.stdout.write(JSON.stringify({{offline, restored}}));
|
||||
process.stdout.write(JSON.stringify({{fullError,beforeReplace,oldest,replacement,offline,restored}}));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["fullError"] == "Drafts full — nothing was deleted."
|
||||
assert len(output["beforeReplace"]) == 20
|
||||
assert output["beforeReplace"][-1]["title"] == "Note 1"
|
||||
assert output["oldest"]["title"] == "Note 1"
|
||||
assert len(output["offline"]) == 20
|
||||
assert output["offline"][0]["title"] == "Note 22"
|
||||
assert output["offline"][-1]["title"] == "Note 3"
|
||||
assert output["offline"][0]["title"] == "Note 21"
|
||||
assert output["offline"][-1]["title"] == "Note 2"
|
||||
assert all(item["quarantined"] for item in output["offline"])
|
||||
assert all(not item["quarantined"] for item in output["restored"])
|
||||
assert output["restored"][0]["ownerLogin"] == "timmy"
|
||||
|
||||
|
||||
def test_replacing_oldest_capture_deletes_only_its_attachment_after_new_capture_is_durable():
|
||||
script = f"""
|
||||
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
|
||||
const values = new Map();
|
||||
const blobs = new Map();
|
||||
const deleted = [];
|
||||
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const attachmentStore = {{
|
||||
put: async (id, attachment) => blobs.set(id, attachment),
|
||||
get: async id => blobs.get(id) || null,
|
||||
delete: async id => {{ deleted.push(id); blobs.delete(id); }},
|
||||
}};
|
||||
(async () => {{
|
||||
let id = 0;
|
||||
const captures = createUnfiledCaptures({{
|
||||
storage, attachmentStore, maxItems:2, getCaptureLogin:()=>'timmy', getCurrentLogin:()=>'timmy',
|
||||
createId:()=>String(++id), now:()=>id,
|
||||
}});
|
||||
const image = name => ({{filename:name,contentType:'image/png',blob:new Blob([name],{{type:'image/png'}})}});
|
||||
const first = await captures.save({{title:'First',body:'one',attachment:image('first.png')}});
|
||||
const second = await captures.save({{title:'Second',body:'two',attachment:image('second.png')}});
|
||||
let mismatch = '';
|
||||
try {{ await captures.replaceOldest({{title:'Third',body:'three'}}, second.id); }}
|
||||
catch (error) {{ mismatch = error.message; }}
|
||||
const third = await captures.replaceOldest({{title:'Third',body:'three',attachment:image('third.png')}}, first.id);
|
||||
process.stdout.write(JSON.stringify({{
|
||||
mismatch, titles:captures.list().map(item=>item.title), deleted,
|
||||
blobs:[...blobs.keys()], third:third.title,
|
||||
}}));
|
||||
}})().catch(error => {{ console.error(error); process.exit(1); }});
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output == {
|
||||
"mismatch": "Drafts changed. Review them before replacing anything.",
|
||||
"titles": ["Third", "Second"],
|
||||
"deleted": ["1"],
|
||||
"blobs": ["2", "3"],
|
||||
"third": "Third",
|
||||
}
|
||||
|
||||
|
||||
def test_unfiled_capture_resume_requires_matching_confirmed_account_and_removes_only_selected_note():
|
||||
script = f"""
|
||||
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
|
||||
@@ -171,6 +224,22 @@ async def test_mobile_composer_exposes_cold_offline_save_and_account_safe_resume
|
||||
assert '@media(max-width:320px)' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_capture_capacity_requires_an_explicit_preserving_decision():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'id="draft-capacity-sheet"' in html
|
||||
assert 'Drafts full — nothing was deleted.' in html
|
||||
assert 'id="review-full-drafts"' in html
|
||||
assert 'id="replace-oldest-draft"' in html
|
||||
assert 'id="keep-editing-draft"' in html
|
||||
assert "showDraftCapacityDialog(unfiledCaptures)" in html
|
||||
feature = (Path(__file__).parents[1] / "frontend" / "draft-capacity-dialog.js").read_text()
|
||||
assert "unfiledCaptures.replaceOldest(draft, oldest.id)" in feature
|
||||
assert '.draft-capacity-panel { box-sizing:border-box; width:min(620px,100%); max-height:100dvh;' in html
|
||||
assert '.draft-capacity-actions button { min-height:44px;' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_new_opens_capture_first_and_progressively_reveals_filing_fields():
|
||||
html = await dashboard()
|
||||
|
||||
Reference in new issue
Block a user