from __future__ import annotations import os import threading from pathlib import Path import pytest if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1": pytest.skip("packaged mobile photo-draft journey runs only in its gated CI job", allow_module_level=True) pytest.importorskip("playwright.sync_api") from playwright.sync_api import expect, sync_playwright from fake_gitea import FakeGiteaServer from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server def test_release_artifact_finds_and_reopens_photo_only_reply_from_mobile_my_work(tmp_path: Path): archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz")) assert len(archives) == 1, "browser job must provide exactly one assembled release archive" fake = FakeGiteaServer(("127.0.0.1", 0)) fake_thread = threading.Thread(target=fake.serve_forever, daemon=True) fake_thread.start() fake_url = f"http://127.0.0.1:{fake.server_port}" try: with release_server(archives[0], tmp_path, fake_url) as origin, sync_playwright() as playwright: browser = playwright.chromium.launch(args=["--ignore-certificate-errors"]) context = browser.new_context(viewport={"width": 390, "height": 844}, ignore_https_errors=True) page = context.new_page() page.goto(origin + "/", wait_until="networkidle") page.locator('input[name="device_label"]').fill("Photo draft release phone") page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator("#submit-sign-in").click() page.wait_for_url(origin + "/", wait_until="networkidle") page.evaluate( """() => new Promise((resolve, reject) => { const request=indexedDB.open('stackchain-conversation-reply-drafts-v1',1); request.onerror=()=>reject(request.error); request.onupgradeneeded=()=>{ if (!request.result.objectStoreNames.contains('drafts')) { request.result.createObjectStore('drafts',{keyPath:'id'}); } }; request.onsuccess=()=>{ const transaction=request.result.transaction('drafts','readwrite'); transaction.objectStore('drafts').put({ id:'timmy:issue:acme%2Fmobile%3A41', version:1, ownerLogin:'timmy', scope:'conversation', kind:'issue', repository:'acme/mobile', number:41, updatedAt:Date.now(), attachments:[{ filename:'rack-label.jpg', contentType:'image/jpeg', blob:new Blob(['field evidence'],{type:'image/jpeg'}), note:'Rack label', }], }); transaction.oncomplete=resolve; transaction.onerror=()=>reject(transaction.error); }; })""" ) page.reload(wait_until="networkidle") stored = page.evaluate( """() => new Promise((resolve,reject)=>{ const request=indexedDB.open('stackchain-conversation-reply-drafts-v1',1); request.onerror=()=>reject(request.error); request.onsuccess=()=>{ const values=request.result.transaction('drafts','readonly').objectStore('drafts').getAll(); values.onsuccess=()=>resolve(values.result.map(item=>({ownerLogin:item.ownerLogin,kind:item.kind,number:item.number}))); values.onerror=()=>reject(values.error); }; })""" ) assert stored == [{"ownerLogin": "timmy", "kind": "issue", "number": 41}] inventory = page.evaluate( """() => createConversationReplyDraftStore({indexedDB,getOwnerLogin:()=> 'timmy'}).list()""" ) assert len(inventory) == 1 expect(page.locator('[data-work-count="draft"]')).to_have_text("1", timeout=10_000) page.locator('[data-mobile-task="queues"]').click() page.locator('[data-mobile-queue="draft"]').click() card = page.locator(".draft-card", has_text="acme/mobile#41") expect(card).to_contain_text("1 saved photo") resume = card.get_by_role("button", name="Resume draft") bounds = resume.bounding_box() assert bounds and bounds["height"] >= 44 assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") resume.dispatch_event("click") expect(page.locator("#issue-sheet")).to_be_visible() expect(page.locator("#issue-attachment-preview")).to_be_visible() expect(page.locator("#issue-attachment-meta")).to_contain_text("rack-label.jpg") assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") browser.close() finally: fake.shutdown() fake.server_close() fake_thread.join(timeout=5)