Complete delegated mobile filing with a shareable issue receipt #867
|
|
@ -360,6 +360,9 @@ function createBackgroundIssueSync({
|
|||
const resource = item.kind === 'pull-comment' ? 'pull' : 'issue';
|
||||
return { id: item.id, status, kind: 'message', route: '#/my-work/' + resource + '/' + repository + '/' + encodeURIComponent(item.number) };
|
||||
}
|
||||
if (typeof delivered.url === 'string' && /^https?:\/\//.test(delivered.url)) {
|
||||
return { id: item.id, status, kind: 'issue', url: delivered.url };
|
||||
}
|
||||
return { id: item.id, status, kind: 'issue', route: '#/my-work/issue/' + repository + '/' + encodeURIComponent(delivered.number) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex
|
|||
.live-data-status-header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
|
||||
.live-data-status-header h2, .live-data-status-header p { margin-top:0; }
|
||||
.live-data-status-header button, .live-data-status-actions button { min-height:44px; }
|
||||
.issue-filing-receipt { position:fixed; inset:0; z-index:108; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
|
||||
.issue-filing-receipt[hidden] { display:none; }
|
||||
.issue-filing-receipt-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #4ade80; border-radius:18px 18px 0 0; background:#0b1526; overflow-wrap:anywhere; }
|
||||
.issue-filing-receipt-panel h2, .issue-filing-receipt-panel h3 { margin:.25rem 0; }
|
||||
.issue-filing-receipt-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:16px; }
|
||||
.issue-filing-receipt-actions button, .issue-filing-receipt-actions .button-link { box-sizing:border-box; display:flex; align-items:center; justify-content:center; min-width:0; min-height:44px; width:100%; text-align:center; }
|
||||
@media (max-width:359px) { .issue-filing-receipt-actions { grid-template-columns:1fr; } }
|
||||
.live-data-status-feeds { display:grid; gap:8px; margin:14px 0; }
|
||||
.live-data-status-feed { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; padding:12px; border:1px solid #2a496e; border-radius:12px; background:#0f2237; }
|
||||
.live-data-status-feed strong, .live-data-status-feed span { overflow-wrap:anywhere; }
|
||||
|
|
|
|||
|
|
@ -3962,6 +3962,14 @@
|
|||
}
|
||||
|
||||
let suppressCreateDraftOnHistoryClose = false;
|
||||
const issueFilingReceipt = createIssueFilingReceipt({
|
||||
root:qs('#issue-filing-receipt'), heading:qs('#issue-filing-receipt-heading'),
|
||||
key:qs('#issue-filing-receipt-key'), title:qs('#issue-filing-receipt-title'),
|
||||
ownership:qs('#issue-filing-receipt-ownership'), openLink:qs('#issue-filing-receipt-open'),
|
||||
shareButton:qs('#issue-filing-receipt-share'), fileAnotherButton:qs('#issue-filing-receipt-another'),
|
||||
doneButton:qs('#issue-filing-receipt-done'), status:qs('#issue-filing-receipt-status'),
|
||||
navigator, onFileAnother:() => qs('#new-issue').click(),
|
||||
});
|
||||
function closeCreateIssueSheet(navigate = true, preserveDraft = true) {
|
||||
if (navigate && taskOverlayHistory.current() === 'new') {
|
||||
suppressCreateDraftOnHistoryClose = !preserveDraft;
|
||||
|
|
@ -4009,6 +4017,9 @@
|
|||
item.kind === 'issue' && item.repository === confirmed.repository && item.number === confirmed.number
|
||||
);
|
||||
const completionHandled = startedCompletions.has(confirmed.repository + '#' + confirmed.number);
|
||||
if (!(confirmed.assignees || []).includes(activeFlushLogin)) {
|
||||
issueFilingReceipt.show(confirmed, qs('#new-issue'));
|
||||
}
|
||||
if (startCreated && !(result.completions || []).length && created) {
|
||||
const outcome = createAndStart.complete(created);
|
||||
if (outcome !== 'started' && openCreated) openRoutedWork(created, qs('#new-issue'));
|
||||
|
|
|
|||
|
|
@ -424,6 +424,23 @@
|
|||
<button id="add-plan-preview" type="button">Add to Today & back</button>
|
||||
</div>
|
||||
|
||||
<div class="issue-filing-receipt" id="issue-filing-receipt" role="dialog" aria-modal="true" aria-labelledby="issue-filing-receipt-heading" hidden>
|
||||
<section class="issue-filing-receipt-panel">
|
||||
<div class="small">Issue created</div>
|
||||
<h2 id="issue-filing-receipt-heading" tabindex="-1">Ready to hand off</h2>
|
||||
<div id="issue-filing-receipt-key" class="small"></div>
|
||||
<h3 id="issue-filing-receipt-title"></h3>
|
||||
<p id="issue-filing-receipt-ownership"></p>
|
||||
<p id="issue-filing-receipt-status" class="small" aria-live="polite"></p>
|
||||
<div class="issue-filing-receipt-actions">
|
||||
<a id="issue-filing-receipt-open" class="button-link" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||
<button id="issue-filing-receipt-share" type="button">Share link</button>
|
||||
<button id="issue-filing-receipt-another" type="button">File another</button>
|
||||
<button id="issue-filing-receipt-done" type="button">Done</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="cmd-palette" role="dialog" aria-label="Command palette">
|
||||
<div class="cmd-palette-header">
|
||||
<strong>Search work</strong>
|
||||
|
|
@ -1301,6 +1318,7 @@
|
|||
<script src="static/mobile-composer-viewport.js"></script>
|
||||
<script src="static/mention-composer.js"></script>
|
||||
<script src="static/push-notifications.js"></script>
|
||||
<script src="static/issue-filing-receipt.js"></script>
|
||||
<script src="static/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
77
frontend/issue-filing-receipt.js
Normal file
77
frontend/issue-filing-receipt.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === 'object' && module.exports) module.exports = api;
|
||||
else root.createIssueFilingReceipt = api;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||
'use strict';
|
||||
|
||||
function createIssueFilingReceipt({
|
||||
root, heading, key, title, ownership, openLink, shareButton,
|
||||
fileAnotherButton, doneButton, status, navigator = {}, clipboard = navigator.clipboard,
|
||||
onFileAnother = function () {},
|
||||
}) {
|
||||
let active = null;
|
||||
let restoreFocus = null;
|
||||
|
||||
function close() {
|
||||
root.hidden = true;
|
||||
active = null;
|
||||
const target = restoreFocus;
|
||||
restoreFocus = null;
|
||||
target?.focus?.();
|
||||
}
|
||||
|
||||
async function share() {
|
||||
if (!active) return;
|
||||
const payload = {
|
||||
title: active.repository + '#' + active.number + ' · ' + active.title,
|
||||
text: active.title,
|
||||
url: active.url,
|
||||
};
|
||||
if (typeof navigator.share === 'function') {
|
||||
try {
|
||||
await navigator.share(payload);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') return;
|
||||
}
|
||||
}
|
||||
if (typeof clipboard?.writeText !== 'function') {
|
||||
status.textContent = 'Issue link is ready in Open in Gitea.';
|
||||
return;
|
||||
}
|
||||
await clipboard.writeText(active.url);
|
||||
status.textContent = 'Issue link copied.';
|
||||
}
|
||||
|
||||
function show(issue, returnTarget) {
|
||||
active = issue;
|
||||
restoreFocus = returnTarget || null;
|
||||
key.textContent = issue.repository + '#' + issue.number;
|
||||
title.textContent = issue.title;
|
||||
ownership.textContent = issue.assignees?.length
|
||||
? 'Assigned to @' + issue.assignees.join(', @') + '.'
|
||||
: 'Created with no owner.';
|
||||
openLink.href = issue.url;
|
||||
status.textContent = '';
|
||||
root.hidden = false;
|
||||
heading.focus();
|
||||
}
|
||||
|
||||
shareButton.addEventListener('click', share);
|
||||
root.addEventListener('keydown', event => {
|
||||
if (event.key !== 'Escape' || root.hidden) return;
|
||||
event.preventDefault();
|
||||
close();
|
||||
});
|
||||
doneButton.addEventListener('click', close);
|
||||
fileAnotherButton.addEventListener('click', () => {
|
||||
close();
|
||||
onFileAnother();
|
||||
});
|
||||
|
||||
return { show, close, share };
|
||||
}
|
||||
|
||||
return createIssueFilingReceipt;
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v101';
|
||||
const CACHE = 'stackchain-dashboard-shell-v102';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
|
@ -100,6 +100,7 @@ const SHELL = [
|
|||
BASE + 'static/mobile-composer-viewport.js',
|
||||
BASE + 'static/mention-composer.js',
|
||||
BASE + 'static/push-notifications.js',
|
||||
BASE + 'static/issue-filing-receipt.js',
|
||||
BASE + 'static/background-issue-sync.js',
|
||||
];
|
||||
const OPTIONAL_FEATURES = [
|
||||
|
|
@ -332,7 +333,7 @@ async function flushAndNotify() {
|
|||
body: needsAuthorization ? 'Tap to authorize it in the Delivery center.' :
|
||||
needsAttention ? 'Tap to review it in Drafts.' : 'Tap to open it in Stackchain.',
|
||||
tag: 'stackchain-delivery-' + receipt.id,
|
||||
data: { route: receipt.route },
|
||||
data: receipt.url ? { url: receipt.url } : { route: receipt.route },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -452,8 +453,29 @@ async function openWorkRoute(route) {
|
|||
return client.focus();
|
||||
}
|
||||
|
||||
async function openCanonicalIssueUrl(rawUrl) {
|
||||
let target;
|
||||
try { target = new URL(rawUrl); } catch (_error) { return; }
|
||||
if (
|
||||
target.origin !== self.location.origin
|
||||
|| !/^\/git\/[^/]+\/[^/]+\/issues\/\d+$/.test(target.pathname)
|
||||
|| target.search || target.hash
|
||||
) return;
|
||||
const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
||||
const client = windows.find(candidate => candidate.url.startsWith(self.location.origin + BASE));
|
||||
if (!client) return self.clients.openWindow(target.href);
|
||||
if (client.navigate) await client.navigate(target.href);
|
||||
return client.focus();
|
||||
}
|
||||
|
||||
self.addEventListener('notificationclick', event => {
|
||||
const route = String(event.notification.data?.route || '');
|
||||
const issueUrl = String(event.notification.data?.url || '');
|
||||
if (issueUrl) {
|
||||
event.notification.close();
|
||||
event.waitUntil(openCanonicalIssueUrl(issueUrl));
|
||||
return;
|
||||
}
|
||||
if (event.action === 'protect-today') {
|
||||
const protectRoute = String(event.notification.data?.protectRoute || route);
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ FEATURE_SOURCES = {
|
|||
"security-center": ("static/security-center.js",),
|
||||
"today-timer": (
|
||||
"static/today-completion.js", "static/work-detail-position.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
|
||||
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
||||
"static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
||||
"static/assign-and-start.js", "static/queue-today.js", "static/create-and-start.js",
|
||||
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||
"static/search-batch-plan.js", "static/issue-evidence-review.js", "static/issue-evidence-editor.js",
|
||||
"static/issue-attachment.js", "static/issue-filing-review.js",
|
||||
"static/issue-attachment.js", "static/issue-filing-review.js", "static/issue-filing-receipt.js",
|
||||
),
|
||||
}
|
||||
CACHE_DECLARATION = re.compile(
|
||||
|
|
|
|||
|
|
@ -78,6 +78,24 @@ const fetchJson=async(url,options={{}})=>{{calls.push({{url,body:options.body||'
|
|||
}
|
||||
|
||||
|
||||
def test_closed_app_issue_receipt_uses_the_confirmed_canonical_gitea_url():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
|
||||
let item={{id:'unowned-link',operationId:'unowned-link',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Backlog capture',body:'',labelIds:[],unassigned:true}};
|
||||
const store={{claimNext:async()=>item,complete:async()=>{{item=null;}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0}};
|
||||
const fetchJson=async url=>url==='api/v1/background-identity'?{{login:'timmy'}}:{{repository:'o/r',number:19,title:'Backlog capture',assignees:[],url:'https://forge.example/git/o/r/issues/19'}};
|
||||
(async()=>{{const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify(result.receipts[0]));}})();
|
||||
"""
|
||||
receipt = run_node(script)
|
||||
|
||||
assert receipt == {
|
||||
"id": "unowned-link",
|
||||
"status": "confirmed",
|
||||
"kind": "issue",
|
||||
"url": "https://forge.example/git/o/r/issues/19",
|
||||
}
|
||||
|
||||
|
||||
def test_closed_app_sync_delivers_desired_blocker_state():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
|
||||
|
|
|
|||
|
|
@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
|
|||
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
|
||||
assert '.update-reply-actions button { min-height:44px;' in html
|
||||
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
assert "stackchain-dashboard-shell-v101" in worker
|
||||
assert "stackchain-dashboard-shell-v102" in worker
|
||||
|
|
|
|||
89
tests/test_issue_filing_receipt.py
Normal file
89
tests/test_issue_filing_receipt.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RECEIPT = Path(__file__).parents[1] / "frontend" / "issue-filing-receipt.js"
|
||||
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
||||
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||
|
||||
|
||||
def run_node(script: str) -> dict:
|
||||
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_confirmed_delegated_issue_opens_an_accessible_shareable_receipt():
|
||||
script = f"""
|
||||
const createReceipt = require({json.dumps(str(RECEIPT))});
|
||||
const element = () => ({{hidden:true,textContent:'',href:'',focused:false,focus(){{this.focused=true;}},addEventListener(name, fn){{this[name]=fn;}}}});
|
||||
const elements = {{root:element(),heading:element(),key:element(),title:element(),ownership:element(),openLink:element(),shareButton:element(),fileAnotherButton:element(),doneButton:element(),status:element()}};
|
||||
const controller = createReceipt({{...elements,navigator:{{share:async payload=>{{elements.shared=payload;}}}}}});
|
||||
controller.show({{repository:'stackchain/dashboard',number:866,title:'Finish the filing journey',assignees:['alex'],url:'https://forge.example/git/stackchain/dashboard/issues/866'}}, elements.doneButton);
|
||||
(async()=>{{await elements.shareButton.click();process.stdout.write(JSON.stringify({{
|
||||
hidden:elements.root.hidden,key:elements.key.textContent,title:elements.title.textContent,
|
||||
ownership:elements.ownership.textContent,href:elements.openLink.href,focused:elements.heading.focused,
|
||||
shared:elements.shared,
|
||||
}}));}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output == {
|
||||
"hidden": False,
|
||||
"key": "stackchain/dashboard#866",
|
||||
"title": "Finish the filing journey",
|
||||
"ownership": "Assigned to @alex.",
|
||||
"href": "https://forge.example/git/stackchain/dashboard/issues/866",
|
||||
"focused": True,
|
||||
"shared": {
|
||||
"title": "stackchain/dashboard#866 · Finish the filing journey",
|
||||
"text": "Finish the filing journey",
|
||||
"url": "https://forge.example/git/stackchain/dashboard/issues/866",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_unassigned_receipt_copies_link_and_escape_restores_launch_focus():
|
||||
script = f"""
|
||||
const createReceipt = require({json.dumps(str(RECEIPT))});
|
||||
const element = () => ({{hidden:true,textContent:'',href:'',focusCount:0,focus(){{this.focusCount++;}},addEventListener(name, fn){{this[name]=fn;}}}});
|
||||
const elements = {{root:element(),heading:element(),key:element(),title:element(),ownership:element(),openLink:element(),shareButton:element(),fileAnotherButton:element(),doneButton:element(),status:element()}};
|
||||
const copied=[];const launcher=element();
|
||||
const controller=createReceipt({{...elements,navigator:{{}},clipboard:{{writeText:async value=>copied.push(value)}}}});
|
||||
controller.show({{repository:'stackchain/dashboard',number:867,title:'Unowned follow-up',assignees:[],url:'https://forge.example/git/stackchain/dashboard/issues/867'}},launcher);
|
||||
(async()=>{{await elements.shareButton.click();elements.root.keydown({{key:'Escape',preventDefault(){{elements.prevented=true;}}}});process.stdout.write(JSON.stringify({{
|
||||
ownership:elements.ownership.textContent,copied,status:elements.status.textContent,hidden:elements.root.hidden,
|
||||
restored:launcher.focusCount,prevented:elements.prevented,
|
||||
}}));}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output == {
|
||||
"ownership": "Created with no owner.",
|
||||
"copied": ["https://forge.example/git/stackchain/dashboard/issues/867"],
|
||||
"status": "Issue link copied.",
|
||||
"hidden": True,
|
||||
"restored": 1,
|
||||
"prevented": True,
|
||||
}
|
||||
|
||||
|
||||
def test_mobile_shell_wires_confirmed_non_my_work_issues_to_the_receipt():
|
||||
index = INDEX.read_text()
|
||||
css = CSS.read_text()
|
||||
dashboard = DASHBOARD.read_text()
|
||||
|
||||
assert 'id="issue-filing-receipt" role="dialog" aria-modal="true"' in index
|
||||
assert 'id="issue-filing-receipt-heading"' in index
|
||||
assert 'id="issue-filing-receipt-open"' in index
|
||||
assert 'id="issue-filing-receipt-share"' in index
|
||||
assert 'id="issue-filing-receipt-another"' in index
|
||||
assert 'id="issue-filing-receipt-done"' in index
|
||||
assert '<script src="static/issue-filing-receipt.js"></script>' in index
|
||||
assert '.issue-filing-receipt-panel' in css
|
||||
assert 'padding-bottom:calc(18px + env(safe-area-inset-bottom))' in css
|
||||
assert '.issue-filing-receipt-actions' in css
|
||||
assert 'min-height:44px' in css
|
||||
assert 'createIssueFilingReceipt({' in dashboard
|
||||
assert "issueFilingReceipt.show(confirmed, qs('#new-issue'))" in dashboard
|
||||
|
|
@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
|
|||
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/later-sync.js'" in source
|
||||
|
|
|
|||
|
|
@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
|
|||
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
|
||||
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
|
||||
assert ".markdown-content a { min-height:44px;" in css
|
||||
assert "stackchain-dashboard-shell-v101" in worker
|
||||
assert "stackchain-dashboard-shell-v102" in worker
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
|
|||
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
|
||||
|
||||
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||
assert "stackchain-dashboard-shell-v101" in worker
|
||||
assert "stackchain-dashboard-shell-v102" in worker
|
||||
|
||||
|
||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
|||
assert "promptStorage:localStorage" in dashboard
|
||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||
assert "stackchain-dashboard-shell-v101" in worker
|
||||
assert "stackchain-dashboard-shell-v102" in worker
|
||||
assert ".device-setup-panel" in css
|
||||
assert ".device-readiness-card" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
|
|||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||
source = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/plan-today.js'" in source
|
||||
assert "BASE + 'static/plan-today-readiness.js'" in source
|
||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||
|
|
|
|||
|
|
@ -120,11 +120,11 @@ async function dispatchMessage(data, ports = []) {{
|
|||
listeners.message({{ data, ports, waitUntil: promise => {{ pending = promise; }} }});
|
||||
if (pending) await pending;
|
||||
}}
|
||||
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null) {{
|
||||
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null, url = null) {{
|
||||
let pending;
|
||||
listeners.notificationclick({{
|
||||
action,
|
||||
notification: {{tag: tag || ('stackchain-update-' + notificationId), data: {{route, notificationId}}, close: () => {{ state.notificationClosed = true; }}}},
|
||||
notification: {{tag: tag || ('stackchain-update-' + notificationId), data: {{route, notificationId, ...(url ? {{url}} : {{}})}}, close: () => {{ state.notificationClosed = true; }}}},
|
||||
waitUntil: promise => {{ pending = promise; }},
|
||||
}});
|
||||
if (pending) await pending;
|
||||
|
|
@ -150,7 +150,7 @@ async function dispatchPush(payload) {{
|
|||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
|
|
@ -159,14 +159,14 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
|||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
||||
def test_offline_review_next_ships_today_completion_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/today-completion.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ def test_offline_review_next_ships_today_completion_atomically():
|
|||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -182,14 +182,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
|||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/later-picker.js'" in source
|
||||
|
||||
|
||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
|
|
@ -198,21 +198,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
|||
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||
|
||||
|
||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
|
@ -460,6 +460,36 @@ def test_opted_in_background_sync_notifies_privately_and_receipt_tap_focuses_rou
|
|||
assert result["notificationClosed"] is True
|
||||
|
||||
|
||||
def test_created_issue_receipt_tap_opens_the_confirmed_canonical_gitea_url():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
const issueUrl = 'https://forge.example/git/stackchain/api/issues/44';
|
||||
state.receiptLogin = 'timmy';
|
||||
state.flushResult = {login:'timmy', receipts:[
|
||||
{id:'capture-1',status:'confirmed',kind:'issue',url:issueUrl},
|
||||
]};
|
||||
state.clientList = [{url:'https://forge.example/dashboard/', navigate:async function(url){ this.url=url; }, focus:async function(){ state.focused.push(this.url); }}];
|
||||
await dispatchSync('stackchain-issue-outbox-v1');
|
||||
await dispatchNotificationClick('', '', null, 'stackchain-delivery-capture-1', issueUrl);
|
||||
process.stdout.write(JSON.stringify(state));
|
||||
"""
|
||||
)
|
||||
|
||||
assert result["notifications"] == [{
|
||||
"title": "Queued issue created",
|
||||
"options": {
|
||||
"body": "Tap to open it in Stackchain.",
|
||||
"tag": "stackchain-delivery-capture-1",
|
||||
"data": {"url": "https://forge.example/git/stackchain/api/issues/44"},
|
||||
},
|
||||
}]
|
||||
assert result["focused"] == [
|
||||
"https://forge.example/git/stackchain/api/issues/44"
|
||||
]
|
||||
assert result["opened"] == []
|
||||
assert result["notificationClosed"] is True
|
||||
|
||||
|
||||
def test_inbound_push_renders_generic_update_and_tap_opens_existing_workflow():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
|
|
@ -764,7 +794,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
|
|||
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/queue-today.js'" in source
|
||||
|
||||
|
||||
|
|
@ -873,6 +903,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
|||
"/dashboard/static/mobile-composer-viewport.js",
|
||||
"/dashboard/static/mention-composer.js",
|
||||
"/dashboard/static/push-notifications.js",
|
||||
"/dashboard/static/issue-filing-receipt.js",
|
||||
"/dashboard/static/background-issue-sync.js",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
|
|||
def test_readiness_runtime_is_available_in_offline_shell():
|
||||
service_worker = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v101';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v102';" in service_worker
|
||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
|
|||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v101" in source
|
||||
assert "stackchain-dashboard-shell-v102" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user