bdy 1.8.11-dev → 1.8.12-dev
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/distTs/package.json +1 -1
- package/distTs/parseDom.js +242 -0
- package/distTs/src/visualTest/server.js +24 -4
- package/package.json +1 -1
package/distTs/package.json
CHANGED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const SNAPSHOT = {
|
|
3
|
+
parseDom,
|
|
4
|
+
};
|
|
5
|
+
// @ts-expect-error
|
|
6
|
+
globalThis.SNAPSHOT = SNAPSHOT;
|
|
7
|
+
function parseDom(dom, enableJavaScript) {
|
|
8
|
+
const resources = new Set();
|
|
9
|
+
const documentType = getDoctype(dom);
|
|
10
|
+
const domClone = getDomClone(dom, resources, enableJavaScript);
|
|
11
|
+
return {
|
|
12
|
+
title: document.title,
|
|
13
|
+
html: documentType + domClone.documentElement.outerHTML,
|
|
14
|
+
resources: [...resources],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function getDoctype(dom) {
|
|
18
|
+
if (dom.doctype) {
|
|
19
|
+
const { name, publicId, systemId } = dom.doctype;
|
|
20
|
+
let legacySuffix = '';
|
|
21
|
+
if (publicId && systemId) {
|
|
22
|
+
legacySuffix = ` PUBLIC "${publicId}" "${systemId}"`;
|
|
23
|
+
}
|
|
24
|
+
else if (publicId) {
|
|
25
|
+
legacySuffix = ` PUBLIC "${publicId}"`;
|
|
26
|
+
}
|
|
27
|
+
else if (systemId) {
|
|
28
|
+
legacySuffix = ` SYSTEM "${systemId}"`;
|
|
29
|
+
}
|
|
30
|
+
return `<!DOCTYPE ${name}${legacySuffix}>`;
|
|
31
|
+
}
|
|
32
|
+
return '<!DOCTYPE html>';
|
|
33
|
+
}
|
|
34
|
+
function getDomClone(dom, resources, enableJavaScript) {
|
|
35
|
+
for (const element of dom.querySelectorAll('input,textarea,select,iframe,canvas,video,style')) {
|
|
36
|
+
if (!element.getAttribute('snapshot-element-id')) {
|
|
37
|
+
element.setAttribute('snapshot-element-id', randomId());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const clone = createClone(dom);
|
|
41
|
+
prepareInputElements(dom, clone);
|
|
42
|
+
prepareVideoElements(dom, clone, resources);
|
|
43
|
+
prepareIframeElements(dom, clone, resources, enableJavaScript);
|
|
44
|
+
if (!enableJavaScript) {
|
|
45
|
+
prepareCanvasElements(dom, clone, resources);
|
|
46
|
+
prepareStyleSheets(dom, clone);
|
|
47
|
+
prepareAdoptedStyleSheet(dom, clone, resources);
|
|
48
|
+
}
|
|
49
|
+
return clone;
|
|
50
|
+
}
|
|
51
|
+
function createClone(dom) {
|
|
52
|
+
const documentClone = dom.createDocumentFragment();
|
|
53
|
+
const clone = dom.documentElement.cloneNode(true);
|
|
54
|
+
documentClone.append(clone);
|
|
55
|
+
documentClone.documentElement = documentClone.firstChild;
|
|
56
|
+
documentClone.head = documentClone.querySelector('head') || undefined;
|
|
57
|
+
documentClone.body = documentClone.querySelector('body') || undefined;
|
|
58
|
+
return documentClone;
|
|
59
|
+
}
|
|
60
|
+
function randomId() {
|
|
61
|
+
return `${Math.random().toString(36).slice(2, 10)}`;
|
|
62
|
+
}
|
|
63
|
+
function prepareInputElements(dom, clone) {
|
|
64
|
+
for (const element of dom.querySelectorAll('input, textarea, select')) {
|
|
65
|
+
const inputId = element.getAttribute('snapshot-element-id');
|
|
66
|
+
const cloneElement = clone.querySelector(`[snapshot-element-id="${inputId}"]`);
|
|
67
|
+
switch (element.type) {
|
|
68
|
+
case 'checkbox':
|
|
69
|
+
case 'radio': {
|
|
70
|
+
if (element.checked) {
|
|
71
|
+
cloneElement.setAttribute('checked', '');
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'select-one': {
|
|
76
|
+
if (element.selectedIndex !== -1) {
|
|
77
|
+
cloneElement.options[element.selectedIndex].setAttribute('selected', 'true');
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'select-multiple': {
|
|
82
|
+
for (const option of element.selectedOptions) {
|
|
83
|
+
cloneElement.options[option.index].setAttribute('selected', 'true');
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'textarea': {
|
|
88
|
+
cloneElement.innerHTML = element.value;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default: {
|
|
92
|
+
cloneElement.setAttribute('value', element.value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function prepareVideoElements(dom, clone, resources) {
|
|
98
|
+
var _a;
|
|
99
|
+
for (const element of dom.querySelectorAll('video')) {
|
|
100
|
+
if (!element.getAttribute('poster')) {
|
|
101
|
+
const videoId = element.getAttribute('snapshot-element-id');
|
|
102
|
+
const cloneElement = clone.querySelector(`[snapshot-element-id="${videoId}"]`);
|
|
103
|
+
const canvas = document.createElement('canvas');
|
|
104
|
+
const width = element.videoWidth;
|
|
105
|
+
const height = element.videoHeight;
|
|
106
|
+
let data;
|
|
107
|
+
canvas.width = width;
|
|
108
|
+
canvas.height = height;
|
|
109
|
+
(_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.drawImage(element, 0, 0, width, height);
|
|
110
|
+
try {
|
|
111
|
+
data = canvas.toDataURL();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.log('Get video poster error', error);
|
|
115
|
+
}
|
|
116
|
+
if (data && data !== 'data:,') {
|
|
117
|
+
const resource = resourceFromDataURL(videoId, data);
|
|
118
|
+
resources.add(resource);
|
|
119
|
+
cloneElement.setAttribute('snapshot-poster-url', resource.url);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function resourceFromDataURL(elementId, data) {
|
|
125
|
+
const [meta, body] = data.split(',');
|
|
126
|
+
const [, contentTypeWithBase] = meta.split(':');
|
|
127
|
+
const [contentType] = contentTypeWithBase.split(';');
|
|
128
|
+
const [, extension] = contentType.split('/');
|
|
129
|
+
const path = `/_resource_/${elementId}.${extension}`;
|
|
130
|
+
const url = new URL(path, document.URL).toString();
|
|
131
|
+
return { url, body, headers: { 'content-type': contentType } };
|
|
132
|
+
}
|
|
133
|
+
function prepareIframeElements(dom, clone, resources, enableJavaScript) {
|
|
134
|
+
var _a, _b, _c;
|
|
135
|
+
for (const iframe of dom.querySelectorAll('iframe')) {
|
|
136
|
+
const iframeId = iframe.getAttribute('snapshot-element-id');
|
|
137
|
+
const cloneElement = clone.querySelector(`[snapshot-element-id="${iframeId}"]`);
|
|
138
|
+
const isCreatedWithJs = (!iframe.src || iframe.src.startsWith('javascript:')) && !iframe.srcdoc;
|
|
139
|
+
const inHead = (_a = clone.head) === null || _a === void 0 ? void 0 : _a.contains(cloneElement);
|
|
140
|
+
if ((isCreatedWithJs && !enableJavaScript) || inHead) {
|
|
141
|
+
cloneElement.remove();
|
|
142
|
+
}
|
|
143
|
+
else if ((_b = iframe.contentDocument) === null || _b === void 0 ? void 0 : _b.documentElement) {
|
|
144
|
+
if (enableJavaScript && isCreatedWithJs) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!isCreatedWithJs &&
|
|
148
|
+
!((_c = iframe.contentWindow) === null || _c === void 0 ? void 0 : _c.performance.timing.loadEventEnd)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const documentType = getDoctype(iframe.contentDocument);
|
|
152
|
+
const domClone = getDomClone(iframe.contentDocument, resources, enableJavaScript);
|
|
153
|
+
addBaseURI(domClone);
|
|
154
|
+
cloneElement.setAttribute('srcdoc', documentType + domClone.documentElement.outerHTML);
|
|
155
|
+
cloneElement.removeAttribute('src');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function addBaseURI(dom) {
|
|
160
|
+
var _a;
|
|
161
|
+
if (new URL(dom.baseURI).hostname) {
|
|
162
|
+
const base = document.createElement('base');
|
|
163
|
+
base.setAttribute('href', dom.baseURI);
|
|
164
|
+
(_a = dom.querySelector('head')) === null || _a === void 0 ? void 0 : _a.append(base);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function prepareCanvasElements(dom, clone, resources) {
|
|
168
|
+
for (const canvas of dom.querySelectorAll('canvas')) {
|
|
169
|
+
const data = canvas.toDataURL();
|
|
170
|
+
if (data && data !== 'data:,') {
|
|
171
|
+
const canvasId = canvas.getAttribute('snapshot-element-id');
|
|
172
|
+
const cloneElement = clone.querySelector(`[snapshot-element-id="${canvasId}"]`);
|
|
173
|
+
const resource = resourceFromDataURL(canvasId, data);
|
|
174
|
+
resources.add(resource);
|
|
175
|
+
const img = document.createElement('img');
|
|
176
|
+
img.setAttribute('snapshot-image-url', resource.url);
|
|
177
|
+
for (const { name, value } of canvas.attributes) {
|
|
178
|
+
img.setAttribute(name, value);
|
|
179
|
+
}
|
|
180
|
+
cloneElement.before(img);
|
|
181
|
+
cloneElement.remove();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function prepareStyleSheets(dom, clone) {
|
|
186
|
+
for (const styleSheet of dom.styleSheets) {
|
|
187
|
+
if (!styleSheet.href && styleSheet.cssRules && styleSheet.ownerNode) {
|
|
188
|
+
const styleId = styleSheet.ownerNode.getAttribute('snapshot-element-id');
|
|
189
|
+
const cloneElement = clone.querySelector(`[snapshot-element-id="${styleId}"]`);
|
|
190
|
+
const cloneElementStyleSheet = styleSheetFromNode(cloneElement);
|
|
191
|
+
if (areStyleSheetsDifferent(styleSheet, cloneElementStyleSheet)) {
|
|
192
|
+
const style = document.createElement('style');
|
|
193
|
+
style.setAttribute('snapshot-element-id', styleId);
|
|
194
|
+
style.innerHTML = [...styleSheet.cssRules]
|
|
195
|
+
.map((cssRule) => cssRule.cssText)
|
|
196
|
+
.join('\n');
|
|
197
|
+
cloneElement.before(style, cloneElement);
|
|
198
|
+
cloneElement.remove();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function styleSheetFromNode(node) {
|
|
204
|
+
const temporaryStyleElement = node.cloneNode();
|
|
205
|
+
temporaryStyleElement.innerHTML = node.innerHTML;
|
|
206
|
+
const temporaryDocument = document.cloneNode();
|
|
207
|
+
temporaryDocument.append(temporaryStyleElement);
|
|
208
|
+
const styleSheet = temporaryStyleElement.sheet;
|
|
209
|
+
temporaryStyleElement.remove();
|
|
210
|
+
return styleSheet;
|
|
211
|
+
}
|
|
212
|
+
function areStyleSheetsDifferent(styleSheet, styleSheetClone) {
|
|
213
|
+
var _a;
|
|
214
|
+
for (const index in styleSheet.cssRules) {
|
|
215
|
+
const ruleA = styleSheet.cssRules[index].cssText;
|
|
216
|
+
const ruleB = (_a = styleSheetClone.cssRules[index]) === null || _a === void 0 ? void 0 : _a.cssText;
|
|
217
|
+
if (ruleA !== ruleB) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
function prepareAdoptedStyleSheet(dom, clone, resources) {
|
|
224
|
+
var _a;
|
|
225
|
+
for (const adoptedStyleSheet of dom.adoptedStyleSheets) {
|
|
226
|
+
const link = document.createElement('link');
|
|
227
|
+
const text = [...adoptedStyleSheet.cssRules]
|
|
228
|
+
.map((cssRule) => cssRule.cssText)
|
|
229
|
+
.join('\n');
|
|
230
|
+
link.setAttribute('rel', 'stylesheet');
|
|
231
|
+
const resource = resourceFromText(text);
|
|
232
|
+
link.setAttribute('snapshot-css-url', resource.url);
|
|
233
|
+
resources.add(resource);
|
|
234
|
+
(_a = clone.body) === null || _a === void 0 ? void 0 : _a.prepend(link);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function resourceFromText(text) {
|
|
238
|
+
const id = randomId();
|
|
239
|
+
const path = `/_resource_/${id}.css`;
|
|
240
|
+
const url = new URL(path, document.URL).toString();
|
|
241
|
+
return { url, body: text, headers: { 'content-type': 'text/css' } };
|
|
242
|
+
}
|
|
@@ -7,10 +7,29 @@ exports.createServer = createServer;
|
|
|
7
7
|
const fastify_1 = __importDefault(require("fastify"));
|
|
8
8
|
const cors_1 = __importDefault(require("@fastify/cors"));
|
|
9
9
|
const snapshots_js_1 = require("./snapshots.js");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
10
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_sea_1 = require("node:sea");
|
|
13
|
+
const utils_js_1 = require("../utils.js");
|
|
13
14
|
const port = 1337;
|
|
15
|
+
let cachedParseDom = '';
|
|
16
|
+
function prepareParseDom(parseDom) {
|
|
17
|
+
return parseDom
|
|
18
|
+
.replaceAll('"use strict";', '')
|
|
19
|
+
.replaceAll('Object.defineProperty(exports, "__esModule", { value: true });', '');
|
|
20
|
+
}
|
|
21
|
+
async function getParseDom() {
|
|
22
|
+
if (!cachedParseDom) {
|
|
23
|
+
if ((0, node_sea_1.isSea)()) {
|
|
24
|
+
cachedParseDom = await prepareParseDom((0, node_sea_1.getAsset)('parseDom.js', 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const filePath = node_path_1.default.resolve((0, utils_js_1.getRootDir)(), 'parseDom.js');
|
|
28
|
+
cachedParseDom = await prepareParseDom(await promises_1.default.readFile(filePath, 'utf-8'));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return cachedParseDom;
|
|
32
|
+
}
|
|
14
33
|
async function createServer() {
|
|
15
34
|
const app = (0, fastify_1.default)({
|
|
16
35
|
// logger: true,
|
|
@@ -18,7 +37,8 @@ async function createServer() {
|
|
|
18
37
|
});
|
|
19
38
|
app.register(cors_1.default);
|
|
20
39
|
app.get('/parseDom.js', async (request, reply) => {
|
|
21
|
-
|
|
40
|
+
const parseDomFile = await getParseDom();
|
|
41
|
+
reply.type('text/javascript').send(parseDomFile);
|
|
22
42
|
});
|
|
23
43
|
app.post('/snapshot', async (request, reply) => {
|
|
24
44
|
const data = request.body;
|