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('meguri-demo-only');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:'{person}'},'fr')),'Intervalles sans remplacement : {person}'); // 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)});