SurfSense/.cursor/skills/playwright-testing/testing-patterns/canvas-webgl.md
2026-05-10 04:19:55 +05:30

13 KiB

Canvas & WebGL Testing

Table of Contents

  1. Canvas Basics
  2. Visual Comparison
  3. Interaction Testing
  4. WebGL Testing
  5. Chart Libraries
  6. Game & Animation Testing

Canvas Basics

Locating Canvas Elements

test("find canvas", async ({ page }) => {
  await page.goto("/canvas-app");

  // By tag
  const canvas = page.locator("canvas");

  // By ID or class
  const gameCanvas = page.locator("canvas#game");
  const chartCanvas = page.locator("canvas.chart-canvas");

  // Verify canvas is present and visible
  await expect(canvas).toBeVisible();

  // Get canvas dimensions
  const box = await canvas.boundingBox();
  console.log(`Canvas size: ${box?.width}x${box?.height}`);
});

Canvas Screenshot Testing

test("canvas renders correctly", async ({ page }) => {
  await page.goto("/chart");

  // Wait for canvas to be ready (check for specific content)
  await page.waitForFunction(() => {
    const canvas = document.querySelector("canvas");
    const ctx = canvas?.getContext("2d");
    // Check if canvas has been drawn to
    return ctx && !isCanvasBlank(canvas);

    function isCanvasBlank(canvas) {
      const ctx = canvas.getContext("2d");
      const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
      return !data.some((channel) => channel !== 0);
    }
  });

  // Screenshot just the canvas
  const canvas = page.locator("canvas");
  await expect(canvas).toHaveScreenshot("chart.png");
});

Extracting Canvas Data

test("verify canvas content", async ({ page }) => {
  await page.goto("/drawing-app");

  // Get canvas image data
  const imageData = await page.evaluate(() => {
    const canvas = document.querySelector("canvas") as HTMLCanvasElement;
    return canvas.toDataURL("image/png");
  });

  // Verify it's not empty
  expect(imageData).toMatch(/^data:image\/png;base64,.+/);

  // Get pixel data at specific location
  const pixelColor = await page.evaluate(() => {
    const canvas = document.querySelector("canvas") as HTMLCanvasElement;
    const ctx = canvas.getContext("2d")!;
    const pixel = ctx.getImageData(100, 100, 1, 1).data;
    return { r: pixel[0], g: pixel[1], b: pixel[2], a: pixel[3] };
  });

  // Verify specific pixel color
  expect(pixelColor.r).toBeGreaterThan(200); // Expecting red-ish
});

Visual Comparison

Screenshot Assertions

test("chart matches baseline", async ({ page }) => {
  await page.goto("/dashboard");

  // Wait for chart animation to complete
  await page.waitForTimeout(1000); // Or better: wait for specific state

  // Full page screenshot
  await expect(page).toHaveScreenshot("dashboard.png", {
    maxDiffPixels: 100, // Allow small differences
  });

  // Just the canvas
  const chart = page.locator("canvas#sales-chart");
  await expect(chart).toHaveScreenshot("sales-chart.png", {
    maxDiffPixelRatio: 0.01, // 1% difference allowed
  });
});

Handling Animation

test("animated canvas", async ({ page }) => {
  await page.goto("/animated-chart");

  // Pause animation before screenshot
  await page.evaluate(() => {
    // Common pattern: chart libraries expose pause method
    window.chartInstance?.stop?.();

    // Or override requestAnimationFrame
    window.requestAnimationFrame = () => 0;
  });

  await expect(page.locator("canvas")).toHaveScreenshot();
});

test("wait for animation complete", async ({ page }) => {
  await page.goto("/chart-with-animation");

  // Wait for animation complete event
  await page.evaluate(() => {
    return new Promise<void>((resolve) => {
      if (window.chart?.isAnimating === false) {
        resolve();
      } else {
        window.chart?.on("animationComplete", resolve);
      }
    });
  });

  await expect(page.locator("canvas")).toHaveScreenshot();
});

Threshold Configuration

// playwright.config.ts
export default defineConfig({
  expect: {
    toHaveScreenshot: {
      // Increased threshold for canvas (anti-aliasing differences)
      maxDiffPixelRatio: 0.02,
      threshold: 0.3, // Per-pixel color threshold
      animations: "disabled",
    },
  },
});

Interaction Testing

Click on Canvas

test("click on canvas element", async ({ page }) => {
  await page.goto("/interactive-map");

  const canvas = page.locator("canvas");

  // Click at specific coordinates
  await canvas.click({ position: { x: 150, y: 200 } });

  // Verify click was registered
  await expect(page.locator("#info-panel")).toContainText("Location: Paris");
});

Drawing on Canvas

test("draw on canvas", async ({ page }) => {
  await page.goto("/whiteboard");

  const canvas = page.locator("canvas");
  const box = await canvas.boundingBox();

  // Draw a line using mouse
  await page.mouse.move(box!.x + 50, box!.y + 50);
  await page.mouse.down();
  await page.mouse.move(box!.x + 200, box!.y + 200, { steps: 10 });
  await page.mouse.up();

  // Verify something was drawn
  const hasDrawing = await page.evaluate(() => {
    const canvas = document.querySelector("canvas") as HTMLCanvasElement;
    const ctx = canvas.getContext("2d")!;
    const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
    return data.some((v, i) => i % 4 !== 3 && v !== 255); // Non-white pixels
  });

  expect(hasDrawing).toBe(true);
});

Drag and Drop

test("drag canvas element", async ({ page }) => {
  await page.goto("/diagram-editor");

  const canvas = page.locator("canvas");
  const box = await canvas.boundingBox();

  // Drag shape from position A to B
  await page.mouse.move(box!.x + 100, box!.y + 100);
  await page.mouse.down();
  await page.mouse.move(box!.x + 300, box!.y + 200, { steps: 20 });
  await page.mouse.up();

  // Verify via screenshot or state check
  await expect(canvas).toHaveScreenshot("shape-moved.png");
});

Touch Gestures on Canvas

test("pinch zoom on canvas", async ({ page }) => {
  await page.goto("/map");

  const canvas = page.locator("canvas");
  const box = await canvas.boundingBox();
  const centerX = box!.x + box!.width / 2;
  const centerY = box!.y + box!.height / 2;

  // Simulate pinch zoom using two touch points
  await page.touchscreen.tap(centerX, centerY);

  // Use evaluate for complex gestures
  await page.evaluate(
    async ({ x, y }) => {
      const target = document.querySelector("canvas")!;

      // Simulate pinch start
      const touch1 = new Touch({
        identifier: 1,
        target,
        clientX: x - 50,
        clientY: y,
      });
      const touch2 = new Touch({
        identifier: 2,
        target,
        clientX: x + 50,
        clientY: y,
      });

      target.dispatchEvent(
        new TouchEvent("touchstart", {
          touches: [touch1, touch2],
          targetTouches: [touch1, touch2],
          bubbles: true,
        }),
      );

      // Simulate pinch out
      const touch1End = new Touch({
        identifier: 1,
        target,
        clientX: x - 100,
        clientY: y,
      });
      const touch2End = new Touch({
        identifier: 2,
        target,
        clientX: x + 100,
        clientY: y,
      });

      target.dispatchEvent(
        new TouchEvent("touchmove", {
          touches: [touch1End, touch2End],
          targetTouches: [touch1End, touch2End],
          bubbles: true,
        }),
      );

      target.dispatchEvent(new TouchEvent("touchend", { bubbles: true }));
    },
    { x: centerX, y: centerY },
  );

  // Verify zoom level changed
  const zoomLevel = await page.locator("#zoom-indicator").textContent();
  expect(parseFloat(zoomLevel!)).toBeGreaterThan(1);
});

WebGL Testing

Checking WebGL Support

test("WebGL is supported", async ({ page }) => {
  await page.goto("/3d-viewer");

  const hasWebGL = await page.evaluate(() => {
    const canvas = document.createElement("canvas");
    const gl =
      canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
    return !!gl;
  });

  expect(hasWebGL).toBe(true);
});

WebGL Screenshot Testing

test("3D scene renders", async ({ page }) => {
  await page.goto("/3d-model-viewer");

  // Wait for WebGL scene to render
  await page.waitForFunction(() => {
    const canvas = document.querySelector("canvas");
    if (!canvas) return false;

    const gl = canvas.getContext("webgl") || canvas.getContext("webgl2");
    if (!gl) return false;

    // Check if something has been drawn
    const pixels = new Uint8Array(4);
    gl.readPixels(
      canvas.width / 2,
      canvas.height / 2,
      1,
      1,
      gl.RGBA,
      gl.UNSIGNED_BYTE,
      pixels,
    );
    return pixels.some((p) => p > 0);
  });

  // Screenshot comparison (higher threshold for WebGL)
  await expect(page.locator("canvas")).toHaveScreenshot("3d-scene.png", {
    maxDiffPixelRatio: 0.05, // WebGL can have more variation
  });
});

Testing Three.js Applications

test("Three.js scene interaction", async ({ page }) => {
  await page.goto("/three-demo");

  // Wait for scene to be ready
  await page.waitForFunction(() => window.scene?.children?.length > 0);

  // Interact with scene (orbit controls)
  const canvas = page.locator("canvas");
  const box = await canvas.boundingBox();

  // Rotate camera by dragging
  await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
  await page.mouse.down();
  await page.mouse.move(
    box!.x + box!.width / 2 + 100,
    box!.y + box!.height / 2,
    {
      steps: 10,
    },
  );
  await page.mouse.up();

  // Verify camera position changed
  const cameraRotation = await page.evaluate(() => {
    return window.camera?.rotation?.y;
  });

  expect(cameraRotation).not.toBe(0);
});

Chart Libraries

Chart.js Testing

test("Chart.js renders data", async ({ page }) => {
  await page.goto("/chartjs-demo");

  // Wait for Chart.js to initialize
  await page.waitForFunction(() => {
    return window.Chart && document.querySelector("canvas")?.__chart__;
  });

  // Get chart data via Chart.js API
  const chartData = await page.evaluate(() => {
    const canvas = document.querySelector("canvas") as any;
    const chart = canvas.__chart__;
    return chart.data.datasets[0].data;
  });

  expect(chartData).toEqual([12, 19, 3, 5, 2, 3]);

  // Screenshot test
  await expect(page.locator("canvas")).toHaveScreenshot();
});

D3.js / ECharts Testing

test("chart library interaction", async ({ page }) => {
  await page.goto("/chart-demo");

  // Wait for chart to render
  await page.waitForFunction(() => document.querySelector("canvas, svg.chart"));

  // For SVG charts (D3)
  const bars = page.locator("svg.chart rect.bar");
  if ((await bars.count()) > 0) {
    await bars.first().hover();
    await expect(page.locator(".tooltip")).toBeVisible();
  }

  // For canvas charts (ECharts, Chart.js)
  const canvas = page.locator("canvas");
  await canvas.click({ position: { x: 200, y: 150 } });
});

Game & Animation Testing

Frame-by-Frame Testing

test("game frame control", async ({ page }) => {
  await page.goto("/game");

  // Pause and step through frames
  await page.evaluate(() => window.gameLoop?.pause());
  await page.evaluate(() => window.gameLoop?.tick());
  await expect(page.locator("canvas")).toHaveScreenshot("frame-1.png");

  for (let i = 0; i < 10; i++) {
    await page.evaluate(() => window.gameLoop?.tick());
  }
  await expect(page.locator("canvas")).toHaveScreenshot("frame-11.png");
});

Testing Game State

test("game state changes", async ({ page }) => {
  await page.goto("/game");

  const initialScore = await page.evaluate(() => window.game?.score);
  expect(initialScore).toBe(0);

  await page.keyboard.press("Space"); // Action
  await page.waitForTimeout(500);

  const newScore = await page.evaluate(() => window.game?.score);
  expect(newScore).toBeGreaterThan(0);
});

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
Pixel-perfect assertions Fails across browsers/OS Use maxDiffPixelRatio threshold
Not waiting for render Blank canvas screenshots Wait for draw completion
Testing raw pixel data Brittle and slow Use visual comparison
Ignoring animation Flaky screenshots Pause/disable animations
Hardcoded coordinates Breaks on resize Calculate relative to canvas bounds