Skip to content

Commit e638537

Browse files
Added Visual Accessibility PErformance and MObile Emulation to the library
1 parent 84eb911 commit e638537

File tree

8 files changed

+2886
-281
lines changed

8 files changed

+2886
-281
lines changed

README.md

Lines changed: 792 additions & 281 deletions
Large diffs are not rendered by default.

src/accessibility/accessibility-helper.ts

Lines changed: 474 additions & 0 deletions
Large diffs are not rendered by default.

src/mobile/mobile-helper.ts

Lines changed: 409 additions & 0 deletions
Large diffs are not rendered by default.

src/performance/performance-helper.ts

Lines changed: 430 additions & 0 deletions
Large diffs are not rendered by default.

src/visual/visual-testing.ts

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
import { Page, Locator, expect } from '@playwright/test';
2+
import { logger } from '../utils/logger';
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
6+
export interface VisualCompareOptions {
7+
maxDiffPixels?: number;
8+
maxDiffPixelRatio?: number;
9+
threshold?: number;
10+
animations?: 'disabled' | 'allow';
11+
mask?: Locator[];
12+
fullPage?: boolean;
13+
}
14+
15+
export interface VisualTestResult {
16+
name: string;
17+
passed: boolean;
18+
diffPixels?: number;
19+
diffRatio?: number;
20+
actualPath?: string;
21+
expectedPath?: string;
22+
diffPath?: string;
23+
}
24+
25+
/**
26+
* Visual regression testing helper
27+
*/
28+
export class VisualTesting {
29+
private snapshotDir: string;
30+
private results: VisualTestResult[] = [];
31+
32+
constructor(private page: Page) {
33+
this.snapshotDir = path.join(process.cwd(), 'test-results', 'screenshots', 'snapshots');
34+
this.ensureSnapshotDir();
35+
}
36+
37+
/**
38+
* Ensure snapshot directory exists
39+
*/
40+
private ensureSnapshotDir(): void {
41+
if (!fs.existsSync(this.snapshotDir)) {
42+
fs.mkdirSync(this.snapshotDir, { recursive: true });
43+
}
44+
}
45+
46+
/**
47+
* Compare full page screenshot with baseline
48+
*/
49+
async compareFullPage(name: string, options?: VisualCompareOptions): Promise<void> {
50+
logger.info('Comparing full page screenshot', { name });
51+
52+
try {
53+
await expect(this.page).toHaveScreenshot(`${name}.png`, {
54+
maxDiffPixels: options?.maxDiffPixels || 100,
55+
maxDiffPixelRatio: options?.maxDiffPixelRatio,
56+
threshold: options?.threshold || 0.2,
57+
animations: options?.animations || 'disabled',
58+
mask: options?.mask,
59+
fullPage: options?.fullPage !== false
60+
});
61+
62+
this.results.push({
63+
name,
64+
passed: true
65+
});
66+
67+
logger.info('Visual comparison passed', { name });
68+
} catch (error: any) {
69+
logger.error('Visual comparison failed', {
70+
name,
71+
error: error.message
72+
});
73+
74+
this.results.push({
75+
name,
76+
passed: false,
77+
diffPixels: error.matcherResult?.diffPixels,
78+
diffRatio: error.matcherResult?.diffRatio
79+
});
80+
81+
throw error;
82+
}
83+
}
84+
85+
/**
86+
* Compare specific element screenshot
87+
*/
88+
async compareElement(selector: string, name: string, options?: VisualCompareOptions): Promise<void> {
89+
logger.info('Comparing element screenshot', { selector, name });
90+
91+
try {
92+
const element = this.page.locator(selector);
93+
await element.scrollIntoViewIfNeeded();
94+
95+
await expect(element).toHaveScreenshot(`${name}-element.png`, {
96+
maxDiffPixels: options?.maxDiffPixels || 50,
97+
maxDiffPixelRatio: options?.maxDiffPixelRatio,
98+
threshold: options?.threshold || 0.2,
99+
animations: options?.animations || 'disabled',
100+
mask: options?.mask
101+
});
102+
103+
this.results.push({
104+
name: `${name}-element`,
105+
passed: true
106+
});
107+
108+
logger.info('Element visual comparison passed', { selector, name });
109+
} catch (error: any) {
110+
logger.error('Element visual comparison failed', {
111+
selector,
112+
name,
113+
error: error.message
114+
});
115+
116+
this.results.push({
117+
name: `${name}-element`,
118+
passed: false,
119+
diffPixels: error.matcherResult?.diffPixels,
120+
diffRatio: error.matcherResult?.diffRatio
121+
});
122+
123+
throw error;
124+
}
125+
}
126+
127+
/**
128+
* Compare multiple elements
129+
*/
130+
async compareElements(selectors: string[], baseName: string, options?: VisualCompareOptions): Promise<void> {
131+
logger.info('Comparing multiple elements', { count: selectors.length, baseName });
132+
133+
for (let i = 0; i < selectors.length; i++) {
134+
const name = `${baseName}-${i + 1}`;
135+
await this.compareElement(selectors[i], name, options);
136+
}
137+
}
138+
139+
/**
140+
* Compare viewport screenshot (visible area only)
141+
*/
142+
async compareViewport(name: string, options?: VisualCompareOptions): Promise<void> {
143+
logger.info('Comparing viewport screenshot', { name });
144+
145+
try {
146+
await expect(this.page).toHaveScreenshot(`${name}-viewport.png`, {
147+
maxDiffPixels: options?.maxDiffPixels || 100,
148+
maxDiffPixelRatio: options?.maxDiffPixelRatio,
149+
threshold: options?.threshold || 0.2,
150+
animations: options?.animations || 'disabled',
151+
mask: options?.mask,
152+
fullPage: false
153+
});
154+
155+
this.results.push({
156+
name: `${name}-viewport`,
157+
passed: true
158+
});
159+
160+
logger.info('Viewport visual comparison passed', { name });
161+
} catch (error: any) {
162+
logger.error('Viewport visual comparison failed', {
163+
name,
164+
error: error.message
165+
});
166+
167+
this.results.push({
168+
name: `${name}-viewport`,
169+
passed: false
170+
});
171+
172+
throw error;
173+
}
174+
}
175+
176+
/**
177+
* Mask dynamic elements before comparison
178+
*/
179+
async compareWithMask(name: string, maskSelectors: string[], options?: VisualCompareOptions): Promise<void> {
180+
logger.info('Comparing with masked elements', {
181+
name,
182+
maskCount: maskSelectors.length
183+
});
184+
185+
const masks = maskSelectors.map(selector => this.page.locator(selector));
186+
187+
await this.compareFullPage(name, {
188+
...options,
189+
mask: masks
190+
});
191+
}
192+
193+
/**
194+
* Compare after hiding elements
195+
*/
196+
async compareWithHiddenElements(name: string, hideSelectors: string[], options?: VisualCompareOptions): Promise<void> {
197+
logger.info('Comparing with hidden elements', {
198+
name,
199+
hideCount: hideSelectors.length
200+
});
201+
202+
// Hide elements
203+
for (const selector of hideSelectors) {
204+
await this.page.locator(selector).evaluate((el: HTMLElement) => {
205+
el.style.visibility = 'hidden';
206+
});
207+
}
208+
209+
await this.compareFullPage(name, options);
210+
211+
// Restore elements
212+
for (const selector of hideSelectors) {
213+
await this.page.locator(selector).evaluate((el: HTMLElement) => {
214+
el.style.visibility = 'visible';
215+
});
216+
}
217+
}
218+
219+
/**
220+
* Compare at different viewport sizes
221+
*/
222+
async compareResponsive(
223+
name: string,
224+
viewports: Array<{ width: number; height: number; name: string }>,
225+
options?: VisualCompareOptions
226+
): Promise<void> {
227+
logger.info('Comparing responsive views', {
228+
name,
229+
viewportCount: viewports.length
230+
});
231+
232+
for (const viewport of viewports) {
233+
await this.page.setViewportSize({
234+
width: viewport.width,
235+
height: viewport.height
236+
});
237+
238+
// Wait for any CSS transitions
239+
await this.page.waitForTimeout(500);
240+
241+
await this.compareFullPage(`${name}-${viewport.name}`, options);
242+
}
243+
}
244+
245+
/**
246+
* Compare hover state
247+
*/
248+
async compareHoverState(selector: string, name: string, options?: VisualCompareOptions): Promise<void> {
249+
logger.info('Comparing hover state', { selector, name });
250+
251+
const element = this.page.locator(selector);
252+
await element.hover();
253+
254+
// Wait for hover animations
255+
await this.page.waitForTimeout(300);
256+
257+
await this.compareElement(selector, `${name}-hover`, options);
258+
}
259+
260+
/**
261+
* Compare focus state
262+
*/
263+
async compareFocusState(selector: string, name: string, options?: VisualCompareOptions): Promise<void> {
264+
logger.info('Comparing focus state', { selector, name });
265+
266+
const element = this.page.locator(selector);
267+
await element.focus();
268+
269+
// Wait for focus styles
270+
await this.page.waitForTimeout(200);
271+
272+
await this.compareElement(selector, `${name}-focus`, options);
273+
}
274+
275+
/**
276+
* Update baseline (accept current as new baseline)
277+
*/
278+
async updateBaseline(name: string): Promise<void> {
279+
logger.warn('Updating baseline', { name });
280+
281+
// Playwright automatically updates baselines with --update-snapshots flag
282+
// This method is for logging/documentation purposes
283+
284+
logger.info('To update baselines, run tests with --update-snapshots flag');
285+
}
286+
287+
/**
288+
* Get all test results
289+
*/
290+
getResults(): VisualTestResult[] {
291+
return this.results;
292+
}
293+
294+
/**
295+
* Get failed tests
296+
*/
297+
getFailedTests(): VisualTestResult[] {
298+
return this.results.filter(r => !r.passed);
299+
}
300+
301+
/**
302+
* Get summary
303+
*/
304+
getSummary(): { total: number; passed: number; failed: number } {
305+
const total = this.results.length;
306+
const passed = this.results.filter(r => r.passed).length;
307+
const failed = total - passed;
308+
309+
return { total, passed, failed };
310+
}
311+
312+
/**
313+
* Clear results
314+
*/
315+
clearResults(): void {
316+
this.results = [];
317+
logger.debug('Visual test results cleared');
318+
}
319+
320+
/**
321+
* Compare with custom diff threshold
322+
*/
323+
async compareWithThreshold(name: string, thresholdPercent: number, options?: VisualCompareOptions): Promise<void> {
324+
logger.info('Comparing with custom threshold', {
325+
name,
326+
threshold: thresholdPercent
327+
});
328+
329+
await this.compareFullPage(name, {
330+
...options,
331+
threshold: thresholdPercent / 100
332+
});
333+
}
334+
335+
/**
336+
* Batch compare multiple pages
337+
*/
338+
async batchCompare(
339+
pages: Array<{ url: string; name: string }>,
340+
options?: VisualCompareOptions
341+
): Promise<void> {
342+
logger.info('Batch comparing pages', { count: pages.length });
343+
344+
for (const pageInfo of pages) {
345+
await this.page.goto(pageInfo.url);
346+
await this.page.waitForLoadState('networkidle');
347+
await this.compareFullPage(pageInfo.name, options);
348+
}
349+
}
350+
}

0 commit comments

Comments
 (0)