2026-10-07 13:28:43 +01:00
const { openSetup , isolatedUser , signIn } = require ( './support/browser.cjs' );
2026-09-24 16:10:08 +03:00
2026-09-24 14:35:05 +03:00
const assert = require ( 'node:assert/strict' );
const base = process . env . MEGURI_BROWSER_URL || 'http://127.0.0.1:3000' ;
assert ([ 'localhost' , '127.0.0.1' ]. includes ( new URL ( base ). hostname ));
( async ()=>{
2026-09-24 16:10:08 +03:00
const browser = await require ( './support/browser.cjs' ). launchBrowser ();
2026-09-24 14:35:05 +03:00
const page = await browser . newPage ({ viewport : { width : 1440 , height : 1100 }}); page . setDefaultTimeout ( 10000 );
const errors = []; page . on ( 'pageerror' , e => errors . push ( e . message ));
const dialog = ()=> page . getByRole ( 'dialog' );
const save = async ()=>{ await dialog (). getByRole ( 'button' ,{ name : 'Save changes' , exact : true }). click (); await dialog (). waitFor ({ state : 'hidden' });};
const fmt = iso => iso . split ( '-' ). reverse (). join ( '-' );
try {
2026-10-07 13:28:43 +01:00
// Sandbox 4 belongs to this suite: an administrator and two substitutes (Bob Chen, Carol Substitute).
const tester = await isolatedUser ({ sandbox : 4 , prefix : 'absence' , display : 'Absence tester' , employees : [ 'Bob Chen' , 'Carol Substitute' ]});
await signIn ( page , tester . username , tester . password );
await page . getByLabel ( 'Workspace' ,{ exact : true }). waitFor ();
2026-09-24 14:35:05 +03:00
const dates = await page . evaluate ( async ()=>{
const app = Alpine . $data ( document . body );
2026-10-07 13:28:43 +01:00
const bob = app . directory . find ( p => p . display_name === 'Bob Chen' ). id , doc = JSON . parse ( JSON . stringify ( app . doc )), start = app . addDays ( app . today (), 2 ), end = app . addDays ( start , 13 );
2026-09-24 14:35:05 +03:00
const rotation = crypto . randomUUID ();
doc . schedules = [{ id : rotation , name : 'General' , handover : '11:00:00' , type : 'rotation' , participants : [ app . user . id ], anchor : start , shift_days : 7 },{ id : crypto . randomUUID (), name : 'Backup' , handover : '09:00:00' , type : 'rotation' , participants : [ app . user . id ], anchor : '2026-09-21' , shift_days : 7 }];
for ( let i = 1 ; i <= 9 ; i ++ ) doc . schedules . push ({ id : crypto . randomUUID (), name : 'Project ' + i , handover : '11:00:00' , type : 'coverage' , primary : app . user . id , secondary : bob , fallback : rotation });
doc . holidays = [{ country : 'CY' , date : '2027-12-25' , name : 'Christmas' },{ country : 'CY' , date : '2027-01-01' , name : 'New Year' },{ country : 'AM' , date : '2027-01-01' , name : 'New Year' }];
await app . api ( app . endpoint ( '/config' ), 'PUT' ,{ revision : app . workspace . revision , document : doc }); await app . loadWorkspace ();
return { start , end , middle : app . addDays ( start , 6 ), next : app . addDays ( start , 7 ), revision : app . workspace . revision };
});
await openSetup ( page , 'Schedules' );
const cy = page . locator ( 'details.holiday-group' ). filter ({ has : page . locator ( 'summary' ). filter ({ hasText : 'Cyprus' })});
assert . equal ( await cy . getAttribute ( 'open' ), null ); assert . equal (( await cy . locator ( '.badge' ). textContent ()). trim (), '2' );
assert ( ! ( await cy . getByText ( 'Christmas' ,{ exact : true }). isVisible ())); await cy . locator ( 'summary' ). click (); await cy . getByText ( '25-12-2027 · country calendar date' ,{ exact : true }). waitFor ();
await page . getByRole ( 'button' ,{ name : '+ Absence' , exact : true }). click ();
await dialog (). getByLabel ( 'First absent duty day' ,{ exact : true }). fill ( fmt ( dates . start )); await dialog (). getByLabel ( 'Last absent duty day (inclusive)' ,{ exact : true }). fill ( fmt ( dates . end ));
await dialog (). getByText ( /11 affected schedules · 0 replacement requests · 2 uncovered intervals/ ). waitFor ();
assert . equal ( await page . evaluate (()=> Alpine . $data ( document . body ). workspace . revision ), dates . revision );
const cell = dialog (). locator ( '.impact-interval' ). first ();
const nameBox = await cell . locator ( 'strong' ). boundingBox (), datesBox = await cell . locator ( 'small' ). boundingBox ();
assert ( datesBox . y >= nameBox . y + nameBox . height + 4 );
await cell . screenshot ({ path : 'test-results/absence-interval.png' });
const gap = await page . evaluate (()=> Alpine . $data ( document . body ). absenceResult . plan . rows . find ( r => r . status === 'uncovered' ));
await dialog (). getByRole ( 'button' ,{ name : 'Choose a substitute' , exact : true }). first (). click ();
await page . getByRole ( 'region' ,{ name : 'Choose a person' }). locator ( '.picker-option' ). filter ({ hasText : 'Bob Chen' }). click ();
await page . waitForFunction (()=>{ const a = Alpine . $data ( document . body ); return a . absenceResult &&! a . absenceLoading ;});
const quick = await page . evaluate (()=> Alpine . $data ( document . body ). form . ranges [ 0 ]);
assert . equal ( quick . schedule_id , gap . schedule_id );
assert . equal ( quick . start , gap . start_day ); assert . equal ( quick . end , await page . evaluate ( end =>{ const a = Alpine . $data ( document . body ); return a . dayKey ( new Date ( end ), a . doc . timezone );}, gap . end ));
assert . equal ( quick . start_time , '11:00:00' ); assert . equal ( quick . end_time , '11:00:00' );
await dialog (). getByLabel ( 'Start time' ,{ exact : true }). fill ( '12:00' );
await page . waitForFunction (()=>{ const a = Alpine . $data ( document . body ); return a . absenceResult &&! a . absenceLoading && a . workspaceClock ( a . absenceResult . plan . groups [ 0 ][ 0 ]. start ). startsWith ( '12:00' );});
assert . equal ( await page . evaluate (()=> Alpine . $data ( document . body ). absenceResult . plan . groups [ 0 ]. length ), 1 );
await dialog (). getByRole ( 'button' ,{ name : 'Remove range' , exact : true }). click ();
await dialog (). getByText ( /11 affected schedules · 0 replacement requests · 2 uncovered intervals/ ). waitFor ();
await dialog (). getByRole ( 'button' ,{ name : '+ Replacement range' , exact : true }). click ();
const range = i => dialog (). locator ( '.absence-range' ). nth ( i );
await range ( 0 ). getByLabel ( 'Range ends (exclusive)' ,{ exact : true }). fill ( fmt ( dates . next ));
await range ( 0 ). getByLabel ( 'Start time' ,{ exact : true }). fill ( '09:00' );
await range ( 0 ). locator ( '.person-select' ). click (); await page . getByRole ( 'region' ,{ name : 'Choose a person' }). locator ( '.picker-option' ). filter ({ hasText : 'Bob Chen' }). click ();
await dialog (). getByRole ( 'button' ,{ name : '+ Replacement range' , exact : true }). click ();
assert . equal ( await range ( 1 ). getByLabel ( 'Range starts' ,{ exact : true }). inputValue (), fmt ( dates . next ));
await range ( 1 ). locator ( '.person-select' ). click (); await page . getByRole ( 'region' ,{ name : 'Choose a person' }). locator ( '.picker-option' ). filter ({ hasText : 'Carol' }). click ();
await dialog (). getByText ( /11 affected schedules · 2 replacement requests · 0 uncovered intervals/ ). waitFor ();
await dialog (). getByRole ( 'button' ,{ name : /Show all .* intervals/ }). click (); assert (( await dialog (). locator ( '.impact-table tbody tr' ). count ()) > 8 );
await dialog (). getByLabel ( 'Range starts' ,{ exact : true }). first (). fill ( '31-02-2027' );
await dialog (). getByText ( 'Complete the dates and choose a person for each replacement range.' ,{ exact : true }). waitFor (); assert ( await dialog (). getByRole ( 'button' ,{ name : 'Save changes' , exact : true }). isDisabled ());
await dialog (). getByLabel ( 'Range starts' ,{ exact : true }). first (). fill ( fmt ( dates . start ));
await dialog (). getByLabel ( 'Apply replacements without confirmation' ). check (); await dialog (). getByLabel ( 'Comment (optional)' ,{ exact : true }). fill ( 'Agreed vacation coverage' );
await dialog (). getByText ( /11 affected schedules · 2 replacement requests · 0 uncovered intervals/ ). waitFor ();
await page . screenshot ({ path : 'test-results/absence-plan.png' , fullPage : true });
await page . setViewportSize ({ width : 390 , height : 844 }); await page . screenshot ({ path : 'test-results/absence-plan-mobile.png' , fullPage : true }); assert ( await page . evaluate (()=> document . documentElement . scrollWidth <= innerWidth )); await save ();
const result = await page . evaluate (()=> JSON . parse ( JSON . stringify ( Alpine . $data ( document . body ). doc )));
assert . equal ( result . absences . length , 1 ); assert . equal ( result . requests . length , 2 ); assert ( result . requests . every ( r => r . status === 'applied' && r . legs . length === 2 )); assert . deepEqual ( errors ,[]);
console . log ( 'PASS: collapsed country holidays, DD-MM-YYYY display/input, immediate invalid-date feedback, server-only absence preview, two bulk replacement ranges, automatic project secondary, forced application, mobile summary' );
} catch ( error ){ console . error ( errors ); console . error ( await page . locator ( '[role=alert]' ). textContent ()); await page . screenshot ({ path : 'test-results/absence-failure.png' , fullPage : true }); throw error ;}
finally { await browser . close ();}
})(). catch ( error =>{ console . error ( error ); process . exitCode = 1 ;});