The sidebar Assistant opens one system page, #/assistant. Each pane is the chat thread view of its own organization: it reads, sends and updates through that organization's routes, with a header carrying the title, the organization chip, the project chip and a close control. "Open thread" lists the threads of every answering organization, grouped by organization, and starts a new thread in a chosen one. The open set and its order live in the hash (open=org:id,...&show=org:id), validated on read. A stopped organization is named at the top and in its own pane; the other panes keep working. At 390 one pane shows with a tab strip. Side by side is removed (panes.js, panes.css and its test); its old address lands on the Assistant page.
84 lines
6.2 KiB
JavaScript
84 lines
6.2 KiB
JavaScript
// E2E6 P2-11, P2-12, P2-13 (#1873): layout pins measured in a real browser.
|
|
// P2-11: a project's header stays inside its column at desktop widths, so its actions are not cut off.
|
|
// P2-12: at phone width the project tabs fade at the right edge while more tabs lie past it, and the
|
|
// fade is gone once the row is scrolled to its end or when every tab fits.
|
|
// P2-13: Restart bus and Stop bus share one top and height, on the organization page and in a pane.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { fixture, close } from './fixture.mjs';
|
|
import { browser } from './browser.mjs';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { createDevOwner } from '../src/dev-owner.mjs';
|
|
import { P, stub } from './helpers/projects-review-fixture.mjs';
|
|
|
|
const until = (b, expression, what) => b.evaluate(`(async()=>{for(let i=0;i<300;i++){if(${expression})return true;await new Promise(r=>setTimeout(r,25))}throw Error(${JSON.stringify(what)})})()`);
|
|
const frames = b => b.evaluate('new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(()=>r(true))))');
|
|
|
|
async function project(t, width) {
|
|
const f = await fixture(), b = await browser({ machine: false, args: ['--disable-extensions'] });
|
|
t.after(async () => { await b.close(); await f.close(); });
|
|
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: stub });
|
|
await b.viewport(width, 900);
|
|
await b.navigate(`${f.base}/#/projects/${P}`);
|
|
await until(b, `document.querySelector('.project-header .project-actions')`, 'project header');
|
|
await b.evaluate('new Promise(r=>setTimeout(r,400))');
|
|
return b;
|
|
}
|
|
|
|
for (const width of [1440, 1280]) {
|
|
test(`E2E6 P2-11: the project header and its actions stay inside the column at ${width}`, { timeout: 60000 }, async t => {
|
|
const b = await project(t, width);
|
|
const m = JSON.parse(await b.evaluate(`JSON.stringify((()=>{const h=document.querySelector('.project-header'),p=h.parentElement.getBoundingClientRect(),r=h.getBoundingClientRect();return {right:r.right,parent:p.right,actions:[...h.querySelectorAll('.project-actions > *')].map(a=>a.getBoundingClientRect().right),page:[document.documentElement.scrollWidth,innerWidth]}})())`));
|
|
assert.ok(m.right <= m.parent + 0.5, `header right ${m.right} inside its column ${m.parent}`);
|
|
for (const a of m.actions) assert.ok(a <= m.parent + 0.5, `action right ${a} inside ${m.parent}`);
|
|
assert.ok(m.page[0] <= m.page[1], 'no sideways page scroll');
|
|
});
|
|
}
|
|
|
|
test('E2E6 P2-12: at 390 the project tabs fade at the edge until scrolled to the end', { timeout: 60000 }, async t => {
|
|
const b = await project(t, 390);
|
|
const read = () => b.evaluate(`JSON.stringify((()=>{const e=document.querySelector('.project-tabs'),s=getComputedStyle(e);return {mask:s.maskImage||s.webkitMaskImage,fade:s.getPropertyValue('--tabs-fade').trim(),sw:e.scrollWidth,cw:e.clientWidth}})())`).then(JSON.parse);
|
|
const start = await read();
|
|
assert.ok(start.sw > start.cw, `the tabs overflow (${start.sw} > ${start.cw})`);
|
|
assert.match(start.mask, /linear-gradient/, 'a fade mask is set');
|
|
assert.equal(start.fade, '48px', 'a 48px fade while more tabs lie past the edge');
|
|
await b.evaluate(`{const e=document.querySelector('.project-tabs');e.scrollLeft=e.scrollWidth}true`);
|
|
await frames(b);
|
|
assert.equal((await read()).fade, '0px', 'no fade once the row is scrolled to its end');
|
|
// Review P3: a strip whose tabs all fit has nothing past the edge, so it has no fade.
|
|
await b.evaluate(`{const e=document.querySelector('.project-tabs');[...e.children].slice(1).forEach(c=>c.style.display='none');e.scrollLeft=0}true`);
|
|
await frames(b);
|
|
const fits = await read();
|
|
assert.ok(fits.sw <= fits.cw, `the remaining tab fits (${fits.sw} <= ${fits.cw})`);
|
|
assert.equal(fits.fade, '0px', 'no fade when nothing is cut off');
|
|
});
|
|
|
|
const org = {
|
|
busHealthy: true, decisionWrite: async () => ({}), send: async () => ({}),
|
|
requestRead: async () => { throw Object.assign(new Error('not-configured'), { code: 'not-configured' }); },
|
|
reads: { business: async () => ({ business: { id: 'acme', projects: [], instances: [] } }), settings: async () => ({}), queue: async () => ({ rows: [] }) },
|
|
bus: { instances: async () => ({ instances: {} }), agents: async () => [], 'thread.claims': async () => [], tasks: async () => [], inbox: async () => [], messages: async () => ({ messages: [] }) },
|
|
chat: { projects: async () => [], threads: async () => [] },
|
|
};
|
|
// SP6 (#1873) removed Side by side, whose panes also held the organization page; the Assistant
|
|
// page's panes hold chat threads only, so the organization page itself is the case left.
|
|
for (const [width, route, where] of [[1440, 'orgs/acme/organization', 'the organization page'], [390, 'orgs/acme/organization', 'the organization page']]) {
|
|
test(`E2E6 P2-13: Restart bus and Stop bus line up on ${where} at ${width}`, { timeout: 60000 }, async t => {
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), send: async () => ({}), log: () => {}, organizations: { list: async () => [{ id: 'acme', label: 'Acme', busHealthy: true }], resolve: async () => org } });
|
|
t.after(() => close(server));
|
|
const b = await browser({ machine: false, args: ['--disable-extensions'] });
|
|
t.after(() => b.close());
|
|
await b.viewport(width, 900);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/${route}`);
|
|
const pair = `[...document.querySelectorAll('[data-bus-restart],[data-bus-restart-off],[data-bus-stop],[data-bus-stop-off]')].filter(e=>e.getClientRects().length)`;
|
|
await until(b, `${pair}.length===2`, 'the two bus buttons');
|
|
await b.evaluate('new Promise(r=>setTimeout(r,300))');
|
|
const [restart, stop] = JSON.parse(await b.evaluate(`JSON.stringify(${pair}.map(e=>{const r=e.getBoundingClientRect();return {text:e.textContent.trim(),top:r.top,height:r.height,left:r.left,right:r.right}}))`));
|
|
assert.equal(restart.text, 'Restart bus');
|
|
assert.equal(stop.text, 'Stop bus');
|
|
assert.ok(Math.abs(restart.top - stop.top) < 0.5, `same top (${restart.top} vs ${stop.top})`);
|
|
assert.ok(Math.abs(restart.height - stop.height) < 0.5, `same height (${restart.height} vs ${stop.height})`);
|
|
assert.ok(stop.left - restart.right >= 4, 'a gap between them');
|
|
});
|
|
}
|