feat: watch issues from mobile search (Closes #1290)
This commit is contained in:
1 parent
7fbbc2f942
commit
9c187b5ae5
8 files changed
+297
-3
No files matched your search
@@ -32,8 +32,14 @@ def test_rendered_mobile_search_preview_navigation_preserves_reading_space_and_d
|
||||
page.locator("#search-preview-conversation").evaluate("node => node.style.minHeight = '500px'")
|
||||
page.locator("#search-preview-reply-workspace").evaluate("node => node.style.minHeight = '500px'")
|
||||
page.locator("#search-preview-actions").evaluate("node => node.style.minHeight = '300px'")
|
||||
page.locator("#watch-search-result").evaluate("node => { node.hidden = false; node.textContent = 'Watch issue'; }")
|
||||
page.add_script_tag(path=FRONTEND / "mobile-search-preview-nav.js")
|
||||
|
||||
watch = page.locator("#watch-search-result")
|
||||
expect(watch).to_be_visible()
|
||||
expect(watch).to_have_text("Watch issue")
|
||||
watch_bounds = watch.bounding_box()
|
||||
assert watch_bounds and watch_bounds["height"] >= 44
|
||||
navigation = page.locator(".mobile-search-preview-nav")
|
||||
expect(navigation).to_be_visible()
|
||||
buttons = navigation.locator("button")
|
||||
|
||||
@@ -806,6 +806,49 @@ if (!states.some(state => state.status === 'reopened')) throw new Error('reopen
|
||||
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
||||
|
||||
|
||||
def test_search_preview_watch_is_single_flight_and_keeps_visible_context_on_failure():
|
||||
script = f"""
|
||||
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
|
||||
(async () => {{
|
||||
let calls = 0; let rejectWatch; const states = [];
|
||||
const detail = {{repository:'stackchain/api',number:42,kind:'issue',claimable:true,watching:false}};
|
||||
const preview = createSearchPreview({{
|
||||
fetchJson:async()=>detail, mutate:async()=>{{}},
|
||||
watch:()=>{{calls+=1;return new Promise((_resolve,reject)=>rejectWatch=reject);}},
|
||||
onState:state=>states.push(state),
|
||||
}});
|
||||
await preview.open(detail);
|
||||
const first=preview.setWatching(true); const second=preview.setWatching(true);
|
||||
if (calls !== 1 || first !== second) throw new Error('watch mutation was not single-flight');
|
||||
rejectWatch(new Error('offline')); await Promise.allSettled([first,second]);
|
||||
const final=states.at(-1);
|
||||
process.stdout.write(JSON.stringify({{calls,status:final.status,number:final.detail.number,
|
||||
watching:final.detail.watching,error:final.error.message}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert json.loads(result.stdout) == {
|
||||
"calls": 1,
|
||||
"status": "watch-error",
|
||||
"number": 42,
|
||||
"watching": False,
|
||||
"error": "offline",
|
||||
}
|
||||
|
||||
|
||||
def test_mobile_search_preview_exposes_touch_safe_watch_action():
|
||||
html = dashboard_bundle_text()
|
||||
css = (FRONTEND / "dashboard.css").read_text()
|
||||
dashboard = (FRONTEND / "dashboard.js").read_text()
|
||||
|
||||
assert '<button id="watch-search-result" type="button" hidden>Watch issue</button>' in html
|
||||
assert "wireSearchPreviewWatch" in dashboard
|
||||
assert "preview.setWatching" in SEARCH_PREVIEW.read_text()
|
||||
assert ".search-preview-actions button" in css and "min-height:44px" in css
|
||||
|
||||
|
||||
def test_search_preview_shares_canonical_url_without_closing_the_preview():
|
||||
script = f"""
|
||||
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
|
||||
|
||||
@@ -218,6 +218,96 @@ async def test_global_search_preview_returns_normalized_action_context(monkeypat
|
||||
assert response.json()["claimable"] is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_search_preview_subscription_reads_server_truth_without_caching(monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def preview(repository, kind, number):
|
||||
calls.append(("preview", repository, kind, number))
|
||||
return {"repository": repository, "kind": kind, "number": number,
|
||||
"state": "open", "claimable": True}
|
||||
|
||||
async def subscription(repository, number):
|
||||
calls.append(("subscription", repository, number))
|
||||
return {"watching": True}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
|
||||
monkeypatch.setattr(main.gitea_proxy, "issue_subscription", subscription, raising=False)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.get(
|
||||
"/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.json() == {"watching": True}
|
||||
assert calls == [
|
||||
("preview", "stackchain/api", "issue", 42),
|
||||
("subscription", "stackchain/api", 42),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(("method", "watching"), [("PUT", True), ("DELETE", False)])
|
||||
async def test_search_preview_subscription_mutation_revalidates_target_and_confirms_truth(
|
||||
monkeypatch, method, watching
|
||||
):
|
||||
calls = []
|
||||
|
||||
async def preview(repository, kind, number):
|
||||
calls.append(("preview", repository, kind, number))
|
||||
return {"repository": repository, "kind": kind, "number": number,
|
||||
"state": "open", "claimable": True}
|
||||
|
||||
async def set_subscription(repository, number, desired):
|
||||
calls.append(("set", repository, number, desired))
|
||||
return {"watching": desired}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
|
||||
monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription, raising=False)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.request(
|
||||
method,
|
||||
"/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.json() == {"watching": watching}
|
||||
assert calls == [
|
||||
("preview", "stackchain/api", "issue", 42),
|
||||
("set", "stackchain/api", 42, watching),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_issue_subscription_mutation_uses_current_login_and_requires_confirmation():
|
||||
requests = []
|
||||
|
||||
async def handler(request):
|
||||
requests.append((request.method, request.url.path))
|
||||
if request.url.path.endswith("/user"):
|
||||
return httpx.Response(200, json={"login": "timmy"})
|
||||
if request.url.path.endswith("/subscriptions/check"):
|
||||
return httpx.Response(200, json={"subscribed": True, "ignored": False})
|
||||
return httpx.Response(201, json={})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
result = await gitea_proxy.set_issue_subscription("stackchain/api", 42, True)
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert result == {"watching": True}
|
||||
assert requests == [
|
||||
("GET", "/api/v1/user"),
|
||||
("PUT", "/api/v1/repos/stackchain/api/issues/42/subscriptions/timmy"),
|
||||
("GET", "/api/v1/repos/stackchain/api/issues/42/subscriptions/check"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("kind", ["issue", "pull"])
|
||||
async def test_global_search_preview_conversation_returns_a_bounded_authorized_page(
|
||||
|
||||
Reference in new issue
Block a user