@assertrx/dino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
# Canvas and WebGL Testing
|
|
2
|
+
|
|
3
|
+
> **When to use**: When your application renders content on `<canvas>` elements -- charts (Chart.js, D3), maps (Mapbox, Leaflet), games, image editors, WebGL visualizations, drawing tools, signature pads.
|
|
4
|
+
> **Prerequisites**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/locators.md](locators.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Screenshot comparison — the primary strategy for canvas
|
|
10
|
+
await expect(page.locator('canvas#chart')).toHaveScreenshot('revenue-chart.png');
|
|
11
|
+
|
|
12
|
+
// Click at specific coordinates on canvas
|
|
13
|
+
await page.locator('canvas').click({ position: { x: 200, y: 150 } });
|
|
14
|
+
|
|
15
|
+
// Read canvas state via page.evaluate
|
|
16
|
+
const pixelColor = await page.evaluate(() => {
|
|
17
|
+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
|
|
18
|
+
const ctx = canvas.getContext('2d')!;
|
|
19
|
+
const pixel = ctx.getImageData(100, 100, 1, 1).data;
|
|
20
|
+
return { r: pixel[0], g: pixel[1], b: pixel[2], a: pixel[3] };
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Patterns
|
|
25
|
+
|
|
26
|
+
### Screenshot Comparison (Visual Regression)
|
|
27
|
+
|
|
28
|
+
**Use when**: Verifying the visual output of canvas-rendered content -- charts, graphs, maps, drawings. This is the most reliable approach because canvas pixels are not queryable via DOM.
|
|
29
|
+
**Avoid when**: The canvas content is dynamic on every render (animations, timestamps). Use threshold or mask options.
|
|
30
|
+
|
|
31
|
+
**TypeScript**
|
|
32
|
+
```typescript
|
|
33
|
+
import { test, expect } from '@playwright/test';
|
|
34
|
+
|
|
35
|
+
test('revenue chart renders correctly', async ({ page }) => {
|
|
36
|
+
await page.goto('/dashboard');
|
|
37
|
+
|
|
38
|
+
// Wait for the chart to finish rendering
|
|
39
|
+
await expect(page.locator('canvas#revenue-chart')).toBeVisible();
|
|
40
|
+
|
|
41
|
+
// Optionally wait for a loading indicator to disappear
|
|
42
|
+
await expect(page.getByTestId('chart-loading')).toBeHidden();
|
|
43
|
+
|
|
44
|
+
// Screenshot comparison against a baseline
|
|
45
|
+
await expect(page.locator('canvas#revenue-chart')).toHaveScreenshot('revenue-chart.png', {
|
|
46
|
+
maxDiffPixelRatio: 0.01, // Allow 1% pixel difference for anti-aliasing
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('chart updates after date range change', async ({ page }) => {
|
|
51
|
+
await page.goto('/dashboard');
|
|
52
|
+
|
|
53
|
+
// Change date range
|
|
54
|
+
await page.getByRole('combobox', { name: 'Date range' }).selectOption('Last 30 days');
|
|
55
|
+
|
|
56
|
+
// Wait for chart to re-render
|
|
57
|
+
await expect(page.getByTestId('chart-loading')).toBeHidden();
|
|
58
|
+
|
|
59
|
+
// Compare against a different baseline
|
|
60
|
+
await expect(page.locator('canvas#revenue-chart')).toHaveScreenshot('revenue-chart-30d.png', {
|
|
61
|
+
maxDiffPixelRatio: 0.01,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('mask dynamic areas in canvas screenshot', async ({ page }) => {
|
|
66
|
+
await page.goto('/dashboard');
|
|
67
|
+
|
|
68
|
+
await expect(page.locator('canvas#chart')).toHaveScreenshot('chart-stable.png', {
|
|
69
|
+
// Mask the timestamp area that changes every render
|
|
70
|
+
mask: [page.locator('.chart-timestamp')],
|
|
71
|
+
maxDiffPixelRatio: 0.02,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**JavaScript**
|
|
77
|
+
```javascript
|
|
78
|
+
const { test, expect } = require('@playwright/test');
|
|
79
|
+
|
|
80
|
+
test('revenue chart renders correctly', async ({ page }) => {
|
|
81
|
+
await page.goto('/dashboard');
|
|
82
|
+
await expect(page.locator('canvas#revenue-chart')).toBeVisible();
|
|
83
|
+
await expect(page.getByTestId('chart-loading')).toBeHidden();
|
|
84
|
+
|
|
85
|
+
await expect(page.locator('canvas#revenue-chart')).toHaveScreenshot('revenue-chart.png', {
|
|
86
|
+
maxDiffPixelRatio: 0.01,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('chart updates after date range change', async ({ page }) => {
|
|
91
|
+
await page.goto('/dashboard');
|
|
92
|
+
await page.getByRole('combobox', { name: 'Date range' }).selectOption('Last 30 days');
|
|
93
|
+
await expect(page.getByTestId('chart-loading')).toBeHidden();
|
|
94
|
+
|
|
95
|
+
await expect(page.locator('canvas#revenue-chart')).toHaveScreenshot('revenue-chart-30d.png', {
|
|
96
|
+
maxDiffPixelRatio: 0.01,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Interacting with Canvas via Coordinates
|
|
102
|
+
|
|
103
|
+
**Use when**: Testing user interactions on canvas -- clicking chart data points, dragging on a drawing tool, selecting map regions.
|
|
104
|
+
**Avoid when**: The element has an accessible DOM overlay (many chart libraries render tooltips as HTML). Interact with the overlay instead.
|
|
105
|
+
|
|
106
|
+
**TypeScript**
|
|
107
|
+
```typescript
|
|
108
|
+
import { test, expect } from '@playwright/test';
|
|
109
|
+
|
|
110
|
+
test('click on chart data point shows tooltip', async ({ page }) => {
|
|
111
|
+
await page.goto('/analytics');
|
|
112
|
+
|
|
113
|
+
const canvas = page.locator('canvas#line-chart');
|
|
114
|
+
await expect(canvas).toBeVisible();
|
|
115
|
+
|
|
116
|
+
// Click at a specific coordinate on the canvas
|
|
117
|
+
await canvas.click({ position: { x: 200, y: 100 } });
|
|
118
|
+
|
|
119
|
+
// Tooltip appears as an HTML overlay (most chart libraries)
|
|
120
|
+
await expect(page.getByTestId('chart-tooltip')).toContainText('Revenue: $12,400');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('draw a line on the canvas', async ({ page }) => {
|
|
124
|
+
await page.goto('/drawing-tool');
|
|
125
|
+
|
|
126
|
+
const canvas = page.locator('canvas#drawing-area');
|
|
127
|
+
|
|
128
|
+
// Simulate a drag to draw a line
|
|
129
|
+
await canvas.hover({ position: { x: 50, y: 50 } });
|
|
130
|
+
await page.mouse.down();
|
|
131
|
+
await page.mouse.move(200, 200, { steps: 10 }); // Smooth drag
|
|
132
|
+
await page.mouse.up();
|
|
133
|
+
|
|
134
|
+
// Verify via screenshot
|
|
135
|
+
await expect(canvas).toHaveScreenshot('drawn-line.png');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('pinch-to-zoom on a map canvas', async ({ page }) => {
|
|
139
|
+
await page.goto('/map');
|
|
140
|
+
|
|
141
|
+
const canvas = page.locator('canvas#map');
|
|
142
|
+
|
|
143
|
+
// Scroll to zoom (common in map libraries)
|
|
144
|
+
await canvas.hover({ position: { x: 300, y: 300 } });
|
|
145
|
+
await page.mouse.wheel(0, -500); // Scroll up = zoom in
|
|
146
|
+
|
|
147
|
+
// Verify zoom level changed
|
|
148
|
+
await expect(page.getByTestId('zoom-level')).toHaveText('Zoom: 12');
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**JavaScript**
|
|
153
|
+
```javascript
|
|
154
|
+
const { test, expect } = require('@playwright/test');
|
|
155
|
+
|
|
156
|
+
test('click on chart data point shows tooltip', async ({ page }) => {
|
|
157
|
+
await page.goto('/analytics');
|
|
158
|
+
|
|
159
|
+
const canvas = page.locator('canvas#line-chart');
|
|
160
|
+
await expect(canvas).toBeVisible();
|
|
161
|
+
|
|
162
|
+
await canvas.click({ position: { x: 200, y: 100 } });
|
|
163
|
+
await expect(page.getByTestId('chart-tooltip')).toContainText('Revenue: $12,400');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('draw a line on the canvas', async ({ page }) => {
|
|
167
|
+
await page.goto('/drawing-tool');
|
|
168
|
+
|
|
169
|
+
const canvas = page.locator('canvas#drawing-area');
|
|
170
|
+
await canvas.hover({ position: { x: 50, y: 50 } });
|
|
171
|
+
await page.mouse.down();
|
|
172
|
+
await page.mouse.move(200, 200, { steps: 10 });
|
|
173
|
+
await page.mouse.up();
|
|
174
|
+
|
|
175
|
+
await expect(canvas).toHaveScreenshot('drawn-line.png');
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Canvas API Testing via `page.evaluate()`
|
|
180
|
+
|
|
181
|
+
**Use when**: You need to inspect canvas pixel data, read the rendering context state, or verify programmatic canvas operations.
|
|
182
|
+
**Avoid when**: A screenshot comparison is sufficient. Pixel-level assertions are brittle.
|
|
183
|
+
|
|
184
|
+
**TypeScript**
|
|
185
|
+
```typescript
|
|
186
|
+
import { test, expect } from '@playwright/test';
|
|
187
|
+
|
|
188
|
+
test('verify specific pixel color on canvas', async ({ page }) => {
|
|
189
|
+
await page.goto('/color-picker');
|
|
190
|
+
|
|
191
|
+
// Click the red swatch
|
|
192
|
+
await page.getByRole('button', { name: 'Red' }).click();
|
|
193
|
+
|
|
194
|
+
// Read the pixel color at the canvas center
|
|
195
|
+
const color = await page.evaluate(() => {
|
|
196
|
+
const canvas = document.querySelector('canvas#preview') as HTMLCanvasElement;
|
|
197
|
+
const ctx = canvas.getContext('2d')!;
|
|
198
|
+
const centerX = Math.floor(canvas.width / 2);
|
|
199
|
+
const centerY = Math.floor(canvas.height / 2);
|
|
200
|
+
const pixel = ctx.getImageData(centerX, centerY, 1, 1).data;
|
|
201
|
+
return { r: pixel[0], g: pixel[1], b: pixel[2], a: pixel[3] };
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(color.r).toBeGreaterThan(200); // Red channel high
|
|
205
|
+
expect(color.g).toBeLessThan(50); // Green channel low
|
|
206
|
+
expect(color.b).toBeLessThan(50); // Blue channel low
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('verify canvas dimensions match expected size', async ({ page }) => {
|
|
210
|
+
await page.goto('/editor');
|
|
211
|
+
|
|
212
|
+
const dimensions = await page.evaluate(() => {
|
|
213
|
+
const canvas = document.querySelector('canvas#main') as HTMLCanvasElement;
|
|
214
|
+
return { width: canvas.width, height: canvas.height };
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(dimensions).toEqual({ width: 1920, height: 1080 });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('canvas has content (is not blank)', async ({ page }) => {
|
|
221
|
+
await page.goto('/chart');
|
|
222
|
+
|
|
223
|
+
// Wait for rendering
|
|
224
|
+
await expect(page.getByTestId('chart-loading')).toBeHidden();
|
|
225
|
+
|
|
226
|
+
const isBlank = await page.evaluate(() => {
|
|
227
|
+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
|
|
228
|
+
const ctx = canvas.getContext('2d')!;
|
|
229
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
230
|
+
// Check if all pixels are transparent (blank canvas)
|
|
231
|
+
return imageData.data.every((value, index) => {
|
|
232
|
+
return index % 4 === 3 ? value === 0 : true; // Check alpha channel
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(isBlank).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**JavaScript**
|
|
241
|
+
```javascript
|
|
242
|
+
const { test, expect } = require('@playwright/test');
|
|
243
|
+
|
|
244
|
+
test('verify specific pixel color on canvas', async ({ page }) => {
|
|
245
|
+
await page.goto('/color-picker');
|
|
246
|
+
await page.getByRole('button', { name: 'Red' }).click();
|
|
247
|
+
|
|
248
|
+
const color = await page.evaluate(() => {
|
|
249
|
+
const canvas = document.querySelector('canvas#preview');
|
|
250
|
+
const ctx = canvas.getContext('2d');
|
|
251
|
+
const centerX = Math.floor(canvas.width / 2);
|
|
252
|
+
const centerY = Math.floor(canvas.height / 2);
|
|
253
|
+
const pixel = ctx.getImageData(centerX, centerY, 1, 1).data;
|
|
254
|
+
return { r: pixel[0], g: pixel[1], b: pixel[2], a: pixel[3] };
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(color.r).toBeGreaterThan(200);
|
|
258
|
+
expect(color.g).toBeLessThan(50);
|
|
259
|
+
expect(color.b).toBeLessThan(50);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('canvas has content (is not blank)', async ({ page }) => {
|
|
263
|
+
await page.goto('/chart');
|
|
264
|
+
await expect(page.getByTestId('chart-loading')).toBeHidden();
|
|
265
|
+
|
|
266
|
+
const isBlank = await page.evaluate(() => {
|
|
267
|
+
const canvas = document.querySelector('canvas');
|
|
268
|
+
const ctx = canvas.getContext('2d');
|
|
269
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
270
|
+
return imageData.data.every((value, index) => {
|
|
271
|
+
return index % 4 === 3 ? value === 0 : true;
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(isBlank).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### WebGL Rendering Verification
|
|
280
|
+
|
|
281
|
+
**Use when**: Your app uses WebGL for 3D visualizations, data plots, or games.
|
|
282
|
+
**Avoid when**: The canvas uses 2D context only.
|
|
283
|
+
|
|
284
|
+
WebGL canvas cannot be read with `getImageData` from a 2D context. Use `toDataURL()` or screenshot comparison.
|
|
285
|
+
|
|
286
|
+
**TypeScript**
|
|
287
|
+
```typescript
|
|
288
|
+
import { test, expect } from '@playwright/test';
|
|
289
|
+
|
|
290
|
+
test('WebGL scene renders a 3D model', async ({ page }) => {
|
|
291
|
+
await page.goto('/3d-viewer');
|
|
292
|
+
|
|
293
|
+
// Wait for WebGL to finish rendering
|
|
294
|
+
await page.waitForFunction(() => {
|
|
295
|
+
const canvas = document.querySelector('canvas#scene') as HTMLCanvasElement;
|
|
296
|
+
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
|
297
|
+
return gl !== null;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Give the renderer time to complete the first frame
|
|
301
|
+
await expect(page.getByTestId('render-status')).toHaveText('Ready');
|
|
302
|
+
|
|
303
|
+
// Screenshot comparison is the most reliable approach for WebGL
|
|
304
|
+
await expect(page.locator('canvas#scene')).toHaveScreenshot('3d-model.png', {
|
|
305
|
+
maxDiffPixelRatio: 0.02, // WebGL has more rendering variance
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('WebGL canvas is not blank', async ({ page }) => {
|
|
310
|
+
await page.goto('/3d-viewer');
|
|
311
|
+
await expect(page.getByTestId('render-status')).toHaveText('Ready');
|
|
312
|
+
|
|
313
|
+
// Check that the WebGL canvas has drawn something
|
|
314
|
+
const hasContent = await page.evaluate(() => {
|
|
315
|
+
const canvas = document.querySelector('canvas#scene') as HTMLCanvasElement;
|
|
316
|
+
// Convert canvas to data URL and check it's not a blank image
|
|
317
|
+
const dataUrl = canvas.toDataURL();
|
|
318
|
+
// A blank canvas produces a very short data URL
|
|
319
|
+
return dataUrl.length > 1000;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(hasContent).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('rotate 3D model and verify new angle', async ({ page }) => {
|
|
326
|
+
await page.goto('/3d-viewer');
|
|
327
|
+
await expect(page.getByTestId('render-status')).toHaveText('Ready');
|
|
328
|
+
|
|
329
|
+
// Take baseline screenshot
|
|
330
|
+
const canvas = page.locator('canvas#scene');
|
|
331
|
+
|
|
332
|
+
// Drag to rotate
|
|
333
|
+
await canvas.hover({ position: { x: 300, y: 300 } });
|
|
334
|
+
await page.mouse.down();
|
|
335
|
+
await page.mouse.move(450, 300, { steps: 20 });
|
|
336
|
+
await page.mouse.up();
|
|
337
|
+
|
|
338
|
+
// Screenshot should differ from default angle
|
|
339
|
+
await expect(canvas).toHaveScreenshot('3d-model-rotated.png', {
|
|
340
|
+
maxDiffPixelRatio: 0.02,
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**JavaScript**
|
|
346
|
+
```javascript
|
|
347
|
+
const { test, expect } = require('@playwright/test');
|
|
348
|
+
|
|
349
|
+
test('WebGL scene renders a 3D model', async ({ page }) => {
|
|
350
|
+
await page.goto('/3d-viewer');
|
|
351
|
+
|
|
352
|
+
await page.waitForFunction(() => {
|
|
353
|
+
const canvas = document.querySelector('canvas#scene');
|
|
354
|
+
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
|
355
|
+
return gl !== null;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await expect(page.getByTestId('render-status')).toHaveText('Ready');
|
|
359
|
+
await expect(page.locator('canvas#scene')).toHaveScreenshot('3d-model.png', {
|
|
360
|
+
maxDiffPixelRatio: 0.02,
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('WebGL canvas is not blank', async ({ page }) => {
|
|
365
|
+
await page.goto('/3d-viewer');
|
|
366
|
+
await expect(page.getByTestId('render-status')).toHaveText('Ready');
|
|
367
|
+
|
|
368
|
+
const hasContent = await page.evaluate(() => {
|
|
369
|
+
const canvas = document.querySelector('canvas#scene');
|
|
370
|
+
const dataUrl = canvas.toDataURL();
|
|
371
|
+
return dataUrl.length > 1000;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
expect(hasContent).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Chart Library Testing Strategies
|
|
379
|
+
|
|
380
|
+
**Use when**: Testing Chart.js, D3, Recharts, Highcharts, or similar chart libraries.
|
|
381
|
+
**Avoid when**: Charts have full HTML/SVG DOM output (D3 with SVG). Use standard locators for SVG elements.
|
|
382
|
+
|
|
383
|
+
**TypeScript**
|
|
384
|
+
```typescript
|
|
385
|
+
import { test, expect } from '@playwright/test';
|
|
386
|
+
|
|
387
|
+
test('bar chart shows correct number of bars (SVG-based chart)', async ({ page }) => {
|
|
388
|
+
await page.goto('/analytics');
|
|
389
|
+
|
|
390
|
+
// SVG-based charts (D3, Recharts) render DOM elements — use locators
|
|
391
|
+
const bars = page.locator('svg.chart rect.bar');
|
|
392
|
+
await expect(bars).toHaveCount(12); // 12 months
|
|
393
|
+
|
|
394
|
+
// Check a specific bar's aria-label or tooltip
|
|
395
|
+
await bars.nth(0).hover();
|
|
396
|
+
await expect(page.getByTestId('chart-tooltip')).toContainText('January: $8,200');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('canvas-based chart displays data (Chart.js)', async ({ page }) => {
|
|
400
|
+
await page.goto('/analytics');
|
|
401
|
+
|
|
402
|
+
// Canvas charts — no DOM elements to query, use screenshot
|
|
403
|
+
await expect(page.getByTestId('chart-loading')).toBeHidden();
|
|
404
|
+
await expect(page.locator('canvas#monthly-chart')).toHaveScreenshot('monthly-chart.png');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('chart legend toggles data series', async ({ page }) => {
|
|
408
|
+
await page.goto('/analytics');
|
|
409
|
+
|
|
410
|
+
// Click legend item (usually HTML, not canvas)
|
|
411
|
+
await page.getByRole('button', { name: 'Revenue' }).click();
|
|
412
|
+
|
|
413
|
+
// Revenue series hidden — chart should look different
|
|
414
|
+
await expect(page.locator('canvas#chart')).toHaveScreenshot('chart-no-revenue.png');
|
|
415
|
+
|
|
416
|
+
// Click again to re-enable
|
|
417
|
+
await page.getByRole('button', { name: 'Revenue' }).click();
|
|
418
|
+
await expect(page.locator('canvas#chart')).toHaveScreenshot('chart-with-revenue.png');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('export chart as image', async ({ page }) => {
|
|
422
|
+
await page.goto('/analytics');
|
|
423
|
+
|
|
424
|
+
// Many chart libraries offer "download as PNG"
|
|
425
|
+
const downloadPromise = page.waitForEvent('download');
|
|
426
|
+
await page.getByRole('button', { name: 'Export as PNG' }).click();
|
|
427
|
+
const download = await downloadPromise;
|
|
428
|
+
|
|
429
|
+
expect(download.suggestedFilename()).toMatch(/chart.*\.png$/);
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**JavaScript**
|
|
434
|
+
```javascript
|
|
435
|
+
const { test, expect } = require('@playwright/test');
|
|
436
|
+
|
|
437
|
+
test('bar chart shows correct number of bars (SVG-based)', async ({ page }) => {
|
|
438
|
+
await page.goto('/analytics');
|
|
439
|
+
|
|
440
|
+
const bars = page.locator('svg.chart rect.bar');
|
|
441
|
+
await expect(bars).toHaveCount(12);
|
|
442
|
+
|
|
443
|
+
await bars.nth(0).hover();
|
|
444
|
+
await expect(page.getByTestId('chart-tooltip')).toContainText('January: $8,200');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('canvas-based chart displays data', async ({ page }) => {
|
|
448
|
+
await page.goto('/analytics');
|
|
449
|
+
await expect(page.getByTestId('chart-loading')).toBeHidden();
|
|
450
|
+
await expect(page.locator('canvas#monthly-chart')).toHaveScreenshot('monthly-chart.png');
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Decision Guide
|
|
455
|
+
|
|
456
|
+
| Scenario | Best Approach | Why |
|
|
457
|
+
|---|---|---|
|
|
458
|
+
| Verify chart looks correct | `toHaveScreenshot()` on canvas element | Canvas pixels are not DOM; screenshot is the source of truth |
|
|
459
|
+
| Click a data point on chart | `canvas.click({ position: { x, y } })` | Canvas does not have clickable child elements |
|
|
460
|
+
| Verify canvas is not blank | `page.evaluate` + `getImageData` or `toDataURL` | Quick programmatic check without baseline image |
|
|
461
|
+
| Test SVG-based chart (D3) | Standard locators (`svg rect`, `svg path`) | SVG elements are in the DOM; use locator queries |
|
|
462
|
+
| Read specific pixel color | `page.evaluate` + `getImageData` | Direct access to pixel data |
|
|
463
|
+
| Test WebGL rendering | `toHaveScreenshot()` with higher `maxDiffPixelRatio` | WebGL has rendering variance; pixel assertions are unreliable |
|
|
464
|
+
| Test canvas drag/draw | `mouse.down()` + `mouse.move()` + `mouse.up()` | Simulates real drawing interactions |
|
|
465
|
+
| Chart tooltip after hover | `canvas.hover({ position })` then assert tooltip DOM | Tooltips are usually HTML overlays |
|
|
466
|
+
|
|
467
|
+
## Anti-Patterns
|
|
468
|
+
|
|
469
|
+
| Don't Do This | Problem | Do This Instead |
|
|
470
|
+
|---|---|---|
|
|
471
|
+
| `page.getByRole('button')` inside a canvas | Canvas content has no DOM elements | Use `canvas.click({ position })` for coordinates |
|
|
472
|
+
| Assert pixel colors with exact RGB values | Anti-aliasing and GPU differences cause 1-2 value variance | Use ranges (`toBeGreaterThan(200)`) or `toHaveScreenshot` |
|
|
473
|
+
| Skip `maxDiffPixelRatio` in canvas screenshots | Different GPUs and OS versions render slightly differently | Set `maxDiffPixelRatio: 0.01` to `0.02` |
|
|
474
|
+
| `waitForTimeout` to wait for chart render | Arbitrary; too slow or too fast | Wait for a loading indicator to disappear or use `waitForFunction` |
|
|
475
|
+
| Read WebGL pixels via 2D context `getImageData` | WebGL and 2D contexts are mutually exclusive on the same canvas | Use `canvas.toDataURL()` or screenshot comparison |
|
|
476
|
+
| Take full-page screenshots for canvas tests | Captures unrelated content; more brittle baselines | Scope screenshot to `page.locator('canvas#specific')` |
|
|
477
|
+
|
|
478
|
+
## Troubleshooting
|
|
479
|
+
|
|
480
|
+
| Symptom | Cause | Fix |
|
|
481
|
+
|---|---|---|
|
|
482
|
+
| Screenshot baseline always differs | GPU rendering differences across CI and local machine | Use Docker with consistent GPU settings, or increase `maxDiffPixelRatio` |
|
|
483
|
+
| Canvas click at coordinates hits wrong element | Coordinates are relative to element, but viewport changed | Use `position` relative to the canvas element, not the page |
|
|
484
|
+
| `getImageData` returns all transparent pixels | Canvas has not finished rendering when evaluated | Wait for a render-complete signal or use `waitForFunction` |
|
|
485
|
+
| `toDataURL` throws `SecurityError` | Canvas is tainted by cross-origin image | Serve images from the same origin or use CORS headers |
|
|
486
|
+
| WebGL context is null | Browser does not support WebGL or it is disabled in CI | Use `--enable-webgl` flag or run tests on a GPU-capable CI runner |
|
|
487
|
+
| Screenshot test passes locally, fails in CI | Different font rendering, DPI, or OS | Pin the Docker image, use `fonts` config, or increase threshold |
|
|
488
|
+
|
|
489
|
+
## Related
|
|
490
|
+
|
|
491
|
+
- [core/assertions-and-waiting.md](assertions-and-waiting.md) -- auto-retrying assertions and visual comparison options
|
|
492
|
+
- [core/configuration.md](configuration.md) -- configure screenshot thresholds and update baselines
|
|
493
|
+
- [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) -- canvas elements inside iframes
|
|
494
|
+
- [core/debugging.md](debugging.md) -- debugging visual regression failures with trace viewer
|