@percy/dom 1.19.1-alpha.0 → 1.19.2

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 (3) hide show
  1. package/README.md +1 -0
  2. package/dist/bundle.js +146 -149
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -37,6 +37,7 @@ const domSnapshot = await page.evaluate(() => PercyDOM.serialize(options))
37
37
 
38
38
  - `enableJavaScript` — When true, does not serialize some DOM elements
39
39
  - `domTransformation` — Function to transform the DOM after serialization
40
+ - `disableShadowDOM` — disable shadow DOM capturing, this option can be passed to `percySnapshot` its part of per-snapshot config.
40
41
 
41
42
  ## Serialized Content
42
43
 
package/dist/bundle.js CHANGED
@@ -10,7 +10,8 @@
10
10
  function serializeInputElements(_ref) {
11
11
  let {
12
12
  dom,
13
- clone
13
+ clone,
14
+ warnings
14
15
  } = _ref;
15
16
  for (let elem of dom.querySelectorAll('input, textarea, select')) {
16
17
  let inputId = elem.getAttribute('data-percy-element-id');
@@ -39,23 +40,12 @@
39
40
  cloneEl.setAttribute('value', elem.value);
40
41
  }
41
42
  }
42
-
43
- // find inputs inside shadow host and recursively serialize them.
44
- for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) {
45
- let percyElementId = shadowHost.getAttribute('data-percy-element-id');
46
- let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`);
47
- if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) {
48
- serializeInputElements({
49
- dom: shadowHost.shadowRoot,
50
- clone: cloneShadowHost.shadowRoot
51
- });
52
- }
53
- }
54
43
  }
55
44
 
56
45
  // Adds a `<base>` element to the serialized iframe's `<head>`. This is necessary when
57
46
  // embedded documents are serialized and their contents become root-relative.
58
47
  function setBaseURI(dom) {
48
+ /* istanbul ignore if: sanity check */
59
49
  if (!new URL(dom.baseURI).hostname) return;
60
50
  let $base = document.createElement('base');
61
51
  $base.href = dom.baseURI;
@@ -69,7 +59,8 @@
69
59
  clone,
70
60
  warnings,
71
61
  resources,
72
- enableJavaScript
62
+ enableJavaScript,
63
+ disableShadowDOM
73
64
  } = _ref;
74
65
  for (let frame of dom.querySelectorAll('iframe')) {
75
66
  var _clone$head;
@@ -94,7 +85,8 @@
94
85
  let serialized = serializeDOM({
95
86
  domTransformation: setBaseURI,
96
87
  dom: frame.contentDocument,
97
- enableJavaScript
88
+ enableJavaScript,
89
+ disableShadowDOM
98
90
  });
99
91
 
100
92
  // append serialized warnings and resources
@@ -112,19 +104,61 @@
112
104
  cloneEl.remove();
113
105
  }
114
106
  }
107
+ }
115
108
 
116
- // find iframes inside shadow host and recursively serialize them.
117
- for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) {
118
- let percyElementId = shadowHost.getAttribute('data-percy-element-id');
119
- let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`);
120
- if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) {
121
- serializeFrames({
122
- dom: shadowHost.shadowRoot,
123
- clone: cloneShadowHost.shadowRoot,
124
- warnings,
125
- resources,
126
- enableJavaScript
127
- });
109
+ // Creates a resource object from an element's unique ID and data URL
110
+ function resourceFromDataURL(uid, dataURL) {
111
+ // split dataURL into desired parts
112
+ let [data, content] = dataURL.split(',');
113
+ let [, mimetype] = data.split(':');
114
+ [mimetype] = mimetype.split(';');
115
+
116
+ // build a URL for the serialized asset
117
+ let [, ext] = mimetype.split('/');
118
+ let path = `/__serialized__/${uid}.${ext}`;
119
+ let url = new URL(path, document.URL).toString();
120
+
121
+ // return the url, base64 content, and mimetype
122
+ return {
123
+ url,
124
+ content,
125
+ mimetype
126
+ };
127
+ }
128
+ function resourceFromText(uid, mimetype, data) {
129
+ // build a URL for the serialized asset
130
+ let [, ext] = mimetype.split('/');
131
+ let path = `/__serialized__/${uid}.${ext}`;
132
+ let url = new URL(path, document.URL).toString();
133
+ // converts text to base64
134
+ let content = window.btoa(data);
135
+
136
+ // return the url, base64 content, and mimetype
137
+ return {
138
+ url,
139
+ content,
140
+ mimetype
141
+ };
142
+ }
143
+
144
+ // Returns a mostly random uid.
145
+ function uid() {
146
+ return `_${Math.random().toString(36).substr(2, 9)}`;
147
+ }
148
+ function markElement(domElement, disableShadowDOM) {
149
+ var _domElement$tagName;
150
+ // Mark elements that are to be serialized later with a data attribute.
151
+ if (['input', 'textarea', 'select', 'iframe', 'canvas', 'video', 'style'].includes((_domElement$tagName = domElement.tagName) === null || _domElement$tagName === void 0 ? void 0 : _domElement$tagName.toLowerCase())) {
152
+ if (!domElement.getAttribute('data-percy-element-id')) {
153
+ domElement.setAttribute('data-percy-element-id', uid());
154
+ }
155
+ }
156
+
157
+ // add special marker for shadow host
158
+ if (!disableShadowDOM && domElement.shadowRoot) {
159
+ domElement.setAttribute('data-percy-shadow-host', '');
160
+ if (!domElement.getAttribute('data-percy-element-id')) {
161
+ domElement.setAttribute('data-percy-element-id', uid());
128
162
  }
129
163
  }
130
164
  }
@@ -145,24 +179,35 @@
145
179
  }
146
180
  return true;
147
181
  }
148
-
149
- // Outputs in-memory CSSOM into their respective DOM nodes.
182
+ function styleSheetFromNode(node) {
183
+ /* istanbul ignore if: sanity check */
184
+ if (node.sheet) return node.sheet;
185
+
186
+ // Cloned style nodes don't have a sheet instance unless they are within
187
+ // a document; we get it by temporarily adding the rules to DOM
188
+ const tempStyle = document.createElement('style');
189
+ tempStyle.setAttribute('data-percy-style-helper', '');
190
+ tempStyle.innerHTML = node.innerHTML;
191
+ const clone = document.cloneNode();
192
+ clone.appendChild(tempStyle);
193
+ const sheet = tempStyle.sheet;
194
+ // Cleanup node
195
+ tempStyle.remove();
196
+ return sheet;
197
+ }
150
198
  function serializeCSSOM(_ref) {
151
199
  let {
152
200
  dom,
153
201
  clone,
154
- warnings
202
+ resources,
203
+ cache
155
204
  } = _ref;
205
+ // in-memory CSSOM into their respective DOM nodes.
156
206
  for (let styleSheet of dom.styleSheets) {
157
207
  if (isCSSOM(styleSheet)) {
158
208
  let styleId = styleSheet.ownerNode.getAttribute('data-percy-element-id');
159
- if (!styleId) {
160
- let attributes = Array.from(styleSheet.ownerNode.attributes).map(attr => `${attr.name}: ${attr.value}`);
161
- warnings.add(`stylesheet with attributes - [ ${attributes} ] - was not serialized`);
162
- continue;
163
- }
164
209
  let cloneOwnerNode = clone.querySelector(`[data-percy-element-id="${styleId}"]`);
165
- if (styleSheetsMatch(styleSheet, cloneOwnerNode.sheet)) continue;
210
+ if (styleSheetsMatch(styleSheet, styleSheetFromNode(cloneOwnerNode))) continue;
166
211
  let style = document.createElement('style');
167
212
  style.type = 'text/css';
168
213
  style.setAttribute('data-percy-element-id', styleId);
@@ -173,39 +218,30 @@
173
218
  }
174
219
  }
175
220
 
176
- // find stylesheets inside shadow host and recursively serialize them.
177
- for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) {
178
- let percyElementId = shadowHost.getAttribute('data-percy-element-id');
179
- let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`);
180
- if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) {
181
- serializeCSSOM({
182
- dom: shadowHost.shadowRoot,
183
- clone: cloneShadowHost.shadowRoot
184
- });
221
+ // clone Adopted Stylesheets
222
+ // Regarding ordering of the adopted stylesheets - https://github.com/WICG/construct-stylesheets/issues/93
223
+ for (let sheet of dom.adoptedStyleSheets) {
224
+ const styleLink = document.createElement('link');
225
+ styleLink.setAttribute('rel', 'stylesheet');
226
+ if (!cache.has(sheet)) {
227
+ const styles = Array.from(sheet.cssRules).map(cssRule => cssRule.cssText).join('\n');
228
+ let resource = resourceFromText(uid(), 'text/css', styles);
229
+ resources.add(resource);
230
+ cache.set(sheet, resource.url);
231
+ }
232
+ styleLink.setAttribute('data-percy-adopted-stylesheets-serialized', 'true');
233
+ styleLink.setAttribute('data-percy-serialized-attribute-href', cache.get(sheet));
234
+
235
+ /* istanbul ignore next: tested, but coverage is stripped */
236
+ if (clone.constructor.name === 'HTMLDocument' || clone.constructor.name === 'DocumentFragment') {
237
+ // handle document and iframe
238
+ clone.body.prepend(styleLink);
239
+ } else if (clone.constructor.name === 'ShadowRoot') {
240
+ clone.prepend(styleLink);
185
241
  }
186
242
  }
187
243
  }
188
244
 
189
- // Creates a resource object from an element's unique ID and data URL
190
- function resourceFromDataURL(uid, dataURL) {
191
- // split dataURL into desired parts
192
- let [data, content] = dataURL.split(',');
193
- let [, mimetype] = data.split(':');
194
- [mimetype] = mimetype.split(';');
195
-
196
- // build a URL for the serialized asset
197
- let [, ext] = mimetype.split('/');
198
- let path = `/__serialized__/${uid}.${ext}`;
199
- let url = new URL(path, document.URL).toString();
200
-
201
- // return the url, base64 content, and mimetype
202
- return {
203
- url,
204
- content,
205
- mimetype
206
- };
207
- }
208
-
209
245
  // Serialize in-memory canvas elements into images.
210
246
  function serializeCanvas(_ref) {
211
247
  let {
@@ -228,7 +264,6 @@
228
264
  resources.add(resource);
229
265
 
230
266
  // create an image element in the cloned dom
231
- // TODO: this works, verify if this is fine?
232
267
  let img = document.createElement('img');
233
268
  // use a data attribute to avoid making a real request
234
269
  img.setAttribute('data-percy-serialized-attribute-src', resource.url);
@@ -257,19 +292,6 @@
257
292
  }
258
293
  cloneEl.remove();
259
294
  }
260
-
261
- // find canvas inside shadow host and recursively serialize them.
262
- for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) {
263
- let percyElementId = shadowHost.getAttribute('data-percy-element-id');
264
- let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`);
265
- if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) {
266
- serializeCanvas({
267
- dom: shadowHost.shadowRoot,
268
- clone: cloneShadowHost.shadowRoot,
269
- resources
270
- });
271
- }
272
- }
273
295
  }
274
296
 
275
297
  // Captures the current frame of videos and sets the poster image
@@ -306,44 +328,6 @@
306
328
  // use a data attribute to avoid making a real request
307
329
  cloneEl.setAttribute('data-percy-serialized-attribute-poster', resource.url);
308
330
  }
309
-
310
- // find video inside shadow host and recursively serialize them.
311
- for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) {
312
- let percyElementId = shadowHost.getAttribute('data-percy-element-id');
313
- let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`);
314
- if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) {
315
- serializeVideos({
316
- dom: shadowHost.shadowRoot,
317
- clone: cloneShadowHost.shadowRoot,
318
- resources,
319
- warnings
320
- });
321
- }
322
- }
323
- }
324
-
325
- // Returns a mostly random uid.
326
- function uid() {
327
- return `_${Math.random().toString(36).substr(2, 9)}`;
328
- }
329
- function markElement(domElement) {
330
- var _domElement$tagName;
331
- // Mark elements that are to be serialized later with a data attribute.
332
- if (['input', 'textarea', 'select', 'iframe', 'canvas', 'video', 'style'].includes((_domElement$tagName = domElement.tagName) === null || _domElement$tagName === void 0 ? void 0 : _domElement$tagName.toLowerCase())) {
333
- if (!domElement.getAttribute('data-percy-element-id')) {
334
- domElement.setAttribute('data-percy-element-id', uid());
335
- }
336
- }
337
-
338
- // add special marker for shadow host
339
- if (domElement.shadowRoot) {
340
- if (!domElement.getAttribute('data-percy-shadow-host')) {
341
- domElement.setAttribute('data-percy-shadow-host', '');
342
- }
343
- if (!domElement.getAttribute('data-percy-element-id')) {
344
- domElement.setAttribute('data-percy-element-id', uid());
345
- }
346
- }
347
331
  }
348
332
 
349
333
  /**
@@ -353,7 +337,7 @@
353
337
  */
354
338
 
355
339
  // returns document fragment
356
- const deepClone = host => {
340
+ const deepClone = (host, disableShadowDOM) => {
357
341
  // clones shadow DOM and light DOM for a given node
358
342
  let cloneNode = (node, parent) => {
359
343
  let walkTree = (nextn, nextp) => {
@@ -364,12 +348,12 @@
364
348
  };
365
349
 
366
350
  // mark the node before cloning
367
- markElement(node);
351
+ markElement(node, disableShadowDOM);
368
352
  let clone = node.cloneNode();
369
353
  parent.appendChild(clone);
370
354
 
371
355
  // clone shadow DOM
372
- if (node.shadowRoot) {
356
+ if (node.shadowRoot && !disableShadowDOM) {
373
357
  // create shadowRoot
374
358
  if (clone.shadowRoot) {
375
359
  // it may be set up in a custom element's constructor
@@ -379,15 +363,6 @@
379
363
  mode: 'open'
380
364
  });
381
365
  }
382
-
383
- // clone stylesheets in shadowRoot
384
- for (let sheet of node.shadowRoot.adoptedStyleSheets) {
385
- let cssText = Array.from(sheet.rules).map(rule => rule.cssText).join('\n');
386
- let style = document.createElement('style');
387
- style.appendChild(document.createTextNode(cssText));
388
- clone.shadowRoot.prepend(style);
389
- }
390
-
391
366
  // clone dom elements
392
367
  walkTree(node.shadowRoot.firstChild, clone.shadowRoot);
393
368
  }
@@ -403,13 +378,12 @@
403
378
  /**
404
379
  * Deep clone a document while also preserving shadow roots and converting adoptedStylesheets to <style> tags.
405
380
  */
406
- const cloneNodeAndShadow = doc => {
407
- let mockDocumentFragment = deepClone(doc.documentElement);
408
- // convert document fragment to document object
409
- let cloneDocument = doc.cloneNode();
410
- // dissolve document fragment in clone document
411
- cloneDocument.appendChild(mockDocumentFragment);
412
- return cloneDocument;
381
+ const cloneNodeAndShadow = ctx => {
382
+ let cloneDocumentFragment = deepClone(ctx.dom.documentElement, ctx.disableShadowDOM);
383
+ cloneDocumentFragment.documentElement = cloneDocumentFragment.firstChild;
384
+ cloneDocumentFragment.head = cloneDocumentFragment.querySelector('head');
385
+ cloneDocumentFragment.body = cloneDocumentFragment.querySelector('body');
386
+ return cloneDocumentFragment;
413
387
  };
414
388
 
415
389
  /**
@@ -432,7 +406,7 @@
432
406
  // Since only chromium currently supports declarative shadow DOM - https://caniuse.com/declarative-shadow-dom
433
407
  function injectDeclarativeShadowDOMPolyfill(ctx) {
434
408
  let clone = ctx.clone;
435
- let scriptEl = clone.createElement('script');
409
+ let scriptEl = document.createElement('script');
436
410
  scriptEl.setAttribute('id', '__percy_shadowdom_helper');
437
411
  scriptEl.setAttribute('data-percy-injected', true);
438
412
  scriptEl.innerHTML = `
@@ -453,7 +427,9 @@
453
427
  document.addEventListener("DOMContentLoaded", () => reversePolyFill());
454
428
  }
455
429
  `.replace(/(\n|\s{2}|\t)/g, '');
456
- clone.body.appendChild(scriptEl);
430
+
431
+ // run polyfill as first thing post dom content is loaded
432
+ clone.head.prepend(scriptEl);
457
433
  }
458
434
 
459
435
  // Returns a copy or new doctype for a document.
@@ -482,6 +458,28 @@
482
458
  // include the doctype with the html string
483
459
  return doctype(ctx.dom) + html;
484
460
  }
461
+ function serializeElements(ctx) {
462
+ serializeInputElements(ctx);
463
+ serializeFrames(ctx);
464
+ serializeVideos(ctx);
465
+ if (!ctx.enableJavaScript) {
466
+ serializeCSSOM(ctx);
467
+ serializeCanvas(ctx);
468
+ }
469
+ for (const shadowHost of ctx.dom.querySelectorAll('[data-percy-shadow-host]')) {
470
+ let percyElementId = shadowHost.getAttribute('data-percy-element-id');
471
+ let cloneShadowHost = ctx.clone.querySelector(`[data-percy-element-id="${percyElementId}"]`);
472
+ if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) {
473
+ serializeElements({
474
+ ...ctx,
475
+ dom: shadowHost.shadowRoot,
476
+ clone: cloneShadowHost.shadowRoot
477
+ });
478
+ } else {
479
+ ctx.warnings.add('data-percy-shadow-host does not have shadowRoot');
480
+ }
481
+ }
482
+ }
485
483
 
486
484
  // Serializes a document and returns the resulting DOM string.
487
485
  function serializeDOM(options) {
@@ -490,24 +488,21 @@
490
488
  // allow snake_case or camelCase
491
489
  enableJavaScript = options === null || options === void 0 ? void 0 : options.enable_javascript,
492
490
  domTransformation = options === null || options === void 0 ? void 0 : options.dom_transformation,
493
- stringifyResponse = options === null || options === void 0 ? void 0 : options.stringify_response
491
+ stringifyResponse = options === null || options === void 0 ? void 0 : options.stringify_response,
492
+ disableShadowDOM = options === null || options === void 0 ? void 0 : options.disable_shadow_dom
494
493
  } = options || {};
495
494
 
496
495
  // keep certain records throughout serialization
497
496
  let ctx = {
498
497
  resources: new Set(),
499
498
  warnings: new Set(),
500
- enableJavaScript
499
+ cache: new Map(),
500
+ enableJavaScript,
501
+ disableShadowDOM
501
502
  };
502
503
  ctx.dom = dom;
503
- ctx.clone = cloneNodeAndShadow(ctx.dom);
504
- serializeInputElements(ctx);
505
- serializeFrames(ctx);
506
- serializeVideos(ctx);
507
- if (!enableJavaScript) {
508
- serializeCSSOM(ctx);
509
- serializeCanvas(ctx);
510
- }
504
+ ctx.clone = cloneNodeAndShadow(ctx);
505
+ serializeElements(ctx);
511
506
  if (domTransformation) {
512
507
  try {
513
508
  domTransformation(ctx.clone.documentElement);
@@ -515,7 +510,9 @@
515
510
  console.error('Could not transform the dom:', err.message);
516
511
  }
517
512
  }
518
- injectDeclarativeShadowDOMPolyfill(ctx);
513
+ if (!disableShadowDOM) {
514
+ injectDeclarativeShadowDOMPolyfill(ctx);
515
+ }
519
516
  let result = {
520
517
  html: serializeHTML(ctx),
521
518
  warnings: Array.from(ctx.warnings),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/dom",
3
- "version": "1.19.1-alpha.0",
3
+ "version": "1.19.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,5 +34,5 @@
34
34
  "devDependencies": {
35
35
  "interactor.js": "^2.0.0-beta.10"
36
36
  },
37
- "gitHead": "1a95e827ef2fb7e6f2c7a7a5a90d265185f31fc2"
37
+ "gitHead": "609ae53aa84ab2f7054a21214b36dccd71025780"
38
38
  }