feat: capture cold-offline work for filing (#301)
This commit is contained in:
1 parent
de8227f4f4
commit
9f6476a5b4
8 files changed
+244
-11
No files matched your search
@@ -95,7 +95,7 @@ async function dispatchNotificationClick(route) {{
|
||||
def test_strict_browser_assets_ship_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v28" in source
|
||||
assert "stackchain-dashboard-shell-v29" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
@@ -217,6 +217,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/search-preview.js",
|
||||
"/dashboard/static/widgets.js",
|
||||
"/dashboard/static/drafts.js",
|
||||
"/dashboard/static/unfiled-captures.js",
|
||||
"/dashboard/static/outbox-coordinator.js",
|
||||
"/dashboard/static/issue-outbox.js",
|
||||
"/dashboard/static/authored-outbox.js",
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.dashboard_bundle import dashboard
|
||||
|
||||
|
||||
UNFILED = Path(__file__).parents[1] / "frontend" / "unfiled-captures.js"
|
||||
|
||||
|
||||
def run_node(script: str):
|
||||
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_unfiled_captures_keep_twenty_newest_account_bound_notes():
|
||||
script = f"""
|
||||
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.get(key) || null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
let sequence = 0;
|
||||
const captures = createUnfiledCaptures({{
|
||||
storage,
|
||||
getCaptureLogin:()=>'timmy',
|
||||
getCurrentLogin:()=>'',
|
||||
createId:()=>String(++sequence),
|
||||
now:()=>1000 + sequence,
|
||||
}});
|
||||
for (let index = 1; index <= 22; index += 1) {{
|
||||
captures.save({{title:'Note ' + index, body:'Context ' + index}});
|
||||
}}
|
||||
const offline = captures.list();
|
||||
const restored = createUnfiledCaptures({{
|
||||
storage, getCaptureLogin:()=>'timmy', getCurrentLogin:()=>'timmy'
|
||||
}}).list();
|
||||
process.stdout.write(JSON.stringify({{offline, restored}}));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert len(output["offline"]) == 20
|
||||
assert output["offline"][0]["title"] == "Note 22"
|
||||
assert output["offline"][-1]["title"] == "Note 3"
|
||||
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_unfiled_capture_resume_requires_matching_confirmed_account_and_removes_only_selected_note():
|
||||
script = f"""
|
||||
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
|
||||
const values = new Map();
|
||||
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
|
||||
let id = 0;
|
||||
const captures = createUnfiledCaptures({{
|
||||
storage, getCaptureLogin:()=>'timmy', getCurrentLogin:()=>'timmy', createId:()=>String(++id), now:()=>id
|
||||
}});
|
||||
const first = captures.save({{title:'First',body:'One'}});
|
||||
const second = captures.save({{title:'Second',body:'Two'}});
|
||||
let mismatch = '';
|
||||
try {{ captures.resume(first.id, 'alexander'); }} catch (error) {{ mismatch = error.message; }}
|
||||
const resumed = captures.resume(first.id, 'timmy');
|
||||
process.stdout.write(JSON.stringify({{mismatch,resumed,remaining:captures.list(),second}}));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["mismatch"] == "Reconnect with the account that saved this capture."
|
||||
assert output["resumed"] == {"repository": "", "title": "First", "body": "One", "labelIds": []}
|
||||
assert [item["id"] for item in output["remaining"]] == [output["second"]["id"]]
|
||||
|
||||
|
||||
def test_unfiled_capture_rejects_empty_or_identityless_records_without_writing():
|
||||
script = f"""
|
||||
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
|
||||
const values = new Map();
|
||||
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const captures = createUnfiledCaptures({{storage,getCaptureLogin:()=>''}});
|
||||
const errors = [];
|
||||
for (const note of [{{title:'',body:'context'}},{{title:'Work',body:'context'}}]) {{
|
||||
try {{ captures.save(note); }} catch (error) {{ errors.push(error.message); }}
|
||||
}}
|
||||
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."],
|
||||
"size": 0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_composer_exposes_cold_offline_save_and_account_safe_resume_flow():
|
||||
html = await dashboard()
|
||||
|
||||
assert '<script src="static/unfiled-captures.js"></script>' in html
|
||||
assert 'id="save-unfiled-issue"' in html
|
||||
assert 'Save for filing' in html
|
||||
assert "createUnfiledCaptures({" in html
|
||||
assert "getCaptureLogin: () => String(lastContextSnapshot?.user?.login || '').trim()" in html
|
||||
assert "unfiledCaptures.save(captureDraft)" in html
|
||||
assert "unfiledCaptures.resume(item.capture_id, activeFlushLogin)" in html
|
||||
assert "issueCapture.saveDraft(resumed)" in html
|
||||
assert "item.kind === 'unfiled-issue'" in html
|
||||
assert '.create-issue-actions button { min-height:44px;' in html
|
||||
assert '@media(max-width:320px)' in html
|
||||
Reference in new issue
Block a user