clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / scripts / qa / ux-browser-menu.js
ux-browser-menu.js 5.9K
1// Run after signing in to the disposable UX relay as its fixture owner.2async page => {3 const base = await page.evaluate(() => location.origin);4 const storageState = await page.context().storageState();5 const results = [], errors = [];6 const check = (ok, message) => { if (!ok) throw Error(message); };7 const run = async (name, action) => {8 try { results.push({name, ok:true, detail:await action()}); }9 catch (error) { results.push({name, ok:false, error:error.message}); }10 };11 await page.goto(base+'/manage/connect');12 check(await page.locator('#session-logout').count() === 1, 'Sign in as the fixture owner before running menu checks.');13 const routes = ['/', '/search', '/repos', '/files', '/sites', '/articles', '/inbox', '/outbox', '/signin', '/terms', '/tools', '/private', '/chat', '/media', '/marmot', '/grasp'];14 routes.push(...await page.locator('#rail nav a').evaluateAll(nodes => nodes.map(e=>e.getAttribute('href'))));15 await page.goto(base+'/repos');16 const repo = await page.getByRole('link', {name:'ux-edge-repo', exact:true}).getAttribute('href');17 await page.goto(base+repo);18 routes.push(...await page.locator('#rail nav a').evaluateAll(nodes => nodes.map(e=>e.getAttribute('href'))));19 await page.goto(base+'/files');20 routes.push(...await page.locator('#rows a').evaluateAll(nodes => nodes.map(e=>e.getAttribute('href'))));21 await page.goto(base+'/articles');22 routes.push(await page.locator('#content a[href^="/e/"]').first().getAttribute('href'));23 await page.goto(base+'/');24 routes.push(await page.locator('#content a[href^="/e/"]').last().getAttribute('href'));25 routes.push('/a/ux-long-article', '/r/main/articles', '/r/main/manage/connect');26 for (const javaScriptEnabled of [true, false]) {27 const context = await page.context().browser().newContext({javaScriptEnabled, storageState, viewport:{width:360,height:800}});28 try {29 const probe = await context.newPage();30 probe.on('pageerror', error=>errors.push(error.message));31 const waitForMenu = async () => {32 // Poll outside the page because disabled JavaScript also disables browser timers.33 const deadline = Date.now()+2000;34 while ((await probe.locator('#rail').boundingBox()).x<0 && Date.now()<deadline) await probe.waitForTimeout(25);35 check((await probe.locator('#rail').boundingBox()).x===0,'menu did not finish opening');36 };37 for (const path of [...new Set(routes)]) await run(`${javaScriptEnabled?'JS':'no JS'} ${path}`, async () => {38 await probe.setViewportSize({width:360,height:800});39 const response = await probe.goto(base+path);40 check(response.ok(), 'HTTP '+response.status());41 check(await probe.locator('#menu').isVisible(), 'menu control missing');42 check(!await probe.locator('#rail').isVisible(), 'navigation starts expanded');43 check(await probe.locator('#rail nav').count()>0, 'navigation missing');44 const before = await probe.locator('#content').boundingBox();45 check(before.y<80, 'navigation pushes content down');46 await probe.locator('#menu').click();47 await waitForMenu();48 check(await probe.locator('#rail').isVisible(), 'menu did not open');49 check(await probe.locator('#rail nav a').first().isVisible(), 'menu links hidden');50 check((await probe.locator('#content').boundingBox()).y===before.y, 'opening menu moves content');51 await probe.locator('#menu').click();52 check(!await probe.locator('#rail').isVisible(), 'menu did not close');53 await probe.locator('#menu').focus();54 await probe.keyboard.press('Enter');55 await waitForMenu();56 check(await probe.locator('#rail').isVisible(), 'Enter did not open menu');57 await probe.keyboard.press('Space');58 check(!await probe.locator('#rail').isVisible(), 'Space did not close menu');59 await probe.setViewportSize({width:1440,height:1000});60 check(await probe.locator('#rail').isVisible(), 'desktop sidebar missing');61 check(!await probe.locator('#menu').isVisible(), 'mobile menu appears on desktop');62 return {title:await probe.title(),contentTop:before.y};63 });64 await run(`${javaScriptEnabled?'JS':'no JS'} navigation resets the menu`, async () => {65 await probe.setViewportSize({width:360,height:800});66 await probe.goto(base+'/repos');67 if(javaScriptEnabled) await probe.evaluate(()=>{window.__menuDocument='preserved';});68 for(const label of ['/articles','/files','/repos']) {69 await probe.locator('#menu').click();70 await waitForMenu();71 await probe.locator('#rail').getByRole('link',{name:label,exact:true}).click();72 await probe.waitForURL(base+label);73 await probe.waitForFunction(()=>document.querySelector('#content').getAttribute('aria-busy')!=='true');74 check(!await probe.locator('#rail').isVisible(),'navigation left menu open');75 }76 if(javaScriptEnabled) {77 check(await probe.evaluate(()=>window.__menuDocument)==='preserved','navigation reloaded document');78 await probe.locator('#menu').click();79 await probe.keyboard.press('Escape');80 check(!await probe.locator('#rail').isVisible(),'Escape did not close menu');81 check(await probe.locator('#menu').evaluate(e=>e===document.activeElement),'Escape did not restore menu focus');82 }83 });84 if(!javaScriptEnabled) {85 await context.clearCookies();86 await probe.goto(base+'/articles');87 await probe.screenshot({path:'output/playwright/ux/nojs-mobile.png'});88 await probe.locator('#menu').click();89 await waitForMenu();90 await probe.screenshot({path:'output/playwright/ux/nojs-mobile-menu-open.png'});91 }92 } finally {await context.close();}93 }94 results.push({name:'uncaught browser errors',ok:errors.length===0,errors});95 return {passed:results.filter(x=>x.ok).length,failed:results.filter(x=>!x.ok).length,results};96}