// 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 () => [] }, }; for (const [width, route, where] of [[1440, 'orgs/acme/organization', 'the organization page'], [390, 'orgs/acme/organization', 'the organization page'], [1440, 'orgs/acme/side-by-side?a=acme/organization&b=acme/agents', 'a pane'], [390, 'orgs/acme/side-by-side?a=acme/agents&b=acme/organization&show=b', 'a pane']]) { 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'); }); }