Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added typescript testcaferc configuration file support #8039

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
944d7ce
feat: added typescript testcaferc configuration file support
Bayheck Oct 11, 2023
56ae266
testing: added test for configuration-test with custom ts config
Bayheck Oct 13, 2023
82e0766
refactor: basic decomposition of typescript compiler for configuration
Bayheck Oct 18, 2023
dddaf3d
add: added tests for testcaferc configuration file compilation from t…
Bayheck Oct 21, 2023
5c57c0a
fix: unnecessary newlines removed
Bayheck Oct 21, 2023
fd8285b
fix: unnecessary newlines removed
Bayheck Oct 21, 2023
e4f106c
refactor: compiler refactored for configuration and test files
Bayheck Oct 24, 2023
b3ecccc
fix: pr comments fixed
Bayheck Oct 24, 2023
23c38d7
testing cli compile options
Bayheck Oct 25, 2023
3a7528b
draft: compiler options from cli
Bayheck Oct 26, 2023
0fa9eab
add: addded tests for cli compiler options and config path
Bayheck Oct 30, 2023
d3082c9
fix: typescript configuration restored
Bayheck Oct 30, 2023
2193e54
fix: pr comments fixed
Bayheck Nov 16, 2023
6333869
Merge branch 'master' into master
aleks-pro Nov 27, 2023
0b2e2f1
refactor: refactored _execAsModule method to separate logic for confi…
Bayheck Nov 28, 2023
ffab977
Merge branch 'master' of https://github.com/Bayheck/testcafe
Bayheck Nov 28, 2023
bf2837a
Merge branch 'master' of https://github.com/DevExpress/testcafe
Feb 19, 2024
3290b27
Merge branch 'master' of https://github.com/DevExpress/testcafe
Apr 4, 2024
8e26f30
Merge branch 'master' of https://github.com/DevExpress/testcafe
May 13, 2024
a0c057e
increase edge workflow timeout
May 15, 2024
1d9f5f6
Merge branch 'master' of https://github.com/Testing-and-Learning/test…
May 15, 2024
f0088aa
increase timeout
May 16, 2024
8249dcb
isolate edge workflow
May 16, 2024
36fe960
fixing error
May 16, 2024
1378c63
isolate reporting tests
May 16, 2024
513b4c8
isolate reporting tests
May 16, 2024
a18dff3
isolate reporting tests
May 16, 2024
74333b1
testing
Jun 6, 2024
5d31263
update branch
Jun 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ async function runTests (argParser) {
v8Flags,
disableCrossDomain,
esm,
compilerOptions,
} = opts;

const testCafe = await createTestCafe({
Expand All @@ -102,6 +103,7 @@ async function runTests (argParser) {
v8Flags,
disableCrossDomain,
esm,
compilerOptions,
});

const correctedBrowsersAndSources = await correctBrowsersAndSources(argParser, testCafe.configuration);
Expand Down Expand Up @@ -189,4 +191,3 @@ async function listBrowsers (providerName) {
error(err);
}
})();

8 changes: 6 additions & 2 deletions src/compiler/test-file/api-based.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ export default class APIBasedTestFileCompilerBase extends TestFileCompilerBase {
mod._compile(code, filename);

cacheProxy.stopExternalCaching();

return mod;
}

return Promise.resolve();
}

_compileCode (code, filename) {
Expand Down Expand Up @@ -191,7 +195,7 @@ export default class APIBasedTestFileCompilerBase extends TestFileCompilerBase {
return global.fixture && global.test;
}

async _runCompiledCode (compiledCode, filename) {
async _runCompiledTestCode (compiledCode, filename) {
const testFile = new TestFile(filename);

this._addGlobalAPI(testFile);
Expand Down Expand Up @@ -230,7 +234,7 @@ export default class APIBasedTestFileCompilerBase extends TestFileCompilerBase {
}

execute (compiledCode, filename) {
return this._runCompiledCode(compiledCode, filename);
return this._runCompiledTestCode(compiledCode, filename);
}

async compile (code, filename) {
Expand Down
18 changes: 18 additions & 0 deletions src/compiler/test-file/formats/typescript/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,22 @@ export default class TypeScriptTestFileCompiler extends APIBasedTestFileCompiler
public getSupportedExtension (): string[] {
return [Extensions.ts, Extensions.tsx];
}

async compileConfiguration (filename: string) : Promise<object | null> {
let compiledConfigurationModule = null;

const [compiledCode] = await this.precompile([{ code: '', filename }]);

if (compiledCode) {
this._setupRequireHook({ });

compiledConfigurationModule = await this._execAsModule(compiledCode, filename);

this._removeRequireHook();

return compiledConfigurationModule?.exports;
}

return null;
}
}
28 changes: 16 additions & 12 deletions src/configuration/configuration-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,24 +154,15 @@ export default class Configuration {
return this._defaultPaths;
}

public async _load (): Promise<null | object> {
public async _load (customCompilerOptions?: object): Promise<null | object> {
if (!this.defaultPaths?.length)
return null;

const configs = await Promise.all(this.defaultPaths.map(async filePath => {
if (!await this._isConfigurationFileExists(filePath))
return { filePath, options: null };

let options = null as object | null;

if (this._isJSConfiguration(filePath))
options = this._readJsConfigurationFileContent(filePath);
else {
const configurationFileContent = await this._readConfigurationFileContent(filePath);

if (configurationFileContent)
options = this._parseConfigurationFileContent(configurationFileContent, filePath);
}
const options = await this._readConfigurationOptions(filePath, customCompilerOptions);

return { filePath, options };
}));
Expand All @@ -189,6 +180,19 @@ export default class Configuration {
return existedConfigs[0].options;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async _readConfigurationOptions (filePath: string, customCompilerOptions?: object): Promise<object | null> {
if (this._isJSConfiguration(filePath))
return this._readJsConfigurationFileContent(filePath);

const configurationFileContent = await this._readConfigurationFileContent(filePath);

if (configurationFileContent)
return this._parseConfigurationFileContent(configurationFileContent, filePath);

return null;
}

protected async _isConfigurationFileExists (filePath = this.filePath): Promise<boolean> {
try {
await stat(filePath);
Expand All @@ -202,7 +206,7 @@ export default class Configuration {
}
}

private static _hasExtension (filePath: string | undefined, extention: string): boolean {
protected static _hasExtension (filePath: string | undefined, extention: string): boolean {
return !!filePath && extname(filePath) === extention;
}

Expand Down
1 change: 1 addition & 0 deletions src/configuration/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ enum Extensions {
js = '.js',
json = '.json',
cjs = '.cjs',
ts = '.ts',
}

export default Extensions;
28 changes: 27 additions & 1 deletion src/configuration/testcafe-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { RUNTIME_ERRORS } from '../errors/types';
import { LOCALHOST_NAMES } from '../utils/localhost-names';
import { BrowserConnectionGatewayOptions } from '../browser/connection/gateway';
import { getValidHostname } from './utils';
import TypeScriptTestFileCompiler from '../compiler/test-file/formats/typescript/compiler';

const BASE_CONFIGURATION_FILENAME = '.testcaferc';
const CONFIGURATION_FILENAMES = (Object.keys(Extensions) as Array<keyof typeof Extensions>).map(ext => `${BASE_CONFIGURATION_FILENAME}${Extensions[ext]}`);
Expand Down Expand Up @@ -100,7 +101,7 @@ export default class TestCafeConfiguration extends Configuration {
public async init (options?: Dictionary<object>): Promise<void> {
await super.init();

const opts = await this._load();
const opts = await this._load(options);

if (opts) {
this._options = Configuration._fromObj(opts);
Expand Down Expand Up @@ -337,4 +338,29 @@ export default class TestCafeConfiguration extends Configuration {
return hostname;
});
}

protected async _readConfigurationOptions (filePath: string, customCompilerOptions?: object): Promise<object | null> {
if (this._isTSConfiguration(filePath)) {
const compilerOptions = (customCompilerOptions as Dictionary<object>)?.compilerOptions;

return this._readTsConfigurationFileContent(filePath, compilerOptions);
}

return super._readConfigurationOptions(filePath);
}

protected _isTSConfiguration (filePath = this.filePath): boolean {
return Configuration._hasExtension(filePath, Extensions.ts);
}

public async _readTsConfigurationFileContent (filePath = this.filePath, compilerOptions?: TypeScriptCompilerOptions): Promise<object | null> {
if (!filePath)
return null;

delete require.cache[filePath];

const compiler = new TypeScriptTestFileCompiler(compilerOptions, {});

return compiler.compileConfiguration(filePath);
}
}
2 changes: 1 addition & 1 deletion src/configuration/typescript-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default class TypescriptConfiguration extends Configuration {
}

public async init (customCompilerOptions?: object): Promise<void> {
const opts = await this._load() as TypescriptConfigurationOptions;
const opts = await this._load(customCompilerOptions) as TypescriptConfigurationOptions;

if (opts && opts.compilerOptions) {
const parsedOpts = this._parseOptions(opts);
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function getConfiguration (args) {
// It should be removed in future TestCafe versions.
// All new APIs should be enabled through the configuration object in the upper clause.
// Please do not add new APIs here.
const [hostname, port1, port2, ssl, developmentMode, retryTestPages, cache, configFile] = args;
const [hostname, port1, port2, ssl, developmentMode, retryTestPages, cache, configFile, compilerOptions] = args;

configuration = new TestCafeConfiguration(configFile);

Expand All @@ -37,6 +37,7 @@ async function getConfiguration (args) {
developmentMode,
retryTestPages,
cache,
compilerOptions,
});
}

Expand Down
2 changes: 1 addition & 1 deletion test/server/compiler-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,7 @@ describe('Compiler', function () {
});

it('Should raise an error if test file has a TypeScript error', function () {
const testfile = posixResolve('test/server/data/test-suites/typescript-compile-errors/testfile.ts');
const testfile = posixResolve('test/server/data/test-suites/typescript-compile-errors/testfile.ts').toLowerCase();

return compile(testfile)
.then(function () {
Expand Down
162 changes: 162 additions & 0 deletions test/server/configuration-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,168 @@ describe('TypeScriptConfiguration', function () {
await del(configuration.defaultPaths);
});

describe('Custom typescript config file', function () {
const CUSTOM_TESTCAFE_CONFIG_FILE_PATH = 'custom11.testcaferc.ts';
const CUSTOM_TS_CONFIG_FILE_PATH = 'custom11.tsconfig.json';

const OPTIONS = {
'hostname': '123.456.789',
'port1': 1234,
'port2': 5678,
'src': 'path1/folder',
'browser': 'edge',
};

function createTestCafeTypescriptConfigurationFile (filePath) {
fs.writeFileSync(filePath, `
const fs = require('fs');
const path = require('path');
const { nanoid } = require('nanoid');
const jsCustomModule = require('./test/server/data/configuration/module/module.js');
const tsCustomModule = require('./test/server/data/configuration/typescript-module/module.ts');
\n
const foo = (p) => {};

\n
module.exports = ${JSON.stringify(OPTIONS)}`);
}

beforeEach(() => {
createTestCafeTypescriptConfigurationFile(CUSTOM_TESTCAFE_CONFIG_FILE_PATH);
});

it('Custom ts config path is used', async () => {
configuration = new TestCafeConfiguration(CUSTOM_TESTCAFE_CONFIG_FILE_PATH);

await configuration.init();

expect(pathUtil.basename(configuration.filePath)).eql(CUSTOM_TESTCAFE_CONFIG_FILE_PATH);
expect(configuration.getOption('hostname')).eql(OPTIONS.hostname);
expect(configuration.getOption('port1')).eql(OPTIONS.port1);
expect(configuration.getOption('port2')).eql(OPTIONS.port2);
expect(configuration.getOption('src')).eql([OPTIONS.src]);
expect(configuration.getOption('browser')).eql(OPTIONS.browser);
});

it('Custom ts config is not well-formed', async () => {
let message = '';

fs.writeFileSync(CUSTOM_TESTCAFE_CONFIG_FILE_PATH, 'const a: string = 1;');

configuration = new TestCafeConfiguration(CUSTOM_TESTCAFE_CONFIG_FILE_PATH);

try {
await configuration.init();
}
catch (err) {
message = err.message;
}

expect(message).contains('custom11.testcaferc.ts (1, 7): ' +
'Type \'number\' is not assignable to type \'string\'.\n',
);
});

it('Custom ts config path is used and compiler options defined in tsconfig.json', async () => {
const tsOptions = JSON.stringify({
compilerOptions: {
noImplicitAny: true,
},
});

fs.writeFileSync(CUSTOM_TS_CONFIG_FILE_PATH, tsOptions);

configuration = new TestCafeConfiguration(CUSTOM_TESTCAFE_CONFIG_FILE_PATH);

let err;

try {
await configuration.init({
compilerOptions: {
configPath: CUSTOM_TS_CONFIG_FILE_PATH,
},
});
}
catch (e) {
err = e;
}

expect(err.message).contains('custom11.testcaferc.ts (9, 18): Parameter \'p\' implicitly has an \'any\' type.');
});

it('Custom ts config path is used and compiler options passed as argument', async () => {
configuration = new TestCafeConfiguration(CUSTOM_TESTCAFE_CONFIG_FILE_PATH);

let err;

try {
await configuration.init({
compilerOptions: {
options: {
noImplicitAny: true,
},
},
});
}
catch (e) {
err = e;
}

expect(err.message).contains('custom11.testcaferc.ts (9, 18): Parameter \'p\' implicitly has an \'any\' type.');
});

it('Custom ts config path is used and custom compiler path passed as argument', async () => {
configuration = new TestCafeConfiguration(CUSTOM_TESTCAFE_CONFIG_FILE_PATH);

let err;

try {
await configuration.init({
compilerOptions: {
customCompilerModulePath: 'non-existing-ts-compiler',
},
});
}
catch (e) {
err = e;
}

expect(err.message).contains('Cannot load the TypeScript compiler.\n' +
'Cannot find module \'non-existing-ts-compiler\'',
);
});

it('Custom ts config is used with compiler options passed as arguments and tsconfig.json', async () => {
const tsOptions = JSON.stringify({
compilerOptions: {
noImplicitAny: false,
},
});

fs.writeFileSync(CUSTOM_TS_CONFIG_FILE_PATH, tsOptions);

configuration = new TestCafeConfiguration(CUSTOM_TESTCAFE_CONFIG_FILE_PATH);

let err;

try {
await configuration.init({
compilerOptions: {
configPath: CUSTOM_TS_CONFIG_FILE_PATH,
options: {
noImplicitAny: true,
},
},
});
}
catch (e) {
err = e;
}

expect(err.message).contains('custom11.testcaferc.ts (9, 18): Parameter \'p\' implicitly has an \'any\' type.');
});
});

it('Custom config path is used', () => {
const customConfigFile = 'custom11.testcaferc.json';

Expand Down
7 changes: 7 additions & 0 deletions test/server/data/configuration/module/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function module() {
return 'module';
}

module.exports = {
module: module,
}
Loading