@pixeyedev/playwright 0.4.5 → 0.4.7
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/LICENSE +21 -0
- package/dist/codemods/from-argos.d.ts +0 -9
- package/dist/codemods/from-argos.js +0 -12
- package/dist/codemods/from-percy.d.ts +0 -9
- package/dist/codemods/from-percy.js +0 -12
- package/dist/fixture.d.ts +0 -14
- package/dist/fixture.js +2 -16
- package/dist/index.js +0 -1
- package/dist/reporter.d.ts +4 -8
- package/dist/reporter.js +26 -40
- package/dist/screenshot.d.ts +1 -1
- package/dist/screenshot.js +2 -31
- package/package.json +5 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pixeye (pixeye.dev)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transform Argos CI code to Pixeye.
|
|
3
|
-
* Usage: import and call transformArgos(sourceCode) -> transformed code
|
|
4
|
-
*
|
|
5
|
-
* Handles:
|
|
6
|
-
* - import { argosScreenshot } from '@argos-ci/playwright' -> import { pixeye } from '@pixeyedev/playwright'
|
|
7
|
-
* - argosScreenshot(page, name, opts) -> pixeye.screenshot(page, name, opts)
|
|
8
|
-
* - { argosCSS: '...' } -> { css: '...' }
|
|
9
|
-
*/
|
|
10
1
|
export declare const transformArgos: (source: string) => string;
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transform Argos CI code to Pixeye.
|
|
3
|
-
* Usage: import and call transformArgos(sourceCode) -> transformed code
|
|
4
|
-
*
|
|
5
|
-
* Handles:
|
|
6
|
-
* - import { argosScreenshot } from '@argos-ci/playwright' -> import { pixeye } from '@pixeyedev/playwright'
|
|
7
|
-
* - argosScreenshot(page, name, opts) -> pixeye.screenshot(page, name, opts)
|
|
8
|
-
* - { argosCSS: '...' } -> { css: '...' }
|
|
9
|
-
*/
|
|
10
1
|
export const transformArgos = (source) => {
|
|
11
2
|
let result = source;
|
|
12
|
-
// 1. Transform import
|
|
13
3
|
result = result.replace(/import\s*\{\s*argosScreenshot\s*\}\s*from\s*['"]@argos-ci\/playwright['"]/g, "import { pixeye } from '@pixeyedev/playwright'");
|
|
14
|
-
// 2. Transform function calls
|
|
15
4
|
result = result.replace(/argosScreenshot\s*\(/g, 'pixeye.screenshot(');
|
|
16
|
-
// 3. Transform option names
|
|
17
5
|
result = result.replace(/argosCSS\s*:/g, 'css:');
|
|
18
6
|
return result;
|
|
19
7
|
};
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transform Percy code to Pixeye.
|
|
3
|
-
* Usage: import and call transformPercy(sourceCode) -> transformed code
|
|
4
|
-
*
|
|
5
|
-
* Handles:
|
|
6
|
-
* - import percySnapshot from '@percy/playwright' -> import { pixeye } from '@pixeyedev/playwright'
|
|
7
|
-
* - percySnapshot(page, name, opts) -> pixeye.screenshot(page, name, opts)
|
|
8
|
-
* - { percyCSS: '...' } -> { css: '...' }
|
|
9
|
-
*/
|
|
10
1
|
export declare const transformPercy: (source: string) => string;
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transform Percy code to Pixeye.
|
|
3
|
-
* Usage: import and call transformPercy(sourceCode) -> transformed code
|
|
4
|
-
*
|
|
5
|
-
* Handles:
|
|
6
|
-
* - import percySnapshot from '@percy/playwright' -> import { pixeye } from '@pixeyedev/playwright'
|
|
7
|
-
* - percySnapshot(page, name, opts) -> pixeye.screenshot(page, name, opts)
|
|
8
|
-
* - { percyCSS: '...' } -> { css: '...' }
|
|
9
|
-
*/
|
|
10
1
|
export const transformPercy = (source) => {
|
|
11
2
|
let result = source;
|
|
12
|
-
// 1. Transform import (default import)
|
|
13
3
|
result = result.replace(/import\s+percySnapshot\s+from\s*['"]@percy\/playwright['"]/g, "import { pixeye } from '@pixeyedev/playwright'");
|
|
14
|
-
// 2. Transform function calls
|
|
15
4
|
result = result.replace(/percySnapshot\s*\(/g, 'pixeye.screenshot(');
|
|
16
|
-
// 3. Transform option names
|
|
17
5
|
result = result.replace(/percyCSS\s*:/g, 'css:');
|
|
18
6
|
return result;
|
|
19
7
|
};
|
package/dist/fixture.d.ts
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright test fixture that provides `pixeye` in every test.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* import { test, expect } from '@pixeyedev/playwright';
|
|
6
|
-
*
|
|
7
|
-
* test('login page', async ({ page, pixeye }) => {
|
|
8
|
-
* await page.goto('/login');
|
|
9
|
-
* await pixeye.screenshot(page, 'login-page');
|
|
10
|
-
* });
|
|
11
|
-
*
|
|
12
|
-
* The fixture automatically populates testFile and testName from
|
|
13
|
-
* Playwright's testInfo, so metadata is always accurate.
|
|
14
|
-
*/
|
|
15
1
|
import { expect } from '@playwright/test';
|
|
16
2
|
import { screenshot, type ScreenshotOptions } from './screenshot.js';
|
|
17
3
|
export interface PixeyeFixture {
|
package/dist/fixture.js
CHANGED
|
@@ -1,29 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright test fixture that provides `pixeye` in every test.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* import { test, expect } from '@pixeyedev/playwright';
|
|
6
|
-
*
|
|
7
|
-
* test('login page', async ({ page, pixeye }) => {
|
|
8
|
-
* await page.goto('/login');
|
|
9
|
-
* await pixeye.screenshot(page, 'login-page');
|
|
10
|
-
* });
|
|
11
|
-
*
|
|
12
|
-
* The fixture automatically populates testFile and testName from
|
|
13
|
-
* Playwright's testInfo, so metadata is always accurate.
|
|
14
|
-
*/
|
|
15
1
|
import { join } from 'node:path';
|
|
16
2
|
import { expect, test as base } from '@playwright/test';
|
|
17
3
|
import { screenshot, setContext } from './screenshot.js';
|
|
18
4
|
export const test = base.extend({
|
|
19
|
-
// eslint-disable-next-line no-empty-pattern
|
|
20
5
|
pixeye: async ({}, use, testInfo) => {
|
|
21
6
|
const outputDir = join(process.cwd(), '.pixeye', 'screenshots');
|
|
22
7
|
const testFile = testInfo.file ? testInfo.file.replace(process.cwd() + '/', '') : 'unknown';
|
|
23
8
|
setContext({
|
|
24
9
|
outputDir,
|
|
25
10
|
testFile,
|
|
26
|
-
testName: testInfo.title
|
|
11
|
+
testName: testInfo.title,
|
|
12
|
+
testId: testInfo.testId
|
|
27
13
|
});
|
|
28
14
|
const pixeyeApi = {
|
|
29
15
|
screenshot: (target, name, options) => screenshot(target, name, options)
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { expect, test } from './fixture.js';
|
|
2
2
|
export { getContext, PIXEYE_BROWSER_ARGS, screenshot, setContext } from './screenshot.js';
|
|
3
|
-
// ── Convenience namespace (backwards compat with design docs) ───────────────
|
|
4
3
|
import { screenshot as screenshotFn } from './screenshot.js';
|
|
5
4
|
export const pixeye = { screenshot: screenshotFn };
|
package/dist/reporter.d.ts
CHANGED
|
@@ -3,26 +3,22 @@ interface ReporterOptions {
|
|
|
3
3
|
token?: string;
|
|
4
4
|
server?: string;
|
|
5
5
|
uploadConcurrency?: number;
|
|
6
|
-
/**
|
|
7
|
-
* Fail the test run (non-zero exit) when the upload is rejected for a
|
|
8
|
-
* CUSTOMER misconfiguration (bad token, lapsed plan/quota, permissions) so a
|
|
9
|
-
* run can't pass green with zero screenshots uploaded. Default `true`. Our
|
|
10
|
-
* transient faults (5xx/network) never fail the run regardless. Opt out with
|
|
11
|
-
* `failOnError: false` or `PIXEYE_FAIL_ON_ERROR=false`.
|
|
12
|
-
*/
|
|
13
6
|
failOnError?: boolean;
|
|
14
7
|
}
|
|
15
8
|
declare const detectShard: (config: FullConfig) => {
|
|
16
9
|
index: number;
|
|
17
10
|
total: number;
|
|
18
11
|
};
|
|
12
|
+
export declare const failedTestIds: (suite: Suite | null) => Set<string>;
|
|
13
|
+
export declare const markFailedTests: (metadata: Record<string, unknown> | undefined, failed: Set<string>) => Record<string, unknown> | undefined;
|
|
19
14
|
declare class PixeyeReporter implements Reporter {
|
|
20
15
|
private options;
|
|
21
16
|
private shard;
|
|
22
17
|
private skipUpload;
|
|
23
18
|
private failOnError;
|
|
19
|
+
private suite;
|
|
24
20
|
constructor(options?: ReporterOptions);
|
|
25
|
-
onBegin(config: FullConfig,
|
|
21
|
+
onBegin(config: FullConfig, suite: Suite): void;
|
|
26
22
|
onTestEnd(_test: TestCase, _result: TestResult): void;
|
|
27
23
|
onEnd(_result: FullResult): Promise<{
|
|
28
24
|
status: FullResult['status'];
|
package/dist/reporter.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { detectCIEnvironment, discoverScreenshots,
|
|
3
|
+
import { detectCIEnvironment, discoverScreenshots, PixeyeClient, renderUploadFailureBanner, resolveFailOnError, resolveUploadConcurrency, runParallel, swapImageExtension } from '@pixeyedev/core';
|
|
4
|
+
import { CONTENT_TYPE_BY_FORMAT } from '@pixeyedev/shared';
|
|
4
5
|
import { getContext } from './screenshot.js';
|
|
5
|
-
const DEFAULT_UPLOAD_CONCURRENCY = 32;
|
|
6
6
|
const detectShard = (config) => {
|
|
7
|
-
// 1. Playwright config (1-indexed → 0-indexed)
|
|
8
7
|
if (config.shard) {
|
|
9
8
|
return { index: config.shard.current - 1, total: config.shard.total };
|
|
10
9
|
}
|
|
11
|
-
// 2. Environment variables (0-indexed)
|
|
12
10
|
const envIndex = process.env.PIXEYE_SHARD_INDEX;
|
|
13
11
|
const envTotal = process.env.PIXEYE_SHARD_TOTAL;
|
|
14
12
|
if (envIndex !== undefined) {
|
|
@@ -17,22 +15,29 @@ const detectShard = (config) => {
|
|
|
17
15
|
total: envTotal ? parseInt(envTotal, 10) : 1
|
|
18
16
|
};
|
|
19
17
|
}
|
|
20
|
-
// 3. Default: no sharding
|
|
21
18
|
return { index: 0, total: 1 };
|
|
22
19
|
};
|
|
23
20
|
const detectRunner = () => {
|
|
24
21
|
return 'playwright';
|
|
25
22
|
};
|
|
23
|
+
export const failedTestIds = (suite) => new Set((suite ? suite.allTests() : [])
|
|
24
|
+
.filter(test => test.outcome() === 'unexpected')
|
|
25
|
+
.map(test => test.id));
|
|
26
|
+
export const markFailedTests = (metadata, failed) => metadata && typeof metadata.testId === 'string' && failed.has(metadata.testId)
|
|
27
|
+
? { ...metadata, testStatus: 'failed' }
|
|
28
|
+
: metadata;
|
|
26
29
|
class PixeyeReporter {
|
|
27
30
|
options;
|
|
28
31
|
shard = { index: 0, total: 1 };
|
|
29
32
|
skipUpload = false;
|
|
30
33
|
failOnError;
|
|
34
|
+
suite = null;
|
|
31
35
|
constructor(options = {}) {
|
|
32
36
|
this.options = options;
|
|
33
37
|
this.failOnError = resolveFailOnError(options.failOnError, process.env.PIXEYE_FAIL_ON_ERROR);
|
|
34
38
|
}
|
|
35
|
-
onBegin(config,
|
|
39
|
+
onBegin(config, suite) {
|
|
40
|
+
this.suite = suite;
|
|
36
41
|
const token = this.options.token ?? process.env.PIXEYE_TOKEN;
|
|
37
42
|
if (!token) {
|
|
38
43
|
console.warn('[pixeye] No PIXEYE_TOKEN found. Skipping upload. Screenshots will still be captured.');
|
|
@@ -42,8 +47,6 @@ class PixeyeReporter {
|
|
|
42
47
|
this.shard = detectShard(config);
|
|
43
48
|
}
|
|
44
49
|
onTestEnd(_test, _result) {
|
|
45
|
-
// Screenshots are already saved to disk by pixeye.screenshot().
|
|
46
|
-
// Nothing to do per-test.
|
|
47
50
|
}
|
|
48
51
|
async onEnd(_result) {
|
|
49
52
|
if (this.skipUpload) {
|
|
@@ -54,15 +57,15 @@ class PixeyeReporter {
|
|
|
54
57
|
if (!token) {
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
|
-
// Discover screenshots saved by pixeye.screenshot()
|
|
58
60
|
const ctx = getContext();
|
|
59
61
|
const outputDir = ctx?.outputDir ?? join(process.cwd(), '.pixeye', 'screenshots');
|
|
60
62
|
let files;
|
|
61
63
|
try {
|
|
62
64
|
files = await discoverScreenshots(outputDir);
|
|
63
65
|
}
|
|
64
|
-
catch {
|
|
65
|
-
console.
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(`[pixeye] Could not read screenshots in ${outputDir}.`);
|
|
68
|
+
console.error(`[pixeye] ${err.message}`);
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
71
|
if (files.length === 0) {
|
|
@@ -78,10 +81,11 @@ class PixeyeReporter {
|
|
|
78
81
|
return;
|
|
79
82
|
}
|
|
80
83
|
const runner = detectRunner();
|
|
81
|
-
|
|
84
|
+
let observes = false;
|
|
82
85
|
try {
|
|
86
|
+
const concurrency = resolveUploadConcurrency(this.options.uploadConcurrency);
|
|
83
87
|
const client = new PixeyeClient({ serverUrl: server, token });
|
|
84
|
-
|
|
88
|
+
const failed = failedTestIds(this.suite);
|
|
85
89
|
const prNumber = process.env.PIXEYE_PR ? parseInt(process.env.PIXEYE_PR, 10) : ci?.prNumber;
|
|
86
90
|
const buildResponse = await client.createBuild({
|
|
87
91
|
projectToken: token,
|
|
@@ -93,9 +97,9 @@ class PixeyeReporter {
|
|
|
93
97
|
baseBranch: ci?.baseBranch
|
|
94
98
|
});
|
|
95
99
|
const buildId = buildResponse.build.id;
|
|
96
|
-
|
|
100
|
+
observes = client.observes;
|
|
97
101
|
const hashes = files.map(f => f.fileHash);
|
|
98
|
-
const formats = Object.fromEntries(files.filter(f => f.format
|
|
102
|
+
const formats = Object.fromEntries(files.filter(f => f.format !== 'png').map(f => [f.fileHash, f.format]));
|
|
99
103
|
const presign = await client.presignUpload({
|
|
100
104
|
buildId,
|
|
101
105
|
hashes,
|
|
@@ -106,7 +110,6 @@ class PixeyeReporter {
|
|
|
106
110
|
console.warn(`[pixeye] ${w}`);
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
|
-
// 3. Upload new files
|
|
110
113
|
const hashUrlPairs = Object.entries(presign.urls);
|
|
111
114
|
const hashToFile = new Map(files.map(f => [f.fileHash, f]));
|
|
112
115
|
let uploaded = 0;
|
|
@@ -117,22 +120,19 @@ class PixeyeReporter {
|
|
|
117
120
|
return;
|
|
118
121
|
}
|
|
119
122
|
const buffer = await readFile(file.filePath);
|
|
120
|
-
const contentType = file.format
|
|
123
|
+
const contentType = CONTENT_TYPE_BY_FORMAT[file.format];
|
|
121
124
|
await client.uploadFile(url, buffer, contentType);
|
|
122
125
|
uploaded++;
|
|
123
126
|
});
|
|
124
127
|
}
|
|
125
|
-
// 4. Read metadata sidecars and build manifest
|
|
126
128
|
const manifest = await Promise.all(files.map(async (f) => {
|
|
127
|
-
|
|
128
|
-
const metaPath = f.filePath.replace(/\.(png|webp)$/, '.meta.json');
|
|
129
|
+
const metaPath = swapImageExtension(f.filePath, '.meta.json');
|
|
129
130
|
let metadata;
|
|
130
131
|
try {
|
|
131
132
|
const metaContent = await readFile(metaPath, 'utf-8');
|
|
132
133
|
metadata = JSON.parse(metaContent);
|
|
133
134
|
}
|
|
134
135
|
catch {
|
|
135
|
-
// No sidecar — that's fine
|
|
136
136
|
}
|
|
137
137
|
return {
|
|
138
138
|
name: f.name,
|
|
@@ -140,10 +140,9 @@ class PixeyeReporter {
|
|
|
140
140
|
format: f.format,
|
|
141
141
|
width: f.width,
|
|
142
142
|
height: f.height,
|
|
143
|
-
metadata
|
|
143
|
+
metadata: markFailedTests(metadata, failed)
|
|
144
144
|
};
|
|
145
145
|
}));
|
|
146
|
-
// 5. Submit shard
|
|
147
146
|
await client.submitShard({
|
|
148
147
|
buildId,
|
|
149
148
|
shardIndex: this.shard.index,
|
|
@@ -152,23 +151,10 @@ class PixeyeReporter {
|
|
|
152
151
|
console.log(`[pixeye] ${files.length} screenshots (${uploaded} new, ${presign.existing.length} cached) — ${server}/builds/${buildId}`);
|
|
153
152
|
}
|
|
154
153
|
catch (err) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (isUploadMisconfig(err)) {
|
|
160
|
-
console.error(renderUploadFailureBanner(err, { willFail: this.failOnError }));
|
|
161
|
-
if (this.failOnError) {
|
|
162
|
-
// Playwright IGNORES `process.exitCode` — its runner calls
|
|
163
|
-
// `process.exit(status === 'passed' ? 0 : 1)`, overriding it. The ONLY
|
|
164
|
-
// reporter-side lever is returning a status from onEnd, which the
|
|
165
|
-
// multiplexer applies (`result.status = outResult.status`). Return
|
|
166
|
-
// 'failed' so a passing run with a rejected upload still exits non-zero.
|
|
167
|
-
return { status: 'failed' };
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
console.warn(`[pixeye] Upload failed (transient — CI not failed): ${uploadErrorSummary(err)}`);
|
|
154
|
+
const willFail = this.failOnError && !observes;
|
|
155
|
+
console.error(renderUploadFailureBanner(err, { willFail }));
|
|
156
|
+
if (willFail) {
|
|
157
|
+
return { status: 'failed' };
|
|
172
158
|
}
|
|
173
159
|
}
|
|
174
160
|
}
|
package/dist/screenshot.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Locator, Page } from '@playwright/test';
|
|
2
|
-
/** Browser args recommended for deterministic screenshots. */
|
|
3
2
|
export declare const PIXEYE_BROWSER_ARGS: string[];
|
|
4
3
|
export interface ScreenshotOptions {
|
|
5
4
|
fullPage?: boolean;
|
|
@@ -27,6 +26,7 @@ export interface ScreenshotContext {
|
|
|
27
26
|
outputDir: string;
|
|
28
27
|
testFile: string;
|
|
29
28
|
testName: string;
|
|
29
|
+
testId?: string;
|
|
30
30
|
}
|
|
31
31
|
export declare const setContext: (ctx: ScreenshotContext) => void;
|
|
32
32
|
export declare const getContext: () => ScreenshotContext | null;
|
package/dist/screenshot.js
CHANGED
|
@@ -2,21 +2,16 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
/** Browser args recommended for deterministic screenshots. */
|
|
6
5
|
export const PIXEYE_BROWSER_ARGS = [
|
|
7
6
|
'--force-device-scale-factor=1',
|
|
8
7
|
'--font-render-hinting=none',
|
|
9
8
|
'--disable-lcd-text'
|
|
10
9
|
];
|
|
11
|
-
// Shared context set by the reporter or test setup
|
|
12
10
|
let _context = null;
|
|
13
11
|
export const setContext = (ctx) => {
|
|
14
12
|
_context = ctx;
|
|
15
13
|
};
|
|
16
14
|
export const getContext = () => _context;
|
|
17
|
-
// ── IIFE loader ──────────────────────────────────────────────────────────────
|
|
18
|
-
// The browser IIFE is copied into dist/ at build time (see package.json build script).
|
|
19
|
-
// This avoids a runtime dependency on @pixeyedev/browser, which is private/internal.
|
|
20
15
|
const THIS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
21
16
|
let _iifeCache = null;
|
|
22
17
|
const loadIIFE = async () => {
|
|
@@ -26,13 +21,9 @@ const loadIIFE = async () => {
|
|
|
26
21
|
_iifeCache = await readFile(join(THIS_DIR, 'browser.iife.js'), 'utf-8');
|
|
27
22
|
return _iifeCache;
|
|
28
23
|
};
|
|
29
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
30
24
|
const injectBrowserScripts = async (page) => {
|
|
31
25
|
const iife = await loadIIFE();
|
|
32
|
-
|
|
33
|
-
const alreadyInjected = await page.evaluate(
|
|
34
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
-
() => typeof window.__pixeye !== 'undefined');
|
|
26
|
+
const alreadyInjected = await page.evaluate(() => typeof window.__pixeye !== 'undefined');
|
|
36
27
|
if (!alreadyInjected) {
|
|
37
28
|
await page.evaluate(iife);
|
|
38
29
|
}
|
|
@@ -49,7 +40,6 @@ const isLocator = (target) => {
|
|
|
49
40
|
const sanitizePath = (str) => {
|
|
50
41
|
return str.replace(/[^a-zA-Z0-9._/-]/g, '_');
|
|
51
42
|
};
|
|
52
|
-
// ── Mask resolution ──────────────────────────────────────────────────────────
|
|
53
43
|
const resolveMaskRegions = async (page, masks) => {
|
|
54
44
|
const regions = [];
|
|
55
45
|
for (const selectorOrLocator of masks) {
|
|
@@ -72,7 +62,6 @@ const resolveMaskRegions = async (page, masks) => {
|
|
|
72
62
|
}
|
|
73
63
|
}
|
|
74
64
|
else {
|
|
75
|
-
// Locator
|
|
76
65
|
const box = await selectorOrLocator.boundingBox();
|
|
77
66
|
if (box) {
|
|
78
67
|
regions.push({
|
|
@@ -90,7 +79,6 @@ const resolveMaskRegions = async (page, masks) => {
|
|
|
90
79
|
}
|
|
91
80
|
return regions;
|
|
92
81
|
};
|
|
93
|
-
// ── Capture with retry ───────────────────────────────────────────────────────
|
|
94
82
|
const RETRY_DELAY_MS = 100;
|
|
95
83
|
const captureOnce = async (target, options) => {
|
|
96
84
|
const screenshotOpts = {};
|
|
@@ -115,7 +103,6 @@ const captureStable = async (target, options) => {
|
|
|
115
103
|
if (retries <= 0) {
|
|
116
104
|
return captureOnce(target, options);
|
|
117
105
|
}
|
|
118
|
-
// Capture retries+1 times, find first consecutive duplicate (stable frame)
|
|
119
106
|
const captures = [];
|
|
120
107
|
const page = getPage(target);
|
|
121
108
|
for (let i = 0; i <= retries; i++) {
|
|
@@ -130,13 +117,10 @@ const captureStable = async (target, options) => {
|
|
|
130
117
|
return captures[i];
|
|
131
118
|
}
|
|
132
119
|
}
|
|
133
|
-
// No stable frame found — return the last capture
|
|
134
120
|
return captures[captures.length - 1];
|
|
135
121
|
};
|
|
136
|
-
// ── Main screenshot function ─────────────────────────────────────────────────
|
|
137
122
|
export const screenshot = async (target, name, options = {}) => {
|
|
138
123
|
const page = getPage(target);
|
|
139
|
-
// Determine output directory
|
|
140
124
|
const outputDir = _context?.outputDir ?? join(process.cwd(), '.pixeye', 'screenshots');
|
|
141
125
|
const testFile = _context?.testFile ?? 'unknown';
|
|
142
126
|
const testName = _context?.testName ?? '';
|
|
@@ -145,38 +129,27 @@ export const screenshot = async (target, name, options = {}) => {
|
|
|
145
129
|
await mkdir(dir, { recursive: true });
|
|
146
130
|
const pngPath = join(dir, `${name}.png`);
|
|
147
131
|
const metaPath = join(dir, `${name}.meta.json`);
|
|
148
|
-
// 1. Inject browser scripts
|
|
149
132
|
await injectBrowserScripts(page);
|
|
150
|
-
// 2. Stabilize the page
|
|
151
133
|
await page.evaluate((opts) => {
|
|
152
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
134
|
window.__pixeye.stabilize({
|
|
154
135
|
css: opts.css,
|
|
155
136
|
colorScheme: opts.colorScheme
|
|
156
137
|
});
|
|
157
138
|
}, { css: options.css, colorScheme: options.colorScheme });
|
|
158
|
-
// 3. Wait for page to be ready
|
|
159
139
|
await page.evaluate((timeout) => {
|
|
160
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
140
|
return window.__pixeye.waitForReady({ timeout });
|
|
162
141
|
}, options.waitForTimeout ?? 10_000);
|
|
163
|
-
// 4. Custom waitFor selector
|
|
164
142
|
if (options.waitFor) {
|
|
165
143
|
await page.waitForSelector(options.waitFor, {
|
|
166
144
|
timeout: options.waitForTimeout ?? 10_000
|
|
167
145
|
});
|
|
168
146
|
}
|
|
169
|
-
// 5. Scan DOM for region attributes
|
|
170
147
|
const scannedRegions = await page.evaluate(() => {
|
|
171
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
148
|
return window.__pixeye.scanRegions();
|
|
173
149
|
});
|
|
174
|
-
// 6. Resolve mask selectors/locators to region bounds
|
|
175
150
|
const maskRegions = options.mask ? await resolveMaskRegions(page, options.mask) : [];
|
|
176
151
|
const allRegions = [...scannedRegions, ...maskRegions];
|
|
177
|
-
// 7. Capture screenshot (with optional retry for stability)
|
|
178
152
|
const buffer = await captureStable(target, options);
|
|
179
|
-
// 8. Get component bounds for element-level screenshots
|
|
180
153
|
let componentBounds = null;
|
|
181
154
|
if (isLocator(target)) {
|
|
182
155
|
const box = await target.boundingBox();
|
|
@@ -189,10 +162,8 @@ export const screenshot = async (target, name, options = {}) => {
|
|
|
189
162
|
};
|
|
190
163
|
}
|
|
191
164
|
}
|
|
192
|
-
// 9. Gather viewport + browser info
|
|
193
165
|
const viewportSize = page.viewportSize();
|
|
194
166
|
const browserName = page.context().browser()?.browserType().name() ?? 'unknown';
|
|
195
|
-
// 10. Build metadata sidecar
|
|
196
167
|
const metadata = {
|
|
197
168
|
version: 1,
|
|
198
169
|
viewport: viewportSize ? { width: viewportSize.width, height: viewportSize.height } : undefined,
|
|
@@ -200,11 +171,11 @@ export const screenshot = async (target, name, options = {}) => {
|
|
|
200
171
|
colorScheme: options.colorScheme,
|
|
201
172
|
testFile,
|
|
202
173
|
testName,
|
|
174
|
+
testId: _context?.testId,
|
|
203
175
|
threshold: options.threshold,
|
|
204
176
|
regions: allRegions.length > 0 ? allRegions : undefined,
|
|
205
177
|
componentBounds
|
|
206
178
|
};
|
|
207
|
-
// 11. Write .png and .meta.json
|
|
208
179
|
await writeFile(pngPath, buffer);
|
|
209
180
|
await writeFile(metaPath, JSON.stringify(metadata, null, 2) + '\n');
|
|
210
181
|
return pngPath;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixeyedev/playwright",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"private": false,
|
|
5
6
|
"type": "module",
|
|
6
7
|
"files": [
|
|
@@ -27,14 +28,15 @@
|
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "tsc && cp ../browser/dist/browser.iife.js dist/browser.iife.js",
|
|
31
|
+
"prepack": "rm -rf dist && npm run build",
|
|
30
32
|
"typecheck": "tsc --noEmit",
|
|
31
33
|
"test": "vitest run",
|
|
32
34
|
"lint": "echo 'ok'",
|
|
33
35
|
"ai-check": "exit 0"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@pixeyedev/core": "^0.4.
|
|
37
|
-
"@pixeyedev/shared": "^0.4.
|
|
38
|
+
"@pixeyedev/core": "^0.4.7",
|
|
39
|
+
"@pixeyedev/shared": "^0.4.7"
|
|
38
40
|
},
|
|
39
41
|
"peerDependencies": {
|
|
40
42
|
"@playwright/test": ">=1.40.0"
|