Work through My Work sequentially on mobile #194
|
|
@ -157,6 +157,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.new-issue { min-height:44px; }
|
.new-issue { min-height:44px; }
|
||||||
.find-work-action { min-height:44px; }
|
.find-work-action { min-height:44px; }
|
||||||
.my-work-actions { display:flex; flex-wrap:wrap; gap:8px; }
|
.my-work-actions { display:flex; flex-wrap:wrap; gap:8px; }
|
||||||
|
.start-work-session { min-height:44px; }
|
||||||
|
.work-session-nav { position:sticky; bottom:0; z-index:5; display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px; padding:10px 4px; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; }
|
||||||
|
.work-session-nav[hidden] { display:none; }
|
||||||
|
.work-session-nav [data-work-session-progress] { grid-column:1 / -1; text-align:center; }
|
||||||
|
.work-session-nav button { min-height:44px; width:100%; }
|
||||||
.find-work-sheet { position:fixed; inset:0; z-index:58; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
|
.find-work-sheet { position:fixed; inset:0; z-index:58; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
|
||||||
.find-work-sheet.open { display:flex; }
|
.find-work-sheet.open { display:flex; }
|
||||||
.find-work-panel { width:min(560px,100%); height:100dvh; overflow:auto; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
|
.find-work-panel { width:min(560px,100%); height:100dvh; overflow:auto; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
|
||||||
|
|
@ -241,6 +246,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<div class="small" id="my-work-status" aria-live="polite">Loading assigned work…</div>
|
<div class="small" id="my-work-status" aria-live="polite">Loading assigned work…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-work-actions">
|
<div class="my-work-actions">
|
||||||
|
<button class="start-work-session" id="start-work-session" type="button">Start work</button>
|
||||||
<button class="find-work-action" id="find-work" type="button">Find work</button>
|
<button class="find-work-action" id="find-work" type="button">Find work</button>
|
||||||
<button class="new-issue" id="new-issue" type="button">New issue</button>
|
<button class="new-issue" id="new-issue" type="button">New issue</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -403,6 +409,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<button id="close-issue" type="button">Close issue</button>
|
<button id="close-issue" type="button">Close issue</button>
|
||||||
<a id="open-issue-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
<a id="open-issue-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||||
</div>
|
</div>
|
||||||
|
<nav class="work-session-nav" aria-label="Work session" hidden>
|
||||||
|
<span class="small" aria-live="polite" data-work-session-progress></span>
|
||||||
|
<button type="button" data-work-session-previous>Previous</button>
|
||||||
|
<button type="button" data-work-session-next>Next work item</button>
|
||||||
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -482,6 +493,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<button id="mark-update-read-next" type="button">Mark read & next</button>
|
<button id="mark-update-read-next" type="button">Mark read & next</button>
|
||||||
<a id="open-update-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
<a id="open-update-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||||
</div>
|
</div>
|
||||||
|
<nav class="work-session-nav" aria-label="Work session" hidden>
|
||||||
|
<span class="small" aria-live="polite" data-work-session-progress></span>
|
||||||
|
<button type="button" data-work-session-previous>Previous</button>
|
||||||
|
<button type="button" data-work-session-next>Next work item</button>
|
||||||
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -509,6 +525,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<button id="merge-pull" type="button" disabled>Merge</button>
|
<button id="merge-pull" type="button" disabled>Merge</button>
|
||||||
<a id="open-pull-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
<a id="open-pull-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||||
</div>
|
</div>
|
||||||
|
<nav class="work-session-nav" aria-label="Work session" hidden>
|
||||||
|
<span class="small" aria-live="polite" data-work-session-progress></span>
|
||||||
|
<button type="button" data-work-session-previous>Previous</button>
|
||||||
|
<button type="button" data-work-session-next>Next work item</button>
|
||||||
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -564,6 +585,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
</section>
|
</section>
|
||||||
<h2>Review history</h2>
|
<h2>Review history</h2>
|
||||||
<div id="review-history" class="muted"></div>
|
<div id="review-history" class="muted"></div>
|
||||||
|
<nav class="work-session-nav" aria-label="Work session" hidden>
|
||||||
|
<span class="small" aria-live="polite" data-work-session-progress></span>
|
||||||
|
<button type="button" data-work-session-previous>Previous</button>
|
||||||
|
<button type="button" data-work-session-next>Next work item</button>
|
||||||
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -837,6 +863,59 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
onClose: () => closeUpdateSheet(false),
|
onClose: () => closeUpdateSheet(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function closeOpenWorkSheets() {
|
||||||
|
['#issue-sheet', '#pull-sheet', '#review-sheet', '#update-sheet'].forEach(selector =>
|
||||||
|
qs(selector).classList.remove('open')
|
||||||
|
);
|
||||||
|
selectedIssue = null;
|
||||||
|
selectedIssueDetail = null;
|
||||||
|
selectedPull = null;
|
||||||
|
selectedPullDetail = null;
|
||||||
|
selectedReview = null;
|
||||||
|
selectedUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWorkSessionItem(item) {
|
||||||
|
closeOpenWorkSheets();
|
||||||
|
if (selectedWorkFilter === 'update' && item.has_update && Number.isInteger(item.notification_id)) {
|
||||||
|
updateTrigger = null;
|
||||||
|
notificationReader.open(item, lastMyWork);
|
||||||
|
} else if (item.is_review) {
|
||||||
|
openReviewSheet(item, null);
|
||||||
|
} else if (item.kind === 'issue') {
|
||||||
|
openIssueSheet(item, null);
|
||||||
|
} else if (item.kind === 'pull') {
|
||||||
|
openPullSheet(item, null);
|
||||||
|
} else if (item.has_update && Number.isInteger(item.notification_id)) {
|
||||||
|
updateTrigger = null;
|
||||||
|
notificationReader.open(item, lastMyWork);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workSession = createWorkSession({
|
||||||
|
getItems: () => lastMyWork,
|
||||||
|
getFilter: () => selectedWorkFilter,
|
||||||
|
onOpen: openWorkSessionItem,
|
||||||
|
onProgress: state => {
|
||||||
|
document.querySelectorAll('.work-session-nav').forEach(nav => { nav.hidden = false; });
|
||||||
|
document.querySelectorAll('[data-work-session-progress]').forEach(element => {
|
||||||
|
element.textContent = 'Item ' + state.index + ' of ' + state.total;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-work-session-previous]').forEach(button => {
|
||||||
|
button.disabled = !state.can_previous;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-work-session-next]').forEach(button => {
|
||||||
|
button.textContent = state.can_next ? 'Next work item' : 'Finish session';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onFinish: () => {
|
||||||
|
closeOpenWorkSheets();
|
||||||
|
document.querySelectorAll('.work-session-nav').forEach(nav => { nav.hidden = true; });
|
||||||
|
qs('#my-work-action-status').textContent = 'Work session complete.';
|
||||||
|
qs('#start-work-session').focus();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function renderContextSnapshot(data) {
|
function renderContextSnapshot(data) {
|
||||||
liveMode = true;
|
liveMode = true;
|
||||||
hasContextSnapshot = true;
|
hasContextSnapshot = true;
|
||||||
|
|
@ -914,6 +993,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
summarizeMyWork(lastMyWork) : 'No assigned work, review requests, or unread updates.';
|
summarizeMyWork(lastMyWork) : 'No assigned work, review requests, or unread updates.';
|
||||||
updateWorkPaginationControls();
|
updateWorkPaginationControls();
|
||||||
renderMyWork();
|
renderMyWork();
|
||||||
|
if (workSession.active()) workSession.reconcile();
|
||||||
}
|
}
|
||||||
|
|
||||||
function activeWorkStreams() {
|
function activeWorkStreams() {
|
||||||
|
|
@ -1955,8 +2035,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
lastContextSnapshot = buildMyWork.removeIssue(
|
lastContextSnapshot = buildMyWork.removeIssue(
|
||||||
lastContextSnapshot, releasing.repository, releasing.number
|
lastContextSnapshot, releasing.repository, releasing.number
|
||||||
);
|
);
|
||||||
paintMyWork(lastContextSnapshot);
|
|
||||||
closeIssueSheet();
|
closeIssueSheet();
|
||||||
|
paintMyWork(lastContextSnapshot);
|
||||||
qs('#my-work-action-status').textContent = releasing.key + ' released.' +
|
qs('#my-work-action-status').textContent = releasing.key + ' released.' +
|
||||||
(confirmed.available ? ' It is available in Find Work.' : ' Other assignees remain.');
|
(confirmed.available ? ' It is available in Find Work.' : ' Other assignees remain.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1973,10 +2053,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
qs('#issue-sheet-status').textContent = 'Closing issue…';
|
qs('#issue-sheet-status').textContent = 'Closing issue…';
|
||||||
try {
|
try {
|
||||||
await issueController.close(selectedIssue);
|
await issueController.close(selectedIssue);
|
||||||
|
closeIssueSheet();
|
||||||
lastMyWork = lastMyWork.filter(item =>
|
lastMyWork = lastMyWork.filter(item =>
|
||||||
!(item.kind === 'issue' && item.repository === closing.repository && item.number === closing.number)
|
!(item.kind === 'issue' && item.repository === closing.repository && item.number === closing.number)
|
||||||
);
|
);
|
||||||
closeIssueSheet();
|
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
qs('#my-work-action-status').textContent = closing.key + ' closed.';
|
qs('#my-work-action-status').textContent = closing.key + ' closed.';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -2025,10 +2105,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
qs('#pull-sheet-status').textContent = 'Merging pull request…';
|
qs('#pull-sheet-status').textContent = 'Merging pull request…';
|
||||||
try {
|
try {
|
||||||
await pullController.merge(selectedPull, selectedPullDetail.head_sha);
|
await pullController.merge(selectedPull, selectedPullDetail.head_sha);
|
||||||
|
closePullSheet();
|
||||||
lastMyWork = lastMyWork.filter(item =>
|
lastMyWork = lastMyWork.filter(item =>
|
||||||
!(item.kind === 'pull' && item.repository === merging.repository && item.number === merging.number)
|
!(item.kind === 'pull' && item.repository === merging.repository && item.number === merging.number)
|
||||||
);
|
);
|
||||||
closePullSheet();
|
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
qs('#my-work-action-status').textContent = merging.key + ' merged.';
|
qs('#my-work-action-status').textContent = merging.key + ' merged.';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -2130,7 +2210,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
if (progress) showReviewProgress(progress.snapshot());
|
if (progress) showReviewProgress(progress.snapshot());
|
||||||
qs('#review-submit-status').textContent = 'Review submitted · ' + (result.state || 'complete') + '.';
|
qs('#review-submit-status').textContent = 'Review submitted · ' + (result.state || 'complete') + '.';
|
||||||
await load();
|
await load();
|
||||||
button.focus();
|
if (workSession.active()) workSession.complete();
|
||||||
|
else button.focus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
qs('#review-submit-status').textContent = error.message + ' Your draft is safe; retry or open in Gitea.';
|
qs('#review-submit-status').textContent = error.message + ' Your draft is safe; retry or open in Gitea.';
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
|
@ -2190,6 +2271,19 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
function load() { return contextPoller.refresh(); }
|
function load() { return contextPoller.refresh(); }
|
||||||
|
|
||||||
qs('#refresh').addEventListener('click', load);
|
qs('#refresh').addEventListener('click', load);
|
||||||
|
qs('#start-work-session').addEventListener('click', () => {
|
||||||
|
if (!filterMyWork(lastMyWork, selectedWorkFilter).length) {
|
||||||
|
qs('#my-work-action-status').textContent = 'No visible work to start.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workSession.start();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-work-session-previous]').forEach(button =>
|
||||||
|
button.addEventListener('click', () => workSession.previous())
|
||||||
|
);
|
||||||
|
document.querySelectorAll('[data-work-session-next]').forEach(button =>
|
||||||
|
button.addEventListener('click', () => workSession.next())
|
||||||
|
);
|
||||||
qs('#load-more-notifications').addEventListener('click', () =>
|
qs('#load-more-notifications').addEventListener('click', () =>
|
||||||
notificationPager.loadMore(lastNotifications)
|
notificationPager.loadMore(lastNotifications)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,84 @@ function filterMyWork(items, selectedFilter) {
|
||||||
return items.filter((item) => item.kind === selectedFilter);
|
return items.filter((item) => item.kind === selectedFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function workIdentity(item) {
|
||||||
|
if (!item) return '';
|
||||||
|
const kind = item.is_review ? 'review' : (item.kind || 'work');
|
||||||
|
const repository = item.repository || '';
|
||||||
|
const number = Number.isInteger(item.number) ? item.number : '';
|
||||||
|
const notification = Number.isInteger(item.notification_id) ? item.notification_id : '';
|
||||||
|
return [kind, repository, number, notification].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWorkSession({ getItems, getFilter, onOpen, onProgress, onFinish }) {
|
||||||
|
let currentIdentity = '';
|
||||||
|
let currentIndex = -1;
|
||||||
|
let running = false;
|
||||||
|
|
||||||
|
const queue = () => filterMyWork(getItems() || [], getFilter());
|
||||||
|
const report = (items, index) => onProgress({
|
||||||
|
index: index + 1,
|
||||||
|
total: items.length,
|
||||||
|
can_previous: index > 0,
|
||||||
|
can_next: index < items.length - 1,
|
||||||
|
});
|
||||||
|
const finish = () => {
|
||||||
|
running = false;
|
||||||
|
currentIdentity = '';
|
||||||
|
currentIndex = -1;
|
||||||
|
onFinish();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const openAt = (items, index) => {
|
||||||
|
if (!items.length || index < 0 || index >= items.length) return finish();
|
||||||
|
currentIndex = index;
|
||||||
|
currentIdentity = workIdentity(items[index]);
|
||||||
|
report(items, index);
|
||||||
|
onOpen(items[index]);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: () => running,
|
||||||
|
start() {
|
||||||
|
const items = queue();
|
||||||
|
if (!items.length) return finish();
|
||||||
|
running = true;
|
||||||
|
return openAt(items, 0);
|
||||||
|
},
|
||||||
|
reconcile() {
|
||||||
|
if (!running) return false;
|
||||||
|
const items = queue();
|
||||||
|
const index = items.findIndex(item => workIdentity(item) === currentIdentity);
|
||||||
|
if (index < 0) return items.length ? openAt(items, Math.min(currentIndex, items.length - 1)) : finish();
|
||||||
|
currentIndex = index;
|
||||||
|
report(items, index);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
previous() {
|
||||||
|
if (!running) return false;
|
||||||
|
const items = queue();
|
||||||
|
const index = items.findIndex(item => workIdentity(item) === currentIdentity);
|
||||||
|
return index > 0 ? openAt(items, index - 1) : false;
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
if (!running) return false;
|
||||||
|
const items = queue();
|
||||||
|
const index = items.findIndex(item => workIdentity(item) === currentIdentity);
|
||||||
|
return index >= 0 && index < items.length - 1 ? openAt(items, index + 1) : finish();
|
||||||
|
},
|
||||||
|
complete() {
|
||||||
|
if (!running) return false;
|
||||||
|
const items = queue();
|
||||||
|
const stillPresent = items.findIndex(item => workIdentity(item) === currentIdentity);
|
||||||
|
if (stillPresent >= 0) {
|
||||||
|
return stillPresent < items.length - 1 ? openAt(items, stillPresent + 1) : finish();
|
||||||
|
}
|
||||||
|
return items.length ? openAt(items, Math.min(currentIndex, items.length - 1)) : finish();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function replaceIssueLabels(data, repository, number, labels) {
|
function replaceIssueLabels(data, repository, number, labels) {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
|
|
@ -418,6 +496,8 @@ function countMyWork(items) {
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
buildMyWork.filterMyWork = filterMyWork;
|
buildMyWork.filterMyWork = filterMyWork;
|
||||||
|
buildMyWork.createWorkSession = createWorkSession;
|
||||||
|
buildMyWork.workIdentity = workIdentity;
|
||||||
buildMyWork.replaceIssueLabels = replaceIssueLabels;
|
buildMyWork.replaceIssueLabels = replaceIssueLabels;
|
||||||
buildMyWork.replaceIssueContent = replaceIssueContent;
|
buildMyWork.replaceIssueContent = replaceIssueContent;
|
||||||
buildMyWork.replaceIssueDueDate = replaceIssueDueDate;
|
buildMyWork.replaceIssueDueDate = replaceIssueDueDate;
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,100 @@ process.stdout.write(JSON.stringify({{
|
||||||
assert output["summary"] == "1 review · 2 assigned"
|
assert output["summary"] == "1 review · 2 assigned"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_work_session_follows_filter_and_reconciles_by_identity():
|
||||||
|
items = [
|
||||||
|
{"kind": "issue", "repository": "stackchain/api", "number": 1, "title": "First"},
|
||||||
|
{"kind": "pull", "repository": "stackchain/web", "number": 2, "title": "Second"},
|
||||||
|
{"kind": "issue", "repository": "stackchain/api", "number": 3, "title": "Third"},
|
||||||
|
]
|
||||||
|
script = f"""
|
||||||
|
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||||
|
let items = {json.dumps(items)};
|
||||||
|
let filter = 'issue';
|
||||||
|
const opened = [];
|
||||||
|
const progress = [];
|
||||||
|
let finished = 0;
|
||||||
|
const session = buildMyWork.createWorkSession({{
|
||||||
|
getItems: () => items,
|
||||||
|
getFilter: () => filter,
|
||||||
|
onOpen: item => opened.push(item.title),
|
||||||
|
onProgress: state => progress.push(state),
|
||||||
|
onFinish: () => {{ finished += 1; }},
|
||||||
|
}});
|
||||||
|
session.start();
|
||||||
|
items = [items[2], items[1], items[0]];
|
||||||
|
session.reconcile();
|
||||||
|
session.previous();
|
||||||
|
items = items.filter(item => item.number !== 3);
|
||||||
|
session.complete();
|
||||||
|
items = [];
|
||||||
|
session.complete();
|
||||||
|
process.stdout.write(JSON.stringify({{opened, progress, finished, active:session.active()}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
output = json.loads(result.stdout)
|
||||||
|
|
||||||
|
assert output["opened"] == ["First", "Third", "First"]
|
||||||
|
assert output["progress"] == [
|
||||||
|
{"index": 1, "total": 2, "can_previous": False, "can_next": True},
|
||||||
|
{"index": 2, "total": 2, "can_previous": True, "can_next": False},
|
||||||
|
{"index": 1, "total": 2, "can_previous": False, "can_next": True},
|
||||||
|
{"index": 1, "total": 1, "can_previous": False, "can_next": False},
|
||||||
|
]
|
||||||
|
assert output["finished"] == 1
|
||||||
|
assert output["active"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_work_session_completion_finishes_when_current_item_is_last():
|
||||||
|
script = f"""
|
||||||
|
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||||
|
const items = [{{kind:'pull',repository:'stackchain/api',number:9,is_review:true}}];
|
||||||
|
let finished = 0;
|
||||||
|
const session = buildMyWork.createWorkSession({{
|
||||||
|
getItems: () => items, getFilter: () => 'review', onOpen: () => {{}},
|
||||||
|
onProgress: () => {{}}, onFinish: () => {{ finished += 1; }},
|
||||||
|
}});
|
||||||
|
session.start();
|
||||||
|
session.complete();
|
||||||
|
process.stdout.write(JSON.stringify({{finished,active:session.active()}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
assert json.loads(result.stdout) == {"finished": 1, "active": False}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_mobile_work_session_renders_touch_safe_controls_for_every_work_sheet():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert 'id="start-work-session"' in html
|
||||||
|
assert html.count('class="work-session-nav"') == 4
|
||||||
|
assert html.count('<span class="small" aria-live="polite" data-work-session-progress>') == 4
|
||||||
|
assert html.count('<button type="button" data-work-session-previous>') == 4
|
||||||
|
assert html.count('<button type="button" data-work-session-next>') == 4
|
||||||
|
assert '.work-session-nav button { min-height:44px;' in html
|
||||||
|
assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom))' in html
|
||||||
|
assert 'aria-live="polite" data-work-session-progress' in html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_dashboard_wires_work_session_to_existing_sheet_flows_and_completion_actions():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert 'const workSession = createWorkSession({' in html
|
||||||
|
assert 'function openWorkSessionItem(item)' in html
|
||||||
|
assert "qs('#start-work-session').addEventListener('click'" in html
|
||||||
|
assert "document.querySelectorAll('[data-work-session-previous]')" in html
|
||||||
|
assert "document.querySelectorAll('[data-work-session-next]')" in html
|
||||||
|
assert 'workSession.complete();' in html
|
||||||
|
assert 'workSession.reconcile();' in html
|
||||||
|
for opener in ('openIssueSheet(item', 'openPullSheet(item', 'openReviewSheet(item', 'notificationReader.open(item'):
|
||||||
|
assert opener in html
|
||||||
|
|
||||||
|
|
||||||
def test_my_work_filter_counts_distinguish_prs_from_review_requests():
|
def test_my_work_filter_counts_distinguish_prs_from_review_requests():
|
||||||
items = [
|
items = [
|
||||||
{"kind": "issue", "is_review": False},
|
{"kind": "issue", "is_review": False},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user