Skip to content

Commit 7f80cfa

Browse files
authored
Promise based helper methods (#3465)
* generate both regular and promise-based helper definitions * load types according to feature 'fullBasedPromised' running npm def * fix runner-related tests * review documentation + add code comments * make the generations of TS definition clearer * fix unit tests related to Init Command
1 parent 8d27e08 commit 7f80cfa

20 files changed

+590
-247
lines changed

Diff for: .gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ testpullfilecache*
2121
package-lock.json
2222
yarn.lock
2323
/.vs
24-
typings/types.d.ts
24+
typings/types.d.ts
25+
typings/promiseBasedTypes.d.ts

Diff for: docs/typescript.md

+12
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,15 @@ declare namespace CodeceptJS {
138138
}
139139
}
140140
```
141+
142+
## Full promise-based methods <Badge text="Since 3.3.6" type="warning"/>
143+
144+
All CodeceptJS methods return a promise; however, some of its are not typed as accordingly.
145+
This feature, which is enabled by [configuration](https://codecept.io/configuration/), refers to alternative typescript definitions transforming all methods to asynchronous actions.
146+
147+
How to enable it?
148+
- Add required configuration
149+
```ts
150+
fullPromiseBased: true;
151+
```
152+
- Refresh internal TypeScript definitions by running following command: `npx codeceptjs def`

Diff for: lib/command/definitions.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ module.exports = function (genPath, options) {
138138
helperPaths[name] = require;
139139
helperNames.push(name);
140140
} else {
141-
helperNames.push(name);
141+
const fullBasedPromised = codecept.config.fullPromiseBased;
142+
helperNames.push(fullBasedPromised === true ? `${name}Ts` : name);
142143
}
143144

144145
if (!actingHelpers.includes(name)) {

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"lint-fix": "eslint bin/ examples/ lib/ test/ translations/ runok.js --fix",
4343
"docs": "./runok.js docs",
4444
"test:unit": "mocha test/unit --recursive",
45-
"test:runner": "mocha test/runner --recursive",
45+
"test:runner": "mocha test/runner --recursive --timeout 5000",
4646
"test": "npm run test:unit && npm run test:runner",
4747
"test:appium-quick": "mocha test/helper/Appium_test.js --grep 'quick'",
4848
"test:appium-other": "mocha test/helper/Appium_test.js --grep 'second'",

Diff for: runok.js

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ module.exports = {
3737

3838
async defTypings() {
3939
console.log('Generate TypeScript definition');
40+
// Generate definitions for promised-based helper methods
41+
await npx('jsdoc -c typings/jsdocPromiseBased.conf.js');
42+
fs.renameSync('typings/types.d.ts', 'typings/promiseBasedTypes.d.ts');
43+
// Generate all other regular definitions
4044
await npx('jsdoc -c typings/jsdoc.conf.js');
4145
},
4246

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
exports.config = {
2+
tests: './*_test.js',
3+
timeout: 10000,
4+
output: './output',
5+
helpers: {
6+
FileSystem: {},
7+
},
8+
include: {},
9+
bootstrap: false,
10+
mocha: {},
11+
name: 'sandbox',
12+
fullPromiseBased: true,
13+
};

Diff for: test/runner/bdd_test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ describe('BDD Gherkin', () => {
193193
});
194194

195195
it('should show all available steps', (done) => {
196-
exec(`${runner} gherkin:steps --config ${codecept_dir}/codecept.bdd.json`, (err, stdout, stderr) => { //eslint-disable-line
196+
exec(`${runner} gherkin:steps --config ${codecept_dir}/codecept.bdd.js`, (err, stdout, stderr) => { //eslint-disable-line
197197
stdout.should.include('Gherkin');
198198
stdout.should.include('/I have product with \\$(\\d+) price/');
199199
stdout.should.include('step_definitions/my_steps.js:3:1');
@@ -206,7 +206,7 @@ describe('BDD Gherkin', () => {
206206
});
207207

208208
it('should generate snippets for missing steps', (done) => {
209-
exec(`${runner} gherkin:snippets --dry-run --config ${codecept_dir}/codecept.dummy.bdd.json`, (err, stdout, stderr) => { //eslint-disable-line
209+
exec(`${runner} gherkin:snippets --dry-run --config ${codecept_dir}/codecept.dummy.bdd.js`, (err, stdout, stderr) => { //eslint-disable-line
210210
stdout.should.include(`Given('I open a browser on a site', () => {
211211
// From "support/dummy.feature" {"line":4,"column":5}
212212
throw new Error('Not implemented yet');
@@ -277,7 +277,7 @@ When(/^I define a step with a \\( paren and a "(.*?)" string$/, () => {
277277
});
278278

279279
it('should not generate duplicated steps', (done) => {
280-
exec(`${runner} gherkin:snippets --dry-run --config ${codecept_dir}/codecept.duplicate.bdd.json`, (err, stdout, stderr) => { //eslint-disable-line
280+
exec(`${runner} gherkin:snippets --dry-run --config ${codecept_dir}/codecept.duplicate.bdd.js`, (err, stdout, stderr) => { //eslint-disable-line
281281
assert.equal(stdout.match(/I open a browser on a site/g).length, 1);
282282
assert(!err);
283283
done();

Diff for: test/runner/before_failure_test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const exec = require('child_process').exec;
33

44
const runner = path.join(__dirname, '/../../bin/codecept.js');
55
const codecept_dir = path.join(__dirname, '/../data/sandbox');
6-
const codecept_run = `${runner} run --config ${codecept_dir}/codecept.beforetest.failure.json `;
6+
const codecept_run = `${runner} run --config ${codecept_dir}/codecept.beforetest.failure.js `;
77

88
describe('Failure in before', function () {
99
this.timeout(5000);

Diff for: test/runner/definitions_test.js

+32-8
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('Definitions', function () {
3434

3535
describe('Static files', () => {
3636
it('should have internal object that is available as variable codeceptjs', (done) => {
37-
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.json`, () => {
37+
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.js`, () => {
3838
const types = typesFrom(`${codecept_dir}/steps.d.ts`);
3939
types.should.be.valid;
4040

@@ -79,7 +79,7 @@ describe('Definitions', function () {
7979
});
8080

8181
it('def should create definition file with correct page def', (done) => {
82-
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.json`, (err, stdout) => {
82+
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.js`, (err, stdout) => {
8383
stdout.should.include('Definitions were generated in steps.d.ts');
8484
const types = typesFrom(`${codecept_dir}/steps.d.ts`);
8585
types.should.be.valid;
@@ -94,7 +94,7 @@ describe('Definitions', function () {
9494
});
9595

9696
it('def should create definition file given a config file', (done) => {
97-
exec(`${runner} def --config ${codecept_dir}/../../codecept.ddt.json`, (err, stdout) => {
97+
exec(`${runner} def --config ${codecept_dir}/../../codecept.ddt.js`, (err, stdout) => {
9898
stdout.should.include('Definitions were generated in steps.d.ts');
9999
const types = typesFrom(`${codecept_dir}/../../steps.d.ts`);
100100
types.should.be.valid;
@@ -104,7 +104,7 @@ describe('Definitions', function () {
104104
});
105105

106106
it('def should create definition file with support object', (done) => {
107-
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.json`, () => {
107+
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.js`, () => {
108108
const types = typesFrom(`${codecept_dir}/steps.d.ts`);
109109
types.should.be.valid;
110110

@@ -128,7 +128,7 @@ describe('Definitions', function () {
128128
});
129129

130130
it('def should create definition file with inject which contains support objects', (done) => {
131-
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.json`, () => {
131+
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.js`, () => {
132132
const types = typesFrom(`${codecept_dir}/steps.d.ts`);
133133
types.should.be.valid;
134134

@@ -145,7 +145,7 @@ describe('Definitions', function () {
145145
});
146146

147147
it('def should create definition file with inject which contains I object', (done) => {
148-
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.json`, (err) => {
148+
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.js`, (err) => {
149149
assert(!err);
150150
const types = typesFrom(`${codecept_dir}/steps.d.ts`);
151151
types.should.be.valid;
@@ -165,7 +165,7 @@ describe('Definitions', function () {
165165
});
166166

167167
it('def should create definition file with inject which contains I object from helpers', (done) => {
168-
exec(`${runner} def --config ${codecept_dir}//codecept.inject.powi.json`, () => {
168+
exec(`${runner} def --config ${codecept_dir}/codecept.inject.powi.js`, () => {
169169
const types = typesFrom(`${codecept_dir}/steps.d.ts`);
170170
types.should.be.valid;
171171

@@ -179,7 +179,7 @@ describe('Definitions', function () {
179179
});
180180

181181
it('def should create definition file with callback params', (done) => {
182-
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.json`, () => {
182+
exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.js`, () => {
183183
const types = typesFrom(`${codecept_dir}/steps.d.ts`);
184184
types.should.be.valid;
185185

@@ -193,6 +193,30 @@ describe('Definitions', function () {
193193
done();
194194
});
195195
});
196+
197+
it('def should create definition file with promise-based feature', (done) => {
198+
exec(`${runner} def --config ${codecept_dir}/codecept.promise.based.js`, (err, stdout) => {
199+
stdout.should.include('Definitions were generated in steps.d.ts');
200+
const types = typesFrom(`${codecept_dir}/steps.d.ts`);
201+
types.should.be.valid;
202+
203+
const definitionFile = types.getSourceFileOrThrow(`${codecept_dir}/steps.d.ts`);
204+
const extend = getExtends(definitionFile.getNamespaceOrThrow('CodeceptJS').getInterfaceOrThrow('I'));
205+
extend.should.containSubset([{
206+
methods: [{
207+
name: 'amInPath',
208+
returnType: 'Promise<any>',
209+
parameters: [{ name: 'openPath', type: 'string' }],
210+
}, {
211+
name: 'seeFile',
212+
returnType: 'Promise<any>',
213+
parameters: [{ name: 'name', type: 'string' }],
214+
}],
215+
}]);
216+
assert(!err);
217+
done();
218+
});
219+
});
196220
});
197221

198222
/** @type {Chai.ChaiPlugin */

Diff for: test/runner/init_test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ describe('Init Command', function () {
3535
});
3636

3737
it('init - Where should logs, screenshots, and reports to be stored? (./output)', async () => {
38-
const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER]);
38+
const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER]);
3939
result.should.include('? What helpers do you want to use? REST');
4040
result.should.include('Where should logs, screenshots, and reports to be stored? (./output)');
4141
});
4242

4343
it('init - Do you want localization for tests? (See https://codecept.io/translation/)', async () => {
44-
const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER]);
44+
const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER, ENTER]);
4545
result.should.include('? Do you want localization for tests? (See https://codecept.io/translation/)');
4646
result.should.include('❯ English (no localization)');
4747
for (const item of ['de-DE', 'it-IT', 'fr-FR', 'ja-JP', 'pl-PL', 'pt-BR']) {
@@ -51,7 +51,7 @@ describe('Init Command', function () {
5151
});
5252

5353
it('init - [REST] Endpoint of API you are going to test (http://localhost:3000/api)', async () => {
54-
const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER, ENTER]);
54+
const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER, ENTER, ENTER]);
5555
result.should.include('Do you want localization for tests? (See https://codecept.io/translation/) Eng');
5656
result.should.include('Configure helpers...');
5757
result.should.include('? [REST] Endpoint of API you are going to test (http://localhost:3000/api)');

Diff for: test/runner/run_multiple_test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const exec = require('child_process').exec;
55

66
const runner = path.join(__dirname, '/../../bin/codecept.js');
77
const codecept_dir = path.join(__dirname, '/../data/sandbox');
8-
const codecept_run = `${runner} run-multiple --config ${codecept_dir}/codecept.multiple.json `;
8+
const codecept_run = `${runner} run-multiple --config ${codecept_dir}/codecept.multiple.js `;
99

1010
describe('CodeceptJS Multiple Runner', function () {
1111
this.timeout(40000);
@@ -175,7 +175,7 @@ describe('CodeceptJS Multiple Runner', function () {
175175

176176
it('should exit with non-zero code for failures during init process', (done) => {
177177
process.chdir(codecept_dir);
178-
exec(`${runner} run-multiple --config codecept.multiple.initFailure.json default --all`, (err, stdout) => {
178+
exec(`${runner} run-multiple --config codecept.multiple.initFailure.js default --all`, (err, stdout) => {
179179
expect(err).not.toBeFalsy();
180180
expect(err.code).toBe(1);
181181
expect(stdout).toContain('Failed on FailureHelper');
@@ -235,7 +235,7 @@ describe('CodeceptJS Multiple Runner', function () {
235235

236236
it('should be executed with several module when described', (done) => {
237237
process.chdir(codecept_dir);
238-
exec(`${runner} ${_codecept_run}/codecept.require.multiple.several.json default`, (err, stdout) => {
238+
exec(`${runner} ${_codecept_run}/codecept.require.multiple.several.js default`, (err, stdout) => {
239239
stdout.should.include(moduleOutput);
240240
stdout.should.include(moduleOutput2);
241241
(stdout.match(new RegExp(moduleOutput, 'g')) || []).should.have.lengthOf(2);

Diff for: test/runner/timeout_test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const debug_this_test = false;
66

77
const config_run_config = (config, grep, verbose = false) => `${codecept_run} ${verbose || debug_this_test ? '--verbose' : ''} --config ${codecept_dir}/configs/timeouts/${config} ${grep ? `--grep "${grep}"` : ''}`;
88

9-
describe.only('CodeceptJS Timeouts', function () {
9+
describe('CodeceptJS Timeouts', function () {
1010
this.timeout(10000);
1111

1212
it('should stop test when timeout exceeded', (done) => {

Diff for: typings/index.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Project: https://github.com/codeception/codeceptjs/
22
/// <reference path="./types.d.ts" />
3+
/// <reference path="./promiseBasedTypes.d.ts" />
34
/// <reference types="webdriverio" />
45
/// <reference path="./Mocha.d.ts" />
56
/// <reference types="joi" />
@@ -260,6 +261,13 @@ declare namespace CodeceptJS {
260261
steps: string | Array<string>
261262
};
262263

264+
/**
265+
* Enable full promise-based helper methods for [TypeScript](https://codecept.io/typescript/) project.
266+
* If true, all helper methods are typed as asynchronous;
267+
* Otherwise, it remains as it works in versions prior to 3.3.6
268+
*/
269+
fullPromiseBased?: boolean;
270+
263271
[key: string]: any;
264272
};
265273

Diff for: typings/jsdoc.promiseBased.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Helps tsd-jsdoc to exports all helpers methods as Promise
2+
// - Before parsing JS file, change class name and remove configuration already exported
3+
// - For each method set by default 'Promise<any>' if there is no returns tag or if the returns tag doesn't handle a promise
4+
5+
const isHelper = (path) => path.includes('docs/build');
6+
const isDocumentedMethod = (doclet) => doclet.undocumented !== true
7+
&& doclet.kind === 'function'
8+
&& doclet.scope === 'instance';
9+
const shouldOverrideReturns = (doclet) => !doclet.returns
10+
|| !doclet.returns[0].type
11+
|| !doclet.returns[0].type.names[0].includes('Promise');
12+
13+
module.exports = {
14+
handlers: {
15+
beforeParse(e) {
16+
if (isHelper(e.filename)) {
17+
e.source = e.source
18+
// add 'Ts' suffix to generate promise-based helpers definition
19+
.replace(/class (.*) extends/, 'class $1Ts extends')
20+
// rename parent class to fix the inheritance
21+
.replace(/(@augments \w+)/, '$1Ts')
22+
// do not export twice the configuration of the helpers
23+
.replace(/\/\*\*(.+?(?=config))config = \{\};/s, '');
24+
}
25+
},
26+
newDoclet: ({ doclet }) => {
27+
if (isHelper(doclet.meta.path)
28+
&& isDocumentedMethod(doclet)
29+
&& shouldOverrideReturns(doclet)) {
30+
doclet.returns = [];
31+
doclet.addTag('returns', '{Promise<any>}');
32+
}
33+
},
34+
},
35+
};

Diff for: typings/jsdocPromiseBased.conf.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
source: {
3+
include: [
4+
'./docs/build',
5+
],
6+
},
7+
opts: {
8+
template: 'node_modules/tsd-jsdoc/dist',
9+
recurse: true,
10+
destination: './typings/',
11+
},
12+
plugins: ['jsdoc.promiseBased.js', 'jsdoc.namespace.js', 'jsdoc-typeof-plugin'],
13+
};

0 commit comments

Comments
 (0)