| 1 | // Run after signing in to the disposable UX relay as its fixture owner. |
| 2 | async 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 | } |