fix(webui): sp4 third review round, a move still writing never paints the board over the not-on-your-list page, an archived project keeps its filter while another filter matches none of its tasks (#1873, lane sp4)
This commit was merged in pull request #1969.
This commit is contained in:
1 parent
79df01f1de
commit
ed4f45ed23
2 files changed
+50
-7
No files matched your search
@@ -139,7 +139,7 @@
|
||||
// The address drops it too, so it never names a filter the board is not using. A dropped project
|
||||
// was filtered on the server, so it returns true and the read runs again without it.
|
||||
// A project is gone only when its organization is off the list, or answered with its projects and
|
||||
// the project is not among them and no task came back for it. An organization that is down,
|
||||
// the project is not among them and no task came back for it (with no other filter on). An organization that is down,
|
||||
// starting or timed out, or whose projects read failed, lists no projects, so its filter stays.
|
||||
// `at` is the address the read started from.
|
||||
function prune(at) {
|
||||
@@ -148,7 +148,9 @@
|
||||
if (filter.project && filter.project !== '-') {
|
||||
const po = snap.orgs.find(o => o.id === filter.project.split(':')[0]);
|
||||
const answered = !po || (po.state === 'ok' && !po.errors?.projects);
|
||||
if (answered && !snap.tasks.length && !snap.projects.some(p => `${p.org}:${p.id}` === filter.project)) { filter.project = ''; gone.project = null; }
|
||||
// No task back is proof only when no other filter narrowed the read on the server.
|
||||
const narrowed = filter.state || filter.assignee || filter.label;
|
||||
if (answered && !narrowed && !snap.tasks.length && !snap.projects.some(p => `${p.org}:${p.id}` === filter.project)) { filter.project = ''; gone.project = null; }
|
||||
}
|
||||
// After this render: bus.js drops a render whose address changed while it ran. Only while the
|
||||
// address is still the one the read started from, and with a repaint in case the render in
|
||||
@@ -325,7 +327,8 @@
|
||||
// Repaint in place, for the board's own changes (filters, layout, a write).
|
||||
function paint(part = 'all') {
|
||||
const root = document.querySelector('[data-tasks-board]');
|
||||
if (!root || !snap) return;
|
||||
// Only over a board drawn from a read: never over the not-on-your-list page or a failed read.
|
||||
if (!root || !snap || !root.querySelector('[data-tb-results]')) return;
|
||||
const kept = snapshotView(root);
|
||||
if (part === 'results') root.querySelector('[data-tb-results]').outerHTML = results();
|
||||
else root.outerHTML = markup();
|
||||
@@ -350,6 +353,8 @@
|
||||
if (dragging) await Promise.race([dragEnded, new Promise(r => setTimeout(r, 30000))]);
|
||||
// Two renders at once (a first load, a poll during a filter change): the replaced one draws from the newer read.
|
||||
try { let p = reload(); while (!(await p) && newest !== p) p = newest; } catch (err) { snap = null; return failed(err); }
|
||||
// A not-on-your-list page opened meanwhile dropped every read; bus.js drops this render as not current.
|
||||
if (!snap) return '';
|
||||
// #/orgs/<id>/tasks?task=<ref> opens that task once, after bus.js has drawn the board.
|
||||
const want = scope ? String(q.get('task') || '') : '';
|
||||
if (!want) autoOpened = null;
|
||||
|
||||
@@ -18,7 +18,7 @@ const waitFor = b => expression => b.evaluate(`(async()=>{for(let i=0;i<200;i++)
|
||||
const at = minutesAgo => new Date(Date.now() - minutesAgo * 60000).toISOString();
|
||||
const D = n => String(n).repeat(64);
|
||||
|
||||
function fixture({ empty = false } = {}) {
|
||||
function fixture({ empty = false, archived = [] } = {}) {
|
||||
const LIST = [
|
||||
{ id: 'acme', label: 'Acme Studio', theme: 'cobalt', busHealthy: true },
|
||||
{ id: 'beta', label: 'Beta Works', theme: 'grove', busHealthy: true },
|
||||
@@ -33,6 +33,7 @@ function fixture({ empty = false } = {}) {
|
||||
beta: [{ n: 2, title: 'Price page', state: 'in-progress', group: 'shop', assignees: [] }, { n: 3, title: 'Spring banner', state: 'todo', group: 'ads', assignees: [] }],
|
||||
};
|
||||
if (empty) { STORE.acme = []; STORE.beta = []; }
|
||||
for (const [org, list] of Object.entries(PROJECTS)) for (const p of list) p.archived = archived.includes(`${org}:${p.id}`);
|
||||
const tasks = id => (STORE[id] || []).map(t => ({ source: 'store', task_ref: `task:${t.n}`, at: at(10), priority: 0, digest: D(t.n), requirement: 'REQ-WEB-1',
|
||||
fields: { title: t.title, state: t.state, group: t.group, assignees: t.assignees, due_date: null } }));
|
||||
const writes = [];
|
||||
@@ -80,8 +81,8 @@ const STUB = noWrite => `window.__noWrite = ${JSON.stringify(noWrite)};
|
||||
};
|
||||
})();`;
|
||||
|
||||
async function setup(t, { viewport = [1440, 900], hash = '#/tasks', empty = false, noWrite = [] } = {}) {
|
||||
const orgs = fixture({ empty });
|
||||
async function setup(t, { viewport = [1440, 900], hash = '#/tasks', empty = false, noWrite = [], archived = [] } = {}) {
|
||||
const orgs = fixture({ empty, archived });
|
||||
const server = await startServer({ port: 0, sso: createDevOwner(), organizations: orgs, send: async () => ({}), log: () => {} });
|
||||
t.after(() => close(server));
|
||||
const b = await browser({ machine: false, args: ['--disable-extensions'] }); t.after(() => b.close());
|
||||
@@ -206,11 +207,18 @@ test('a card opened from the system board opens the task in its organization', a
|
||||
});
|
||||
|
||||
// Holds each task board read for ?hold= ms (or window.__hold, set later) and counts the reads that
|
||||
// started and the ones that came back, so a test can act while a read is in flight.
|
||||
// started and the ones that came back, so a test can act while a read is in flight. A task write is
|
||||
// held for window.__holdW ms and counted the same way.
|
||||
const HOLD = `window.__hold = Number(new URLSearchParams(location.search).get('hold')) || 0; window.__started = 0; window.__landed = 0;
|
||||
window.__holdW = 0; window.__wStarted = 0; window.__wLanded = 0;
|
||||
(() => {
|
||||
const real = window.fetch.bind(window);
|
||||
window.fetch = async (url, init = {}) => {
|
||||
if (String(url) === '/api/workspace/tasks' && init.method === 'POST' && window.__holdW) {
|
||||
window.__wStarted++;
|
||||
await new Promise(r => setTimeout(r, window.__holdW));
|
||||
try { return await real(url, init); } finally { window.__wLanded++; }
|
||||
}
|
||||
if (!/^\\/api\\/workspace\\?sections=tasks(?:%2C|,)projects/.test(String(url))) return real(url, init);
|
||||
window.__started++;
|
||||
if (window.__hold) await new Promise(r => setTimeout(r, window.__hold));
|
||||
@@ -263,3 +271,33 @@ test('a read in flight does not paint the board over the page for an address no
|
||||
assert.match(await b.evaluate('document.querySelector("[data-tasks-board]")?.textContent || ""'), /not on your list/);
|
||||
assert.equal(await b.evaluate(cards), '[]');
|
||||
});
|
||||
|
||||
test('a move still writing when the not-on-your-list page opens does not paint the board over it', async t => {
|
||||
const { b, base, wait } = await setup(t);
|
||||
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: HOLD });
|
||||
await b.navigate(`${base}/?reload=1#/tasks`);
|
||||
await wait(`${cards} === ${ALL}`);
|
||||
await b.evaluate('document.querySelector("[data-tb-layout=list]").click()');
|
||||
await wait('document.querySelector("[data-tb-move=\'beta|task:2\']")');
|
||||
await b.evaluate('window.__holdW = 800; window.__started = 0; window.__landed = 0');
|
||||
await b.evaluate('(() => { const s = document.querySelector("[data-tb-move=\'beta|task:2\']"); s.value = "in-review"; s.dispatchEvent(new Event("change", { bubbles: true })); })()');
|
||||
await wait('window.__wStarted >= 1');
|
||||
await b.evaluate('location.hash = "#/orgs/Not%20An%20Id/tasks"');
|
||||
await wait('/not on your list/.test(document.querySelector("[data-tasks-board]")?.textContent || "")');
|
||||
await wait('window.__wLanded >= 1 && window.__landed >= window.__started');
|
||||
await settle(b);
|
||||
assert.match(await b.evaluate('document.querySelector("[data-tasks-board]")?.textContent || ""'), /not on your list/);
|
||||
assert.equal(await b.evaluate(cards), '[]');
|
||||
});
|
||||
|
||||
test('an archived project keeps its filter while it has tasks, and while another filter matches none of them', async t => {
|
||||
const { b, wait } = await setup(t, { hash: '#/tasks?project=beta%3Ashop', archived: ['beta:shop'] });
|
||||
await wait(`${cards} === ${JSON.stringify(JSON.stringify(['beta|task:2']))}`);
|
||||
await settle(b);
|
||||
assert.equal(await b.evaluate('location.hash'), '#/tasks?project=beta%3Ashop');
|
||||
// No shop task is done: the read comes back empty because of the state filter, not because shop is gone.
|
||||
await b.evaluate('(() => { const s = document.querySelector("[data-tb-filter=state]"); s.value = "done"; s.dispatchEvent(new Event("change", { bubbles: true })); })()');
|
||||
await wait(`${cards} === "[]" && /No tasks match/.test(document.querySelector("[data-tb-results]")?.textContent || "")`);
|
||||
await settle(b);
|
||||
assert.equal(await b.evaluate('location.hash'), '#/tasks?project=beta%3Ashop');
|
||||
});
|
||||
Reference in new issue
Block a user