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;});