Files

93 lines
8.1 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,openSetup,isolatedUser,createUser,signIn}=require('./support/browser.cjs');
2026-10-07 15:30:31 +01:00
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 launchBrowser();
const context=await browser.newContext({locale:'en-US',viewport:{width:1440,height:1000}});
const page=await context.newPage();page.setDefaultTimeout(15000);
const errors=[];page.on('pageerror',e=>errors.push(e.message));
try{
// Sandbox 13 belongs to this suite: two administrators (the owner and a colleague with the same access).
const owner=await isolatedUser({sandbox:13,prefix:'sync-owner',display:'Workflow owner'});
const colleague=await createUser(owner.admin,{sandbox:13,role:'admin',prefix:'sync-colleague',display:'Workflow colleague'});
const username=owner.username,password=owner.password;
await signIn(page,username,password);
await page.getByLabel('Workspace',{exact:true}).waitFor();
2026-10-07 15:30:31 +01:00
const workspace=await page.evaluate(()=>Alpine.$data(document.body).workspace.id);
const secondContext=await browser.newContext({locale:'en-US'});
await page.evaluate(async()=>{const a=Alpine.$data(document.body);const doc=structuredClone(Alpine.raw(a.doc));doc.schedules.push({id:crypto.randomUUID(),name:'Awaiting publication',type:'rotation',participants:[a.user.id],anchor:new Date().toISOString().slice(0,10),shift_days:7,handover:'11:00:00'});await a.api(a.endpoint('/config'),'PUT',{revision:a.workspace.revision,document:doc});await a.loadWorkspace();});
2026-10-07 15:30:31 +01:00
const review=page.locator('[data-publication-review]');
await review.getByText('+1 new',{exact:true}).waitFor();
const initialSync=await page.evaluate(()=>structuredClone(Alpine.raw(Alpine.$data(document.body).detail.sync)));
await page.evaluate(()=>{const a=Alpine.$data(document.body);a.detail.sync={...a.detail.sync,state:'publishing',publication_busy:true,lease_until:new Date(Date.now()+300000).toISOString()};});
assert(await review.locator('.publication-sync').isDisabled());
await page.evaluate(()=>{Alpine.$data(document.body).detail.sync.publication_busy=false;});
await page.waitForFunction(()=>!document.querySelector('.publication-sync').disabled);
await review.getByText('The publication lock has expired. The worker can resume; you can also request synchronization again.',{exact:true}).waitFor();
await page.screenshot({path:'test-results/manual-sync-expired-lease.png',fullPage:true});
await page.evaluate(sync=>{Alpine.$data(document.body).detail.sync=sync;},initialSync);
assert(await review.locator('.publication-sync').evaluate(el=>el.classList.contains('sync-needed')));
await openSetup(page,'Variables & rules');
await page.getByRole('button',{name:'+ Variable',exact:true}).click();
await page.getByLabel('Variable name',{exact:true}).fill('Shared unsynchronized draft');
await page.getByRole('button',{name:'Use in draft',exact:true}).click();
await page.waitForFunction(()=>{const a=Alpine.$data(document.body);return a.detail.generation_draft?.revision>0 && !a.generationSaving && !a.generationLoading;});
await review.getByRole('button',{name:'Reload saved state',exact:true}).waitFor();
await review.getByRole('button',{name:'Save checked changes',exact:true}).waitFor();
page.once('dialog',dialog=>dialog.dismiss());
await page.getByRole('button',{name:'Remove variable Shared unsynchronized draft',exact:true}).click();
await page.getByRole('button',{name:'Edit variable Shared unsynchronized draft',exact:true}).waitFor();
await page.getByRole('button',{name:'+ Variable',exact:true}).click();
await page.getByLabel('Variable name',{exact:true}).fill('Delete confirmation test');
await page.getByRole('button',{name:'Use in draft',exact:true}).click();
page.once('dialog',dialog=>dialog.accept());
await page.getByRole('button',{name:'Remove variable Delete confirmation test',exact:true}).click();
await page.getByRole('button',{name:'Edit variable Delete confirmation test',exact:true}).waitFor({state:'hidden'});
await page.evaluate(()=>{const a=Alpine.$data(document.body),block=a.newBlock('rotation');block.fields.name=a.literal('Generated pending schedule');a.generationDraft.rules.push({id:crypto.randomUUID(),name:'Shared rotation rule',enabled:true,iterations:[],when:null,blocks:[block]});});
await review.getByText('+2 new',{exact:true}).waitFor();
// More than one worker tick passes; saving has not authorized a publication.
await page.waitForTimeout(6000);
const before=await page.evaluate(async()=>{const a=Alpine.$data(document.body);return (await a.api(a.endpoint())).sync;});assert.equal(before.published_revision,null);assert.equal(before.authorized_revision,null);
await page.getByRole('button',{name:'Sign out',exact:true}).click();
await page.getByLabel('Username',{exact:true}).fill(username);await page.getByLabel('Password',{exact:true}).fill(password);
await page.getByRole('button',{name:'Sign in',exact:true}).click();await openSetup(page,'Variables & rules');
await page.getByRole('button',{name:'Edit variable Shared unsynchronized draft',exact:true}).waitFor();
const second=await secondContext.newPage();second.on('pageerror',e=>errors.push(e.message));
await signIn(second,colleague.username,colleague.password);
2026-10-07 15:30:31 +01:00
await second.goto(base+'/?workspace='+workspace+'&view=configuration');
await second.getByRole('button',{name:'Edit variable Shared unsynchronized draft',exact:true}).waitFor();
await second.waitForFunction(()=>!Alpine.$data(document.body).generationLoading);
await second.screenshot({path:'test-results/manual-sync-shared-desktop.png',fullPage:true});
const secondReview=second.locator('[data-publication-review]');
let holdQueued=true;
const workspaceUrl=base+'/api/workspaces/'+workspace;
await second.route(workspaceUrl,async route=>{const response=await route.fetch();const data=await response.json();if(holdQueued)data.sync={...data.sync,state:'pending',authorized_revision:data.workspace?.revision||1,publication_busy:false,lease_until:null};await route.fulfill({response,json:data});});
await secondReview.locator('.publication-sync').click();
await second.waitForFunction(()=>!Alpine.$data(document.body).busy);
assert.equal(await second.evaluate(()=>Alpine.$data(document.body).error),'');
assert(await secondReview.locator('.publication-sync').isDisabled());
await secondReview.getByRole('button',{name:'Queued…',exact:true}).waitFor();
await secondReview.getByText('Synchronization requested. Waiting for the worker; status updates automatically.',{exact:true}).waitFor();
await second.screenshot({path:'test-results/manual-sync-queued.png',fullPage:true});
holdQueued=false;
await second.waitForFunction(()=>Alpine.$data(document.body).detail.sync.state==='synced');
const after=await second.evaluate(()=>Alpine.$data(document.body).detail.sync);
assert.equal(after.state,'synced');assert.equal(after.has_changes,false);
assert(!await secondReview.locator('.publication-sync').evaluate(el=>el.classList.contains('sync-needed')));
assert.equal(await second.evaluate(()=>Alpine.$data(document.body).doc.schedules.length),2);
await second.waitForFunction(()=>!Alpine.$data(document.body).generationLoading);
await second.setViewportSize({width:390,height:844});
await second.evaluate(()=>{Alpine.$data(document.body).setupOpen=false;});
await second.waitForFunction(()=>getComputedStyle(document.querySelector('.publication-sync')).backgroundColor==='rgb(255, 255, 255)');
await second.screenshot({path:'test-results/manual-sync-mobile.png',fullPage:true});
assert(await second.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));
assert.deepEqual(errors,[]);
console.log('Shared draft, no automatic publication, colleague sync, published review and mobile layout passed.');
await secondContext.close();
}catch(e){await page.screenshot({path:'test-results/manual-sync-failure.png',fullPage:true});console.error('Browser errors:',errors);console.error(await page.locator('body').innerText());throw e;}finally{await browser.close();}
})().catch(e=>{console.error(e);process.exit(1);});