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); } });