2026-09-24 16:10:08 +03:00
const assert = require ( 'node:assert/strict' ), fs = require ( 'node:fs' );
2026-09-24 14:35:05 +03:00
const base = process . env . MEGURI_BROWSER_URL || 'http://127.0.0.1:3000' ; assert ([ '127.0.0.1' , 'localhost' ]. includes ( new URL ( base ). hostname ));
2026-09-24 16:10:08 +03:00
( async ()=>{ const browser = await require ( './support/browser.cjs' ). launchBrowser (); const errors = []; let page ; try {
2026-09-24 14:35:05 +03:00
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 ));
2026-10-07 13:28:43 +01:00
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 ();
2026-09-24 14:35:05 +03:00
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 )});