@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 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 };
@@ -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, _suite: Suite): void;
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, isUploadMisconfig, PixeyeClient, renderUploadFailureBanner, resolveFailOnError, runParallel, uploadErrorSummary } from '@pixeyedev/core';
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, _suite) {
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.log('[pixeye] No screenshots found. Nothing to upload.');
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
- const concurrency = this.options.uploadConcurrency ?? DEFAULT_UPLOAD_CONCURRENCY;
84
+ let observes = false;
82
85
  try {
86
+ const concurrency = resolveUploadConcurrency(this.options.uploadConcurrency);
83
87
  const client = new PixeyeClient({ serverUrl: server, token });
84
- // 1. Create build
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
- // 2. Presign upload URLs
100
+ observes = client.observes;
97
101
  const hashes = files.map(f => f.fileHash);
98
- const formats = Object.fromEntries(files.filter(f => f.format === 'webp').map(f => [f.fileHash, 'webp']));
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 === 'webp' ? 'image/webp' : 'image/png';
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
- // Try to read companion .meta.json
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
- // P0-2: a CUSTOMER misconfiguration (bad token, lapsed plan/quota,
156
- // permissions) must NOT pass silently — surface a loud banner and fail
157
- // the run by default so a green CI can't hide zero uploaded screenshots.
158
- // Our transient faults (5xx/network/R2) only warn — never break their CI.
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
  }
@@ -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;
@@ -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
- // Check if already injected to avoid double-injection
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.5",
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.0",
37
- "@pixeyedev/shared": "^0.4.0"
38
+ "@pixeyedev/core": "^0.4.7",
39
+ "@pixeyedev/shared": "^0.4.7"
38
40
  },
39
41
  "peerDependencies": {
40
42
  "@playwright/test": ">=1.40.0"