diff --git a/frontend/mobile-queue-launcher.js b/frontend/mobile-queue-launcher.js index b54623f..c9edb36 100644 --- a/frontend/mobile-queue-launcher.js +++ b/frontend/mobile-queue-launcher.js @@ -15,11 +15,13 @@ ['today', 'Continue Today'], ['update', 'Resume Updates'], ['agenda', 'Open Agenda'], + ['following', 'Review Following'], + ['authored', 'Open My PRs'], ['filed', 'Review Filed'], ['later', 'Start Later'], ['draft', 'Open Drafts'], ]; - const activeQueueNames = continuation.map(([name]) => name).concat(['following', 'authored']); + const activeQueueNames = continuation.map(([name]) => name); const allQueues = [ 'today', 'tomorrow', 'week', 'agenda', 'delivery', 'gate', 'attention', 'update', 'following', 'filed', 'authored', 'later', 'draft', 'find', 'recaps', diff --git a/frontend/mobile-task-dock.js b/frontend/mobile-task-dock.js index 8fc374e..2eeb170 100644 --- a/frontend/mobile-task-dock.js +++ b/frontend/mobile-task-dock.js @@ -70,9 +70,10 @@ const text = { continue:'Continue', resume:'Resume', start:'Start', plan:'Plan', find:'Find', prepare:'Prepare', 'prepare-resume':'Resume prep', - delivery:'Delivery', attention:'Attention', update:'Updates', agenda:'Agenda', filed:'Filed', later:'Later', draft:'Drafts', + delivery:'Delivery', attention:'Attention', update:'Updates', agenda:'Agenda', + following:'Following', authored:'My PRs', filed:'Filed', later:'Later', draft:'Drafts', }[mode] || 'Work'; - const queue = ['Delivery', 'Attention', 'Updates', 'Agenda', 'Filed', 'Later', 'Drafts'].includes(text); + const queue = ['Delivery', 'Attention', 'Updates', 'Agenda', 'Following', 'My PRs', 'Filed', 'Later', 'Drafts'].includes(text); if (options.workLabel) options.workLabel.textContent = text; const actionLabel = mode === 'prepare' ? 'Prepare Today' : mode === 'prepare-resume' ? 'Resume preparation' : diff --git a/tests/e2e/test_adaptive_mobile_queues_release.py b/tests/e2e/test_adaptive_mobile_queues_release.py index 57d93e3..c9788b0 100644 --- a/tests/e2e/test_adaptive_mobile_queues_release.py +++ b/tests/e2e/test_adaptive_mobile_queues_release.py @@ -59,3 +59,61 @@ def test_adaptive_queues_put_truthful_next_action_above_the_fold(viewport): 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() diff --git a/tests/test_following_frontend.py b/tests/test_following_frontend.py index 11f1b6e..237207b 100644 --- a/tests/test_following_frontend.py +++ b/tests/test_following_frontend.py @@ -275,7 +275,7 @@ const feature=createFollowing({{ ] -def test_following_opens_explicitly_but_never_becomes_work_recommendation(): +def test_following_opens_explicitly_and_becomes_work_recommendation(): launcher = ROOT / "frontend" / "mobile-queue-launcher.js" script = f""" const createLauncher = require({json.dumps(str(launcher))}); @@ -293,7 +293,7 @@ process.stdout.write(JSON.stringify({{opened:feature.open('following'),recommend ).stdout) assert result == { "opened": "opened-following", - "recommended": {"name": "find", "count": 0, "label": "Find Work"}, + "recommended": {"name": "following", "count": 7, "label": "Review Following (7)"}, "calls": ["following"], } diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py index 683af41..9c03a8f 100644 --- a/tests/test_mobile_task_dock.py +++ b/tests/test_mobile_task_dock.py @@ -797,6 +797,42 @@ process.stdout.write(JSON.stringify({{recommendation, opened, calls}})); } +def test_mobile_work_continues_following_then_authored_before_claiming_new_work(): + script = f""" +const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); +const calls = []; +let counts = {{following:3, authored:2, filed:1, later:4}}; +const launcher = createLauncher({{ + getCounts: () => counts, + openFollowing: () => {{ calls.push('following'); return 'opened-following'; }}, + selectFilter: name => calls.push('filter:' + name), + firstAction: name => name === 'authored' ? {{click() {{ calls.push('open:authored'); }}}} : null, + announce: message => calls.push('announce:' + message), + openFindWork: () => calls.push('find'), +}}); +const following = launcher.recommend(); +const openedFollowing = launcher.continueWork(); +counts = {{following:0, authored:2, filed:1, later:4}}; +const authored = launcher.recommend(); +const openedAuthored = launcher.continueWork(); +counts = {{following:0, authored:0, filed:0, later:0}}; +const fallback = launcher.recommend(); +launcher.continueWork(); +process.stdout.write(JSON.stringify({{following, openedFollowing, authored, openedAuthored, fallback, calls}})); +""" + result = subprocess.run(["node", "-e", script], capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == { + "following": {"name": "following", "count": 3, "label": "Review Following (3)"}, + "openedFollowing": "opened-following", + "authored": {"name": "authored", "count": 2, "label": "Open My PRs (2)"}, + "openedAuthored": "opened", + "fallback": {"name": "find", "count": 0, "label": "Find Work"}, + "calls": ["following", "filter:authored", "open:authored", "find"], + } + + def test_mobile_queue_launcher_recommends_and_revalidates_cross_queue_continuation(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); @@ -973,7 +1009,7 @@ const work = {{attributes: {{}}, setAttribute(name, value) {{ this.attributes[na const workLabel = {{textContent:''}}; const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}}); const labels = {{}}; -for (const mode of ['delivery','attention','update','agenda','filed','later','draft']) {{ +for (const mode of ['delivery','attention','update','agenda','following','authored','filed','later','draft']) {{ dock.updateWork(mode); labels[mode] = [workLabel.textContent, work.attributes['aria-label']]; }} @@ -987,6 +1023,8 @@ process.stdout.write(JSON.stringify(labels)); "attention": ["Attention", "Open Attention"], "update": ["Updates", "Resume Updates"], "agenda": ["Agenda", "Open Agenda"], + "following": ["Following", "Open Following"], + "authored": ["My PRs", "Open My PRs"], "filed": ["Filed", "Open Filed"], "later": ["Later", "Open Later"], "draft": ["Drafts", "Open Drafts"],