UNPKG

flexpect

Version:

Automated layout validation tool using Playwright. Ensures responsive design accuracy across multiple viewports by inspecting element positions and visual alignment.

126 lines (89 loc) 18.4 kB
var y=class extends Error{constructor(r){super(`Expected element located by "${r.toString()}" to have a bounding box, but none was returned. This could indicate that the element is not visible, not rendered, or detached from the DOM.`),this.name="BoundingBoxError"}};async function u(n){let r=await n.boundingBox();if(r)return r;throw new y(n)}var g=(t=>(t.Percent="percent",t.Pixels="pixels",t))(g||{});async function Y(n,r,t={}){let{tolerance:e=0,toleranceUnit:o="percent"}=t;if(e<0)throw new Error('"tolerance" must be greater than or equal to 0');let i=await u(n),a=await u(r),c=o==="percent"?e/100*a.height:e,s=a.y-(i.y+i.height);return s>=-c?{pass:!0,message:()=>{if(c===0)return"Element is strictly above the reference.";let l=o==="percent"?"%":"px";return`Element is above the reference within ${e}${l} tolerance.`}}:{pass:!1,message:()=>{let l=c.toFixed(2),p=s.toFixed(2),m=o==="percent"?"%":"px";return`Element is not above the reference. Details: - Allowed deviation: \u2264 ${l}px (${e}${m}) - Actual deviation: ${p}px To fix this, move the element upward or increase the tolerance.`}}}function L(n,r,t){let e=r==="horizontal"?n.x:n.y,o=r==="horizontal"?n.width:n.height;switch(t){case"start":return e;case"center":return e+o/2;case"end":return e+o;default:throw new Error(`Invalid alignment mode: ${t}`)}}var R=(e=>(e.Start="start",e.Center="center",e.End="end",e))(R||{}),E=(t=>(t.Horizontal="horizontal",t.Vertical="vertical",t))(E||{});async function N(n,r,t,e,o={}){let{tolerance:i=0,toleranceUnit:a="percent"}=o;if(i<0)throw new Error('"tolerance" must be greater than or equal to 0');let c=await u(n),s=await u(r),l=L(c,t,e),p=L(s,t,e),m=t==="horizontal"?s.width:s.height,d=a==="percent"?i/100*m:i,h=Math.abs(l-p);return h<=d?{pass:!0,message:()=>{if(i===0)return`Element is aligned (${e}) along ${t} axis.`;let x=a==="percent"?"%":"px";return`Element is aligned (${e}) along ${t} axis within ${i}${x} tolerance.`}}:{pass:!1,message:()=>{let x=d.toFixed(2),f=h.toFixed(2),w=e.toLowerCase(),B=t.toLowerCase(),$=a==="percent"?"%":"px";return`Element is misaligned with the container (${e}, ${t}). Details: - Allowed deviation: \xB1${x}px (${i}${$}) - Actual deviation: ${f}px To fix this, ensure the element is aligned to the container's ${w} edge along the ${B} axis.`}}}async function J(n,r,t={}){let{tolerance:e=0,toleranceUnit:o="percent"}=t;if(e<0)throw new Error('"tolerance" must be greater than or equal to 0');let i=await u(n),a=i.width/i.height,c=Math.abs(a-r),s=o==="percent"?e/100*r:e,l=r-s,p=r+s;if(a>=l&&a<=p)return{pass:!0,message:()=>{let f=r.toFixed(4),w=a.toFixed(4),B=c.toFixed(4);if(e===0)return`Element aspect ratio matches the expected value exactly, with ${a.toFixed(4)} actual and ${r.toFixed(4)} expected.`;let $=o==="percent"?"%":"px";return`Element aspect ratio is within ${e}${$} of the expected value, with ${w} actual, ${f} expected, and an offset of ${B}.`}};let m=r.toFixed(4),d=a.toFixed(4),h=c.toFixed(4),x=s.toFixed(4);return{pass:!1,message:()=>{let f=o==="percent"?"%":"px";return`Element's aspect ratio is outside the allowed ${e}${f} range. Details: - Expected ratio: ~${m} - Actual ratio: ${d} - Difference: ${h} (allowed: \xB1${x}) To fix this, adjust the element's width or height so that its ratio more closely matches the expected ${m}.`}}}function M(n){return{top:n.y,bottom:n.y+n.height,left:n.x,right:n.x+n.width}}function K(n,r,t){let e=M(r),o=M(t);switch(n){case"top":return Math.abs(e.top-o.bottom);case"right":return Math.abs(e.left-o.right);case"bottom":return Math.abs(e.bottom-o.top);case"left":return Math.abs(e.right-o.left);default:throw new Error(`Unknown side: ${n}`)}}var H=(o=>(o.Top="top",o.Right="right",o.Bottom="bottom",o.Left="left",o))(H||{});async function Q(n,r,t,e,o={}){let{tolerance:i=0,toleranceUnit:a="percent"}=o;if(i<0)throw new Error('"tolerance" must be greater than or equal to 0');let c=await u(n),s=await u(r),l=K(t,c,s),p=a==="percent"?e*i/100:i;return l<=e+p&&l>=e-p?{pass:!0,message:()=>{if(i===0)return`Element is exactly ${t}-aligned at ${e}px.`;let m=a==="percent"?"%":"px";return`Element is ${t}-aligned within the tolerance of \xB1${i}${m} from the expected ${e}px.`}}:{pass:!1,message:()=>{let m=a==="percent"?"%":"px",d=l.toFixed(2),h=p.toFixed(2),x=(l-e).toFixed(2),f={top:"bottom",right:"left",bottom:"top",left:"right"}[t];return`Element is not ${t}-aligned within the allowed tolerance of \xB1${i}${m} from the expected ${e}px. Details: - Expected distance: ${e}px - Actual distance: ${d}px - Allowed deviation: \xB1${h}px (\xB1${i}${m}) - Difference: ${x}px To fix this, adjust the ${t} position of the element (or the ${f} of the reference) using margin, padding, or layout properties so the gap is closer to ${e}px.`}}}async function Z(n,r,t={}){let{tolerance:e=0,toleranceUnit:o="percent"}=t;if(e<0)throw new Error('"tolerance" must be greater than or equal to 0');let i=await u(n),a=await u(r),c=o==="percent"?e/100*a.height:e,s=i.y-(a.y+a.height);return s>=-c?{pass:!0,message:()=>{if(c===0)return"Element is strictly below the reference.";let l=o==="percent"?"%":"px";return`Element is below the reference within ${e}${l} tolerance.`}}:{pass:!1,message:()=>{let l=c.toFixed(2),p=s.toFixed(2),m=o==="percent"?"%":"px";return`Element is not below the reference. Details: - Allowed deviation: \u2264 ${l}px (${e}${m}) - Actual deviation: ${p}px To fix this, move the element downward or increase the tolerance.`}}}function T(n){return Math.max(0,Math.min(255,n)).toString(16).padStart(2,"0").toUpperCase()}function v(n){let r=parseInt(n,10);if(isNaN(r)||r<0||r>255)throw new Error(`Invalid alpha value: ${n}. Expected a number between 0 and 255.`);return r}function b(n){let{r,g:t,b:e}=n,o=T(r),i=T(t),a=T(e);return`#${o}${i}${a}`}function F(n){let r=n.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);if(!r||r.length<4)throw new Error(`Invalid color format: ${n}. Expected rgb(r, g, b).`);let[,t,e,o]=r,i=v(t),a=v(e),c=v(o);return{r:i,g:a,b:c}}async function _(n){let r=await n.evaluate(t=>{let e=(o=t)=>{if(o===null)return"rgb(255, 255, 255)";let i=getComputedStyle(o);return i.backgroundColor==="transparent"||i.backgroundColor==="rgba(0, 0, 0, 0)"&&o.parentElement?e(o.parentElement):i.backgroundColor};return e()});return F(r)}function P(n){return n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4)}function C(n){let{r,g:t,b:e}=n,o=P(r/255),i=P(t/255),a=P(e/255);return .2126*o+.7152*i+.0722*a}function ee(n,r){let t=Math.max(n,r),e=Math.min(n,r);return(t+.05)/(e+.05)}async function te(n,r){let t=await n.evaluate(s=>getComputedStyle(s)),e=F(t.color),o=C(e),i=await _(n),a=C(i),c=ee(o,a);return c>=r?{pass:!0,message:()=>`Element color contrast is good, with a ratio of ${c.toFixed(2)}.`}:{pass:!1,message:()=>{let s=b(e),l=b(i),p=c.toFixed(2),m=r.toFixed(0),d=o.toFixed(4),h=a.toFixed(4);return`Element does not have sufficient color contrast. Details: - Actual contrast ratio: ${p} - Required contrast ratio: ${m}:1 - Text color: ${s} (Luminance: ${d}) - Background color: ${l} (Luminance: ${h}) Adjust the text or background color to achieve a contrast ratio of at least ${m}:1.`}}}async function oe(n,r){let t=await u(n),e=await u(r);return t.x===e.x&&t.y===e.y&&t.width===e.width&&t.height===e.height?{pass:!0,message:()=>"Element fits exactly within container."}:{pass:!1,message:()=>{let o=t.x-e.x,i=t.y-e.y,a=t.width-e.width,c=t.height-e.height,s=o.toFixed(2),l=i.toFixed(2),p=a.toFixed(2),m=c.toFixed(2);return`Element does not fit exactly within the container. Details: - Position delta: x = ${s}px, y = ${l}px - Size delta: width = ${p}px, height = ${m}px Please ensure the element's position and size exactly match the container's.`}}}function D(n){return{x:n.x+n.width/2,y:n.y+n.height/2}}function z(n,r,t){return Math.abs(n-r)<=t}async function ne(n,r,t={}){let{tolerance:e=0,toleranceUnit:o="percent"}=t;if(e<0)throw new Error('"tolerance" must be greater than or equal to 0');let i=await u(n),a=await u(r),c=o==="percent"?a.width*e/100:e,s=o==="percent"?a.height*e/100:e,l=D(i),p=D(a),m=z(p.x,l.x,c),d=z(p.y,l.y,s);if(m&&d)return{pass:!0,message:()=>{if(e===0)return"Element is perfectly centered.";let f=o==="percent"?"%":"px";return`Element is perfectly centered with a tolerance of ${e}${f}.`}};let h=Math.abs(p.x-l.x),x=Math.abs(p.y-l.y);return{pass:!1,message:()=>{let f=o==="percent"?"%":"px";return`Element is not fully centered within the container (allowed tolerance: \xB1${e}${f}). Details: - Horizontal: ${h.toFixed(2)}px (tolerance: \xB1${c.toFixed(2)}px) - Vertical: ${x.toFixed(2)}px (tolerance: \xB1${s.toFixed(2)}px) Adjust the element position to bring it closer to the container's center.`}}}function re(n,r,t){switch(n){case"left":return Math.abs(r.x-t.x);case"right":{let e=r.x+r.width,o=t.x+t.width;return Math.abs(e-o)}case"center":{let e=t.x+t.width/2,o=r.x+r.width/2;return Math.abs(o-e)}default:throw new Error(`Unknown horizontal alignment: ${n}`)}}var S=(e=>(e.Center="center",e.Left="left",e.Right="right",e))(S||{});async function ie(n,r,t,e={}){let{tolerance:o=0,toleranceUnit:i="percent"}=e;if(o<0)throw new Error('"tolerance" must be greater than or equal to 0');let a=await u(n),c=await u(r),s=re(t,a,c),l=i==="percent"?c.width*o/100:o;return s<=l?{pass:!0,message:()=>{if(o===0)return`Element is perfectly ${t}-aligned.`;let p=i==="percent"?"%":"px";return`Element is properly ${t}-aligned within the allowed tolerance (${o}${p}).`}}:{pass:!1,message:()=>{let p=i==="percent"?"%":"px",m=l.toFixed(2),d=s.toFixed(2);return`Element is not ${t}-aligned within the allowed tolerance of ${o}${p}. Details: - Allowed delta: \xB1${m}px - Actual delta: ${d}px Adjust the element's horizontal position to reduce the alignment difference.`}}}function ae(n,r,t){let e=Math.max(0,r.x-n.x-t),o=Math.max(0,n.x+n.width-(r.x+r.width)-t);return e+o}function ce(n,r,t){let e=Math.max(0,r.y-n.y-t),o=Math.max(0,n.y+n.height-(r.y+r.height)-t);return e+o}async function se(n,r,t={}){let{tolerance:e=0,toleranceUnit:o="percent"}=t;if(e<0)throw new Error('"tolerance" must be greater than or equal to 0');let i=await u(n),a=await u(r),c=o==="percent"?a.width*e/100:e,s=o==="percent"?a.height*e/100:e,l=ae(i,a,c),p=ce(i,a,s);return l===0&&p===0?{pass:!0,message:()=>{if(e===0)return"Element is properly inside the container.";let m=o==="percent"?"%":"px";return`Element is properly inside the container with a tolerance of ${e}${m}.`}}:{pass:!1,message:()=>{let m=o==="percent"?"%":"px",d=l.toFixed(2),h=p.toFixed(2),x=c.toFixed(2),f=s.toFixed(2);return`Element is not fully inside the container within the allowed tolerance of ${o==="percent"?`${e}${m}`:`${e.toFixed(2)}${m}`}. Details: - Horizontal overflow: ${d}px (allowed: \xB1${x}px) - Vertical overflow: ${h}px (allowed: \xB1${f}px) Please adjust the element's position or size to fit entirely inside the container.`}}}async function le(n,r,t={}){let{tolerance:e=0,toleranceUnit:o="percent"}=t;if(e<0)throw new Error('"tolerance" must be greater than or equal to 0');let i=await u(n),a=await u(r),c=o==="percent"?e/100*a.width:e,s=a.x-(i.x+i.width);return s>=-c?{pass:!0,message:()=>{if(c===0)return"Element is strictly to the left of the reference.";let l=o==="percent"?"%":"px";return`Element is to the left of the reference within ${e}${l} tolerance.`}}:{pass:!1,message:()=>{let l=c.toFixed(2),p=s.toFixed(2),m=o==="percent"?"%":"px";return`Element is not to the left of the reference. Details: - Allowed deviation: \u2264 ${l}px (${e}${m}) - Actual deviation: ${p}px To fix this, move the element leftward or increase the tolerance.`}}}async function pe(n,r){let t=await u(n),e=await u(r);return t.x+t.width<=e.x||e.x+e.width<=t.x||t.y+t.height<=e.y||e.y+e.height<=t.y?{pass:!0,message:()=>"Elements do not overlap within the expected layout boundaries."}:{pass:!1,message:()=>{let i=Math.max(t.x,e.x),a=Math.min(t.x+t.width,e.x+e.width),c=Math.max(0,a-i),s=Math.max(t.y,e.y),l=Math.min(t.y+t.height,e.y+e.height),p=Math.max(0,l-s),m=c*p,d=c.toFixed(2),h=p.toFixed(2);return`Elements overlap unexpectedly. Details: - Intersection area: ${m.toFixed(2)}px\xB2 - Overlap width: ${d}px - Overlap height: ${h}px Adjust the layout or positioning to ensure the elements do not overlap.`}}}async function ue(n,r,t={}){let{tolerance:e=0,toleranceUnit:o="percent"}=t;if(e<0)throw new Error('"tolerance" must be greater than or equal to 0');let i=await u(n),a=await u(r),c=o==="percent"?e/100*a.width:e,s=i.x-(a.x+a.width);return s>=-c?{pass:!0,message:()=>{if(c===0)return"Element is strictly to the right of the reference.";let l=o==="percent"?"%":"px";return`Element is to the right of the reference within ${e}${l} tolerance.`}}:{pass:!1,message:()=>{let l=c.toFixed(2),p=s.toFixed(2),m=o==="percent"?"%":"px";return`Element is not to the right of the reference. Details: - Allowed deviation: \u2264 ${l}px (${e}${m}) - Actual deviation: ${p}px To fix this, move the element rightward or increase the tolerance.`}}}async function me(n,r,t={}){let{tolerance:e=0,toleranceUnit:o="percent"}=t;if(e<0)throw new Error('"tolerance" must be greater than or equal to 0');let i=await u(n),a=await u(r),c=o==="percent"?a.width*e/100:e,s=o==="percent"?a.height*e/100:e,l=Math.abs(i.width-a.width),p=Math.abs(i.height-a.height);return l<=c&&p<=s?{pass:!0,message:()=>{if(e===0)return"Element size matches the container size exactly.";let m=o==="percent"?"%":"px";return`Element size fits the container perfectly with a tolerance of ${e}${m}.`}}:{pass:!1,message:()=>{let m=o==="percent"?"%":"px";return`Element size differs from container size beyond the allowed tolerance of ${e}${m}. Details: - Width: expected ${a.width.toFixed(2)}px \xB1${c.toFixed(2)}px, got ${i.width.toFixed(2)}px (delta: ${l.toFixed(2)}px) - Height: expected ${a.height.toFixed(2)}px \xB1${s.toFixed(2)}px, got ${i.height.toFixed(2)}px (delta: ${p.toFixed(2)}px) Please adjust the element's size to match the container.`}}}function de(n,r,t){let e=t==="horizontal"?n.x:n.y,o=e+(t==="horizontal"?n.width:n.height),i=t==="horizontal"?r.x:r.y,a=i+(t==="horizontal"?r.width:r.height),c=o<i?i-o:e-a;return Math.max(0,c)}var W=(t=>(t.Horizontal="horizontal",t.Vertical="vertical",t))(W||{});async function he(n,r,t,e,o={}){let{tolerance:i=0,toleranceUnit:a="percent"}=o;if(i<0)throw new Error('"tolerance" must be greater than or equal to 0');let c=await u(n),s=await u(r),l=de(c,s,e),p=a==="percent"?t*i/100:i,m=Math.abs(l-t);return m<=p?{pass:!0,message:()=>{let d=l.toFixed(2),h=p.toFixed(2);if(i===0)return`Element spacing on the ${e} axis is exactly ${d}px as expected.`;let x=a==="percent"?"%":"px";return`Element spacing on the ${e} axis is ${d}px, within \xB1${i}${x} (\xB1${h}px) of the expected ${t}px.`}}:{pass:!1,message:()=>{let d=a==="percent"?"%":"px",h=t.toFixed(2),x=l.toFixed(2),f=m.toFixed(2);if(e==="horizontal"){let O=c.x<s.x?c:s,A=c.x<s.x?s:c,I=O.x.toFixed(2),k=O.width.toFixed(2),j=A.x.toFixed(2),X=A.width.toFixed(2);return`Horizontal spacing between elements does not match expected value. Details: - Expected spacing: ${h}px \xB1${i}${d} - Measured spacing: ${x}px - Difference: ${f}px - Left element: X=${I}, width=${k}px - Right element: X=${j}, width=${X}px - Gap between: ${x}px Use margin, padding, or flex/grid gap to adjust spacing.`}let w=c.y<s.y?c:s,B=c.y<s.y?s:c,$=w.y.toFixed(2),V=w.height.toFixed(2),G=B.y.toFixed(2),q=B.height.toFixed(2);return`Vertical spacing between elements does not match expected value. Details: - Expected spacing: ${h}px \xB1${i}${d} - Measured spacing: ${x}px - Difference: ${f}px - Top element: Y=${$}, height=${V}px - Bottom element: Y=${G}, height=${q}px - Gap between: ${x}px Use margin, padding, or flex/grid gap to adjust spacing.`}}}function xe(n,r,t){switch(n){case"top":return Math.abs(r.y-t.y);case"bottom":{let e=r.y+r.height,o=t.y+t.height;return Math.abs(e-o)}case"center":{let e=t.y+t.height/2,o=r.y+r.height/2;return Math.abs(o-e)}default:throw new Error(`Unknown vertical alignment: ${n}`)}}var U=(e=>(e.Bottom="bottom",e.Center="center",e.Top="top",e))(U||{});async function fe(n,r,t,e={}){let{tolerance:o=0,toleranceUnit:i="percent"}=e;if(o<0)throw new Error('"tolerance" must be greater than or equal to 0');let a=await u(n),c=await u(r),s=xe(t,a,c),l=i==="percent"?c.height*o/100:o;return s<=l?{pass:!0,message:()=>{if(o===0)return`Element is properly ${t} aligned.`;let p=i==="percent"?"%":"px";return`Element is properly ${t} aligned with a tolerance of ${o}${p}.`}}:{pass:!1,message:()=>{let p=i==="percent"?"%":"px",m=l.toFixed(2),d=s.toFixed(2);return`Element is not ${t}-aligned within the allowed tolerance of ${o}${p}. Details: - Allowed delta: \xB1${m}px - Actual delta: ${d}px Please adjust the element's vertical position to reduce the alignment difference.`}}}async function ge(n,r={}){let{marginPixel:t=0}=r;if(t<0)throw new Error("marginPixel must be greater than or equal to 0");let o=n.page().viewportSize();if(!o)return{pass:!1,message:()=>"Viewport size is not available. Ensure the page is fully loaded and has a defined viewport."};let i=await u(n),{x:a,y:c,width:s,height:l}=i,{width:p,height:m}=o,d=p-t,h=m-t;return a>=t&&c>=t&&a+s<=d&&c+l<=h?{pass:!0,message:()=>t?`Element is fully visible in the viewport, including a ${t}px margin.`:"Element is fully visible in the viewport."}:{pass:!1,message:()=>{let x=Math.max(0,t-a),f=Math.max(0,t-c),w=Math.max(0,a+s-d),B=Math.max(0,c+l-h);return`Element is not fully visible in the viewport${t>0?` (required ${t}px margin)`:""}. Details: - Viewport size: ${p}\xD7${m}px - Required safe area: [${t}, ${t}] \u2192 [${d.toFixed(2)}, ${h.toFixed(2)}] - Element bounds: x=${a}, y=${c}, width=${s}, height=${l} - Overflow: left=${x.toFixed(2)}px, top=${f.toFixed(2)}px, right=${w.toFixed(2)}px, bottom=${B.toFixed(2)}px Scroll the page or adjust layout to bring the element fully into view.`}}}export{R as Alignment,E as Axis,H as DistanceSide,S as HorizontalAlignment,W as SpacingAxis,g as ToleranceUnit,U as VerticalAlignment,Y as toBeAbove,N as toBeAlignedWith,Z as toBeBelow,ne as toBeFullyCentered,ie as toBeHorizontallyAlignedWith,se as toBeInside,le as toBeLeftOf,ue as toBeRightOf,fe as toBeVerticallyAlignedWith,ge as toBeWithinViewport,oe as toFitContainer,J as toHaveAspectRatio,te as toHaveColorContrast,Q as toHaveDistanceFrom,me as toHaveSameSizeAs,he as toHaveSpacingBetween,pe as toNotOverlapWith};