diff --git a/example/mocha/cypress_tags_spec.js b/example/mocha/cypress_tags_spec.js new file mode 100644 index 00000000..cda29045 --- /dev/null +++ b/example/mocha/cypress_tags_spec.js @@ -0,0 +1,46 @@ +/// + +const TestTypes = { + regression: '@regression', + smoke: '@smoke', +}; + +const Services = { + integrations: 'integrations', +}; + +describe( + 'Ability to Edit and Delete Assets', + { + tags: [TestTypes.regression, Services.integrations, '@multiTenant', 'v8'], + }, + () => { + beforeEach(() => { + cy.visit('http://localhost:8080/assets'); + }); + + it('edits an asset', () => { + cy.get('.edit').click(); + }); + + it('deletes an asset', { tags: ['@slow', `nightly`] }, () => { + cy.get('.delete').click(); + }); + + describe('nested suite', { tags: 'wip' }, () => { + it('inherits tags from all parent suites', () => { + cy.get('.nested').click(); + }); + }); + }, +); + +describe('Suite without tags', () => { + it('has no tags', () => { + cy.visit('http://localhost:8080'); + }); + + it.skip('skipped test with own tags', { tags: '@quarantine' }, () => { + cy.visit('http://localhost:8080'); + }); +}); diff --git a/src/lib/frameworks/mocha.js b/src/lib/frameworks/mocha.js index 67cba213..536c1172 100644 --- a/src/lib/frameworks/mocha.js +++ b/src/lib/frameworks/mocha.js @@ -7,6 +7,8 @@ const { getLineNumber, getEndLineNumber, getCode, + cypress, + getAllSuiteTags, } = require('../utils'); module.exports = (ast, file = '', source = '', opts = {}) => { @@ -23,6 +25,7 @@ module.exports = (ast, file = '', source = '', opts = {}) => { function addSuite(path) { currentSuite = currentSuite.filter(s => s.loc.end.line > path.loc.start.line); + path.tags = cypress.getTags(path); currentSuite.push(path); } @@ -94,6 +97,7 @@ module.exports = (ast, file = '', source = '', opts = {}) => { line: getLineNumber(path), code: getCode(source, getLineNumber(path), getEndLineNumber(path), isLineNumber), file, + tags: [...getAllSuiteTags(currentSuite), ...cypress.getTags(path.parentPath.container)], skipped: true, }); } @@ -117,6 +121,7 @@ module.exports = (ast, file = '', source = '', opts = {}) => { updatePoint: getUpdatePoint(path.parent), line: getLineNumber(path), code: getCode(source, getLineNumber(path), getEndLineNumber(path), isLineNumber), + tags: [...getAllSuiteTags(currentSuite), ...cypress.getTags(path.parent)], skipped: true, file, }); @@ -147,6 +152,7 @@ module.exports = (ast, file = '', source = '', opts = {}) => { line: getLineNumber(path), code, file, + tags: [...getAllSuiteTags(currentSuite), ...cypress.getTags(path.parent)], skipped: !!currentSuite.filter(s => s.skipped).length, }); } diff --git a/src/lib/utils.js b/src/lib/utils.js index 6f8e07c6..3cf129ac 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -196,6 +196,38 @@ const playwright = { }, }; +const cypress = { + // extracts tags from Cypress test config object: describe('title', { tags: [...] }, fn) or it('title', { tags: '@smoke' }, fn) + getTags: node => { + const args = node?.arguments; + if (!args?.length) return []; + const configArg = args.find(arg => arg.type === 'ObjectExpression'); + if (!configArg) return []; + + const tagsProp = configArg.properties.find(prop => prop.key?.name === 'tags' || prop.key?.value === 'tags'); + if (!tagsProp || !tagsProp.value) return []; + + // tags value could be a single tag or an array of tags + const elements = tagsProp.value.type === 'ArrayExpression' ? tagsProp.value.elements : [tagsProp.value]; + + const getTagName = el => { + if (!el) return; + if (el.type === 'StringLiteral' || el.type === 'Literal') return el.value; + if (el.type === 'TemplateLiteral' && !el.expressions.length && el.quasis.length === 1) { + return el.quasis[0].value.cooked; + } + // enum-style tags like TestTypes.regression can't be resolved statically; use the property name + if (el.type === 'MemberExpression' && el.property?.type === 'Identifier') return el.property.name; + if (el.type === 'Identifier') return el.name; + }; + + return elements + .map(getTagName) + .filter(tag => typeof tag === 'string' && tag.length) + .map(tag => (tag.startsWith('@') ? tag.substring(1) : tag)); + }, +}; + const arrayCompare = function (a, b, id) { const missing = []; const found = []; @@ -300,6 +332,7 @@ module.exports = { replaceAtPoint, cleanAtPoint, playwright, + cypress, arrayCompare, getAllSuiteTags, formatErrorMessage, diff --git a/tests/mocha_test.js b/tests/mocha_test.js index 84fd774e..e6a68c18 100644 --- a/tests/mocha_test.js +++ b/tests/mocha_test.js @@ -51,6 +51,42 @@ describe('mocha parser', () => { }); }); + context('cypress tags', () => { + let tests; + + before(() => { + source = fs.readFileSync('./example/mocha/cypress_tags_spec.js').toString(); + ast = parser.parse(source); + tests = mochaParser(ast, '', source); + }); + + it('should sync tags from describe config to tests', () => { + const test = tests.find(t => t.name === 'edits an asset'); + expect(test.tags).to.eql(['regression', 'integrations', 'multiTenant', 'v8']); + }); + + it('should combine suite tags with test tags', () => { + const test = tests.find(t => t.name === 'deletes an asset'); + expect(test.tags).to.eql(['regression', 'integrations', 'multiTenant', 'v8', 'slow', 'nightly']); + }); + + it('should inherit tags from all parent suites', () => { + const test = tests.find(t => t.name === 'inherits tags from all parent suites'); + expect(test.tags).to.eql(['regression', 'integrations', 'multiTenant', 'v8', 'wip']); + }); + + it('should not add tags to tests without them', () => { + const test = tests.find(t => t.name === 'has no tags'); + expect(test.tags).to.eql([]); + }); + + it('should parse tags of skipped tests', () => { + const test = tests.find(t => t.name === 'skipped test with own tags'); + expect(test.skipped).to.eql(true); + expect(test.tags).to.eql(['quarantine']); + }); + }); + context('graphql tests', () => { before(() => { source = fs.readFileSync('./example/mocha/graphql_test.js').toString();