@browserflow-ai/generator 0.0.6
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/dist/config-emit.d.ts +41 -0
- package/dist/config-emit.d.ts.map +1 -0
- package/dist/config-emit.js +191 -0
- package/dist/config-emit.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/locator-emit.d.ts +76 -0
- package/dist/locator-emit.d.ts.map +1 -0
- package/dist/locator-emit.js +239 -0
- package/dist/locator-emit.js.map +1 -0
- package/dist/locator-emit.test.d.ts +6 -0
- package/dist/locator-emit.test.d.ts.map +1 -0
- package/dist/locator-emit.test.js +425 -0
- package/dist/locator-emit.test.js.map +1 -0
- package/dist/playwright-ts.d.ts +97 -0
- package/dist/playwright-ts.d.ts.map +1 -0
- package/dist/playwright-ts.js +373 -0
- package/dist/playwright-ts.js.map +1 -0
- package/dist/playwright-ts.test.d.ts +6 -0
- package/dist/playwright-ts.test.d.ts.map +1 -0
- package/dist/playwright-ts.test.js +548 -0
- package/dist/playwright-ts.test.js.map +1 -0
- package/dist/visual-checks.d.ts +76 -0
- package/dist/visual-checks.d.ts.map +1 -0
- package/dist/visual-checks.js +195 -0
- package/dist/visual-checks.js.map +1 -0
- package/dist/visual-checks.test.d.ts +6 -0
- package/dist/visual-checks.test.d.ts.map +1 -0
- package/dist/visual-checks.test.js +188 -0
- package/dist/visual-checks.test.js.map +1 -0
- package/package.json +34 -0
- package/src/config-emit.ts +253 -0
- package/src/index.ts +57 -0
- package/src/locator-emit.test.ts +533 -0
- package/src/locator-emit.ts +310 -0
- package/src/playwright-ts.test.ts +704 -0
- package/src/playwright-ts.ts +519 -0
- package/src/visual-checks.test.ts +232 -0
- package/src/visual-checks.ts +294 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* visual-checks.test.ts
|
|
3
|
+
* Tests for visual check code generation, including region mask support.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'bun:test';
|
|
7
|
+
import type { MaskRegion } from '@browserflow-ai/core';
|
|
8
|
+
import {
|
|
9
|
+
generateScreenshotAssertion,
|
|
10
|
+
generateScreenshotCapture,
|
|
11
|
+
generateMaskSetupCode,
|
|
12
|
+
} from './visual-checks.js';
|
|
13
|
+
|
|
14
|
+
describe('visual-checks', () => {
|
|
15
|
+
describe('generateScreenshotAssertion', () => {
|
|
16
|
+
it('generates basic screenshot assertion without masks', () => {
|
|
17
|
+
const result = generateScreenshotAssertion({ name: 'homepage' });
|
|
18
|
+
|
|
19
|
+
expect(result).toContain("await expect(page).toHaveScreenshot('homepage.png');");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('generates screenshot assertion with selector-based mask', () => {
|
|
23
|
+
const masks: MaskRegion[] = [
|
|
24
|
+
{ selector: '.timestamp', reason: 'Dynamic timestamp' },
|
|
25
|
+
];
|
|
26
|
+
const result = generateScreenshotAssertion({ name: 'homepage', mask: masks });
|
|
27
|
+
|
|
28
|
+
expect(result).toContain("mask: [page.locator('.timestamp')]");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('generates screenshot assertion with region-based mask', () => {
|
|
32
|
+
const masks: MaskRegion[] = [
|
|
33
|
+
{
|
|
34
|
+
region: { x: 10, y: 20, width: 100, height: 50 },
|
|
35
|
+
reason: 'Dynamic ad section',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const result = generateScreenshotAssertion({ name: 'homepage', mask: masks });
|
|
39
|
+
|
|
40
|
+
// Should generate overlay element reference
|
|
41
|
+
expect(result).toContain("mask: [page.locator('[data-bf-mask=\"0\"]')]");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('generates screenshot assertion with mixed masks', () => {
|
|
45
|
+
const masks: MaskRegion[] = [
|
|
46
|
+
{ selector: '.timestamp' },
|
|
47
|
+
{ region: { x: 10, y: 20, width: 100, height: 50 } },
|
|
48
|
+
{ selector: '.user-avatar' },
|
|
49
|
+
{ region: { x: 80, y: 5, width: 15, height: 10 } },
|
|
50
|
+
];
|
|
51
|
+
const result = generateScreenshotAssertion({ name: 'dashboard', mask: masks });
|
|
52
|
+
|
|
53
|
+
// Should include both selector and region masks
|
|
54
|
+
expect(result).toContain("page.locator('.timestamp')");
|
|
55
|
+
expect(result).toContain("page.locator('[data-bf-mask=\"0\"]')");
|
|
56
|
+
expect(result).toContain("page.locator('.user-avatar')");
|
|
57
|
+
expect(result).toContain("page.locator('[data-bf-mask=\"1\"]')");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('handles empty mask array', () => {
|
|
61
|
+
const result = generateScreenshotAssertion({ name: 'page', mask: [] });
|
|
62
|
+
|
|
63
|
+
// Should not include mask option when array is empty
|
|
64
|
+
expect(result).not.toContain('mask:');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('uses correct page variable for masks', () => {
|
|
68
|
+
const masks: MaskRegion[] = [
|
|
69
|
+
{ selector: '.dynamic-content' },
|
|
70
|
+
];
|
|
71
|
+
const result = generateScreenshotAssertion(
|
|
72
|
+
{ name: 'test', mask: masks },
|
|
73
|
+
{ pageVar: 'customPage' }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result).toContain("customPage.locator('.dynamic-content')");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('generateScreenshotCapture', () => {
|
|
81
|
+
it('generates capture with selector-based mask', () => {
|
|
82
|
+
const masks: MaskRegion[] = [
|
|
83
|
+
{ selector: '.timestamp' },
|
|
84
|
+
];
|
|
85
|
+
const result = generateScreenshotCapture({ name: 'capture', mask: masks });
|
|
86
|
+
|
|
87
|
+
expect(result).toContain("mask: [page.locator('.timestamp')]");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('generates capture with region-based mask', () => {
|
|
91
|
+
const masks: MaskRegion[] = [
|
|
92
|
+
{ region: { x: 25, y: 30, width: 40, height: 20 } },
|
|
93
|
+
];
|
|
94
|
+
const result = generateScreenshotCapture({ name: 'capture', mask: masks });
|
|
95
|
+
|
|
96
|
+
expect(result).toContain("mask: [page.locator('[data-bf-mask=\"0\"]')]");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('generates capture with mixed masks', () => {
|
|
100
|
+
const masks: MaskRegion[] = [
|
|
101
|
+
{ selector: '.header' },
|
|
102
|
+
{ region: { x: 0, y: 90, width: 100, height: 10 } },
|
|
103
|
+
];
|
|
104
|
+
const result = generateScreenshotCapture({ name: 'capture', mask: masks });
|
|
105
|
+
|
|
106
|
+
expect(result).toContain("page.locator('.header')");
|
|
107
|
+
expect(result).toContain("page.locator('[data-bf-mask=\"0\"]')");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('generateMaskSetupCode', () => {
|
|
112
|
+
it('generates no setup code when no region masks present', () => {
|
|
113
|
+
const masks: MaskRegion[] = [
|
|
114
|
+
{ selector: '.timestamp' },
|
|
115
|
+
{ selector: '.user-avatar' },
|
|
116
|
+
];
|
|
117
|
+
const result = generateMaskSetupCode(masks);
|
|
118
|
+
|
|
119
|
+
expect(result).toBe('');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('generates overlay injection for single region mask', () => {
|
|
123
|
+
const masks: MaskRegion[] = [
|
|
124
|
+
{ region: { x: 10, y: 20, width: 100, height: 50 } },
|
|
125
|
+
];
|
|
126
|
+
const result = generateMaskSetupCode(masks);
|
|
127
|
+
|
|
128
|
+
// Should inject overlay element with correct positioning
|
|
129
|
+
expect(result).toContain('await page.evaluate');
|
|
130
|
+
expect(result).toContain('document.createElement');
|
|
131
|
+
expect(result).toContain("div.setAttribute('data-bf-mask'");
|
|
132
|
+
expect(result).toContain('left:10%');
|
|
133
|
+
expect(result).toContain('top:20%');
|
|
134
|
+
expect(result).toContain('width:100%');
|
|
135
|
+
expect(result).toContain('height:50%');
|
|
136
|
+
expect(result).toContain('position:fixed');
|
|
137
|
+
expect(result).toContain('pointer-events:none');
|
|
138
|
+
expect(result).toContain('z-index:99999');
|
|
139
|
+
expect(result).toContain('document.body.appendChild');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('generates overlay injection for multiple region masks', () => {
|
|
143
|
+
const masks: MaskRegion[] = [
|
|
144
|
+
{ selector: '.header' },
|
|
145
|
+
{ region: { x: 10, y: 20, width: 30, height: 40 } },
|
|
146
|
+
{ region: { x: 80, y: 5, width: 15, height: 10 } },
|
|
147
|
+
{ selector: '.footer' },
|
|
148
|
+
];
|
|
149
|
+
const result = generateMaskSetupCode(masks);
|
|
150
|
+
|
|
151
|
+
// Should only process region masks (indices 0 and 1 for regions)
|
|
152
|
+
expect(result).toContain('await page.evaluate');
|
|
153
|
+
expect(result).toContain('left:10%');
|
|
154
|
+
expect(result).toContain('top:20%');
|
|
155
|
+
expect(result).toContain('left:80%');
|
|
156
|
+
expect(result).toContain('top:5%');
|
|
157
|
+
// Should create two overlay elements
|
|
158
|
+
expect((result.match(/data-bf-mask/g) || []).length).toBe(2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('uses correct data attribute indices for region masks', () => {
|
|
162
|
+
const masks: MaskRegion[] = [
|
|
163
|
+
{ region: { x: 0, y: 0, width: 50, height: 50 } }, // index 0
|
|
164
|
+
{ selector: '.selector1' }, // not counted
|
|
165
|
+
{ region: { x: 50, y: 50, width: 50, height: 50 } }, // index 1
|
|
166
|
+
{ selector: '.selector2' }, // not counted
|
|
167
|
+
{ region: { x: 25, y: 25, width: 50, height: 50 } }, // index 2
|
|
168
|
+
];
|
|
169
|
+
const result = generateMaskSetupCode(masks);
|
|
170
|
+
|
|
171
|
+
// Should use indices 0, 1, 2 for the three region masks
|
|
172
|
+
expect(result).toContain("'data-bf-mask', '0'");
|
|
173
|
+
expect(result).toContain("'data-bf-mask', '1'");
|
|
174
|
+
expect(result).toContain("'data-bf-mask', '2'");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('handles empty mask array', () => {
|
|
178
|
+
const result = generateMaskSetupCode([]);
|
|
179
|
+
|
|
180
|
+
expect(result).toBe('');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('uses custom page variable when provided', () => {
|
|
184
|
+
const masks: MaskRegion[] = [
|
|
185
|
+
{ region: { x: 10, y: 20, width: 30, height: 40 } },
|
|
186
|
+
];
|
|
187
|
+
const result = generateMaskSetupCode(masks, 'customPage');
|
|
188
|
+
|
|
189
|
+
expect(result).toContain('await customPage.evaluate');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('mask array generation edge cases', () => {
|
|
194
|
+
it('preserves order of mixed masks', () => {
|
|
195
|
+
const masks: MaskRegion[] = [
|
|
196
|
+
{ selector: '.first' },
|
|
197
|
+
{ region: { x: 10, y: 10, width: 10, height: 10 } },
|
|
198
|
+
{ selector: '.second' },
|
|
199
|
+
{ region: { x: 20, y: 20, width: 20, height: 20 } },
|
|
200
|
+
{ selector: '.third' },
|
|
201
|
+
];
|
|
202
|
+
const result = generateScreenshotAssertion({ name: 'test', mask: masks });
|
|
203
|
+
|
|
204
|
+
// Should maintain order in mask array
|
|
205
|
+
// Use greedy match to handle attribute selectors with brackets
|
|
206
|
+
const maskArrayMatch = result.match(/mask: \[(.*)\]/s);
|
|
207
|
+
expect(maskArrayMatch).toBeTruthy();
|
|
208
|
+
|
|
209
|
+
const maskArray = maskArrayMatch![1];
|
|
210
|
+
const firstIndex = maskArray.indexOf("'.first'");
|
|
211
|
+
const secondIndex = maskArray.indexOf('[data-bf-mask="0"]');
|
|
212
|
+
const thirdIndex = maskArray.indexOf("'.second'");
|
|
213
|
+
const fourthIndex = maskArray.indexOf('[data-bf-mask="1"]');
|
|
214
|
+
const fifthIndex = maskArray.indexOf("'.third'");
|
|
215
|
+
|
|
216
|
+
expect(firstIndex).toBeLessThan(secondIndex);
|
|
217
|
+
expect(secondIndex).toBeLessThan(thirdIndex);
|
|
218
|
+
expect(thirdIndex).toBeLessThan(fourthIndex);
|
|
219
|
+
expect(fourthIndex).toBeLessThan(fifthIndex);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('escapes special characters in selector masks', () => {
|
|
223
|
+
const masks: MaskRegion[] = [
|
|
224
|
+
{ selector: "div[data-test='value']" },
|
|
225
|
+
];
|
|
226
|
+
const result = generateScreenshotAssertion({ name: 'test', mask: masks });
|
|
227
|
+
|
|
228
|
+
// Should properly escape the selector string
|
|
229
|
+
expect(result).toContain("page.locator('div[data-test=\\'value\\']')");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* visual-checks.ts
|
|
3
|
+
* Generates Playwright screenshot assertion code.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MaskRegion } from '@browserflow-ai/core';
|
|
7
|
+
import { escapeString } from './locator-emit.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for screenshot assertion generation.
|
|
11
|
+
*/
|
|
12
|
+
export interface ScreenshotOptions {
|
|
13
|
+
/** Name for the screenshot (used in baseline path) */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Regions to mask during comparison */
|
|
16
|
+
mask?: MaskRegion[];
|
|
17
|
+
/** Maximum allowed pixel difference (0-1) */
|
|
18
|
+
maxDiffPixelRatio?: number;
|
|
19
|
+
/** Threshold for pixel color difference (0-1) */
|
|
20
|
+
threshold?: number;
|
|
21
|
+
/** Animation handling: "disabled" waits for animations to finish */
|
|
22
|
+
animations?: 'disabled' | 'allow';
|
|
23
|
+
/** Whether to capture full page */
|
|
24
|
+
fullPage?: boolean;
|
|
25
|
+
/** Timeout for screenshot capture */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options for visual check code generation.
|
|
31
|
+
*/
|
|
32
|
+
export interface VisualCheckEmitOptions {
|
|
33
|
+
/** Variable name for the page object (default: "page") */
|
|
34
|
+
pageVar?: string;
|
|
35
|
+
/** Base path for baseline screenshots */
|
|
36
|
+
baselinesPath?: string;
|
|
37
|
+
/** Include comments explaining the assertion */
|
|
38
|
+
includeComments?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generates code for a Playwright toHaveScreenshot assertion.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* generateScreenshotAssertion({ name: 'homepage' })
|
|
46
|
+
* // Returns: "await expect(page).toHaveScreenshot('homepage.png');"
|
|
47
|
+
*/
|
|
48
|
+
export function generateScreenshotAssertion(
|
|
49
|
+
options: ScreenshotOptions,
|
|
50
|
+
emitOptions: VisualCheckEmitOptions = {}
|
|
51
|
+
): string {
|
|
52
|
+
const { pageVar = 'page', includeComments = false } = emitOptions;
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
|
|
55
|
+
if (includeComments && options.name) {
|
|
56
|
+
lines.push(`// Visual check: ${options.name}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const screenshotName = options.name.endsWith('.png')
|
|
60
|
+
? options.name
|
|
61
|
+
: `${options.name}.png`;
|
|
62
|
+
|
|
63
|
+
const assertionOptions = buildAssertionOptions(options, pageVar);
|
|
64
|
+
|
|
65
|
+
if (assertionOptions) {
|
|
66
|
+
lines.push(
|
|
67
|
+
`await expect(${pageVar}).toHaveScreenshot('${escapeString(screenshotName)}', ${assertionOptions});`
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
lines.push(
|
|
71
|
+
`await expect(${pageVar}).toHaveScreenshot('${escapeString(screenshotName)}');`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generates code for an element-specific screenshot assertion.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* generateElementScreenshotAssertion("page.locator('.card')", { name: 'card' })
|
|
83
|
+
* // Returns: "await expect(page.locator('.card')).toHaveScreenshot('card.png');"
|
|
84
|
+
*/
|
|
85
|
+
export function generateElementScreenshotAssertion(
|
|
86
|
+
locatorCode: string,
|
|
87
|
+
options: ScreenshotOptions,
|
|
88
|
+
emitOptions: VisualCheckEmitOptions = {}
|
|
89
|
+
): string {
|
|
90
|
+
const { pageVar = 'page', includeComments = false } = emitOptions;
|
|
91
|
+
const lines: string[] = [];
|
|
92
|
+
|
|
93
|
+
if (includeComments && options.name) {
|
|
94
|
+
lines.push(`// Visual check (element): ${options.name}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const screenshotName = options.name.endsWith('.png')
|
|
98
|
+
? options.name
|
|
99
|
+
: `${options.name}.png`;
|
|
100
|
+
|
|
101
|
+
const assertionOptions = buildAssertionOptions(options, pageVar);
|
|
102
|
+
|
|
103
|
+
if (assertionOptions) {
|
|
104
|
+
lines.push(
|
|
105
|
+
`await expect(${locatorCode}).toHaveScreenshot('${escapeString(screenshotName)}', ${assertionOptions});`
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(
|
|
109
|
+
`await expect(${locatorCode}).toHaveScreenshot('${escapeString(screenshotName)}');`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds the options object string for toHaveScreenshot.
|
|
118
|
+
*/
|
|
119
|
+
function buildAssertionOptions(options: ScreenshotOptions, pageVar = 'page'): string {
|
|
120
|
+
const parts: string[] = [];
|
|
121
|
+
|
|
122
|
+
if (options.maxDiffPixelRatio !== undefined) {
|
|
123
|
+
parts.push(`maxDiffPixelRatio: ${options.maxDiffPixelRatio}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (options.threshold !== undefined) {
|
|
127
|
+
parts.push(`threshold: ${options.threshold}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.animations) {
|
|
131
|
+
parts.push(`animations: '${options.animations}'`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (options.fullPage !== undefined) {
|
|
135
|
+
parts.push(`fullPage: ${options.fullPage}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (options.timeout !== undefined) {
|
|
139
|
+
parts.push(`timeout: ${options.timeout}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.mask && options.mask.length > 0) {
|
|
143
|
+
const maskCode = generateMaskArray(options.mask, pageVar);
|
|
144
|
+
parts.push(`mask: ${maskCode}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (parts.length === 0) {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return `{ ${parts.join(', ')} }`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generates code for a mask array.
|
|
156
|
+
*/
|
|
157
|
+
function generateMaskArray(masks: MaskRegion[], pageVar = 'page'): string {
|
|
158
|
+
let regionMaskIndex = 0;
|
|
159
|
+
const maskItems = masks
|
|
160
|
+
.map((mask) => {
|
|
161
|
+
if (mask.selector) {
|
|
162
|
+
return `${pageVar}.locator('${escapeString(mask.selector)}')`;
|
|
163
|
+
}
|
|
164
|
+
if (mask.region) {
|
|
165
|
+
// Create reference to overlay element that will be injected
|
|
166
|
+
const selector = `[data-bf-mask="${regionMaskIndex}"]`;
|
|
167
|
+
const locatorCode = `${pageVar}.locator('${escapeString(selector)}')`;
|
|
168
|
+
regionMaskIndex++;
|
|
169
|
+
return locatorCode;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
})
|
|
173
|
+
.filter((item): item is string => item !== null);
|
|
174
|
+
|
|
175
|
+
if (maskItems.length === 0) {
|
|
176
|
+
return '[]';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return `[${maskItems.join(', ')}]`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Generates a screenshot capture statement (not an assertion).
|
|
184
|
+
*/
|
|
185
|
+
export function generateScreenshotCapture(
|
|
186
|
+
options: ScreenshotOptions,
|
|
187
|
+
emitOptions: VisualCheckEmitOptions = {}
|
|
188
|
+
): string {
|
|
189
|
+
const { pageVar = 'page', baselinesPath, includeComments = false } = emitOptions;
|
|
190
|
+
const lines: string[] = [];
|
|
191
|
+
|
|
192
|
+
if (includeComments && options.name) {
|
|
193
|
+
lines.push(`// Capture screenshot: ${options.name}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const screenshotName = options.name.endsWith('.png')
|
|
197
|
+
? options.name
|
|
198
|
+
: `${options.name}.png`;
|
|
199
|
+
|
|
200
|
+
const path = baselinesPath
|
|
201
|
+
? `${baselinesPath}/${screenshotName}`
|
|
202
|
+
: screenshotName;
|
|
203
|
+
|
|
204
|
+
const captureOptions: string[] = [];
|
|
205
|
+
captureOptions.push(`path: '${escapeString(path)}'`);
|
|
206
|
+
|
|
207
|
+
if (options.fullPage !== undefined) {
|
|
208
|
+
captureOptions.push(`fullPage: ${options.fullPage}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (options.animations) {
|
|
212
|
+
captureOptions.push(`animations: '${options.animations}'`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (options.mask && options.mask.length > 0) {
|
|
216
|
+
const maskCode = generateMaskArray(options.mask, pageVar);
|
|
217
|
+
captureOptions.push(`mask: ${maskCode}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
lines.push(
|
|
221
|
+
`await ${pageVar}.screenshot({ ${captureOptions.join(', ')} });`
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Generates code for comparing two screenshot paths.
|
|
229
|
+
* This is useful for custom comparison logic outside of Playwright's built-in assertions.
|
|
230
|
+
*/
|
|
231
|
+
export function generateScreenshotCompare(
|
|
232
|
+
actualPath: string,
|
|
233
|
+
expectedPath: string,
|
|
234
|
+
options: { threshold?: number } = {}
|
|
235
|
+
): string {
|
|
236
|
+
const { threshold = 0.05 } = options;
|
|
237
|
+
return `
|
|
238
|
+
// Compare screenshots
|
|
239
|
+
const actualBuffer = await fs.readFile('${escapeString(actualPath)}');
|
|
240
|
+
const expectedBuffer = await fs.readFile('${escapeString(expectedPath)}');
|
|
241
|
+
const { diffPixelRatio } = await compareImages(actualBuffer, expectedBuffer);
|
|
242
|
+
expect(diffPixelRatio).toBeLessThanOrEqual(${threshold});
|
|
243
|
+
`.trim();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generates a helper import statement for visual comparisons.
|
|
248
|
+
*/
|
|
249
|
+
export function generateVisualImports(): string {
|
|
250
|
+
return `import { expect } from '@playwright/test';`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Generates wait-for-animations code before taking a screenshot.
|
|
255
|
+
*/
|
|
256
|
+
export function generateWaitForAnimations(pageVar = 'page'): string {
|
|
257
|
+
return `// Wait for animations to complete
|
|
258
|
+
await ${pageVar}.waitForLoadState('networkidle');
|
|
259
|
+
await ${pageVar}.waitForTimeout(500);`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Generates code to inject overlay elements for region-based masks.
|
|
264
|
+
* Returns empty string if no region masks are present.
|
|
265
|
+
*/
|
|
266
|
+
export function generateMaskSetupCode(
|
|
267
|
+
masks: MaskRegion[],
|
|
268
|
+
pageVar = 'page'
|
|
269
|
+
): string {
|
|
270
|
+
// Filter to only region-based masks
|
|
271
|
+
const regionMasks = masks.filter((m) => m.region);
|
|
272
|
+
|
|
273
|
+
if (regionMasks.length === 0) {
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Generate individual injection statements for each region mask
|
|
278
|
+
// Each block is wrapped in its own scope to reuse variable name 'div'
|
|
279
|
+
const injectionStatements = regionMasks
|
|
280
|
+
.map((mask, index) => {
|
|
281
|
+
const r = mask.region!;
|
|
282
|
+
return ` {
|
|
283
|
+
const div = document.createElement('div');
|
|
284
|
+
div.setAttribute('data-bf-mask', '${index}');
|
|
285
|
+
div.style.cssText = 'position:fixed;left:${r.x}%;top:${r.y}%;width:${r.width}%;height:${r.height}%;pointer-events:none;z-index:99999';
|
|
286
|
+
document.body.appendChild(div);
|
|
287
|
+
}`;
|
|
288
|
+
})
|
|
289
|
+
.join('\n');
|
|
290
|
+
|
|
291
|
+
return `await ${pageVar}.evaluate(() => {
|
|
292
|
+
${injectionStatements}
|
|
293
|
+
});`;
|
|
294
|
+
}
|