262 lines
8.6 KiB
JavaScript
262 lines
8.6 KiB
JavaScript
const path = require('path');
|
|
const fs = require('fs-extra');
|
|
const argv = require('yargs').argv;
|
|
const globby = require('globby');
|
|
const xSpawn = require('cross-spawn');
|
|
const treeKill = require('tree-kill');
|
|
|
|
const AIO_PATH = path.join(__dirname, '../../');
|
|
const SHARED_PATH = path.join(__dirname, '/shared');
|
|
const EXAMPLES_PATH = path.join(AIO_PATH, './content/examples/');
|
|
const PROTRACTOR_CONFIG_FILENAME = path.join(__dirname, './shared/protractor.config.js');
|
|
const SPEC_FILENAME = 'e2e-spec.ts';
|
|
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
|
const IGNORED_EXAMPLES = [
|
|
'ts-to-js/'
|
|
];
|
|
|
|
/**
|
|
* Run Protractor End-to-End Tests for Doc Samples
|
|
*
|
|
* Flags
|
|
* --filter to filter/select _example app subdir names
|
|
* e.g. --filter=foo // all example apps with 'foo' in their folder names.
|
|
*
|
|
* --setup run yarn install, copy boilerplate and update webdriver
|
|
* e.g. --setup
|
|
*
|
|
* --local to use the locally built Angular packages, rather than versions from npm
|
|
* Must be used in conjunction with --setup as this is when the packages are copied.
|
|
* e.g. --setup --local
|
|
*
|
|
* --shard to shard the specs into groups to allow you to run them in parallel
|
|
* e.g. --shard=0/2 // the even specs: 0, 2, 4, etc
|
|
* e.g. --shard=1/2 // the odd specs: 1, 3, 5, etc
|
|
* e.g. --shard=1/3 // the second of every three specs: 1, 4, 7, etc
|
|
*/
|
|
function runE2e() {
|
|
let promise = Promise.resolve();
|
|
if (argv.setup) {
|
|
// Run setup.
|
|
console.log('runE2e: copy boilerplate');
|
|
const spawnInfo = spawnExt('yarn', ['boilerplate:add', argv.local ? '-- --local': ''], { cwd: AIO_PATH });
|
|
promise = spawnInfo.promise
|
|
.then(() => {
|
|
console.log('runE2e: update webdriver');
|
|
return spawnExt('yarn', ['webdriver:update'], { cwd: SHARED_PATH }).promise;
|
|
});
|
|
};
|
|
|
|
const outputFile = path.join(AIO_PATH, './protractor-results.txt');
|
|
|
|
return promise
|
|
.then(() => findAndRunE2eTests(argv.filter, outputFile, argv.shard))
|
|
.then((status) => {
|
|
reportStatus(status, outputFile);
|
|
if (status.failed.length > 0) {
|
|
return Promise.reject('Some test suites failed');
|
|
}
|
|
}).catch(function (e) {
|
|
console.log(e);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
// Finds all of the *e2e-spec.tests under the examples folder along with the corresponding apps
|
|
// that they should run under. Then run each app/spec collection sequentially.
|
|
function findAndRunE2eTests(filter, outputFile, shard) {
|
|
|
|
const shardParts = shard ? shard.split('/') : [0,1];
|
|
const shardModulo = parseInt(shardParts[0], 10);
|
|
const shardDivider = parseInt(shardParts[1], 10);
|
|
|
|
// create an output file with header.
|
|
const startTime = new Date().getTime();
|
|
let header = `Doc Sample Protractor Results on ${new Date().toLocaleString()}\n`;
|
|
header += ` Filter: ${filter ? filter : 'All tests'}\n\n`;
|
|
fs.writeFileSync(outputFile, header);
|
|
|
|
// Run the tests sequentially.
|
|
const status = { passed: [], failed: [] };
|
|
return getE2eSpecPaths(EXAMPLES_PATH, filter)
|
|
.then(e2eSpecPaths => e2eSpecPaths
|
|
.filter((paths, index) => index % shardDivider === shardModulo)
|
|
.reduce((promise, specPath) => {
|
|
return promise.then(() => {
|
|
const examplePath = path.dirname(specPath);
|
|
return runE2eTests(examplePath, outputFile).then((ok) => {
|
|
const arr = ok ? status.passed : status.failed;
|
|
arr.push(examplePath);
|
|
});
|
|
});
|
|
}, Promise.resolve()))
|
|
.then(function () {
|
|
const stopTime = new Date().getTime();
|
|
status.elapsedTime = (stopTime - startTime) / 1000;
|
|
return status;
|
|
});
|
|
}
|
|
|
|
// Start the example in appDir; then run protractor with the specified
|
|
// fileName; then shut down the example.
|
|
// All protractor output is appended to the outputFile.
|
|
function runE2eTests(appDir, outputFile) {
|
|
|
|
const config = loadExampleConfig(appDir);
|
|
|
|
const appBuildSpawnInfo = spawnExt('yarn', [config.build], { cwd: appDir });
|
|
const appRunSpawnInfo = spawnExt('yarn', [config.run, '--', '-s'], { cwd: appDir }, true);
|
|
|
|
let run = runProtractor(appBuildSpawnInfo.promise, appDir, appRunSpawnInfo, outputFile);
|
|
|
|
if (fs.existsSync(appDir + '/aot/index.html')) {
|
|
run = run.then((ok) => ok && runProtractorAoT(appDir, outputFile));
|
|
}
|
|
return run;
|
|
}
|
|
|
|
function runProtractor(prepPromise, appDir, appRunSpawnInfo, outputFile) {
|
|
const specFilename = path.resolve(`${appDir}/${SPEC_FILENAME}`);
|
|
return prepPromise
|
|
.catch(function () {
|
|
const emsg = `Application at ${appDir} failed to transpile.\n\n`;
|
|
console.log(emsg);
|
|
fs.appendFileSync(outputFile, emsg);
|
|
return Promise.reject(emsg);
|
|
})
|
|
.then(function (data) {
|
|
let transpileError = false;
|
|
|
|
// Start protractor.
|
|
|
|
const spawnInfo = spawnExt('yarn', ['protractor', '--',
|
|
PROTRACTOR_CONFIG_FILENAME,
|
|
`--specs=${specFilename}`,
|
|
'--params.appDir=' + appDir,
|
|
'--params.outputFile=' + outputFile
|
|
], { cwd: SHARED_PATH });
|
|
|
|
spawnInfo.proc.stderr.on('data', function (data) {
|
|
transpileError = transpileError || /npm ERR! Exit status 100/.test(data.toString());
|
|
});
|
|
return spawnInfo.promise.catch(function (err) {
|
|
if (transpileError) {
|
|
const emsg = `${specFilename} failed to transpile.\n\n`;
|
|
console.log(emsg);
|
|
fs.appendFileSync(outputFile, emsg);
|
|
}
|
|
return Promise.reject(emsg);
|
|
});
|
|
})
|
|
.then(
|
|
function () { return finish(true); },
|
|
function () { return finish(false); }
|
|
)
|
|
|
|
function finish(ok) {
|
|
// Ugh... proc.kill does not work properly on windows with child processes.
|
|
// appRun.proc.kill();
|
|
treeKill(appRunSpawnInfo.proc.pid);
|
|
return ok;
|
|
}
|
|
}
|
|
|
|
// Run e2e tests over the AOT build for projects that examples it.
|
|
function runProtractorAoT(appDir, outputFile) {
|
|
fs.appendFileSync(outputFile, '++ AoT version ++\n');
|
|
const aotBuildSpawnInfo = spawnExt('yarn', ['build:aot'], { cwd: appDir });
|
|
let promise = aotBuildSpawnInfo.promise;
|
|
|
|
const copyFileCmd = 'copy-dist-files.js';
|
|
if (fs.existsSync(appDir + '/' + copyFileCmd)) {
|
|
promise = promise.then(() => spawnExt('node', [copyFileCmd], { cwd: appDir }).promise);
|
|
}
|
|
const aotRunSpawnInfo = spawnExt('yarn', ['serve:aot'], { cwd: appDir }, true);
|
|
return runProtractor(promise, appDir, aotRunSpawnInfo, outputFile);
|
|
}
|
|
|
|
// Report final status.
|
|
function reportStatus(status, outputFile) {
|
|
let log = [''];
|
|
log.push('Suites passed:');
|
|
status.passed.forEach(function (val) {
|
|
log.push(' ' + val);
|
|
});
|
|
|
|
if (status.failed.length == 0) {
|
|
log.push('All tests passed');
|
|
} else {
|
|
log.push('Suites failed:');
|
|
status.failed.forEach(function (val) {
|
|
log.push(' ' + val);
|
|
});
|
|
}
|
|
log.push('\nElapsed time: ' + status.elapsedTime + ' seconds');
|
|
log = log.join('\n');
|
|
console.log(log);
|
|
fs.appendFileSync(outputFile, log);
|
|
}
|
|
|
|
// Returns both a promise and the spawned process so that it can be killed if needed.
|
|
function spawnExt(command, args, options, ignoreClose = false) {
|
|
let proc;
|
|
const promise = new Promise((resolve, reject) => {
|
|
let descr = command + " " + args.join(' ');
|
|
console.log('running: ' + descr);
|
|
try {
|
|
proc = xSpawn.spawn(command, args, options);
|
|
} catch (e) {
|
|
console.log(e);
|
|
reject(e);
|
|
return { proc: null, promise };
|
|
}
|
|
proc.stdout.on('data', function (data) {
|
|
process.stdout.write(data.toString());
|
|
});
|
|
proc.stderr.on('data', function (data) {
|
|
process.stdout.write(data.toString());
|
|
});
|
|
proc.on('close', function (returnCode) {
|
|
console.log(`completed: ${descr} \n`);
|
|
// Many tasks (e.g., tsc) complete but are actually errors;
|
|
// Confirm return code is zero.
|
|
returnCode === 0 || ignoreClose ? resolve(0) : reject(returnCode);
|
|
});
|
|
proc.on('error', function (data) {
|
|
console.log(`completed with error: ${descr} \n`);
|
|
console.log(data.toString());
|
|
reject(data);
|
|
});
|
|
});
|
|
return { proc, promise };
|
|
}
|
|
|
|
// Find all e2e specs in a given example folder.
|
|
function getE2eSpecPaths(basePath, filter) {
|
|
// Only get spec file at the example root.
|
|
const e2eSpecGlob = `${filter ? `*${filter}*` : '*'}/${SPEC_FILENAME}`;
|
|
return globby(e2eSpecGlob, { cwd: basePath, nodir: true })
|
|
.then(paths => paths
|
|
.filter(file => IGNORED_EXAMPLES.some(ignored => !file.startsWith(ignored)))
|
|
.map(file => path.join(basePath, file))
|
|
);
|
|
}
|
|
|
|
// Load configuration for an example.
|
|
function loadExampleConfig(exampleFolder) {
|
|
// Default config.
|
|
let config = {
|
|
build: 'build',
|
|
run: 'serve:e2e'
|
|
};
|
|
|
|
try {
|
|
const exampleConfig = fs.readJsonSync(`${exampleFolder}/${EXAMPLE_CONFIG_FILENAME}`);
|
|
Object.assign(config, exampleConfig);
|
|
} catch (e) { }
|
|
|
|
return config;
|
|
}
|
|
|
|
runE2e();
|