Files
meguri/tests/browser-generation.cjs
T
Ultradesu 5260c2828b
Checks / test (push) Failing after 2m25s
NEXT
2026-10-07 15:30:31 +01:00

85 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const {openSetup}=require('./support/browser.cjs');
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(['127.0.0.1','localhost'].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(name='Save changes')=>{await dialog().getByRole('button',{name,exact:true}).click();await dialog().waitFor({state:'hidden'});};
const checked=()=>page.waitForFunction(()=>{const a=Alpine.$data(document.body);return a.generationResult && !a.generationLoading && a.generationResult.key===JSON.stringify(a.generationPayload());});
try {
await page.goto(base);await page.locator('.demo-note').waitFor();await page.getByRole('status',{name:'Demo data retention',exact:true}).waitFor();assert.equal(await page.locator('.demo-banner button').count(),0);assert.match(await page.locator('.demo-protection').innerText(),/left untouched/);assert.match(await page.locator('.demo-note').innerText(),/alice[\s\S]*all four workspaces[\s\S]*bob[\s\S]*carol/);
await page.screenshot({path:'test-results/demo-login.png',fullPage:true});
await page.getByLabel('Username',{exact:true}).fill('alice');await page.getByLabel('Password',{exact:true}).fill('meguri-demo-only');await page.getByRole('button',{name:'Sign in',exact:true}).click();await page.waitForFunction(()=>Alpine.$data(document.body).workspace);
const demo=await page.evaluate(()=>{const a=Alpine.$data(document.body);return {matrix:a.directoryState.workspaces.find(t=>t.name==='Acme Engineering'),support:a.directoryState.workspaces.find(t=>t.name==='Customer Support')};});
assert(demo.matrix && demo.support);assert.notEqual(demo.matrix.id,demo.support.id);
await page.getByLabel('Workspace',{exact:true}).selectOption(demo.matrix.id);await openSetup(page,'Variables & rules');await checked();
assert.deepEqual(await page.evaluate(()=>Alpine.$data(document.body).generationResult.issues),[]);
const matrix=await page.evaluate(()=>Alpine.$data(document.body).doc);
assert.deepEqual(matrix.generation.variables.map(v=>v.name),['projects','on_call']);
assert.equal(await page.evaluate(()=>{const a=Alpine.$data(document.body);return a.nodeSummary(a.generationDraft.variables.find(v=>v.name==='projects').node);}), 'Payments, Search, Billing, Identity…');
for(const project of matrix.generation.variables[0].node.entries){
const name=project.entries.find(n=>n.key==='name').value;
const owners=project.entries.find(n=>n.key==='responsible').entries.map(n=>n.value);
const schedule=matrix.schedules.find(s=>s.name===name+' · owners');
assert.deepEqual(schedule.participants,owners);
assert(matrix.routes.some(r=>r.name===name+' · working hours' && r.conditions.some(c=>c.value==='project:'+name)));
}
assert.equal(matrix.schedules.filter(s=>s.type==='priority').length,6);assert.equal(matrix.schedules.find(s=>s.name==='Time off').shift_days,7);assert.equal(matrix.schedules.find(s=>s.name==='Time off').handover,'11:00:00');
assert.equal(matrix.routes.filter(r=>r.window).length,6);assert.equal(matrix.routes.at(-1).window,null);
await page.screenshot({path:'test-results/matrix-demo.png',fullPage:true});
await page.getByRole('button',{name:'Edit variable projects',exact:true}).click();await checked();await page.screenshot({path:'test-results/matrix-data-editor.png',fullPage:true});
await page.setViewportSize({width:390,height:844});await page.screenshot({path:'test-results/matrix-data-mobile.png',fullPage:true});assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));await page.setViewportSize({width:1440,height:1100});await dialog().getByRole('button',{name:'Cancel',exact:true}).click();
await page.getByLabel('Workspace',{exact:true}).selectOption(demo.support.id);await checked();
assert.equal(await page.evaluate(()=>Alpine.$data(document.body).doc.generation.rules[1].name),'Daily queue coverage');
await openSetup(page,'Escalations & routing');await page.screenshot({path:'test-results/support-demo.png',fullPage:true});
// Distinct members cannot read the other example's team, even with its URL.
for(const [username,denied] of [['bob',demo.support],['dave',demo.support]]){
const ctx=await browser.newContext();const api=ctx.request;const login=await api.post(base+'/api/login',{headers:{Origin:base},data:{username,password:'meguri-demo-only'}});assert.equal(login.status(),200);const response=await api.get(`${base}/api/workspaces/${denied.id}`);assert.equal(response.status(),404);await ctx.close();
}
// Mutating UI checks use a separate account, leaving the examples untouched.
await page.getByRole('button',{name:'Sign out',exact:true}).click();await page.getByRole('button',{name:'New here? Create an account',exact:true}).click();const unique=Date.now();await page.getByLabel('Username',{exact:true}).fill('matrix-'+unique);await page.getByLabel('Display name',{exact:true}).fill('Matrix 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('Matrix '+unique);await save();
await openSetup(page,'Variables & rules');
await page.evaluate(()=>{const a=Alpine.$data(document.body),projects={id:crypto.randomUUID(),name:'projects',node:a.newNode('list')},responsible={id:crypto.randomUUID(),name:'responsible',node:a.newNode('dictionary')};for(const name of ['A','B']){projects.node.entries.push(a.newNode('text',name));const entry=a.newNode('list',null,name);entry.entries=['alice@example.com','bob@example.com'].map(email=>a.newNode('person',email));responsible.node.entries.push(entry);}const iteration={id:crypto.randomUUID(),name:'Project',source:{kind:'variable',variable:projects.id,path:[]}},block=a.newBlock('rotation');block.fields.name={kind:'item',iteration:iteration.id,path:[],suffix:' on call'};block.fields.participants={kind:'lookup',variable:responsible.id,key:{kind:'item',iteration:iteration.id,path:[]},path:[]};a.generationDraft.variables.push(projects,responsible);a.generationDraft.rules.push({id:crypto.randomUUID(),name:'Project rotations',enabled:true,iterations:[iteration],when:null,blocks:[block]});a.generationChanged();});await checked();
assert.equal(await page.evaluate(()=>Alpine.$data(document.body).generationCount('create')),2);
const revision=await page.evaluate(()=>Alpine.$data(document.body).workspace.revision);
await page.getByRole('button',{name:'Edit variable responsible',exact:true}).click();await dialog().getByLabel('Dictionary key').first().fill('Missing');await checked();await dialog().locator('.modal-footer:visible .validation-toggle').click();assert.match(await dialog().locator('.validation-details').innerText(),/key ‘A’/);
assert.equal(await page.evaluate(async()=>{const a=Alpine.$data(document.body);return (await a.api(a.endpoint())).workspace.revision;}),revision);
await dialog().getByLabel('Dictionary key').first().fill('A');await checked();await save('Use in draft');await checked();
await page.getByRole('button',{name:'Edit rule Project rotations',exact:true}).click();await dialog().getByLabel('Shift length (days) · Fixed value',{exact:true}).fill('0');await checked();
await dialog().evaluate(el=>el.scrollTop=0);const status=dialog().locator('.modal-footer:visible .validation-control');assert.match(await status.innerText(),/Issues to resolve/);const box=await status.boundingBox();assert(box && box.y>=0 && box.y+box.height<=1100,'Validation stays in the viewport');
if(await status.locator('.validation-toggle').getAttribute('aria-expanded')==='false')await status.locator('.validation-toggle').click();assert.match(await status.locator('.validation-details').innerText(),/shift_days/);
await dialog().getByRole('button',{name:'Bind Shift length (days)',exact:true}).click();await dialog().getByLabel('Fixed number',{exact:true}).waitFor();assert.equal(await dialog().getByLabel('Rule name',{exact:true}).isVisible(),true,'The value picker preserves the constructor');
await dialog().getByLabel('Fixed number',{exact:true}).fill('1');await dialog().getByRole('button',{name:'Done',exact:true}).click();await dialog().getByLabel('Rule name',{exact:true}).waitFor();await checked();assert.deepEqual(await page.evaluate(()=>Alpine.$data(document.body).generationResult.issues),[]);await page.screenshot({path:'test-results/generation-rule-editor.png',fullPage:true});await save('Use in draft');await checked();
await page.getByRole('button',{name:'Save checked changes',exact:true}).click();await page.waitForFunction(rev=>Alpine.$data(document.body).workspace.revision>rev,revision);await checked();
const ids=await page.evaluate(()=>Alpine.$data(document.body).doc.schedules.map(s=>s.id));assert.equal(ids.length,2);assert.equal(await page.evaluate(()=>Alpine.$data(document.body).doc.schedules[0].shift_days),1);
await page.getByRole('button',{name:'Edit variable projects',exact:true}).click();await dialog().getByLabel('Variable name',{exact:true}).fill('services');await save('Use in draft');await checked();await page.getByRole('button',{name:'Save checked changes',exact:true}).click();await page.waitForFunction(()=>!Alpine.$data(document.body).busy);await page.reload();await openSetup(page,'Variables & rules');await checked();assert.deepEqual(await page.evaluate(()=>Alpine.$data(document.body).doc.schedules.map(s=>s.id)),ids);
await openSetup(page,'Schedules');assert.equal(await page.locator('[data-resource-kind="schedules"] .source-rule-link').count(),2);
// The guided editor resolves the ordinary project/dictionary case without opening a binding editor.
await page.getByRole('button',{name:'+ Generation rule',exact:true}).click();await checked();
assert.equal(await page.evaluate(()=>Alpine.$data(document.body).form.iterations.length),0);
assert.equal(await page.evaluate(()=>Alpine.$data(document.body).form.blocks.length),0);
await dialog().getByLabel('Rule name',{exact:true}).fill('Explicit iteration');
await dialog().getByRole('button',{name:'+ Iteration',exact:true}).click();
await dialog().getByLabel('Add output block',{exact:true}).selectOption('priority');
const lookup=await page.evaluate(()=>Alpine.$data(document.body).sourceOptions('list').find(o=>o.kind==='lookup').id);
await dialog().getByLabel('Responsible people, in priority order',{exact:true}).selectOption(lookup);
assert.equal(await dialog().locator('.binding-editor').count(),0);
// Iterated generated instances are not offered as fixed fallback targets; choose an explicit generated rotation.
await dialog().getByLabel('Add output block',{exact:true}).selectOption('rotation');
const backup=await page.evaluate(()=>Alpine.$data(document.body).form.blocks.at(-1).id);
await dialog().getByLabel('Fallback schedule',{exact:true}).selectOption('output:'+backup);await checked();
assert.deepEqual(await page.evaluate(()=>Alpine.$data(document.body).generationResult.issues),[]);
await page.setViewportSize({width:390,height:844});await page.screenshot({path:'test-results/generation-guided-mobile.png'});assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));await page.setViewportSize({width:1440,height:1100});await dialog().getByRole('button',{name:'Cancel',exact:true}).click();
await page.locator('[data-resource-kind="schedules"] > summary').first().click();await page.locator('[data-resource-kind="schedules"] .source-rule-link').first().click();assert.equal(await dialog().getByLabel('Rule name',{exact:true}).inputValue(),'Project rotations');await dialog().getByRole('button',{name:'Cancel',exact:true}).click();
assert.deepEqual(errors,[]);console.log('PASS: four isolated demo workspaces, minimal login guide, country-aware matrix data, live dictionary errors, no preview writes, binding editor, atomic apply, stable IDs after rename/reload, generated source links, desktop/mobile');
}catch(e){await page.screenshot({path:'test-results/generation-failure.png',fullPage:true});console.error('Page errors:',errors);throw e;}
finally{await browser.close();}
})().catch(e=>{console.error(e);process.exit(1)});