@percy/playwright 1.1.0-beta.0 → 1.1.0-beta.1

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.
Files changed (2) hide show
  1. package/index.js +145 -59
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -20,13 +20,6 @@ async function processFrame(page, frame, options, percyDOM) {
20
20
  return PercyDOM.serialize(opts);
21
21
  }, { ...options, enableJavascript: true });
22
22
 
23
- // Create a new resource for the iframe's HTML
24
- const iframeResource = {
25
- url: frameUrl,
26
- content: iframeSnapshot.html,
27
- mimetype: 'text/html'
28
- };
29
-
30
23
  // Get the iframe's element data from the main page context
31
24
  /* istanbul ignore next: browser-executed evaluation function */
32
25
  const iframeData = await page.evaluate((fUrl) => {
@@ -41,12 +34,155 @@ async function processFrame(page, frame, options, percyDOM) {
41
34
 
42
35
  return {
43
36
  iframeData,
44
- iframeResource,
45
37
  iframeSnapshot,
46
38
  frameUrl
47
39
  };
48
40
  }
49
41
 
42
+ async function captureSerializedDOM(page, options, percyDOM) {
43
+ /* istanbul ignore next: no instrumenting injected code */
44
+ let domSnapshot = await page.evaluate((options) => {
45
+ /* eslint-disable-next-line no-undef */
46
+ return PercyDOM.serialize(options);
47
+ }, options);
48
+
49
+ // Process CORS IFrames
50
+ // Note: Blob URL handling (data-src images, blob background images) is now handled
51
+ // in the CLI via async DOM serialization. See: percy/cli packages/dom/src/serialize-blob-urls.js
52
+ // This section only handles cross-origin iframe serialization and resource merging.
53
+ const pageUrl = new URL(page.url());
54
+ const crossOriginFrames = page.frames()
55
+ .filter(frame => {
56
+ const frameUrl = frame.url();
57
+ if (!frameUrl || frameUrl === 'about:blank') return false;
58
+ try {
59
+ return new URL(frameUrl).origin !== pageUrl.origin;
60
+ } catch {
61
+ return false;
62
+ }
63
+ });
64
+
65
+ // Inject Percy DOM into all cross-origin frames before processing them in parallel
66
+ await Promise.all(crossOriginFrames.map(frame => frame.evaluate(percyDOM)));
67
+
68
+ const processedFrames = await Promise.all(
69
+ crossOriginFrames.map(frame => processFrame(page, frame, options, percyDOM))
70
+ );
71
+ domSnapshot.corsIframes = processedFrames;
72
+ domSnapshot.cookies = await page.context().cookies();
73
+ return domSnapshot;
74
+ }
75
+
76
+ async function changeViewportAndWait(page, width, height, resizeCount) {
77
+ try {
78
+ await page.setViewportSize({ width, height });
79
+ } catch (error) {
80
+ log.debug(`Resizing using setViewportSize failed for width ${width}`, error);
81
+ return;
82
+ }
83
+
84
+ try {
85
+ /* istanbul ignore next: no instrumenting injected code */
86
+ await page.waitForFunction((count) => window.resizeCount === count, resizeCount, { timeout: 1000 });
87
+ } catch (error) {
88
+ log.debug(`Timed out waiting for window resize event for width ${width}`, error);
89
+ }
90
+ }
91
+
92
+ function isResponsiveDOMCaptureValid(options) {
93
+ if (utils.percy?.config?.percy?.deferUploads) {
94
+ return false;
95
+ }
96
+ return (
97
+ options?.responsive_snapshot_capture ||
98
+ options?.responsiveSnapshotCapture ||
99
+ utils.percy?.config?.snapshot?.responsiveSnapshotCapture ||
100
+ false
101
+ );
102
+ }
103
+
104
+ async function captureResponsiveDOM(page, options, percyDOM) {
105
+ const domSnapshots = [];
106
+ /* istanbul ignore next: no instrumenting injected code */
107
+ const currentViewport = page.viewportSize() || await page.evaluate(() => ({
108
+ width: window.innerWidth,
109
+ height: window.innerHeight
110
+ }));
111
+ let currentWidth = currentViewport.width;
112
+ let currentHeight = currentViewport.height;
113
+ let lastWindowWidth = currentWidth;
114
+ let resizeCount = 0;
115
+
116
+ /* istanbul ignore next: no instrumenting injected code */
117
+ await page.evaluate(() => {
118
+ /* eslint-disable-next-line no-undef */
119
+ PercyDOM.waitForResize();
120
+ });
121
+
122
+ // Calculate default height for non-mobile widths
123
+ let defaultHeight = currentHeight;
124
+ if (process.env.PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT?.toLowerCase() === 'true') {
125
+ if (options.minHeight) {
126
+ defaultHeight = options.minHeight;
127
+ } else {
128
+ const configMinHeight = utils.percy?.config?.snapshot?.minHeight;
129
+ /* istanbul ignore else: CLI always provides default value for config.snapshot */
130
+ if (configMinHeight) {
131
+ defaultHeight = configMinHeight;
132
+ }
133
+ }
134
+ }
135
+
136
+ // Get width and height combinations
137
+ /* istanbul ignore next: CLI version compatibility check */
138
+ if (!utils.getResponsiveWidths) {
139
+ throw new Error('Update Percy CLI to the latest version to use responsiveSnapshotCapture');
140
+ }
141
+ const widthHeights = await utils.getResponsiveWidths(options.widths || []);
142
+
143
+ try {
144
+ for (let { width, height } of widthHeights) {
145
+ height = height || defaultHeight;
146
+ if (lastWindowWidth !== width) {
147
+ resizeCount++;
148
+ await changeViewportAndWait(page, width, height, resizeCount);
149
+ lastWindowWidth = width;
150
+ }
151
+
152
+ if (process.env.PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE?.toLowerCase() === 'true') {
153
+ await page.reload();
154
+ await page.evaluate(percyDOM);
155
+ /* istanbul ignore next: no instrumenting injected code */
156
+ await page.evaluate(() => {
157
+ /* eslint-disable-next-line no-undef */
158
+ PercyDOM.waitForResize();
159
+ });
160
+ resizeCount = 0; // Reset local counter to match window.resizeCount after reload
161
+ }
162
+
163
+ if (process.env.RESPONSIVE_CAPTURE_SLEEP_TIME) {
164
+ await new Promise(resolve => setTimeout(resolve, parseInt(process.env.RESPONSIVE_CAPTURE_SLEEP_TIME) * 1000));
165
+ }
166
+
167
+ let domSnapshot = await captureSerializedDOM(page, options, percyDOM);
168
+ domSnapshot.width = width;
169
+ domSnapshots.push(domSnapshot);
170
+ }
171
+ } finally {
172
+ await changeViewportAndWait(page, currentWidth, currentHeight, resizeCount + 1);
173
+ }
174
+ return domSnapshots;
175
+ }
176
+
177
+ async function captureDOM(page, options, percyDOM) {
178
+ const responsiveSnapshotCapture = isResponsiveDOMCaptureValid(options);
179
+ if (responsiveSnapshotCapture) {
180
+ return await captureResponsiveDOM(page, options, percyDOM);
181
+ } else {
182
+ return await captureSerializedDOM(page, options, percyDOM);
183
+ }
184
+ }
185
+
50
186
  // Take a DOM snapshot and post it to the snapshot endpoint
51
187
  const percySnapshot = async function(page, name, options) {
52
188
  if (!page) throw new Error('A Playwright `page` object is required.');
@@ -58,57 +194,7 @@ const percySnapshot = async function(page, name, options) {
58
194
  const percyDOM = await utils.fetchPercyDOM();
59
195
  await page.evaluate(percyDOM);
60
196
 
61
- // Serialize and capture the DOM
62
- /* istanbul ignore next: no instrumenting injected code */
63
- let domSnapshot = await page.evaluate((options) => {
64
- /* eslint-disable-next-line no-undef */
65
- return PercyDOM.serialize(options);
66
- }, options);
67
-
68
- // Process CORS IFrames
69
- // Note: Blob URL handling (data-src images, blob background images) is now handled
70
- // in the CLI via async DOM serialization. See: percy/cli packages/dom/src/serialize-blob-urls.js
71
- // This section only handles cross-origin iframe serialization and resource merging.
72
- const pageUrl = new URL(page.url());
73
- const crossOriginFrames = page.frames()
74
- .filter(frame => {
75
- const frameUrl = frame.url();
76
- if (!frameUrl || frameUrl === 'about:blank') return false;
77
- try {
78
- return new URL(frameUrl).origin !== pageUrl.origin;
79
- } catch {
80
- return false;
81
- }
82
- });
83
-
84
- // Inject Percy DOM into all cross-origin frames before processing them in parallel
85
- await Promise.all(crossOriginFrames.map(frame => frame.evaluate(percyDOM)));
86
-
87
- const processedFrames = await Promise.all(
88
- crossOriginFrames.map(frame => processFrame(page, frame, options, percyDOM))
89
- );
90
-
91
- for (const { iframeData, iframeResource, iframeSnapshot, frameUrl } of processedFrames) {
92
- // Add the iframe's own resources to the main snapshot
93
- domSnapshot.resources.push(...iframeSnapshot.resources);
94
- // Add the iframe HTML resource itself
95
- domSnapshot.resources.push(iframeResource);
96
-
97
- if (iframeData && iframeData.percyElementId) {
98
- const regex = new RegExp(`(<iframe[^>]*data-percy-element-id=["']${iframeData.percyElementId}["'][^>]*>)`);
99
- const match = domSnapshot.html.match(regex);
100
-
101
- /* istanbul ignore next: iframe matching logic depends on DOM structure */
102
- if (match) {
103
- const iframeTag = match[1];
104
- // Replace the original iframe tag with one that points to the new resource.
105
- const newIframeTag = iframeTag.replace(/src="[^"]*"/i, `src="${frameUrl}"`);
106
- domSnapshot.html = domSnapshot.html.replace(iframeTag, newIframeTag);
107
- }
108
- }
109
- }
110
-
111
- domSnapshot.cookies = await page.context().cookies();
197
+ let domSnapshot = await captureDOM(page, options || {}, percyDOM);
112
198
 
113
199
  // Post the DOM to the snapshot endpoint with snapshot options and other info
114
200
  const response = await utils.postSnapshot({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@percy/playwright",
3
3
  "description": "Playwright client library for visual testing with Percy",
4
- "version": "1.1.0-beta.0",
4
+ "version": "1.1.0-beta.1",
5
5
  "license": "MIT",
6
6
  "author": "Perceptual Inc.",
7
7
  "repository": "https://github.com/percy/percy-playwright",
@@ -35,7 +35,7 @@
35
35
  "playwright-core": ">=1"
36
36
  },
37
37
  "devDependencies": {
38
- "@percy/cli": "^1.30.9",
38
+ "@percy/cli": "1.31.10-alpha.0",
39
39
  "@playwright/test": "^1.24.2",
40
40
  "babel-eslint": "^10.1.0",
41
41
  "cross-env": "^7.0.2",