100 lines
8.4 KiB
JavaScript
100 lines
8.4 KiB
JavaScript
const {openSetup}=require('./support/browser.cjs');
|
|
// Regression for dependency-aware removal and routing precedence through the real UI/API.
|
|
|
|
const assert=require('node:assert/strict');
|
|
const fs=require('node:fs');
|
|
const base=process.env.MEGURI_BROWSER_URL || 'http://127.0.0.1:3000';
|
|
assert(['localhost','127.0.0.1'].includes(new URL(base).hostname));
|
|
fs.mkdirSync('test-results',{recursive:true});
|
|
(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 state=()=>page.evaluate(()=>JSON.parse(JSON.stringify(Alpine.$data(document.body).workspace)));
|
|
const route=name=>page.locator('.routing-card').filter({has:page.getByText(name,{exact:true})});
|
|
const schedule=name=>page.locator('[data-resource-kind="schedules"]').filter({has:page.getByText(name,{exact:true})});
|
|
const order=async names=>{
|
|
await page.waitForFunction(names=>{
|
|
const app=Alpine.$data(document.body);
|
|
return !app.busy && JSON.stringify(app.orderedRoutes.map(r=>r.name))===JSON.stringify(names);
|
|
},names);
|
|
assert.deepEqual((await page.locator('.routing-card .resource-name > span:last-child').allTextContents()).map(s=>s.trim()),names);
|
|
assert.deepEqual((await state()).document.routes.map(r=>r.order),names.map((_,i)=>i));
|
|
};
|
|
try {
|
|
const unique=Date.now();
|
|
await page.goto(base);await page.getByRole('button',{name:'New here? Create an account'}).click();
|
|
await page.getByLabel('Username',{exact:true}).fill('configuration-'+unique);
|
|
await page.getByLabel('Display name',{exact:true}).fill('Configuration tester');
|
|
await page.getByLabel('Password',{exact:true}).fill('local-browser-test-password');
|
|
await page.getByLabel('Email address',{exact:true}).fill('browser-'+Date.now()+'-'+Math.random().toString(16).slice(2)+'@example.com');await page.getByRole('button',{name:'Create account',exact:true}).click();
|
|
await page.getByRole('button',{name:'New workspace',exact:true}).click();
|
|
await dialog().getByLabel('Name',{exact:true}).fill('Configuration '+unique);await save();
|
|
await page.getByRole('button',{name:'New workspace',exact:true}).click();
|
|
await dialog().getByLabel('Name',{exact:true}).fill('Configuration '+unique);await save();
|
|
// Seed only this test's new team; all mutations under test use visible controls.
|
|
await page.evaluate(async()=>{
|
|
const app=Alpine.$data(document.body),doc=JSON.parse(JSON.stringify(app.doc));
|
|
doc.schedules=['Payments · primary','Payments · secondary','Alternate'].map(name=>({id:crypto.randomUUID(),name,handover:'11:00:00',type:'rotation',participants:[app.user.id],anchor:'2026-09-21',shift_days:7}));
|
|
const policy={id:crypto.randomUUID(),name:'Payments escalation',steps:doc.schedules.slice(0,2).map((s,i)=>({delay_minutes:i*10,condition:'if-not-acked',recipient:{type:'schedule',id:s.id}})),repeat:{count:0,wait_minutes:0,reset_recipient_states:false,close_alert_after_all:false}};
|
|
doc.escalations=[policy];
|
|
doc.routes=['First','Second','Third'].map((name,i)=>({id:crypto.randomUUID(),name,order:[10,20,99][i],conditions:[{field:'tags',operation:'contains',value:name.toLowerCase(),negate:false}],window:null,escalation_id:policy.id}));
|
|
await app.api(app.endpoint('/config'),'PUT',{revision:app.workspace.revision,document:doc});await app.loadWorkspace();
|
|
});
|
|
await openSetup(page,'Schedules');
|
|
await schedule('Payments · secondary').locator('summary').click();
|
|
await schedule('Payments · secondary').getByRole('button',{name:'Remove',exact:true}).click();
|
|
await dialog().getByText('Payments escalation',{exact:true}).waitFor();
|
|
await dialog().getByText('Step 2 · 10 min',{exact:true}).waitFor();
|
|
await page.screenshot({path:'test-results/schedule-removal.png',fullPage:true});
|
|
await dialog().getByRole('button',{name:'Remove schedule & affected steps',exact:true}).click();
|
|
await dialog().waitFor({state:'hidden'});
|
|
let current=await state();assert.equal(current.document.schedules.length,2);assert.equal(current.document.escalations[0].steps.length,1);
|
|
assert.equal(current.document.escalations[0].steps[0].recipient.id,current.document.schedules[0].id);
|
|
await schedule('Payments · primary').locator('summary').click();
|
|
await schedule('Payments · primary').getByRole('button',{name:'Remove',exact:true}).click();
|
|
await dialog().getByText(/These policies would have no steps left/).waitFor();
|
|
assert(await dialog().getByRole('button',{name:'Remove schedule & affected steps',exact:true}).isDisabled());
|
|
await dialog().getByLabel('Existing references',{exact:true}).selectOption({label:'Use Alternate instead'});
|
|
await dialog().getByRole('button',{name:'Remove & redirect references',exact:true}).click();await dialog().waitFor({state:'hidden'});
|
|
current=await state();assert.equal(current.document.schedules.length,1);
|
|
assert.equal(current.document.escalations[0].steps[0].recipient.id,current.document.schedules[0].id);
|
|
await openSetup(page,'Escalations & routing');
|
|
while(await page.locator('.routing-card:not([open]) > summary').count())await page.locator('.routing-card:not([open]) > summary').first().click();
|
|
await page.getByRole('button',{name:'Drag Third to reorder',exact:true}).dragTo(route('First'),{targetPosition:{x:180,y:15}});
|
|
await order(['Third','First','Second']);
|
|
await page.reload();await openSetup(page,'Escalations & routing');
|
|
while(await page.locator('.routing-card:not([open]) > summary').count())await page.locator('.routing-card:not([open]) > summary').first().click();
|
|
await order(['Third','First','Second']);
|
|
await page.getByRole('button',{name:'Move Third down',exact:true}).click();await order(['First','Third','Second']);
|
|
await route('Second').getByRole('button',{name:'Edit',exact:true}).click();
|
|
await dialog().getByLabel('Field',{exact:true}).selectOption('priority');
|
|
await dialog().getByLabel('Operation',{exact:true}).selectOption('less-than');
|
|
await dialog().getByLabel('Value',{exact:true}).fill('P3');
|
|
await dialog().getByRole('checkbox',{name:'Not',exact:true}).check();
|
|
|
|
await page.screenshot({path:'test-results/routing-priority.png',fullPage:true});
|
|
assert.equal(await dialog().getByLabel('Position',{exact:true}).inputValue(),'3');
|
|
await dialog().getByLabel('Position',{exact:true}).selectOption('1');await save();await order(['Second','First','Third']);
|
|
assert.deepEqual((await state()).document.routes.find(r=>r.name==='Second').conditions,[{field:'priority',operation:'less-than',value:'P3',negate:true}]);
|
|
await page.getByRole('button',{name:'+ Routing rule',exact:true}).click();
|
|
await dialog().getByLabel('Rule name',{exact:true}).fill('Fourth');
|
|
await dialog().getByLabel('Position',{exact:true}).selectOption('2');await save();await order(['Second','Fourth','First','Third']);
|
|
await route('Fourth').locator('summary').click();
|
|
await route('Fourth').getByRole('button',{name:'Remove',exact:true}).click();await order(['Second','First','Third']);
|
|
await page.screenshot({path:'test-results/routing-order.png',fullPage:true});
|
|
await page.setViewportSize({width:390,height:844});
|
|
await page.getByRole('button',{name:'Move Third up',exact:true}).click();await order(['Second','Third','First']);
|
|
assert(await page.getByRole('button',{name:'Move Second up',exact:true}).isDisabled());
|
|
assert(await page.getByRole('button',{name:'Move First down',exact:true}).isDisabled());
|
|
await page.screenshot({path:'test-results/routing-order-mobile.png',fullPage:true});
|
|
assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'Routing rules must fit mobile');
|
|
assert.deepEqual(errors,[]);
|
|
console.log('PASS: schedule dependency preview, step removal, last-step protection, redirect, native drag-and-drop, persistent routing order, editor position, insert/remove renumbering, mobile arrow controls');
|
|
} catch(error) {
|
|
console.error('Browser errors:',errors);console.error('Visible alert:',await page.locator('[role=alert]').textContent());await page.screenshot({path:'test-results/configuration-failure.png',fullPage:true});throw error;
|
|
} finally {await browser.close();}
|
|
})().catch(error=>{console.error(error);process.exitCode=1;});
|