@typed/test
Version:
Testing made simple.
175 lines • 9.12 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { createServer } from 'http';
import { sync } from 'resolve';
import { findOpenPort } from '../browser/findOpenPort';
import { getLauncher, openBrowser } from '../browser/openBrowser';
import { setupServer } from '../browser/server';
import { basename, join, isAbsolute } from 'path';
import * as tempy from 'tempy';
import { writeFileSync } from 'fs';
import { generateTestBundle } from '../browser/generateTestBundle';
import { createIndexHtml } from '../browser/createIndexHtml';
import { typecheckInAnotherProcess } from '../typescript/typeCheckInAnotherProcess';
import { watchTestMetadata } from '../tests/watchTestMetadata';
import { watchFile } from '../browser/webpack/watchFile';
import { collectByKey } from '../common/collectByKey';
import { Results } from './Results';
import { makeAbsolute } from '../common/makeAbsolute';
import { getTestStats, getTestResults } from '../results';
const isEqual = require('lodash.isequal');
const uniq = (list) => Array.from(new Set(list));
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const updateQueue = (metadata, queue) => {
const queuedMetadataByFilePath = collectByKey(x => x.filePath, queue);
const queuedFilePaths = Object.keys(queuedMetadataByFilePath).sort();
const metadataByFilePath = collectByKey(x => x.filePath, metadata);
const filePaths = Object.keys(metadataByFilePath).sort();
const allFilePaths = uniq([...filePaths, ...queuedFilePaths]);
return allFilePaths.reduce((xs, filePath) => metadataByFilePath[filePath] // replace updated files
? xs.concat(metadataByFilePath[filePath])
: xs.concat(queuedMetadataByFilePath[filePath]), []);
};
export function watchBrowserTests(fileGlobs, compilerOptions, options, cwd, logger, cb, errCb, typeCheckCb) {
return __awaiter(this, void 0, void 0, function* () {
const { keepAlive, timeout, typeCheck, mode } = options;
const outputDirectory = tempy.directory();
const temporaryPath = join(outputDirectory, basename(tempy.file({ extension: 'ts' })));
const bundlePath = join(outputDirectory, basename(tempy.file({ extension: 'js' })));
const indexHtmlPath = join(outputDirectory, 'index.html');
const port = yield findOpenPort();
const url = `http://localhost:${port}`;
const logErrors = (errors) => Promise.all(errors.map(logger.error));
const makeAbsolutePath = (path) => (isAbsolute(path) ? path : join(cwd, path));
const { updateResults, removeFilePath } = new Results();
// shared mutable state
let queuedMetadata = [];
let previousMetadata = [];
let testsAreRunning = false;
let writingToDisk = false;
let scheduleNextRunHandle;
let newFileOnDisk = false;
let killBrowser = () => void 0;
let previousWebpackHash = '';
let firstRun = true;
let timeToRunTests = 0;
let browserOpenTime = 0;
let testsCompletedTime = 0;
const killTestBrowser = () => (!keepAlive && killBrowser ? killBrowser() : void 0);
const setupTestBrowser = () => __awaiter(this, void 0, void 0, function* () {
const { browser, keepAlive } = options;
if (browser !== 'chrome-headless') {
const launcher = yield getLauncher();
const run = () => __awaiter(this, void 0, void 0, function* () {
killTestBrowser();
logger.log('Opening browser...');
browserOpenTime = Date.now();
const instance = yield openBrowser(browser, url, keepAlive, launcher);
if (!firstRun) {
const lastCompletion = testsCompletedTime;
// Re-run tests if they seem to have stalled - this can happen when browsers are opened in succession quite quickly
// I've only been able to make it do this when I am very rapidly changing and saving a test file to purposely try and break things.
delay(timeToRunTests * 3).then(() => (testsCompletedTime === lastCompletion ? run() : void 0));
}
killBrowser = () => instance.stop();
});
return run;
}
const { launch } = require(sync('chrome-launcher', { basedir: cwd }));
const run = () => __awaiter(this, void 0, void 0, function* () {
killTestBrowser();
logger.log('Opening browser...');
browserOpenTime = Date.now();
const chrome = yield launch({ startingUrl: url });
if (!firstRun) {
const lastCompletion = testsCompletedTime;
delay(timeToRunTests * 3).then(() => (testsCompletedTime === lastCompletion ? run() : void 0));
}
killBrowser = () => chrome.kill();
});
return run;
});
const runTestsInBrowser = yield setupTestBrowser();
const newStats = (stats) => __awaiter(this, void 0, void 0, function* () {
const shouldReturnEarly = stats.hash === previousWebpackHash || !newFileOnDisk || testsAreRunning;
if (shouldReturnEarly) {
previousWebpackHash = stats.hash;
return;
}
if (stats.hasErrors()) {
const { errors } = stats.toJson();
return logErrors(errors);
}
testsAreRunning = true;
runTestsInBrowser();
});
const writeToDisk = (metadata) => {
writeFileSync(temporaryPath, generateTestBundle(cwd, outputDirectory, port, timeout, metadata));
newFileOnDisk = true;
writingToDisk = false;
if (firstRun) {
watchFile(cwd, temporaryPath, bundlePath, options.webpackConfiguration, newStats, errCb);
}
};
const typeCheckMetadata = (metadata) => __awaiter(this, void 0, void 0, function* () {
logger.log('Typechecking...');
const processResults = yield typecheckInAnotherProcess(cwd, metadata.map(x => x.filePath));
logger.log('Type checking complete');
typeCheckCb(processResults);
});
const removeFileFromQueue = (filePath) => {
const path = makeAbsolute(cwd, filePath);
removeFilePath(path);
queuedMetadata = queuedMetadata.filter(x => makeAbsolutePath(x.filePath) !== path);
};
const scheduleNextBundleWrite = () => {
if (testsAreRunning || writingToDisk) {
clearTimeout(scheduleNextRunHandle);
scheduleNextRunHandle = setTimeout(scheduleNextBundleWrite, 600);
return;
}
writingToDisk = true;
const currentQueue = queuedMetadata.slice();
writeToDisk(currentQueue);
if (typeCheck) {
typeCheckMetadata(currentQueue);
}
queuedMetadata = [];
};
const updateMetadataAndWriteBundle = (metadata) => __awaiter(this, void 0, void 0, function* () {
queuedMetadata = updateQueue(metadata, queuedMetadata);
if (isEqual(metadata, previousMetadata) || queuedMetadata.length === 0) {
return;
}
previousMetadata = metadata;
scheduleNextBundleWrite();
});
const server = createServer(setupServer(logger, outputDirectory, newResults => {
firstRun = false;
if (!keepAlive && killBrowser) {
killBrowser();
}
const results = updateResults(newResults);
const stats = getTestStats(getTestResults(results));
cb({ results, stats });
testsCompletedTime = Date.now();
timeToRunTests = testsCompletedTime - browserOpenTime;
newFileOnDisk = testsAreRunning = false;
}));
writeFileSync(indexHtmlPath, createIndexHtml(basename(bundlePath)));
server.listen(port, '0.0.0.0');
const { dispose: stopWatchingMetadata } = yield watchTestMetadata(cwd, fileGlobs, compilerOptions, mode, logger, removeFileFromQueue, updateMetadataAndWriteBundle);
const dispose = () => {
stopWatchingMetadata();
server.close();
};
return { dispose };
});
}
//# sourceMappingURL=watchBrowserTests.js.map