Files
aego/tools/artstudio/release/tests/effect_templates_ui.py

136 lines
8.4 KiB
Python

"""Real New Effect UI and GPU checks, called by both existing browser GPU gates.
All mutations use controls/pointers. Go is queried only for state and checksums.
"""
import base64
import hashlib
def exercise_effect_templates(page, manifest, evidence, checked):
def state():
return page.evaluate('() => JSON.parse(aegoArt.request(\'{"action":"state"}\'))')
def snapshot_hash():
return page.evaluate('async () => AegoStudioProtocol.sha256(AegoStudioHost.capture().bytes)')
original = state()
original_hash = snapshot_hash()
catalog = page.evaluate('() => AegoStudioHost.effectTemplates()')
expected_ids = ['dust', 'spark', 'smoke', 'explosion', 'fire', 'hit-flash', 'magic-projectile', 'trail']
assert [item['id'] for item in catalog] == expected_ids
records = {item['id']: item for item in manifest['templates']}
assert set(records) == set(expected_ids)
receipts = []
for template in catalog:
identifier = template['id']
entry = records[identifier]
assert entry['particlePreset'] == template['particlePreset']
page.locator('#new').click()
page.locator('#new-dialog [name=template]').select_option(identifier)
for field in ('width', 'height', 'mode'):
assert page.locator(f'#new-dialog [name={field}]').is_disabled()
page.locator('#new-dialog [value=create]').click()
page.wait_for_function('(old) => AegoStudioHost.summary().handle !== old', arg=original['active'])
created = state()
doc = created['state']['document']
assert doc['properties']['aego.effectTemplate'] == identifier
assert len(doc['frames']) == 8 and len(doc['layers']) >= 2 and doc['tags'] and doc['slices']
assert created['state']['dirty'] and created['state']['undo'] == 0
assert page.locator('[data-workspace=vfx]').get_attribute('class') == 'active'
page.wait_for_function('''(id) => {
if(!window.aegoArtPreview)return false;
const s=JSON.parse(aegoArtPreview.status()).status;
return s.state==='ready'&&s.mode==='particles'&&s.effectTemplate===id&&s.frames===8&&s.stats.MaterialPending===0;
}''', arg=identifier, timeout=60000)
status = page.evaluate('() => JSON.parse(aegoArtPreview.status()).status')
assert status['particlePreset'] == template['particlePreset']
page.wait_for_function('() => Number(document.getElementById("gpu-rate").value)===JSON.parse(aegoArtPreview.status()).status.particleRate')
assert float(page.locator('#gpu-rate').input_value()) == status['particleRate']
chosen_rate = status['particleRate'] + 1
page.locator('#gpu-rate').press('ArrowRight')
page.wait_for_function('(rate) => JSON.parse(aegoArtPreview.status()).status.particleRate===rate', arg=chosen_rate)
page.locator('#next-frame').click()
page.wait_for_function('() => JSON.parse(aegoArtPreview.status()).status.frame===JSON.parse(aegoArt.request(\'{"action":"state"}\')).state.frame')
assert page.evaluate('() => JSON.parse(aegoArtPreview.status()).status.particleRate') == chosen_rate
page.locator('#first-frame').click()
assert state()['state']['undo'] == 0
before = page.evaluate('() => aegoArt.hash()')
page.locator('#hex-color').fill('#de17b9')
page.locator('#hex-color').press('Tab')
page.locator('[data-tool=pencil]').click()
box = page.locator('#canvas').bounding_box()
page.mouse.click(box['x']+1.5*box['width']/doc['canvas']['width'], box['y']+1.5*box['height']/doc['canvas']['height'])
after = page.evaluate('() => aegoArt.hash()')
assert before != after and state()['state']['undo'] == 1
revision = state()['state']['document']['revision']
page.wait_for_function('(revision) => JSON.parse(aegoArtPreview.status()).status.revision===revision', arg=revision)
assert page.evaluate('() => JSON.parse(aegoArtPreview.status()).status.particleRate') == chosen_rate
edit_checkpoint = state()['state']
# Sample the real WebGL drawing buffer during RAF, including brief
# one-shot HitFlash. No synthetic render or reference PNG is used.
page.evaluate('''({id,revision}) => {
let armed=false;
document.getElementById('gpu-restart').addEventListener('click',()=>armed=true,{once:true});
window.effectFrameProbe=new Promise((resolve,reject)=>{
let finished=false;
const timer=setTimeout(()=>{finished=true;reject(new Error('No visible runtime particle frame for '+id));},10000);
async function sample(){
if(finished)return;
try {
const s=JSON.parse(aegoArtPreview.status()).status;
if(armed&&s.state==='ready'&&s.effectTemplate===id&&s.revision===revision&&s.particles>0&&s.stats.Instances>0){
const png=document.getElementById('gpu-preview').toDataURL('image/png');
const bytes=Uint8Array.from(atob(png.split(',')[1]),c=>c.charCodeAt(0));
const image=await createImageBitmap(new Blob([bytes],{type:'image/png'}));
const reader=document.createElement('canvas');reader.width=image.width;reader.height=image.height;
const context=reader.getContext('2d',{willReadFrequently:true});context.drawImage(image,0,0);image.close();
const rgba=context.getImageData(0,0,reader.width,reader.height).data;
let visible=0;for(let i=4;i<rgba.length;i+=4)if(Math.abs(rgba[i]-rgba[0])+Math.abs(rgba[i+1]-rgba[1])+Math.abs(rgba[i+2]-rgba[2])>12)visible++;
if(visible>=5){finished=true;clearTimeout(timer);resolve({png,visible,status:s,width:reader.width,height:reader.height});return;}
}
requestAnimationFrame(sample);
} catch(error) { finished=true;clearTimeout(timer);reject(error); }
}
requestAnimationFrame(sample);
});
}''', {'id':identifier, 'revision':revision})
page.locator('#gpu-restart').click()
observed = page.evaluate('() => window.effectFrameProbe')
png = base64.b64decode(observed.pop('png').split(',', 1)[1])
(evidence/f'effect-{identifier}.png').write_bytes(png)
assert observed['status']['stats']['MaterialCompileFailures'] == 0
assert observed['status']['particleEmitted'] > 0
assert state()['state'] == edit_checkpoint, 'Preview changed the document, selection or History'
assert state()['state']['undo'] == 1 and page.evaluate('() => aegoArt.hash()') == after
page.locator('#undo').click()
assert page.evaluate('() => aegoArt.hash()') == before
page.locator('#redo').click()
assert page.evaluate('() => aegoArt.hash()') == after
expected_source = snapshot_hash()
with page.expect_download() as event:
page.locator('#studio-download-source').click()
download = event.value
path = evidence/f'effect-{identifier}.aeart'
download.save_as(path)
assert hashlib.sha256(path.read_bytes()).hexdigest() == expected_source
assert state()['state']['dirty'], 'Download guessed that a source was durably saved'
edited = state()['state']['document']
page.locator('#file-input').set_input_files(path)
page.wait_for_function('(old) => AegoStudioHost.summary().handle!==old', arg=created['active'])
reopened = state()
assert reopened['state']['document'] == edited
assert page.evaluate('() => aegoArt.hash()') == after
page.locator('.document-tab.active .close-document').click()
page.wait_for_function('(handle) => AegoStudioHost.summary().handle===handle', arg=created['active'])
page.locator('.document-tab.active .close-document').click()
page.wait_for_function('(handle) => AegoStudioHost.summary().handle===handle', arg=original['active'])
assert snapshot_hash() == original_hash and state()['state'] == original['state']
receipts.append({'id':identifier, 'preset':template['particlePreset'], 'sourceSHA256':expected_source,
'presetRate':status['particleRate'], 'editedPreviewRate':chosen_rate,
'beforePixelHash':before, 'afterPixelHash':after,
'framePNG_SHA256':hashlib.sha256(png).hexdigest(), 'observed':observed})
checked('shared-effect-template-ui-render-edit-export-'+identifier)
page.locator('[data-workspace=art]').click()
return receipts