feat: review completed mobile Filed outcomes (Closes #878)
This commit is contained in:
1 parent
eb88355229
commit
8411e907af
9 files changed
+253
-17
No files matched your search
@@ -67,15 +67,16 @@ async def test_work_page_preserves_total_and_reason_without_loading_other_pages(
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_filed_work_page_loads_open_authored_issues_with_a_distinct_reason():
|
||||
async def test_filed_work_page_loads_open_and_closed_authored_issues_with_a_distinct_reason():
|
||||
requests = []
|
||||
|
||||
def upstream(request):
|
||||
requests.append(str(request.url))
|
||||
return httpx.Response(200, headers={"X-Total-Count": "61"}, json=[{
|
||||
"id": 870, "number": 870, "title": "Delegated filing", "state": "open",
|
||||
"id": 878, "number": 878, "title": "Completed delegation", "state": "closed",
|
||||
"updated_at": "2026-08-15T12:00:00Z",
|
||||
"repository": {"full_name": "stackchain/dashboard"},
|
||||
"html_url": "https://forge.example/stackchain/dashboard/issues/870",
|
||||
"html_url": "https://forge.example/stackchain/dashboard/issues/878",
|
||||
}])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
|
||||
@@ -85,14 +86,15 @@ async def test_filed_work_page_loads_open_authored_issues_with_a_distinct_reason
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert requests == [
|
||||
"http://127.0.0.1:3000/api/v1/repos/issues/search?state=open&created=true&type=issues&limit=50&page=2"
|
||||
"http://127.0.0.1:3000/api/v1/repos/issues/search?state=all&created=true&type=issues&limit=50&page=2"
|
||||
]
|
||||
assert result == {
|
||||
"stream": "filed", "page": 2, "total": 61, "has_more": False,
|
||||
"items": [{
|
||||
"id": 870, "number": 870, "title": "Delegated filing", "state": "open",
|
||||
"id": 878, "number": 878, "title": "Completed delegation", "state": "closed",
|
||||
"updated_at": "2026-08-15T12:00:00Z",
|
||||
"repository": {"full_name": "stackchain/dashboard"},
|
||||
"html_url": "https://forge.example/stackchain/dashboard/issues/870",
|
||||
"html_url": "https://forge.example/stackchain/dashboard/issues/878",
|
||||
"work_reasons": ["created_by_me"],
|
||||
}],
|
||||
}
|
||||
|
||||
+94
-1
@@ -72,6 +72,98 @@ process.stdout.write(JSON.stringify({{available, results}}));
|
||||
}
|
||||
|
||||
|
||||
def test_completed_filed_outcomes_are_distinct_and_rank_before_open_filings():
|
||||
payload = {
|
||||
"user": {"login": "timmy"},
|
||||
"issues": [
|
||||
{"number": 10, "title": "Still delegated", "repository": "stackchain/api",
|
||||
"state": "open", "assignees": ["alex"], "work_reasons": ["created_by_me"],
|
||||
"updated_at": "2026-08-15T12:00:00Z"},
|
||||
{"number": 9, "title": "Outcome ready", "repository": "stackchain/api",
|
||||
"state": "closed", "assignees": ["timmy"], "work_reasons": ["created_by_me"],
|
||||
"updated_at": "2026-08-14T12:00:00Z"},
|
||||
],
|
||||
}
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||
process.stdout.write(JSON.stringify(buildMyWork({json.dumps(payload)})));
|
||||
"""
|
||||
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
result = json.loads(completed.stdout)
|
||||
assert [item["number"] for item in result] == [9, 10]
|
||||
assert result[0]["is_completed"] is True
|
||||
assert result[0]["reason"] == "Completed · review outcome"
|
||||
assert result[1]["is_completed"] is False
|
||||
assert result[1]["reason"] == "Filed by you"
|
||||
|
||||
|
||||
def test_completed_filed_acknowledgement_survives_refresh_until_upstream_update():
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.has(key) ? values.get(key) : null,
|
||||
setItem:(key, value) => values.set(key, value),
|
||||
}};
|
||||
const create = buildMyWork.createCompletedFiledReview;
|
||||
if (typeof create !== 'function') {{
|
||||
process.stdout.write(JSON.stringify({{available:false}}));
|
||||
}} else {{
|
||||
const items = [
|
||||
{{kind:'issue', key:'stackchain/api#9', repository:'stackchain/api', number:9,
|
||||
is_filed:true, is_completed:true, updated_at:'2026-08-14T12:00:00Z'}},
|
||||
{{kind:'issue', key:'stackchain/api#10', repository:'stackchain/api', number:10,
|
||||
is_filed:true, is_completed:false, updated_at:'2026-08-15T12:00:00Z'}},
|
||||
];
|
||||
const first = create({{storage, getLogin:() => 'timmy'}});
|
||||
const before = first.visible(items).map(item => item.number);
|
||||
const acknowledged = first.acknowledge(items[0]);
|
||||
const after = first.visible(items).map(item => item.number);
|
||||
const reloaded = create({{storage, getLogin:() => 'timmy'}});
|
||||
const afterRefresh = reloaded.visible(items).map(item => item.number);
|
||||
const updated = reloaded.visible([{{...items[0], updated_at:'2026-08-16T12:00:00Z'}}, items[1]])
|
||||
.map(item => item.number);
|
||||
process.stdout.write(JSON.stringify({{available:true, before, acknowledged, after, afterRefresh, updated}}));
|
||||
}}
|
||||
"""
|
||||
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
assert json.loads(completed.stdout) == {
|
||||
"available": True,
|
||||
"before": [9, 10],
|
||||
"acknowledged": True,
|
||||
"after": [10],
|
||||
"afterRefresh": [10],
|
||||
"updated": [9, 10],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_completed_filed_sheet_exposes_mobile_acknowledge_and_next_flow():
|
||||
markup = (Path(__file__).parents[1] / "frontend" / "index.html").read_text()
|
||||
css = (Path(__file__).parents[1] / "frontend" / "dashboard.css").read_text()
|
||||
source = await dashboard()
|
||||
|
||||
assert 'id="completed-filed-actions"' in markup
|
||||
assert 'id="acknowledge-completed-filed"' in markup
|
||||
assert 'Acknowledge & next' in markup
|
||||
assert "createCompletedFiledReview({" in source
|
||||
assert "completedFiledReview.visible(buildMyWork(data))" in source
|
||||
assert "filed: 'filed issues'" in source
|
||||
handler = source.split("qs('#acknowledge-completed-filed').addEventListener('click'", 1)[1]
|
||||
assert "completedFiledReview.acknowledge(selectedIssue)" in handler
|
||||
assert "filedFollowUpTarget(lastMyWork)" in handler
|
||||
assert "openRoutedWork" in handler
|
||||
assert ".completed-filed-actions" in css
|
||||
action_rule = css.split(".completed-filed-actions", 1)[1].split("}", 1)[0]
|
||||
assert "position:fixed" in action_rule
|
||||
assert "env(safe-area-inset-bottom)" in action_rule
|
||||
assert "min-height:44px" in css.split(".completed-filed-actions", 1)[1]
|
||||
|
||||
|
||||
def test_updates_rank_actionable_work_before_newer_routine_activity():
|
||||
payload = {
|
||||
"user": {"login": "timmy"},
|
||||
@@ -1612,6 +1704,7 @@ const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||
const controller = createIssueSheet({{fetchJson:async () => ({{}}), storage:null}});
|
||||
process.stdout.write(JSON.stringify([
|
||||
controller.readOnly({{kind:'filed',is_filed:true,is_assigned:false}}),
|
||||
controller.readOnly({{kind:'filed',is_filed:true,is_assigned:true,is_completed:true}}),
|
||||
controller.readOnly({{kind:'issue',is_filed:true,is_assigned:true}}),
|
||||
controller.readOnly({{kind:'issue',is_assigned:true}}),
|
||||
]));
|
||||
@@ -1619,7 +1712,7 @@ process.stdout.write(JSON.stringify([
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
assert json.loads(result.stdout) == [True, False, False]
|
||||
assert json.loads(result.stdout) == [True, True, False, False]
|
||||
|
||||
|
||||
def test_issue_sheet_loads_filed_items_through_read_only_access():
|
||||
|
||||
@@ -147,6 +147,61 @@ async def test_resolve_work_route_returns_an_open_issue_filed_by_the_current_use
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_resolve_work_route_returns_a_completed_issue_filed_by_the_current_user():
|
||||
async def upstream(request):
|
||||
if request.url.path.endswith("/user"):
|
||||
return httpx.Response(200, json={"login": "timmy"})
|
||||
return httpx.Response(200, json={
|
||||
"id": 702,
|
||||
"number": 89,
|
||||
"title": "Completed delegation",
|
||||
"state": "closed",
|
||||
"html_url": gitea_proxy.GITEA_URL + "/stackchain/dashboard/issues/89",
|
||||
"assignees": [{"login": "alex"}],
|
||||
"user": {"login": "timmy"},
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
|
||||
try:
|
||||
result = await gitea_proxy.resolve_work_route(
|
||||
"filed", "stackchain/dashboard", 89, None
|
||||
)
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert result == {
|
||||
"kind": "filed",
|
||||
"repository": "stackchain/dashboard",
|
||||
"number": 89,
|
||||
"title": "Completed delegation",
|
||||
"state": "closed",
|
||||
"url": gitea_proxy.GITEA_URL + "/stackchain/dashboard/issues/89",
|
||||
"is_filed": True,
|
||||
"is_assigned": False,
|
||||
"work_reasons": ["created_by_me"],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_completed_filed_issue_remains_authorized_for_detail_and_reply():
|
||||
async def upstream(request):
|
||||
if request.url.path.endswith("/user"):
|
||||
return httpx.Response(200, json={"login": "timmy"})
|
||||
return httpx.Response(200, json={
|
||||
"state": "closed",
|
||||
"user": {"login": "timmy"},
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
|
||||
try:
|
||||
authorized = await gitea_proxy.is_authored_issue("stackchain/dashboard", 89)
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert authorized is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_resolve_work_route_rejects_a_notification_that_is_already_read():
|
||||
async def upstream(request):
|
||||
|
||||
Reference in new issue
Block a user