Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
testing-patterns/canvas-webgl.md
1# Canvas & WebGL Testing23## Table of Contents451. [Canvas Basics](#canvas-basics)62. [Visual Comparison](#visual-comparison)73. [Interaction Testing](#interaction-testing)84. [WebGL Testing](#webgl-testing)95. [Chart Libraries](#chart-libraries)106. [Game & Animation Testing](#game--animation-testing)1112## Canvas Basics1314### Locating Canvas Elements1516```typescript17test("find canvas", async ({ page }) => {18await page.goto("/canvas-app");1920// By tag21const canvas = page.locator("canvas");2223// By ID or class24const gameCanvas = page.locator("canvas#game");25const chartCanvas = page.locator("canvas.chart-canvas");2627// Verify canvas is present and visible28await expect(canvas).toBeVisible();2930// Get canvas dimensions31const box = await canvas.boundingBox();32console.log(`Canvas size: ${box?.width}x${box?.height}`);33});34```3536### Canvas Screenshot Testing3738```typescript39test("canvas renders correctly", async ({ page }) => {40await page.goto("/chart");4142// Wait for canvas to be ready (check for specific content)43await page.waitForFunction(() => {44const canvas = document.querySelector("canvas");45const ctx = canvas?.getContext("2d");46// Check if canvas has been drawn to47return ctx && !isCanvasBlank(canvas);4849function isCanvasBlank(canvas) {50const ctx = canvas.getContext("2d");51const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;52return !data.some((channel) => channel !== 0);53}54});5556// Screenshot just the canvas57const canvas = page.locator("canvas");58await expect(canvas).toHaveScreenshot("chart.png");59});60```6162### Extracting Canvas Data6364```typescript65test("verify canvas content", async ({ page }) => {66await page.goto("/drawing-app");6768// Get canvas image data69const imageData = await page.evaluate(() => {70const canvas = document.querySelector("canvas") as HTMLCanvasElement;71return canvas.toDataURL("image/png");72});7374// Verify it's not empty75expect(imageData).toMatch(/^data:image\/png;base64,.+/);7677// Get pixel data at specific location78const pixelColor = await page.evaluate(() => {79const canvas = document.querySelector("canvas") as HTMLCanvasElement;80const ctx = canvas.getContext("2d")!;81const pixel = ctx.getImageData(100, 100, 1, 1).data;82return { r: pixel[0], g: pixel[1], b: pixel[2], a: pixel[3] };83});8485// Verify specific pixel color86expect(pixelColor.r).toBeGreaterThan(200); // Expecting red-ish87});88```8990## Visual Comparison9192### Screenshot Assertions9394```typescript95test("chart matches baseline", async ({ page }) => {96await page.goto("/dashboard");9798// Wait for chart animation to complete99await page.waitForTimeout(1000); // Or better: wait for specific state100101// Full page screenshot102await expect(page).toHaveScreenshot("dashboard.png", {103maxDiffPixels: 100, // Allow small differences104});105106// Just the canvas107const chart = page.locator("canvas#sales-chart");108await expect(chart).toHaveScreenshot("sales-chart.png", {109maxDiffPixelRatio: 0.01, // 1% difference allowed110});111});112```113114### Handling Animation115116```typescript117test("animated canvas", async ({ page }) => {118await page.goto("/animated-chart");119120// Pause animation before screenshot121await page.evaluate(() => {122// Common pattern: chart libraries expose pause method123window.chartInstance?.stop?.();124125// Or override requestAnimationFrame126window.requestAnimationFrame = () => 0;127});128129await expect(page.locator("canvas")).toHaveScreenshot();130});131132test("wait for animation complete", async ({ page }) => {133await page.goto("/chart-with-animation");134135// Wait for animation complete event136await page.evaluate(() => {137return new Promise<void>((resolve) => {138if (window.chart?.isAnimating === false) {139resolve();140} else {141window.chart?.on("animationComplete", resolve);142}143});144});145146await expect(page.locator("canvas")).toHaveScreenshot();147});148```149150### Threshold Configuration151152```typescript153// playwright.config.ts154export default defineConfig({155expect: {156toHaveScreenshot: {157// Increased threshold for canvas (anti-aliasing differences)158maxDiffPixelRatio: 0.02,159threshold: 0.3, // Per-pixel color threshold160animations: "disabled",161},162},163});164```165166## Interaction Testing167168### Click on Canvas169170```typescript171test("click on canvas element", async ({ page }) => {172await page.goto("/interactive-map");173174const canvas = page.locator("canvas");175176// Click at specific coordinates177await canvas.click({ position: { x: 150, y: 200 } });178179// Verify click was registered180await expect(page.locator("#info-panel")).toContainText("Location: Paris");181});182```183184### Drawing on Canvas185186```typescript187test("draw on canvas", async ({ page }) => {188await page.goto("/whiteboard");189190const canvas = page.locator("canvas");191const box = await canvas.boundingBox();192193// Draw a line using mouse194await page.mouse.move(box!.x + 50, box!.y + 50);195await page.mouse.down();196await page.mouse.move(box!.x + 200, box!.y + 200, { steps: 10 });197await page.mouse.up();198199// Verify something was drawn200const hasDrawing = await page.evaluate(() => {201const canvas = document.querySelector("canvas") as HTMLCanvasElement;202const ctx = canvas.getContext("2d")!;203const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;204return data.some((v, i) => i % 4 !== 3 && v !== 255); // Non-white pixels205});206207expect(hasDrawing).toBe(true);208});209```210211### Drag and Drop212213```typescript214test("drag canvas element", async ({ page }) => {215await page.goto("/diagram-editor");216217const canvas = page.locator("canvas");218const box = await canvas.boundingBox();219220// Drag shape from position A to B221await page.mouse.move(box!.x + 100, box!.y + 100);222await page.mouse.down();223await page.mouse.move(box!.x + 300, box!.y + 200, { steps: 20 });224await page.mouse.up();225226// Verify via screenshot or state check227await expect(canvas).toHaveScreenshot("shape-moved.png");228});229```230231### Touch Gestures on Canvas232233```typescript234test("pinch zoom on canvas", async ({ page }) => {235await page.goto("/map");236237const canvas = page.locator("canvas");238const box = await canvas.boundingBox();239const centerX = box!.x + box!.width / 2;240const centerY = box!.y + box!.height / 2;241242// Simulate pinch zoom using two touch points243await page.touchscreen.tap(centerX, centerY);244245// Use evaluate for complex gestures246await page.evaluate(247async ({ x, y }) => {248const target = document.querySelector("canvas")!;249250// Simulate pinch start251const touch1 = new Touch({252identifier: 1,253target,254clientX: x - 50,255clientY: y,256});257const touch2 = new Touch({258identifier: 2,259target,260clientX: x + 50,261clientY: y,262});263264target.dispatchEvent(265new TouchEvent("touchstart", {266touches: [touch1, touch2],267targetTouches: [touch1, touch2],268bubbles: true,269}),270);271272// Simulate pinch out273const touch1End = new Touch({274identifier: 1,275target,276clientX: x - 100,277clientY: y,278});279const touch2End = new Touch({280identifier: 2,281target,282clientX: x + 100,283clientY: y,284});285286target.dispatchEvent(287new TouchEvent("touchmove", {288touches: [touch1End, touch2End],289targetTouches: [touch1End, touch2End],290bubbles: true,291}),292);293294target.dispatchEvent(new TouchEvent("touchend", { bubbles: true }));295},296{ x: centerX, y: centerY },297);298299// Verify zoom level changed300const zoomLevel = await page.locator("#zoom-indicator").textContent();301expect(parseFloat(zoomLevel!)).toBeGreaterThan(1);302});303```304305## WebGL Testing306307### Checking WebGL Support308309```typescript310test("WebGL is supported", async ({ page }) => {311await page.goto("/3d-viewer");312313const hasWebGL = await page.evaluate(() => {314const canvas = document.createElement("canvas");315const gl =316canvas.getContext("webgl") || canvas.getContext("experimental-webgl");317return !!gl;318});319320expect(hasWebGL).toBe(true);321});322```323324### WebGL Screenshot Testing325326```typescript327test("3D scene renders", async ({ page }) => {328await page.goto("/3d-model-viewer");329330// Wait for WebGL scene to render331await page.waitForFunction(() => {332const canvas = document.querySelector("canvas");333if (!canvas) return false;334335const gl = canvas.getContext("webgl") || canvas.getContext("webgl2");336if (!gl) return false;337338// Check if something has been drawn339const pixels = new Uint8Array(4);340gl.readPixels(341canvas.width / 2,342canvas.height / 2,3431,3441,345gl.RGBA,346gl.UNSIGNED_BYTE,347pixels,348);349return pixels.some((p) => p > 0);350});351352// Screenshot comparison (higher threshold for WebGL)353await expect(page.locator("canvas")).toHaveScreenshot("3d-scene.png", {354maxDiffPixelRatio: 0.05, // WebGL can have more variation355});356});357```358359### Testing Three.js Applications360361```typescript362test("Three.js scene interaction", async ({ page }) => {363await page.goto("/three-demo");364365// Wait for scene to be ready366await page.waitForFunction(() => window.scene?.children?.length > 0);367368// Interact with scene (orbit controls)369const canvas = page.locator("canvas");370const box = await canvas.boundingBox();371372// Rotate camera by dragging373await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);374await page.mouse.down();375await page.mouse.move(376box!.x + box!.width / 2 + 100,377box!.y + box!.height / 2,378{379steps: 10,380},381);382await page.mouse.up();383384// Verify camera position changed385const cameraRotation = await page.evaluate(() => {386return window.camera?.rotation?.y;387});388389expect(cameraRotation).not.toBe(0);390});391```392393## Chart Libraries394395### Chart.js Testing396397```typescript398test("Chart.js renders data", async ({ page }) => {399await page.goto("/chartjs-demo");400401// Wait for Chart.js to initialize402await page.waitForFunction(() => {403return window.Chart && document.querySelector("canvas")?.__chart__;404});405406// Get chart data via Chart.js API407const chartData = await page.evaluate(() => {408const canvas = document.querySelector("canvas") as any;409const chart = canvas.__chart__;410return chart.data.datasets[0].data;411});412413expect(chartData).toEqual([12, 19, 3, 5, 2, 3]);414415// Screenshot test416await expect(page.locator("canvas")).toHaveScreenshot();417});418```419420### D3.js / ECharts Testing421422```typescript423test("chart library interaction", async ({ page }) => {424await page.goto("/chart-demo");425426// Wait for chart to render427await page.waitForFunction(() => document.querySelector("canvas, svg.chart"));428429// For SVG charts (D3)430const bars = page.locator("svg.chart rect.bar");431if ((await bars.count()) > 0) {432await bars.first().hover();433await expect(page.locator(".tooltip")).toBeVisible();434}435436// For canvas charts (ECharts, Chart.js)437const canvas = page.locator("canvas");438await canvas.click({ position: { x: 200, y: 150 } });439});440```441442## Game & Animation Testing443444### Frame-by-Frame Testing445446```typescript447test("game frame control", async ({ page }) => {448await page.goto("/game");449450// Pause and step through frames451await page.evaluate(() => window.gameLoop?.pause());452await page.evaluate(() => window.gameLoop?.tick());453await expect(page.locator("canvas")).toHaveScreenshot("frame-1.png");454455for (let i = 0; i < 10; i++) {456await page.evaluate(() => window.gameLoop?.tick());457}458await expect(page.locator("canvas")).toHaveScreenshot("frame-11.png");459});460```461462### Testing Game State463464```typescript465test("game state changes", async ({ page }) => {466await page.goto("/game");467468const initialScore = await page.evaluate(() => window.game?.score);469expect(initialScore).toBe(0);470471await page.keyboard.press("Space"); // Action472await page.waitForTimeout(500);473474const newScore = await page.evaluate(() => window.game?.score);475expect(newScore).toBeGreaterThan(0);476});477```478479## Anti-Patterns to Avoid480481| Anti-Pattern | Problem | Solution |482| ------------------------ | ------------------------ | ----------------------------------- |483| Pixel-perfect assertions | Fails across browsers/OS | Use maxDiffPixelRatio threshold |484| Not waiting for render | Blank canvas screenshots | Wait for draw completion |485| Testing raw pixel data | Brittle and slow | Use visual comparison |486| Ignoring animation | Flaky screenshots | Pause/disable animations |487| Hardcoded coordinates | Breaks on resize | Calculate relative to canvas bounds |488489## Related References490491- **Visual Testing**: See [test-suite-structure.md](../core/test-suite-structure.md) for visual regression setup492- **Mobile Gestures**: See [mobile-testing.md](../advanced/mobile-testing.md) for touch interactions493- **Performance**: See [performance-testing.md](performance-testing.md) for FPS monitoring494