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() @pytest.mark.parametrize("viewport", [ {"width": 320, "height": 568}, {"width": 375, "height": 667}, {"width": 430, "height": 932}, ]) @pytest.mark.parametrize(("queue", "count", "label", "destination"), [ ("following", 3, "Review Following (3)", ["following"]), ("authored", 2, "Open My PRs (2)", ["filter:authored", "open:authored"]), ]) def test_adaptive_queues_open_existing_following_and_authored_work(viewport, queue, count, label, destination): 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("""({queue, count}) => { const rows = Object.fromEntries(Array.from(document.querySelectorAll('[data-mobile-queue]')) .map(row => [row.dataset.mobileQueue, row])); window.destinations = []; window.adaptiveQueueLauncher = createMobileQueueLauncher({ getCounts:() => ({[queue]:count}), 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, openFollowing:() => window.destinations.push('following'), selectFilter:name => window.destinations.push('filter:' + name), firstAction:name => name === 'authored' ? {click:() => window.destinations.push('open:authored')} : null, announce:message => window.destinations.push('announce:' + message), }); window.adaptiveQueueLauncher.renderPresentation(); document.querySelector('#mobile-queue-next-action').addEventListener( 'click', () => window.adaptiveQueueLauncher.continueWork() ); document.querySelector('#mobile-queue-sheet').showModal(); }""", {"queue": queue, "count": count}) next_action = page.locator("#mobile-queue-next-action") expect(next_action).to_be_visible() expect(next_action).to_have_text(label) expect(next_action).to_have_attribute("aria-label", "Next up: " + label) bounds = next_action.bounding_box() assert bounds and bounds["height"] >= 44 assert bounds["y"] + bounds["height"] <= viewport["height"] assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") assert page.locator("#mobile-queue-active-list [data-mobile-queue]").evaluate_all( "rows => rows.map(row => row.dataset.mobileQueue)" ) == [queue] next_action.focus() next_action.press("Enter") assert page.evaluate("window.destinations") == destination browser.close() def test_adaptive_queues_keep_start_continue_actionable_offline_on_phone(): viewport = {"width": 390, "height": 844} 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.online = false; window.destinations = []; window.adaptiveQueueLauncher = createMobileQueueLauncher({ getCounts:() => ({delivery:1, gate:2, today:3}), isOnline:() => window.online, 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, openToday:() => window.destinations.push('today'), }); window.adaptiveQueueLauncher.renderPresentation(); document.querySelector('#mobile-queue-next-action').addEventListener( 'click', () => window.adaptiveQueueLauncher.continueWork() ); 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("Continue Today (3)") next_action.press("Enter") assert page.evaluate("window.destinations") == ["today"] page.evaluate("""() => { window.online = true; window.adaptiveQueueLauncher.renderPresentation(); }""") expect(next_action).to_have_text("Recover Delivery (1)") bounds = next_action.bounding_box() assert bounds and bounds["height"] >= 44 assert bounds["y"] + bounds["height"] <= viewport["height"] assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") browser.close() @pytest.mark.parametrize("viewport", [ {"width": 320, "height": 568}, {"width": 375, "height": 667}, {"width": 430, "height": 932}, ]) def test_operator_reorders_routine_queues_without_leaking_priority_between_accounts(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-priority.js") page.add_script_tag(path=FRONTEND / "mobile-queue-launcher.js") page.evaluate("""() => { const values = new Map(); window.login = 'alice'; const rows = Object.fromEntries(Array.from(document.querySelectorAll('[data-mobile-queue]')) .map(row => [row.dataset.mobileQueue, row])); window.launcher = createMobileQueueLauncher({ getCounts:() => ({attention:2, following:3, authored:1}), getRoutineOrder:() => window.priority.getOrder(), 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.priority = createMobileQueuePriority({ storage:{getItem:key => values.get(key) || null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}, getLogin:() => window.login, document, list:document.querySelector('#mobile-queue-priority-list'), resetButton:document.querySelector('#reset-mobile-queue-priority'), status:document.querySelector('#mobile-queue-priority-status'), labels:{attention:'Attention',today:'Today',update:'Updates',agenda:'Agenda',following:'Following',authored:'My PRs',filed:'Filed',later:'Later',draft:'Drafts'}, onChange:() => window.launcher.renderPresentation(), }); window.priority.start(); window.launcher.renderPresentation(); document.querySelector('#mobile-queue-priority').open = true; document.querySelector('#mobile-queue-sheet').showModal(); }""") for _ in range(4): page.get_by_role("button", name="Move Following earlier").click() expect(page.locator("#mobile-queue-next-action")).to_have_text("Review Following (3)") assert page.locator("#mobile-queue-active-list [data-mobile-queue]").evaluate_all( "rows => rows.map(row => row.dataset.mobileQueue)" ) == ["following", "attention", "authored"] expect(page.locator("#mobile-queue-priority-status")).to_have_text("Sync pending.") controls = page.locator(".mobile-queue-priority-controls button") assert controls.count() == 18 assert all((controls.nth(i).bounding_box() or {}).get("height", 0) >= 44 for i in range(controls.count())) assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") page.evaluate("""() => { window.login = 'bob'; window.priority.render(); window.launcher.renderPresentation(); }""") expect(page.locator("#mobile-queue-next-action")).to_have_text("Start Attention (2)") assert page.locator("#mobile-queue-priority-list [data-queue-priority]").evaluate_all( "rows => rows.map(row => row.dataset.queuePriority)" )[:3] == ["attention", "today", "update"] browser.close()