const assert=require('node:assert/strict'); const fs=require('node:fs'); const {launchBrowser,openSetup,isolatedUser,createUser,signIn}=require('./support/browser.cjs'); 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(); 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();}); 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); 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);});