Make Following and My PRs actionable from adaptive mobile Work #1457
|
|
@ -15,11 +15,13 @@
|
||||||
['today', 'Continue Today'],
|
['today', 'Continue Today'],
|
||||||
['update', 'Resume Updates'],
|
['update', 'Resume Updates'],
|
||||||
['agenda', 'Open Agenda'],
|
['agenda', 'Open Agenda'],
|
||||||
|
['following', 'Review Following'],
|
||||||
|
['authored', 'Open My PRs'],
|
||||||
['filed', 'Review Filed'],
|
['filed', 'Review Filed'],
|
||||||
['later', 'Start Later'],
|
['later', 'Start Later'],
|
||||||
['draft', 'Open Drafts'],
|
['draft', 'Open Drafts'],
|
||||||
];
|
];
|
||||||
const activeQueueNames = continuation.map(([name]) => name).concat(['following', 'authored']);
|
const activeQueueNames = continuation.map(([name]) => name);
|
||||||
const allQueues = [
|
const allQueues = [
|
||||||
'today', 'tomorrow', 'week', 'agenda', 'delivery', 'gate', 'attention',
|
'today', 'tomorrow', 'week', 'agenda', 'delivery', 'gate', 'attention',
|
||||||
'update', 'following', 'filed', 'authored', 'later', 'draft', 'find', 'recaps',
|
'update', 'following', 'filed', 'authored', 'later', 'draft', 'find', 'recaps',
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,10 @@
|
||||||
const text = {
|
const text = {
|
||||||
continue:'Continue', resume:'Resume', start:'Start', plan:'Plan', find:'Find',
|
continue:'Continue', resume:'Resume', start:'Start', plan:'Plan', find:'Find',
|
||||||
prepare:'Prepare', 'prepare-resume':'Resume prep',
|
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';
|
}[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;
|
if (options.workLabel) options.workLabel.textContent = text;
|
||||||
const actionLabel = mode === 'prepare' ? 'Prepare Today' :
|
const actionLabel = mode === 'prepare' ? 'Prepare Today' :
|
||||||
mode === 'prepare-resume' ? 'Resume preparation' :
|
mode === 'prepare-resume' ? 'Resume preparation' :
|
||||||
|
|
|
||||||
|
|
@ -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", "")
|
expect(page.locator(".mobile-queue-all")).not_to_have_attribute("open", "")
|
||||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||||
browser.close()
|
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()
|
||||||
|
|
|
||||||
|
|
@ -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"
|
launcher = ROOT / "frontend" / "mobile-queue-launcher.js"
|
||||||
script = f"""
|
script = f"""
|
||||||
const createLauncher = require({json.dumps(str(launcher))});
|
const createLauncher = require({json.dumps(str(launcher))});
|
||||||
|
|
@ -293,7 +293,7 @@ process.stdout.write(JSON.stringify({{opened:feature.open('following'),recommend
|
||||||
).stdout)
|
).stdout)
|
||||||
assert result == {
|
assert result == {
|
||||||
"opened": "opened-following",
|
"opened": "opened-following",
|
||||||
"recommended": {"name": "find", "count": 0, "label": "Find Work"},
|
"recommended": {"name": "following", "count": 7, "label": "Review Following (7)"},
|
||||||
"calls": ["following"],
|
"calls": ["following"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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():
|
def test_mobile_queue_launcher_recommends_and_revalidates_cross_queue_continuation():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
|
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 workLabel = {{textContent:''}};
|
||||||
const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}});
|
const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}});
|
||||||
const labels = {{}};
|
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);
|
dock.updateWork(mode);
|
||||||
labels[mode] = [workLabel.textContent, work.attributes['aria-label']];
|
labels[mode] = [workLabel.textContent, work.attributes['aria-label']];
|
||||||
}}
|
}}
|
||||||
|
|
@ -987,6 +1023,8 @@ process.stdout.write(JSON.stringify(labels));
|
||||||
"attention": ["Attention", "Open Attention"],
|
"attention": ["Attention", "Open Attention"],
|
||||||
"update": ["Updates", "Resume Updates"],
|
"update": ["Updates", "Resume Updates"],
|
||||||
"agenda": ["Agenda", "Open Agenda"],
|
"agenda": ["Agenda", "Open Agenda"],
|
||||||
|
"following": ["Following", "Open Following"],
|
||||||
|
"authored": ["My PRs", "Open My PRs"],
|
||||||
"filed": ["Filed", "Open Filed"],
|
"filed": ["Filed", "Open Filed"],
|
||||||
"later": ["Later", "Open Later"],
|
"later": ["Later", "Open Later"],
|
||||||
"draft": ["Drafts", "Open Drafts"],
|
"draft": ["Drafts", "Open Drafts"],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user