101 lines
9.1 KiB
JavaScript
101 lines
9.1 KiB
JavaScript
const assert=require('node:assert/strict');
|
|||
|
|
const fs=require('node:fs');
|
||
|
|
const {launchBrowser,openSetup}=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));
|
||
|
|
const suffix=Date.now(),username='sync-owner-'+suffix,password='local-workflow-test-password';
|
||
|
|
const colleague='sync-colleague-'+suffix;
|
||
|
|
try{
|
||
|
|
await page.goto(base);
|
||
|
|
await page.getByRole('button',{name:'New here? Create an account'}).click();
|
||
|
|
await page.getByLabel('Username',{exact:true}).fill(username);
|
||
|
|
await page.getByLabel('Display name',{exact:true}).fill('Workflow owner');
|
||
|
|
await page.getByLabel('Password',{exact:true}).fill(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.waitForFunction(()=>Alpine.$data(document.body).user && !Alpine.$data(document.body).busy);
|
||
|
|
await page.getByRole('button',{name:'New workspace',exact:true}).click();
|
||
|
|
await page.getByRole('dialog').getByLabel('Name',{exact:true}).fill('Shared publication test');
|
||
|
|
await page.getByRole('dialog').getByRole('button',{name:'Save changes',exact:true}).click();
|
||
|
|
await page.getByRole('dialog').waitFor({state:'hidden'});
|
||
|
|
const workspace=await page.evaluate(()=>Alpine.$data(document.body).workspace.id);
|
||
|
|
const secondContext=await browser.newContext({locale:'en-US'});
|
||
|
|
const registered=await secondContext.request.post(base+'/api/register',{headers:{Origin:base},data:{username:colleague,email:colleague+'@example.com',password,display_name:'Workflow colleague'}});assert.equal(registered.status(),200);
|
||
|
|
await page.evaluate(async colleague=>{const a=Alpine.$data(document.body);await a.api(a.endpoint('/members'),'PUT',{revision:a.workspace.revision,username:colleague,permissions:['view','edit_schedules','manage_integration']});await a.loadWorkspace();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();},colleague);
|
||
|
|
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.getByRole('button',{name:'Already registered? Sign in',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 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);});
|