const assert=require('node:assert/strict'); const fs=require('node:fs'); const {launchBrowser}=require('./support/browser.cjs'); 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 launchBrowser(); try { const page=await browser.newPage({locale:'en-US',viewport:{width:1280,height:900}}); page.setDefaultTimeout(12000); const errors=[];page.on('pageerror',e=>errors.push(e.message)); await page.goto(base); await page.getByRole('button',{name:'New here? Create an account'}).click(); await page.getByLabel('Username',{exact:true}).fill('config-transfer-'+Date.now()); await page.getByLabel('Display name',{exact:true}).fill('Path 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(); const dialog=page.getByRole('dialog'); await dialog.getByLabel('Name',{exact:true}).fill('Portable config regression'); await dialog.getByRole('button',{name:'Save changes',exact:true}).click(); await dialog.waitFor({state:'hidden'}); await page.evaluate(()=>{const a=Alpine.$data(document.body);a.view='generation';}); const initial=await page.evaluate(()=>({revision:Alpine.$data(document.body).workspace.revision,workspace:Alpine.$data(document.body).workspace.id})); await page.getByRole('button',{name:'Import / Export',exact:true}).click(); const modal=page.getByRole('dialog'); await modal.getByLabel('Configuration · meguri/v2 JSON').waitFor(); await page.waitForFunction(()=>!!Alpine.$data(document.body).transferExport); await modal.getByRole('button',{name:'Guide',exact:true}).click(); const guide=modal.locator('.config-guide');await guide.waitFor({state:'visible'}); await page.context().grantPermissions(['clipboard-read','clipboard-write'],{origin:base}); await modal.getByRole('button',{name:'Copy full guide',exact:true}).click(); const copiedGuide=await page.evaluate(()=>navigator.clipboard.readText()); assert(copiedGuide.includes('$lookup'));assert(copiedGuide.includes('not_equals'));assert(copiedGuide.includes('ends-with')); const sample=JSON.parse(copiedGuide.match(/```json\n([\s\S]*?)\n```/)[1]); const guidePreview=await page.evaluate(async text=>{const a=Alpine.$data(document.body);return a.api(a.endpoint('/generation-import-preview'),'POST',{...a.transferBase,text});},JSON.stringify(sample)); assert.deepEqual(guidePreview.preview.issues,[]);assert.equal(guidePreview.preview.changes.filter(c=>c.action==='create').length,4); fs.mkdirSync('test-results',{recursive:true});await page.screenshot({path:'test-results/config-guide-desktop.png'}); await page.setViewportSize({width:390,height:844});await page.screenshot({path:'test-results/config-guide-mobile.png'});assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));await page.setViewportSize({width:1280,height:900}); await modal.getByRole('button',{name:'Import',exact:true}).click(); const input=modal.getByLabel('Paste or edit configuration'),apply=modal.getByRole('button',{name:'Apply to shared draft',exact:true}); await input.fill('{ broken'); await modal.getByRole('button',{name:'Guide',exact:true}).click();await modal.getByRole('button',{name:'Import',exact:true}).click();assert.equal(await input.inputValue(),'{ broken'); await modal.getByRole('alert').filter({hasText:'Invalid configuration'}).waitFor();assert(await apply.isDisabled()); const config={format:'meguri/v1',variables:[{name:'projects',node:{kind:'list',entries:['cse','opd'].map(name=>({id:name,kind:'dictionary',entries:[{kind:'text',key:'name',value:name},{kind:'list',key:'owners',entries:[{kind:'person',value:'owner@example.com'}]}]}))}},{name:'keep me',node:{kind:'text',value:'retained'}}],generators:[{name:'Weekly projects',iterations:[{name:'Project',source:{kind:'variable',variable:'projects'}}],blocks:[{id:'weekly',kind:'rotation',fields:{name:{kind:'item',iteration:'Project',path:['name'],suffix:' on-call'},participants:{kind:'item',iteration:'Project',path:['owners']},anchor:{kind:'literal',value:'2026-09-28'},shift_days:{kind:'literal',value:7},handover:{kind:'literal',value:'11:00'}}}]}]}; await input.fill(JSON.stringify(config,null,2)); await page.waitForFunction(()=>Alpine.$data(document.body).transferCanApply()); assert((await modal.locator('.transfer-preview').innerText()).includes('cse on-call')); assert.deepEqual(await page.evaluate(()=>Alpine.$data(document.body).transferResult.contacts.map(p=>p.email)),['owner@example.com']); assert.equal(await page.evaluate(()=>Alpine.$data(document.body).workspace.revision),initial.revision); const before=await page.evaluate(async()=>{const a=Alpine.$data(document.body);return (await a.api(a.endpoint())).generation_draft;});assert(!before.payload||!before.payload.rules.length); fs.mkdirSync('test-results',{recursive:true});await page.screenshot({path:'test-results/config-import-desktop.png'}); await page.setViewportSize({width:390,height:844});await page.screenshot({path:'test-results/config-import-mobile.png'});assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));await page.setViewportSize({width:1280,height:900}); await apply.click();await modal.waitFor({state:'hidden'}); await page.waitForFunction(()=>!Alpine.$data(document.body).generationLoading); assert.equal(await page.evaluate(()=>Alpine.$data(document.body).workspace.revision),initial.revision); assert.equal(await page.evaluate(()=>Alpine.$data(document.body).doc.schedules.length),0); const ids=await page.evaluate(()=>Alpine.$data(document.body).generationDraft.rules.map(r=>r.blocks.map(b=>b.id))); await page.getByRole('button',{name:'Import / Export',exact:true}).click();await page.waitForFunction(()=>!!Alpine.$data(document.body).transferExport); const exported=await modal.getByLabel('Configuration · meguri/v2 JSON').inputValue();assert(exported.includes('owner@example.com'));assert.equal(JSON.parse(exported).format,'meguri/v2');assert(!exported.includes(ids[0][0]));await page.screenshot({path:'test-results/config-v2-export.png'}); const downloaded=page.waitForEvent('download');await modal.getByRole('button',{name:'Download JSON',exact:true}).click();assert.equal((await downloaded).suggestedFilename(),'meguri-config.json'); const edited=JSON.parse(exported);delete edited.variables['keep me'];edited.generators[0].outputs[0].shift_days=14; await modal.getByRole('button',{name:'Import',exact:true}).click();await input.fill(JSON.stringify(edited));await page.waitForFunction(()=>Alpine.$data(document.body).transferCanApply()); assert.equal(await page.evaluate(()=>Alpine.$data(document.body).transferResult.preview.changes.filter(c=>c.action==='create').length),2); await apply.click();await modal.waitFor({state:'hidden'}); assert.deepEqual(await page.evaluate(()=>Alpine.$data(document.body).generationDraft.rules.map(r=>r.blocks.map(b=>b.id))),ids); assert(await page.evaluate(()=>Alpine.$data(document.body).generationDraft.variables.some(v=>v.name==='keep me'))); await page.reload();await page.waitForFunction(()=>Alpine.$data(document.body).generationDraft?.rules.length===1); assert.equal(await page.evaluate(()=>Alpine.$data(document.body).generationDraft.rules[0].blocks[0].fields.shift_days.value),14); assert.deepEqual(errors,[]); console.log('PASS: portable config live preview, parse errors, no writes before apply, shared draft persistence, identities, merge, export, download and mobile'); }finally{await browser.close();} })().catch(e=>{console.error(e);process.exitCode=1;});