parent
1e8d9a27be
commit
8b2fd305ca
|
|
@ -1496,7 +1496,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.composer-keyboard-active .review-inline-composer,
|
.composer-keyboard-active .review-inline-composer,
|
||||||
.composer-keyboard-active .update-reply { scroll-margin-block:12px; padding-bottom:env(safe-area-inset-bottom); }
|
.composer-keyboard-active .update-reply { scroll-margin-block:12px; padding-bottom:env(safe-area-inset-bottom); }
|
||||||
.mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); }
|
.mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); }
|
||||||
.mobile-queue-sheet { width:100%; max-width:none; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
.mobile-queue-sheet { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
||||||
.following-sheet { width:100%; max-width:none; border:0; border-radius:18px 18px 0 0; }
|
.following-sheet { width:100%; max-width:none; border:0; border-radius:18px 18px 0 0; }
|
||||||
|
|
||||||
.mobile-first-task { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
.mobile-first-task { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
||||||
|
|
@ -1509,9 +1509,18 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.mobile-first-task-actions button:disabled { opacity:.55; }
|
.mobile-first-task-actions button:disabled { opacity:.55; }
|
||||||
|
|
||||||
.mobile-queue-sheet::backdrop { background:rgba(3,9,18,.7); }
|
.mobile-queue-sheet::backdrop { background:rgba(3,9,18,.7); }
|
||||||
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
|
.mobile-queue-panel { box-sizing:border-box; max-height:100dvh; overflow-y:auto; overscroll-behavior:contain; padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
|
||||||
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||||
.mobile-queue-panel h2 { margin:0; }
|
.mobile-queue-panel h2 { margin:0; }
|
||||||
|
.mobile-queue-next { display:grid; gap:6px; margin-top:12px; padding:12px; border:1px solid #60a5fa; border-radius:14px; background:#122f50; }
|
||||||
|
.mobile-queue-next p, .mobile-queue-group h3 { margin:0; }
|
||||||
|
.mobile-queue-next button { min-height:48px; width:100%; text-align:center; font-weight:800; }
|
||||||
|
.mobile-queue-group { margin-top:16px; }
|
||||||
|
.mobile-queue-group h3 { font-size:1rem; }
|
||||||
|
.mobile-queue-all { margin-top:16px; }
|
||||||
|
.mobile-queue-all summary { min-height:44px; display:flex; align-items:center; cursor:pointer; font-weight:700; }
|
||||||
|
.mobile-queue-list [data-unavailable="true"] { border-color:#f59e0b; }
|
||||||
|
.mobile-queue-list [data-unavailable="true"]::after { content:'Sync unavailable · open to retry'; color:#fbbf24; font-size:.75rem; }
|
||||||
.mobile-delivery-recovery { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
.mobile-delivery-recovery { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
||||||
.mobile-delivery-recovery::backdrop { background:rgba(3,9,18,.78); }
|
.mobile-delivery-recovery::backdrop { background:rgba(3,9,18,.78); }
|
||||||
.mobile-delivery-recovery-panel { box-sizing:border-box; display:grid; gap:12px; width:100%; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); overflow-wrap:anywhere; }
|
.mobile-delivery-recovery-panel { box-sizing:border-box; display:grid; gap:12px; width:100%; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); overflow-wrap:anywhere; }
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@
|
||||||
}
|
}
|
||||||
let queueCounts = {};
|
let queueCounts = {};
|
||||||
let preparationItems = {};
|
let preparationItems = {};
|
||||||
|
let renderMobileQueuePresentation = () => {};
|
||||||
const followingQueue = attachFollowing(item => {
|
const followingQueue = attachFollowing(item => {
|
||||||
searchPreviewReturnKind = 'following';
|
searchPreviewReturnKind = 'following';
|
||||||
return searchPreview.open(item);
|
return searchPreview.open(item);
|
||||||
|
|
@ -119,11 +120,13 @@
|
||||||
queueCounts.following = count;
|
queueCounts.following = count;
|
||||||
queueCounts.followingUnavailable = false;
|
queueCounts.followingUnavailable = false;
|
||||||
preparationItems.following = items.filter(item => item.has_unseen_change === true);
|
preparationItems.following = items.filter(item => item.has_unseen_change === true);
|
||||||
|
renderMobileQueuePresentation();
|
||||||
mobileStartDay.render();
|
mobileStartDay.render();
|
||||||
},
|
},
|
||||||
onStatus:status => {
|
onStatus:status => {
|
||||||
if (status === 'loading') return;
|
if (status === 'loading') return;
|
||||||
queueCounts.followingUnavailable = status === 'error';
|
queueCounts.followingUnavailable = status === 'error';
|
||||||
|
renderMobileQueuePresentation();
|
||||||
mobileStartDay.render();
|
mobileStartDay.render();
|
||||||
},
|
},
|
||||||
onReviewComplete:() => mobileStartDay.completePhase('following'),
|
onReviewComplete:() => mobileStartDay.completePhase('following'),
|
||||||
|
|
@ -175,7 +178,18 @@
|
||||||
announce: announceWork,
|
announce: announceWork,
|
||||||
getCounts: () => queueCounts,
|
getCounts: () => queueCounts,
|
||||||
openFindWork: () => qs('#find-work').click(),
|
openFindWork: () => qs('#find-work').click(),
|
||||||
|
rows: Object.fromEntries(
|
||||||
|
Array.from(document.querySelectorAll('[data-mobile-queue]')).map(button => [button.dataset.mobileQueue, button])
|
||||||
|
),
|
||||||
|
nextAction: qs('#mobile-queue-next-action'),
|
||||||
|
activeList: qs('#mobile-queue-active-list'),
|
||||||
|
planningList: qs('#mobile-queue-planning-list'),
|
||||||
|
allList: qs('#mobile-queue-all-list'),
|
||||||
|
activeSection: qs('#mobile-queue-active-list').parentElement,
|
||||||
});
|
});
|
||||||
|
renderMobileQueuePresentation = () => mobileQueueLauncher.renderPresentation();
|
||||||
|
renderMobileQueuePresentation();
|
||||||
|
qs('#mobile-queue-next-action').addEventListener('click', () => mobileQueueLauncher.continueWork());
|
||||||
function openMobileStartDay() {
|
function openMobileStartDay() {
|
||||||
followingQueue.load().catch(() => {});
|
followingQueue.load().catch(() => {});
|
||||||
const state = mobileStartDay.state();
|
const state = mobileStartDay.state();
|
||||||
|
|
@ -3530,6 +3544,7 @@
|
||||||
if (element) element.textContent = count;
|
if (element) element.textContent = count;
|
||||||
});
|
});
|
||||||
queueCounts = counts;
|
queueCounts = counts;
|
||||||
|
mobileQueueLauncher.renderPresentation();
|
||||||
mobileStartDay.reconcile({
|
mobileStartDay.reconcile({
|
||||||
authoritative:authoritativeMyWorkRefresh,
|
authoritative:authoritativeMyWorkRefresh,
|
||||||
authoritativePhases:['delivery'],
|
authoritativePhases:['delivery'],
|
||||||
|
|
|
||||||
|
|
@ -2174,23 +2174,39 @@
|
||||||
<button class="mobile-start-day-action" id="mobile-start-day-action" type="button">Prepare Today</button>
|
<button class="mobile-start-day-action" id="mobile-start-day-action" type="button">Prepare Today</button>
|
||||||
<button class="mobile-start-day-finish" id="finish-mobile-start-day" type="button" hidden>Finish for now</button>
|
<button class="mobile-start-day-finish" id="finish-mobile-start-day" type="button" hidden>Finish for now</button>
|
||||||
</section>
|
</section>
|
||||||
<div class="mobile-queue-list">
|
<section class="mobile-queue-next" aria-labelledby="mobile-queue-next-heading">
|
||||||
<button data-mobile-queue="today" type="button"><span><strong>Today</strong><small>Planned work</small></span><span data-mobile-queue-count="today">0</span></button>
|
<p class="small muted" id="mobile-queue-next-heading">Next up</p>
|
||||||
<button data-mobile-queue="tomorrow" type="button"><span><strong>Tomorrow</strong><small id="mobile-tomorrow-summary" aria-live="polite">Nothing planned</small></span></button>
|
<button id="mobile-queue-next-action" type="button">Find Work</button>
|
||||||
<button data-mobile-queue="week" type="button"><span><strong>Week Ahead</strong><small id="mobile-week-summary" aria-live="polite">Nothing planned</small></span></button>
|
</section>
|
||||||
<button data-mobile-queue="agenda" type="button"><span><strong>Agenda</strong><small>Upcoming deadlines</small></span><span data-mobile-queue-count="agenda">0</span></button>
|
<section class="mobile-queue-group" aria-labelledby="mobile-queue-active-heading" hidden>
|
||||||
<button data-mobile-queue="delivery" type="button"><span><strong>Delivery</strong><small>Needs recovery</small></span><span data-mobile-queue-count="delivery">0</span></button>
|
<h3 id="mobile-queue-active-heading">Active now</h3>
|
||||||
<button data-mobile-queue="gate" type="button"><span><strong>Human Gates</strong><small>Release decisions</small></span><span data-mobile-queue-count="gate">0</span></button>
|
<div class="mobile-queue-list" id="mobile-queue-active-list"></div>
|
||||||
<button data-mobile-queue="attention" type="button"><span><strong>Attention</strong><small>Needs a response</small></span><span data-mobile-queue-count="attention">0</span></button>
|
</section>
|
||||||
<button data-mobile-queue="update" type="button" aria-label="Updates, 0 unread conversations"><span><strong>Updates</strong><small>Unread conversations</small></span><span data-mobile-queue-count="update">0</span></button>
|
<section class="mobile-queue-group" aria-labelledby="mobile-queue-planning-heading">
|
||||||
<button data-mobile-queue="following" type="button" aria-label="Following, 0 unseen changes"><span><strong>Following</strong><small>Issues and pull requests you watch</small></span><span data-mobile-queue-count="following">0</span></button>
|
<h3 id="mobile-queue-planning-heading">Plan & organize</h3>
|
||||||
<button data-mobile-queue="filed" type="button"><span><strong>Filed</strong><small>Issues you delegated</small></span><span data-mobile-queue-count="filed">0</span></button>
|
<div class="mobile-queue-list" id="mobile-queue-planning-list">
|
||||||
<button data-mobile-queue="authored" type="button"><span><strong>My PRs</strong><small>Pull requests you authored</small></span><span data-mobile-queue-count="authored">0</span></button>
|
<button data-mobile-queue="today" type="button"><span><strong>Today</strong><small>Planned work</small></span><span data-mobile-queue-count="today">0</span></button>
|
||||||
<button data-mobile-queue="later" type="button"><span><strong>Later</strong><small>Deferred work</small></span><span data-mobile-queue-count="later">0</span></button>
|
<button data-mobile-queue="tomorrow" type="button"><span><strong>Tomorrow</strong><small id="mobile-tomorrow-summary" aria-live="polite">Nothing planned</small></span></button>
|
||||||
<button data-mobile-queue="draft" type="button"><span><strong>Drafts</strong><small>Unfiled captures</small></span><span data-mobile-queue-count="draft">0</span></button>
|
<button data-mobile-queue="week" type="button"><span><strong>Week Ahead</strong><small id="mobile-week-summary" aria-live="polite">Nothing planned</small></span></button>
|
||||||
<button data-mobile-queue="find" type="button"><span><strong>Find Work</strong><small>Claim something new</small></span></button>
|
</div>
|
||||||
<button data-mobile-queue="recaps" type="button"><span><strong>Recaps</strong><small>History</small></span></button>
|
</section>
|
||||||
</div>
|
<details class="mobile-queue-all">
|
||||||
|
<summary>All queues</summary>
|
||||||
|
<div class="mobile-queue-list" id="mobile-queue-all-list">
|
||||||
|
<button data-mobile-queue="agenda" type="button"><span><strong>Agenda</strong><small>Upcoming deadlines</small></span><span data-mobile-queue-count="agenda">0</span></button>
|
||||||
|
<button data-mobile-queue="delivery" type="button"><span><strong>Delivery</strong><small>Needs recovery</small></span><span data-mobile-queue-count="delivery">0</span></button>
|
||||||
|
<button data-mobile-queue="gate" type="button"><span><strong>Human Gates</strong><small>Release decisions</small></span><span data-mobile-queue-count="gate">0</span></button>
|
||||||
|
<button data-mobile-queue="attention" type="button"><span><strong>Attention</strong><small>Needs a response</small></span><span data-mobile-queue-count="attention">0</span></button>
|
||||||
|
<button data-mobile-queue="update" type="button" aria-label="Updates, 0 unread conversations"><span><strong>Updates</strong><small>Unread conversations</small></span><span data-mobile-queue-count="update">0</span></button>
|
||||||
|
<button data-mobile-queue="following" type="button" aria-label="Following, 0 unseen changes"><span><strong>Following</strong><small>Issues and pull requests you watch</small></span><span data-mobile-queue-count="following">0</span></button>
|
||||||
|
<button data-mobile-queue="filed" type="button"><span><strong>Filed</strong><small>Issues you delegated</small></span><span data-mobile-queue-count="filed">0</span></button>
|
||||||
|
<button data-mobile-queue="authored" type="button"><span><strong>My PRs</strong><small>Pull requests you authored</small></span><span data-mobile-queue-count="authored">0</span></button>
|
||||||
|
<button data-mobile-queue="later" type="button"><span><strong>Later</strong><small>Deferred work</small></span><span data-mobile-queue-count="later">0</span></button>
|
||||||
|
<button data-mobile-queue="draft" type="button"><span><strong>Drafts</strong><small>Unfiled captures</small></span><span data-mobile-queue-count="draft">0</span></button>
|
||||||
|
<button data-mobile-queue="find" type="button"><span><strong>Find Work</strong><small>Claim something new</small></span></button>
|
||||||
|
<button data-mobile-queue="recaps" type="button"><span><strong>Recaps</strong><small>History</small></span></button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</section>
|
</section>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@
|
||||||
['later', 'Start Later'],
|
['later', 'Start Later'],
|
||||||
['draft', 'Open Drafts'],
|
['draft', 'Open Drafts'],
|
||||||
];
|
];
|
||||||
|
const activeQueueNames = continuation.map(([name]) => name).concat(['following', 'authored']);
|
||||||
|
const allQueues = [
|
||||||
|
'today', 'tomorrow', 'week', 'agenda', 'delivery', 'gate', 'attention',
|
||||||
|
'update', 'following', 'filed', 'authored', 'later', 'draft', 'find', 'recaps',
|
||||||
|
];
|
||||||
|
|
||||||
function recommend() {
|
function recommend() {
|
||||||
const counts = options.getCounts ? options.getCounts() : {};
|
const counts = options.getCounts ? options.getCounts() : {};
|
||||||
|
|
@ -29,6 +34,56 @@
|
||||||
return {name, count, label: label + ' (' + count + ')'};
|
return {name, count, label: label + ' (' + count + ')'};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function presentation() {
|
||||||
|
const counts = options.getCounts ? options.getCounts() : {};
|
||||||
|
const active = activeQueueNames
|
||||||
|
.map(name => ({name, count: Math.max(0, Number(counts[name]) || 0)}))
|
||||||
|
.filter(item => item.count > 0);
|
||||||
|
activeQueueNames.forEach(name => {
|
||||||
|
if (counts[name + 'Unavailable'] && !active.some(item => item.name === name)) {
|
||||||
|
active.push({name, unavailable: true});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
nextUp: recommend(),
|
||||||
|
active,
|
||||||
|
planning: ['today', 'tomorrow', 'week'],
|
||||||
|
all: allQueues.slice(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPresentation() {
|
||||||
|
const view = presentation();
|
||||||
|
const planning = new Set(view.planning);
|
||||||
|
const active = view.active.filter(item => !planning.has(item.name));
|
||||||
|
const activeNames = new Set(active.map(item => item.name));
|
||||||
|
if (options.nextAction) {
|
||||||
|
options.nextAction.textContent = view.nextUp.label;
|
||||||
|
options.nextAction.dataset.queue = view.nextUp.name;
|
||||||
|
options.nextAction.setAttribute('aria-label', 'Next up: ' + view.nextUp.label);
|
||||||
|
}
|
||||||
|
active.forEach(item => {
|
||||||
|
const row = options.rows?.[item.name];
|
||||||
|
if (!row) return;
|
||||||
|
if (item.unavailable) row.setAttribute('data-unavailable', 'true');
|
||||||
|
else row.removeAttribute('data-unavailable');
|
||||||
|
options.activeList?.append(row);
|
||||||
|
});
|
||||||
|
view.planning.forEach(name => {
|
||||||
|
const row = options.rows?.[name];
|
||||||
|
if (row) options.planningList?.append(row);
|
||||||
|
});
|
||||||
|
view.all.forEach(name => {
|
||||||
|
if (planning.has(name) || activeNames.has(name)) return;
|
||||||
|
const row = options.rows?.[name];
|
||||||
|
if (!row) return;
|
||||||
|
row.removeAttribute('data-unavailable');
|
||||||
|
options.allList?.append(row);
|
||||||
|
});
|
||||||
|
if (options.activeSection) options.activeSection.hidden = active.length === 0;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
function open(name) {
|
function open(name) {
|
||||||
if (name === 'delivery' && options.openDelivery) return options.openDelivery();
|
if (name === 'delivery' && options.openDelivery) return options.openDelivery();
|
||||||
if (name === 'gate' && options.openHumanGates) return options.openHumanGates();
|
if (name === 'gate' && options.openHumanGates) return options.openHumanGates();
|
||||||
|
|
@ -56,5 +111,5 @@
|
||||||
return open(next.name);
|
return open(next.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { open, recommend, continueWork };
|
return { open, recommend, presentation, renderPresentation, continueWork };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
61
tests/e2e/test_adaptive_mobile_queues_release.py
Normal file
61
tests/e2e/test_adaptive_mobile_queues_release.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1":
|
||||||
|
pytest.skip("packaged adaptive Queues 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": 375, "height": 667},
|
||||||
|
{"width": 430, "height": 932},
|
||||||
|
])
|
||||||
|
def test_adaptive_queues_put_truthful_next_action_above_the_fold(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-queue-launcher.js")
|
||||||
|
page.evaluate("""() => {
|
||||||
|
const rows = Object.fromEntries(Array.from(document.querySelectorAll('[data-mobile-queue]'))
|
||||||
|
.map(row => [row.dataset.mobileQueue, row]));
|
||||||
|
window.adaptiveQueueLauncher = createMobileQueueLauncher({
|
||||||
|
getCounts:() => ({delivery:1, gate:2, today:3, attentionUnavailable:true}),
|
||||||
|
rows,
|
||||||
|
nextAction:document.querySelector('#mobile-queue-next-action'),
|
||||||
|
activeList:document.querySelector('#mobile-queue-active-list'),
|
||||||
|
planningList:document.querySelector('#mobile-queue-planning-list'),
|
||||||
|
allList:document.querySelector('#mobile-queue-all-list'),
|
||||||
|
activeSection:document.querySelector('#mobile-queue-active-list').parentElement,
|
||||||
|
});
|
||||||
|
window.adaptiveQueueLauncher.renderPresentation();
|
||||||
|
document.querySelector('#mobile-queue-sheet').showModal();
|
||||||
|
}""")
|
||||||
|
|
||||||
|
next_action = page.locator("#mobile-queue-next-action")
|
||||||
|
expect(next_action).to_be_visible()
|
||||||
|
expect(next_action).to_have_text("Recover Delivery (1)")
|
||||||
|
bounds = next_action.bounding_box()
|
||||||
|
assert bounds and bounds["height"] >= 44
|
||||||
|
assert bounds["x"] >= 0 and bounds["x"] + bounds["width"] <= viewport["width"]
|
||||||
|
assert bounds["y"] + bounds["height"] <= viewport["height"]
|
||||||
|
|
||||||
|
assert page.locator("#mobile-queue-active-list [data-mobile-queue]").evaluate_all(
|
||||||
|
"rows => rows.map(row => row.dataset.mobileQueue)"
|
||||||
|
) == ["delivery", "gate", "attention"]
|
||||||
|
expect(page.locator('[data-mobile-queue="attention"]')).to_have_attribute("data-unavailable", "true")
|
||||||
|
assert page.locator("#mobile-queue-planning-list [data-mobile-queue]").evaluate_all(
|
||||||
|
"rows => rows.map(row => row.dataset.mobileQueue)"
|
||||||
|
) == ["today", "tomorrow", "week"]
|
||||||
|
expect(page.locator(".mobile-queue-all")).not_to_have_attribute("open", "")
|
||||||
|
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||||
|
browser.close()
|
||||||
|
|
@ -279,6 +279,27 @@ process.stdout.write(JSON.stringify({{calls, open:queues.open}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_mobile_queue_sheet_prioritizes_next_active_and_planning_without_duplicate_rows():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert 'aria-labelledby="mobile-queue-next-heading"' in html
|
||||||
|
assert 'id="mobile-queue-next-action"' in html
|
||||||
|
assert 'aria-labelledby="mobile-queue-active-heading"' in html
|
||||||
|
assert 'id="mobile-queue-active-list"' in html
|
||||||
|
assert 'aria-labelledby="mobile-queue-planning-heading"' in html
|
||||||
|
assert 'id="mobile-queue-planning-list"' in html
|
||||||
|
assert '<details class="mobile-queue-all"' in html
|
||||||
|
assert 'id="mobile-queue-all-list"' in html
|
||||||
|
assert "nextAction: qs('#mobile-queue-next-action')" in html
|
||||||
|
assert "activeList: qs('#mobile-queue-active-list')" in html
|
||||||
|
assert "mobileQueueLauncher.renderPresentation();" in html
|
||||||
|
assert "queueCounts.followingUnavailable = status === 'error';\n renderMobileQueuePresentation();" in html
|
||||||
|
assert "mobileQueueLauncher.continueWork()" in html
|
||||||
|
for name in ("today", "tomorrow", "week", "agenda", "delivery", "gate", "attention", "update", "following", "filed", "authored", "later", "draft", "find", "recaps"):
|
||||||
|
assert html.count(f'<button data-mobile-queue="{name}"') == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing():
|
async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -731,6 +752,73 @@ process.stdout.write(JSON.stringify({{first, agenda, opened, fallback, calls}}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_queue_launcher_builds_truthful_next_up_and_active_sections():
|
||||||
|
script = f"""
|
||||||
|
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
|
||||||
|
const counts = {{delivery:1, gate:2, today:3, authored:4, update:0, attentionUnavailable:true, followingUnavailable:true}};
|
||||||
|
const launcher = createLauncher({{getCounts:() => counts}});
|
||||||
|
process.stdout.write(JSON.stringify(launcher.presentation()));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"nextUp": {"name": "delivery", "count": 1, "label": "Recover Delivery (1)"},
|
||||||
|
"active": [
|
||||||
|
{"name": "delivery", "count": 1},
|
||||||
|
{"name": "gate", "count": 2},
|
||||||
|
{"name": "today", "count": 3},
|
||||||
|
{"name": "authored", "count": 4},
|
||||||
|
{"name": "attention", "unavailable": True},
|
||||||
|
{"name": "following", "unavailable": True},
|
||||||
|
],
|
||||||
|
"planning": ["today", "tomorrow", "week"],
|
||||||
|
"all": [
|
||||||
|
"today", "tomorrow", "week", "agenda", "delivery", "gate",
|
||||||
|
"attention", "update", "following", "filed", "authored", "later",
|
||||||
|
"draft", "find", "recaps",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_queue_launcher_renders_one_set_of_rows_into_adaptive_groups():
|
||||||
|
script = f"""
|
||||||
|
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
|
||||||
|
class Box {{
|
||||||
|
constructor(name) {{ this.name=name; this.children=[]; this.hidden=false; this.attributes={{}}; this.textContent=''; this.dataset={{}}; }}
|
||||||
|
append(row) {{ if (row.parent) row.parent.children=row.parent.children.filter(item => item !== row); this.children.push(row); row.parent=this; }}
|
||||||
|
setAttribute(name, value) {{ this.attributes[name]=value; }}
|
||||||
|
removeAttribute(name) {{ delete this.attributes[name]; }}
|
||||||
|
}}
|
||||||
|
const names=['today','tomorrow','week','agenda','delivery','gate','attention','update','following','filed','authored','later','draft','find','recaps'];
|
||||||
|
const rows=Object.fromEntries(names.map(name => [name,new Box(name)]));
|
||||||
|
const nextAction=new Box('next');
|
||||||
|
const activeList=new Box('active'); const planningList=new Box('planning'); const allList=new Box('all');
|
||||||
|
const activeSection=new Box('active-section');
|
||||||
|
const launcher=createLauncher({{
|
||||||
|
getCounts:()=>({{delivery:1,gate:2,today:3,attentionUnavailable:true}}), rows,
|
||||||
|
nextAction, activeList, planningList, allList, activeSection,
|
||||||
|
}});
|
||||||
|
launcher.renderPresentation();
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
next:[nextAction.textContent,nextAction.dataset.queue,nextAction.attributes['aria-label']],
|
||||||
|
active:activeList.children.map(row => [row.name,row.attributes['data-unavailable'] || null]),
|
||||||
|
planning:planningList.children.map(row => row.name),
|
||||||
|
all:allList.children.map(row => row.name), activeHidden:activeSection.hidden,
|
||||||
|
}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"next": ["Recover Delivery (1)", "delivery", "Next up: Recover Delivery (1)"],
|
||||||
|
"active": [["delivery", None], ["gate", None], ["attention", "true"]],
|
||||||
|
"planning": ["today", "tomorrow", "week"],
|
||||||
|
"all": ["agenda", "update", "following", "filed", "authored", "later", "draft", "find", "recaps"],
|
||||||
|
"activeHidden": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_queue_launcher_prioritizes_updates_before_agenda_and_resumes_launcher():
|
def test_mobile_queue_launcher_prioritizes_updates_before_agenda_and_resumes_launcher():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
|
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user