feat: publish draft pull requests from mobile (Closes #1344)
CI / lint (pull_request) Successful in 3m22s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 5m33s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-24 10:06:51 +00:00
1 parent 1a2297e943
commit a4ae3e1c7f
6 files changed
+314 -2

No files matched your search

@@ -0,0 +1,87 @@
from pathlib import Path
import pytest
ROOT = Path(__file__).parents[2]
@pytest.mark.parametrize("viewport", [(320, 568), (390, 844)])
def test_mobile_author_publishes_draft_and_continues_to_reviewer_picker(viewport):
playwright = pytest.importorskip("playwright.sync_api")
html = (ROOT / "frontend" / "index.html").read_text()
detail = {
"number": 7,
"title": "WIP: Ship mobile flow",
"author": "timmy",
"state": "open",
"draft": True,
"merged": False,
"head_sha": "abc1234",
}
with playwright.sync_playwright() as runtime:
try:
browser = runtime.chromium.launch(headless=True)
except Exception as error:
pytest.skip(f"Chromium is not installed: {error}")
page = browser.new_page(viewport={"width": viewport[0], "height": viewport[1]})
page.on("dialog", lambda dialog: dialog.accept())
page.set_content(html, wait_until="domcontentloaded")
page.add_style_tag(path=ROOT / "frontend" / "dashboard.css")
page.add_script_tag(path=ROOT / "frontend" / "pull-sheet.js")
page.evaluate(
"""detail => {
window.publishDetail = detail;
window.publishItem = {repository:'stackchain/api', number:7, key:'stackchain/api#7'};
const controller = createPullSheet({
storage:null,
fetchJson:(path, options) => {
if (!path.endsWith('/ready') || options.method !== 'PATCH') {
return Promise.reject(new Error('unexpected request'));
}
const payload = JSON.parse(options.body);
if (payload.expected_head_sha !== detail.head_sha) {
return Promise.reject(new Error('wrong head'));
}
return Promise.resolve({
repository:'stackchain/api', number:7, title:'Ship mobile flow',
head_sha:detail.head_sha, state:'open', draft:false,
});
},
});
createPullSheet.bindOwnershipControls(
document, controller, () => window.publishItem, async () => false,
() => window.publishDetail, () => 'timmy'
);
document.querySelector('#pull-sheet').classList.add('open');
controller.setReviewDetail(detail);
}""",
detail,
)
ready = page.get_by_role("button", name="Ready for review")
before = ready.evaluate("button => ({height:button.getBoundingClientRect().height, hidden:button.hidden})")
ready.click()
page.get_by_text("Published and ready for review.").wait_for()
metrics = page.evaluate(
"""() => ({
scrollWidth:document.documentElement.scrollWidth,
clientWidth:document.documentElement.clientWidth,
readyHidden:document.querySelector('#publish-draft-pull').hidden,
requestOpen:document.querySelector('#pull-review-request').open,
reviewerFocused:document.activeElement === document.querySelector('#load-pull-reviewers'),
title:window.publishDetail.title,
draft:window.publishDetail.draft,
})"""
)
browser.close()
assert before["hidden"] is False
assert before["height"] >= 44
assert metrics["scrollWidth"] <= metrics["clientWidth"]
assert metrics["readyHidden"] is True
assert metrics["requestOpen"] is True
assert metrics["reviewerFocused"] is True
assert metrics["title"] == "Ship mobile flow"
assert metrics["draft"] is False
+80
View File
@@ -8,6 +8,86 @@ from src import gitea_proxy, main
from src.security_event_store import SecurityEventStoreError
@pytest.mark.anyio
async def test_pull_author_can_publish_assigned_draft_at_expected_head(monkeypatch):
calls = []
async def publish(repository, number, expected_head_sha):
calls.append((repository, number, expected_head_sha))
return {
"repository": repository,
"number": number,
"title": "Ship mobile flow",
"head_sha": expected_head_sha,
"state": "open",
"draft": False,
}
monkeypatch.setattr(
main.gitea_proxy, "publish_authored_assigned_pull", publish, raising=False
)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.patch(
"/api/v1/repos/stackchain/api/pulls/7/ready",
json={"expected_head_sha": "abc1234"},
)
assert response.status_code == 200
assert response.json()["draft"] is False
assert calls == [("stackchain/api", 7, "abc1234")]
@pytest.mark.anyio
async def test_gitea_publishes_only_authored_assigned_draft_and_verifies_ready_head():
requests = []
pull = {
"number": 7,
"title": "WIP: Ship mobile flow",
"state": "open",
"draft": True,
"merged": False,
"user": {"login": "alex"},
"assignees": [{"login": "alex"}],
"head": {"sha": "abc1234"},
}
async def handler(request):
requests.append((request.method, request.url.path, request.content))
if request.url.path == "/api/v1/user":
return httpx.Response(200, json={"login": "alex"})
if request.url.path == "/api/v1/repos/stackchain/api/pulls/7" and request.method == "GET":
return httpx.Response(200, json=pull)
if request.url.path == "/api/v1/repos/stackchain/api/pulls/7" and request.method == "PATCH":
assert json.loads(request.content) == {"title": "Ship mobile flow"}
pull.update({"title": "Ship mobile flow", "draft": False})
return httpx.Response(201, json=pull)
raise AssertionError(f"unexpected request: {request.method} {request.url.path}")
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
try:
result = await gitea_proxy.publish_authored_assigned_pull(
"stackchain/api", 7, "abc1234"
)
finally:
await gitea_proxy.stop_client()
assert result == {
"repository": "stackchain/api",
"number": 7,
"title": "Ship mobile flow",
"head_sha": "abc1234",
"state": "open",
"draft": False,
}
assert [item[:2] for item in requests] == [
("GET", "/api/v1/user"),
("GET", "/api/v1/repos/stackchain/api/pulls/7"),
("PATCH", "/api/v1/repos/stackchain/api/pulls/7"),
("GET", "/api/v1/repos/stackchain/api/pulls/7"),
]
@pytest.mark.anyio
async def test_pull_author_can_update_assigned_open_pull_context(monkeypatch):
calls = []