From 8dd8ad763e603e37b8b62fb59e2b97819f296c66 Mon Sep 17 00:00:00 2001 From: Oleksandr Pelykh Date: Thu, 11 Jun 2026 16:36:19 +0300 Subject: [PATCH 1/2] feat: add test alias support for playwright --- example/playwright/annotations-status.ts | 27 ++++ .../playwright/custom-fixture-annotations.ts | 29 ++++ .../playwright/sibling-after-skipped-suite.ts | 16 ++ src/analyzer.js | 18 ++- src/lib/frameworks/playwright.js | 147 ++++++++---------- tests/analyzer_test.js | 27 ++++ tests/playwright_test.js | 97 +++++++++++- 7 files changed, 277 insertions(+), 84 deletions(-) create mode 100644 example/playwright/annotations-status.ts create mode 100644 example/playwright/custom-fixture-annotations.ts create mode 100644 example/playwright/sibling-after-skipped-suite.ts diff --git a/example/playwright/annotations-status.ts b/example/playwright/annotations-status.ts new file mode 100644 index 00000000..01cabe55 --- /dev/null +++ b/example/playwright/annotations-status.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; + +test('plain test', async () => { + await expect(true).toBe(true); +}); + +// .fail marks a test as expected to fail, but it still runs => not skipped +test.fail('expected to fail test', async () => { + await expect(true).toBe(false); +}); + +// .todo => skipped test +test.todo('todo test'); + +// runtime forms without a title declare no separate test +test('runtime annotations have no title', async () => { + test.fail(); + test.skip(); + await expect(true).toBe(true); +}); + +// .fail inside a skipped suite => skipped (suite wins) +test.describe.skip('skipped suite', () => { + test.fail('fail inside skipped suite', async () => { + await expect(true).toBe(false); + }); +}); diff --git a/example/playwright/custom-fixture-annotations.ts b/example/playwright/custom-fixture-annotations.ts new file mode 100644 index 00000000..ba94f8db --- /dev/null +++ b/example/playwright/custom-fixture-annotations.ts @@ -0,0 +1,29 @@ +import { test as base } from '@playwright/test'; + +const testFixture = base.extend<{ someFixture: any }>({ + someFixture: async ({}, use) => { + await use({ name: 'custom fixture name' }); + }, +}); + +testFixture('plain alias test', async ({ someFixture }) => { + console.warn(someFixture.name); +}); + +testFixture.skip('skipped alias test', async ({ someFixture }) => { + console.warn(someFixture.name); +}); + +testFixture.fixme('fixme alias test', async ({ someFixture }) => { + console.warn(someFixture.name); +}); + +testFixture.fail('failing alias test', async ({ someFixture }) => { + console.warn(someFixture.name); +}); + +testFixture.describe('alias suite', () => { + testFixture.fixme('fixme test inside alias suite', async ({ someFixture }) => { + console.warn(someFixture.name); + }); +}); diff --git a/example/playwright/sibling-after-skipped-suite.ts b/example/playwright/sibling-after-skipped-suite.ts new file mode 100644 index 00000000..74e96325 --- /dev/null +++ b/example/playwright/sibling-after-skipped-suite.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@playwright/test'; + +test.describe.skip('skipped suite', () => { + test('inside skipped suite', async () => { + await expect(true).toBe(true); + }); +}); + +// these are siblings declared AFTER the skipped suite closed - they must not inherit skipped +test('sibling after skipped suite', async () => { + await expect(true).toBe(true); +}); + +test.fail('failing sibling after skipped suite', async () => { + await expect(true).toBe(false); +}); diff --git a/src/analyzer.js b/src/analyzer.js index 965296e5..19d8cfbe 100644 --- a/src/analyzer.js +++ b/src/analyzer.js @@ -83,6 +83,20 @@ class Analyzer { // this.addPlugin('@babel/plugin-transform-typescript'); } + // Build the list of glob patterns to scan. When TypeScript support is enabled, a JS-only + // glob (e.g. "**/*.test.js") would silently match nothing in a TS project, so we also scan + // the TypeScript equivalents by swapping the trailing `.js` extension for `.ts`/`.tsx`/etc. + buildPatterns(pattern) { + const patterns = [pattern]; + if (this.typeScript && /\.js$/.test(pattern)) { + const base = pattern.replace(/\.js$/, ''); + for (const ext of ['ts', 'tsx', 'mts', 'cts']) { + patterns.push(`${base}.${ext}`); + } + } + return patterns; + } + analyze(pattern) { if (!this.frameworkParser) throw new Error("No test framework specified. Can't analyze"); @@ -93,7 +107,9 @@ class Analyzer { const originalCwd = process.cwd(); process.chdir(this.workDir); - let files = glob.sync(pattern, { windowsPathsNoEscape: true }); + const patterns = this.buildPatterns(pattern); + debug('Patterns:', patterns); + let files = [...new Set(patterns.flatMap(p => glob.sync(p, { windowsPathsNoEscape: true })))]; // Exclude files matching the exclude pattern if provided if (this.opts.exclude) { diff --git a/src/lib/frameworks/playwright.js b/src/lib/frameworks/playwright.js index e7a6782e..0f9d0262 100644 --- a/src/lib/frameworks/playwright.js +++ b/src/lib/frameworks/playwright.js @@ -23,12 +23,52 @@ module.exports = (ast, file = '', source = '', opts = {}) => { let beforeEachCode = ''; let afterCode = ''; + // valid test identifiers: built-in `test`/`it` plus any custom fixtures/aliases + const testNames = ['test', 'it', ...(opts?.testAlias || [])]; + function addSuite(path) { currentSuite = currentSuite.filter(s => s.loc.end.line > path.loc.start.line); path.tags = playwright.getTestProps({ parent: { expression: path } }).tags; currentSuite.push(path); } + // suites that actually enclose the call at `path`. `currentSuite` is only pruned when a + // new suite is added, so it can still hold sibling suites that already closed above this + // line — those must not leak their name or `skipped` flag onto a test declared after them. + function getEnclosingSuites(path) { + return currentSuite.filter(s => getEndLineNumber({ container: s }) >= getLineNumber(path)); + } + + // resolve the name of the test object an annotation (`.skip`, `.fixme`, `.fail`, `.todo`) + // is called on, e.g. `test`, `it`, `describe` or a custom test alias / fixture name + function getTestObjectName(path) { + if (!path.parent || !path.parent.object) return null; + return ( + path.parent.object.name || path.parent.object.property?.name || path.parent.object.callee?.object?.name || null + ); + } + + // Register a single named test declared with an annotation call + // (`test.skip` / `test.fixme` / `test.fail` / `test.todo`, or the alias equivalents). + // `path` is the annotation identifier node; its enclosing call holds the test title. + // Calls without a string title (the runtime form `test.skip()` used inside a test body) + // declare no test and are ignored. The caller decides `skipped`. + function registerAnnotatedTest(path, { skipped }) { + if (!hasStringOrTemplateArgument(path.parentPath.container)) return; + + tests.push({ + name: getStringValue(path.parentPath.container), + suites: getEnclosingSuites(path).map(s => getStringValue(s)), + line: getLineNumber(path), + // `path` is the annotation identifier (`fixme`/`skip`/...); its container ends on the + // member-expression line only. The full call (and its body) is `path.parentPath.container`, + // so take the end line from there to capture the complete test code. + code: getCode(source, getLineNumber(path), getEndLineNumber(path.parentPath), isLineNumber), + file, + skipped, + }); + } + traverse(ast, { enter(path) { if (path.isIdentifier({ name: 'describe' })) { @@ -90,31 +130,16 @@ module.exports = (ast, file = '', source = '', opts = {}) => { } } - if (path.isIdentifier({ name: 'skip' })) { - if (!path.parent || !path.parent.object) { - return; - } - const name = - path.parent.object.name || path.parent.object.property.name || path.parent.object.callee.object.name; - - if (name === 'test' || name === 'it') { - // test or it - if (!hasStringOrTemplateArgument(path.parentPath.container)) return; - - const testName = getStringValue(path.parentPath.container); - tests.push({ - name: testName, - suites: currentSuite - .filter(s => getEndLineNumber({ container: s }) >= getLineNumber(path)) - .map(s => getStringValue(s)), - line: getLineNumber(path), - code: getCode(source, getLineNumber(path), getEndLineNumber(path), isLineNumber), - file, - skipped: true, - }); - } + // `.skip` / `.fixme` mark a test (or every test in a suite) as skipped, + // supporting `test`, `it`, `describe` and any custom test alias / fixture + if (path.isIdentifier({ name: 'skip' }) || path.isIdentifier({ name: 'fixme' })) { + const name = getTestObjectName(path); + if (!name) return; - if (name === 'describe') { + if (testNames.includes(name)) { + // test or it (or alias), e.g. `myFixture.fixme('...', ...)` + registerAnnotatedTest(path, { skipped: true }); + } else if (name === 'describe') { // suite if (!hasStringOrTemplateArgument(path.parentPath.container)) return; const suite = path.parentPath.container; @@ -125,61 +150,23 @@ module.exports = (ast, file = '', source = '', opts = {}) => { // todo: handle "context" } - if (path.isIdentifier({ name: 'fixme' })) { - if (!path.parent || !path.parent.object) { - return; - } - const name = - path.parent.object.name || path.parent.object.property.name || path.parent.object.callee.object.name; - - if (name === 'test' || name === 'it') { - // test or it - if (!hasStringOrTemplateArgument(path.parentPath.container)) return; + // `.fail` marks a test as expected to fail; it still runs, so it is not skipped + if (path.isIdentifier({ name: 'fail' })) { + const name = getTestObjectName(path); + if (!name) return; - const testName = getStringValue(path.parentPath.container); - tests.push({ - name: testName, - suites: currentSuite - .filter(s => getEndLineNumber({ container: s }) >= getLineNumber(path)) - .map(s => getStringValue(s)), - line: getLineNumber(path), - code: getCode(source, getLineNumber(path), getEndLineNumber(path), isLineNumber), - file, - skipped: true, - }); + if (testNames.includes(name)) { + registerAnnotatedTest(path, { skipped: getEnclosingSuites(path).some(s => s.skipped) }); } - - if (name === 'describe') { - // suite - if (!hasStringOrTemplateArgument(path.parentPath.container)) return; - const suite = path.parentPath.container; - suite.skipped = true; - addSuite(suite); - } - - // todo: handle "context" } if (path.isIdentifier({ name: 'todo' })) { - if (!path.parent || !path.parent.object) { - return; - } - // todo tests => skipped tests - if (path.parent.object.name === 'test') { - // test - if (!hasStringOrTemplateArgument(path.parentPath.container)) return; + const name = getTestObjectName(path); + if (!name) return; - const testName = getStringValue(path.parentPath.container); - tests.push({ - name: testName, - suites: currentSuite - .filter(s => getEndLineNumber({ container: s }) >= getLineNumber(path)) - .map(s => getStringValue(s)), - line: getLineNumber(path), - code: getCode(source, getLineNumber(path), getEndLineNumber(path), isLineNumber), - file, - skipped: true, - }); + // todo tests => skipped tests + if (testNames.includes(name)) { + registerAnnotatedTest(path, { skipped: true }); } } @@ -202,19 +189,18 @@ module.exports = (ast, file = '', source = '', opts = {}) => { afterCode; const testName = getStringValue(path.parent); + const enclosingSuites = getEnclosingSuites(path); tests.push({ name: testName, - suites: currentSuite - .filter(s => getEndLineNumber({ container: s }) >= getLineNumber(path)) - .map(s => getStringValue(s)), + suites: enclosingSuites.map(s => getStringValue(s)), updatePoint: getUpdatePoint(path.parent), line: getLineNumber(path), code, file, tags: [...getAllSuiteTags(currentSuite), ...playwright.getTestProps(path.parentPath).tags], annotations: playwright.getTestProps(path.parentPath).annotations, - skipped: !!currentSuite.filter(s => s.skipped).length, + skipped: enclosingSuites.some(s => s.skipped), }); // stop the loop if the test is found @@ -227,16 +213,15 @@ module.exports = (ast, file = '', source = '', opts = {}) => { if (!hasStringOrTemplateArgument(currentPath.parent)) return; const testName = getStringValue(currentPath.parent); + const enclosingSuites = getEnclosingSuites(path); tests.push({ name: testName, - suites: currentSuite - .filter(s => getEndLineNumber({ container: s }) >= getLineNumber(path)) - .map(s => getStringValue(s)), + suites: enclosingSuites.map(s => getStringValue(s)), updatePoint: getUpdatePoint(path.parent), line: getLineNumber(currentPath), code: getCode(source, getLineNumber(currentPath), getEndLineNumber(currentPath), isLineNumber), file, - skipped: !!currentSuite.filter(s => s.skipped).length, + skipped: enclosingSuites.some(s => s.skipped), }); } }, diff --git a/tests/analyzer_test.js b/tests/analyzer_test.js index 31fc7a07..06ae8fc7 100644 --- a/tests/analyzer_test.js +++ b/tests/analyzer_test.js @@ -42,6 +42,33 @@ describe('analyzer', () => { expect(decorator.getSuiteNames()).to.include('Login - Global Header: Institutional Sign In Modal'); }); + it('should also scan TypeScript files when given a JS-only glob and TypeScript is enabled', () => { + analyzer = new Analyzer('mocha', path.join(__dirname, '..')); + analyzer.withTypeScript(); + // a `.js` pattern would match nothing in this TS-only dir; buildPatterns adds the `.ts` variant + analyzer.analyze('./example/protractor/**.js'); + const decorator = analyzer.getDecorator(); + expect(decorator.getSuiteNames()).to.include('Login - Global Header: Institutional Sign In Modal'); + }); + + it('buildPatterns expands a trailing .js extension to TS variants only when TypeScript is enabled', () => { + analyzer = new Analyzer('mocha', path.join(__dirname, '..')); + + // without TypeScript the pattern is left untouched + expect(analyzer.buildPatterns('./example/foo/**.js')).to.deep.equal(['./example/foo/**.js']); + // non-.js patterns are never expanded, even with TypeScript on + analyzer.withTypeScript(); + expect(analyzer.buildPatterns('./example/foo/**.ts')).to.deep.equal(['./example/foo/**.ts']); + // .js patterns gain the TS equivalents + expect(analyzer.buildPatterns('./example/foo/**.js')).to.deep.equal([ + './example/foo/**.js', + './example/foo/**.ts', + './example/foo/**.tsx', + './example/foo/**.mts', + './example/foo/**.cts', + ]); + }); + it('should exclude dir in file name if dir specified', () => { analyzer = new Analyzer('mocha', 'example'); analyzer.analyze('mocha/**_test.js'); diff --git a/tests/playwright_test.js b/tests/playwright_test.js index 42bcb5ed..474aa5e4 100644 --- a/tests/playwright_test.js +++ b/tests/playwright_test.js @@ -248,9 +248,11 @@ test.describe.only('my test', () => { ast = jsParser.parse(source, { sourceType: 'unambiguous' }); const tests = playwrightParser(ast, '', source); - expect(tests[1].code.trim()).to.equal("test.skip('my skip test @first', async ({ page }) => {".trim()); expect(tests[1].name).to.equal('my skip test @first'); expect(tests[1].suites.length).to.eql(1); + // code captures the full test body (signature + assertions), not just the signature line + expect(tests[1].code).to.include("test.skip('my skip test @first', async ({ page }) => {"); + expect(tests[1].code).to.include("await expect(page).toHaveURL('https://my.start.url/');"); }); it('should parse playwright-js tests with annotation including fixme', () => { @@ -258,9 +260,11 @@ test.describe.only('my test', () => { ast = jsParser.parse(source, { sourceType: 'unambiguous' }); const tests = playwrightParser(ast, '', source); - expect(tests[2].code.trim()).to.equal("test.fixme('my fixme test @third', async ({ page }) => {".trim()); expect(tests[2].name).to.equal('my fixme test @third'); expect(tests[2].suites.length).to.eql(1); + // code captures the full test body (signature + assertions), not just the signature line + expect(tests[2].code).to.include("test.fixme('my fixme test @third', async ({ page }) => {"); + expect(tests[2].code).to.include("await expect(page).toHaveURL('https://my.start.url/');"); }); it('should parse playwright-ts tests with annotations', () => { @@ -505,6 +509,95 @@ test.describe.only('my test', () => { expect(tests.length).to.equal(1); }); + it('should parse annotations (.skip/.fixme/.fail) on a custom test alias', () => { + source = fs.readFileSync('./example/playwright/custom-fixture-annotations.ts').toString(); + ast = jsParser.parse(source, { sourceType: 'unambiguous', plugins: ['typescript'] }); + const tests = playwrightParser(ast, '', source, { testAlias: ['testFixture'] }); + + const byName = name => tests.find(t => t.name === name); + + expect(tests.length).to.equal(5); + + expect(byName('plain alias test').skipped).to.be.false; + // .skip and .fixme on the alias mark the test as skipped + expect(byName('skipped alias test').skipped).to.be.true; + expect(byName('fixme alias test').skipped).to.be.true; + // .fail still runs, so it is not skipped + expect(byName('failing alias test').skipped).to.be.false; + // annotations work for aliases nested inside an alias suite + expect(byName('fixme test inside alias suite').skipped).to.be.true; + expect(byName('fixme test inside alias suite').suites).to.deep.equal(['alias suite']); + }); + + it('should not parse custom alias annotations when the alias is not configured', () => { + source = fs.readFileSync('./example/playwright/custom-fixture-annotations.ts').toString(); + ast = jsParser.parse(source, { sourceType: 'unambiguous', plugins: ['typescript'] }); + const tests = playwrightParser(ast, '', source); + + expect(tests.length).to.equal(0); + }); + + describe('annotations status (.skip/.fixme/.fail/.todo)', () => { + let tests; + + beforeEach(() => { + source = fs.readFileSync('./example/playwright/annotations-status.ts').toString(); + ast = jsParser.parse(source, { sourceType: 'unambiguous', plugins: ['typescript'] }); + tests = playwrightParser(ast, '', source); + }); + + const byName = name => tests.find(t => t.name === name); + + it('registers every named test exactly once (runtime no-title forms excluded)', () => { + // 5 named tests; inline `test.fail()` / `test.skip()` without a title add nothing + expect(tests.length).to.equal(5); + expect(tests.map(t => t.name)).to.deep.equal([ + 'plain test', + 'expected to fail test', + 'todo test', + 'runtime annotations have no title', + 'fail inside skipped suite', + ]); + }); + + it('marks .todo as skipped', () => { + expect(byName('todo test').skipped).to.be.true; + }); + + it('keeps .fail tests runnable (not skipped)', () => { + expect(byName('expected to fail test').skipped).to.be.false; + }); + + it('treats .fail inside a skipped suite as skipped', () => { + const test = byName('fail inside skipped suite'); + expect(test.skipped).to.be.true; + expect(test.suites).to.deep.equal(['skipped suite']); + }); + + it('ignores runtime `test.fail()` / `test.skip()` calls without a title', () => { + const test = byName('runtime annotations have no title'); + expect(test).to.not.be.undefined; + expect(test.skipped).to.be.false; + }); + }); + + it('should not leak a skipped suite onto sibling tests declared after it', () => { + source = fs.readFileSync('./example/playwright/sibling-after-skipped-suite.ts').toString(); + ast = jsParser.parse(source, { sourceType: 'unambiguous', plugins: ['typescript'] }); + const tests = playwrightParser(ast, '', source); + + const byName = name => tests.find(t => t.name === name); + + // nested test inherits the skipped suite + expect(byName('inside skipped suite').skipped).to.be.true; + expect(byName('inside skipped suite').suites).to.deep.equal(['skipped suite']); + // siblings declared after the suite closed must not inherit it (skipped or suite name) + expect(byName('sibling after skipped suite').skipped).to.be.false; + expect(byName('sibling after skipped suite').suites).to.deep.equal([]); + expect(byName('failing sibling after skipped suite').skipped).to.be.false; + expect(byName('failing sibling after skipped suite').suites).to.deep.equal([]); + }); + it('should not crash when test is assigned to a variable or inside an array (regression for issue #1)', () => { const source = ` // This works normally From 72855353261a077203245a405561ebfbfaf5c2c6 Mon Sep 17 00:00:00 2001 From: Oleksandr Pelykh Date: Sat, 13 Jun 2026 09:09:15 +0300 Subject: [PATCH 2/2] add .slow processing; update docs --- README.md | 23 +++++++++++++++++++++++ example/playwright/annotations-status.ts | 6 ++++++ src/lib/frameworks/playwright.js | 7 ++++--- tests/playwright_test.js | 13 +++++++++---- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 130cf961..0649522a 100644 --- a/README.md +++ b/README.md @@ -855,6 +855,29 @@ Test aliases are used to map tests in source code to tests in Testomat.io. By de TESTOMATIO=11111111 npx check-tests Playwright "**/*{.,_}{test,spec}.ts" --test-alias myTest,myCustomFunction ``` +For Playwright, aliases are also recognized on test annotations, so `myTest.skip(...)`, `myTest.fixme(...)`, `myTest.fail(...)`, `myTest.slow(...)` and `myTest.todo(...)` are parsed the same way as the built-in `test`/`it`: + +```js +import { test as base } from '@playwright/test'; + +const myTest = base.extend({ + /* ... */ +}); + +myTest.skip('skipped alias test', async () => { + /* ... */ +}); +myTest.fixme('fixme alias test', async () => { + /* ... */ +}); +``` + +> **Important:** annotated tests declared on a custom object (e.g. `myTest.skip(...)`, `myTest.fail(...)`, `myTest.fixme(...)`, `myTest.slow(...)`) are **only** parsed when that object is passed via `--test-alias`. Without it, only the built-in `test`/`it` annotations are detected and these tests are silently skipped: +> +> ``` +> TESTOMATIO={token} npx check-tests Playwright "**/*{.,_}{test,spec}.ts" --test-alias myTest,myTest2 +> ``` + ## Programmatic API Import Analyzer from module: diff --git a/example/playwright/annotations-status.ts b/example/playwright/annotations-status.ts index 01cabe55..fc01c53b 100644 --- a/example/playwright/annotations-status.ts +++ b/example/playwright/annotations-status.ts @@ -9,6 +9,11 @@ test.fail('expected to fail test', async () => { await expect(true).toBe(false); }); +// .slow triples the timeout, but the test still runs => not skipped +test.slow('slow test', async () => { + await expect(true).toBe(true); +}); + // .todo => skipped test test.todo('todo test'); @@ -16,6 +21,7 @@ test.todo('todo test'); test('runtime annotations have no title', async () => { test.fail(); test.skip(); + test.slow(); await expect(true).toBe(true); }); diff --git a/src/lib/frameworks/playwright.js b/src/lib/frameworks/playwright.js index 0f9d0262..63d3144f 100644 --- a/src/lib/frameworks/playwright.js +++ b/src/lib/frameworks/playwright.js @@ -39,7 +39,7 @@ module.exports = (ast, file = '', source = '', opts = {}) => { return currentSuite.filter(s => getEndLineNumber({ container: s }) >= getLineNumber(path)); } - // resolve the name of the test object an annotation (`.skip`, `.fixme`, `.fail`, `.todo`) + // resolve the name of the test object an annotation (`.skip`, `.fixme`, `.fail`, `.slow`) // is called on, e.g. `test`, `it`, `describe` or a custom test alias / fixture name function getTestObjectName(path) { if (!path.parent || !path.parent.object) return null; @@ -150,8 +150,9 @@ module.exports = (ast, file = '', source = '', opts = {}) => { // todo: handle "context" } - // `.fail` marks a test as expected to fail; it still runs, so it is not skipped - if (path.isIdentifier({ name: 'fail' })) { + // `.fail` (expected to fail) and `.slow` (extended timeout) still run, so they are not + // skipped on their own — only inherited skip from an enclosing suite applies + if (path.isIdentifier({ name: 'fail' }) || path.isIdentifier({ name: 'slow' })) { const name = getTestObjectName(path); if (!name) return; diff --git a/tests/playwright_test.js b/tests/playwright_test.js index 474aa5e4..728d8b67 100644 --- a/tests/playwright_test.js +++ b/tests/playwright_test.js @@ -537,7 +537,7 @@ test.describe.only('my test', () => { expect(tests.length).to.equal(0); }); - describe('annotations status (.skip/.fixme/.fail/.todo)', () => { + describe('annotations status (.skip/.fixme/.fail/.slow/.todo)', () => { let tests; beforeEach(() => { @@ -549,11 +549,12 @@ test.describe.only('my test', () => { const byName = name => tests.find(t => t.name === name); it('registers every named test exactly once (runtime no-title forms excluded)', () => { - // 5 named tests; inline `test.fail()` / `test.skip()` without a title add nothing - expect(tests.length).to.equal(5); + // 6 named tests; inline `test.fail()` / `test.skip()` / `test.slow()` without a title add nothing + expect(tests.length).to.equal(6); expect(tests.map(t => t.name)).to.deep.equal([ 'plain test', 'expected to fail test', + 'slow test', 'todo test', 'runtime annotations have no title', 'fail inside skipped suite', @@ -568,13 +569,17 @@ test.describe.only('my test', () => { expect(byName('expected to fail test').skipped).to.be.false; }); + it('keeps .slow tests runnable (not skipped)', () => { + expect(byName('slow test').skipped).to.be.false; + }); + it('treats .fail inside a skipped suite as skipped', () => { const test = byName('fail inside skipped suite'); expect(test.skipped).to.be.true; expect(test.suites).to.deep.equal(['skipped suite']); }); - it('ignores runtime `test.fail()` / `test.skip()` calls without a title', () => { + it('ignores runtime `test.fail()` / `test.skip()` / `test.slow()` calls without a title', () => { const test = byName('runtime annotations have no title'); expect(test).to.not.be.undefined; expect(test.skipped).to.be.false;