UNPKG

@bahmutov/cy-grep

Version:

Filter Cypress tests using title or tags

273 lines (232 loc) 6.29 kB
// @ts-check // Universal code - should run in Node or in the browser /** * Parses test title grep string. * The string can have "-" in front of it to invert the match. * @param {string} s Input substring of the test title */ function parseTitleGrep(s) { if (!s || typeof s !== 'string') { return null } s = s.trim() if (s.startsWith('-')) { return { title: s.substring(1), invert: true, } } return { title: s, invert: false, } } function parseFullTitleGrep(s) { if (!s || typeof s !== 'string') { return [] } // separate each title return s.split(';').map(parseTitleGrep) } /** * Parses tags to grep for. * @param {string|string[]} s Tags string like "@tag1+@tag2", or array of tags * @param {boolean} grepPrefixAt Prefix all tags with "@" if needed * @example * parseTagsGrep('@tag1+@tag2') * @example * parseTagsGrep(['@tag1', '@tag2']) */ function parseTagsGrep(s, grepPrefixAt = false) { if (!s) { return [] } if (Array.isArray(s)) { s = s.join(',') } const explicitNotTags = [] // top level split - using space or comma, each part is OR const ORS = s .split(/[ ,]/) // remove any empty tags .filter(Boolean) .map((part) => { // now every part is an AND if (part.startsWith('--')) { explicitNotTags.push({ tag: part.slice(2), invert: true, }) return } const parsed = part.split('+').map((tag) => { if (tag.startsWith('-')) { return { tag: tag.slice(1), invert: true, } } return { tag, invert: false, } }) return parsed }) // filter out undefined from explicit not tags const ORS_filtered = ORS.filter((x) => x !== undefined) if (explicitNotTags.length > 0) { ORS_filtered.forEach((OR, index) => { ORS_filtered[index] = OR.concat(explicitNotTags) }) if (ORS_filtered.length === 0) { ORS_filtered[0] = explicitNotTags } } if (grepPrefixAt) { const prefix = '@' ORS_filtered.forEach((OR) => { OR.forEach((token) => { if (token.tag && !token.tag.startsWith(prefix)) { token.tag = prefix + token.tag } }) }) } return ORS_filtered } /** * Given a user string of tags to find, with various connectors, * returns the list of just the tags themselves. Could be used to * quickly filter test specs or find misspelled tags. * @param {string} s String of tags passed by the user * @param {boolean} grepPrefixAt Enforce the `@` character at the start of each tag * @returns {string[]} list of unique tags */ function getMentionedTags(s, grepPrefixAt = false) { if (!s) { return [] } const spaced = s.replaceAll(/[+,]/g, ' ') const tags = spaced .split(' ') .map((s) => s.trim()) .filter(Boolean) // remove any "-" at the start of the tag // because these are to signal inverted tags .map((s) => (s.startsWith('-') ? s.slice(1) : s)) .map((tag) => { if (grepPrefixAt) { if (!tag.startsWith('@')) { return '@' + tag } } return tag }) const uniqueTags = [...new Set(tags)] return uniqueTags.sort() } function shouldTestRunRequiredTags(parsedGrepTags, requiredTags = []) { if (!requiredTags.length) { // there are no tags to check return true } return requiredTags.every((onlyTag) => { return parsedGrepTags.some((orPart) => { return orPart.some((p) => { return !p.invert && p.tag === onlyTag }) }) }) } function shouldTestRunTags(parsedGrepTags, tags = []) { if (!parsedGrepTags.length) { // there are no parsed tags to search for, the test should run return true } // now the test has tags and the parsed tags are present // top levels are OR const onePartMatched = parsedGrepTags.some((orPart) => { const everyAndPartMatched = orPart.every((p) => { if (p.invert) { return !tags.includes(p.tag) } return tags.includes(p.tag) }) // console.log('every part matched %o?', orPart, everyAndPartMatched) return everyAndPartMatched }) // console.log('onePartMatched', onePartMatched) return onePartMatched } function shouldTestRunTitle(parsedGrep, testName) { if (!testName) { // if there is no title, let it run return true } if (!parsedGrep) { return true } if (!Array.isArray(parsedGrep)) { console.error('Invalid parsed title grep') console.error(parsedGrep) throw new Error('Expected title grep to be an array') } if (!parsedGrep.length) { return true } const inverted = parsedGrep.filter((g) => g.invert) const straight = parsedGrep.filter((g) => !g.invert) return ( inverted.every((titleGrep) => !testName.includes(titleGrep.title)) && (!straight.length || straight.some((titleGrep) => testName.includes(titleGrep.title))) ) } // note: tags take precedence over the test name /** * Returns boolean if the test with the given name and effective tags * should run, given the runtime grep (parsed) structure. * @param {string|undefined} testName The full test title * @param {string[]} tags The effective test tags * @param {string[]} requiredTags The effective "required" test tags */ function shouldTestRun( parsedGrep, testName, tags = [], grepUntagged = false, requiredTags = [], ) { if (grepUntagged) { return !tags.length } if (Array.isArray(testName)) { // the caller passed tags only, no test name tags = testName testName = undefined } const combinedTagsAndRequiredTags = [...tags, ...requiredTags] return ( shouldTestRunTitle(parsedGrep.title, testName) && shouldTestRunTags(parsedGrep.tags, combinedTagsAndRequiredTags) && shouldTestRunRequiredTags(parsedGrep.tags, requiredTags) ) } function parseGrep(titlePart, tags, grepPrefixAt) { return { title: parseFullTitleGrep(titlePart), tags: parseTagsGrep(tags, grepPrefixAt), } } module.exports = { parseGrep, parseTitleGrep, parseFullTitleGrep, parseTagsGrep, shouldTestRun, shouldTestRunTags, shouldTestRunRequiredTags, shouldTestRunTitle, getMentionedTags, }