diff --git a/src/compiler/compilers.js b/src/compiler/compilers.js new file mode 100644 index 00000000000..7f2df843757 --- /dev/null +++ b/src/compiler/compilers.js @@ -0,0 +1,29 @@ +import hammerhead from 'testcafe-hammerhead'; +import { Compiler as LegacyTestFileCompiler } from 'testcafe-legacy-api'; +import EsNextTestFileCompiler from './test-file/formats/es-next/compiler'; +import TypeScriptTestFileCompiler from './test-file/formats/typescript/compiler'; +import CoffeeScriptTestFileCompiler from './test-file/formats/coffeescript/compiler'; +import RawTestFileCompiler from './test-file/formats/raw'; + +function createTestFileCompilers (options) { + return [ + new LegacyTestFileCompiler(hammerhead.processScript), + new EsNextTestFileCompiler(), + new TypeScriptTestFileCompiler(options), + new CoffeeScriptTestFileCompiler(), + new RawTestFileCompiler() + ]; +} + +let testFileCompilers = []; + +export function getTestFileCompilers () { + if (!testFileCompilers.length) + initTestFileCompilers(); + + return testFileCompilers; +} + +export function initTestFileCompilers (options = {}) { + testFileCompilers = createTestFileCompilers(options); +} diff --git a/src/compiler/index.js b/src/compiler/index.js index 754496c53e6..a4f83a2fc90 100644 --- a/src/compiler/index.js +++ b/src/compiler/index.js @@ -1,34 +1,23 @@ import Promise from 'pinkie'; import { flattenDeep, find, chunk, uniq } from 'lodash'; import stripBom from 'strip-bom'; -import { Compiler as LegacyTestFileCompiler } from 'testcafe-legacy-api'; -import hammerhead from 'testcafe-hammerhead'; -import EsNextTestFileCompiler from './test-file/formats/es-next/compiler'; -import TypeScriptTestFileCompiler from './test-file/formats/typescript/compiler'; -import CoffeeScriptTestFileCompiler from './test-file/formats/coffeescript/compiler'; -import RawTestFileCompiler from './test-file/formats/raw'; import { readFile } from '../utils/promisified-functions'; import { GeneralError } from '../errors/runtime'; import { RUNTIME_ERRORS } from '../errors/types'; +import { getTestFileCompilers, initTestFileCompilers } from './compilers'; const SOURCE_CHUNK_LENGTH = 1000; -const testFileCompilers = [ - new LegacyTestFileCompiler(hammerhead.processScript), - new EsNextTestFileCompiler(), - new TypeScriptTestFileCompiler(), - new CoffeeScriptTestFileCompiler(), - new RawTestFileCompiler() -]; - export default class Compiler { - constructor (sources) { + constructor (sources, options) { this.sources = sources; + + initTestFileCompilers(options); } static getSupportedTestFileExtensions () { - return uniq(testFileCompilers.map(compiler => compiler.getSupportedExtension())); + return uniq(getTestFileCompilers().map(compiler => compiler.getSupportedExtension())); } async _createTestFileInfo (filename) { @@ -43,7 +32,7 @@ export default class Compiler { code = stripBom(code).toString(); - const compiler = find(testFileCompilers, someCompiler => someCompiler.canCompile(code, filename)); + const compiler = find(getTestFileCompilers(), someCompiler => someCompiler.canCompile(code, filename)); if (!compiler) return null; @@ -128,6 +117,6 @@ export default class Compiler { } static cleanUp () { - testFileCompilers.forEach(compiler => compiler.cleanUp()); + getTestFileCompilers().forEach(compiler => compiler.cleanUp()); } } diff --git a/src/compiler/test-file/formats/typescript/compiler.js b/src/compiler/test-file/formats/typescript/compiler.js index 34a0a87e25c..d627bb0b753 100644 --- a/src/compiler/test-file/formats/typescript/compiler.js +++ b/src/compiler/test-file/formats/typescript/compiler.js @@ -3,30 +3,18 @@ import { zipObject } from 'lodash'; import OS from 'os-family'; import APIBasedTestFileCompilerBase from '../../api-based'; import ESNextTestFileCompiler from '../es-next/compiler'; - +import TypescriptConfiguration from '../../../../configuration/typescript-configuration'; const RENAMED_DEPENDENCIES_MAP = new Map([['testcafe', APIBasedTestFileCompilerBase.EXPORTABLE_LIB_PATH]]); +const tsDefsPath = path.resolve(__dirname, '../../../../../ts-defs/index.d.ts'); export default class TypeScriptTestFileCompiler extends APIBasedTestFileCompilerBase { - static _getTypescriptOptions () { - // NOTE: lazy load the compiler - const ts = require('typescript'); + constructor ({ typeScriptOptions } = {}) { + super(); - return { - experimentalDecorators: true, - emitDecoratorMetadata: true, - allowJs: true, - pretty: true, - inlineSourceMap: true, - noImplicitAny: false, - module: ts.ModuleKind.CommonJS, - target: 2 /* ES6 */, - lib: ['lib.es6.d.ts'], - baseUrl: __dirname, - paths: { testcafe: ['../../../../../ts-defs/index.d.ts'] }, - suppressOutputPathCheck: true, - skipLibCheck: true - }; + const tsConfigPath = typeScriptOptions ? typeScriptOptions.tsConfigPath : null; + + this.tsConfig = new TypescriptConfiguration(tsConfigPath); } static _reportErrors (diagnostics) { @@ -35,11 +23,16 @@ export default class TypeScriptTestFileCompiler extends APIBasedTestFileCompiler let errMsg = 'TypeScript compilation failed.\n'; diagnostics.forEach(d => { - const file = d.file; - const { line, character } = file.getLineAndCharacterOfPosition(d.start); - const message = ts.flattenDiagnosticMessageText(d.messageText, '\n'); + const message = ts.flattenDiagnosticMessageText(d.messageText, '\n'); + const file = d.file; + + if (file) { + const { line, character } = file.getLineAndCharacterOfPosition(d.start); + + errMsg += `${file.fileName} (${line + 1}, ${character + 1}): `; + } - errMsg += `${file.fileName} (${line + 1}, ${character + 1}): ${message}\n`; + errMsg += `${message}\n`; }); throw new Error(errMsg); @@ -54,11 +47,18 @@ export default class TypeScriptTestFileCompiler extends APIBasedTestFileCompiler return filename; } + _compileCodeForTestFiles (testFilesInfo) { + return this.tsConfig.init() + .then(() => { + return super._compileCodeForTestFiles(testFilesInfo); + }); + } + _precompileCode (testFilesInfo) { // NOTE: lazy load the compiler const ts = require('typescript'); - const filenames = testFilesInfo.map(({ filename }) => filename); + const filenames = testFilesInfo.map(({ filename }) => filename).concat([tsDefsPath]); const normalizedFilenames = filenames.map(filename => TypeScriptTestFileCompiler._normalizeFilename(filename)); const normalizedFilenamesMap = zipObject(normalizedFilenames, filenames); @@ -66,7 +66,7 @@ export default class TypeScriptTestFileCompiler extends APIBasedTestFileCompiler .filter(filename => !this.cache[filename]) .map(filename => normalizedFilenamesMap[filename]); - const opts = TypeScriptTestFileCompiler._getTypescriptOptions(); + const opts = this.tsConfig.getOptions(); const program = ts.createProgram(uncachedFiles, opts); program.getSourceFiles().forEach(sourceFile => { diff --git a/src/compiler/test-file/formats/typescript/get-test-list.js b/src/compiler/test-file/formats/typescript/get-test-list.js index e5490d9d134..d11807338c9 100644 --- a/src/compiler/test-file/formats/typescript/get-test-list.js +++ b/src/compiler/test-file/formats/typescript/get-test-list.js @@ -1,7 +1,7 @@ import ts from 'typescript'; import { repeat, merge } from 'lodash'; -import TypeScriptTestFileCompiler from './compiler'; import { TestFileParserBase } from '../../test-file-parser-base'; +import TypescriptConfiguration from '../../../../configuration/typescript-configuration'; function replaceComments (code) { return code.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, match => { @@ -232,7 +232,8 @@ class TypeScriptTestFileParser extends TestFileParserBase { this.codeArr = code.split('\n'); this.codeWithoutComments = replaceComments(code); - const sourceFile = ts.createSourceFile('', code, TypeScriptTestFileCompiler._getTypescriptOptions(), true); + const tsConfig = new TypescriptConfiguration(); + const sourceFile = ts.createSourceFile('', code, tsConfig.getOptions(), true); return this.analyze(sourceFile.statements); } diff --git a/src/configuration/configuration-base.js b/src/configuration/configuration-base.js new file mode 100644 index 00000000000..87ed0c3ae30 --- /dev/null +++ b/src/configuration/configuration-base.js @@ -0,0 +1,179 @@ +import debug from 'debug'; +import { stat, readFile } from '../utils/promisified-functions'; +import Option from './option'; +import optionSource from './option-source'; +import { cloneDeep, castArray } from 'lodash'; +import resolvePathRelativelyCwd from '../utils/resolve-path-relatively-cwd'; +import JSON5 from 'json5'; +import renderTemplate from '../utils/render-template'; +import WARNING_MESSAGES from '../notifications/warning-message'; +import log from '../cli/log'; + +const DEBUG_LOGGER = debug('testcafe:configuration'); + +export default class Configuration { + constructor (configurationFileName) { + this._options = {}; + this._filePath = resolvePathRelativelyCwd(configurationFileName); + this._overridenOptions = []; + } + + static _fromObj (obj) { + const result = Object.create(null); + + Object.entries(obj).forEach(([key, value]) => { + const option = new Option(key, value); + + result[key] = option; + }); + + return result; + } + + static async _isConfigurationFileExists (path) { + try { + await stat(path); + + return true; + } + catch (error) { + DEBUG_LOGGER(renderTemplate(WARNING_MESSAGES.cannotFindConfigurationFile, path, error.stack)); + + return false; + } + } + + static _showConsoleWarning (message) { + log.write(message); + } + + static _showWarningForError (error, warningTemplate, ...args) { + const message = renderTemplate(warningTemplate, ...args); + + Configuration._showConsoleWarning(message); + + DEBUG_LOGGER(message); + DEBUG_LOGGER(error); + } + + async init () { + } + + mergeOptions (options) { + Object.entries(options).map(([key, value]) => { + const option = this._ensureOption(key, value, optionSource.input); + + if (value === void 0) + return; + + if (option.value !== value && + option.source === optionSource.configuration) + this._overridenOptions.push(key); + + this._setOptionValue(option, value); + }); + } + + getOption (key) { + if (!key) + return void 0; + + const option = this._options[key]; + + if (!option) + return void 0; + + return option.value; + } + + getOptions () { + const result = Object.create(null); + + Object.entries(this._options).forEach(([name, option]) => { + result[name] = option.value; + }); + + return result; + } + + clone () { + return cloneDeep(this); + } + + get filePath () { + return this._filePath; + } + + async _load () { + const configurationFileExists = await Configuration._isConfigurationFileExists(this.filePath); + + if (configurationFileExists) { + const configurationFileContent = await this._readConfigurationFileContent(); + + if (configurationFileContent) + return this._parseConfigurationFileContent(configurationFileContent); + } + + return null; + } + + async _readConfigurationFileContent () { + try { + return await readFile(this.filePath); + } + catch (error) { + Configuration._showWarningForError(error, WARNING_MESSAGES.cannotReadConfigFile); + } + + return null; + } + + _parseConfigurationFileContent (configurationFileContent) { + try { + return JSON5.parse(configurationFileContent); + } + catch (error) { + Configuration._showWarningForError(error, WARNING_MESSAGES.cannotParseConfigFile); + } + + return null; + } + + _ensureArrayOption (name) { + const options = this._options[name]; + + if (!options) + return; + + options.value = castArray(options.value); + } + + _ensureOption (name, value, source) { + let option = null; + + if (name in this._options) + option = this._options[name]; + else { + option = new Option(name, value, source); + + this._options[name] = option; + } + + return option; + } + + _ensureOptionWithValue (name, defaultValue, source) { + const option = this._ensureOption(name, defaultValue, source); + + if (option.value !== void 0) + return; + + option.value = defaultValue; + option.source = source; + } + + _setOptionValue (option, value) { + option.value = value; + option.source = optionSource.input; + } +} diff --git a/src/configuration/default-values.js b/src/configuration/default-values.js index 2f93c7b92c1..b1e1cad09e3 100644 --- a/src/configuration/default-values.js +++ b/src/configuration/default-values.js @@ -15,3 +15,16 @@ export const DEFAULT_APP_INIT_DELAY = 1000; export const DEFAULT_CONCURRENCY_VALUE = 1; +export const DEFAULT_TYPESCRIPT_COMPILER_OPTIONS = { + experimentalDecorators: true, + emitDecoratorMetadata: true, + allowJs: true, + pretty: true, + inlineSourceMap: true, + noImplicitAny: false, + module: 1 /* ts.ModuleKind.CommonJS */, + target: 2 /* ES6 */, + suppressOutputPathCheck: true +}; + +export const TYPESCRIPT_COMPILER_NON_OVERRIDABLE_OPTIONS = ['module', 'target']; diff --git a/src/configuration/option-names.js b/src/configuration/option-names.js index 66774ab6e52..3357c7e546d 100644 --- a/src/configuration/option-names.js +++ b/src/configuration/option-names.js @@ -26,5 +26,6 @@ export default { pageLoadTimeout: 'pageLoadTimeout', videoPath: 'videoPath', videoOptions: 'videoOptions', - videoEncodingOptions: 'videoEncodingOptions' + videoEncodingOptions: 'videoEncodingOptions', + tsConfigPath: 'tsConfigPath' }; diff --git a/src/configuration/index.js b/src/configuration/testcafe-configuration.js similarity index 53% rename from src/configuration/index.js rename to src/configuration/testcafe-configuration.js index 19020505e2c..d653301234d 100644 --- a/src/configuration/index.js +++ b/src/configuration/testcafe-configuration.js @@ -1,18 +1,13 @@ -import debug from 'debug'; -import { stat, readFile } from '../utils/promisified-functions'; -import Option from './option'; +import Configuration from './configuration-base'; import optionSource from './option-source'; -import { cloneDeep, castArray } from 'lodash'; +import { castArray } from 'lodash'; import { getSSLOptions, getGrepOptions } from '../utils/get-options'; import OPTION_NAMES from './option-names'; import getFilterFn from '../utils/get-filter-fn'; -import resolvePathRelativelyCwd from '../utils/resolve-path-relatively-cwd'; -import JSON5 from 'json5'; -import renderTemplate from '../utils/render-template'; import prepareReporters from '../utils/prepare-reporters'; -import WARNING_MESSAGES from '../notifications/warning-message'; -import log from '../cli/log'; import { getConcatenatedValuesString, getPluralSuffix } from '../utils/string'; +import renderTemplate from '../utils/render-template'; +import WARNING_MESSAGES from '../notifications/warning-message'; import { DEFAULT_TIMEOUT, @@ -22,8 +17,6 @@ import { DEFAULT_CONCURRENCY_VALUE } from './default-values'; -const DEBUG_LOGGER = debug('testcafe:configuration'); - const CONFIGURATION_FILENAME = '.testcaferc.json'; const OPTION_FLAG_NAMES = [ @@ -37,78 +30,64 @@ const OPTION_FLAG_NAMES = [ OPTION_NAMES.takeScreenshotsOnFails ]; -export default class Configuration { +export default class TestCafeConfiguration extends Configuration { constructor () { - this._options = {}; - this._filePath = resolvePathRelativelyCwd(CONFIGURATION_FILENAME); - this._overridenOptions = []; - } - - static _fromObj (obj) { - const result = Object.create(null); - - Object.entries(obj).forEach(([key, value]) => { - const option = new Option(key, value); - - result[key] = option; - }); - - return result; + super(CONFIGURATION_FILENAME); } - static async _isConfigurationFileExists (path) { - try { - await stat(path); + async init (options = {}) { + const opts = await this._load(); - return true; - } - catch (error) { - DEBUG_LOGGER(renderTemplate(WARNING_MESSAGES.cannotFindConfigurationFile, path, error.stack)); + if (opts) { + this._options = Configuration._fromObj(opts); - return false; + await this._normalizeOptionsAfterLoad(); } - } - static _showConsoleWarning (message) { - log.write(message); + this.mergeOptions(options); } - static _showWarningForError (error, warningTemplate, ...args) { - const message = renderTemplate(warningTemplate, ...args); - - Configuration._showConsoleWarning(message); - - DEBUG_LOGGER(message); - DEBUG_LOGGER(error); + prepare () { + this._prepareFlags(); + this._setDefaultValues(); } - async _load () { - if (!await Configuration._isConfigurationFileExists(this.filePath)) + notifyAboutOverridenOptions () { + if (!this._overridenOptions.length) return; - let configurationFileContent = null; + const optionsStr = getConcatenatedValuesString(this._overridenOptions); + const optionsSuffix = getPluralSuffix(this._overridenOptions); - try { - configurationFileContent = await readFile(this.filePath); - } - catch (error) { - Configuration._showWarningForError(error, WARNING_MESSAGES.cannotReadConfigFile); + Configuration._showConsoleWarning(renderTemplate(WARNING_MESSAGES.configOptionsWereOverriden, optionsStr, optionsSuffix)); - return; - } + this._overridenOptions = []; + } + + get startOptions () { + const result = { + hostname: this.getOption('hostname'), + port1: this.getOption('port1'), + port2: this.getOption('port2'), + options: { + ssl: this.getOption('ssl'), + developmentMode: this.getOption('developmentMode'), + retryTestPages: !!this.getOption('retryTestPages') + } + }; - try { - const optionsObj = JSON5.parse(configurationFileContent); + if (result.options.retryTestPages) + result.options.staticContentCaching = STATIC_CONTENT_CACHING_SETTINGS; - this._options = Configuration._fromObj(optionsObj); - } - catch (error) { - Configuration._showWarningForError(error, WARNING_MESSAGES.cannotParseConfigFile); + return result; + } - return; - } + _prepareFlags () { + OPTION_FLAG_NAMES.forEach(name => { + const option = this._ensureOption(name, void 0, optionSource.configuration); - await this._normalizeOptionsAfterLoad(); + option.value = !!option.value; + }); } async _normalizeOptionsAfterLoad () { @@ -119,15 +98,6 @@ export default class Configuration { this._prepareReporters(); } - _ensureArrayOption (name) { - const options = this._options[name]; - - if (!options) - return; - - options.value = castArray(options.value); - } - _prepareFilterFn () { const filterOption = this._ensureOption(OPTION_NAMES.filter, null); @@ -163,59 +133,6 @@ export default class Configuration { sslOptions.value = await getSSLOptions(sslOptions.value); } - _ensureOption (name, value, source) { - let option = null; - - if (name in this._options) - option = this._options[name]; - else { - option = new Option(name, value, source); - - this._options[name] = option; - } - - return option; - } - - _ensureOptionWithValue (name, defaultValue, source) { - const option = this._ensureOption(name, defaultValue, source); - - if (option.value !== void 0) - return; - - option.value = defaultValue; - option.source = source; - } - - async init (options = {}) { - await this._load(); - this.mergeOptions(options); - } - - mergeOptions (options) { - Object.entries(options).map(([key, value]) => { - const option = this._ensureOption(key, value, optionSource.input); - - if (value === void 0) - return; - - if (option.value !== value && - option.source === optionSource.configuration) - this._overridenOptions.push(key); - - option.value = value; - option.source = optionSource.input; - }); - } - - _prepareFlags () { - OPTION_FLAG_NAMES.forEach(name => { - const option = this._ensureOption(name, void 0, optionSource.configuration); - - option.value = !!option.value; - }); - } - _setDefaultValues () { this._ensureOptionWithValue(OPTION_NAMES.selectorTimeout, DEFAULT_TIMEOUT.selector, optionSource.configuration); this._ensureOptionWithValue(OPTION_NAMES.assertionTimeout, DEFAULT_TIMEOUT.assertion, optionSource.configuration); @@ -224,69 +141,4 @@ export default class Configuration { this._ensureOptionWithValue(OPTION_NAMES.appInitDelay, DEFAULT_APP_INIT_DELAY, optionSource.configuration); this._ensureOptionWithValue(OPTION_NAMES.concurrency, DEFAULT_CONCURRENCY_VALUE, optionSource.configuration); } - - prepare () { - this._prepareFlags(); - this._setDefaultValues(); - } - - notifyAboutOverridenOptions () { - if (!this._overridenOptions.length) - return; - - const optionsStr = getConcatenatedValuesString(this._overridenOptions); - const optionsSuffix = getPluralSuffix(this._overridenOptions); - - Configuration._showConsoleWarning(renderTemplate(WARNING_MESSAGES.configOptionsWereOverriden, optionsStr, optionsSuffix)); - - this._overridenOptions = []; - } - - getOption (key) { - if (!key) - return void 0; - - const option = this._options[key]; - - if (!option) - return void 0; - - return option.value; - } - - getOptions () { - const result = Object.create(null); - - Object.entries(this._options).forEach(([name, option]) => { - result[name] = option.value; - }); - - return result; - } - - clone () { - return cloneDeep(this); - } - - get startOptions () { - const result = { - hostname: this.getOption('hostname'), - port1: this.getOption('port1'), - port2: this.getOption('port2'), - options: { - ssl: this.getOption('ssl'), - developmentMode: this.getOption('developmentMode'), - retryTestPages: !!this.getOption('retryTestPages') - } - }; - - if (result.options.retryTestPages) - result.options.staticContentCaching = STATIC_CONTENT_CACHING_SETTINGS; - - return result; - } - - get filePath () { - return this._filePath; - } } diff --git a/src/configuration/typescript-configuration.js b/src/configuration/typescript-configuration.js new file mode 100644 index 00000000000..4282479f357 --- /dev/null +++ b/src/configuration/typescript-configuration.js @@ -0,0 +1,44 @@ +import Configuration from './configuration-base'; +import optionSource from './option-source'; +import { DEFAULT_TYPESCRIPT_COMPILER_OPTIONS, TYPESCRIPT_COMPILER_NON_OVERRIDABLE_OPTIONS } from './default-values'; +import { intersection } from 'lodash'; +import WARNING_MESSAGES from '../notifications/warning-message'; +import renderTemplate from '../utils/render-template'; + +const DEFAULT_CONFIGURATION_FILENAME = 'tsconfig.json'; + +export default class TypescriptConfiguration extends Configuration { + constructor (tsConfigPath) { + super(tsConfigPath || DEFAULT_CONFIGURATION_FILENAME); + + for (const option in DEFAULT_TYPESCRIPT_COMPILER_OPTIONS) + this._ensureOptionWithValue(option, DEFAULT_TYPESCRIPT_COMPILER_OPTIONS[option], optionSource.configuration); + } + + async init () { + const opts = await this._load(); + + if (opts && opts.compilerOptions) + this.mergeOptions(opts.compilerOptions); + + this._notifyThatOptionsCannotBeOverriden(); + } + + _notifyThatOptionsCannotBeOverriden () { + const warnedOptions = intersection(this._overridenOptions, TYPESCRIPT_COMPILER_NON_OVERRIDABLE_OPTIONS); + + if (!warnedOptions.length) + return; + + const warningMessage = warnedOptions + .map(option => renderTemplate(WARNING_MESSAGES.cannotOverrideTypeScriptConfigOptions, option)) + .join('\n'); + + Configuration._showConsoleWarning(warningMessage); + } + + _setOptionValue (option, value) { + if (TYPESCRIPT_COMPILER_NON_OVERRIDABLE_OPTIONS.indexOf(option.name) === -1) + super._setOptionValue(option, value); + } +} diff --git a/src/index.js b/src/index.js index 2bee60cf479..662eb6e8c16 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,7 @@ import { GeneralError } from './errors/runtime'; import { RUNTIME_ERRORS } from './errors/types'; import embeddingUtils from './embedding-utils'; import exportableLib from './api/exportable-lib'; -import Configuration from './configuration'; +import TestCafeConfiguration from './configuration/testcafe-configuration'; const lazyRequire = require('import-lazy')(require); const TestCafe = lazyRequire('./testcafe'); @@ -39,7 +39,7 @@ async function getValidPort (port) { // API async function createTestCafe (hostname, port1, port2, sslOptions, developmentMode, retryTestPages) { - const configuration = new Configuration(); + const configuration = new TestCafeConfiguration(); await configuration.init({ hostname, diff --git a/src/notifications/warning-message.js b/src/notifications/warning-message.js index 244c2193b05..6ceef19abfe 100644 --- a/src/notifications/warning-message.js +++ b/src/notifications/warning-message.js @@ -16,6 +16,7 @@ export default { cannotReadConfigFile: 'An error has occurred while reading the configuration file.', cannotParseConfigFile: "Failed to parse the '.testcaferc.json' file.\n\nThis file is not a well-formed JSON file.", configOptionsWereOverriden: 'The {optionsString} option{suffix} from the configuration file will be ignored.', + cannotOverrideTypeScriptConfigOptions: 'You cannot override the "{optionName}" compiler option in the TypeScript configuration file.', cannotFindSSLCertFile: 'Unable to find the "{path}" file, specified by the "{option}" ssl option. Error details:\n' + '\n' + diff --git a/src/runner/bootstrapper.js b/src/runner/bootstrapper.js index 94b77bf346c..393d6144c22 100644 --- a/src/runner/bootstrapper.js +++ b/src/runner/bootstrapper.js @@ -17,13 +17,14 @@ export default class Bootstrapper { constructor (browserConnectionGateway) { this.browserConnectionGateway = browserConnectionGateway; - this.concurrency = null; - this.sources = []; - this.browsers = []; - this.reporters = []; - this.filter = null; - this.appCommand = null; - this.appInitDelay = null; + this.concurrency = null; + this.sources = []; + this.browsers = []; + this.reporters = []; + this.filter = null; + this.appCommand = null; + this.appInitDelay = null; + this.tsConfigPath = null; } static _splitBrowserInfo (browserInfo) { @@ -74,9 +75,10 @@ export default class Bootstrapper { if (!this.sources.length) throw new GeneralError(RUNTIME_ERRORS.testSourcesNotSet); - const parsedFileList = await parseFileList(this.sources, process.cwd()); - const compiler = new Compiler(parsedFileList); - let tests = await compiler.getTests(); + const { parsedFileList, compilerOptions } = await this._getCompilerArguments(); + + const compiler = new Compiler(parsedFileList, compilerOptions); + let tests = await compiler.getTests(); const testsWithOnlyFlag = tests.filter(test => test.only); @@ -92,6 +94,18 @@ export default class Bootstrapper { return tests; } + async _getCompilerArguments () { + const parsedFileList = await parseFileList(this.sources, process.cwd()); + + const compilerOptions = { + typeScriptOptions: { + tsConfigPath: this.tsConfigPath + } + }; + + return { parsedFileList, compilerOptions }; + } + async _ensureOutStream (outStream) { if (typeof outStream !== 'string') return outStream; diff --git a/src/runner/index.js b/src/runner/index.js index 3ce11bcc27b..2b24ac7e100 100644 --- a/src/runner/index.js +++ b/src/runner/index.js @@ -29,6 +29,7 @@ export default class Runner extends EventEmitter { this.bootstrapper = this._createBootstrapper(browserConnectionGateway); this.pendingTaskPromises = []; this.configuration = configuration; + this.tsConfiguration = null; this.isCli = false; // NOTE: This code is necessary only for displaying marketing messages. @@ -302,6 +303,7 @@ export default class Runner extends EventEmitter { this.bootstrapper.appInitDelay = this.configuration.getOption(OPTION_NAMES.appInitDelay); this.bootstrapper.filter = this.configuration.getOption(OPTION_NAMES.filter) || this.bootstrapper.filter; this.bootstrapper.reporters = this.configuration.getOption(OPTION_NAMES.reporter) || this.bootstrapper.reporters; + this.bootstrapper.tsConfigPath = this.configuration.getOption(OPTION_NAMES.tsConfigPath); } // API diff --git a/test/server/configuration-test.js b/test/server/configuration-test.js index af5f9f55429..6c2af09884f 100644 --- a/test/server/configuration-test.js +++ b/test/server/configuration-test.js @@ -1,29 +1,32 @@ /*eslint-disable no-console */ -const Configuration = require('../../lib/configuration'); -const { cloneDeep } = require('lodash'); -const { expect } = require('chai'); -const fs = require('fs'); -const tmp = require('tmp'); -const nanoid = require('nanoid'); -const consoleWrapper = require('./helpers/console-wrapper'); - -describe('Configuration', () => { - let configuration = null; - let configPath = null; - let keyFileContent = null; - +const { cloneDeep } = require('lodash'); +const { expect } = require('chai'); +const fs = require('fs'); +const tmp = require('tmp'); +const nanoid = require('nanoid'); + +const TestCafeConfiguration = require('../../lib/configuration/testcafe-configuration'); +const TypescriptConfiguration = require('../../lib/configuration/typescript-configuration'); +const { DEFAULT_TYPESCRIPT_COMPILER_OPTIONS } = require('../../lib/configuration/default-values'); +const consoleWrapper = require('./helpers/console-wrapper'); + +let configuration = null; +let configPath = null; +let keyFileContent = null; + +const createConfigFile = options => { + options = options || {}; + fs.writeFileSync(configPath, JSON.stringify(options)); +}; + +describe('TestCafeConfiguration', () => { consoleWrapper.init(); tmp.setGracefulCleanup(); - const createConfigFile = options => { - options = options || {}; - fs.writeFileSync(configPath, JSON.stringify(options)); - }; - beforeEach(() => { - configuration = new Configuration(); + configuration = new TestCafeConfiguration(); configPath = configuration.filePath; const keyFile = tmp.fileSync(); @@ -204,3 +207,109 @@ describe('Configuration', () => { }); }); }); + +describe('TypeScriptConfiguration', () => { + it('Default', () => { + configuration = new TypescriptConfiguration(); + + return configuration.init() + .then(() => { + expect(configuration.getOptions()).to.deep.equal(DEFAULT_TYPESCRIPT_COMPILER_OPTIONS); + }); + }); + + describe('With configuration file', () => { + tmp.setGracefulCleanup(); + + beforeEach(() => { + consoleWrapper.init(); + consoleWrapper.wrap(); + }); + + afterEach(() => { + fs.unlinkSync(configuration.filePath); + + consoleWrapper.unwrap(); + consoleWrapper.messages.clear(); + }); + + it('override options', () => { + configuration = new TypescriptConfiguration(); + configPath = configuration.filePath; + + createConfigFile({ + compilerOptions: { + experimentalDecorators: false, + emitDecoratorMetadata: false, + allowJs: false, + pretty: false, + inlineSourceMap: false, + noImplicitAny: true, + module: 2, + target: 3, + suppressOutputPathCheck: false + } + }); + + return configuration.init() + .then(() => { + consoleWrapper.unwrap(); + + expect(configuration.getOption('experimentalDecorators')).eql(false); + expect(configuration.getOption('emitDecoratorMetadata')).eql(false); + expect(configuration.getOption('allowJs')).eql(false); + expect(configuration.getOption('pretty')).eql(false); + expect(configuration.getOption('inlineSourceMap')).eql(false); + expect(configuration.getOption('noImplicitAny')).eql(true); + expect(configuration.getOption('suppressOutputPathCheck')).eql(false); + + // NOTE: `module` and `target` default options can not be overridden by custom config + expect(configuration.getOption('module')).eql(1); + expect(configuration.getOption('target')).eql(2); + + expect(consoleWrapper.messages.log).contains('You cannot override the "module" compiler option in the TypeScript configuration file.'); + expect(consoleWrapper.messages.log).contains('You cannot override the "target" compiler option in the TypeScript configuration file.'); + }); + }); + + it('TestCafe config + TypeScript config', () => { + let runner = null; + + configuration = new TestCafeConfiguration(); + + configPath = configuration.filePath; + + const customConfigFilePath = 'custom-config.json'; + + createConfigFile({ + tsConfigPath: customConfigFilePath + }); + + configPath = customConfigFilePath; + + createConfigFile({ + compilerOptions: { + target: 'override-target' + } + }); + + return configuration.init() + .then(() => { + const RunnerCtor = require('../../lib/runner'); + + runner = new RunnerCtor(null, null, configuration); + + runner.src('test/server/data/test-suites/typescript-basic/testfile1.ts'); + runner._setBootstrapperOptions(); + + return runner.bootstrapper._getTests(); + }) + .then(() => { + fs.unlinkSync(customConfigFilePath); + + expect(runner.bootstrapper.tsConfigPath).eql(customConfigFilePath); + expect(consoleWrapper.messages.log).contains('You cannot override the "target" compiler option in the TypeScript configuration file.'); + }); + }); + }); +});