Skip to content

Commit 97c2237

Browse files
authored
Fixed generating typescript definitions (#1032)
* changed parser * fixed typescript definitions * fixed linting * fixed parser error
1 parent 953d252 commit 97c2237

File tree

7 files changed

+184
-24
lines changed

7 files changed

+184
-24
lines changed

lib/command/definitions.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@ const getTestRoot = require('./utils').getTestRoot;
33
const Codecept = require('../codecept');
44
const container = require('../container');
55
const methodsOfObject = require('../utils').methodsOfObject;
6-
const getParamsToString = require('../utils').getParamsToString;
76
const output = require('../output');
7+
const { toTypeDef } = require('../parser');
88
const fs = require('fs');
99
const path = require('path');
1010

1111
const template = `
1212
type ICodeceptCallback = (i: CodeceptJS.{{I}}) => void;
1313
14+
interface ILocator {
15+
xpath?: string;
16+
css?: string;
17+
name?: string;
18+
value?: string;
19+
frame?: string;
20+
android?: string;
21+
ios?: string;
22+
}
23+
1424
declare const actor: () => CodeceptJS.{{I}};
1525
declare const Feature: (string: string) => void;
1626
declare const Scenario: (string: string, callback: ICodeceptCallback) => void;
@@ -47,26 +57,25 @@ module.exports = function (genPath, options) {
4757
const helpers = container.helpers();
4858
const suppportI = container.support('I');
4959
const translations = container.translation();
50-
const methods = [];
60+
let methods = [];
5161
const actions = [];
5262
for (const name in helpers) {
5363
const helper = helpers[name];
54-
methodsOfObject(helper).forEach((action) => {
64+
for (const action of methodsOfObject(helper)) {
5565
const actionAlias = container.translation() ? container.translation().actionAliasFor(action) : action;
5666
if (!actions[actionAlias]) {
57-
const params = getParamsToString(helper[action]);
58-
methods.push(` ${(actionAlias)}: (${params}) => any; \n`);
67+
methods = methods.concat(toTypeDef(helper[action]));
5968
actions[actionAlias] = 1;
6069
}
61-
});
70+
}
6271
}
6372
for (const name in suppportI) {
6473
if (actions[name]) {
6574
continue;
6675
}
6776
const actor = suppportI[name];
68-
const params = getParamsToString(actor);
69-
methods.push(` ${(name)}: (${params}) => any; \n`);
77+
// const params = toTypeDef(actor);
78+
// methods.push(` ${(name)}: (${params}) => any; \n`);
7079
}
7180
let definitionsTemplate = template.replace('{{methods}}', methods.join(''));
7281
definitionsTemplate = definitionsTemplate.replace(/\{\{I\}\}/g, container.translation().I);

lib/command/list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const getConfig = require('./utils').getConfig;
22
const getTestRoot = require('./utils').getTestRoot;
33
const Codecept = require('../codecept');
44
const container = require('../container');
5-
const getParamsToString = require('../utils').getParamsToString;
5+
const getParamsToString = require('../parser').getParamsToString;
66
const methodsOfObject = require('../utils').methodsOfObject;
77
const output = require('../output');
88

lib/parser.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const parser = require('parse-function')({ ecmaVersion: 2017 });
2+
const { flatTail } = require('./utils');
3+
4+
module.exports.getParamsToString = function (fn) {
5+
return getParams(fn).join(', ');
6+
};
7+
8+
module.exports.toTypeDef = function (fn) {
9+
const types = [];
10+
const paramNames = [];
11+
const params = getParams(fn);
12+
for (const key in params) {
13+
let paramName = params[key];
14+
const paramValue = paramName.split('=');
15+
const isOptional = paramValue.length !== 1;
16+
if (isOptional) {
17+
paramName = paramValue[0];
18+
}
19+
paramNames[key] = paramName;
20+
if (isOptional) {
21+
paramNames[key] += '?';
22+
}
23+
types[key] = guessParamTypes(paramName);
24+
}
25+
26+
let combinedTypes = types[types.length - 1];
27+
if (!combinedTypes) return methodSignature(fn);
28+
if (types.length === 1) return methodSignature(fn, paramNames, combinedTypes);
29+
30+
31+
for (let i = types.length - 2; i >= 0; i--) {
32+
combinedTypes = flatTail(combinedTypes, types[i]);
33+
}
34+
return combinedTypes.map(type => methodSignature(fn, paramNames, type)).join('');
35+
};
36+
37+
function methodSignature(fn, paramNames, params) {
38+
if (!params) params = [];
39+
params = params.map((p, i) => `${paramNames[i]}: ${p}`).join(', ');
40+
let returnType = 'void';
41+
if (fn.name.indexOf('grab') === 0) {
42+
returnType = 'Promise';
43+
}
44+
return ` ${fn.name}(${params}) : ${returnType},\n`;
45+
}
46+
47+
function guessParamTypes(paramName) {
48+
switch (paramName) {
49+
case 'fn':
50+
return ['Function'];
51+
case 'locator':
52+
case 'field':
53+
case 'context':
54+
case 'select':
55+
return ['ILocator', 'string'];
56+
case 'num':
57+
case 'sec':
58+
case 'width':
59+
case 'height':
60+
return ['number'];
61+
}
62+
return ['string'];
63+
}
64+
65+
function getParams(fn) {
66+
if (fn.isSinonProxy) return [];
67+
const newFn = fn.toString().replace(/^async/, 'async function');
68+
try {
69+
const reflected = parser.parse(newFn);
70+
const params = reflected.args.map((p) => {
71+
const def = reflected.defaults[p];
72+
if (def) {
73+
return `${p}=${def}`;
74+
}
75+
return p;
76+
});
77+
return params;
78+
} catch (err) {
79+
console.error(err, newFn);
80+
}
81+
}
82+

lib/utils.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
const fs = require('fs');
22
const path = require('path');
3-
const getParams = require('js-function-reflector');
43
const getFunctionArguments = require('fn-args');
54
const { convertColorToRGBA, isColorProperty } = require('./colorUtils');
65

6+
function getParams(fn) {
7+
return parser.parse(fn);
8+
}
9+
710
function isObject(item) {
811
return item && typeof item === 'object' && !Array.isArray(item);
912
}
1013

14+
function flatTail(flatParams, params) {
15+
const res = [];
16+
for (const fp of flatParams) {
17+
for (const p of params) {
18+
res.push([p].concat(fp));
19+
}
20+
}
21+
return res;
22+
}
23+
24+
module.exports.flatTail = flatTail;
25+
1126
function deepMerge(target, source) {
1227
if (isObject(target) && isObject(source)) {
1328
for (const key in source) {
@@ -64,15 +79,6 @@ module.exports.getParamNames = function (fn) {
6479
};
6580

6681

67-
module.exports.getParamsToString = function (fn) {
68-
if (fn.isSinonProxy) return [];
69-
const params = getParams(fn).args.map(p => (Array.isArray(p) ? `${p[0]}=${p[1]}` : p));
70-
if (isGenerator(fn) && params[0] && params[0][0] === '*') {
71-
params[0] = params[2].substr(2);
72-
}
73-
return params.join(', ');
74-
};
75-
7682
module.exports.installedLocally = function () {
7783
return path.resolve(`${__dirname}/../`).indexOf(process.cwd()) === 0;
7884
};

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,17 @@
3535
"test": "npm run lint && mocha test/unit && mocha test/runner"
3636
},
3737
"dependencies": {
38+
"acorn": "^5.5.3",
3839
"chalk": "^1.1.3",
3940
"commander": "^2.14.1",
4041
"escape-string-regexp": "^1.0.3",
4142
"fn-args": "^3.0.0",
4243
"glob": "^6.0.1",
4344
"inquirer": "^0.11.0",
44-
"js-function-reflector": "^1.3.1",
45+
"js-function-reflector": "^1.3.0",
4546
"mkdirp": "^0.5.1",
4647
"mocha": "^4.1.0",
48+
"parse-function": "^5.2.7",
4749
"promise-retry": "^1.1.1",
4850
"requireg": "^0.1.5"
4951
},

test/runner/list_test.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const fs = require('fs');
12
const assert = require('assert');
23
const path = require('path');
34
const exec = require('child_process').exec;
@@ -18,29 +19,32 @@ describe('list/def commands', () => {
1819

1920
it('def should create definition file', (done) => {
2021
try {
21-
require('fs').unlinkSync(`${codecept_dir}/steps.d.ts`);
22+
fs.unlinkSync(`${codecept_dir}/steps.d.ts`);
2223
} catch (e) {
2324
// continue regardless of error
2425
}
2526
exec(`${runner} def ${codecept_dir}`, (err, stdout, stderr) => {
2627
stdout.should.include('Definitions were generated in steps.d.ts');
2728
stdout.should.include('<reference path="./steps.d.ts" />');
28-
require('fs').existsSync(`${codecept_dir}/steps.d.ts`).should.be.ok;
29+
fs.existsSync(`${codecept_dir}/steps.d.ts`).should.be.ok;
30+
const def = fs.readFileSync(`${codecept_dir}/steps.d.ts`).toString();
31+
def.should.include('amInPath(openPath: string) : void');
32+
def.should.include(' seeFile(name: string) : void');
2933
assert(!err);
3034
done();
3135
});
3236
});
3337

3438
it('def should create definition file given a config file', (done) => {
3539
try {
36-
require('fs').unlinkSync(`${codecept_dir}/steps.d.ts`);
40+
fs.unlinkSync(`${codecept_dir}/steps.d.ts`);
3741
} catch (e) {
3842
// continue regardless of error
3943
}
4044
exec(`${runner} def --config ${codecept_dir}/codecept.ddt.json`, (err, stdout, stderr) => {
4145
stdout.should.include('Definitions were generated in steps.d.ts');
4246
stdout.should.include('<reference path="./steps.d.ts" />');
43-
require('fs').existsSync(`${codecept_dir}/steps.d.ts`).should.be.ok;
47+
fs.existsSync(`${codecept_dir}/steps.d.ts`).should.be.ok;
4448
assert(!err);
4549
done();
4650
});

test/unit/parser_test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const parser = require('../../lib/parser');
2+
const assert = require('assert');
3+
const chai = require('chai');
4+
5+
const expect = chai.expect;
6+
7+
class Obj {
8+
method1(locator, sec) {}
9+
method2(locator, value, sec) {}
10+
method3(locator, context) {}
11+
async method4(locator, context) {
12+
return false;
13+
}
14+
}
15+
16+
describe('parser', () => {
17+
const obj = new Obj();
18+
19+
describe('#getParamsToString', () => {
20+
it('should get params for normal function', () => {
21+
expect(parser.getParamsToString(obj.method1)).to.eql('locator, sec');
22+
});
23+
it('should get params for async function', () => {
24+
expect(parser.getParamsToString(obj.method4)).to.eql('locator, context');
25+
});
26+
});
27+
28+
describe('#toTypeDef', () => {
29+
it('should transform function to TS types', () => {
30+
const res = parser.toTypeDef(obj.method1);
31+
expect(res).to.include(' method1(locator: ILocator, sec: number) : void');
32+
expect(res).to.include(' method1(locator: string, sec: number) : void');
33+
});
34+
35+
it('should transform function to TS types', () => {
36+
const res = parser.toTypeDef(obj.method2);
37+
expect(res).to.include('method2(locator: ILocator, value: string, sec: number) : void');
38+
expect(res).to.include('method2(locator: string, value: string, sec: number) : void');
39+
});
40+
41+
it('should transform function to TS types', () => {
42+
const res = parser.toTypeDef(obj.method3);
43+
expect(res).to.include('method3(locator: ILocator, context: ILocator) : void');
44+
expect(res).to.include('method3(locator: ILocator, context: string) : void');
45+
expect(res).to.include('method3(locator: string, context: ILocator) : void');
46+
expect(res).to.include('method3(locator: string, context: string) : void');
47+
});
48+
49+
it('should transform function to TS types', () => {
50+
const res = parser.toTypeDef(obj.method4);
51+
expect(res).to.include('method4(locator: ILocator, context: ILocator) : void');
52+
expect(res).to.include('method4(locator: ILocator, context: string) : void');
53+
expect(res).to.include('method4(locator: string, context: ILocator) : void');
54+
expect(res).to.include('method4(locator: string, context: string) : void');
55+
});
56+
});
57+
});

0 commit comments

Comments
 (0)