Compare commits
No commits in common. "021af60cf33314319492a76c59397995530a354c" and "484dcc6b9c1c99a283956b4c1c0911bc055bc295" have entirely different histories.
021af60cf3
...
484dcc6b9c
|
|
@ -27,7 +27,6 @@
|
||||||
actions:qs('#issue-planning'),
|
actions:qs('#issue-planning'),
|
||||||
},
|
},
|
||||||
planning:qs('#issue-planning'),
|
planning:qs('#issue-planning'),
|
||||||
onSectionChange:(section, options) => workRoute.section(section, options),
|
|
||||||
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||||
});
|
});
|
||||||
mobileIssueDetailNavigation.start();
|
mobileIssueDetailNavigation.start();
|
||||||
|
|
@ -42,7 +41,6 @@
|
||||||
review:qs('#pull-review'),
|
review:qs('#pull-review'),
|
||||||
},
|
},
|
||||||
beforeNavigate:{review(target) { target.open = true; }},
|
beforeNavigate:{review(target) { target.open = true; }},
|
||||||
onSectionChange:(section, options) => workRoute.section(section, options),
|
|
||||||
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||||
});
|
});
|
||||||
mobilePullDetailNavigation.start();
|
mobilePullDetailNavigation.start();
|
||||||
|
|
@ -1411,7 +1409,6 @@
|
||||||
},
|
},
|
||||||
replyComposer:qs('#update-reply'),
|
replyComposer:qs('#update-reply'),
|
||||||
jumpToNewActivity:() => updateReadPosition.jump(),
|
jumpToNewActivity:() => updateReadPosition.jump(),
|
||||||
onSectionChange:(section, options) => workRoute.section(section, options),
|
|
||||||
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||||
});
|
});
|
||||||
mobileUpdateDetailNavigation.start();
|
mobileUpdateDetailNavigation.start();
|
||||||
|
|
@ -1425,7 +1422,6 @@
|
||||||
history:qs('#review-history-workspace'),
|
history:qs('#review-history-workspace'),
|
||||||
},
|
},
|
||||||
summaryComposer:qs('#review-summary'),
|
summaryComposer:qs('#review-summary'),
|
||||||
onSectionChange:(section, options) => workRoute.section(section, options),
|
|
||||||
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||||
});
|
});
|
||||||
mobileReviewDetailNavigation.start();
|
mobileReviewDetailNavigation.start();
|
||||||
|
|
@ -1669,25 +1665,6 @@
|
||||||
workRoute.open(routedWorkItem(item), options);
|
workRoute.open(routedWorkItem(item), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateWorkSection(kind, section) {
|
|
||||||
if (['issue', 'filed'].includes(kind)) {
|
|
||||||
mobileIssueDetailNavigation.navigate(section, { focus:false });
|
|
||||||
} else if (kind === 'pull') {
|
|
||||||
mobilePullDetailNavigation.navigate(section, { focus:false });
|
|
||||||
} else if (kind === 'update') {
|
|
||||||
mobileUpdateDetailNavigation.navigate(section, { focus:false });
|
|
||||||
} else if (kind === 'review') {
|
|
||||||
mobileReviewDetailNavigation.navigate(section, { focus:false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openRoutedWorkSection(item) {
|
|
||||||
if (item.kind === 'update') await notificationReader.open(item, lastMyWork);
|
|
||||||
else if (item.kind === 'review') await openReviewSheet(item, reviewTrigger);
|
|
||||||
else if (item.kind === 'issue' || item.kind === 'filed') await openIssueSheet(item, issueTrigger);
|
|
||||||
else if (item.kind === 'pull') await openPullSheet(item, pullTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workRoute = createWorkRoute.createController({
|
const workRoute = createWorkRoute.createController({
|
||||||
location: window.location,
|
location: window.location,
|
||||||
history: window.history,
|
history: window.history,
|
||||||
|
|
@ -1705,17 +1682,14 @@
|
||||||
qs('#retry-work-route').hidden = true;
|
qs('#retry-work-route').hidden = true;
|
||||||
qs('#my-work-action-status').textContent = 'Loading shared work item…';
|
qs('#my-work-action-status').textContent = 'Loading shared work item…';
|
||||||
},
|
},
|
||||||
onOpen: async item => {
|
onOpen: item => {
|
||||||
qs('#retry-work-route').hidden = true;
|
qs('#retry-work-route').hidden = true;
|
||||||
qs('#my-work-action-status').textContent = '';
|
qs('#my-work-action-status').textContent = '';
|
||||||
closeOpenWorkSheets();
|
closeOpenWorkSheets();
|
||||||
await openRoutedWorkSection(item);
|
if (item.kind === 'update') notificationReader.open(item, lastMyWork);
|
||||||
const route = createWorkRoute.parse(window.location.hash);
|
else if (item.kind === 'review') openReviewSheet(item, reviewTrigger);
|
||||||
if (route?.section === item.section) navigateWorkSection(item.kind, item.section);
|
else if (item.kind === 'issue' || item.kind === 'filed') openIssueSheet(item, issueTrigger);
|
||||||
},
|
else if (item.kind === 'pull') openPullSheet(item, pullTrigger);
|
||||||
onSection: (section, options) => {
|
|
||||||
const route = createWorkRoute.parse(window.location.hash);
|
|
||||||
if (options.restore) navigateWorkSection(route?.kind, section);
|
|
||||||
},
|
},
|
||||||
onQueue: openWorkQueueRoute,
|
onQueue: openWorkQueueRoute,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function createMobileIssueDetailNavigation(options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigate(name, navigationOptions = {}) {
|
function navigate(name) {
|
||||||
const target = targets[name];
|
const target = targets[name];
|
||||||
if (!target) return false;
|
if (!target) return false;
|
||||||
const prepare = options.beforeNavigate && options.beforeNavigate[name];
|
const prepare = options.beforeNavigate && options.beforeNavigate[name];
|
||||||
|
|
@ -24,7 +24,7 @@ function createMobileIssueDetailNavigation(options) {
|
||||||
block: 'start',
|
block: 'start',
|
||||||
behavior: prefersReducedMotion() ? 'auto' : 'smooth',
|
behavior: prefersReducedMotion() ? 'auto' : 'smooth',
|
||||||
});
|
});
|
||||||
if (name === 'reply' && navigationOptions.focus !== false) target.focus();
|
if (name === 'reply') target.focus();
|
||||||
select(name);
|
select(name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,6 @@ function createMobileIssueDetailNavigation(options) {
|
||||||
const listener = event => {
|
const listener = event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
navigate(name);
|
navigate(name);
|
||||||
if (options.onSectionChange) options.onSectionChange(name, { replace:false });
|
|
||||||
};
|
};
|
||||||
listeners.set(button, listener);
|
listeners.set(button, listener);
|
||||||
button.addEventListener('click', listener);
|
button.addEventListener('click', listener);
|
||||||
|
|
@ -56,11 +55,7 @@ function createMobileIssueDetailNavigation(options) {
|
||||||
const visible = entries
|
const visible = entries
|
||||||
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
|
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
|
||||||
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
|
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
|
||||||
if (visible) {
|
if (visible) select(targetNames.get(visible.target));
|
||||||
const name = targetNames.get(visible.target);
|
|
||||||
select(name);
|
|
||||||
if (options.onSectionChange) options.onSectionChange(name, { replace:true });
|
|
||||||
}
|
|
||||||
}, Array.from(targetNames.keys()).filter(Boolean));
|
}, Array.from(targetNames.keys()).filter(Boolean));
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ function createMobileReviewDetailNavigation(options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigate(name, navigationOptions = {}) {
|
function navigate(name) {
|
||||||
const target = targets[name];
|
const target = targets[name];
|
||||||
if (!target) return false;
|
if (!target) return false;
|
||||||
target.scrollIntoView({
|
target.scrollIntoView({
|
||||||
block:'start',
|
block:'start',
|
||||||
behavior:prefersReducedMotion() ? 'auto' : 'smooth',
|
behavior:prefersReducedMotion() ? 'auto' : 'smooth',
|
||||||
});
|
});
|
||||||
if (name === 'feedback' && options.summaryComposer && navigationOptions.focus !== false) {
|
if (name === 'feedback' && options.summaryComposer) {
|
||||||
options.summaryComposer.focus({ preventScroll:true });
|
options.summaryComposer.focus({ preventScroll:true });
|
||||||
}
|
}
|
||||||
select(name);
|
select(name);
|
||||||
|
|
@ -39,7 +39,6 @@ function createMobileReviewDetailNavigation(options) {
|
||||||
const listener = event => {
|
const listener = event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
navigate(name);
|
navigate(name);
|
||||||
if (options.onSectionChange) options.onSectionChange(name, { replace:false });
|
|
||||||
};
|
};
|
||||||
listeners.set(button, listener);
|
listeners.set(button, listener);
|
||||||
button.addEventListener('click', listener);
|
button.addEventListener('click', listener);
|
||||||
|
|
@ -59,11 +58,7 @@ function createMobileReviewDetailNavigation(options) {
|
||||||
const visible = entries
|
const visible = entries
|
||||||
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
|
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
|
||||||
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
|
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
|
||||||
if (visible) {
|
if (visible) select(targetNames.get(visible.target));
|
||||||
const name = targetNames.get(visible.target);
|
|
||||||
select(name);
|
|
||||||
if (options.onSectionChange) options.onSectionChange(name, { replace:true });
|
|
||||||
}
|
|
||||||
}, Array.from(targetNames.keys()).filter(Boolean), options.root || null);
|
}, Array.from(targetNames.keys()).filter(Boolean), options.root || null);
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function createMobileUpdateDetailNavigation(options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigate(name, navigationOptions = {}) {
|
function navigate(name) {
|
||||||
if (name === 'activity') {
|
if (name === 'activity') {
|
||||||
if (options.jumpToNewActivity) options.jumpToNewActivity();
|
if (options.jumpToNewActivity) options.jumpToNewActivity();
|
||||||
select(name);
|
select(name);
|
||||||
|
|
@ -27,7 +27,7 @@ function createMobileUpdateDetailNavigation(options) {
|
||||||
block:'start',
|
block:'start',
|
||||||
behavior:prefersReducedMotion() ? 'auto' : 'smooth',
|
behavior:prefersReducedMotion() ? 'auto' : 'smooth',
|
||||||
});
|
});
|
||||||
if (name === 'reply' && options.replyComposer && navigationOptions.focus !== false) {
|
if (name === 'reply' && options.replyComposer) {
|
||||||
options.replyComposer.focus({ preventScroll:true });
|
options.replyComposer.focus({ preventScroll:true });
|
||||||
}
|
}
|
||||||
select(name);
|
select(name);
|
||||||
|
|
@ -46,7 +46,6 @@ function createMobileUpdateDetailNavigation(options) {
|
||||||
const listener = event => {
|
const listener = event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
navigate(name);
|
navigate(name);
|
||||||
if (options.onSectionChange) options.onSectionChange(name, { replace:false });
|
|
||||||
};
|
};
|
||||||
listeners.set(button, listener);
|
listeners.set(button, listener);
|
||||||
button.addEventListener('click', listener);
|
button.addEventListener('click', listener);
|
||||||
|
|
@ -66,11 +65,7 @@ function createMobileUpdateDetailNavigation(options) {
|
||||||
const visible = entries
|
const visible = entries
|
||||||
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
|
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
|
||||||
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
|
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
|
||||||
if (visible) {
|
if (visible) select(targetNames.get(visible.target));
|
||||||
const name = targetNames.get(visible.target);
|
|
||||||
select(name);
|
|
||||||
if (options.onSectionChange) options.onSectionChange(name, { replace:true });
|
|
||||||
}
|
|
||||||
}, Array.from(targetNames.keys()).filter(Boolean));
|
}, Array.from(targetNames.keys()).filter(Boolean));
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,6 @@
|
||||||
|
|
||||||
const repositoryPart = /^[A-Za-z0-9_.-]+$/;
|
const repositoryPart = /^[A-Za-z0-9_.-]+$/;
|
||||||
const queueFilters = ['today', 'agenda', 'attention', 'filed', 'update', 'later', 'draft'];
|
const queueFilters = ['today', 'agenda', 'attention', 'filed', 'update', 'later', 'draft'];
|
||||||
const sections = {
|
|
||||||
issue: ['overview', 'conversation', 'reply', 'actions'],
|
|
||||||
filed: ['overview', 'conversation', 'reply', 'actions'],
|
|
||||||
pull: ['overview', 'conversation', 'reply', 'review'],
|
|
||||||
review: ['overview', 'files', 'feedback', 'history'],
|
|
||||||
update: ['activity', 'conversation', 'context', 'reply'],
|
|
||||||
};
|
|
||||||
|
|
||||||
function positiveInteger(value) {
|
function positiveInteger(value) {
|
||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
|
|
@ -30,37 +23,27 @@
|
||||||
if (queue === 'agenda' && parts[3] === 'protect-today' && parts.length === 4) {
|
if (queue === 'agenda' && parts[3] === 'protect-today' && parts.length === 4) {
|
||||||
return { kind: 'queue', filter: 'agenda', action: 'protect-today' };
|
return { kind: 'queue', filter: 'agenda', action: 'protect-today' };
|
||||||
}
|
}
|
||||||
if (parts[2] === 'update' && [4, 5].includes(parts.length)) {
|
if (parts[2] === 'update' && parts.length === 4) {
|
||||||
const notificationId = positiveInteger(parts[3]);
|
const notificationId = positiveInteger(parts[3]);
|
||||||
const section = parts[4];
|
return notificationId ? { kind: 'update', notification_id: notificationId } : null;
|
||||||
if (!notificationId || (section && !sections.update.includes(section))) return null;
|
|
||||||
return { kind: 'update', notification_id: notificationId, ...(section ? { section } : {}) };
|
|
||||||
}
|
}
|
||||||
if (!['issue', 'filed', 'pull', 'review'].includes(parts[2]) || ![6, 7].includes(parts.length)) return null;
|
if (!['issue', 'filed', 'pull', 'review'].includes(parts[2]) || parts.length !== 6) return null;
|
||||||
if (!repositoryPart.test(parts[3]) || !repositoryPart.test(parts[4])) return null;
|
if (!repositoryPart.test(parts[3]) || !repositoryPart.test(parts[4])) return null;
|
||||||
const number = positiveInteger(parts[5]);
|
const number = positiveInteger(parts[5]);
|
||||||
const section = parts[6];
|
return number ? { kind: parts[2], repository: parts[3] + '/' + parts[4], number } : null;
|
||||||
if (!number || (section && !sections[parts[2]].includes(section))) return null;
|
|
||||||
return {
|
|
||||||
kind: parts[2], repository: parts[3] + '/' + parts[4], number,
|
|
||||||
...(section ? { section } : {}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function serialize(item) {
|
function serialize(item) {
|
||||||
if (item?.kind === 'update') {
|
if (item?.kind === 'update') {
|
||||||
const notificationId = positiveInteger(item.notification_id);
|
const notificationId = positiveInteger(item.notification_id);
|
||||||
if (!notificationId || (item.section && !sections.update.includes(item.section))) return '';
|
return notificationId ? '#/my-work/update/' + notificationId : '';
|
||||||
return '#/my-work/update/' + notificationId + (item.section ? '/' + item.section : '');
|
|
||||||
}
|
}
|
||||||
const kind = item?.is_filed && !item?.is_assigned ? 'filed' : item?.kind;
|
const kind = item?.is_filed && !item?.is_assigned ? 'filed' : item?.kind;
|
||||||
if (!['issue', 'filed', 'pull', 'review'].includes(kind)) return '';
|
if (!['issue', 'filed', 'pull', 'review'].includes(kind)) return '';
|
||||||
const repository = String(item.repository || '').split('/');
|
const repository = String(item.repository || '').split('/');
|
||||||
const number = positiveInteger(item.number);
|
const number = positiveInteger(item.number);
|
||||||
if (repository.length !== 2 || !repository.every(part => repositoryPart.test(part)) || !number) return '';
|
if (repository.length !== 2 || !repository.every(part => repositoryPart.test(part)) || !number) return '';
|
||||||
if (item.section && !sections[kind].includes(item.section)) return '';
|
return '#/my-work/' + kind + '/' + repository.join('/') + '/' + number;
|
||||||
return '#/my-work/' + kind + '/' + repository.join('/') + '/' + number +
|
|
||||||
(item.section ? '/' + item.section : '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameRoute(item, route) {
|
function sameRoute(item, route) {
|
||||||
|
|
@ -76,7 +59,7 @@
|
||||||
|
|
||||||
function createController({
|
function createController({
|
||||||
location, history, eventTarget, onOpen, onClose, onInvalid,
|
location, history, eventTarget, onOpen, onClose, onInvalid,
|
||||||
onQueue = function () {}, onSection = function () {},
|
onQueue = function () {},
|
||||||
resolve, onResolving = function () {}, onError = function () {},
|
resolve, onResolving = function () {}, onError = function () {},
|
||||||
}) {
|
}) {
|
||||||
let items = [];
|
let items = [];
|
||||||
|
|
@ -103,7 +86,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
active = fragment;
|
active = fragment;
|
||||||
onOpen({ ...item, kind: route.kind, ...(route.section ? { section: route.section } : {}) });
|
onOpen({ ...item, kind: route.kind });
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (request !== resolution || String(location.hash || '') !== fragment) return;
|
if (request !== resolution || String(location.hash || '') !== fragment) return;
|
||||||
resolving = '';
|
resolving = '';
|
||||||
|
|
@ -145,16 +128,10 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (active === fragment) return;
|
if (active === fragment) return;
|
||||||
const activeRoute = parse(active);
|
|
||||||
if (activeRoute && sameRoute(item, activeRoute)) {
|
|
||||||
active = fragment;
|
|
||||||
onSection(route.section || sections[route.kind][0], { restore: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolution += 1;
|
resolution += 1;
|
||||||
resolving = '';
|
resolving = '';
|
||||||
active = fragment;
|
active = fragment;
|
||||||
onOpen({ ...item, kind: route.kind, ...(route.section ? { section: route.section } : {}) });
|
onOpen({ ...item, kind: route.kind });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -179,17 +156,6 @@
|
||||||
onOpen(item);
|
onOpen(item);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
section(name, options = {}) {
|
|
||||||
const route = parse(location.hash);
|
|
||||||
if (!route || route.kind === 'queue' || !sections[route.kind].includes(name)) return false;
|
|
||||||
if ((route.section || sections[route.kind][0]) === name) return true;
|
|
||||||
const fragment = serialize({ ...route, section: name });
|
|
||||||
const method = options.replace ? 'replaceState' : 'pushState';
|
|
||||||
history[method]({ workRoute: fragment }, '', fragment);
|
|
||||||
active = fragment;
|
|
||||||
onSection(name, { restore: false });
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
queue(filter) {
|
queue(filter) {
|
||||||
if (!queueFilters.includes(filter)) return false;
|
if (!queueFilters.includes(filter)) return false;
|
||||||
const name = filter + (['update', 'draft'].includes(filter) ? 's' : '');
|
const name = filter + (['update', 'draft'].includes(filter) ? 's' : '');
|
||||||
|
|
|
||||||
|
|
@ -116,45 +116,6 @@ process.stdout.write(JSON.stringify({{current,disconnected}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_issue_navigation_distinguishes_taps_scrolls_and_keyboard_safe_restore():
|
|
||||||
script = f"""
|
|
||||||
const createNavigation = require({json.dumps(str(CONTROLLER))});
|
|
||||||
class FakeElement {{
|
|
||||||
constructor(name) {{ this.name=name; this.listeners={{}}; this.attributes={{}}; this.focuses=0; }}
|
|
||||||
addEventListener(name, callback) {{ this.listeners[name]=callback; }}
|
|
||||||
removeEventListener() {{}}
|
|
||||||
click() {{ this.listeners.click({{preventDefault() {{}}}}); }}
|
|
||||||
setAttribute(name, value) {{ this.attributes[name]=value; }}
|
|
||||||
removeAttribute(name) {{ delete this.attributes[name]; }}
|
|
||||||
scrollIntoView() {{}}
|
|
||||||
focus() {{ this.focuses += 1; }}
|
|
||||||
}}
|
|
||||||
const names=['overview','conversation','reply','actions'];
|
|
||||||
const buttons=Object.fromEntries(names.map(name => [name,new FakeElement(name)]));
|
|
||||||
const targets=Object.fromEntries(names.map(name => [name,new FakeElement(name)]));
|
|
||||||
const changes=[];
|
|
||||||
let observer;
|
|
||||||
const navigation=createNavigation({{
|
|
||||||
buttons, targets,
|
|
||||||
onSectionChange:(name, options) => changes.push([name, options.replace]),
|
|
||||||
observe(handler) {{ observer=handler; return {{disconnect() {{}}}}; }},
|
|
||||||
}});
|
|
||||||
navigation.start();
|
|
||||||
buttons.reply.click();
|
|
||||||
observer([{{target:targets.actions,isIntersecting:true,intersectionRatio:1}}]);
|
|
||||||
navigation.navigate('reply', {{focus:false}});
|
|
||||||
process.stdout.write(JSON.stringify({{changes,replyFocuses:targets.reply.focuses,current:buttons.reply.attributes['aria-current']}}));
|
|
||||||
"""
|
|
||||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
|
||||||
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
assert json.loads(result.stdout) == {
|
|
||||||
"changes": [["reply", False], ["actions", True]],
|
|
||||||
"replyFocuses": 1,
|
|
||||||
"current": "location",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_issue_sheet_ships_a_mobile_only_safe_area_navigation_rail():
|
def test_issue_sheet_ships_a_mobile_only_safe_area_navigation_rail():
|
||||||
html = (FRONTEND / "index.html").read_text()
|
html = (FRONTEND / "index.html").read_text()
|
||||||
css = (FRONTEND / "dashboard.css").read_text()
|
css = (FRONTEND / "dashboard.css").read_text()
|
||||||
|
|
|
||||||
|
|
@ -116,38 +116,6 @@ process.stdout.write(JSON.stringify({{current,rootMatches:observedRoot === panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_review_navigation_routes_taps_and_scrolls_without_focusing_on_restore():
|
|
||||||
script = f"""
|
|
||||||
const createNavigation=require({json.dumps(str(CONTROLLER))});
|
|
||||||
const element=name => ({{name,listeners:{{}},attributes:{{}},focuses:0,
|
|
||||||
addEventListener(key,fn) {{ this.listeners[key]=fn; }}, removeEventListener() {{}},
|
|
||||||
setAttribute(key,value) {{ this.attributes[key]=value; }}, removeAttribute(key) {{ delete this.attributes[key]; }},
|
|
||||||
scrollIntoView() {{}}, focus() {{ this.focuses += 1; }},
|
|
||||||
}});
|
|
||||||
const names=['overview','files','feedback','history'];
|
|
||||||
const buttons=Object.fromEntries(names.map(name => [name,element(name)]));
|
|
||||||
const targets=Object.fromEntries(names.map(name => [name,element(name)]));
|
|
||||||
const composer=element('composer'); const changes=[]; let observer;
|
|
||||||
const navigation=createNavigation({{buttons,targets,summaryComposer:composer,
|
|
||||||
onSectionChange:(name,options)=>changes.push([name,options.replace]),
|
|
||||||
observe(handler) {{ observer=handler; return {{disconnect() {{}}}}; }},
|
|
||||||
}});
|
|
||||||
navigation.start();
|
|
||||||
buttons.feedback.listeners.click({{preventDefault() {{}}}});
|
|
||||||
observer([{{target:targets.history,isIntersecting:true,intersectionRatio:1}}]);
|
|
||||||
navigation.navigate('feedback',{{focus:false}});
|
|
||||||
process.stdout.write(JSON.stringify({{changes,focuses:composer.focuses,current:buttons.feedback.attributes['aria-current']}}));
|
|
||||||
"""
|
|
||||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
|
||||||
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
assert json.loads(result.stdout) == {
|
|
||||||
"changes": [["feedback", False], ["history", True]],
|
|
||||||
"focuses": 1,
|
|
||||||
"current": "location",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_review_sheet_wires_a_mobile_safe_area_navigation_rail_and_offline_asset():
|
def test_review_sheet_wires_a_mobile_safe_area_navigation_rail_and_offline_asset():
|
||||||
html = (FRONTEND / "index.html").read_text()
|
html = (FRONTEND / "index.html").read_text()
|
||||||
css = (FRONTEND / "dashboard.css").read_text()
|
css = (FRONTEND / "dashboard.css").read_text()
|
||||||
|
|
|
||||||
|
|
@ -123,38 +123,6 @@ process.stdout.write(JSON.stringify({{current,disconnected}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_update_navigation_routes_taps_and_scrolls_without_focusing_on_restore():
|
|
||||||
script = f"""
|
|
||||||
const createNavigation=require({json.dumps(str(CONTROLLER))});
|
|
||||||
const element=name => ({{name,listeners:{{}},attributes:{{}},focuses:0,open:false,
|
|
||||||
addEventListener(key,fn) {{ this.listeners[key]=fn; }}, removeEventListener() {{}},
|
|
||||||
setAttribute(key,value) {{ this.attributes[key]=value; }}, removeAttribute(key) {{ delete this.attributes[key]; }},
|
|
||||||
scrollIntoView() {{}}, focus() {{ this.focuses += 1; }},
|
|
||||||
}});
|
|
||||||
const names=['activity','conversation','context','reply'];
|
|
||||||
const buttons=Object.fromEntries(names.map(name => [name,element(name)]));
|
|
||||||
const targets=Object.fromEntries(['conversation','context','reply'].map(name => [name,element(name)]));
|
|
||||||
const composer=element('composer'); const changes=[]; let observer;
|
|
||||||
const navigation=createNavigation({{buttons,targets,replyComposer:composer,jumpToNewActivity() {{}},
|
|
||||||
onSectionChange:(name,options)=>changes.push([name,options.replace]),
|
|
||||||
observe(handler) {{ observer=handler; return {{disconnect() {{}}}}; }},
|
|
||||||
}});
|
|
||||||
navigation.start();
|
|
||||||
buttons.reply.listeners.click({{preventDefault() {{}}}});
|
|
||||||
observer([{{target:targets.context,isIntersecting:true,intersectionRatio:1}}]);
|
|
||||||
navigation.navigate('reply',{{focus:false}});
|
|
||||||
process.stdout.write(JSON.stringify({{changes,focuses:composer.focuses,current:buttons.reply.attributes['aria-current']}}));
|
|
||||||
"""
|
|
||||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
|
||||||
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
assert json.loads(result.stdout) == {
|
|
||||||
"changes": [["reply", False], ["context", True]],
|
|
||||||
"focuses": 1,
|
|
||||||
"current": "location",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_sheet_wires_a_mobile_only_safe_area_navigation_rail():
|
def test_update_sheet_wires_a_mobile_only_safe_area_navigation_rail():
|
||||||
html = (FRONTEND / "index.html").read_text()
|
html = (FRONTEND / "index.html").read_text()
|
||||||
css = (FRONTEND / "dashboard.css").read_text()
|
css = (FRONTEND / "dashboard.css").read_text()
|
||||||
|
|
|
||||||
|
|
@ -1477,68 +1477,6 @@ process.stdout.write(JSON.stringify({{calls, hash:location.hash}}));
|
||||||
assert output["hash"] == "#/my-work/review/stackchain/dashboard/10"
|
assert output["hash"] == "#/my-work/review/stackchain/dashboard/10"
|
||||||
|
|
||||||
|
|
||||||
def test_work_route_sections_are_validated_back_safe_and_do_not_reopen_the_item():
|
|
||||||
script = f"""
|
|
||||||
const routes = require({json.dumps(str(WORK_ROUTE))});
|
|
||||||
const listeners = {{}};
|
|
||||||
const location = {{hash:'#/my-work/issue/stackchain/api/17'}};
|
|
||||||
const calls = [];
|
|
||||||
const stack = [location.hash];
|
|
||||||
let cursor = 0;
|
|
||||||
const history = {{
|
|
||||||
pushState(state, _, hash) {{ stack.splice(cursor + 1); stack.push(hash); cursor += 1; location.hash = hash; calls.push(['push', hash]); }},
|
|
||||||
replaceState(state, _, hash) {{ stack[cursor] = hash; location.hash = hash; calls.push(['replace', hash]); }},
|
|
||||||
back() {{ cursor -= 1; location.hash = stack[cursor]; listeners.popstate(); }},
|
|
||||||
}};
|
|
||||||
const controller = routes.createController({{
|
|
||||||
location, history,
|
|
||||||
eventTarget: {{addEventListener(name, fn) {{ listeners[name] = fn; }}}},
|
|
||||||
onOpen: item => calls.push(['open', item.number, item.section || null]),
|
|
||||||
onSection: (section, options) => calls.push(['section', section, options.restore]),
|
|
||||||
onClose() {{}}, onInvalid() {{}},
|
|
||||||
}});
|
|
||||||
controller.start();
|
|
||||||
controller.setItems([{{kind:'issue', repository:'stackchain/api', number:17}}]);
|
|
||||||
controller.section('reply');
|
|
||||||
controller.section('actions', {{replace:true}});
|
|
||||||
history.back();
|
|
||||||
process.stdout.write(JSON.stringify({{
|
|
||||||
calls,
|
|
||||||
hash:location.hash,
|
|
||||||
valid:[
|
|
||||||
routes.parse('#/my-work/issue/stackchain/api/17/reply'),
|
|
||||||
routes.parse('#/my-work/update/42/context'),
|
|
||||||
],
|
|
||||||
invalid:[
|
|
||||||
routes.parse('#/my-work/issue/stackchain/api/17/files'),
|
|
||||||
routes.parse('#/my-work/update/42/overview'),
|
|
||||||
routes.parse('#/my-work/review/stackchain/api/17/feedback/extra'),
|
|
||||||
],
|
|
||||||
}}));
|
|
||||||
"""
|
|
||||||
result = subprocess.run(
|
|
||||||
["node", "-e", script], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
assert json.loads(result.stdout) == {
|
|
||||||
"calls": [
|
|
||||||
["open", 17, None],
|
|
||||||
["push", "#/my-work/issue/stackchain/api/17/reply"],
|
|
||||||
["section", "reply", False],
|
|
||||||
["replace", "#/my-work/issue/stackchain/api/17/actions"],
|
|
||||||
["section", "actions", False],
|
|
||||||
["section", "overview", True],
|
|
||||||
],
|
|
||||||
"hash": "#/my-work/issue/stackchain/api/17",
|
|
||||||
"valid": [
|
|
||||||
{"kind": "issue", "repository": "stackchain/api", "number": 17, "section": "reply"},
|
|
||||||
{"kind": "update", "notification_id": 42, "section": "context"},
|
|
||||||
],
|
|
||||||
"invalid": [None, None, None],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_updates_inbox_route_survives_hydration_and_detail_back_navigation():
|
def test_updates_inbox_route_survives_hydration_and_detail_back_navigation():
|
||||||
script = f"""
|
script = f"""
|
||||||
const routes = require({json.dumps(str(WORK_ROUTE))});
|
const routes = require({json.dumps(str(WORK_ROUTE))});
|
||||||
|
|
@ -1752,7 +1690,7 @@ const calls = [];
|
||||||
async def test_dashboard_opens_delegated_filings_with_follow_up_only_capabilities():
|
async def test_dashboard_opens_delegated_filings_with_follow_up_only_capabilities():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
||||||
assert "else if (item.kind === 'issue' || item.kind === 'filed') await openIssueSheet(item, issueTrigger);" in html
|
assert "else if (item.kind === 'issue' || item.kind === 'filed') openIssueSheet(item, issueTrigger);" in html
|
||||||
assert "const readOnly = issueController.readOnly(item);" in html
|
assert "const readOnly = issueController.readOnly(item);" in html
|
||||||
assert "qs('#issue-sheet').classList.toggle('read-only', readOnly);" in html
|
assert "qs('#issue-sheet').classList.toggle('read-only', readOnly);" in html
|
||||||
assert "#issue-sheet.read-only .issue-comment-composer" not in html
|
assert "#issue-sheet.read-only .issue-comment-composer" not in html
|
||||||
|
|
@ -1792,24 +1730,6 @@ async def test_dashboard_wires_addressable_work_sheets_back_navigation_and_share
|
||||||
assert "openDeliveryReceiptRoute" not in html
|
assert "openDeliveryReceiptRoute" not in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_dashboard_wires_mobile_section_routes_to_each_detail_navigation():
|
|
||||||
html = await dashboard()
|
|
||||||
|
|
||||||
assert html.count(
|
|
||||||
"onSectionChange:(section, options) => workRoute.section(section, options)"
|
|
||||||
) == 4
|
|
||||||
assert "function navigateWorkSection(kind, section)" in html
|
|
||||||
assert "mobileIssueDetailNavigation.navigate(section, { focus:false })" in html
|
|
||||||
assert "mobilePullDetailNavigation.navigate(section, { focus:false })" in html
|
|
||||||
assert "mobileUpdateDetailNavigation.navigate(section, { focus:false })" in html
|
|
||||||
assert "mobileReviewDetailNavigation.navigate(section, { focus:false })" in html
|
|
||||||
assert "onSection: (section, options) => {" in html
|
|
||||||
assert "if (options.restore) navigateWorkSection(route?.kind, section);" in html
|
|
||||||
assert "await openRoutedWorkSection(item);" in html
|
|
||||||
assert "navigateWorkSection(item.kind, item.section);" in html
|
|
||||||
|
|
||||||
|
|
||||||
def test_my_work_queue_prioritizes_labels_then_reviews_and_keeps_repo_identity():
|
def test_my_work_queue_prioritizes_labels_then_reviews_and_keeps_repo_identity():
|
||||||
payload = {
|
payload = {
|
||||||
"user": {"login": "timmy"},
|
"user": {"login": "timmy"},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user