import os from pathlib import Path import pytest if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1": pytest.skip("packaged mobile Following 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 ROOT = Path(__file__).parents[2] FRONTEND = ROOT / "frontend" @pytest.mark.parametrize("viewport", [ {"width": 320, "height": 568}, {"width": 390, "height": 844}, ]) def test_following_queue_is_phone_usable_at_narrow_viewport(viewport): with sync_playwright() as playwright: browser = playwright.chromium.launch(headless=True) page = browser.new_page(viewport=viewport) page.set_content((FRONTEND / "index.html").read_text()) page.add_style_tag(path=FRONTEND / "dashboard.css") page.add_script_tag(path=FRONTEND / "search-preview.js") page.add_script_tag(path=FRONTEND / "following.js") row = page.locator('[data-mobile-queue="following"]') expect(row).to_have_count(1) expect(row).to_contain_text("Issues and pull requests you watch") following_alert = page.locator('label[for="push-following"]') expect(following_alert).to_have_count(1) expect(following_alert).to_contain_text("Notify me when Following changes") page.locator("#work-settings-toggle").click() expect(following_alert).to_be_visible() assert following_alert.bounding_box()["height"] >= 44 page.evaluate("""() => { globalThis.fetch = async () => ({ ok:true, json:async () => ({revision:1,items:[{ repository:'stackchain/api', kind:'pull', number:42, title:'Changed pull request with a long mobile title', state:'open', updated_at:'2026-08-23T05:00:00Z', has_unseen_change:true, change_summary:'Closed since last review' }]}) }); globalThis.followingReleaseQueue = attachFollowing(() => {}); globalThis.followingReleaseQueue.open(); }""") expect(page.locator("#following-sheet")).to_be_visible() expect(page.locator(".following-card")).to_be_visible() expect(page.locator(".following-card")).to_contain_text("Closed since last review") expect(page.locator(".following-card")).to_contain_text("Pull request") expect(page.locator("#review-following")).to_have_text("Review new activity") assert page.locator("#review-following").bounding_box()["height"] >= 44 assert page.locator(".following-card").bounding_box()["height"] >= 44 assert page.locator("#close-following").bounding_box()["height"] >= 44 page.evaluate("""() => { document.querySelector('#following-sheet').close(); document.querySelector('#search-preview').classList.add('open'); renderSearchPreviewReview({status:'ready',data:{ci_state:'success',files:[{ filename:'src/a/very/long/mobile/path/that-must-wrap/review.py',status:'modified', additions:1,deletions:1,diff_available:true, diff_lines:['@@ -1 +1 @@','-unsafe long line that scrolls inside the diff only', '+safe long line that scrolls inside the diff only'] }]}},document,value=>String(value)); document.querySelector('[data-search-preview-section="changes"]').hidden=false; }""") changes = page.locator("#search-preview-review") expect(changes).to_be_visible() expect(changes).to_contain_text("CI passed ยท 1 changed file.") expect(changes).to_contain_text("review.py") assert page.locator('[data-search-preview-section="changes"]').bounding_box()["height"] >= 44 assert page.locator("#search-preview-review .pull-diff").evaluate( "node => node.scrollWidth >= node.clientWidth" ) is True assert page.evaluate( "document.documentElement.scrollWidth > document.documentElement.clientWidth" ) is False retire = page.locator("#watch-search-result") page.evaluate("""() => { document.querySelector('#following-sheet').close(); document.querySelector('#search-preview').classList.add('open'); const button = document.querySelector('#watch-search-result'); renderSearchPreviewWatch( {repository:'stackchain/api', number:42, kind:'issue', state:'closed', following:true, watching:true}, {status:'ready'}, button ); }""") expect(retire).to_be_visible() expect(retire).to_have_text("Stop watching & next") assert retire.bounding_box()["height"] >= 44 page.evaluate("""() => renderSearchPreviewWatch( {repository:'stackchain/api', number:42, kind:'pull', state:'open', watching:false}, {status:'ready'}, document.querySelector('#watch-search-result'))""") expect(retire).to_have_text("Watch pull request") assert retire.bounding_box()["height"] >= 44 keep = page.locator("#keep-following-for-later") page.evaluate("document.querySelector('#keep-following-for-later').hidden = false") expect(keep).to_be_visible() expect(keep).to_have_text("Keep for later & next") assert keep.bounding_box()["height"] >= 44 assert page.locator("#keep-following-status").evaluate( "node => getComputedStyle(node).overflowWrap" ) == "anywhere" assert page.locator("#keep-following-status").bounding_box()["height"] >= 20 overflow = page.evaluate("document.documentElement.scrollWidth > document.documentElement.clientWidth") assert overflow is False browser.close() def test_following_alert_route_opens_truthful_empty_state_with_one_refresh(): with sync_playwright() as playwright: browser = playwright.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 390, "height": 844}) page.set_content((FRONTEND / "index.html").read_text()) page.add_style_tag(path=FRONTEND / "dashboard.css") page.add_script_tag(path=FRONTEND / "following.js") result = page.evaluate("""async () => { let requests=0; globalThis.fetch=async () => { requests += 1; return {ok:true,json:async () => ({revision:2,items:[]})}; }; const queue=attachFollowing(() => {}); await queue.route(); return {requests,open:document.querySelector('#following-sheet').open, status:document.querySelector('#following-status').textContent}; }""") assert result == { "requests": 1, "open": True, "status": "No watched items yet. Watch an issue or pull request from Search to keep it here.", } expect(page.locator("#following-sheet")).to_be_visible() assert page.locator("#close-following").bounding_box()["height"] >= 44 browser.close()