-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathTestFileGenerator.js
204 lines (176 loc) · 9.31 KB
/
TestFileGenerator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
const fs = require('fs');
const { HtmlValidate, formatterFactory } = require('html-validate'); //(https://html-validate.org/dev/using-api.html)
const scriptRegex = /<script>([\s\S]*?)<\/script>/gmi;
const wetBuilderRegex = /wet\.builder\..*\({[\s\S]*}\)/gm; //NOTE: Relies on the fact that there are no spaces between '('+'{' and '}'+')', e.g. wet.builder.function({...})
const wetLanguageRegex = /\/wet-(.*)\.js/;
const setupAttributeRegex = /<script[\s\S]*?data-cdts-setup='({[\s\S]*?})'/gm;
let warningIssued = false;
function searchFunctionCalls(content) {
const scripts = [...content.matchAll(scriptRegex)];
const functionCalls = [];
scripts.forEach((script) => {
functionCalls.push(...(script[1].trim().match(wetBuilderRegex) || []));
});
//If we find a script with the data-cdts-setup attribute, add its content as a call to setup
const setupAttributeMatches = [...content.matchAll(setupAttributeRegex)];
if (setupAttributeMatches.length > 0) {
functionCalls.push(`wet.builder.setup(${setupAttributeMatches[0][1].replace("'", "'")})`);
}
return functionCalls;
}
function validateBuilderFunctions(content, theme, version) {
const htmlValidate = new HtmlValidate(require('./htmlvalidator.conf.js')); //config path relative
const htmlValidateFormatReport = formatterFactory('codeframe'); //possible formatters: checkstyle, codeframe, json, stylish, text
function validateHTML(functionName, htmlContent) {
const report = htmlValidate.validateStringSync(htmlContent);
if ((!report.valid) || (report.warningCount > 0)) {
console.error(`${functionName}: ${report.errorCount} error(s), ${report.warningCount} warning(s) reported: `);
console.error(htmlValidateFormatReport(report.results));
if (report.errorCount > 0) {
console.error(`${functionName} content: [${htmlContent}]`);
throw new Error('HTML validator error reported, aborting.');
}
}
if (htmlContent.match(/\/\/(?<!:\/\/)(?<!"\/\/)(?<!"sha512-\S{0,88})/gm) || htmlContent.includes('/*')) {
console.error(`${functionName}: HTML content should not contain stray '//' or '/*' strings. HTML content: [${htmlContent}]`);
throw new Error('Encountered invalid HTML content, aborting.');
}
}
//---[ Extract language from the "wet-??.js" files found in data
const language = (content.match(wetLanguageRegex) || [null, 'en'])[1] || 'en';
const distCompiledDirName = `./dist/app/cls/WET/${theme}/${version}/cdts/compiled`;
const wetFileName = `wet-${language}.js`;
//---[ Extract all the "wet.builder" calls out of the page content
const functionCalls = searchFunctionCalls(content);
if (!functionCalls || functionCalls.length <= 0) return; //don't bother if the content does not include any wet.builder call
//---[ Mock global variable available in browsers and needed by wet-[en|fr].js
//NOTE: this is the navigator language, always setting to en-CA should be ok... right?
const navigator = { language: 'en-CA', }; //eslint-disable-line
//---[ Mock global variables needed by setup (we validate what "setup" passes to these mock objects/functions)
const document = { //eslint-disable-line
readyState: 'loading', //can be anything as long as value is not 'complete' or 'interactive'
addEventListener: function addEventListener(name, fn) {
fn(); //just call event handler function right away
},
getElementById: function getElementById(id) {
const vtr = {
id,
get innerHTML() {
return this.innerHTMLValue;
},
set innerHTML(htmlContent) {
this.innerHTMLValue = htmlContent;
console.log(`***** Function[setup#${id}]; length = ${htmlContent.length} `);
validateHTML(`setup#${id}`, htmlContent);
},
get outerHTML() {
return this.outerHTMLValue;
},
set outerHTML(htmlContent) {
this.outerHTMLValue = htmlContent;
console.log(`***** Function[setup#${id}]; length = ${htmlContent.length} `);
validateHTML(`setup#${id}`, htmlContent);
},
};
return vtr;
},
querySelector: function querySelector() {
return null;
},
head: {
querySelector: function querySelector() {
return null;
}
}
};
const $ = function $() { //eslint-disable-line
return ({
on: function on() { }, //eslint-disable-line
});
};
function DOMParser() { //eslint-disable-line
this.parseFromString = function parseFromString(htmlContent) {
const output = htmlContent.replace(/(<[/]?html>)|(<[/]?body>)|(<[/]?head>)/gm, '');
console.log(`***** Function[setup#DOMParser]; length = ${output.length} `);
validateHTML(`setup#DOMParser`, output);
return ({
body: {
childNodes: [],
},
head: {
childNodes: [],
}
});
}
}
//---[ Load soy/wet functions
//NOTE: Using eval on arbritrary files is a huge NO-NO, but we just generated these files and trust them
// (not to mention that they are not modules so require/import does not work with them)
eval(fs.readFileSync(`${distCompiledDirName}/soyutils.js`, 'utf8')); //eslint-disable-line
eval(fs.readFileSync(`${distCompiledDirName}/${wetFileName}`, 'utf8')); //eslint-disable-line
//---[ For each call in content: validate the html it generates
console.log(`***** Validating ${functionCalls.length} function(s) for ${wetFileName}...`);
for (let i = 0; i < functionCalls.length; i++) {
const functionName = functionCalls[i].match(/(wet\.builder\..*)\(/)[1];
//---[ Call function, it returns our HTML content
//console.log(`Evaluating [${functionCalls[i]}]`);
const output = (eval(functionCalls[i]) || "").toString(); //eslint-disable-line
if (output === "") {
if (!functionName.toUpperCase().endsWith('SETUP')) {
//not "setup": having no content is an error
console.warn(`${functionName} returned no content, SKIPPING!`);
}
continue; //it was setup, skip output validation (it was done by mock functions above) and move on
}
//---[ Validate HTML
console.log(`***** Function[${functionName}]; length = ${output.length} `);
validateHTML(functionName, output);
}
console.log('***************');
}
module.exports = function generateTestFile(inputFilePath, theme, outputFileName, sections) {
const version = process.env.CDTS_TEST_VERSION_NAME || 'v4_0_46';
const filePath = `./dist/app/cls/WET/${theme}/${version}/cdts/test/${outputFileName}.html`;
if (fs.existsSync(filePath)) {
if (!warningIssued) {
console.warn(`***** WARNING ***** Test file ${outputFileName}.html already exists. Skipping generation of this file and subsequent warnings will be suppressed.`);
warningIssued = true;
}
return;
}
console.log(`TestFileGenerator processing ${inputFilePath} -> ${theme}/${outputFileName}`);
let data = fs.readFileSync(inputFilePath, 'utf8');
if (data.match(/wet\.builder\.[\S]*[sS]etup\(/gm) !== null || data.match(setupAttributeRegex) !== null) {
//---[ To support the wet.builder.setup function, we must make sure cdnEnv placeholder is defined
//---[ also have to merge refFooter and refTop objects into a single "base" object
//---[ also have to handle sub-theme if specified in refTop (for gcintranet/esdc|eccc)
const tmpTopSection = sections.refTop || sections.serverRefTop || sections.splashTop || null;
const tmpFooterSection = sections.refFooter || null;
const tmpTopParsed = tmpTopSection ? JSON.parse(tmpTopSection) : {};
const tmpFooterParsed = tmpFooterSection ? JSON.parse(tmpFooterSection) : {};
//set cdnEnv
if (!sections.cdnEnv) {
if (tmpTopSection) {
sections.cdnEnv = tmpTopParsed ? `"${tmpTopParsed.cdnEnv || 'localhost'}"` : '"localhost"';
}
else {
sections.cdnEnv = '"localhost"';
}
}
//merge refTop/refFooter
//(instead of trying to figure out which was set, we'll reset them all to merged object)
const tmpBase = JSON.stringify({ ...tmpTopParsed, ...tmpFooterParsed });
sections.refTop = tmpBase;
sections.serverRefTop = tmpBase;
sections.splashTop = tmpBase;
sections.refFooter = tmpBase;
//subTheme vs css
data = data.replace('~~subTheme~~', tmpTopParsed.subTheme ? `${tmpTopParsed.subTheme}-` : '');
}
for (let i = 0; i < Object.keys(sections).length; i++) {
data = data.replace('"~' + Object.keys(sections)[i] + '~"', sections[Object.keys(sections)[i]]);
}
fs.writeFileSync(filePath, data, 'utf8');
//---[ Before writing data to disk, validate the output of the various 'wet.builder.*' functions.
validateBuilderFunctions(data, theme, version);
}