Files
meguri/tests/browser-absence.cjs
T
Ultradesu 5260c2828b
Checks / test (push) Failing after 2m25s
NEXT
2026-10-07 15:30:31 +01:00

78 lines
7.6 KiB
JavaScript

const {openSetup,isolatedUser,signIn}=require('./support/browser.cjs');
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()=>{
const browser=await require('./support/browser.cjs').launchBrowser();
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 {
// 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();
const dates=await page.evaluate(async()=>{
const app=Alpine.$data(document.body);
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);
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;});