Files
meguri/tests/browser-configuration.cjs
T

85 lines
7.2 KiB
JavaScript
Raw Normal View History

const {openSetup,isolatedUser,signIn}=require('./support/browser.cjs');
2026-09-24 14:35:05 +03:00
// Regression for dependency-aware removal and routing precedence through the real UI/API.
2026-09-24 16:10:08 +03:00
2026-09-24 14:35:05 +03:00
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()=>{
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 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 tester=await isolatedUser({sandbox:3,prefix:'configuration',display:'Configuration tester'});
await signIn(page,tester.username,tester.password);
await page.getByLabel('Workspace',{exact:true}).waitFor();
// Seed only this test's emptied workspace; all mutations under test use visible controls.
2026-09-24 14:35:05 +03:00
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();
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']);
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;});