feat: prioritize adaptive mobile queues
CI / lint (pull_request) Successful in 3m38s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Failing after 9m7s
CI / release-candidate (pull_request) Has been skipped

Closes #1423
This commit is contained in:
timmy committed 2026-08-26 07:12:27 +00:00
1 parent 1e8d9a27be
commit 8b2fd305ca
6 files changed
+264 -20

No files matched your search

@@ -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()
+88
View File
@@ -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
async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing():
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():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});