ui-regression-testing-3 / utils /figma_client.py
riazmo's picture
Upload 17 files
cfec14d verified
"""
Figma API Client
Handles communication with Figma API to extract design screenshots.
"""
import requests
from typing import Dict, List, Tuple, Optional
from pathlib import Path
class FigmaClient:
"""Client for interacting with Figma API."""
BASE_URL = "https://api.figma.com/v1"
def __init__(self, access_token: str):
self.access_token = access_token
self.headers = {
"X-Figma-Token": access_token
}
def get_file(self, file_key: str) -> Dict:
"""Get file metadata from Figma."""
url = f"{self.BASE_URL}/files/{file_key}"
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
def get_frame_nodes(self, file_key: str) -> List[Dict]:
"""
Get all top-level frames from the Figma file.
Returns frames that match viewport patterns (Desktop/Mobile).
"""
file_data = self.get_file(file_key)
frames = []
# Navigate through document -> pages -> frames
document = file_data.get("document", {})
for page in document.get("children", []):
if page.get("type") == "CANVAS":
for child in page.get("children", []):
if child.get("type") == "FRAME":
frame_name = child.get("name", "")
frame_id = child.get("id", "")
bounds = child.get("absoluteBoundingBox", {})
# Determine viewport type from name
viewport = None
if "desktop" in frame_name.lower() or bounds.get("width", 0) >= 1000:
viewport = "desktop"
elif "mobile" in frame_name.lower() or bounds.get("width", 0) <= 500:
viewport = "mobile"
if viewport:
frames.append({
"id": frame_id,
"name": frame_name,
"viewport": viewport,
"width": bounds.get("width", 0),
"height": bounds.get("height", 0)
})
return frames
def export_frame(
self,
file_key: str,
frame_id: str,
output_path: str,
scale: float = 1.0,
format: str = "png"
) -> Tuple[str, Dict[str, int]]:
"""
Export a frame as an image.
Args:
file_key: Figma file key
frame_id: Node ID of the frame to export
output_path: Where to save the image
scale: Export scale (1.0 = actual size, 0.5 = half size)
format: Image format (png, jpg, svg, pdf)
Returns:
Tuple of (saved_path, dimensions_dict)
"""
# Get export URL
url = f"{self.BASE_URL}/images/{file_key}"
params = {
"ids": frame_id,
"scale": scale,
"format": format
}
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
data = response.json()
image_url = data.get("images", {}).get(frame_id)
if not image_url:
raise ValueError(f"Could not get export URL for frame {frame_id}")
# Download the image
img_response = requests.get(image_url)
img_response.raise_for_status()
# Save to file
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "wb") as f:
f.write(img_response.content)
# Get image dimensions
from PIL import Image
with Image.open(output_path) as img:
width, height = img.size
return output_path, {"width": width, "height": height}
def export_frames_for_comparison(
self,
file_key: str,
output_dir: str,
execution_id: str
) -> Tuple[Dict[str, str], Dict[str, Dict[str, int]]]:
"""
Export all relevant frames for UI comparison.
Automatically finds Desktop and Mobile frames and exports them
at 1x scale (not 2x) for proper comparison with website screenshots.
Returns:
Tuple of (screenshot_paths, dimensions)
"""
frames = self.get_frame_nodes(file_key)
screenshots = {}
dimensions = {}
# Group by viewport, prefer larger frames
viewport_frames = {}
for frame in frames:
viewport = frame["viewport"]
if viewport not in viewport_frames:
viewport_frames[viewport] = frame
elif frame["width"] * frame["height"] > viewport_frames[viewport]["width"] * viewport_frames[viewport]["height"]:
viewport_frames[viewport] = frame
# Export each viewport
for viewport, frame in viewport_frames.items():
print(f" 📥 Exporting frame: {frame['name']} ({frame['width']}px width)")
print(f" Frame ID: {frame['id']}")
print(f" Dimensions: {frame['width']}x{frame['height']}")
output_path = f"{output_dir}/{viewport}_{execution_id}.png"
# Export at scale 1.0 (actual design size)
# Note: Figma often has designs at 2x, we'll handle normalization in comparison
saved_path, dims = self.export_frame(
file_key,
frame["id"],
output_path,
scale=1.0 # Export at 1x to get actual design dimensions
)
screenshots[viewport] = saved_path
dimensions[viewport] = dims
print(f" ✓ Exported: {saved_path}")
return screenshots, dimensions