Compact and rank pinned mobile work
This commit is contained in:
parent
5f9b5b3a7f
commit
2766a75f24
|
|
@ -1555,6 +1555,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.mobile-queue-list .mobile-recent-work-row button { min-height:44px; width:auto; }
|
.mobile-queue-list .mobile-recent-work-row button { min-height:44px; width:auto; }
|
||||||
.mobile-queue-list .mobile-recent-work-row [data-recent-work-route] { min-width:0; width:100%; min-height:56px; overflow-wrap:anywhere; }
|
.mobile-queue-list .mobile-recent-work-row [data-recent-work-route] { min-width:0; width:100%; min-height:56px; overflow-wrap:anywhere; }
|
||||||
.mobile-queue-list .mobile-recent-work-row [data-recent-work-pin] { min-width:64px; justify-content:center; padding-inline:12px; }
|
.mobile-queue-list .mobile-recent-work-row [data-recent-work-pin] { min-width:64px; justify-content:center; padding-inline:12px; }
|
||||||
|
.mobile-pinned-work-toggle { min-height:44px; width:100%; margin-top:8px; }
|
||||||
.mobile-queue-list [data-mobile-queue-count] { min-width:28px; padding:3px 8px; border-radius:999px; text-align:center; background:#1d426d; }
|
.mobile-queue-list [data-mobile-queue-count] { min-width:28px; padding:3px 8px; border-radius:999px; text-align:center; background:#1d426d; }
|
||||||
.mobile-queue-list [data-mobile-queue="agenda"][data-deadlines="true"] { border-color:#f59e0b; background:#30240f; box-shadow:inset 3px 0 #f59e0b; }
|
.mobile-queue-list [data-mobile-queue="agenda"][data-deadlines="true"] { border-color:#f59e0b; background:#30240f; box-shadow:inset 3px 0 #f59e0b; }
|
||||||
.mobile-queue-list [data-recommended="true"] { border-color:#60a5fa; box-shadow:0 0 0 2px #60a5fa; }
|
.mobile-queue-list [data-recommended="true"] { border-color:#60a5fa; box-shadow:0 0 0 2px #60a5fa; }
|
||||||
|
|
|
||||||
|
|
@ -441,6 +441,7 @@
|
||||||
list:qs('#mobile-recent-work-list'),
|
list:qs('#mobile-recent-work-list'),
|
||||||
pinnedSection:qs('#mobile-pinned-work'),
|
pinnedSection:qs('#mobile-pinned-work'),
|
||||||
pinnedList:qs('#mobile-pinned-work-list'),
|
pinnedList:qs('#mobile-pinned-work-list'),
|
||||||
|
pinnedToggle:qs('#mobile-pinned-work-toggle'),
|
||||||
status:qs('#mobile-recent-work-status'),
|
status:qs('#mobile-recent-work-status'),
|
||||||
openRoute:fragment => {
|
openRoute:fragment => {
|
||||||
const sheet = qs('#mobile-queue-sheet');
|
const sheet = qs('#mobile-queue-sheet');
|
||||||
|
|
|
||||||
|
|
@ -2187,6 +2187,7 @@
|
||||||
<section class="mobile-queue-group" id="mobile-pinned-work" aria-labelledby="mobile-pinned-work-heading" hidden>
|
<section class="mobile-queue-group" id="mobile-pinned-work" aria-labelledby="mobile-pinned-work-heading" hidden>
|
||||||
<h3 id="mobile-pinned-work-heading">Pinned work</h3>
|
<h3 id="mobile-pinned-work-heading">Pinned work</h3>
|
||||||
<div class="mobile-queue-list" id="mobile-pinned-work-list"></div>
|
<div class="mobile-queue-list" id="mobile-pinned-work-list"></div>
|
||||||
|
<button class="mobile-pinned-work-toggle" id="mobile-pinned-work-toggle" type="button" aria-controls="mobile-pinned-work-list" aria-expanded="false" hidden>Show all</button>
|
||||||
</section>
|
</section>
|
||||||
<section class="mobile-queue-group" id="mobile-recent-work" aria-labelledby="mobile-recent-work-heading" hidden>
|
<section class="mobile-queue-group" id="mobile-recent-work" aria-labelledby="mobile-recent-work-heading" hidden>
|
||||||
<h3 id="mobile-recent-work-heading">Recent work</h3>
|
<h3 id="mobile-recent-work-heading">Recent work</h3>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@
|
||||||
let retryAccount = '';
|
let retryAccount = '';
|
||||||
let retryAttempt = 0;
|
let retryAttempt = 0;
|
||||||
let operationSequence = 0;
|
let operationSequence = 0;
|
||||||
|
let pinsExpanded = false;
|
||||||
|
|
||||||
|
options.pinnedToggle?.addEventListener?.('click', () => {
|
||||||
|
pinsExpanded = !pinsExpanded;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
function operationId() {
|
function operationId() {
|
||||||
operationSequence += 1;
|
operationSequence += 1;
|
||||||
|
|
@ -196,7 +202,9 @@
|
||||||
if (!accountKey || !normalized) return false;
|
if (!accountKey || !normalized) return false;
|
||||||
const current = read();
|
const current = read();
|
||||||
current.items = [normalized, ...current.items.filter(existing => existing.route !== normalized.route)].slice(0, limit);
|
current.items = [normalized, ...current.items.filter(existing => existing.route !== normalized.route)].slice(0, limit);
|
||||||
current.pinned = current.pinned.map(existing => existing.route === normalized.route ? normalized : existing);
|
current.pinned = current.pinned.some(existing => existing.route === normalized.route)
|
||||||
|
? [normalized, ...current.pinned.filter(existing => existing.route !== normalized.route)]
|
||||||
|
: current.pinned;
|
||||||
current.pending = [{...normalized, operationId:operationId()}, ...current.pending.filter(existing => existing.route !== normalized.route)].slice(0, limit);
|
current.pending = [{...normalized, operationId:operationId()}, ...current.pending.filter(existing => existing.route !== normalized.route)].slice(0, limit);
|
||||||
if (!persist(current, accountKey)) return false;
|
if (!persist(current, accountKey)) return false;
|
||||||
announce(current);
|
announce(current);
|
||||||
|
|
@ -374,7 +382,10 @@
|
||||||
button.setAttribute('type', 'button');
|
button.setAttribute('type', 'button');
|
||||||
button.setAttribute('data-recent-work-route', item.route);
|
button.setAttribute('data-recent-work-route', item.route);
|
||||||
button.setAttribute('aria-label', 'Open ' + item.title + ', ' + itemDetail.toLowerCase().replace(' · ', ' '));
|
button.setAttribute('aria-label', 'Open ' + item.title + ', ' + itemDetail.toLowerCase().replace(' · ', ' '));
|
||||||
button.addEventListener('click', () => options.openRoute?.(item.route));
|
button.addEventListener('click', () => {
|
||||||
|
if (isPinned) record(item);
|
||||||
|
options.openRoute?.(item.route);
|
||||||
|
});
|
||||||
action.textContent = isPinned ? 'Unpin' : 'Pin';
|
action.textContent = isPinned ? 'Unpin' : 'Pin';
|
||||||
action.setAttribute('type', 'button');
|
action.setAttribute('type', 'button');
|
||||||
action.setAttribute('data-recent-work-pin', isPinned ? 'unpin' : 'pin');
|
action.setAttribute('data-recent-work-pin', isPinned ? 'unpin' : 'pin');
|
||||||
|
|
@ -388,19 +399,28 @@
|
||||||
function render() {
|
function render() {
|
||||||
const recent = items();
|
const recent = items();
|
||||||
const fixed = pinned();
|
const fixed = pinned();
|
||||||
|
const fixedRoutes = new Set(fixed.map(item => item.route));
|
||||||
|
const visibleRecent = recent.filter(item => !fixedRoutes.has(item.route));
|
||||||
const list = options.list;
|
const list = options.list;
|
||||||
const section = options.section;
|
const section = options.section;
|
||||||
if (list && section && options.document) {
|
if (list && section && options.document) {
|
||||||
const rows = recent.map(item => row(item, false));
|
const rows = visibleRecent.map(item => row(item, false));
|
||||||
list.replaceChildren(...rows);
|
list.replaceChildren(...rows);
|
||||||
section.hidden = rows.length === 0;
|
section.hidden = rows.length === 0;
|
||||||
}
|
}
|
||||||
if (options.pinnedList && options.pinnedSection && options.document) {
|
if (options.pinnedList && options.pinnedSection && options.document) {
|
||||||
const rows = fixed.map(item => row(item, true));
|
const visiblePins = pinsExpanded ? fixed : fixed.slice(0, 3);
|
||||||
|
const rows = visiblePins.map(item => row(item, true));
|
||||||
options.pinnedList.replaceChildren(...rows);
|
options.pinnedList.replaceChildren(...rows);
|
||||||
options.pinnedSection.hidden = rows.length === 0;
|
options.pinnedSection.hidden = rows.length === 0;
|
||||||
}
|
}
|
||||||
return recent.length + fixed.length;
|
if (options.pinnedToggle) {
|
||||||
|
options.pinnedToggle.hidden = fixed.length <= 3;
|
||||||
|
options.pinnedToggle.textContent = pinsExpanded ? 'Show fewer' : 'Show all ' + fixed.length;
|
||||||
|
options.pinnedToggle.setAttribute('aria-expanded', pinsExpanded ? 'true' : 'false');
|
||||||
|
options.pinnedToggle.setAttribute('aria-controls', 'mobile-pinned-work-list');
|
||||||
|
}
|
||||||
|
return visibleRecent.length + fixed.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {items, pinned, record, pin, unpin, render, load, sync, startLifecycle, state};
|
return {items, pinned, record, pin, unpin, render, load, sync, startLifecycle, state};
|
||||||
|
|
|
||||||
|
|
@ -141,9 +141,9 @@ class RecentWorkStore:
|
||||||
state, _legacy = self._state(row, login)
|
state, _legacy = self._state(row, login)
|
||||||
state["items"] = [normalized, *(entry for entry in state["items"] if entry["route"] != normalized["route"])][: self.limit]
|
state["items"] = [normalized, *(entry for entry in state["items"] if entry["route"] != normalized["route"])][: self.limit]
|
||||||
state["pinned"] = [
|
state["pinned"] = [
|
||||||
normalized if entry["route"] == normalized["route"] else entry
|
normalized,
|
||||||
for entry in state["pinned"]
|
*(entry for entry in state["pinned"] if entry["route"] != normalized["route"]),
|
||||||
]
|
] if any(entry["route"] == normalized["route"] for entry in state["pinned"]) else state["pinned"]
|
||||||
self._write(connection, login, state)
|
self._write(connection, login, state)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,6 @@ def test_operator_pins_and_reopens_frequent_work_without_phone_overflow(viewport
|
||||||
getItem:key => values.get(key) || null,
|
getItem:key => values.get(key) || null,
|
||||||
setItem:(key, value) => values.set(key, value),
|
setItem:(key, value) => values.set(key, value),
|
||||||
};
|
};
|
||||||
const item = {
|
|
||||||
kind:'issue', repository:'stackchain/stackchain-dashboard', number:1477,
|
|
||||||
title:'Pin frequent work across signed-in mobile devices',
|
|
||||||
};
|
|
||||||
window.opened = [];
|
window.opened = [];
|
||||||
window.recentWork = createMobileRecentWork({
|
window.recentWork = createMobileRecentWork({
|
||||||
storage, getLogin:() => 'timmy', document,
|
storage, getLogin:() => 'timmy', document,
|
||||||
|
|
@ -41,32 +37,48 @@ def test_operator_pins_and_reopens_frequent_work_without_phone_overflow(viewport
|
||||||
list:document.querySelector('#mobile-recent-work-list'),
|
list:document.querySelector('#mobile-recent-work-list'),
|
||||||
pinnedSection:document.querySelector('#mobile-pinned-work'),
|
pinnedSection:document.querySelector('#mobile-pinned-work'),
|
||||||
pinnedList:document.querySelector('#mobile-pinned-work-list'),
|
pinnedList:document.querySelector('#mobile-pinned-work-list'),
|
||||||
|
pinnedToggle:document.querySelector('#mobile-pinned-work-toggle'),
|
||||||
status:document.querySelector('#mobile-recent-work-status'),
|
status:document.querySelector('#mobile-recent-work-status'),
|
||||||
openRoute:route => window.opened.push(route),
|
openRoute:route => window.opened.push(route),
|
||||||
});
|
});
|
||||||
window.recentWork.record(item);
|
for (let number=1; number<=20; number += 1) {
|
||||||
window.recentWork.pin(item);
|
const item = {
|
||||||
|
kind:'issue', repository:'stackchain/stackchain-dashboard', number,
|
||||||
|
title:'Pinned mobile work ' + number,
|
||||||
|
};
|
||||||
|
window.recentWork.record(item);
|
||||||
|
window.recentWork.pin(item);
|
||||||
|
}
|
||||||
document.querySelector('#mobile-queue-sheet').showModal();
|
document.querySelector('#mobile-queue-sheet').showModal();
|
||||||
}""")
|
}""")
|
||||||
|
|
||||||
expect(page.locator("#mobile-pinned-work")).to_be_visible()
|
expect(page.locator("#mobile-pinned-work")).to_be_visible()
|
||||||
expect(page.locator("#mobile-recent-work")).to_be_visible()
|
expect(page.locator("#mobile-recent-work")).to_be_hidden()
|
||||||
|
expect(page.locator("#mobile-pinned-work-list .mobile-recent-work-row")).to_have_count(3)
|
||||||
expect(page.locator("#mobile-recent-work-status")).to_have_text("Sync pending.")
|
expect(page.locator("#mobile-recent-work-status")).to_have_text("Sync pending.")
|
||||||
|
toggle = page.locator("#mobile-pinned-work-toggle")
|
||||||
open_button = page.get_by_role(
|
open_button = page.get_by_role(
|
||||||
"button", name="Open Pin frequent work across signed-in mobile devices, issue stackchain/stackchain-dashboard #1477"
|
"button", name="Open Pinned mobile work 20, issue stackchain/stackchain-dashboard #20"
|
||||||
).first
|
).first
|
||||||
pin_button = page.get_by_role(
|
pin_button = page.get_by_role(
|
||||||
"button", name="Unpin Pin frequent work across signed-in mobile devices"
|
"button", name="Unpin Pinned mobile work 20"
|
||||||
)
|
)
|
||||||
for control in (open_button, pin_button):
|
for control in (open_button, pin_button, toggle):
|
||||||
bounds = control.bounding_box()
|
bounds = control.bounding_box()
|
||||||
assert bounds and bounds["height"] >= 44
|
assert bounds and bounds["height"] >= 44
|
||||||
assert bounds["x"] >= 0 and bounds["x"] + bounds["width"] <= viewport["width"]
|
assert bounds["x"] >= 0 and bounds["x"] + bounds["width"] <= viewport["width"]
|
||||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||||
|
|
||||||
|
toggle.click()
|
||||||
|
expect(page.locator("#mobile-pinned-work-list .mobile-recent-work-row")).to_have_count(20)
|
||||||
|
expect(toggle).to_have_text("Show fewer")
|
||||||
|
assert toggle.get_attribute("aria-expanded") == "true"
|
||||||
|
toggle.click()
|
||||||
|
expect(page.locator("#mobile-pinned-work-list .mobile-recent-work-row")).to_have_count(3)
|
||||||
|
|
||||||
open_button.focus()
|
open_button.focus()
|
||||||
open_button.press("Enter")
|
open_button.press("Enter")
|
||||||
assert page.evaluate("window.opened") == [
|
assert page.evaluate("window.opened") == [
|
||||||
"#/my-work/issue/stackchain/stackchain-dashboard/1477"
|
"#/my-work/issue/stackchain/stackchain-dashboard/20"
|
||||||
]
|
]
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ const recent=createRecentWork({{
|
||||||
const pinnedImmediately=recent.pin(item);
|
const pinnedImmediately=recent.pin(item);
|
||||||
recent.render();
|
recent.render();
|
||||||
const immediate={{pinned:recent.pinned(),status:status.textContent,recentHidden:recentSection.hidden,pinnedHidden:pinnedSection.hidden,
|
const immediate={{pinned:recent.pinned(),status:status.textContent,recentHidden:recentSection.hidden,pinnedHidden:pinnedSection.hidden,
|
||||||
recentActions:recentList.children[0].children.map(child=>child.attributes),
|
recentRows:recentList.children.length,
|
||||||
pinnedActions:pinnedList.children[0].children.map(child=>child.attributes)}};
|
pinnedActions:pinnedList.children[0].children.map(child=>child.attributes)}};
|
||||||
await recent.sync();
|
await recent.sync();
|
||||||
pinnedList.children[0].children[0].click();
|
pinnedList.children[0].children[0].click();
|
||||||
|
|
@ -320,16 +320,16 @@ process.stdout.write(JSON.stringify({{pinnedImmediately,immediate,opened,unpinne
|
||||||
assert payload["pinnedImmediately"] is True
|
assert payload["pinnedImmediately"] is True
|
||||||
assert payload["immediate"]["pinned"][0]["number"] == 1477
|
assert payload["immediate"]["pinned"][0]["number"] == 1477
|
||||||
assert payload["immediate"]["status"] == "Sync pending."
|
assert payload["immediate"]["status"] == "Sync pending."
|
||||||
assert payload["immediate"]["recentHidden"] is False
|
assert payload["immediate"]["recentHidden"] is True
|
||||||
assert payload["immediate"]["pinnedHidden"] is False
|
assert payload["immediate"]["pinnedHidden"] is False
|
||||||
assert payload["immediate"]["recentActions"][0]["data-recent-work-route"].endswith("/1477")
|
assert payload["immediate"]["recentRows"] == 0
|
||||||
assert payload["immediate"]["recentActions"][1]["data-recent-work-pin"] == "pin"
|
|
||||||
assert payload["immediate"]["pinnedActions"][1]["data-recent-work-pin"] == "unpin"
|
assert payload["immediate"]["pinnedActions"][1]["data-recent-work-pin"] == "unpin"
|
||||||
assert payload["opened"] == ["#/my-work/issue/stackchain/dashboard/1477"]
|
assert payload["opened"] == ["#/my-work/issue/stackchain/dashboard/1477"]
|
||||||
assert payload["unpinnedImmediately"] is True
|
assert payload["unpinnedImmediately"] is True
|
||||||
assert payload["settled"] == []
|
assert payload["settled"] == []
|
||||||
assert payload["calls"] == [
|
assert payload["calls"] == [
|
||||||
["api/v1/recent-work/pin", "PUT", payload["immediate"]["pinned"][0]],
|
["api/v1/recent-work/pin", "PUT", payload["immediate"]["pinned"][0]],
|
||||||
|
["api/v1/recent-work", "POST", payload["immediate"]["pinned"][0]],
|
||||||
[
|
[
|
||||||
"api/v1/recent-work/pin",
|
"api/v1/recent-work/pin",
|
||||||
"DELETE",
|
"DELETE",
|
||||||
|
|
@ -338,6 +338,61 @@ process.stdout.write(JSON.stringify({{pinnedImmediately,immediate,opened,unpinne
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pinned_work_is_compact_deduplicated_and_promotes_on_open():
|
||||||
|
script = f"""
|
||||||
|
const createRecentWork = require({json.dumps(str(RECENT_WORK))});
|
||||||
|
const makeItem=number=>({{kind:'issue',repository:'stackchain/dashboard',number,title:'Issue '+number}});
|
||||||
|
const values=new Map([['stackchain.mobile-recent-work.v1.alice',JSON.stringify({{
|
||||||
|
items:[1,2,3,4,5].map(makeItem),pinned:[1,2,3,4,5].map(makeItem),pending:[],pinOps:[]
|
||||||
|
}})]]);
|
||||||
|
function node(tag) {{ return {{tag,children:[],attributes:{{}},listeners:{{}},hidden:false,textContent:'',
|
||||||
|
appendChild(child){{this.children.push(child);return child;}},
|
||||||
|
replaceChildren(...children){{this.children=children;}},
|
||||||
|
setAttribute(name,value){{this.attributes[name]=String(value);}},
|
||||||
|
addEventListener(name,callback){{this.listeners[name]=callback;}},
|
||||||
|
click(){{this.listeners.click?.();}}, focus(){{this.focused=true;}},
|
||||||
|
}}; }}
|
||||||
|
const recentList=node('div'), recentSection=node('section');
|
||||||
|
const pinnedList=node('div'), pinnedSection=node('section'), pinnedToggle=node('button');
|
||||||
|
const opened=[];
|
||||||
|
const recent=createRecentWork({{
|
||||||
|
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}},
|
||||||
|
getLogin:()=>'alice',document:{{createElement:node}},debounceMs:99999,
|
||||||
|
list:recentList,section:recentSection,pinnedList,pinnedSection,pinnedToggle,
|
||||||
|
openRoute:route=>opened.push(route),
|
||||||
|
}});
|
||||||
|
recent.render();
|
||||||
|
const collapsed={{pinnedRows:pinnedList.children.length,recentRows:recentList.children.length,
|
||||||
|
recentHidden:recentSection.hidden,toggleHidden:pinnedToggle.hidden,toggleText:pinnedToggle.textContent,
|
||||||
|
expanded:pinnedToggle.attributes['aria-expanded'],controls:pinnedToggle.attributes['aria-controls']}};
|
||||||
|
pinnedList.children[2].children[0].click();
|
||||||
|
const promoted=recent.pinned().map(item=>item.number);
|
||||||
|
pinnedToggle.click();
|
||||||
|
const expanded={{pinnedRows:pinnedList.children.length,toggleText:pinnedToggle.textContent,
|
||||||
|
ariaExpanded:pinnedToggle.attributes['aria-expanded']}};
|
||||||
|
process.stdout.write(JSON.stringify({{collapsed,promoted,expanded,opened,state:recent.state()}}));
|
||||||
|
"""
|
||||||
|
payload = run_node(script)
|
||||||
|
|
||||||
|
assert payload["collapsed"] == {
|
||||||
|
"pinnedRows": 3,
|
||||||
|
"recentRows": 0,
|
||||||
|
"recentHidden": True,
|
||||||
|
"toggleHidden": False,
|
||||||
|
"toggleText": "Show all 5",
|
||||||
|
"expanded": "false",
|
||||||
|
"controls": "mobile-pinned-work-list",
|
||||||
|
}
|
||||||
|
assert payload["promoted"] == [3, 1, 2, 4, 5]
|
||||||
|
assert payload["expanded"] == {
|
||||||
|
"pinnedRows": 5,
|
||||||
|
"toggleText": "Show fewer",
|
||||||
|
"ariaExpanded": "true",
|
||||||
|
}
|
||||||
|
assert payload["opened"] == ["#/my-work/issue/stackchain/dashboard/3"]
|
||||||
|
assert payload["state"] == {"pending": True, "pendingCount": 1}
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes():
|
def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes():
|
||||||
html = INDEX.read_text()
|
html = INDEX.read_text()
|
||||||
dashboard = DASHBOARD.read_text()
|
dashboard = DASHBOARD.read_text()
|
||||||
|
|
@ -347,6 +402,8 @@ def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes():
|
||||||
assert 'id="mobile-recent-work-list"' in html
|
assert 'id="mobile-recent-work-list"' in html
|
||||||
assert 'id="mobile-pinned-work"' in html
|
assert 'id="mobile-pinned-work"' in html
|
||||||
assert 'id="mobile-pinned-work-list"' in html
|
assert 'id="mobile-pinned-work-list"' in html
|
||||||
|
assert 'id="mobile-pinned-work-toggle"' in html
|
||||||
|
assert 'aria-controls="mobile-pinned-work-list"' in html
|
||||||
assert 'id="mobile-recent-work-status" role="status" aria-live="polite"' in html
|
assert 'id="mobile-recent-work-status" role="status" aria-live="polite"' in html
|
||||||
assert '<script src="static/mobile-recent-work.js"></script>' in html
|
assert '<script src="static/mobile-recent-work.js"></script>' in html
|
||||||
assert "createMobileRecentWork({" in dashboard
|
assert "createMobileRecentWork({" in dashboard
|
||||||
|
|
@ -354,6 +411,7 @@ def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes():
|
||||||
assert "status:qs('#mobile-recent-work-status')" in dashboard
|
assert "status:qs('#mobile-recent-work-status')" in dashboard
|
||||||
assert "pinnedList:qs('#mobile-pinned-work-list')" in dashboard
|
assert "pinnedList:qs('#mobile-pinned-work-list')" in dashboard
|
||||||
assert "pinnedSection:qs('#mobile-pinned-work')" in dashboard
|
assert "pinnedSection:qs('#mobile-pinned-work')" in dashboard
|
||||||
|
assert "pinnedToggle:qs('#mobile-pinned-work-toggle')" in dashboard
|
||||||
assert "mobileRecentWork.startLifecycle({window, document})" in dashboard
|
assert "mobileRecentWork.startLifecycle({window, document})" in dashboard
|
||||||
assert "void mobileRecentWork.load();" in dashboard
|
assert "void mobileRecentWork.load();" in dashboard
|
||||||
assert "mobileRecentWork.record(item)" in dashboard
|
assert "mobileRecentWork.record(item)" in dashboard
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,21 @@ def test_pinned_work_survives_recent_limit_and_unpin_keeps_recent_item(tmp_path)
|
||||||
assert "#/my-work/issue" not in payload
|
assert "#/my-work/issue" not in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_reopening_pinned_work_promotes_it_for_every_device(tmp_path):
|
||||||
|
store = RecentWorkStore(
|
||||||
|
tmp_path / "recent-work.sqlite3",
|
||||||
|
encryption_key=b"r" * 32,
|
||||||
|
)
|
||||||
|
for number in range(1, 5):
|
||||||
|
store.pin("timmy", item(number))
|
||||||
|
|
||||||
|
reopened = store.record("timmy", item(1, title="Issue 1 current"))
|
||||||
|
|
||||||
|
assert [entry["number"] for entry in reopened["pinned"]] == [1, 4, 3, 2]
|
||||||
|
assert reopened["pinned"][0]["title"] == "Issue 1 current"
|
||||||
|
assert store.get("timmy")["pinned"] == reopened["pinned"]
|
||||||
|
|
||||||
|
|
||||||
def test_recent_work_rejects_noncanonical_or_unsupported_items(tmp_path):
|
def test_recent_work_rejects_noncanonical_or_unsupported_items(tmp_path):
|
||||||
store = RecentWorkStore(tmp_path / "recent-work.sqlite3", encryption_key=b"r" * 32)
|
store = RecentWorkStore(tmp_path / "recent-work.sqlite3", encryption_key=b"r" * 32)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user