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>
78 lines
7.6 KiB
JavaScript
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;});
|