Files
meguri/tests/browser-navigation.cjs
Aleksandr BogomiakovandClaude 4764f3545f Add OIDC sign-in, group-derived roles and declarative instance configuration
Workspaces, group mappings and sign-in methods now come from a configuration
file synchronized at startup; workspaces dropped from it are disabled, never
deleted. Instance administrators (a new role) see and help every workspace and
may delete disabled ones; workspace administrators and employees are derived
from the user's groups at every SSO sign-in or administrator edit, and the
employee permission set belongs to the workspace. Self-registration is gone:
users come from SSO (only when their groups map) or from the administrator.
Break-glass bootstrap accounts get an HMAC-derived hourly password that all
replicas agree on. First SSO sign-in links a matching email contact to the
user. The demo runs on the same model with a generated per-start password.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-07 13:28:43 +01:00

26 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const assert=require('node:assert/strict'),fs=require('node:fs');
const base=process.env.MEGURI_BROWSER_URL||'http://127.0.0.1:3000';assert(['127.0.0.1','localhost'].includes(new URL(base).hostname));
(async()=>{const browser=await require('./support/browser.cjs').launchBrowser();const errors=[];let page;try{
const context=await browser.newContext({locale:'en-US',viewport:{width:1440,height:1000}});page=await context.newPage();page.setDefaultTimeout(12000);page.on('pageerror',e=>errors.push(e.message));
await page.goto(base+'/?view=policies');await page.getByLabel('Username',{exact:true}).fill('alice');await page.getByLabel('Password',{exact:true}).fill(await require('./support/browser.cjs').demoPassword());await page.getByRole('button',{name:'Sign in',exact:true}).click();await page.waitForFunction(()=>Alpine.$data(document.body).workspace);await page.getByRole('heading',{name:'Schedule setup',exact:true}).waitFor();
const matrix=await page.evaluate(()=>Alpine.$data(document.body).selected);const support=await page.evaluate(()=>Alpine.$data(document.body).directoryState.workspaces.find(t=>t.id!==Alpine.$data(document.body).selected).id);
assert.equal(new URL(page.url()).searchParams.get('view'),'configuration');await page.reload();await page.getByRole('heading',{name:'Schedule setup',exact:true}).waitFor();
await page.getByRole('link',{name:'Requests',exact:true}).click();await page.waitForURL(/view=requests/);await page.getByLabel('Workspace',{exact:true}).selectOption(support);await page.waitForFunction(id=>Alpine.$data(document.body).workspace.id===id,support);assert.equal(new URL(page.url()).searchParams.get('workspace'),support);
await page.goBack();await page.waitForFunction(id=>Alpine.$data(document.body).workspace.id===id,matrix);assert.equal(await page.evaluate(()=>Alpine.$data(document.body).view),'requests');await page.goBack();await page.getByRole('heading',{name:'Schedule setup',exact:true}).waitFor();await page.goForward();await page.getByRole('heading',{name:'Requests',exact:true}).waitFor();
const href=await page.getByRole('link',{name:'Activity',exact:true}).getAttribute('href');const tab=await context.newPage();await tab.goto(base+href);await tab.getByRole('heading',{name:'Activity',exact:true}).waitFor();assert.equal(await tab.evaluate(()=>Alpine.$data(document.body).selected),matrix);await tab.close();
// Locale is a saved display preference; use the real account form, then a fresh browser locale.
await page.locator('.account-button').click();await page.getByLabel('Language',{exact:true}).selectOption('ru');await page.getByRole('dialog').getByRole('button',{name:'Save changes',exact:true}).click();await page.waitForFunction(()=>document.documentElement.lang==='ru');await page.reload();await page.waitForFunction(()=>document.documentElement.lang==='ru' && Alpine.$data(document.body).workspace);await page.screenshot({path:'test-results/localization-ru.png',fullPage:true});
const snapshot=await page.evaluate(async()=>{const a=Alpine.$data(document.body);return {revision:a.workspace.revision,schedules:JSON.stringify(a.doc.schedules),user:a.user};});
const fr=await browser.newContext({locale:'fr-CA',storageState:await context.storageState(),viewport:{width:390,height:844}});const fp=await fr.newPage();fp.on('pageerror',e=>errors.push(e.message));await fp.goto(base+'/?view=configuration&workspace='+matrix);await fp.waitForFunction(()=>document.documentElement.lang==='ru' && Alpine.$data(document.body).workspace);
await fp.evaluate(async()=>{const a=Alpine.$data(document.body);await a.api('/api/account','PUT',{...a.user,language:null});});await fp.reload();await fp.waitForFunction(()=>document.documentElement.lang==='fr' && Alpine.$data(document.body).workspace);await fp.getByRole('heading',{name:'Configuration des plannings',exact:true}).waitFor();assert.equal(await fp.evaluate(()=>Alpine.$data(document.body).workspace.revision),snapshot.revision);assert.equal(await fp.evaluate(()=>JSON.stringify(Alpine.$data(document.body).doc.schedules)),snapshot.schedules);assert(await fp.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));await fp.screenshot({path:'test-results/localization-fr-mobile.png',fullPage:true});
assert.equal(await fp.evaluate(()=>I18n.diagnostic('Choose an existing workspace variable')),'Choisissez une variable existante de l’espace');assert.equal(await fp.evaluate(()=>I18n.translate('coverage_to_arrange',{count:'<b>{person}</b>'},'fr')),'Intervalles sans remplacement : <b>{person}</b>');
// Invalid preference cannot be persisted.
const status=await fp.evaluate(async()=>{const a=Alpine.$data(document.body);return (await fetch('/api/account',{method:'PUT',headers:{'Content-Type':'application/json','X-CSRF-Token':a.csrf},body:JSON.stringify({...a.user,language:'xx'})})).status;});assert.equal(status,422);
await fr.close();
for(const [locale,expected]of [['ru-RU','ru'],['de-DE','en'],['fr-CA','fr']]){const cx=await browser.newContext({locale});const p=await cx.newPage();await p.goto(base);await p.waitForFunction(()=>Alpine.$data(document.body).ready);assert.equal(await p.locator('html').getAttribute('lang'),expected);await cx.close();}
// Parse templates rather than rendered text: built-in copy must be catalog-backed.
for(const file of fs.readdirSync('static').filter(f=>f.endsWith('.html'))){const source=fs.readFileSync('static/'+file,'utf8');const literals=await page.evaluate(source=>{const doc=new DOMParser().parseFromString(source,'text/html'),out=[];const visit=node=>{for(const n of node.childNodes){if(n.nodeType===3 && n.textContent.trim())out.push(n.textContent.trim());if(n.nodeType===1){if(n.tagName==='TEMPLATE')visit(n.content);else if(!['SCRIPT','STYLE'].includes(n.tagName))visit(n);}}};visit(doc);return out;},source);assert.deepEqual(literals,[],file+' contains inline text');}
const catalog=JSON.parse(fs.readFileSync('static/locales/messages.json','utf8'));const placeholders=s=>[...s.matchAll(/\{(\w+)\}/g)].map(m=>m[1]).sort();for(const [key,entry]of Object.entries(catalog))for(const lang of ['en','ru','fr']){assert(entry[lang],key+': '+lang);assert.deepEqual(placeholders(entry[lang]),placeholders(entry.en),key);}
assert.deepEqual(errors,[]);console.log('PASS: menu links, reload, cross-team Back/Forward, new tab, language persistence and browser fallback, translated validation, unchanged schedules, mobile, complete catalog, no inline template copy');
}finally{await browser.close();}})().catch(e=>{console.error(e);process.exit(1)});