Open progressive mobile My Work in-app before hydration #1414
|
|
@ -280,6 +280,15 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.event { padding: 8px 0; border-bottom: 1px solid #1b2d45; }
|
.event { padding: 8px 0; border-bottom: 1px solid #1b2d45; }
|
||||||
.event:last-child { border-bottom: 0; }
|
.event:last-child { border-bottom: 0; }
|
||||||
.my-work { grid-column: 1 / -1; }
|
.my-work { grid-column: 1 / -1; }
|
||||||
|
.progressive-work-detail { position:fixed; inset:0; z-index:120; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
|
||||||
|
.progressive-work-detail[hidden] { display:none; }
|
||||||
|
.progressive-work-detail-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:calc(18px + env(safe-area-inset-top)) 18px calc(18px + env(safe-area-inset-bottom)); border:1px solid #55d6be; border-radius:18px 18px 0 0; background:#0b1526; overflow-wrap:anywhere; }
|
||||||
|
.progressive-work-detail-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
|
||||||
|
.progressive-work-detail-panel h2, .progressive-work-detail-panel p { margin-top:0; }
|
||||||
|
.progressive-work-detail-panel button, .progressive-work-detail-panel .button-link { box-sizing:border-box; min-height:44px; }
|
||||||
|
.progressive-work-detail-panel .button-link { display:flex; align-items:center; justify-content:center; width:100%; }
|
||||||
|
@media(max-width:320px) { .progressive-work-detail-panel { width:100%; padding-inline:14px; } }
|
||||||
|
|
||||||
.plan-today-sheet { position:fixed; inset:0; z-index:75; display:flex; justify-content:flex-end; background:rgba(5,12,21,.78); backdrop-filter:blur(4px); }
|
.plan-today-sheet { position:fixed; inset:0; z-index:75; display:flex; justify-content:flex-end; background:rgba(5,12,21,.78); backdrop-filter:blur(4px); }
|
||||||
.plan-today-sheet[hidden] { display:none; }
|
.plan-today-sheet[hidden] { display:none; }
|
||||||
.plan-today-panel { box-sizing:border-box; width:min(620px,100%); height:100%; overflow:auto; overflow-x:hidden; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
|
.plan-today-panel { box-sizing:border-box; width:min(620px,100%); height:100%; overflow:auto; overflow-x:hidden; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
|
||||||
|
|
|
||||||
|
|
@ -8453,6 +8453,16 @@
|
||||||
adoptedProgressiveSnapshot = await contextPoller.adoptPending(progressiveWorkHandoff.liveSnapshotPromise);
|
adoptedProgressiveSnapshot = await contextPoller.adoptPending(progressiveWorkHandoff.liveSnapshotPromise);
|
||||||
}
|
}
|
||||||
if (!adoptedProgressiveSnapshot) await load();
|
if (!adoptedProgressiveSnapshot) await load();
|
||||||
|
if (progressiveWorkHandoff?.openWork) {
|
||||||
|
const progressiveItem = lastMyWork.find(item =>
|
||||||
|
item.repository === progressiveWorkHandoff.openWork.repository &&
|
||||||
|
Number(item.number) === Number(progressiveWorkHandoff.openWork.number));
|
||||||
|
if (progressiveItem) {
|
||||||
|
openRoutedWork(progressiveItem, null);
|
||||||
|
qs('#progressive-work-detail').hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await appShortcut.run();
|
await appShortcut.run();
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
contextPoller.setVisible(!document.hidden);
|
contextPoller.setVisible(!document.hidden);
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="progressive-work-detail" id="progressive-work-detail" role="dialog" aria-modal="true" aria-labelledby="progressive-work-detail-title" hidden>
|
||||||
|
<div class="progressive-work-detail-panel">
|
||||||
|
<header>
|
||||||
|
<div><p class="small muted">My Work</p><h2 id="progressive-work-detail-title">Work item</h2></div>
|
||||||
|
<button id="close-progressive-work-detail" type="button">Close</button>
|
||||||
|
</header>
|
||||||
|
<p class="small" id="progressive-work-detail-meta"></p>
|
||||||
|
<p id="progressive-work-detail-reason"></p>
|
||||||
|
<p class="muted">The full workspace is still loading. You can read this assignment now or open it in Gitea.</p>
|
||||||
|
<a class="button-link" id="open-progressive-work-gitea" href="" hidden>Open in Gitea</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<dialog class="release-receipt-sheet" id="release-receipt-sheet" aria-labelledby="release-receipt-title">
|
<dialog class="release-receipt-sheet" id="release-receipt-sheet" aria-labelledby="release-receipt-title">
|
||||||
<section class="release-receipt-panel">
|
<section class="release-receipt-panel">
|
||||||
<header><div><p class="small muted">Exact merge evidence</p><h2 id="release-receipt-title">Release watchlist</h2></div><button id="close-release-receipt" type="button">Close</button></header>
|
<header><div><p class="small muted">Exact merge evidence</p><h2 id="release-receipt-title">Release watchlist</h2></div><button id="close-release-receipt" type="button">Close</button></header>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ function createProgressiveMyWork({
|
||||||
}) {
|
}) {
|
||||||
const list = document.querySelector('#my-work-list');
|
const list = document.querySelector('#my-work-list');
|
||||||
const status = document.querySelector('#my-work-status');
|
const status = document.querySelector('#my-work-status');
|
||||||
|
const detail = document.querySelector('#progressive-work-detail');
|
||||||
|
const detailTitle = document.querySelector('#progressive-work-detail-title');
|
||||||
|
const detailMeta = document.querySelector('#progressive-work-detail-meta');
|
||||||
|
const detailReason = document.querySelector('#progressive-work-detail-reason');
|
||||||
|
const closeDetail = document.querySelector('#close-progressive-work-detail');
|
||||||
|
const openGitea = document.querySelector('#open-progressive-work-gitea');
|
||||||
const filters = Array.from(document.querySelectorAll('[data-work-filter]'));
|
const filters = Array.from(document.querySelectorAll('[data-work-filter]'));
|
||||||
const listeners = [];
|
const listeners = [];
|
||||||
const lifecycleListeners = [];
|
const lifecycleListeners = [];
|
||||||
|
|
@ -14,6 +20,8 @@ function createProgressiveMyWork({
|
||||||
let liveSnapshotPromise = null;
|
let liveSnapshotPromise = null;
|
||||||
let confirmedLogin = '';
|
let confirmedLogin = '';
|
||||||
let poller = null;
|
let poller = null;
|
||||||
|
let detailTrigger = null;
|
||||||
|
let openWork = null;
|
||||||
const deferredQueues = {
|
const deferredQueues = {
|
||||||
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
||||||
};
|
};
|
||||||
|
|
@ -35,6 +43,28 @@ function createProgressiveMyWork({
|
||||||
filter === 'authored' ? item.is_authored :
|
filter === 'authored' ? item.is_authored :
|
||||||
filter === 'pull' ? item.kind === 'pull' && !item.is_review : item.kind === filter;
|
filter === 'pull' ? item.kind === 'pull' && !item.is_review : item.kind === filter;
|
||||||
const visibleItems = () => active === 'all' ? items : items.filter(item => matches(item, active));
|
const visibleItems = () => active === 'all' ? items : items.filter(item => matches(item, active));
|
||||||
|
const closeProgressiveDetail = ({ restoreFocus = true } = {}) => {
|
||||||
|
if (!detail || detail.hidden) return;
|
||||||
|
detail.hidden = true;
|
||||||
|
if (restoreFocus) detailTrigger?.focus?.();
|
||||||
|
detailTrigger = null;
|
||||||
|
openWork = null;
|
||||||
|
};
|
||||||
|
const openProgressiveDetail = (item, trigger) => {
|
||||||
|
if (!detail || !item) return;
|
||||||
|
detailTrigger = trigger;
|
||||||
|
openWork = item;
|
||||||
|
detailTitle.textContent = item.title || item.key || 'Untitled work';
|
||||||
|
detailMeta.textContent = (item.key || 'Unknown work item') + ' · ' +
|
||||||
|
(item.kind === 'pull' ? 'Pull request' : 'Issue');
|
||||||
|
detailReason.textContent = item.reason || 'Assigned to you';
|
||||||
|
const href = safeUrl(item.url);
|
||||||
|
openGitea.hidden = !href;
|
||||||
|
if (href) openGitea.href = href;
|
||||||
|
else openGitea.removeAttribute?.('href');
|
||||||
|
detail.hidden = false;
|
||||||
|
closeDetail?.focus?.();
|
||||||
|
};
|
||||||
const updateCounts = () => {
|
const updateCounts = () => {
|
||||||
['all','attention','filed','authored','issue','pull','review','update'].forEach(filter => {
|
['all','attention','filed','authored','issue','pull','review','update'].forEach(filter => {
|
||||||
const count = filter === 'all' ? items.length : items.filter(item => matches(item, filter)).length;
|
const count = filter === 'all' ? items.length : items.filter(item => matches(item, filter)).length;
|
||||||
|
|
@ -46,15 +76,14 @@ function createProgressiveMyWork({
|
||||||
if (stopped || !list) return;
|
if (stopped || !list) return;
|
||||||
const visible = visibleItems();
|
const visible = visibleItems();
|
||||||
list.innerHTML = deferredQueues[active] ?
|
list.innerHTML = deferredQueues[active] ?
|
||||||
'<div class="muted">' + deferredQueues[active] + ' is still loading…</div>' : visible.length ? visible.map(item => {
|
'<div class="muted">' + deferredQueues[active] + ' is still loading…</div>' : visible.length ? visible.map((item, index) => {
|
||||||
const href = safeUrl(item.url);
|
|
||||||
const title = escapeHtml(item.title || item.key || 'Untitled work');
|
const title = escapeHtml(item.title || item.key || 'Untitled work');
|
||||||
const context = escapeHtml(item.key || '');
|
const context = escapeHtml(item.key || '');
|
||||||
const reason = escapeHtml(item.reason || 'Assigned to you');
|
const reason = escapeHtml(item.reason || 'Assigned to you');
|
||||||
return '<article class="my-work-card progressive-my-work-card">' +
|
return '<article class="my-work-card progressive-my-work-card">' +
|
||||||
(href ? '<a class="my-work-card-main" href="' + escapeHtml(href) + '">' : '<div class="my-work-card-main">') +
|
'<button class="my-work-card-main" type="button" data-progressive-work-index="' + index + '">' +
|
||||||
'<strong>' + title + '</strong><span class="small">' + context + ' · ' + reason + '</span>' +
|
'<strong>' + title + '</strong><span class="small">' + context + ' · ' + reason + '</span>' +
|
||||||
(href ? '</a>' : '</div>') + '</article>';
|
'</button></article>';
|
||||||
}).join('') : '<div class="muted">No work in this queue.</div>';
|
}).join('') : '<div class="muted">No work in this queue.</div>';
|
||||||
filters.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.workFilter === active)));
|
filters.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.workFilter === active)));
|
||||||
};
|
};
|
||||||
|
|
@ -63,6 +92,23 @@ function createProgressiveMyWork({
|
||||||
button.addEventListener('click', listener);
|
button.addEventListener('click', listener);
|
||||||
listeners.push([button, listener]);
|
listeners.push([button, listener]);
|
||||||
});
|
});
|
||||||
|
const openListener = event => {
|
||||||
|
const trigger = event.target?.closest?.('[data-progressive-work-index]');
|
||||||
|
if (!trigger) return;
|
||||||
|
const item = visibleItems()[Number(trigger.dataset.progressiveWorkIndex)];
|
||||||
|
if (!item) return;
|
||||||
|
event.preventDefault?.();
|
||||||
|
openProgressiveDetail(item, trigger);
|
||||||
|
};
|
||||||
|
const closeListener = () => closeProgressiveDetail();
|
||||||
|
const keyListener = event => {
|
||||||
|
if (event.key !== 'Escape' || detail?.hidden) return;
|
||||||
|
event.preventDefault?.();
|
||||||
|
closeProgressiveDetail();
|
||||||
|
};
|
||||||
|
list?.addEventListener?.('click', openListener);
|
||||||
|
closeDetail?.addEventListener?.('click', closeListener);
|
||||||
|
lifecycleTarget.addEventListener?.('keydown', keyListener);
|
||||||
|
|
||||||
const applySnapshot = snapshot => {
|
const applySnapshot = snapshot => {
|
||||||
if (stopped) return false;
|
if (stopped) return false;
|
||||||
|
|
@ -102,6 +148,13 @@ function createProgressiveMyWork({
|
||||||
login() { return confirmedLogin; },
|
login() { return confirmedLogin; },
|
||||||
handoff() {
|
handoff() {
|
||||||
const state = { selectedFilter:selectedByUser ? active : null };
|
const state = { selectedFilter:selectedByUser ? active : null };
|
||||||
|
if (openWork) {
|
||||||
|
state.openWork = {
|
||||||
|
kind:openWork.kind, key:openWork.key, number:openWork.number,
|
||||||
|
repository:openWork.repository,
|
||||||
|
};
|
||||||
|
openWork = null;
|
||||||
|
}
|
||||||
if (liveSnapshot) {
|
if (liveSnapshot) {
|
||||||
state.liveSnapshot = liveSnapshot;
|
state.liveSnapshot = liveSnapshot;
|
||||||
liveSnapshot = null;
|
liveSnapshot = null;
|
||||||
|
|
@ -142,6 +195,9 @@ function createProgressiveMyWork({
|
||||||
stopped = true;
|
stopped = true;
|
||||||
poller?.stop();
|
poller?.stop();
|
||||||
listeners.forEach(([button, listener]) => button.removeEventListener?.('click', listener));
|
listeners.forEach(([button, listener]) => button.removeEventListener?.('click', listener));
|
||||||
|
list?.removeEventListener?.('click', openListener);
|
||||||
|
closeDetail?.removeEventListener?.('click', closeListener);
|
||||||
|
lifecycleTarget.removeEventListener?.('keydown', keyListener);
|
||||||
lifecycleListeners.forEach(([eventName, listener]) =>
|
lifecycleListeners.forEach(([eventName, listener]) =>
|
||||||
lifecycleTarget.removeEventListener?.(eventName, listener));
|
lifecycleTarget.removeEventListener?.(eventName, listener));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ from pathlib import Path
|
||||||
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js"
|
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js"
|
||||||
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
||||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||||
|
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
||||||
|
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
||||||
|
|
||||||
|
|
||||||
def test_progressive_my_work_renders_and_filters_assigned_work_before_full_workspace():
|
def test_progressive_my_work_renders_and_filters_assigned_work_before_full_workspace():
|
||||||
|
|
@ -35,7 +37,8 @@ const flow=createProgressiveMyWork({{document,fetchSnapshot:async()=>({{
|
||||||
state = json.loads(result.stdout)
|
state = json.loads(result.stdout)
|
||||||
assert state["status"] == "1 assigned work item ready."
|
assert state["status"] == "1 assigned work item ready."
|
||||||
assert "Fix mobile queue" in state["rendered"]
|
assert "Fix mobile queue" in state["rendered"]
|
||||||
assert 'href="https://forge.example/issues/7"' in state["rendered"]
|
assert 'data-progressive-work-index="0"' in state["rendered"]
|
||||||
|
assert "href=" not in state["rendered"]
|
||||||
assert state["pressed"] == ["false", "true"]
|
assert state["pressed"] == ["false", "true"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -292,7 +295,7 @@ const flow=context.module.exports({{
|
||||||
"reconnecting": "Assigned work is reconnecting…",
|
"reconnecting": "Assigned work is reconnecting…",
|
||||||
"attempts": 2,
|
"attempts": 2,
|
||||||
"status": "1 assigned work item ready.",
|
"status": "1 assigned work item ready.",
|
||||||
"html": '<article class="my-work-card progressive-my-work-card"><a class="my-work-card-main" href="https://forge.example/issues/9"><strong>Recovered assignment</strong><span class="small">stackchain/dashboard#9 · Assigned to you</span></a></article>',
|
"html": '<article class="my-work-card progressive-my-work-card"><button class="my-work-card-main" type="button" data-progressive-work-index="0"><strong>Recovered assignment</strong><span class="small">stackchain/dashboard#9 · Assigned to you</span></button></article>',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -363,5 +366,92 @@ const flow=context.module.exports({{
|
||||||
"hasSignal": True,
|
"hasSignal": True,
|
||||||
}
|
}
|
||||||
assert state["latestTitle"] == "New assignment"
|
assert state["latestTitle"] == "New assignment"
|
||||||
assert state["removed"] == ["online", "visibilitychange"]
|
assert state["removed"] == ["keydown", "online", "visibilitychange"]
|
||||||
assert state["status"] == "1 assigned work item ready."
|
assert state["status"] == "1 assigned work item ready."
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_card_opens_safe_in_app_detail_and_hands_it_off_once():
|
||||||
|
harness = f"""
|
||||||
|
const fs=require('fs'); const vm=require('vm');
|
||||||
|
function element(extra={{}}) {{
|
||||||
|
return Object.assign({{
|
||||||
|
hidden:false,textContent:'',attrs:{{}},listeners:{{}},focusCount:0,
|
||||||
|
addEventListener(name,cb){{this.listeners[name]=cb;}},
|
||||||
|
removeEventListener(name,cb){{if(this.listeners[name]===cb)delete this.listeners[name];}},
|
||||||
|
setAttribute(name,value){{this.attrs[name]=String(value);}},
|
||||||
|
removeAttribute(name){{delete this.attrs[name];}},
|
||||||
|
focus(){{this.focusCount += 1;}},
|
||||||
|
}},extra);
|
||||||
|
}}
|
||||||
|
const list=element({{innerHTML:''}}); const status=element();
|
||||||
|
const sheet=element({{hidden:true}}); const title=element(); const meta=element(); const reason=element();
|
||||||
|
const close=element(); const gitea=element({{hidden:true,href:''}});
|
||||||
|
const elements={{
|
||||||
|
'#my-work-list':list,'#my-work-status':status,'#progressive-work-detail':sheet,
|
||||||
|
'#progressive-work-detail-title':title,'#progressive-work-detail-meta':meta,
|
||||||
|
'#progressive-work-detail-reason':reason,'#close-progressive-work-detail':close,
|
||||||
|
'#open-progressive-work-gitea':gitea,
|
||||||
|
}};
|
||||||
|
const document={{querySelector:s=>elements[s]||null,querySelectorAll:()=>[]}};
|
||||||
|
const lifecycleTarget=element();
|
||||||
|
const context={{module:{{exports:{{}}}},exports:{{}},console,URL,document}}; vm.createContext(context);
|
||||||
|
vm.runInContext(fs.readFileSync({json.dumps(str(MY_WORK))},'utf8'),context);
|
||||||
|
context.buildMyWork=context.module.exports; context.module={{exports:{{}}}};
|
||||||
|
vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context);
|
||||||
|
const flow=context.module.exports({{document,lifecycleTarget,fetchSnapshot:async()=>({{
|
||||||
|
user:{{login:'timmy'}},issues:[{{number:7,title:'<img src=x onerror=alert(1)>',repository:'stackchain/dashboard',assignees:['timmy'],url:'https://forge.example/issues/7'}}],pull_requests:[]
|
||||||
|
}})}});
|
||||||
|
(async()=>{{
|
||||||
|
await flow.start();
|
||||||
|
const trigger=element({{dataset:{{progressiveWorkIndex:'0'}},closest:()=>trigger}});
|
||||||
|
list.listeners.click({{target:trigger,preventDefault(){{}}}});
|
||||||
|
const opened={{sheetHidden:sheet.hidden,title:title.textContent,meta:meta.textContent,reason:reason.textContent,giteaHidden:gitea.hidden,giteaHref:gitea.href,html:list.innerHTML}};
|
||||||
|
lifecycleTarget.listeners.keydown({{key:'Escape',preventDefault(){{}}}});
|
||||||
|
list.listeners.click({{target:trigger,preventDefault(){{}}}});
|
||||||
|
const first=flow.handoff(); const second=flow.handoff();
|
||||||
|
console.log(JSON.stringify({{opened,closed:sheet.hidden,focusCount:trigger.focusCount,first,second}}));
|
||||||
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
||||||
|
state = json.loads(result.stdout)
|
||||||
|
|
||||||
|
assert state["opened"] == {
|
||||||
|
"sheetHidden": False,
|
||||||
|
"title": "<img src=x onerror=alert(1)>",
|
||||||
|
"meta": "stackchain/dashboard#7 · Issue",
|
||||||
|
"reason": "Assigned to you",
|
||||||
|
"giteaHidden": False,
|
||||||
|
"giteaHref": "https://forge.example/issues/7",
|
||||||
|
"html": '<article class="my-work-card progressive-my-work-card"><button class="my-work-card-main" type="button" data-progressive-work-index="0"><strong><img src=x onerror=alert(1)></strong><span class="small">stackchain/dashboard#7 · Assigned to you</span></button></article>',
|
||||||
|
}
|
||||||
|
assert state["closed"] is False
|
||||||
|
assert state["focusCount"] == 1
|
||||||
|
assert state["first"]["openWork"] == {
|
||||||
|
"kind": "issue",
|
||||||
|
"key": "stackchain/dashboard#7",
|
||||||
|
"number": 7,
|
||||||
|
"repository": "stackchain/dashboard",
|
||||||
|
}
|
||||||
|
assert "openWork" not in state["second"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_detail_is_mobile_safe_and_adopted_by_hydrated_workspace():
|
||||||
|
html = INDEX.read_text()
|
||||||
|
css = CSS.read_text()
|
||||||
|
dashboard = DASHBOARD.read_text()
|
||||||
|
|
||||||
|
assert 'id="progressive-work-detail" role="dialog" aria-modal="true"' in html
|
||||||
|
assert 'aria-labelledby="progressive-work-detail-title" hidden' in html
|
||||||
|
assert 'id="close-progressive-work-detail"' in html
|
||||||
|
assert 'id="open-progressive-work-gitea"' in html
|
||||||
|
assert ".progressive-work-detail[hidden] { display:none; }" in css
|
||||||
|
assert "max-height:100dvh" in css
|
||||||
|
assert "overflow-x:hidden" in css
|
||||||
|
assert "env(safe-area-inset-top)" in css
|
||||||
|
assert "env(safe-area-inset-bottom)" in css
|
||||||
|
assert ".progressive-work-detail-panel button, .progressive-work-detail-panel .button-link" in css
|
||||||
|
assert "min-height:44px" in css
|
||||||
|
assert "@media(max-width:320px)" in css
|
||||||
|
assert "progressiveWorkHandoff?.openWork" in dashboard
|
||||||
|
assert "openRoutedWork(progressiveItem, null)" in dashboard
|
||||||
|
assert "qs('#progressive-work-detail').hidden = true" in dashboard
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user