feat: pin frequent mobile work (Closes #1477)
CI / lint (pull_request) Successful in 4m1s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 7m46s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-27 18:34:01 +00:00
1 parent 2b630d3fa8
commit 94bd2cafba
11 files changed
+527 -75

No files matched your search

@@ -0,0 +1,72 @@
import os
from pathlib import Path
import pytest
if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1":
pytest.skip("packaged pinned-work 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_operator_pins_and_reopens_frequent_work_without_phone_overflow(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 / "mobile-recent-work.js")
page.evaluate("""() => {
const values = new Map();
const storage = {
getItem:key => values.get(key) || null,
setItem:(key, value) => values.set(key, value),
};
const item = {
kind:'issue', repository:'stackchain/stackchain-dashboard', number:1477,
title:'Pin frequent work across signed-in mobile devices',
};
window.opened = [];
window.recentWork = createMobileRecentWork({
storage, getLogin:() => 'timmy', document,
section:document.querySelector('#mobile-recent-work'),
list:document.querySelector('#mobile-recent-work-list'),
pinnedSection:document.querySelector('#mobile-pinned-work'),
pinnedList:document.querySelector('#mobile-pinned-work-list'),
status:document.querySelector('#mobile-recent-work-status'),
openRoute:route => window.opened.push(route),
});
window.recentWork.record(item);
window.recentWork.pin(item);
document.querySelector('#mobile-queue-sheet').showModal();
}""")
expect(page.locator("#mobile-pinned-work")).to_be_visible()
expect(page.locator("#mobile-recent-work")).to_be_visible()
expect(page.locator("#mobile-recent-work-status")).to_have_text("Sync pending.")
open_button = page.get_by_role(
"button", name="Open Pin frequent work across signed-in mobile devices, issue stackchain/stackchain-dashboard #1477"
).first
pin_button = page.get_by_role(
"button", name="Unpin Pin frequent work across signed-in mobile devices"
)
for control in (open_button, pin_button):
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
assert bounds["x"] >= 0 and bounds["x"] + bounds["width"] <= viewport["width"]
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
open_button.focus()
open_button.press("Enter")
assert page.evaluate("window.opened") == [
"#/my-work/issue/stackchain/stackchain-dashboard/1477"
]
browser.close()
+80 -4
View File
@@ -93,13 +93,14 @@ const recent=createRecentWork({{
openRoute:route=>opened.push(route),
}});
const rendered=recent.render();
list.children[1].click();
list.children[1].children[0].click();
process.stdout.write(JSON.stringify({{
rendered,hidden:section.hidden,rows:list.children.map(button=>({{
rendered,hidden:section.hidden,rows:list.children.map(row=>{{const button=row.children[0];return ({{
label:button.attributes['aria-label'],route:button.attributes['data-recent-work-route'],
primary:button.children[0].children[0].textContent,
secondary:button.children[0].children[1].textContent,
}})),opened,
pin:row.children[1].attributes['data-recent-work-pin'],
}});}}),opened,
}}));
"""
payload = run_node(script)
@@ -113,12 +114,14 @@ process.stdout.write(JSON.stringify({{
"route": "#/my-work/issue/stackchain/dashboard/7",
"primary": "Fix mobile queue",
"secondary": "Issue · stackchain/dashboard #7",
"pin": "pin",
},
{
"label": "Open Review release status, update #42",
"route": "#/my-work/update/42",
"primary": "Review release status",
"secondary": "Update · #42",
"pin": "pin",
},
],
"opened": ["#/my-work/update/42"],
@@ -156,6 +159,73 @@ process.stdout.write(JSON.stringify({{immediate,settled:recent.items(),status:st
assert payload["calls"] == [["api/v1/recent-work", "POST"]]
def test_recent_work_pins_offline_first_syncs_and_renders_separate_touch_actions():
script = f"""
const createRecentWork = require({json.dumps(str(RECENT_WORK))});
(async()=>{{
const item={{kind:'issue',repository:'stackchain/dashboard',number:1477,title:'Pin frequent work',route:'#/my-work/issue/stackchain/dashboard/1477'}};
const storageKey='stackchain.mobile-recent-work.v1.alice';
const values=new Map([[storageKey,JSON.stringify({{items:[item],pinned:[],pending:[],pinOps:[]}})]]);
const status={{textContent:''}}; const calls=[]; const opened=[];
function node(tag) {{
return {{tag,children:[],attributes:{{}},listeners:{{}},hidden:false,textContent:'',
appendChild(child){{this.children.push(child);return child;}},
replaceChildren(...children){{this.children=children;}},
setAttribute(name,value){{this.attributes[name]=String(value);}},
addEventListener(name,callback){{this.listeners[name]=callback;}},
click(){{this.listeners.click?.();}}, focus(){{this.focused=true;}},
}};
}}
const recentList=node('div'); const recentSection=node('section');
const pinnedList=node('div'); const pinnedSection=node('section');
let remote={{items:[item],pinned:[]}};
const recent=createRecentWork({{
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}},
getLogin:()=>'alice',status,debounceMs:99999,document:{{createElement:node}},
list:recentList,section:recentSection,pinnedList,pinnedSection,
openRoute:route=>opened.push(route),
fetchJson:async (url,options={{}})=>{{
calls.push([url,options.method||'GET',JSON.parse(options.body||'null')]);
if (options.method==='PUT') remote={{items:[item],pinned:[item]}};
if (options.method==='DELETE') remote={{items:[item],pinned:[]}};
return remote;
}},
}});
const pinnedImmediately=recent.pin(item);
recent.render();
const immediate={{pinned:recent.pinned(),status:status.textContent,recentHidden:recentSection.hidden,pinnedHidden:pinnedSection.hidden,
recentActions:recentList.children[0].children.map(child=>child.attributes),
pinnedActions:pinnedList.children[0].children.map(child=>child.attributes)}};
await recent.sync();
pinnedList.children[0].children[0].click();
const unpinnedImmediately=recent.unpin(item.route);
await recent.sync();
process.stdout.write(JSON.stringify({{pinnedImmediately,immediate,opened,unpinnedImmediately,settled:recent.pinned(),calls}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
payload = run_node(script)
assert payload["pinnedImmediately"] is True
assert payload["immediate"]["pinned"][0]["number"] == 1477
assert payload["immediate"]["status"] == "Sync pending."
assert payload["immediate"]["recentHidden"] is False
assert payload["immediate"]["pinnedHidden"] is False
assert payload["immediate"]["recentActions"][0]["data-recent-work-route"].endswith("/1477")
assert payload["immediate"]["recentActions"][1]["data-recent-work-pin"] == "pin"
assert payload["immediate"]["pinnedActions"][1]["data-recent-work-pin"] == "unpin"
assert payload["opened"] == ["#/my-work/issue/stackchain/dashboard/1477"]
assert payload["unpinnedImmediately"] is True
assert payload["settled"] == []
assert payload["calls"] == [
["api/v1/recent-work/pin", "PUT", payload["immediate"]["pinned"][0]],
[
"api/v1/recent-work/pin",
"DELETE",
{"route": "#/my-work/issue/stackchain/dashboard/1477"},
],
]
def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes():
html = INDEX.read_text()
dashboard = DASHBOARD.read_text()
@@ -163,15 +233,21 @@ def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes():
assert 'id="mobile-recent-work"' in html
assert 'id="mobile-recent-work-list"' in html
assert 'id="mobile-pinned-work"' in html
assert 'id="mobile-pinned-work-list"' in html
assert 'id="mobile-recent-work-status" role="status" aria-live="polite"' in html
assert '<script src="static/mobile-recent-work.js"></script>' in html
assert "createMobileRecentWork({" in dashboard
assert "fetchJson:fetchReviewJson" in dashboard
assert "status:qs('#mobile-recent-work-status')" in dashboard
assert "pinnedList:qs('#mobile-pinned-work-list')" in dashboard
assert "pinnedSection:qs('#mobile-pinned-work')" in dashboard
assert "mobileRecentWork.startLifecycle({window, document})" in dashboard
assert "void mobileRecentWork.load();" in dashboard
assert "mobileRecentWork.record(item)" in dashboard
assert "mobileRecentWork.render()" in dashboard
assert "workRoute.sync()" in dashboard
assert "[data-recent-work-route]" in css
assert "min-height:56px" in css
assert "[data-recent-work-pin]" in css
assert "min-height:44px" in css
assert "min-width:0" in css
+55 -2
View File
@@ -47,7 +47,60 @@ async def test_recent_work_api_is_authenticated_csrf_protected_no_store_and_acco
assert forbidden.status_code == 403
assert saved.status_code == 200
assert saved.json() == {"items": [entry]}
assert saved.json() == {"items": [entry], "pinned": []}
assert fetched.json() == saved.json()
assert fetched.headers["cache-control"] == "no-store"
assert other_account.json() == {"items": []}
assert other_account.json() == {"items": [], "pinned": []}
@pytest.mark.anyio
async def test_recent_work_pin_api_is_csrf_protected_and_account_scoped(monkeypatch, tmp_path):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
monkeypatch.setenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", "correct horse battery staple")
monkeypatch.setenv(
"STACKCHAIN_DASHBOARD_SESSION_SECRET",
"a-separate-session-signing-secret-with-enough-entropy",
)
monkeypatch.setenv("STACKCHAIN_SESSION_DB", str(tmp_path / "sessions.sqlite3"))
monkeypatch.setenv("STACKCHAIN_LOGIN_ATTEMPT_DB", str(tmp_path / "login.sqlite3"))
monkeypatch.setenv("STACKCHAIN_RECENT_WORK_DB", str(tmp_path / "recent-work.sqlite3"))
active_login = "Timmy"
async def user():
return {"id": 1, "login": active_login}
monkeypatch.setattr(main, "current_user", user)
entry = {
"kind": "pull",
"repository": "stackchain/dashboard",
"number": 1476,
"title": "Sync recent work",
"route": "#/my-work/pull/stackchain/dashboard/1476",
}
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
await client.post(
"/api/v1/session", json={"access_token": "correct horse battery staple"}
)
forbidden = await client.put("/api/v1/recent-work/pin", json=entry)
headers = {
"Origin": "https://test",
"X-CSRF-Token": client.cookies["stackchain_csrf"],
}
pinned = await client.put("/api/v1/recent-work/pin", json=entry, headers=headers)
active_login = "Alexander"
isolated = await client.get("/api/v1/recent-work")
active_login = "Timmy"
unpinned = await client.request(
"DELETE",
"/api/v1/recent-work/pin",
json={"route": entry["route"]},
headers=headers,
)
assert forbidden.status_code == 403
assert pinned.status_code == 200
assert pinned.json() == {"items": [], "pinned": [entry]}
assert isolated.json() == {"items": [], "pinned": []}
assert unpinned.status_code == 200
assert unpinned.json() == {"items": [], "pinned": []}
+28 -1
View File
@@ -23,7 +23,7 @@ def test_recent_work_is_encrypted_account_scoped_deduplicated_and_bounded(tmp_pa
assert [entry["number"] for entry in expected["items"]] == [3, 6, 5, 4, 2]
assert RecentWorkStore(database, encryption_key=b"r" * 32).get("timmy") == expected
assert store.get("alexander") == {"items": []}
assert store.get("alexander") == {"items": [], "pinned": []}
with sqlite3.connect(database) as connection:
payload = connection.execute(
"SELECT items FROM recent_work WHERE login = 'timmy'"
@@ -33,6 +33,33 @@ def test_recent_work_is_encrypted_account_scoped_deduplicated_and_bounded(tmp_pa
assert "#/my-work/issue" not in payload
def test_pinned_work_survives_recent_limit_and_unpin_keeps_recent_item(tmp_path):
database = tmp_path / "recent-work.sqlite3"
store = RecentWorkStore(database, encryption_key=b"r" * 32, limit=5)
store.record("timmy", item(1))
pinned = store.pin("timmy", item(1))
for number in range(2, 8):
store.record("timmy", item(number))
assert pinned["pinned"] == [item(1)]
assert [entry["number"] for entry in store.get("timmy")["items"]] == [7, 6, 5, 4, 3]
assert store.get("timmy")["pinned"] == [item(1)]
assert store.get("alexander") == {"items": [], "pinned": []}
store.record("timmy", item(1, title="Issue 1 current"))
unpinned = store.unpin("timmy", item(1)["route"])
assert unpinned["pinned"] == []
assert unpinned["items"][0] == item(1, title="Issue 1 current")
with sqlite3.connect(database) as connection:
payload = connection.execute(
"SELECT items FROM recent_work WHERE login = 'timmy'"
).fetchone()[0]
assert "Issue 1" not in payload
assert "#/my-work/issue" not in payload
def test_recent_work_rejects_noncanonical_or_unsupported_items(tmp_path):
store = RecentWorkStore(tmp_path / "recent-work.sqlite3", encryption_key=b"r" * 32)