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

CoffeeScript support (close #1556) #2651

Merged
merged 7 commits into from
Aug 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"chalk": "^1.1.0",
"chrome-emulated-devices-list": "^0.1.0",
"chrome-remote-interface": "^0.25.3",
"coffeescript": "^2.3.1",
"commander": "^2.8.1",
"debug": "^2.2.0",
"dedent": "^0.4.0",
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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';
Expand All @@ -18,6 +19,7 @@ var testFileCompilers = [
new LegacyTestFileCompiler(hammerhead.processScript),
new EsNextTestFileCompiler(),
new TypeScriptTestFileCompiler(),
new CoffeeScriptTestFileCompiler(),
new RawTestFileCompiler()
];

Expand Down
41 changes: 41 additions & 0 deletions src/compiler/test-file/formats/coffeescript/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import CoffeeScript from 'coffeescript';
import loadBabelLibs from '../../../load-babel-libs';
import ESNextTestFileCompiler from '../es-next/compiler.js';

const FIXTURE_RE = /(^|;|\s+)fixture\s*(\.|\(|'|")/;
const TEST_RE = /(^|;|\s+)test\s*/;

export default class CoffeeScriptTestFileCompiler extends ESNextTestFileCompiler {
_hasTests (code) {
return FIXTURE_RE.test(code) && TEST_RE.test(code);
}

_compileCode (code, filename) {
if (this.cache[filename])
return this.cache[filename];

var transpiled = CoffeeScript.compile(code, {
filename,
bare: true,
sourceMap: true,
inlineMap: true,
header: false
});

var { babel } = loadBabelLibs();
var babelOptions = ESNextTestFileCompiler.getBabelOptions(filename, code);
var compiled = babel.transform(transpiled.js, babelOptions);

this.cache[filename] = compiled.code;

return compiled.code;
}

_getRequireCompilers () {
return { '.coffee': (code, filename) => this._compileCode(code, filename) };
}

getSupportedExtension () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's okay to override the canCompile method if you want to support files with different extensions, as you stated in your comment in the nodejs repository. Our Legacy API compiler does it: testcafe-legacy-api/blob/master/src/compiler/index.js#L157

return '.coffee';
}
}
29 changes: 29 additions & 0 deletions src/compiler/test-file/formats/coffeescript/get-test-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import CoffeeScript from 'coffeescript';
import { transform } from 'babel-core';
import ESNextTestFileCompiler from '../es-next/compiler.js';
import { EsNextTestFileParser } from '../es-next/get-test-list';

export class CoffeeScriptTestFileParser extends EsNextTestFileParser {
parse (code) {
const babelOptions = ESNextTestFileCompiler.getBabelOptions(null, code);

delete babelOptions.filename;
babelOptions.ast = true;

code = CoffeeScript.compile(code, {
bare: true,
sourceMap: false,
inlineMap: false,
header: false
});

const ast = transform(code, babelOptions).ast;

return this.analyze(ast.program.body);
}
}

const parser = new CoffeeScriptTestFileParser();

export const getCoffeeScriptTestList = parser.getTestList.bind(parser);
export const getCoffeeScriptTestListFromCode = parser.getTestListFromCode.bind(parser);
3 changes: 2 additions & 1 deletion src/compiler/test-file/formats/es-next/get-test-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ const TOKEN_TYPE = {
ArrowFunctionExpression: 'ArrowFunctionExpression',
FunctionExpression: 'FunctionExpression',
ExpressionStatement: 'ExpressionStatement',
ReturnStatement: 'ReturnStatement',
FunctionDeclaration: 'FunctionDeclaration',
VariableDeclaration: 'VariableDeclaration'
};

class EsNextTestFileParser extends TestFileParserBase {
export class EsNextTestFileParser extends TestFileParserBase {
constructor () {
super(TOKEN_TYPE);
}
Expand Down
10 changes: 8 additions & 2 deletions src/compiler/test-file/test-file-parser-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,19 @@ export class TestFileParserBase {

return this.getFunctionBody(token).map(this.analyzeToken, this);

case tokenType.VariableDeclaration:
return this.analyzeToken(this.getRValue(token));
case tokenType.VariableDeclaration: {
const variableValue = this.getRValue(token); // Skip variable declarations like `var foo;`

return variableValue ? this.analyzeToken(variableValue) : null;
}

case tokenType.CallExpression:
case tokenType.PropertyAccessExpression:
case tokenType.TaggedTemplateExpression:
return this.analyzeFnCall(token);

case tokenType.ReturnStatement:
return token.argument ? this.analyzeToken(token.argument) : null;
}

return null;
Expand Down
3 changes: 3 additions & 0 deletions src/embedding-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import COMMAND_TYPE from './test-run/commands/type';
import Assignable from './utils/assignable';
import { getTestList, getTestListFromCode } from './compiler/test-file/formats/es-next/get-test-list';
import { getTypeScriptTestList, getTypeScriptTestListFromCode } from './compiler/test-file/formats/typescript/get-test-list';
import { getCoffeeScriptTestList, getCoffeeScriptTestListFromCode } from './compiler/test-file/formats/coffeescript/get-test-list';
import { initSelector } from './test-run/commands/validations/initializers';

export default {
getTestList,
getTypeScriptTestList,
getCoffeeScriptTestList,
getTestListFromCode,
getTypeScriptTestListFromCode,
getCoffeeScriptTestListFromCode,
TestRunErrorFormattableAdapter,
TestRun,
testRunErrors,
Expand Down
15 changes: 15 additions & 0 deletions test/functional/fixtures/api/coffeescript/smoke/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
var expect = require('chai').expect;

describe('[CoffeeScript] Smoke tests', function () {
it('Should run non-trivial tests', function () {
return runTests('./testcafe-fixtures/non-trivial-test.coffee', null, { selectorTimeout: 5000 });
});

it('Should produce correct callsites on error', function () {
return runTests('./testcafe-fixtures/callsite-test.coffee', null, { shouldFail: true })
.catch(function (errs) {
expect(errs[0]).contains('The specified selector does not match any element in the DOM tree.');
expect(errs[0]).contains('> 5 |doSmthg = (selector, t) -> await t.click selector');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'testcafe'

fixture 'CoffeeScript callsites'

doSmthg = (selector, t) -> await t.click selector

test 'Test', (t) =>
await doSmthg '#heyheyhey', t
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Selector, Role, t } from 'testcafe'

iframeElement = Selector '#element-in-iframe'
pageElement = Selector '#element-on-page'
showAlertBtn = Selector '#show-alert'

initConfiguration = ->
await t
.setNativeDialogHandler => on
.click showAlertBtn

history = await t.getNativeDialogHistory()

await t
.expect(history[0].text).eql 'Hey!'
.switchToIframe '#iframe'
.expect(iframeElement.exists).ok()
.setTestSpeed 0.95
.setPageLoadTimeout 95

t.ctx['someVal'] = 'ctxVal'
t.fixtureCtx['someVal'] = 'fixtureCtxVal'

role1 = Role 'http://localhost:3000/fixtures/api/es-next/roles/pages/index.html', =>
await t
.setNativeDialogHandler => on

await t
.expect(pageElement.exists).ok()
.expect(t.ctx['someVal']).notOk()
.expect(t.fixtureCtx['someVal']).notOk()

await t.click showAlertBtn

role2 = Role 'http://localhost:3000/fixtures/api/es-next/roles/pages/index.html', =>

fixture 'CoffeeScript smoke tests'
.page 'http://localhost:3000/fixtures/api/es-next/roles/pages/index.html'

test 'Clear configuration', =>
await initConfiguration()
await t.useRole role1

test 'Restore configuration', =>
await initConfiguration()

await t
.useRole role2
.expect(iframeElement.exists).ok()
.expect(t.ctx['someVal']).eql 'ctxVal'
.expect(t.fixtureCtx['someVal']).eql 'fixtureCtxVal'

await t
.switchToMainWindow()
.click showAlertBtn

history = await t.getNativeDialogHistory()

await t.expect(history[0].text).eql 'Hey!'
58 changes: 58 additions & 0 deletions test/server/compiler-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,64 @@ describe('Compiler', function () {
});


describe('CoffeeScript', function () {
it('Should compile test files and their dependencies', function () {
var sources = [
'test/server/data/test-suites/coffeescript-basic/testfile1.coffee',
'test/server/data/test-suites/coffeescript-basic/testfile2.coffee'
];

return compile(sources)
.then(function (compiled) {
var testfile1 = path.resolve('test/server/data/test-suites/coffeescript-basic/testfile1.coffee');
var testfile2 = path.resolve('test/server/data/test-suites/coffeescript-basic/testfile2.coffee');

var tests = compiled.tests;
var fixtures = compiled.fixtures;

expect(tests.length).eql(4);
expect(fixtures.length).eql(3);

expect(fixtures[0].name).eql('Fixture1');
expect(fixtures[0].path).eql(testfile1);
expect(fixtures[0].pageUrl).eql('about:blank');

expect(fixtures[1].name).eql('Fixture2');
expect(fixtures[1].path).eql(testfile1);
expect(fixtures[1].pageUrl).eql('http://example.org');

expect(fixtures[2].name).eql('Fixture3');
expect(fixtures[2].path).eql(testfile2);
expect(fixtures[2].pageUrl).eql('https://example.com');

expect(tests[0].name).eql('Fixture1Test1');
expect(tests[0].fixture).eql(fixtures[0]);

expect(tests[1].name).eql('Fixture1Test2');
expect(tests[1].fixture).eql(fixtures[0]);

expect(tests[2].name).eql('Fixture2Test1');
expect(tests[2].fixture).eql(fixtures[1]);

expect(tests[3].name).eql('Fixture3Test1');
expect(tests[3].fixture).eql(fixtures[2]);

return Promise.all(tests.map(function (test) {
return test.fn(testRunMock);
}));
})
.then(function (results) {
expect(results).eql([
'F1T1: Hey from dep1',
'F1T2',
'F2T1',
'F3T1: Hey from dep1 and dep2'
]);
});
});
});


describe('RAW file', function () {
it('Should compile test files', function () {
var sources = ['test/server/data/test-suites/raw/test.testcafe'];
Expand Down
2 changes: 2 additions & 0 deletions test/server/data/test-suites/coffeescript-basic/dep1.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export default ->
'Hey from dep1'
4 changes: 4 additions & 0 deletions test/server/data/test-suites/coffeescript-basic/dep2.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import dep1Fn from './dep1'

export default ->
"#{await dep1Fn()} and dep2"
21 changes: 21 additions & 0 deletions test/server/data/test-suites/coffeescript-basic/testfile1.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'testcafe'
import dep1Fn from './dep1'

fixture 'Fixture1'

test 'Fixture1Test1', =>
res = await dep1Fn()
"F1T1: #{res}"

test2Name = 'Fixture1Test2'

test test2Name, =>
'F1T2'

fixture "Fixture#{1 + 1}"
.page 'http://example.org'
.beforeEach => 'yo'
.afterEach => 'yo'

test 'Fixture2Test1', =>
'F2T1'
13 changes: 13 additions & 0 deletions test/server/data/test-suites/coffeescript-basic/testfile2.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'testcafe'
import dep2Fn from './dep2'

fixture 'Fixture3'
.page 'https://example.com'
.afterEach => 'yo'
.beforeEach => 'yo'

fixture3Name = 'Fixture3Test1'

test fixture3Name, =>
res = await dep2Fn()
"F3T1: #{res}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
getPageUrl = (index) ->
"http://page/#{index}"

fixture('fixture 1').page getFixtureName(1)

doSmthg = (selector, t) ->
await t.click selector

test 'test 1', (t) =>
await doSmthg '#my-el', t

((fixtureName, testName) ->
fixture(fixtureName).page 'http://myPage'
test testName, (t) =>
) 'fixture 2', 'test 2'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fixture('fixture 1').page 'https://page'

test.before((t) =>
await t.wait 1
) 'test 1'
Loading