feat(admin): edit domain settings inline and copy saved configurations
deploy-preland / deploy zoomstage.online (push) Successful in 57s
deploy-preland / deploy zoomstage.online (push) Successful in 57s
This commit is contained in:
1 parent
84f70b1c71
commit
dd6d221dd7
19 files changed
+402
-409
No files matched your search
+8
-85
@@ -1,85 +1,8 @@
|
||||
/* Real UI UTM regression. Disposable Docker fixtures a/b only.
|
||||
* Browser plugin not available. No route mocks or host-installed runtimes.
|
||||
* VALID_ZIP must contain set3; PUBLIC_A/B are optional fixture origins. */
|
||||
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 output=process.env.UI_OUTPUT||'/tmp';
|
||||
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',dialog=>dialog.accept());
|
||||
const read=async id=>{const r=await page.request.get('/api/config?target='+encodeURIComponent(id)).then(r=>r.json());assert.ok(r.ok,r.error);assert.ok(r.config,'config missing');return r;};
|
||||
const poll=async id=>{for(let n=0;n<480;n++){const r=await page.request.get('/api/jobs?id='+id).then(r=>r.json());assert.ok(r.ok,r.error);assert.ok(r.job,'job missing: '+id);if(r.job.status==='completed')return r.job;await new Promise(resolve=>setTimeout(resolve,250));}throw Error('job timeout: '+id);};
|
||||
const group=()=>page.locator('section.field-block').filter({has:page.getByRole('heading',{name:'indpic= UTM',exact:true})});
|
||||
const open=async id=>{await page.locator('#source-target').selectOption(id);await page.getByLabel('indpic default',{exact:true}).waitFor();await page.waitForFunction(()=>document.querySelector('#read-state').textContent.includes('версия'));};
|
||||
const chooseBoth=async()=>{await page.locator('#targets input[value="b"]').check();await page.waitForFunction(()=>!document.querySelector('#targets').textContent.includes('чтение версии'));};
|
||||
const submit=async()=>{const response=page.waitForResponse(r=>r.url().endsWith('/api/apply')&&r.request().method()==='POST');await page.getByRole('button',{name:'Применить к выбранным',exact:true}).click();const accepted=await(await response).json();assert.ok(accepted.ok,accepted.error);assert.ok(accepted.job);const job=await poll(accepted.job.id);const rendered=page.locator('.job').filter({hasText:job.id});await rendered.getByText('Завершено',{exact:false}).first().waitFor();return {job,rendered};};
|
||||
const setWeights=async weights=>{const section=group();const rows=section.locator('.weight-table tbody tr');while(await rows.count())await rows.last().getByRole('button',{name:'Удалить',exact:true}).click();for(const [variant,weight]of Object.entries(weights)){await section.getByRole('button',{name:'Добавить вариант',exact:true}).click();const row=rows.last();await row.getByLabel('indpic вариант',{exact:true}).selectOption(variant);await row.locator('input[type=number]').fill(String(weight));}};
|
||||
let originals=[],primaryError=null;
|
||||
try{
|
||||
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());assert.ok(state.ok,state.error);
|
||||
for(const id of ['a','b'])assert.ok(state.targets.some(t=>t.id===id&&t.enabled),'enabled fixture '+id+' required');
|
||||
originals=await Promise.all(['a','b'].map(read));
|
||||
for(const original of originals){assert.ok(!Object.hasOwn(original.config.utm.indpic.weights,'3'),'original fixture must omit weights3 for rejection scenario');assert.notEqual(String(original.config.utm.indpic.default),'3');}
|
||||
await open('a');await chooseBoth();
|
||||
// Delivery is a real two-target ZIP form submission, not an API shortcut.
|
||||
assert.ok(process.env.VALID_ZIP,'VALID_ZIP=/fixture/valid-set3.zip required');
|
||||
await page.locator('#set-add input[name=number]').fill('3');
|
||||
await page.locator('#set-add input[name=name]').fill('UI UTM — набор 3');
|
||||
await page.locator('#set-add input[type=file]').setInputFiles(process.env.VALID_ZIP);
|
||||
const uploadResponse=page.waitForResponse(r=>r.url().endsWith('/api/sets/upload'));
|
||||
await page.locator('#set-add button').click();
|
||||
const upload=await(await uploadResponse).json();assert.ok(upload.ok,upload.error);
|
||||
const uploaded=await poll(upload.job.id);assert.deepEqual(uploaded.results.map(r=>r.status),['success','success']);
|
||||
await page.locator('#result-body').getByText('Сохранено и проверено',{exact:false}).waitFor();
|
||||
await page.getByRole('button',{name:'Готово',exact:true}).click();
|
||||
// Wrong path must fail clearly, without altering either runtime config.
|
||||
await page.getByLabel('indpic default',{exact:true}).selectOption('3');
|
||||
const rejected=await submit();assert.deepEqual(rejected.job.results.map(r=>r.status),['error','error']);
|
||||
for(const result of rejected.job.results)assert.match(result.message,/Добавьте вариант 3 в таблицу весов indpic.*вес 0/);
|
||||
await rejected.rendered.getByText(/Добавьте вариант 3 в таблицу весов indpic/).first().waitFor();
|
||||
assert.deepEqual((await read('a')).config,originals[0].config);assert.deepEqual((await read('b')).config,originals[1].config);
|
||||
await page.screenshot({path:output+'/admin-utm-rejected-default.png',fullPage:true});
|
||||
// Correct path: a zero-weight row and explicit default3, only these two fields.
|
||||
await group().getByRole('button',{name:'Добавить вариант',exact:true}).click();
|
||||
const added=group().locator('.weight-table tbody tr').last();
|
||||
await added.getByLabel('indpic вариант',{exact:true}).selectOption('3');
|
||||
await added.locator('input[type=number]').fill('0');
|
||||
// Rejected submission cleared explicit field selection; select default again.
|
||||
await page.getByLabel('indpic default',{exact:true}).selectOption('3');
|
||||
assert.equal(await page.locator('#targets input:checked').count(),2);
|
||||
assert.match(await page.locator('#change-summary').innerText(),/utm.indpic.weights/);
|
||||
const applied=await submit();assert.deepEqual(applied.job.results.map(r=>r.status),['success','success']);
|
||||
const actuals=await Promise.all(['a','b'].map(read));
|
||||
for(let i=0;i<2;i++){const expected=structuredClone(originals[i].config);expected.utm.indpic.default=typeof originals[0].config.utm.indpic.default==='number'?3:'3';expected.utm.indpic.weights={...originals[0].config.utm.indpic.weights,'3':0};assert.deepEqual(actuals[i].config,expected,'only selected indpic fields changed on '+['a','b'][i]);}
|
||||
fs.writeFileSync(output+'/admin-utm-before-restore.json',JSON.stringify({job:applied.job.id,configs:{a:actuals[0].config,b:actuals[1].config}},null,2));
|
||||
await page.getByRole('button',{name:'Перечитать',exact:true}).click();await page.waitForFunction(()=>document.querySelector('[aria-label="indpic default"]')?.value==='3');
|
||||
assert.equal(await group().locator('.weight-table tbody tr').filter({has:page.locator('option[value="3"]:checked')}).count(),1);
|
||||
await page.screenshot({path:output+'/admin-utm-applied.png',fullPage:true});
|
||||
const publicEvidence=[];
|
||||
for(const origin of [process.env.PUBLIC_A,process.env.PUBLIC_B].filter(Boolean)){
|
||||
const response=await page.request.get(new URL('/?indpic=abcd3zzzz',origin).href);assert.equal(response.status(),200,'public HTML '+origin);const html=await response.text();assert.match(html,/data-source="[^\"]*\/3\/01_desk_1-source\.avif/,'public resolver selects uploaded set3');
|
||||
const image=await page.request.get(new URL('/3/01_desk_1-source.jpg',origin).href);assert.equal(image.status(),200,'public uploaded JPEG '+origin);const bytes=await image.body();assert.equal(bytes[0],0xff);assert.equal(bytes[1],0xd8);publicEvidence.push({origin,htmlStatus:response.status(),imageStatus:image.status(),bytes:bytes.length});
|
||||
}
|
||||
fs.writeFileSync(output+'/admin-utm-public.json',JSON.stringify(publicEvidence,null,2));
|
||||
assert.deepEqual(errors,[]);
|
||||
}catch(error){primaryError=error;}finally{
|
||||
const cleanupErrors=[];
|
||||
// The restoration itself uses actual form row removal/addition/default controls.
|
||||
for(let i=0;i<originals.length;i++)try{await open(['a','b'][i]);await setWeights(originals[i].config.utm.indpic.weights);await page.getByLabel('indpic default',{exact:true}).selectOption(String(originals[i].config.utm.indpic.default));const restored=await submit();assert.equal(restored.job.results[0].status,'success',restored.job.results[0].message);assert.deepEqual((await read(['a','b'][i])).config,originals[i].config,'UI restored full original config');}catch(error){cleanupErrors.push(error);}
|
||||
fs.writeFileSync(output+'/admin-utm-console.json',JSON.stringify({pageErrors:errors,console:consoleMessages},null,2));
|
||||
try{await browser.close();}catch(error){cleanupErrors.push(error);}
|
||||
if(primaryError&&cleanupErrors.length)throw new AggregateError([primaryError,...cleanupErrors],'UTM scenario failed; separate UI restore errors',{cause:primaryError});
|
||||
if(primaryError)throw primaryError;if(cleanupErrors.length)throw new AggregateError(cleanupErrors,'UI restore failed');
|
||||
}
|
||||
console.log('PASS real UI bulk ZIP3 / rejected default without weight / weights3=0+default3 to a+b / reread / preserve other fields / public origins / UI restore');
|
||||
})().catch(error=>{console.error(error);process.exit(1);});
|
||||
/* preland#25: UTM inline editor and two-target ZIP regression, disposable fixtures only. */
|
||||
const {chromium}=require('playwright-core'),assert=require('node:assert/strict');
|
||||
(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'});page.on('dialog',d=>d.accept());const errors=[];page.on('pageerror',e=>errors.push(e.message));const originals=new Map();
|
||||
const cell=id=>page.locator('[data-matrix-target="'+id+'"][data-marker=indpic]');const saved=async id=>page.waitForFunction(id=>document.querySelector('[data-overview-target="'+id+'"] .matrix-status').textContent==='Сохранено',id,{timeout:90000});const save=async id=>{await page.getByRole('button',{name:'Сохранить настройки '+id,exact:true}).click();await saved(id);};const read=async id=>{const r=await page.request.get('/api/config?target='+id).then(r=>r.json());assert.ok(r.ok,r.error);return r;};
|
||||
try{await page.goto('/');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();for(const id of ['a','b']){await saved(id);originals.set(id,(await read(id)).config);assert.ok(!Object.hasOwn(originals.get(id).utm.indpic.weights,'3'),'fixture originally omits3');}
|
||||
await page.locator('#source-target').selectOption('a');await page.locator('#sets-body tr').first().waitFor();await page.locator('#targets input[value=b]').check();assert.ok(process.env.VALID_ZIP,'VALID_ZIP with set3 required');await page.locator('#set-add input[name=number]').fill('3');await page.locator('#set-add input[name=name]').fill('UI #25 — набор 3');await page.locator('#set-add input[type=file]').setInputFiles(process.env.VALID_ZIP);await page.locator('#set-add button').click();await page.locator('#result-body').getByText('Сохранено и проверено',{exact:false}).waitFor({timeout:90000});await page.getByRole('button',{name:'Готово',exact:true}).click();for(const id of ['a','b']){assert.deepEqual((await read(id)).config,originals.get(id),'ZIP leaves config unchanged');assert.equal(await cell(id).locator('input[type=radio][aria-label="indpic default 3 '+id+'"]').count(),0,'missing variant cannot be silently selected as default');await cell(id).getByRole('button',{name:'+ Вариант',exact:true}).click();const row=cell(id).locator('.matrix-variant').last();await row.locator('select').selectOption('3');await row.locator('input[type=number]').fill('0');await row.locator('input[type=radio]').check();await save(id);const expected=structuredClone(originals.get(id));expected.utm.indpic.weights['3']=0;expected.utm.indpic.default=typeof expected.utm.indpic.default==='number'?3:'3';assert.deepEqual((await read(id)).config,expected);await page.getByRole('button',{name:'Обновить настройки '+id,exact:true}).click();await saved(id);assert.equal(await cell(id).locator('input[type=radio]:checked').evaluate(n=>n.closest('.matrix-variant').querySelector('select').value),'3');}
|
||||
assert.deepEqual(await page.locator('#targets input:checked').evaluateAll(n=>n.map(i=>i.value)),['a','b']);assert.deepEqual(errors,[]);console.log('PASS actual two-target ZIP / new variant3=0+explicit default / per-column Save+reread / otherfields preserved');
|
||||
}finally{for(const [id,config]of originals){const rows=cell(id).locator('.matrix-variant');for(const row of await rows.all()){if(await row.locator('select').inputValue()==='3')await row.locator('button').click();}const def=String(config.utm.indpic.default);for(const row of await cell(id).locator('.matrix-variant').all())if(await row.locator('select').inputValue()===def)await row.locator('input[type=radio]').check();const button=page.getByRole('button',{name:'Сохранить настройки '+id,exact:true});if(await button.isEnabled()){await save(id);assert.deepEqual((await read(id)).config,config);}}await browser.close();}})().catch(e=>{console.error(e);process.exit(1)});
|
||||
Reference in new issue
Block a user