// Copyright 2017 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const parse5 = require('parse5');
const fs = require('fs');
const path = require('path');
const process = require('process');
const PATH = '';
/**
* @typedef {{
* basePath: string,
* depsFile: string,
* output: string,
* overwriteExistingFiles: boolean,
* paths: !Array<{path: string, directory: boolean}>,
* recursive: boolean,
* }}
*/
let Args;
/**
* Entry point for this script.
*/
function main() {
let args;
try {
args = processArgs(process.argv.slice(2));
} catch (error) {
showHelp(error.message);
}
for (const path of args.paths) {
if (path.directory) {
generateHtmlForDirectory(path.path, args);
} else {
maybeGenerateHtmlForFile(path.path, args);
}
}
}
/**
* Shows script help.
* @param {string=} errorMessage
*/
function showHelp(errorMessage = '') {
if (errorMessage) {
console.error(errorMessage);
}
console.log(`Auto generates _test.html files for Closure unit tests.
Usage:
generate_test_html [OPTIONS]... PATH...
Paths:
list of paths to directories or _test.js files. For directories a
_test.html file will be generated for each _test.js file in the
directory.
Options:
--base
Path to base.js file.
--dep_file:
Path to a deps file to use for all tests. If not specified but a
*_test_deps.js file exists it will be automatically included.
--recursive
generate _test.html for each _test.js in each directory recursively.
Defaults to false.
--output
output file path, should be named _test.html. Only valid if a single
_test.js file is specified for PATH.
--overwrite
overwrite any existing _test.html files. Defaults to true.`);
process.exit(errorMessage ? 1 : 0);
}
/**
* @param {!Array<string>} args
* @return {!Args}
*/
function processArgs(args) {
const processedArgs = {
basePath: '',
output: '',
overwriteExistingFiles: true,
paths: [],
recursive: false,
};
args.forEach((arg) => {
const [key, value] = arg.split('=');
switch (key) {
case '--base':
processedArgs.basePath = value;
break;
case '--dep_file':
processedArgs.depsFile = value;
break;
case '--help':
showHelp();
break;
case '--recursive':
processedArgs.recursive = String(value).toLowerCase() !== 'false';
break;
case '--output':
processedArgs.output = value;
break;
case '--overwrite':
processedArgs.overwriteExistingFiles =
String(value).toLowerCase() !== 'false';
break;
default:
const stats = fs.statSync(arg);
processedArgs.paths.push({path: arg, directory: stats.isDirectory()});
}
});
if (!processedArgs.basePath.endsWith('base.js')) {
throw new Error(
`Path to base must end with base.js: ${processedArgs.basePath}.`);
}
if (processedArgs.output) {
if (processedArgs.paths.length > 1) {
throw new Error(
'Cannot specify an output file when there is more than one path.');
}
if (processedArgs.paths.some(path => path.directory)) {
throw new Error('Cannot specify an output when path is a directory.');
}
if (!processedArgs.output.endsWith('_test.html')) {
throw new Error('Output file should end with _test.html.');
}
}
if (processedArgs.depsFile && !fs.existsSync(processedArgs.depsFile)) {
throw new Error(
'Specified deps file does not exist: ' + processedArgs.depsFile);
}
return processedArgs;
}
/**
* Generates one _test.html file for each _test.js file in the given directory.
*
* @param {string} directory Directory to generate files for.
* @param {!Args} args
*/
function generateHtmlForDirectory(directory, args) {
const directoryContents = fs.readdirSync(directory);
const subDirectories = [];
for (const filename of directoryContents) {
const fullname = path.join(directory, filename);
const stats = fs.statSync(fullname);
if (stats.isDirectory() && args.recursive) {
subDirectories.push(fullname);
} else if (stats.isFile()) {
maybeGenerateHtmlForFile(fullname, args);
}
}
for (const subDirectory of subDirectories) {
generateHtmlForDirectory(subDirectory, args);
}
}
/**
* Generates a _test.html file for the given file if it is a _test.js file.
* @param {string} filename Full path to the file.
* @param {!Args} args
*/
function maybeGenerateHtmlForFile(filename, args) {
if (!filename.endsWith('_test.js')) {
return;
}
const htmlFilename =
args.output || filename.replace('_test.js', '_test.html');
if (!args.overwriteExistingFiles && fs.existsSync(htmlFilename)) {
console.warn(`"${htmlFilename}" exists - skipping.`);
return;
}
const originalJs = fs.readFileSync(filename, 'utf8');
const provide = /goog\.(?:provide|module)\('([^']*?)'\);/g.exec(originalJs);
if (!provide || !provide[1]) {
console.error(
`File ${filename} does not provide or module the tests, ` +
'cannot generate html.');
return;
}
const newJS = `goog.require('${provide[1]}');`;
const title = `Closure Unit Tests - ${provide[1]}`;
const baseFileName = filename.replace('_test.js', '');
const testDomFilename = baseFileName + '_test_dom.html';
const testDom = fs.existsSync(testDomFilename) ?
fs.readFileSync(testDomFilename, 'utf8') :
'';
const testBootstrapFilename = baseFileName + '_test_bootstrap.js';
const pathToBootstrap = fs.existsSync(testBootstrapFilename) ?
path.basename(testBootstrapFilename) :
'';
const testDepsFilename = args.depsFile || baseFileName + '_test_deps.js';
const pathToDeps =
fs.existsSync(testDepsFilename) ? path.basename(testDepsFilename) : '';
const pathToBase = path.relative(path.dirname(htmlFilename), args.basePath);
const html = createHtml(
title, pathToBootstrap, pathToDeps, newJS, testDom, pathToBase);
fs.writeFileSync(htmlFilename, html);
}
/**
* @param {string} title Title of the test.
* @param {string} pathToBootstrap Path to a bootstrap javascript file to run
* first, if any. Generally this file will contain closure defines.
* @param {string} pathToDeps Bath to a custom deps file, if any.
* @param {string} js Script content of the test.
* @param {string} testDom Any test DOM related to the test or the empty string
* if none.
* @param {string} pathToBase Path to the base.js file.
* @return {string} The text content of the test HTML file.
*/
function createHtml(
title, pathToBootstrap, pathToDeps, js, testDom, pathToBase) {
// Use parse5 to parse and reserialize the test dom. This generates any
// optional tags (html, body, head) if missing. Meaning test doms can specify
// these tags, if needed.
return parse5.serialize(parse5.parse(`<!DOCTYPE html>
<!-- DO NOT EDIT. This file auto-generated by generate_closure_unit_tests.js -->
<!--
Copyright 2017 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<meta charset="UTF-8" />
${pathToBootstrap ? `<script src=${pathToBootstrap}></script>` : ''}
<script src="${pathToBase}"></script>
${pathToDeps ? `<script src=${pathToDeps}></script>` : ''}
<script>${js}</script>
<title>${title}</title>` + testDom));
}
if (require.main === module) {
main();
}