-
+
' in html
+ assert 'for="create-issue-attachment">Choose existing' in html
+ assert 'id="create-issue-attachment" type="file" accept="image/png,image/jpeg,image/webp" multiple' in html
+ assert "inputs: [qs('#take-create-issue-photo'), qs('#create-issue-attachment')]" in source
+ controls = css.split(".photo-evidence-actions", 1)[1].split("}", 1)[0]
+ assert "grid-template-columns:repeat(2,minmax(0,1fr))" in controls
+ assert "min-width:0" in 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; }}
+ dispatch(type) {{ return this.listeners[type]({{target:this}}); }}
+}}
+const camera=new Element(),library=new Element(),preview=new Element(),image=new Element(),meta=new Element(),remove=new Element(),status=new Element();
+library.multiple=true;
+const controller=attachment.mount({{
+ input:library,inputs:[camera,library],preview,image,meta,remove,status,
+ optimizeImage:async file=>file,
+ createObjectURL:blob=>'blob:'+blob.name,revokeObjectURL:()=>{{}},upload:async()=>{{}},
+}});
+const photo=name=>{{const blob=new Blob([name],{{type:'image/jpeg'}});blob.name=name;return blob;}};
+;(async()=>{{
+ camera.files=[photo('camera-one.jpg')]; await camera.dispatch('change');
+ const afterCamera=controller.state();
+ camera.files=[]; await camera.dispatch('change');
+ const afterCancel=controller.state();
+ library.files=[photo('library-two.jpg')]; await library.dispatch('change');
+ process.stdout.write(JSON.stringify({{afterCamera,afterCancel,afterLibrary:controller.state()}}));
+}})().catch(error=>{{console.error(error);process.exit(1);}});
+"""
+ output = json.loads(run_node(script))
+ assert output["afterCamera"]["name"] == "camera-one.jpg"
+ assert output["afterCancel"] == output["afterCamera"]
+ assert [item["name"] for item in output["afterLibrary"]] == [
+ "camera-one.jpg", "library-two.jpg"
+ ]
+
+
def test_queued_screenshot_is_hydrated_before_the_issue_editor_opens_and_failure_is_retryable():
source = DASHBOARD.read_text()
handler = re.search(
diff --git a/tests/test_later_sync.py b/tests/test_later_sync.py
index b2068a0..3b2c26c 100644
--- a/tests/test_later_sync.py
+++ b/tests/test_later_sync.py
@@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
def test_later_sync_ships_atomically_in_the_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/later-sync.js'" in source
diff --git a/tests/test_markdown_renderer.py b/tests/test_markdown_renderer.py
index e013b6d..33b28a5 100644
--- a/tests/test_markdown_renderer.py
+++ b/tests/test_markdown_renderer.py
@@ -256,4 +256,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css
- assert "stackchain-dashboard-shell-v108" in worker
+ assert "stackchain-dashboard-shell-v109" in worker
diff --git a/tests/test_mobile_composer_integration.py b/tests/test_mobile_composer_integration.py
index 7f7a689..96acbcb 100644
--- a/tests/test_mobile_composer_integration.py
+++ b/tests/test_mobile_composer_integration.py
@@ -45,7 +45,7 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
- assert "stackchain-dashboard-shell-v108" in worker
+ assert "stackchain-dashboard-shell-v109" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():
diff --git a/tests/test_mobile_device_setup.py b/tests/test_mobile_device_setup.py
index 6a78a17..8c8eaa5 100644
--- a/tests/test_mobile_device_setup.py
+++ b/tests/test_mobile_device_setup.py
@@ -214,7 +214,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert "promptStorage:localStorage" in dashboard
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
assert "BASE + 'static/mobile-device-setup.js'" in worker
- assert "stackchain-dashboard-shell-v108" in worker
+ assert "stackchain-dashboard-shell-v109" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
diff --git a/tests/test_mobile_start_day.py b/tests/test_mobile_start_day.py
index 612eb0c..ef6bec3 100644
--- a/tests/test_mobile_start_day.py
+++ b/tests/test_mobile_start_day.py
@@ -283,4 +283,4 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
assert ".mobile-start-day-finish { min-height:44px;" in html
assert "max-width:100%; overflow-wrap:anywhere;" in html
assert "BASE + 'static/mobile-start-day.js'" in service_worker
- assert "stackchain-dashboard-shell-v108" in service_worker
+ assert "stackchain-dashboard-shell-v109" in service_worker
diff --git a/tests/test_plan_today.py b/tests/test_plan_today.py
index 6ca607e..e03cb8d 100644
--- a/tests/test_plan_today.py
+++ b/tests/test_plan_today.py
@@ -410,7 +410,7 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
def test_plan_today_controller_is_available_in_the_offline_shell():
source = SERVICE_WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/plan-today.js'" in source
assert "BASE + 'static/plan-today-readiness.js'" in source
assert "BASE + 'static/plan-today-preview.js'" in source
diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py
index 33a81ad..11f1fb0 100644
--- a/tests/test_service_worker.py
+++ b/tests/test_service_worker.py
@@ -155,7 +155,7 @@ async function dispatchPush(payload) {{
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -164,14 +164,14 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/dashboard.js'" in source
def test_offline_review_next_ships_today_completion_atomically():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -179,7 +179,7 @@ def test_offline_review_next_ships_today_completion_atomically():
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -187,7 +187,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/issue-sheet.js'" in source
assert "BASE + 'static/checklist-conflict.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -197,14 +197,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/later-picker.js'" in source
def test_navigation_deadline_ships_in_a_new_shell_cache():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -213,21 +213,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
def test_today_convergence_ships_in_a_new_shell_cache():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/today-sync.js'" in source
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/mobile-search-viewport.js'" in source
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -893,7 +893,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
def test_queue_today_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/queue-today.js'" in source
diff --git a/tests/test_today_readiness.py b/tests/test_today_readiness.py
index 4470adf..3b5b37b 100644
--- a/tests/test_today_readiness.py
+++ b/tests/test_today_readiness.py
@@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
def test_readiness_runtime_is_available_in_offline_shell():
service_worker = SERVICE_WORKER.read_text()
- assert "const CACHE = 'stackchain-dashboard-shell-v108';" in service_worker
+ assert "const CACHE = 'stackchain-dashboard-shell-v109';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker
diff --git a/tests/test_today_sync.py b/tests/test_today_sync.py
index 5e72c5d..722b318 100644
--- a/tests/test_today_sync.py
+++ b/tests/test_today_sync.py
@@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
def test_inflight_today_drain_ships_in_a_new_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
- assert "stackchain-dashboard-shell-v108" in source
+ assert "stackchain-dashboard-shell-v109" in source
assert "BASE + 'static/today-sync.js'" in source