Files

74 lines
7.8 KiB
JavaScript
Raw Permalink Normal View History

2026-10-07 15:30:31 +01:00
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;});