/* Real HTTP + rendered regression: stale version, independent partial result, * invalid ZIP recovery, safe DOM. Run only against disposable Docker fixtures. * No browser network mocking. Browser plugin not available. */ const {chromium}=require('playwright-core'); const assert=require('node:assert/strict'); const fs=require('node:fs'); (async()=>{ const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',args:['--no-sandbox']}); const page=await browser.newPage({baseURL:process.env.ADMIN_URL||'http://admin',viewport:{width:1440,height:1000}}); const errors=[],consoleMessages=[];page.on('pageerror',e=>errors.push(e.message));page.on('console',m=>{if(['error','warning'].includes(m.type()))consoleMessages.push({type:m.type(),text:m.text()});});page.on('dialog',d=>d.accept()); await page.goto(process.env.ADMIN_URL||'http://admin'); await page.getByLabel('Логин',{exact:true}).fill(process.env.ADMIN_USER||'admin'); await page.getByLabel('Пароль',{exact:true}).fill(process.env.ADMIN_PASSWORD||'test-password'); await page.getByRole('button',{name:'Войти',exact:true}).click(); await page.locator('#workspace').waitFor({state:'visible'}); const state=await page.request.get('/api/state').then(r=>r.json()); const api=async(path,data)=>{const response=await page.request.post('/api/'+path,{data,headers:{'X-CSRF-Token':state.csrf}});const result=await response.json();assert.equal(result.ok,true,result.error);return result;}; const read=async(id)=>page.request.get('/api/config?target='+encodeURIComponent(id)).then(r=>r.json()); const poll=async(id)=>{for(let n=0;n<120;n++){const r=await page.request.get('/api/jobs?id='+id).then(r=>r.json());assert.ok(r.ok,r.error||'GET jobs вернул ошибку без описания');assert.ok(r.job,'GET jobs не вернул задание '+id);if(r.job.status==='completed')return r.job;await new Promise(r=>setTimeout(r,250));}throw Error('job timeout '+id);}; const ids=state.targets.filter(t=>t.enabled).slice(0,2).map(t=>t.id);assert.equal(ids.length,2,'two disposable targets required'); const originals=await Promise.all(ids.map(read)); const externalHost='external-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,10)+'.example.org'; let primaryError=null; const restore=async()=>{const targets=await Promise.all(ids.map(async id=>({id,version:(await read(id)).version})));for(let i=0;i!document.querySelector('#targets').textContent.includes('чтение версии')); const altered=await api('apply',{targets:[{id:ids[1],version:originals[1].version}],changes:[{path:['host'],value:externalHost}]}); assert.equal((await poll(altered.job.id)).results[0].status,'success'); await page.getByLabel('Основной сайт',{exact:true}).fill('partial-check.example.org'); const responsePromise=page.waitForResponse(r=>r.url().endsWith('/api/apply')&&r.request().method()==='POST'); await page.getByRole('button',{name:'Применить к выбранным',exact:true}).click(); const submitted=await (await responsePromise).json(); const result=await poll(submitted.job.id); assert.deepEqual(result.results.map(r=>r.status),['success','conflict']); const job=page.locator('.job').filter({hasText:submitted.job.id}); await job.getByText('Конфликт версии — перечитайте домен',{exact:true}).waitFor(); assert.equal((await read(ids[0])).config.host,'partial-check.example.org'); assert.equal((await read(ids[1])).config.host,externalHost,'stale UI did not overwrite external writer'); await page.screenshot({path:(process.env.UI_OUTPUT||'/tmp')+'/admin-conflict-partial.png',fullPage:true}); await page.reload(); await page.locator('#workspace').waitFor({state:'visible'}); await page.locator('.job').filter({hasText:submitted.job.id}).waitFor(); await page.locator('#source-target').selectOption(ids[0]); await page.getByLabel('Основной сайт',{exact:true}).waitFor(); await page.locator('#set-add input[name=number]').fill(process.env.ZIP_NUMBER||'9'); await page.locator('#set-add input[name=name]').fill('Тест ZIP'); await page.locator('#set-add input[type=file]').setInputFiles({name:'invalid.zip',mimeType:'application/zip',buffer:Buffer.from('not a ZIP')}); const uploadResponse=page.waitForResponse(r=>r.url().endsWith('/api/sets/upload')); await page.locator('#set-add button').click(); const invalidResult=await (await uploadResponse).json(); if(invalidResult.ok){const invalidJob=await poll(invalidResult.job.id);assert.equal(invalidJob.results[0].status,'error');await page.locator('#indpic-result-modal').waitFor({state:'visible'});await page.getByRole('button',{name:'Готово',exact:true}).click();} else await page.locator('#notice.alert-danger').waitFor({state:'visible'}); assert.equal((await read(ids[0])).config.host,'partial-check.example.org','invalid ZIP kept working config'); if(process.env.VALID_ZIP){ await page.locator('#set-add input[type=file]').setInputFiles(process.env.VALID_ZIP); const response=page.waitForResponse(r=>r.url().endsWith('/api/sets/upload')); await page.locator('#set-add button').click();const accepted=await(await response).json();assert.equal(accepted.ok,true,accepted.error); const done=await poll(accepted.job.id);assert.equal(done.results[0].status,'success',done.results[0].message); await page.locator('#result-body').getByText('Сохранено и проверено',{exact:false}).waitFor(); await page.getByRole('button',{name:'Готово',exact:true}).click(); } await page.screenshot({path:(process.env.UI_OUTPUT||'/tmp')+'/admin-zip-recovery.png',fullPage:true}); fs.writeFileSync((process.env.UI_OUTPUT||'/tmp')+'/admin-regression-console.json',JSON.stringify({pageErrors:errors,console:consoleMessages},null,2)); assert.deepEqual(errors,[]);console.log('PASS real partial success/conflict/external state/persistent history/invalid ZIP'+(process.env.VALID_ZIP?'/valid ZIP recovery':'')); }catch(error){primaryError=error;}finally{ const cleanupErrors=[]; try{await restore();}catch(error){cleanupErrors.push(error);} try{await browser.close();}catch(error){cleanupErrors.push(error);} if(primaryError&&cleanupErrors.length)throw new AggregateError([primaryError,...cleanupErrors],'Основная ошибка browser-сценария; отдельно — ошибки восстановления fixture / закрытия browser',{cause:primaryError}); if(primaryError)throw primaryError; if(cleanupErrors.length)throw new AggregateError(cleanupErrors,'Ошибка восстановления fixture / закрытия browser'); } })().catch(e=>{console.error(e);process.exit(1);});