// Isolated browser acceptance checks against the actual built app and SQLite backend. import {chromium, expect} from '@playwright/test'; import {spawn, spawnSync} from 'node:child_process'; import {mkdtemp, mkdir, rm} from 'node:fs/promises'; import {tmpdir} from 'node:os'; import path from 'node:path'; import net from 'node:net'; import {fileURLToPath} from 'node:url'; const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); const data=await mkdtemp(path.join(tmpdir(),'conductor-management-')); const artifacts=path.join(root,'test-results/management'); await mkdir(artifacts,{recursive:true}); const port=await new Promise(resolve=>{ const socket=net.createServer();socket.listen(0,'127.0.0.1',()=>{const value=socket.address().port;socket.close(()=>resolve(value));}); }); const base='http://127.0.0.1:'+port; const env={...process.env,PORT:String(port),CONDUCTOR_DATA_DIR:data,CONDUCTOR_STATIC_DIR:path.join(root,'frontend/build'),CONDUCTOR_SECRET_KEY:Buffer.alloc(32,19).toString('base64')}; let output=''; const server=spawn(process.execPath,['backend/dist/index.js'],{cwd:root,env,stdio:['ignore','pipe','pipe']}); server.stdout.on('data',chunk=>{output+=chunk;});server.stderr.on('data',chunk=>{output+=chunk;}); let browser,page; const registry=(command,manifest)=>{ const args=['backend/dist/scripts/includedApps.js',command]; if(command==='unregister')args.push(manifest); const result=spawnSync(process.execPath,args,{cwd:root,env,input:command==='register'?JSON.stringify(manifest):undefined,encoding:'utf8'}); if(result.status!==0)throw new Error(result.stderr||result.stdout); return JSON.parse(result.stdout); }; try { for(let attempt=0;attempt<100;attempt++){ if(server.exitCode!==null)throw new Error(output); if(await fetch(base+'/api/health').then(r=>r.ok).catch(()=>false))break; if(attempt===99)throw new Error('Backend did not start.\n'+output); await new Promise(resolve=>setTimeout(resolve,50)); } browser=await chromium.launch({headless:true}); const context=await browser.newContext({baseURL:base,viewport:{width:1360,height:960}}); const setup=await context.request.get('/api/auth/setup'); const token=(await setup.json()).setupToken; const created=await context.request.post('/api/auth/setup',{headers:{'X-Setup-Token':token},data:{username:'management-admin',password:'management-test-password',displayName:'Test Administrator'}}); expect(created.status()).toBe(201); const csrf=(await created.json()).csrfToken; const request=(method,url,body)=>context.request.fetch(url,{method,headers:{'X-CSRF-Token':csrf},data:body}); page=await context.newPage(); const errors=[];page.on('pageerror',error=>errors.push(error.message)); const nav=name=>page.getByRole('navigation').getByRole('button',{name,exact:true}).click(); await page.goto('/'); await expect(page.getByRole('heading',{name:'Welcome to Conductor'})).toBeVisible(); await expect(page.getByText(/Backend scaffold|coming in a later step/)).toHaveCount(0); await expect(page.getByText('No included applications are installed yet.',{exact:false})).toBeVisible(); await expect(page.getByText('Social Scheduler',{exact:true})).toHaveCount(0); await page.screenshot({path:path.join(artifacts,'home-empty.png'),fullPage:true}); await page.getByRole('button',{name:/^Projects Create a project/}).click(); await expect(page.getByText('No saved projects yet.')).toBeVisible(); await page.getByRole('button',{name:'New project',exact:true}).click(); await page.getByLabel('Project name',{exact:true}).fill('Customer Dashboard'); await page.getByLabel('Description',{exact:true}).fill('A saved builder project'); await page.getByRole('button',{name:'Create project',exact:true}).click(); await expect(page.getByTitle('Click to rename project')).toHaveText('Customer Dashboard'); const saved=(await (await request('GET','/api/projects')).json())[0]; const document=await (await request('GET','/api/projects/'+saved.id)).json(); expect(JSON.parse(document.project_json).project.description).toBe('A saved builder project'); await nav('Projects'); let card=page.getByRole('article',{name:'Project Customer Dashboard',exact:true}); await card.getByRole('button',{name:'Rename',exact:true}).click(); await card.getByLabel('Project name',{exact:true}).fill('Customer Operations'); await card.getByRole('button',{name:'Save details',exact:true}).click(); card=page.getByRole('article',{name:'Project Customer Operations',exact:true}); await expect(card).toBeVisible(); await page.getByLabel('Search projects').fill('missing'); await expect(page.getByText('No projects match your search.')).toBeVisible(); await page.getByLabel('Search projects').fill(''); await card.getByRole('button',{name:'Preview',exact:true}).click(); await expect(page.getByRole('heading',{name:'Projects',exact:true})).toHaveCount(0); await nav('Projects'); const other=await request('POST','/api/projects',{name:'Second Project',description:'Another builder project'}); expect(other.status()).toBe(201); await card.getByRole('button',{name:'Open editor',exact:true}).click(); await page.getByTitle('Click to rename project').click(); await page.locator('[aria-label="Project commands"] input').fill('Unsaved name'); await page.locator('[aria-label="Project commands"] input').press('Enter'); await nav('Projects'); await expect(page.getByText('You have unsaved changes to',{exact:false})).toBeVisible(); await expect(card.getByRole('button',{name:'Rename',exact:true})).toBeDisabled(); page.once('dialog',dialog=>dialog.dismiss()); await page.getByRole('article',{name:'Project Second Project',exact:true}).getByRole('button',{name:'Open editor',exact:true}).click(); await expect(page.getByRole('heading',{name:'Projects',exact:true})).toBeVisible(); page.once('dialog',dialog=>dialog.accept()); await page.getByRole('article',{name:'Project Second Project',exact:true}).getByRole('button',{name:'Open editor',exact:true}).click(); await expect(page.getByTitle('Click to rename project')).toHaveText('Second Project'); await nav('Projects'); await page.screenshot({path:path.join(artifacts,'projects.png'),fullPage:true}); await card.getByRole('button',{name:'Publish',exact:true}).click(); await expect(page.getByLabel('Saved project')).toHaveValue(String(saved.id)); await page.getByLabel('Application name',{exact:true}).fill('Operations App'); await page.getByLabel('Application address',{exact:true}).fill('operations-app'); await page.getByRole('button',{name:'Publish',exact:true}).click(); await expect(page.getByRole('heading',{name:'Operations App'})).toBeVisible(); await page.getByRole('button',{name:'Republish',exact:true}).click(); await expect(page.getByText('Signed-in users ยท Version 2')).toBeVisible(); await nav('Projects'); page.once('dialog',dialog=>dialog.accept()); await card.getByRole('button',{name:'Delete',exact:true}).click(); await expect(page.getByRole('alert')).toContainText('Unpublish'); await nav('Publishing'); await page.getByRole('button',{name:'Unpublish',exact:true}).click(); await expect(page.getByText('Unpublished',{exact:true})).toBeVisible(); await nav('Projects'); page.once('dialog',dialog=>dialog.accept()); await card.getByRole('button',{name:'Delete',exact:true}).click(); await expect(card).toHaveCount(0); await nav('Publishing'); await expect(page.getByText('No included applications are installed yet.',{exact:false})).toBeVisible(); const manifest={schemaVersion:1,id:'example-application',name:'Example Application',description:'An application registered by its installer.',version:'1.0.0',launchPath:'/example-application',configurationPath:'/example-application?view=settings',audience:'all-users'}; registry('register',manifest);registry('register',{...manifest,version:'1.1.0'}); registry('register',{...manifest,id:'admin-application',name:'Admin Application',audience:'admins'}); await page.getByRole('button',{name:'Refresh applications',exact:true}).click(); await expect(page.getByRole('heading',{name:'Example Application',exact:true})).toHaveCount(1); await expect(page.getByText('Version 1.1.0',{exact:true})).toBeVisible(); await expect(page.getByRole('link',{name:'Open Example Application',exact:true})).toHaveAttribute('href','/example-application'); await expect(page.getByRole('link',{name:'Configure',exact:true})).toHaveCount(2); await page.screenshot({path:path.join(artifacts,'publishing-included.png'),fullPage:true}); await page.route('**/api/included-apps',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({error:'Applications temporarily unavailable.'})})); await page.getByRole('button',{name:'Refresh applications',exact:true}).click(); await expect(page.getByRole('alert')).toContainText('Included applications could not be loaded. Please try again.'); await page.unroute('**/api/included-apps'); await page.getByRole('button',{name:'Refresh applications',exact:true}).click(); await expect(page.getByRole('heading',{name:'Example Application',exact:true})).toBeVisible(); const user=await request('POST','/api/admin/users',{username:'management-user',password:'management-user-password',role:'user'}); expect(user.status()).toBe(201); const userContext=await browser.newContext({baseURL:base,viewport:{width:1100,height:800}}); expect((await userContext.request.post('/api/auth/login',{data:{username:'management-user',password:'management-user-password'}})).status()).toBe(200); const userPage=await userContext.newPage();await userPage.goto('/'); await expect(userPage.getByRole('heading',{name:'Example Application',exact:true})).toBeVisible(); await expect(userPage.getByRole('heading',{name:'Admin Application',exact:true})).toHaveCount(0); await expect(userPage.getByRole('link',{name:'Configure',exact:true})).toHaveCount(0); await userPage.screenshot({path:path.join(artifacts,'user-applications.png'),fullPage:true}); registry('unregister','example-application'); await userPage.getByRole('button',{name:'Refresh applications',exact:true}).click(); await expect(userPage.getByText('No included applications are installed yet.',{exact:false})).toBeVisible(); await page.goto('/'); await expect(page.getByRole('heading',{name:'Admin Application',exact:true})).toBeVisible(); await expect(page.getByRole('heading',{name:'Example Application',exact:true})).toHaveCount(0); expect(errors).toEqual([]); console.log('PASS: home, projects CRUD/search/unsaved protection, publish/republish/unpublish, installer registration/update/removal, role filtering, API failure recovery.'); } catch(error) { if(page)await page.screenshot({path:path.join(artifacts,'failure.png'),fullPage:true}).catch(()=>{}); throw error; } finally { await browser?.close(); if(server.exitCode===null){await new Promise(resolve=>{server.once('exit',resolve);server.kill('SIGTERM');});} await rm(data,{recursive:true,force:true}); }