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

scripts/qa/ux-browser-flow.js raw
1// Run with playwright-cli run-code --filename after opening the disposable UX relay.
2async page => {
3 const base = await page.evaluate(() => location.origin);
4 const results = [], errors = [];
5 const onError = error => errors.push(error.message);
6 page.on('pageerror', onError);
7 const check = (ok, message) => { if (!ok) throw Error(message); };
8 const run = async (name, action) => {
9 try { const detail = await action(); results.push({name, ok:true, detail}); }
10 catch (error) { results.push({name, ok:false, error:error.message}); }
11 };
12 const clickNav = async name => {
13 const link = page.locator('#rail').getByRole('link', {name, exact:true});
14 const href = await link.getAttribute('href');
15 await link.click();
16 await page.waitForURL(href.startsWith('/') ? base+href : href);
17 await page.waitForFunction(() => document.querySelector('#content')?.getAttribute('aria-busy') !== 'true');
18 };
19 const forms = () => page.evaluate(() => [...document.querySelectorAll('rpc-form,signed-form,publish-list')].map(e => ({tag:e.localName,method:e.getAttribute('method'),form:!!e.form?.isConnected,output:!!e.output?.isConnected,nested:!!e.querySelector('form form')})));
20 await page.setViewportSize({width:1440,height:1000});
21 await page.goto(base + '/repos');
22 await page.evaluate(() => { window.__uxMarker = 'preserved'; });
23 await run('repository navigation preserves document and ref filters', async () => {
24 await page.getByRole('link', {name:'ux-edge-repo',exact:true}).click();
25 await page.waitForURL('**/repo?**');
26 await clickNav('/code');
27 await page.locator('#ref-select').selectOption('refs/heads/feature/café');
28 await page.locator('#panel form').getByRole('button', {name:'go',exact:true}).click();
29 await page.waitForFunction(() => new URL(location.href).searchParams.get('ref') === 'refs/heads/feature/café');
30 check(await page.evaluate(() => window.__uxMarker) === 'preserved', 'navigation reloaded the document');
31 const links = await page.locator('#tree a').evaluateAll(nodes => nodes.map(n => ({name:n.textContent,href:n.href})));
32 check(links.length > 0, 'tree links missing');
33 await clickNav('/history');
34 await clickNav('/activity');
35 await clickNav('/code');
36 await page.screenshot({path:'output/playwright/ux/repo-desktop.png'});
37 return {links:links.length,url:page.url()};
38 });
39 await run('search GET submission and history', async () => {
40 await clickNav('← all repositories');
41 await clickNav('/search');
42 await page.locator('#search input[name=q]').fill('fixture');
43 await page.locator('#search').getByRole('button', {name:'Search',exact:true}).click();
44 await page.waitForFunction(() => new URL(location.href).searchParams.get('q') === 'fixture');
45 const searched = page.url();
46 await clickNav('/articles');
47 await page.goBack();
48 await page.waitForFunction(() => document.querySelector('#search input[name=q]')?.value === 'fixture');
49 check(page.url() === searched, 'back URL incorrect');
50 await page.goForward();
51 await page.waitForFunction(() => document.querySelector('#content h1')?.textContent === 'Articles');
52 check(await page.evaluate(() => window.__uxMarker) === 'preserved', 'filter or history reloaded document');
53 });
54 await run('repeated signed forms survive swaps', async () => {
55 await clickNav('/manage');
56 for (const name of ['/connect','/identity','/connect','/people','/connect']) {
57 await clickNav(name);
58 const current = await forms();
59 check(current.length > 0 && current.every(x => x.form && x.output && !x.nested), 'detached or nested forms: '+JSON.stringify(current));
60 }
61 await page.waitForFunction(() => !!window.nostr?.signEvent);
62 await page.waitForFunction(() => document.querySelector('connect-list #rows tbody tr[data-index]') !== null);
63 check(await page.locator('connect-list select[name=template] option').count() > 1, 'connect list offers no catalog cards');
64 await clickNav('/people');
65 const list = page.locator('rpc-form[method=listmembers]');
66 let submissions = 0;
67 const count = request => { if (request.method()==='POST' && request.url().endsWith('/manage/rpc')) submissions++; };
68 page.on('request', count);
69 try {
70 await list.evaluate(node => { node.form.requestSubmit(); node.form.requestSubmit(); });
71 await page.waitForFunction(() => document.querySelector('rpc-form[method=listmembers] output')?.textContent === 'Done.');
72 check(submissions===1, 'duplicate form submissions sent '+submissions+' signed requests');
73 } finally { page.off('request',count); }
74 await clickNav('/connect');
75 await page.waitForFunction(() => document.querySelector('connect-list #rows tbody tr[data-index]') !== null);
76 const result = await forms();
77 await page.screenshot({path:'output/playwright/ux/connect-desktop.png'});
78 return result;
79 });
80 await run('job status stream closes when leaving the page', async () => {
81 await clickNav('/sync');
82 await page.locator('#watch-jobs').click();
83 await page.waitForFunction(() => {
84 const requests = document.querySelector('#watch-jobs').__fixi?.requests;
85 const config = requests && [...requests].find(cfg => cfg.sse);
86 if (config) { window.__uxStream = config.sse; return true; }
87 return false;
88 });
89 await clickNav('/connect');
90 check(await page.evaluate(() => window.__uxStream.closed), 'job stream remained open after navigation');
91 });
92 await run('file viewers preserve one content section and media bounds', async () => {
93 await page.goto(base+'/files');
94 const links = await page.locator('#rows a').evaluateAll(nodes => nodes.map(n => n.href));
95 check(links.length >= 4, 'fixture files missing');
96 const visited=[];
97 for (const href of links.slice(0,4)) {
98 await page.goto(href);
99 const info = await page.evaluate(() => ({sections:document.querySelectorAll('#content>section').length,children:[...document.querySelector('#content').children].map(e=>e.localName),overflow:document.documentElement.scrollWidth>innerWidth,raw:document.querySelector('#facts a')?.getAttribute('fx-action'),forms:[...document.querySelectorAll('rpc-form,signed-form')].every(e=>e.form?.isConnected)}));
100 check(info.sections===1 && info.children.length===1, 'file viewer has content outside section: '+JSON.stringify(info));
101 check(!info.raw, 'download intercepted by fixi');
102 check(info.forms, 'file forms disconnected');
103 visited.push(info);
104 }
105 await page.setViewportSize({width:360,height:800});
106 const mobile = await page.evaluate(() => ({width:innerWidth,scroll:document.documentElement.scrollWidth,content:document.querySelector('#content').scrollWidth}));
107 check(mobile.scroll<=mobile.width && mobile.content<=mobile.width, 'mobile file page overflows: '+JSON.stringify(mobile));
108 await page.screenshot({path:'output/playwright/ux/file-mobile.png'});
109 return {visited,mobile};
110 });
111 await run('mobile menu and theme survive navigation', async () => {
112 await page.goto(base+'/');
113 await page.setViewportSize({width:360,height:800});
114 await page.locator('#menu').click();
115 check(await page.locator('#nav-menu').evaluate(e=>e.open),'menu did not open');
116 await clickNav('/articles');
117 check(!await page.locator('#nav-menu').evaluate(e=>e.open),'navigation did not close menu');
118 await page.locator('#menu').click();
119 check(await page.locator('#nav-menu').evaluate(e=>e.open),'morphed menu did not open');
120 await page.keyboard.press('Escape');
121 check(!await page.locator('#nav-menu').evaluate(e=>e.open),'Escape did not close menu');
122 await page.locator('#menu').click();
123 const before = await page.locator('html').getAttribute('data-theme');
124 await page.locator('#theme').click();
125 check(await page.locator('html').getAttribute('data-theme') !== before,'morphed theme button inactive');
126 await page.setViewportSize({width:1440,height:1000});
127 check(await page.locator('#rail').getAttribute('aria-hidden')!=='true','desktop rail inaccessible');
128 });
129 await run('no JavaScript mobile content and navigation', async () => {
130 const context=await page.context().browser().newContext({javaScriptEnabled:false,viewport:{width:360,height:800}});
131 try {
132 const noJS=await context.newPage();
133 await noJS.goto(base+'/repos');
134 check(await noJS.locator('#menu').isVisible(),'no-JS menu missing');
135 check(!await noJS.locator('#rail').isVisible(),'no-JS navigation starts expanded');
136 await noJS.locator('#menu').click();
137 const deadline=Date.now()+2000;
138 while ((await noJS.locator('#rail').boundingBox()).x<0 && Date.now()<deadline) await noJS.waitForTimeout(25);
139 check((await noJS.locator('#rail').boundingBox()).x===0,'no-JS menu did not finish opening');
140 check(await noJS.locator('#rail').isVisible(),'no-JS menu did not open');
141 await noJS.locator('#rail').getByRole('link',{name:'/articles',exact:true}).click();
142 check(noJS.url()===base+'/articles','no-JS rail link inaccessible');
143 check(await noJS.locator('#content h1').first().textContent()==='Articles','no-JS articles missing');
144 check(!await noJS.locator('#rail').isVisible(),'no-JS navigation did not reset closed');
145 check((await noJS.locator('#content').boundingBox()).y<80,'no-JS navigation pushes content down');
146 await noJS.screenshot({path:'output/playwright/ux/nojs-mobile.png'});
147 } finally {await context.close();}
148 });
149 page.off('pageerror',onError);
150 results.push({name:'uncaught browser errors',ok:errors.length===0,errors});
151 return {passed:results.filter(x=>x.ok).length,failed:results.filter(x=>!x.ok).length,results};
152}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs