@lambdatest/smartui-cli 1.0.3 → 2.0.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.
- package/dist/dom-serializer.js +601 -0
- package/dist/index.cjs +689 -0
- package/package.json +29 -25
- package/src/capture.js +0 -15
- package/src/config.js +0 -79
- package/src/constant.js +0 -9
- package/src/index.js +0 -50
- package/src/validate.js +0 -134
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
(function (exports) {
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
// Translates JavaScript properties of inputs into DOM attributes.
|
|
6
|
+
function serializeInputElements(_ref) {
|
|
7
|
+
let {
|
|
8
|
+
dom,
|
|
9
|
+
clone,
|
|
10
|
+
warnings
|
|
11
|
+
} = _ref;
|
|
12
|
+
for (let elem of dom.querySelectorAll('input, textarea, select')) {
|
|
13
|
+
let inputId = elem.getAttribute('data-smartui-element-id');
|
|
14
|
+
let cloneEl = clone.querySelector(`[data-smartui-element-id="${inputId}"]`);
|
|
15
|
+
switch (elem.type) {
|
|
16
|
+
case 'checkbox':
|
|
17
|
+
case 'radio':
|
|
18
|
+
if (elem.checked) {
|
|
19
|
+
cloneEl.setAttribute('checked', '');
|
|
20
|
+
}
|
|
21
|
+
break;
|
|
22
|
+
case 'select-one':
|
|
23
|
+
if (elem.selectedIndex !== -1) {
|
|
24
|
+
cloneEl.options[elem.selectedIndex].setAttribute('selected', 'true');
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
case 'select-multiple':
|
|
28
|
+
for (let option of elem.selectedOptions) {
|
|
29
|
+
cloneEl.options[option.index].setAttribute('selected', 'true');
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
case 'textarea':
|
|
33
|
+
cloneEl.innerHTML = elem.value;
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
cloneEl.setAttribute('value', elem.value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Adds a `<base>` element to the serialized iframe's `<head>`. This is necessary when
|
|
42
|
+
// embedded documents are serialized and their contents become root-relative.
|
|
43
|
+
function setBaseURI(dom) {
|
|
44
|
+
/* istanbul ignore if: sanity check */
|
|
45
|
+
if (!new URL(dom.baseURI).hostname) return;
|
|
46
|
+
let $base = document.createElement('base');
|
|
47
|
+
$base.href = dom.baseURI;
|
|
48
|
+
dom.querySelector('head').prepend($base);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Recursively serializes iframe documents into srcdoc attributes.
|
|
52
|
+
function serializeFrames(_ref) {
|
|
53
|
+
let {
|
|
54
|
+
dom,
|
|
55
|
+
clone,
|
|
56
|
+
warnings,
|
|
57
|
+
resources,
|
|
58
|
+
enableJavaScript,
|
|
59
|
+
disableShadowDOM
|
|
60
|
+
} = _ref;
|
|
61
|
+
for (let frame of dom.querySelectorAll('iframe')) {
|
|
62
|
+
var _clone$head;
|
|
63
|
+
let smartuiElementId = frame.getAttribute('data-smartui-element-id');
|
|
64
|
+
let cloneEl = clone.querySelector(`[data-smartui-element-id="${smartuiElementId}"]`);
|
|
65
|
+
let builtWithJs = !frame.srcdoc && (!frame.src || frame.src.split(':')[0] === 'javascript');
|
|
66
|
+
|
|
67
|
+
// delete frames within the head since they usually break pages when
|
|
68
|
+
// rerendered and do not effect the visuals of a page
|
|
69
|
+
if ((_clone$head = clone.head) !== null && _clone$head !== void 0 && _clone$head.contains(cloneEl)) {
|
|
70
|
+
cloneEl.remove();
|
|
71
|
+
|
|
72
|
+
// if the frame document is accessible and not empty, we can serialize it
|
|
73
|
+
} else if (frame.contentDocument && frame.contentDocument.documentElement) {
|
|
74
|
+
// js is enabled and this frame was built with js, don't serialize it
|
|
75
|
+
if (enableJavaScript && builtWithJs) continue;
|
|
76
|
+
|
|
77
|
+
// the frame has yet to load and wasn't built with js, it is unsafe to serialize
|
|
78
|
+
if (!builtWithJs && !frame.contentWindow.performance.timing.loadEventEnd) continue;
|
|
79
|
+
|
|
80
|
+
// recersively serialize contents
|
|
81
|
+
let serialized = serializeDOM({
|
|
82
|
+
domTransformation: setBaseURI,
|
|
83
|
+
dom: frame.contentDocument,
|
|
84
|
+
enableJavaScript,
|
|
85
|
+
disableShadowDOM
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// append serialized warnings and resources
|
|
89
|
+
/* istanbul ignore next: warnings not implemented yet */
|
|
90
|
+
for (let w of serialized.warnings) warnings.add(w);
|
|
91
|
+
for (let r of serialized.resources) resources.add(r);
|
|
92
|
+
|
|
93
|
+
// assign serialized html to srcdoc and remove src
|
|
94
|
+
cloneEl.setAttribute('srcdoc', serialized.html);
|
|
95
|
+
cloneEl.removeAttribute('src');
|
|
96
|
+
|
|
97
|
+
// delete inaccessible frames built with js when js is disabled because they
|
|
98
|
+
// break asset discovery by creating non-captured requests that hang
|
|
99
|
+
} else if (!enableJavaScript && builtWithJs) {
|
|
100
|
+
cloneEl.remove();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Creates a resource object from an element's unique ID and data URL
|
|
106
|
+
function resourceFromDataURL(uid, dataURL) {
|
|
107
|
+
// split dataURL into desired parts
|
|
108
|
+
let [data, content] = dataURL.split(',');
|
|
109
|
+
let [, mimetype] = data.split(':');
|
|
110
|
+
[mimetype] = mimetype.split(';');
|
|
111
|
+
|
|
112
|
+
// build a URL for the serialized asset
|
|
113
|
+
let [, ext] = mimetype.split('/');
|
|
114
|
+
let path = `/__serialized__/${uid}.${ext}`;
|
|
115
|
+
let url = rewriteLocalhostURL(new URL(path, document.URL).toString());
|
|
116
|
+
|
|
117
|
+
// return the url, base64 content, and mimetype
|
|
118
|
+
return {
|
|
119
|
+
url,
|
|
120
|
+
content,
|
|
121
|
+
mimetype
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function resourceFromText(uid, mimetype, data) {
|
|
125
|
+
// build a URL for the serialized asset
|
|
126
|
+
let [, ext] = mimetype.split('/');
|
|
127
|
+
let path = `/__serialized__/${uid}.${ext}`;
|
|
128
|
+
let url = rewriteLocalhostURL(new URL(path, document.URL).toString());
|
|
129
|
+
// return the url, text content, and mimetype
|
|
130
|
+
return {
|
|
131
|
+
url,
|
|
132
|
+
content: data,
|
|
133
|
+
mimetype
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function styleSheetFromNode(node) {
|
|
137
|
+
/* istanbul ignore if: sanity check */
|
|
138
|
+
if (node.sheet) return node.sheet;
|
|
139
|
+
|
|
140
|
+
// Cloned style nodes don't have a sheet instance unless they are within
|
|
141
|
+
// a document; we get it by temporarily adding the rules to DOM
|
|
142
|
+
const tempStyle = node.cloneNode();
|
|
143
|
+
tempStyle.setAttribute('data-smartui-style-helper', '');
|
|
144
|
+
tempStyle.innerHTML = node.innerHTML;
|
|
145
|
+
const clone = document.cloneNode();
|
|
146
|
+
clone.appendChild(tempStyle);
|
|
147
|
+
const sheet = tempStyle.sheet;
|
|
148
|
+
// Cleanup node
|
|
149
|
+
tempStyle.remove();
|
|
150
|
+
return sheet;
|
|
151
|
+
}
|
|
152
|
+
function rewriteLocalhostURL(url) {
|
|
153
|
+
return url.replace(/(http[s]{0,1}:\/\/)localhost[:\d+]*/, '$1render.smartui.local');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Returns a mostly random uid.
|
|
157
|
+
function uid() {
|
|
158
|
+
return `_${Math.random().toString(36).substr(2, 9)}`;
|
|
159
|
+
}
|
|
160
|
+
function markElement(domElement, disableShadowDOM) {
|
|
161
|
+
var _domElement$tagName;
|
|
162
|
+
// Mark elements that are to be serialized later with a data attribute.
|
|
163
|
+
if (['input', 'textarea', 'select', 'iframe', 'canvas', 'video', 'style'].includes((_domElement$tagName = domElement.tagName) === null || _domElement$tagName === void 0 ? void 0 : _domElement$tagName.toLowerCase())) {
|
|
164
|
+
if (!domElement.getAttribute('data-smartui-element-id')) {
|
|
165
|
+
domElement.setAttribute('data-smartui-element-id', uid());
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// add special marker for shadow host
|
|
170
|
+
if (!disableShadowDOM && domElement.shadowRoot) {
|
|
171
|
+
domElement.setAttribute('data-smartui-shadow-host', '');
|
|
172
|
+
if (!domElement.getAttribute('data-smartui-element-id')) {
|
|
173
|
+
domElement.setAttribute('data-smartui-element-id', uid());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Returns true if a stylesheet is a CSSOM-based stylesheet.
|
|
179
|
+
function isCSSOM(styleSheet) {
|
|
180
|
+
// no href, has a rulesheet, and has an owner node
|
|
181
|
+
return !styleSheet.href && styleSheet.cssRules && styleSheet.ownerNode;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Returns false if any stylesheet rules do not match between two stylesheets
|
|
185
|
+
function styleSheetsMatch(sheetA, sheetB) {
|
|
186
|
+
for (let i = 0; i < sheetA.cssRules.length; i++) {
|
|
187
|
+
var _sheetB$cssRules$i;
|
|
188
|
+
let ruleA = sheetA.cssRules[i].cssText;
|
|
189
|
+
let ruleB = (_sheetB$cssRules$i = sheetB.cssRules[i]) === null || _sheetB$cssRules$i === void 0 ? void 0 : _sheetB$cssRules$i.cssText;
|
|
190
|
+
if (ruleA !== ruleB) return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
function createStyleResource(styleSheet) {
|
|
195
|
+
const styles = Array.from(styleSheet.cssRules).map(cssRule => cssRule.cssText).join('\n');
|
|
196
|
+
let resource = resourceFromText(uid(), 'text/css', styles);
|
|
197
|
+
return resource;
|
|
198
|
+
}
|
|
199
|
+
function serializeCSSOM(_ref) {
|
|
200
|
+
let {
|
|
201
|
+
dom,
|
|
202
|
+
clone,
|
|
203
|
+
resources,
|
|
204
|
+
cache,
|
|
205
|
+
warnings
|
|
206
|
+
} = _ref;
|
|
207
|
+
// in-memory CSSOM into their respective DOM nodes.
|
|
208
|
+
for (let styleSheet of dom.styleSheets) {
|
|
209
|
+
var _styleSheet$href;
|
|
210
|
+
if (isCSSOM(styleSheet)) {
|
|
211
|
+
let styleId = styleSheet.ownerNode.getAttribute('data-smartui-element-id');
|
|
212
|
+
let cloneOwnerNode = clone.querySelector(`[data-smartui-element-id="${styleId}"]`);
|
|
213
|
+
if (styleSheetsMatch(styleSheet, styleSheetFromNode(cloneOwnerNode))) continue;
|
|
214
|
+
let style = document.createElement('style');
|
|
215
|
+
style.type = 'text/css';
|
|
216
|
+
style.setAttribute('data-smartui-element-id', styleId);
|
|
217
|
+
style.setAttribute('data-smartui-cssom-serialized', 'true');
|
|
218
|
+
style.innerHTML = Array.from(styleSheet.cssRules).map(cssRule => cssRule.cssText).join('\n');
|
|
219
|
+
cloneOwnerNode.parentNode.insertBefore(style, cloneOwnerNode.nextSibling);
|
|
220
|
+
cloneOwnerNode.remove();
|
|
221
|
+
} else if ((_styleSheet$href = styleSheet.href) !== null && _styleSheet$href !== void 0 && _styleSheet$href.startsWith('blob:')) {
|
|
222
|
+
const styleLink = document.createElement('link');
|
|
223
|
+
styleLink.setAttribute('rel', 'stylesheet');
|
|
224
|
+
let resource = createStyleResource(styleSheet);
|
|
225
|
+
resources.add(resource);
|
|
226
|
+
styleLink.setAttribute('data-smartui-blob-stylesheets-serialized', 'true');
|
|
227
|
+
styleLink.setAttribute('data-smartui-serialized-attribute-href', resource.url);
|
|
228
|
+
|
|
229
|
+
/* istanbul ignore next: tested, but coverage is stripped */
|
|
230
|
+
if (clone.constructor.name === 'HTMLDocument' || clone.constructor.name === 'DocumentFragment') {
|
|
231
|
+
// handle document and iframe
|
|
232
|
+
clone.body.prepend(styleLink);
|
|
233
|
+
} else if (clone.constructor.name === 'ShadowRoot') {
|
|
234
|
+
clone.prepend(styleLink);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// clone Adopted Stylesheets
|
|
240
|
+
// Regarding ordering of the adopted stylesheets - https://github.com/WICG/construct-stylesheets/issues/93
|
|
241
|
+
/* istanbul ignore next: tested, but coverage is stripped */
|
|
242
|
+
if (dom.adoptedStyleSheets) {
|
|
243
|
+
for (let sheet of dom.adoptedStyleSheets) {
|
|
244
|
+
const styleLink = document.createElement('link');
|
|
245
|
+
styleLink.setAttribute('rel', 'stylesheet');
|
|
246
|
+
if (!cache.has(sheet)) {
|
|
247
|
+
let resource = createStyleResource(sheet);
|
|
248
|
+
resources.add(resource);
|
|
249
|
+
cache.set(sheet, resource.url);
|
|
250
|
+
}
|
|
251
|
+
styleLink.setAttribute('data-smartui-adopted-stylesheets-serialized', 'true');
|
|
252
|
+
styleLink.setAttribute('data-smartui-serialized-attribute-href', cache.get(sheet));
|
|
253
|
+
|
|
254
|
+
/* istanbul ignore next: tested, but coverage is stripped */
|
|
255
|
+
if (clone.constructor.name === 'HTMLDocument' || clone.constructor.name === 'DocumentFragment') {
|
|
256
|
+
// handle document and iframe
|
|
257
|
+
clone.body.prepend(styleLink);
|
|
258
|
+
} else if (clone.constructor.name === 'ShadowRoot') {
|
|
259
|
+
clone.prepend(styleLink);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
warnings.add('Skipping `adoptedStyleSheets` as it is not supported.');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Serialize in-memory canvas elements into images.
|
|
268
|
+
function serializeCanvas(_ref) {
|
|
269
|
+
let {
|
|
270
|
+
dom,
|
|
271
|
+
clone,
|
|
272
|
+
resources
|
|
273
|
+
} = _ref;
|
|
274
|
+
for (let canvas of dom.querySelectorAll('canvas')) {
|
|
275
|
+
// Note: the `.toDataURL` API requires WebGL canvas elements to use
|
|
276
|
+
// `preserveDrawingBuffer: true`. This is because `.toDataURL` uses the
|
|
277
|
+
// drawing buffer, which is cleared after each render for WebGL by default.
|
|
278
|
+
let dataUrl = canvas.toDataURL();
|
|
279
|
+
|
|
280
|
+
// skip empty canvases
|
|
281
|
+
if (!dataUrl || dataUrl === 'data:,') continue;
|
|
282
|
+
|
|
283
|
+
// get the element's smartui id and create a resource for it
|
|
284
|
+
let smartuiElementId = canvas.getAttribute('data-smartui-element-id');
|
|
285
|
+
let resource = resourceFromDataURL(smartuiElementId, dataUrl);
|
|
286
|
+
resources.add(resource);
|
|
287
|
+
|
|
288
|
+
// create an image element in the cloned dom
|
|
289
|
+
let img = document.createElement('img');
|
|
290
|
+
// use a data attribute to avoid making a real request
|
|
291
|
+
img.setAttribute('data-smartui-serialized-attribute-src', resource.url);
|
|
292
|
+
|
|
293
|
+
// copy canvas element attributes to the image element such as style, class,
|
|
294
|
+
// or data attributes that may be targeted by CSS
|
|
295
|
+
for (let {
|
|
296
|
+
name,
|
|
297
|
+
value
|
|
298
|
+
} of canvas.attributes) {
|
|
299
|
+
img.setAttribute(name, value);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// mark the image as serialized (can be targeted by CSS)
|
|
303
|
+
img.setAttribute('data-smartui-canvas-serialized', '');
|
|
304
|
+
// set a default max width to account for canvases that might resize with JS
|
|
305
|
+
img.style.maxWidth = img.style.maxWidth || '100%';
|
|
306
|
+
|
|
307
|
+
// insert the image into the cloned DOM and remove the cloned canvas element
|
|
308
|
+
let cloneEl = clone.querySelector(`[data-smartui-element-id=${smartuiElementId}]`);
|
|
309
|
+
// `parentElement` for elements directly under shadow root is `null` -> Incase of Nested Shadow DOM.
|
|
310
|
+
if (cloneEl.parentElement) {
|
|
311
|
+
cloneEl.parentElement.insertBefore(img, cloneEl);
|
|
312
|
+
} else {
|
|
313
|
+
clone.insertBefore(img, cloneEl);
|
|
314
|
+
}
|
|
315
|
+
cloneEl.remove();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Captures the current frame of videos and sets the poster image
|
|
320
|
+
function serializeVideos(_ref) {
|
|
321
|
+
let {
|
|
322
|
+
dom,
|
|
323
|
+
clone,
|
|
324
|
+
resources,
|
|
325
|
+
warnings
|
|
326
|
+
} = _ref;
|
|
327
|
+
for (let video of dom.querySelectorAll('video')) {
|
|
328
|
+
// if the video already has a poster image, no work for us to do
|
|
329
|
+
if (video.getAttribute('poster')) continue;
|
|
330
|
+
let videoId = video.getAttribute('data-smartui-element-id');
|
|
331
|
+
let cloneEl = clone.querySelector(`[data-smartui-element-id="${videoId}"]`);
|
|
332
|
+
let canvas = document.createElement('canvas');
|
|
333
|
+
let width = canvas.width = video.videoWidth;
|
|
334
|
+
let height = canvas.height = video.videoHeight;
|
|
335
|
+
let dataUrl;
|
|
336
|
+
canvas.getContext('2d').drawImage(video, 0, 0, width, height);
|
|
337
|
+
try {
|
|
338
|
+
dataUrl = canvas.toDataURL();
|
|
339
|
+
} catch (e) {
|
|
340
|
+
warnings.add(`data-smartui-element-id="${videoId}" : ${e.toString()}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// if the canvas produces a blank image, skip
|
|
344
|
+
if (!dataUrl || dataUrl === 'data:,') continue;
|
|
345
|
+
|
|
346
|
+
// create a resource from the serialized data url
|
|
347
|
+
let resource = resourceFromDataURL(videoId, dataUrl);
|
|
348
|
+
resources.add(resource);
|
|
349
|
+
|
|
350
|
+
// use a data attribute to avoid making a real request
|
|
351
|
+
cloneEl.setAttribute('data-smartui-serialized-attribute-poster', resource.url);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Drop loading attribute. We do not scroll page in discovery stage but we want to make sure that
|
|
356
|
+
// all resources are requested, so we drop loading attribute [as it can be set to lazy]
|
|
357
|
+
function dropLoadingAttribute(domElement) {
|
|
358
|
+
var _domElement$tagName;
|
|
359
|
+
if (!['img', 'iframe'].includes((_domElement$tagName = domElement.tagName) === null || _domElement$tagName === void 0 ? void 0 : _domElement$tagName.toLowerCase())) return;
|
|
360
|
+
domElement.removeAttribute('loading');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// All transformations that we need to apply for a successful discovery and stable render
|
|
364
|
+
function applyElementTransformations(domElement) {
|
|
365
|
+
dropLoadingAttribute(domElement);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Custom deep clone function that replaces smartui's current clone behavior.
|
|
370
|
+
* This enables us to capture shadow DOM in snapshots. It takes advantage of `attachShadow`'s mode option set to open
|
|
371
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Deep clone a document while also preserving shadow roots
|
|
376
|
+
* returns document fragment
|
|
377
|
+
*/
|
|
378
|
+
|
|
379
|
+
const ignoreTags = ['NOSCRIPT'];
|
|
380
|
+
function cloneNodeAndShadow(_ref) {
|
|
381
|
+
let {
|
|
382
|
+
dom,
|
|
383
|
+
disableShadowDOM
|
|
384
|
+
} = _ref;
|
|
385
|
+
// clones shadow DOM and light DOM for a given node
|
|
386
|
+
let cloneNode = (node, parent) => {
|
|
387
|
+
let walkTree = (nextn, nextp) => {
|
|
388
|
+
while (nextn) {
|
|
389
|
+
if (!ignoreTags.includes(nextn.nodeName)) {
|
|
390
|
+
cloneNode(nextn, nextp);
|
|
391
|
+
}
|
|
392
|
+
nextn = nextn.nextSibling;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// mark the node before cloning
|
|
397
|
+
markElement(node, disableShadowDOM);
|
|
398
|
+
let clone = node.cloneNode();
|
|
399
|
+
|
|
400
|
+
// We apply any element transformations here to avoid another treeWalk
|
|
401
|
+
applyElementTransformations(clone);
|
|
402
|
+
parent.appendChild(clone);
|
|
403
|
+
|
|
404
|
+
// shallow clone should not contain children
|
|
405
|
+
if (clone.children) {
|
|
406
|
+
Array.from(clone.children).forEach(child => clone.removeChild(child));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// clone shadow DOM
|
|
410
|
+
if (node.shadowRoot && !disableShadowDOM) {
|
|
411
|
+
// create shadowRoot
|
|
412
|
+
if (clone.shadowRoot) {
|
|
413
|
+
// it may be set up in a custom element's constructor
|
|
414
|
+
clone.shadowRoot.innerHTML = '';
|
|
415
|
+
} else {
|
|
416
|
+
clone.attachShadow({
|
|
417
|
+
mode: 'open'
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// clone dom elements
|
|
421
|
+
walkTree(node.shadowRoot.firstChild, clone.shadowRoot);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// clone light DOM
|
|
425
|
+
walkTree(node.firstChild, clone);
|
|
426
|
+
};
|
|
427
|
+
let fragment = dom.createDocumentFragment();
|
|
428
|
+
cloneNode(dom.documentElement, fragment);
|
|
429
|
+
fragment.documentElement = fragment.firstChild;
|
|
430
|
+
fragment.head = fragment.querySelector('head');
|
|
431
|
+
fragment.body = fragment.querySelector('body');
|
|
432
|
+
return fragment;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Use `getInnerHTML()` to serialize shadow dom as <template> tags. `innerHTML` and `outerHTML` don't do this. Buzzword: "declarative shadow dom"
|
|
437
|
+
*/
|
|
438
|
+
function getOuterHTML(docElement) {
|
|
439
|
+
// firefox doesn't serialize shadow DOM, we're awaiting API's by firefox to become ready and are not polyfilling it.
|
|
440
|
+
if (!docElement.getInnerHTML) {
|
|
441
|
+
return docElement.outerHTML;
|
|
442
|
+
}
|
|
443
|
+
// chromium gives us declarative shadow DOM serialization API
|
|
444
|
+
let innerHTML = docElement.getInnerHTML({
|
|
445
|
+
includeShadowRoots: true
|
|
446
|
+
});
|
|
447
|
+
docElement.textContent = '';
|
|
448
|
+
// Note: Here we are specifically passing replacer function to avoid any replacements due to
|
|
449
|
+
// special characters in client's dom like $&
|
|
450
|
+
return docElement.outerHTML.replace('</html>', () => `${innerHTML}</html>`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// we inject declarative shadow dom polyfill to allow shadow dom to load in non chromium infrastructure browsers
|
|
454
|
+
// Since only chromium currently supports declarative shadow DOM - https://caniuse.com/declarative-shadow-dom
|
|
455
|
+
function injectDeclarativeShadowDOMPolyfill(ctx) {
|
|
456
|
+
let clone = ctx.clone;
|
|
457
|
+
let scriptEl = document.createElement('script');
|
|
458
|
+
scriptEl.setAttribute('id', '__smartui_shadowdom_helper');
|
|
459
|
+
scriptEl.setAttribute('data-smartui-injected', true);
|
|
460
|
+
scriptEl.innerHTML = `
|
|
461
|
+
function reversePolyFill(root=document){
|
|
462
|
+
root.querySelectorAll('template[shadowroot]').forEach(template => {
|
|
463
|
+
const mode = template.getAttribute('shadowroot');
|
|
464
|
+
const shadowRoot = template.parentNode.attachShadow({ mode });
|
|
465
|
+
shadowRoot.appendChild(template.content);
|
|
466
|
+
template.remove();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
root.querySelectorAll('[data-smartui-shadow-host]').forEach(shadowHost => reversePolyFill(shadowHost.shadowRoot));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (["interactive", "complete"].includes(document.readyState)) {
|
|
473
|
+
reversePolyFill();
|
|
474
|
+
} else {
|
|
475
|
+
document.addEventListener("DOMContentLoaded", () => reversePolyFill());
|
|
476
|
+
}
|
|
477
|
+
`.replace(/(\n|\s{2}|\t)/g, '');
|
|
478
|
+
|
|
479
|
+
// run polyfill as first thing post dom content is loaded
|
|
480
|
+
clone.head.prepend(scriptEl);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Returns a copy or new doctype for a document.
|
|
484
|
+
function doctype(dom) {
|
|
485
|
+
let {
|
|
486
|
+
name = 'html',
|
|
487
|
+
publicId = '',
|
|
488
|
+
systemId = ''
|
|
489
|
+
} = (dom === null || dom === void 0 ? void 0 : dom.doctype) ?? {};
|
|
490
|
+
let deprecated = '';
|
|
491
|
+
if (publicId && systemId) {
|
|
492
|
+
deprecated = ` PUBLIC "${publicId}" "${systemId}"`;
|
|
493
|
+
} else if (publicId) {
|
|
494
|
+
deprecated = ` PUBLIC "${publicId}"`;
|
|
495
|
+
} else if (systemId) {
|
|
496
|
+
deprecated = ` SYSTEM "${systemId}"`;
|
|
497
|
+
}
|
|
498
|
+
return `<!DOCTYPE ${name}${deprecated}>`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Serializes and returns the cloned DOM as an HTML string
|
|
502
|
+
function serializeHTML(ctx) {
|
|
503
|
+
let html = getOuterHTML(ctx.clone.documentElement);
|
|
504
|
+
// replace serialized data attributes with real attributes
|
|
505
|
+
html = html.replace(/ data-smartui-serialized-attribute-(\w+?)=/ig, ' $1=');
|
|
506
|
+
// include the doctype with the html string
|
|
507
|
+
return doctype(ctx.dom) + html;
|
|
508
|
+
}
|
|
509
|
+
function serializeElements(ctx) {
|
|
510
|
+
serializeInputElements(ctx);
|
|
511
|
+
serializeFrames(ctx);
|
|
512
|
+
serializeVideos(ctx);
|
|
513
|
+
if (!ctx.enableJavaScript) {
|
|
514
|
+
serializeCSSOM(ctx);
|
|
515
|
+
serializeCanvas(ctx);
|
|
516
|
+
}
|
|
517
|
+
for (const shadowHost of ctx.dom.querySelectorAll('[data-smartui-shadow-host]')) {
|
|
518
|
+
let smartuiElementId = shadowHost.getAttribute('data-smartui-element-id');
|
|
519
|
+
let cloneShadowHost = ctx.clone.querySelector(`[data-smartui-element-id="${smartuiElementId}"]`);
|
|
520
|
+
if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) {
|
|
521
|
+
serializeElements({
|
|
522
|
+
...ctx,
|
|
523
|
+
dom: shadowHost.shadowRoot,
|
|
524
|
+
clone: cloneShadowHost.shadowRoot
|
|
525
|
+
});
|
|
526
|
+
} else {
|
|
527
|
+
ctx.warnings.add('data-smartui-shadow-host does not have shadowRoot');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Serializes a document and returns the resulting DOM string.
|
|
533
|
+
function serializeDOM(options) {
|
|
534
|
+
let {
|
|
535
|
+
dom = document,
|
|
536
|
+
// allow snake_case or camelCase
|
|
537
|
+
enableJavaScript = options === null || options === void 0 ? void 0 : options.enable_javascript,
|
|
538
|
+
domTransformation = options === null || options === void 0 ? void 0 : options.dom_transformation,
|
|
539
|
+
stringifyResponse = options === null || options === void 0 ? void 0 : options.stringify_response,
|
|
540
|
+
disableShadowDOM = options === null || options === void 0 ? void 0 : options.disable_shadow_dom,
|
|
541
|
+
reshuffleInvalidTags = options === null || options === void 0 ? void 0 : options.reshuffle_invalid_tags
|
|
542
|
+
} = options || {};
|
|
543
|
+
|
|
544
|
+
// keep certain records throughout serialization
|
|
545
|
+
let ctx = {
|
|
546
|
+
resources: new Set(),
|
|
547
|
+
warnings: new Set(),
|
|
548
|
+
hints: new Set(),
|
|
549
|
+
cache: new Map(),
|
|
550
|
+
enableJavaScript,
|
|
551
|
+
disableShadowDOM
|
|
552
|
+
};
|
|
553
|
+
ctx.dom = dom;
|
|
554
|
+
ctx.clone = cloneNodeAndShadow(ctx);
|
|
555
|
+
serializeElements(ctx);
|
|
556
|
+
if (domTransformation) {
|
|
557
|
+
try {
|
|
558
|
+
// eslint-disable-next-line no-eval
|
|
559
|
+
if (typeof domTransformation === 'string') domTransformation = window.eval(domTransformation);
|
|
560
|
+
domTransformation(ctx.clone.documentElement);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
let errorMessage = `Could not transform the dom: ${err.message}`;
|
|
563
|
+
ctx.warnings.add(errorMessage);
|
|
564
|
+
console.error(errorMessage);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (!disableShadowDOM) {
|
|
568
|
+
injectDeclarativeShadowDOMPolyfill(ctx);
|
|
569
|
+
}
|
|
570
|
+
if (reshuffleInvalidTags) {
|
|
571
|
+
let clonedBody = ctx.clone.body;
|
|
572
|
+
while (clonedBody.nextSibling) {
|
|
573
|
+
let sibling = clonedBody.nextSibling;
|
|
574
|
+
clonedBody.append(sibling);
|
|
575
|
+
}
|
|
576
|
+
} else if (ctx.clone.body.nextSibling) {
|
|
577
|
+
ctx.hints.add('DOM elements found outside </body>');
|
|
578
|
+
}
|
|
579
|
+
let result = {
|
|
580
|
+
html: serializeHTML(ctx),
|
|
581
|
+
warnings: Array.from(ctx.warnings),
|
|
582
|
+
resources: Array.from(ctx.resources),
|
|
583
|
+
hints: Array.from(ctx.hints)
|
|
584
|
+
};
|
|
585
|
+
return stringifyResponse ? JSON.stringify(result) : result;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
exports["default"] = serializeDOM;
|
|
589
|
+
exports.serialize = serializeDOM;
|
|
590
|
+
exports.serializeDOM = serializeDOM;
|
|
591
|
+
|
|
592
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
593
|
+
|
|
594
|
+
})(this.SmartUIDOM = this.SmartUIDOM || {});
|
|
595
|
+
}).call(window);
|
|
596
|
+
|
|
597
|
+
if (typeof define === "function" && define.amd) {
|
|
598
|
+
define("@smartui/dom", [], () => window.SmartUIDOM);
|
|
599
|
+
} else if (typeof module === "object" && module.exports) {
|
|
600
|
+
module.exports = window.SmartUIDOM;
|
|
601
|
+
}
|