avatarReference.spec.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142
  1. import {expect,test} from '@playwright/test'
  2. import {readFileSync} from 'node:fs'
  3. const source=JSON.parse(readFileSync(new URL('../../assets/mascot/laoa-source.json',import.meta.url),'utf8'))
  4. test('matches LaoA original paths and all 25 eye expressions at full visible size',async({page,context},testInfo)=>{
  5. const reference=await context.newPage()
  6. await reference.goto('http://127.0.0.1:18806')
  7. await page.goto('http://127.0.0.1:18807')
  8. await expect(page.locator('[data-part=eye0]').first()).toHaveAttribute('d',/.+/)
  9. const ids=await page.locator('[data-testid]').evaluateAll(nodes=>nodes.map(n=>n.getAttribute('data-testid')!))
  10. expect(ids).toHaveLength(53)
  11. for(const id of ids){
  12. const svg=page.getByTestId(id).locator('svg')
  13. await expect(svg.locator('[data-part=mouth]')).toHaveCount(0)
  14. const measure=await svg.evaluate(svg=>{
  15. const viewport=(svg as SVGSVGElement).viewBox.baseVal,b=(svg.querySelector('[data-part=outline] path') as SVGGraphicsElement).getBBox()
  16. return {side:viewport.width,body:Math.max(b.width,b.height),centerX:viewport.x+viewport.width/2,bodyX:b.x+b.width/2,centerY:viewport.y+viewport.height/2,bodyY:b.y+b.height/2}
  17. })
  18. expect(measure.side,id).toBeCloseTo(measure.body,3)
  19. expect(measure.centerX,id).toBeCloseTo(measure.bodyX,3);expect(measure.centerY,id).toBeCloseTo(measure.bodyY,3)
  20. if(id.startsWith('raw-')){
  21. const index=Number(id.slice(4))
  22. for(let eye=0;eye<2;eye++)await expect(svg.locator(`[data-part=eye${eye}]`)).toHaveAttribute('d','M'+source.expressions[index][eye].map((p:number[])=>p.map(n=>n.toFixed(2)).join(' ')).join('L')+'Z')
  23. }
  24. async function pixels(locator:typeof svg){return locator.evaluate(async svg=>{
  25. const clone=svg.cloneNode(true) as SVGElement;clone.setAttribute('xmlns','http://www.w3.org/2000/svg');clone.setAttribute('width','96');clone.setAttribute('height','96')
  26. const image=new Image();image.src='data:image/svg+xml;base64,'+btoa(new XMLSerializer().serializeToString(clone));await image.decode()
  27. const canvas=document.createElement('canvas');canvas.width=96;canvas.height=96;const ctx=canvas.getContext('2d')!;ctx.fillStyle='white';ctx.fillRect(0,0,96,96);ctx.drawImage(image,0,0,96,96);return Array.from(ctx.getImageData(0,0,96,96).data)
  28. })}
  29. const a=await pixels(svg),b=await pixels(reference.getByTestId(id).locator('svg'))
  30. const mean=a.reduce((sum,v,i)=>sum+Math.abs(v-b[i]!),0)/a.length
  31. expect(mean,id).toBeLessThan(0.5)
  32. await svg.screenshot({path:testInfo.outputPath(id+'.png')})
  33. }
  34. await reference.screenshot({path:testInfo.outputPath('laoa-reference.png'),fullPage:true})
  35. await page.screenshot({path:testInfo.outputPath('web-avatar-grid.png'),fullPage:true})
  36. await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
  37. await page.addStyleTag({content:'body{background:#151515;color:#eee}'})
  38. await page.screenshot({path:testInfo.outputPath('web-avatar-grid-dark.png'),fullPage:true})
  39. await expect(page.locator('[data-animated=true]')).toHaveCount(0)
  40. await reference.close()
  41. })