feat: route mobile My Work sheets (#205)
This commit is contained in:
1 parent
5e826bcc7e
commit
9e697dc63f
3 files changed
+361
-34
No files matched your search
+129
-1
@@ -13,6 +13,134 @@ ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "issue-sheet.js"
|
||||
CREATE_ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "create-issue-sheet.js"
|
||||
PULL_SHEET = Path(__file__).parents[1] / "frontend" / "pull-sheet.js"
|
||||
PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js"
|
||||
WORK_ROUTE = Path(__file__).parents[1] / "frontend" / "work-route.js"
|
||||
|
||||
|
||||
def test_work_routes_round_trip_all_sheet_kinds_and_reject_unsafe_fragments():
|
||||
script = f"""
|
||||
const routes = require({json.dumps(str(WORK_ROUTE))});
|
||||
const inputs = [
|
||||
{{kind:'issue',repository:'stackchain/api',number:17}},
|
||||
{{kind:'pull',repository:'stackchain/dashboard',number:42}},
|
||||
{{kind:'review',repository:'stackchain/dashboard',number:42}},
|
||||
{{kind:'update',notification_id:913}},
|
||||
];
|
||||
process.stdout.write(JSON.stringify({{
|
||||
paths: inputs.map(routes.serialize),
|
||||
parsed: inputs.map(item => routes.parse(routes.serialize(item))),
|
||||
invalid: [
|
||||
'#/my-work/issue/../../etc/1',
|
||||
'#/my-work/issue/stackchain/api/not-a-number',
|
||||
'#/my-work/update/-1',
|
||||
'#/other/issue/stackchain/api/1',
|
||||
].map(routes.parse),
|
||||
}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {
|
||||
"paths": [
|
||||
"#/my-work/issue/stackchain/api/17",
|
||||
"#/my-work/pull/stackchain/dashboard/42",
|
||||
"#/my-work/review/stackchain/dashboard/42",
|
||||
"#/my-work/update/913",
|
||||
],
|
||||
"parsed": [
|
||||
{"kind": "issue", "repository": "stackchain/api", "number": 17},
|
||||
{"kind": "pull", "repository": "stackchain/dashboard", "number": 42},
|
||||
{"kind": "review", "repository": "stackchain/dashboard", "number": 42},
|
||||
{"kind": "update", "notification_id": 913},
|
||||
],
|
||||
"invalid": [None, None, None, None],
|
||||
}
|
||||
|
||||
|
||||
def test_work_route_controller_restores_direct_links_and_uses_history_for_close():
|
||||
script = f"""
|
||||
const routes = require({json.dumps(str(WORK_ROUTE))});
|
||||
const listeners = {{}};
|
||||
const location = {{hash:'#/my-work/issue/stackchain/api/17', href:'https://forge.example/dashboard/#/my-work/issue/stackchain/api/17'}};
|
||||
const calls = [];
|
||||
const history = {{
|
||||
state: null,
|
||||
pushState(state, _, hash) {{ this.state = state; location.hash = hash; calls.push(['push', hash]); }},
|
||||
replaceState(state, _, hash) {{ this.state = state; location.hash = hash; calls.push(['replace', hash]); }},
|
||||
back() {{ calls.push(['back']); location.hash = ''; listeners.popstate(); }},
|
||||
}};
|
||||
const controller = routes.createController({{
|
||||
location, history,
|
||||
eventTarget: {{addEventListener(name, fn) {{ listeners[name] = fn; }}}},
|
||||
onOpen(item) {{ calls.push(['open', item.kind, item.repository, item.number || item.notification_id]); }},
|
||||
onClose() {{ calls.push(['close']); }},
|
||||
onInvalid() {{ calls.push(['invalid']); }},
|
||||
}});
|
||||
controller.start();
|
||||
controller.setItems([{{kind:'issue',repository:'stackchain/api',number:17}}]);
|
||||
controller.close();
|
||||
controller.open({{kind:'pull',repository:'stackchain/dashboard',number:9}});
|
||||
controller.open({{kind:'review',repository:'stackchain/dashboard',number:10}}, {{replace:true}});
|
||||
process.stdout.write(JSON.stringify({{calls, hash:location.hash}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
output = json.loads(result.stdout)
|
||||
|
||||
assert output["calls"] == [
|
||||
["open", "issue", "stackchain/api", 17],
|
||||
["back"],
|
||||
["close"],
|
||||
["push", "#/my-work/pull/stackchain/dashboard/9"],
|
||||
["open", "pull", "stackchain/dashboard", 9],
|
||||
["replace", "#/my-work/review/stackchain/dashboard/10"],
|
||||
["open", "review", "stackchain/dashboard", 10],
|
||||
]
|
||||
assert output["hash"] == "#/my-work/review/stackchain/dashboard/10"
|
||||
|
||||
|
||||
def test_work_route_share_prefers_native_share_and_falls_back_to_clipboard():
|
||||
script = f"""
|
||||
const routes = require({json.dumps(str(WORK_ROUTE))});
|
||||
const calls = [];
|
||||
(async () => {{
|
||||
const native = await routes.share('https://forge.example/dashboard/#/my-work/update/9', {{
|
||||
share: async payload => calls.push(['native', payload.url]),
|
||||
}}, null);
|
||||
const fallback = await routes.share('https://forge.example/dashboard/#/my-work/update/10', {{}}, {{
|
||||
writeText: async text => calls.push(['clipboard', text]),
|
||||
}});
|
||||
process.stdout.write(JSON.stringify({{native, fallback, calls}}));
|
||||
}})();
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {
|
||||
"native": "shared",
|
||||
"fallback": "copied",
|
||||
"calls": [
|
||||
["native", "https://forge.example/dashboard/#/my-work/update/9"],
|
||||
["clipboard", "https://forge.example/dashboard/#/my-work/update/10"],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_wires_addressable_work_sheets_back_navigation_and_share():
|
||||
html = await dashboard()
|
||||
|
||||
assert '<script src="static/work-route.js"></script>' in html
|
||||
assert 'const workRoute = createWorkRoute.createController({' in html
|
||||
assert 'workRoute.setItems(lastMyWork);' in html
|
||||
assert 'href="' + "' + escAttr(createWorkRoute.serialize(" in html
|
||||
assert '.read-update { min-height:44px; width:100%; display:flex;' in html
|
||||
assert 'workRoute.close();' in html
|
||||
assert html.count('class="share-work-route"') == 4
|
||||
assert 'createWorkRoute.share(window.location.href, navigator, navigator.clipboard)' in html
|
||||
assert 'Route unavailable · this item is no longer in My Work.' in html
|
||||
|
||||
|
||||
def test_my_work_queue_prioritizes_labels_then_reviews_and_keeps_repo_identity():
|
||||
@@ -1771,7 +1899,7 @@ async def test_mobile_my_work_captures_new_issue_in_accessible_draft_safe_sheet(
|
||||
assert '<script src="static/create-issue-sheet.js"></script>' in html
|
||||
assert 'createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage })' in html
|
||||
assert 'lastMyWork = buildMyWork(lastContextSnapshot);' in html
|
||||
assert 'openIssueSheet(created' in html
|
||||
assert 'openRoutedWork(created' in html
|
||||
assert 'issueCapture.loadLabels(repository)' in html
|
||||
assert "input[name=\"create-issue-label\"]:checked" in html
|
||||
assert '.create-issue-label-option' in html and 'min-height:44px' in html
|
||||
|
||||
Reference in new issue
Block a user