Files
stack/packages/webui/tests/s8-header-browser.test.mjs

67 lines
4.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { fixture } from './fixture.mjs';
import { browser } from './browser.mjs';
import { P, stub } from './helpers/projects-review-fixture.mjs';
async function setup(t, width, before = '') {
const f = await fixture(), b = await browser({machine:false});
t.after(async () => { await b.close(); await f.close(); });
await b.call('Fetch.enable', {patterns:[{urlPattern:'*'}]});
b.on('Fetch.requestPaused', p => void b.call(p.request.url.startsWith(f.base+'/') ? 'Fetch.continueRequest' : 'Fetch.failRequest', p.request.url.startsWith(f.base+'/') ? {requestId:p.requestId} : {requestId:p.requestId,errorReason:'BlockedByClient'}).catch(()=>{}));
await b.call('Page.addScriptToEvaluateOnNewDocument', {source:stub + before});
await b.viewport(width, width === 390 ? 900 : 1000);
await b.navigate(f.base+'/#/projects/'+P);
const wait = expr => b.evaluate(`(async()=>{for(let i=0;i<180;i++){if(${expr})return;await new Promise(r=>setTimeout(r,30))}throw Error('Timed out: '+${JSON.stringify(expr)})})()`);
await wait(`document.querySelector('[data-project-content] [data-project-room-status]')`);
return {b,wait};
}
async function accessibleName(b, selector) {
const {result} = await b.call('Runtime.evaluate', {expression:`document.querySelector(${JSON.stringify(selector)})`});
const {nodes} = await b.call('Accessibility.getPartialAXTree', {objectId:result.objectId, fetchRelatives:false});
return nodes[0].name.value;
}
for (const width of [1440,390]) {
test(`S8 project action icons, accessible names and positions at ${width}`, async t => {
const {b,wait} = await setup(t,width);
const expected = width === 1440 ? [[1060,1190],[1202,1290],[1302,1418]] : [[12,132],[140,220],[228,340]];
const y = width === 1440 ? [130,174] : [198,242];
for (const route of ['', '/conversations', '/tasks', '/requirements', '/files', '/settings', ...(width === 1440 ? ['/room'] : [])]) {
if (route) {
await b.evaluate(`location.hash=${JSON.stringify(route === '/room' ? '#/projects/rooms/'+P : '#/projects/'+P+route)}`);
await wait(`document.querySelector('.project-header')?.dataset.projectSection===${JSON.stringify(route.slice(1))}`);
}
for (const [i,name] of ['North star','PRD','Settings'].entries()) {
const selector = `.project-header .project-primary-action:nth-of-type(${i+1})`;
const data = await b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(selector)}),r=e.getBoundingClientRect();return {icon:!!e.querySelector('svg[aria-hidden="true"]'),left:r.left,right:r.right,top:r.top,bottom:r.bottom}})()`);
assert.equal(await accessibleName(b,selector),name,`${route || '/overview'} accessible name`);
assert.ok(data.icon,`${route || '/overview'} ${name} missing decorative icon`);
for (const [actual,want] of [[data.left,expected[i][0]],[data.right,expected[i][1]],[data.top,y[0]],[data.bottom,y[1]]]) assert.ok(Math.abs(actual-want)<=4,`${route || '/overview'} ${name}: ${JSON.stringify(data)} expected x=${expected[i]} y=${y}`);
}
}
});
test(`S8 Overview New conversation has a decorative plus at ${width}`, async t => {
const {b} = await setup(t,width);
const selector = '[data-project-content] .project-section-title .btn';
assert.equal(await accessibleName(b,selector),'New conversation');
assert.equal(await b.evaluate(`!!document.querySelector('${selector} svg[aria-hidden="true"]')`),true);
});
}
test('S8 room state and link remain in Project facts, never the Overview header', async t => {
const {b,wait} = await setup(t,1440);
const facts = '[data-project-content] aside [data-project-room-status]';
for (const [state, expected] of [['open','Open'],['closed','Closed'],['unavailable','Unavailable']]) {
if (state === 'closed') await b.evaluate(`reviewClosed=true;document.querySelector('#refresh').click()`);
if (state === 'unavailable') await b.evaluate(`reviewFail['/api/chat/rooms/${P}/messages?limit=100']=true;document.querySelector('#refresh').click()`);
await wait(`document.querySelector('${facts}')?.textContent.includes('${expected}')`);
const result = await b.evaluate(`(()=>{const h=document.querySelector('.project-header'),f=document.querySelector('${facts}');return {header:h.textContent,headerLink:!![...h.querySelectorAll('a')].find(a=>a.textContent.trim()==='Read room'),facts:f.textContent,link:f.querySelector('a')?.getAttribute('href')}})()`);
assert.doesNotMatch(result.header,/Room:/);
assert.equal(result.headerLink,false);
assert.match(result.facts,new RegExp('Room: '+expected));
assert.equal(result.link,'#/projects/rooms/'+P);
}
});