tinyrelayfile
tinyrelay/scripts/qa/ux-browser-menu.js/

scripts/qa/ux-browser-menu.js raw
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}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs