From dcba5f1f1c6429a8bce2ff9aae71c02a6ffa1c2b Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sat, 4 Jan 2020 20:50:49 +1300 Subject: [PATCH] feat(rules): add .concurrent support (#498) (#502) * feat(rules): add .concurrent support * refactor(no-focused): remove unnecessary type cast * chore(utils): inline `isConcurrentTestCase` * chore(no-focused-tests): use static checks instead of dynamic RegExp * chore(no-focused-tests): add test case for if `concurrent` is called * chore(utils): remove body from arrow functions Co-authored-by: Leonardo Villela --- .../__tests__/consistent-test-it.test.ts | 72 ++++++++++++ .../__tests__/no-commented-out-tests.test.ts | 10 ++ src/rules/__tests__/no-disabled-tests.test.ts | 10 ++ src/rules/__tests__/no-focused-tests.test.ts | 11 ++ .../__tests__/no-identical-title.test.ts | 33 ++++++ src/rules/__tests__/no-if.test.ts | 34 +++++- .../__tests__/no-standalone-expect.test.ts | 1 + src/rules/__tests__/no-test-prefixes.test.ts | 18 +++ src/rules/__tests__/no-try-expect.test.ts | 6 + src/rules/__tests__/prefer-todo.test.ts | 4 + src/rules/__tests__/valid-title.test.ts | 107 ++++++++++++++++++ src/rules/no-disabled-tests.ts | 2 + src/rules/no-focused-tests.ts | 23 +++- src/rules/no-if.ts | 5 + src/rules/utils.ts | 51 ++++----- 15 files changed, 354 insertions(+), 33 deletions(-) diff --git a/src/rules/__tests__/consistent-test-it.test.ts b/src/rules/__tests__/consistent-test-it.test.ts index 875b61791..1d5994280 100644 --- a/src/rules/__tests__/consistent-test-it.test.ts +++ b/src/rules/__tests__/consistent-test-it.test.ts @@ -24,6 +24,10 @@ ruleTester.run('consistent-test-it with fn=test', rule, { code: 'test.skip("foo")', options: [{ fn: TestCaseName.test }], }, + { + code: 'test.concurrent("foo")', + options: [{ fn: TestCaseName.test }], + }, { code: 'xtest("foo")', options: [{ fn: TestCaseName.test }], @@ -90,6 +94,20 @@ ruleTester.run('consistent-test-it with fn=test', rule, { ], output: 'test.skip("foo")', }, + { + code: 'it.concurrent("foo")', + options: [{ fn: TestCaseName.test }], + errors: [ + { + messageId: 'consistentMethod', + data: { + testKeyword: TestCaseName.test, + oppositeTestKeyword: TestCaseName.it, + }, + }, + ], + output: 'test.concurrent("foo")', + }, { code: 'it.only("foo")', options: [{ fn: TestCaseName.test }], @@ -143,6 +161,10 @@ ruleTester.run('consistent-test-it with fn=it', rule, { code: 'it.skip("foo")', options: [{ fn: TestCaseName.it }], }, + { + code: 'it.concurrent("foo")', + options: [{ fn: TestCaseName.it }], + }, { code: 'describe("suite", () => { it("foo") })', options: [{ fn: TestCaseName.it }], @@ -191,6 +213,20 @@ ruleTester.run('consistent-test-it with fn=it', rule, { ], output: 'it.skip("foo")', }, + { + code: 'test.concurrent("foo")', + options: [{ fn: TestCaseName.it }], + errors: [ + { + messageId: 'consistentMethod', + data: { + testKeyword: TestCaseName.it, + oppositeTestKeyword: TestCaseName.test, + }, + }, + ], + output: 'it.concurrent("foo")', + }, { code: 'test.only("foo")', options: [{ fn: TestCaseName.it }], @@ -236,6 +272,10 @@ ruleTester.run('consistent-test-it with fn=test and withinDescribe=it ', rule, { code: 'test.skip("foo")', options: [{ fn: TestCaseName.test, withinDescribe: TestCaseName.it }], }, + { + code: 'test.concurrent("foo")', + options: [{ fn: TestCaseName.test, withinDescribe: TestCaseName.it }], + }, { code: 'xtest("foo")', options: [{ fn: TestCaseName.test, withinDescribe: TestCaseName.it }], @@ -302,6 +342,20 @@ ruleTester.run('consistent-test-it with fn=test and withinDescribe=it ', rule, { ], output: 'describe("suite", () => { it.skip("foo") })', }, + { + code: 'describe("suite", () => { test.concurrent("foo") })', + options: [{ fn: TestCaseName.test, withinDescribe: TestCaseName.it }], + errors: [ + { + messageId: 'consistentMethodWithinDescribe', + data: { + testKeywordWithinDescribe: TestCaseName.it, + oppositeTestKeyword: TestCaseName.test, + }, + }, + ], + output: 'describe("suite", () => { it.concurrent("foo") })', + }, ], }); @@ -319,6 +373,10 @@ ruleTester.run('consistent-test-it with fn=it and withinDescribe=test ', rule, { code: 'it.skip("foo")', options: [{ fn: TestCaseName.it, withinDescribe: TestCaseName.test }], }, + { + code: 'it.concurrent("foo")', + options: [{ fn: TestCaseName.it, withinDescribe: TestCaseName.test }], + }, { code: 'xit("foo")', options: [{ fn: TestCaseName.it, withinDescribe: TestCaseName.test }], @@ -385,6 +443,20 @@ ruleTester.run('consistent-test-it with fn=it and withinDescribe=test ', rule, { ], output: 'describe("suite", () => { test.skip("foo") })', }, + { + code: 'describe("suite", () => { it.concurrent("foo") })', + options: [{ fn: TestCaseName.it, withinDescribe: TestCaseName.test }], + errors: [ + { + messageId: 'consistentMethodWithinDescribe', + data: { + testKeywordWithinDescribe: TestCaseName.test, + oppositeTestKeyword: TestCaseName.it, + }, + }, + ], + output: 'describe("suite", () => { test.concurrent("foo") })', + }, ], }); diff --git a/src/rules/__tests__/no-commented-out-tests.test.ts b/src/rules/__tests__/no-commented-out-tests.test.ts index a0d3e0811..1944dce53 100644 --- a/src/rules/__tests__/no-commented-out-tests.test.ts +++ b/src/rules/__tests__/no-commented-out-tests.test.ts @@ -17,8 +17,10 @@ ruleTester.run('no-commented-out-tests', rule, { 'it("foo", function () {})', 'describe.only("foo", function () {})', 'it.only("foo", function () {})', + 'it.concurrent("foo", function () {})', 'test("foo", function () {})', 'test.only("foo", function () {})', + 'test.concurrent("foo", function () {})', 'var appliedSkip = describe.skip; appliedSkip.apply(describe)', 'var calledSkip = it.skip; calledSkip.call(it)', '({ f: function () {} }).f()', @@ -80,6 +82,10 @@ ruleTester.run('no-commented-out-tests', rule, { code: '// it.only("foo", function () {})', errors: [{ messageId: 'commentedTests', column: 1, line: 1 }], }, + { + code: '// it.concurrent("foo", function () {})', + errors: [{ messageId: 'commentedTests', column: 1, line: 1 }], + }, { code: '// it["skip"]("foo", function () {})', errors: [{ messageId: 'commentedTests', column: 1, line: 1 }], @@ -88,6 +94,10 @@ ruleTester.run('no-commented-out-tests', rule, { code: '// test.skip("foo", function () {})', errors: [{ messageId: 'commentedTests', column: 1, line: 1 }], }, + { + code: '// test.concurrent("foo", function () {})', + errors: [{ messageId: 'commentedTests', column: 1, line: 1 }], + }, { code: '// test["skip"]("foo", function () {})', errors: [{ messageId: 'commentedTests', column: 1, line: 1 }], diff --git a/src/rules/__tests__/no-disabled-tests.test.ts b/src/rules/__tests__/no-disabled-tests.test.ts index 504896d29..3f317c0be 100644 --- a/src/rules/__tests__/no-disabled-tests.test.ts +++ b/src/rules/__tests__/no-disabled-tests.test.ts @@ -16,8 +16,10 @@ ruleTester.run('no-disabled-tests', rule, { 'it("foo", function () {})', 'describe.only("foo", function () {})', 'it.only("foo", function () {})', + 'it.concurrent("foo", function () {})', 'test("foo", function () {})', 'test.only("foo", function () {})', + 'test.concurrent("foo", function () {})', 'describe[`${"skip"}`]("foo", function () {})', 'var appliedSkip = describe.skip; appliedSkip.apply(describe)', 'var calledSkip = it.skip; calledSkip.call(it)', @@ -73,6 +75,10 @@ ruleTester.run('no-disabled-tests', rule, { code: 'it.skip("foo", function () {})', errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], }, + { + code: 'it.concurrent.skip("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, { code: 'it["skip"]("foo", function () {})', errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], @@ -81,6 +87,10 @@ ruleTester.run('no-disabled-tests', rule, { code: 'test.skip("foo", function () {})', errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], }, + { + code: 'test.concurrent.skip("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, { code: 'test["skip"]("foo", function () {})', errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], diff --git a/src/rules/__tests__/no-focused-tests.test.ts b/src/rules/__tests__/no-focused-tests.test.ts index 52155e473..decf128b1 100644 --- a/src/rules/__tests__/no-focused-tests.test.ts +++ b/src/rules/__tests__/no-focused-tests.test.ts @@ -15,14 +15,17 @@ ruleTester.run('no-focused-tests', rule, { 'it()', 'describe.skip()', 'it.skip()', + 'it.concurrent.skip()', 'test()', 'test.skip()', + 'test.concurrent.skip()', 'var appliedOnly = describe.only; appliedOnly.apply(describe)', 'var calledOnly = it.only; calledOnly.call(it)', 'it.each()()', 'it.each`table`()', 'test.each()()', 'test.each`table`()', + 'test.concurrent()', ], invalid: [ @@ -46,6 +49,10 @@ ruleTester.run('no-focused-tests', rule, { code: 'it.only()', errors: [{ messageId: 'focusedTest', column: 4, line: 1 }], }, + { + code: 'it.concurrent.only()', + errors: [{ messageId: 'focusedTest', column: 4, line: 1 }], + }, { code: 'it.only.each()', errors: [{ messageId: 'focusedTest', column: 4, line: 1 }], @@ -62,6 +69,10 @@ ruleTester.run('no-focused-tests', rule, { code: 'test.only()', errors: [{ messageId: 'focusedTest', column: 6, line: 1 }], }, + { + code: 'test.concurrent.only()', + errors: [{ messageId: 'focusedTest', column: 6, line: 1 }], + }, { code: 'test.only.each()', errors: [{ messageId: 'focusedTest', column: 6, line: 1 }], diff --git a/src/rules/__tests__/no-identical-title.test.ts b/src/rules/__tests__/no-identical-title.test.ts index 477b511aa..913cbe5e2 100644 --- a/src/rules/__tests__/no-identical-title.test.ts +++ b/src/rules/__tests__/no-identical-title.test.ts @@ -25,10 +25,22 @@ ruleTester.run('no-identical-title', rule, { '});', ].join('\n'), ['it("it1", function() {});', 'it("it2", function() {});'].join('\n'), + [ + 'it.concurrent("it1", function() {});', + 'it.concurrent("it2", function() {});', + ].join('\n'), ['it.only("it1", function() {});', 'it("it2", function() {});'].join('\n'), ['it.only("it1", function() {});', 'it.only("it2", function() {});'].join( '\n', ), + [ + 'it.concurrent.only("it1", function() {});', + 'it.concurrent("it2", function() {});', + ].join('\n'), + [ + 'it.concurrent.only("it1", function() {});', + 'it.concurrent.only("it2", function() {});', + ].join('\n'), ['describe("title", function() {});', 'it("title", function() {});'].join( '\n', ), @@ -118,6 +130,13 @@ ruleTester.run('no-identical-title', rule, { ), errors: [{ messageId: 'multipleTestTitle', column: 4, line: 2 }], }, + { + code: [ + 'it.concurrent("it1", function() {});', + 'it.concurrent("it1", function() {});', + ].join('\n'), + errors: [{ messageId: 'multipleTestTitle', column: 15, line: 2 }], + }, { code: [ 'it.only("it1", function() {});', @@ -125,6 +144,13 @@ ruleTester.run('no-identical-title', rule, { ].join('\n'), errors: [{ messageId: 'multipleTestTitle', column: 4, line: 2 }], }, + { + code: [ + 'it.concurrent.only("it1", function() {});', + 'it.concurrent("it1", function() {});', + ].join('\n'), + errors: [{ messageId: 'multipleTestTitle', column: 15, line: 2 }], + }, { code: ['fit("it1", function() {});', 'it("it1", function() {});'].join( '\n', @@ -138,6 +164,13 @@ ruleTester.run('no-identical-title', rule, { ].join('\n'), errors: [{ messageId: 'multipleTestTitle', column: 9, line: 2 }], }, + { + code: [ + 'it.concurrent.only("it1", function() {});', + 'it.concurrent.only("it1", function() {});', + ].join('\n'), + errors: [{ messageId: 'multipleTestTitle', column: 20, line: 2 }], + }, { code: [ 'describe("describe1", function() {});', diff --git a/src/rules/__tests__/no-if.test.ts b/src/rules/__tests__/no-if.test.ts index 11a33f729..fe4e13580 100644 --- a/src/rules/__tests__/no-if.test.ts +++ b/src/rules/__tests__/no-if.test.ts @@ -190,6 +190,16 @@ ruleTester.run('no-if', rule, { }, ], }, + { + code: `it.concurrent.skip('foo', () => { + if('bar') {} + })`, + errors: [ + { + messageId: 'noIf', + }, + ], + }, { code: `it.only('foo', () => { if('bar') {} @@ -200,6 +210,16 @@ ruleTester.run('no-if', rule, { }, ], }, + { + code: `it.concurrent.only('foo', () => { + if('bar') {} + })`, + errors: [ + { + messageId: 'noIf', + }, + ], + }, { code: `xit('foo', () => { if('bar') {} @@ -220,6 +240,16 @@ ruleTester.run('no-if', rule, { }, ], }, + { + code: `fit.concurrent('foo', () => { + if('bar') {} + })`, + errors: [ + { + messageId: 'noIf', + }, + ], + }, { code: `test('foo', () => { if('bar') {} @@ -231,7 +261,7 @@ ruleTester.run('no-if', rule, { ], }, { - code: `test.skip('foo', () => { + code: `test.concurrent.skip('foo', () => { if('bar') {} })`, errors: [ @@ -241,7 +271,7 @@ ruleTester.run('no-if', rule, { ], }, { - code: `test.only('foo', () => { + code: `test.concurrent.only('foo', () => { if('bar') {} })`, errors: [ diff --git a/src/rules/__tests__/no-standalone-expect.test.ts b/src/rules/__tests__/no-standalone-expect.test.ts index 7717fff76..d5afb269e 100644 --- a/src/rules/__tests__/no-standalone-expect.test.ts +++ b/src/rules/__tests__/no-standalone-expect.test.ts @@ -32,6 +32,7 @@ ruleTester.run('no-standalone-expect', rule, { }); `, 'it.only("an only", value => { expect(value).toBe(true); });', + 'it.concurrent("an concurrent", value => { expect(value).toBe(true); });', 'describe.each([1, true])("trues", value => { it("an it", () => expect(value).toBe(true) ); });', ], invalid: [ diff --git a/src/rules/__tests__/no-test-prefixes.test.ts b/src/rules/__tests__/no-test-prefixes.test.ts index cb1900dcd..dbc4a2058 100644 --- a/src/rules/__tests__/no-test-prefixes.test.ts +++ b/src/rules/__tests__/no-test-prefixes.test.ts @@ -7,13 +7,19 @@ ruleTester.run('no-test-prefixes', rule, { valid: [ 'describe("foo", function () {})', 'it("foo", function () {})', + 'it.concurrent("foo", function () {})', 'test("foo", function () {})', + 'test.concurrent("foo", function () {})', 'describe.only("foo", function () {})', 'it.only("foo", function () {})', + 'it.concurrent.only("foo", function () {})', 'test.only("foo", function () {})', + 'test.concurrent.only("foo", function () {})', 'describe.skip("foo", function () {})', 'it.skip("foo", function () {})', + 'it.concurrent.skip("foo", function () {})', 'test.skip("foo", function () {})', + 'test.concurrent.skip("foo", function () {})', 'foo()', '[1,2,3].forEach()', ], @@ -42,6 +48,18 @@ ruleTester.run('no-test-prefixes', rule, { ], output: 'it.only("foo", function () {})', }, + { + code: 'fit.concurrent("foo", function () {})', + errors: [ + { + messageId: 'usePreferredName', + data: { preferredNodeName: 'it.concurrent.only' }, + column: 1, + line: 1, + }, + ], + output: 'it.concurrent.only("foo", function () {})', + }, { code: 'xdescribe("foo", function () {})', errors: [ diff --git a/src/rules/__tests__/no-try-expect.test.ts b/src/rules/__tests__/no-try-expect.test.ts index d6e358d08..8c6ce8e37 100644 --- a/src/rules/__tests__/no-try-expect.test.ts +++ b/src/rules/__tests__/no-try-expect.test.ts @@ -25,6 +25,12 @@ ruleTester.run('no-try-catch', rule, { `it.skip('foo'); try { + } catch { + expect('foo').toEqual('foo'); + }`, + `it.concurrent.skip('foo'); + try { + } catch { expect('foo').toEqual('foo'); }`, diff --git a/src/rules/__tests__/prefer-todo.test.ts b/src/rules/__tests__/prefer-todo.test.ts index 95b3c226f..4d6a870e4 100644 --- a/src/rules/__tests__/prefer-todo.test.ts +++ b/src/rules/__tests__/prefer-todo.test.ts @@ -10,11 +10,15 @@ const ruleTester = new TSESLint.RuleTester({ ruleTester.run('prefer-todo', rule, { valid: [ 'test()', + 'test.concurrent()', 'test.todo("i need to write this test");', 'test(obj)', + 'test.concurrent(obj)', 'fit("foo")', + 'fit.concurrent("foo")', 'xit("foo")', 'test("stub", () => expect(1).toBe(1));', + 'test.concurrent("stub", () => expect(1).toBe(1));', ` supportsDone && params.length < test.length ? done => test(...params, done) diff --git a/src/rules/__tests__/valid-title.test.ts b/src/rules/__tests__/valid-title.test.ts index a43a1bf99..7c293c49e 100644 --- a/src/rules/__tests__/valid-title.test.ts +++ b/src/rules/__tests__/valid-title.test.ts @@ -55,6 +55,16 @@ ruleTester.run('title-must-be-string', rule, { }, ], }, + { + code: 'it.concurrent(123, () => {});', + errors: [ + { + messageId: 'titleMustBeString', + column: 15, + line: 1, + }, + ], + }, { code: 'it(1 + 2 + 3, () => {});', errors: [ @@ -65,6 +75,16 @@ ruleTester.run('title-must-be-string', rule, { }, ], }, + { + code: 'it.concurrent(1 + 2 + 3, () => {});', + errors: [ + { + messageId: 'titleMustBeString', + column: 15, + line: 1, + }, + ], + }, { code: 'test.skip(123, () => {});', options: [{ ignoreTypeOfDescribeName: true }], @@ -76,6 +96,17 @@ ruleTester.run('title-must-be-string', rule, { }, ], }, + { + code: 'test.concurrent.skip(123, () => {});', + options: [{ ignoreTypeOfDescribeName: true }], + errors: [ + { + messageId: 'titleMustBeString', + column: 22, + line: 1, + }, + ], + }, { code: 'describe(String(/.+/), () => {});', errors: [ @@ -147,9 +178,13 @@ ruleTester.run('no-empty-title', rule, { 'describe("foo", function () {})', 'describe("foo", function () { it("bar", function () {}) })', 'test("foo", function () {})', + 'test.concurrent("foo", function () {})', 'test(`foo`, function () {})', + 'test.concurrent(`foo`, function () {})', 'test(`${foo}`, function () {})', + 'test.concurrent(`${foo}`, function () {})', "it('foo', function () {})", + "it.concurrent('foo', function () {})", "xdescribe('foo', function () {})", "xit('foo', function () {})", "xtest('foo', function () {})", @@ -188,6 +223,17 @@ ruleTester.run('no-empty-title', rule, { }, ], }, + { + code: 'it.concurrent("", function () {})', + errors: [ + { + messageId: 'emptyTitle', + column: 1, + line: 1, + data: { jestFunctionName: 'test' }, + }, + ], + }, { code: 'test("", function () {})', errors: [ @@ -199,6 +245,17 @@ ruleTester.run('no-empty-title', rule, { }, ], }, + { + code: 'test.concurrent("", function () {})', + errors: [ + { + messageId: 'emptyTitle', + column: 1, + line: 1, + data: { jestFunctionName: 'test' }, + }, + ], + }, { code: 'test(``, function () {})', errors: [ @@ -210,6 +267,17 @@ ruleTester.run('no-empty-title', rule, { }, ], }, + { + code: 'test.concurrent(``, function () {})', + errors: [ + { + messageId: 'emptyTitle', + column: 1, + line: 1, + data: { jestFunctionName: 'test' }, + }, + ], + }, { code: "xdescribe('', () => {})", errors: [ @@ -249,15 +317,19 @@ ruleTester.run('no-empty-title', rule, { ruleTester.run('no-accidental-space', rule, { valid: [ 'it()', + 'it.concurrent()', 'describe()', 'it.each()()', 'describe("foo", function () {})', 'fdescribe("foo", function () {})', 'xdescribe("foo", function () {})', 'it("foo", function () {})', + 'it.concurrent("foo", function () {})', 'fit("foo", function () {})', + 'fit.concurrent("foo", function () {})', 'xit("foo", function () {})', 'test("foo", function () {})', + 'test.concurrent("foo", function () {})', 'xtest("foo", function () {})', 'xtest(`foo`, function () {})', 'someFn("foo", function () {})', @@ -303,16 +375,31 @@ ruleTester.run('no-accidental-space', rule, { errors: [{ messageId: 'accidentalSpace', column: 4, line: 1 }], output: 'it("foo", function () {})', }, + { + code: 'it.concurrent(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 15, line: 1 }], + output: 'it.concurrent("foo", function () {})', + }, { code: 'fit(" foo", function () {})', errors: [{ messageId: 'accidentalSpace', column: 5, line: 1 }], output: 'fit("foo", function () {})', }, + { + code: 'fit.concurrent(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 16, line: 1 }], + output: 'fit.concurrent("foo", function () {})', + }, { code: 'fit("foo ", function () {})', errors: [{ messageId: 'accidentalSpace', column: 5, line: 1 }], output: 'fit("foo", function () {})', }, + { + code: 'fit.concurrent("foo ", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 16, line: 1 }], + output: 'fit.concurrent("foo", function () {})', + }, { code: 'xit(" foo", function () {})', errors: [{ messageId: 'accidentalSpace', column: 5, line: 1 }], @@ -323,21 +410,41 @@ ruleTester.run('no-accidental-space', rule, { errors: [{ messageId: 'accidentalSpace', column: 6, line: 1 }], output: 'test("foo", function () {})', }, + { + code: 'test.concurrent(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 17, line: 1 }], + output: 'test.concurrent("foo", function () {})', + }, { code: 'test(` foo`, function () {})', errors: [{ messageId: 'accidentalSpace', column: 6, line: 1 }], output: 'test(`foo`, function () {})', }, + { + code: 'test.concurrent(` foo`, function () {})', + errors: [{ messageId: 'accidentalSpace', column: 17, line: 1 }], + output: 'test.concurrent(`foo`, function () {})', + }, { code: 'test(` foo bar bang`, function () {})', errors: [{ messageId: 'accidentalSpace', column: 6, line: 1 }], output: 'test(`foo bar bang`, function () {})', }, + { + code: 'test.concurrent(` foo bar bang`, function () {})', + errors: [{ messageId: 'accidentalSpace', column: 17, line: 1 }], + output: 'test.concurrent(`foo bar bang`, function () {})', + }, { code: 'test(` foo bar bang `, function () {})', errors: [{ messageId: 'accidentalSpace', column: 6, line: 1 }], output: 'test(`foo bar bang`, function () {})', }, + { + code: 'test.concurrent(` foo bar bang `, function () {})', + errors: [{ messageId: 'accidentalSpace', column: 17, line: 1 }], + output: 'test.concurrent(`foo bar bang`, function () {})', + }, { code: 'xtest(" foo", function () {})', errors: [{ messageId: 'accidentalSpace', column: 7, line: 1 }], diff --git a/src/rules/no-disabled-tests.ts b/src/rules/no-disabled-tests.ts index 68fac8e1c..c526e5f2d 100644 --- a/src/rules/no-disabled-tests.ts +++ b/src/rules/no-disabled-tests.ts @@ -45,7 +45,9 @@ export default createRule({ break; case 'it.skip': + case 'it.concurrent.skip': case 'test.skip': + case 'test.concurrent.skip': context.report({ messageId: 'skippedTest', node }); break; } diff --git a/src/rules/no-focused-tests.ts b/src/rules/no-focused-tests.ts index 7d1d93154..abc0a45b8 100644 --- a/src/rules/no-focused-tests.ts +++ b/src/rules/no-focused-tests.ts @@ -5,16 +5,30 @@ import { import { DescribeAlias, TestCaseName, + TestCaseProperty, createRule, isSupportedAccessor, } from './utils'; +const validTestCaseNames = [TestCaseName.test, TestCaseName.it]; + const testFunctions = new Set([ DescribeAlias.describe, - TestCaseName.test, - TestCaseName.it, + ...validTestCaseNames, ]); +interface ConcurrentExpression extends TSESTree.MemberExpression { + parent: TSESTree.MemberExpression; +} + +const isConcurrentExpression = ( + expression: TSESTree.MemberExpression, +): expression is ConcurrentExpression => + expression.type === AST_NODE_TYPES.MemberExpression && + isSupportedAccessor(expression.property, TestCaseProperty.concurrent) && + !!expression.parent && + expression.parent.type === AST_NODE_TYPES.MemberExpression; + const matchesTestFunction = (object: TSESTree.LeftHandSideExpression) => 'name' in object && (object.name in TestCaseName || object.name in DescribeAlias); @@ -24,7 +38,10 @@ const isCallToFocusedTestFunction = (object: TSESTree.Identifier) => const isCallToTestOnlyFunction = (callee: TSESTree.MemberExpression) => matchesTestFunction(callee.object) && - isSupportedAccessor(callee.property, 'only'); + isSupportedAccessor( + isConcurrentExpression(callee) ? callee.parent.property : callee.property, + 'only', + ); export default createRule({ name: __filename, diff --git a/src/rules/no-if.ts b/src/rules/no-if.ts index ce8ccc145..f4eca9d2c 100644 --- a/src/rules/no-if.ts +++ b/src/rules/no-if.ts @@ -7,9 +7,14 @@ import { const testCaseNames = new Set([ ...Object.keys(TestCaseName), 'it.only', + 'it.concurrent.only', 'it.skip', + 'it.concurrent.skip', 'test.only', + 'test.concurrent.only', 'test.skip', + 'test.concurrent.skip', + 'fit.concurrent', ]); const isTestArrowFunction = (node: TSESTree.ArrowFunctionExpression) => diff --git a/src/rules/utils.ts b/src/rules/utils.ts index 2422010e1..8f6e8911a 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -542,6 +542,7 @@ export enum DescribeProperty { export enum TestCaseProperty { 'each' = 'each', + 'concurrent' = 'concurrent', 'only' = 'only', 'skip' = 'skip', 'todo' = 'todo', @@ -624,40 +625,34 @@ export const isFunction = (node: TSESTree.Node): node is FunctionExpression => export const isHook = ( node: TSESTree.CallExpression, -): node is JestFunctionCallExpressionWithIdentifierCallee => { - return ( - node.callee.type === AST_NODE_TYPES.Identifier && - HookName.hasOwnProperty(node.callee.name) - ); -}; +): node is JestFunctionCallExpressionWithIdentifierCallee => + node.callee.type === AST_NODE_TYPES.Identifier && + HookName.hasOwnProperty(node.callee.name); export const isTestCase = ( node: TSESTree.CallExpression, -): node is JestFunctionCallExpression => { - return ( - (node.callee.type === AST_NODE_TYPES.Identifier && - TestCaseName.hasOwnProperty(node.callee.name)) || - (node.callee.type === AST_NODE_TYPES.MemberExpression && - node.callee.object.type === AST_NODE_TYPES.Identifier && - TestCaseName.hasOwnProperty(node.callee.object.name) && - node.callee.property.type === AST_NODE_TYPES.Identifier && - TestCaseProperty.hasOwnProperty(node.callee.property.name)) - ); -}; +): node is JestFunctionCallExpression => + (node.callee.type === AST_NODE_TYPES.Identifier && + TestCaseName.hasOwnProperty(node.callee.name)) || + (node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.property.type === AST_NODE_TYPES.Identifier && + TestCaseProperty.hasOwnProperty(node.callee.property.name) && + ((node.callee.object.type === AST_NODE_TYPES.Identifier && + TestCaseName.hasOwnProperty(node.callee.object.name)) || + (node.callee.object.type === AST_NODE_TYPES.MemberExpression && + node.callee.object.object.type === AST_NODE_TYPES.Identifier && + TestCaseName.hasOwnProperty(node.callee.object.object.name)))); export const isDescribe = ( node: TSESTree.CallExpression, -): node is JestFunctionCallExpression => { - return ( - (node.callee.type === AST_NODE_TYPES.Identifier && - DescribeAlias.hasOwnProperty(node.callee.name)) || - (node.callee.type === AST_NODE_TYPES.MemberExpression && - node.callee.object.type === AST_NODE_TYPES.Identifier && - DescribeAlias.hasOwnProperty(node.callee.object.name) && - node.callee.property.type === AST_NODE_TYPES.Identifier && - DescribeProperty.hasOwnProperty(node.callee.property.name)) - ); -}; +): node is JestFunctionCallExpression => + (node.callee.type === AST_NODE_TYPES.Identifier && + DescribeAlias.hasOwnProperty(node.callee.name)) || + (node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.object.type === AST_NODE_TYPES.Identifier && + DescribeAlias.hasOwnProperty(node.callee.object.name) && + node.callee.property.type === AST_NODE_TYPES.Identifier && + DescribeProperty.hasOwnProperty(node.callee.property.name)); /** * Checks if the given `describe` is a call to `describe.each`.