@percy/playwright 1.0.9 → 1.0.10-alpha.0

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 +146 -1
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -8,6 +8,110 @@ const CLIENT_INFO = `${sdkPkg.name}/${sdkPkg.version}`;
8
8
  const ENV_INFO = `${playwrightPkg.name}/${playwrightPkg.version}`;
9
9
  const log = utils.logger('playwright');
10
10
 
11
+ // This function is executed in the browser context to handle dynamic resources.
12
+ /* istanbul ignore next: browser-executed function difficult to instrument */
13
+ const handleDynamicResources = async () => {
14
+ // Handle lazy-loaded images
15
+ document.querySelectorAll('img').forEach(img => {
16
+ const dataSrc = img.getAttribute('data-src');
17
+ if (dataSrc) {
18
+ try {
19
+ // Only allow http, https, data, or blob URLs
20
+ const url = new URL(dataSrc, window.location.origin);
21
+ if (
22
+ url.protocol === 'http:' ||
23
+ url.protocol === 'https:' ||
24
+ url.protocol === 'data:' ||
25
+ url.protocol === 'blob:'
26
+ ) {
27
+ img.src = url.href;
28
+ } else {
29
+ // Ignored unsafe data-src value
30
+ }
31
+ } catch (e) {
32
+ // If dataSrc is not a valid URL, ignore it
33
+ }
34
+ }
35
+ });
36
+
37
+ // Handle blob background images
38
+ const elements = Array.from(document.querySelectorAll('*'));
39
+ const promises = [];
40
+
41
+ for (const el of elements) {
42
+ const style = window.getComputedStyle(el);
43
+ const backgroundImage = style.getPropertyValue('background-image');
44
+
45
+ if (backgroundImage && backgroundImage.includes('blob:')) {
46
+ const blobUrlMatch = backgroundImage.match(/url\("?(blob:.+?)"?\)/);
47
+ if (blobUrlMatch && blobUrlMatch[1]) {
48
+ const blobUrl = blobUrlMatch[1];
49
+
50
+ /* eslint-disable-next-line no-undef */
51
+ const promise = fetch(blobUrl)
52
+ .then(res => res.blob())
53
+ .then(blob => new Promise((resolve, reject) => {
54
+ /* eslint-disable-next-line no-undef */
55
+ const reader = new FileReader();
56
+ reader.onloadend = () => {
57
+ el.style.backgroundImage = style.getPropertyValue('background-image').replace(blobUrl, reader.result);
58
+ resolve();
59
+ };
60
+ reader.onerror = reject;
61
+ reader.readAsDataURL(blob);
62
+ }))
63
+ .catch(err => {
64
+ log.warn(`Failed to process blob URL "${blobUrl}": ${err.message}`);
65
+ // Silently handle errors
66
+ });
67
+ promises.push(promise);
68
+ }
69
+ }
70
+ }
71
+ await Promise.all(promises);
72
+ };
73
+
74
+ // Processes a single cross-origin frame to capture its snapshot and resources.
75
+ async function processFrame(page, frame, options, percyDOM) {
76
+ const frameUrl = frame.url();
77
+
78
+ // Execute pre-serialization transformations in the iframe
79
+ /* istanbul ignore next: browser-executed function call */
80
+ await frame.evaluate(handleDynamicResources);
81
+
82
+ /* istanbul ignore next: browser-executed iframe serialization */
83
+ const iframeSnapshot = await frame.evaluate((opts) => {
84
+ /* eslint-disable-next-line no-undef */
85
+ return PercyDOM.serialize(opts);
86
+ }, { ...options, enableJavascript: true });
87
+
88
+ // Create a new resource for the iframe's HTML
89
+ const iframeResource = {
90
+ url: frameUrl,
91
+ content: iframeSnapshot.html,
92
+ mimetype: 'text/html'
93
+ };
94
+
95
+ // Get the iframe's element data from the main page context
96
+ /* istanbul ignore next: browser-executed evaluation function */
97
+ const iframeData = await page.evaluate((fUrl) => {
98
+ const iframes = Array.from(document.querySelectorAll('iframe'));
99
+ const matchingIframe = iframes.find(iframe => iframe.src.startsWith(fUrl));
100
+ if (matchingIframe) {
101
+ return {
102
+ percyElementId: matchingIframe.getAttribute('data-percy-element-id')
103
+ };
104
+ }
105
+ }, frameUrl);
106
+
107
+ return {
108
+ iframeData,
109
+ iframeResource,
110
+ iframeSnapshot,
111
+ frameUrl
112
+ };
113
+ }
114
+
11
115
  // Take a DOM snapshot and post it to the snapshot endpoint
12
116
  const percySnapshot = async function(page, name, options) {
13
117
  if (!page) throw new Error('A Playwright `page` object is required.');
@@ -16,7 +120,16 @@ const percySnapshot = async function(page, name, options) {
16
120
 
17
121
  try {
18
122
  // Inject the DOM serialization script
19
- await page.evaluate(await utils.fetchPercyDOM());
123
+ const percyDOM = await utils.fetchPercyDOM();
124
+ await page.evaluate(percyDOM);
125
+
126
+ // Execute pre-serialization transformations on the main page
127
+ try {
128
+ /* istanbul ignore next: browser-executed function call */
129
+ await page.evaluate(handleDynamicResources);
130
+ } catch (error) {
131
+ // Silently handle any errors from handleDynamicResources to not disrupt snapshots
132
+ }
20
133
 
21
134
  // Serialize and capture the DOM
22
135
  /* istanbul ignore next: no instrumenting injected code */
@@ -25,6 +138,38 @@ const percySnapshot = async function(page, name, options) {
25
138
  return PercyDOM.serialize(options);
26
139
  }, options);
27
140
 
141
+ // Process CORS IFrames
142
+ const pageUrl = new URL(page.url());
143
+ const crossOriginFrames = page.frames()
144
+ .filter(frame => frame.url() !== 'about:blank' && new URL(frame.url()).origin !== pageUrl.origin);
145
+
146
+ // Inject Percy DOM into all cross-origin frames before processing them in parallel
147
+ await Promise.all(crossOriginFrames.map(frame => frame.evaluate(percyDOM)));
148
+
149
+ const processedFrames = await Promise.all(
150
+ crossOriginFrames.map(frame => processFrame(page, frame, options, percyDOM))
151
+ );
152
+
153
+ for (const { iframeData, iframeResource, iframeSnapshot, frameUrl } of processedFrames) {
154
+ // Add the iframe's own resources to the main snapshot
155
+ domSnapshot.resources.push(...iframeSnapshot.resources);
156
+ // Add the iframe HTML resource itself
157
+ domSnapshot.resources.push(iframeResource);
158
+
159
+ if (iframeData && iframeData.percyElementId) {
160
+ const regex = new RegExp(`(<iframe[^>]*data-percy-element-id=["']${iframeData.percyElementId}["'][^>]*>)`);
161
+ const match = domSnapshot.html.match(regex);
162
+
163
+ /* istanbul ignore next: iframe matching logic depends on DOM structure */
164
+ if (match) {
165
+ const iframeTag = match[1];
166
+ // Replace the original iframe tag with one that points to the new resource.
167
+ const newIframeTag = iframeTag.replace(/src="[^"]*"/i, `src="${frameUrl}"`);
168
+ domSnapshot.html = domSnapshot.html.replace(iframeTag, newIframeTag);
169
+ }
170
+ }
171
+ }
172
+
28
173
  domSnapshot.cookies = await page.context().cookies();
29
174
 
30
175
  // Post the DOM to the snapshot endpoint with snapshot options and other info
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.0.9",
4
+ "version": "1.0.10-alpha.0",
5
5
  "license": "MIT",
6
6
  "author": "Perceptual Inc.",
7
7
  "repository": "https://github.com/percy/percy-playwright",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public",
32
- "tag": "latest"
32
+ "tag": "alpha"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "playwright-core": ">=1"