Skip to content
Open
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
94 changes: 92 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ it('works', { tags: '@user' })

But sometimes you want to use variables to set them. To be able to statically analyze the source files, this package currently supports:

- local constants
#### local constants

```js
const USER = '@user'
Expand All @@ -138,7 +138,7 @@ it('works', { tags: USER })
it('works', { tags: ['@sanity', USER] })
```

- local objects with literal property access
#### Local objects with literal property access

```js
const TAGS = {
Expand All @@ -150,6 +150,96 @@ it('works', { tags: TAGS.user })
it('works', { tags: ['@sanity', TAGS.user] })
```

#### Imported values (named imports)

```js
// tags.js
export const userTag = '@user'
export const adminTag = '@admin'

// spec.cy.js
import { userTag, adminTag } from './tags'

it('works for user', { tags: userTag })
it('works for admin', { tags: adminTag })
```

#### Imported values (default imports)

```js
// tag.js
export default '@smoke'

// spec.cy.js
import smokeTag from './tag'

it('works', { tags: smokeTag })
```

Default object imports are also supported:

```js
// roles.js
export default {
user: '@user',
admin: '@admin'
}

// spec.cy.js
import roles from './roles'

it('works for user', { tags: roles.user })
it('works for admin', { tags: roles.admin })
```

#### Imported values (namespace imports)

```js
// tags.js
export const smoke = '@smoke'
export const regression = '@regression'

// spec.cy.js
import * as Tags from './tags'

it('smoke test', { tags: Tags.smoke })
it('regression test', { tags: Tags.regression })
```

#### TypeScript enums

```js
// tags.ts
export enum TestTags {
smoke = '@smoke',
regression = '@regression'
}

// spec.cy.js
import { TestTags } from './tags'

it('smoke test', { tags: TestTags.smoke })
it('regression test', { tags: TestTags.regression })
```

Namespace imports with enums are also supported:

```js
import * as TagsModule from './tags'

it('test', { tags: TagsModule.TestTags.smoke })
```

#### File extension resolution

Import paths automatically resolve `.js` and `.ts` extensions:

```js
// Both work the same way
import { tag } from './tags' // resolves to ./tags.js or ./tags.ts
import { tag } from './tags.js' // explicit extension
```

### Bin

This package includes [bin/find-test-names.js](./bin/find-test-names.js) that you can use from the command line
Expand Down
45 changes: 42 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const isItOnly = (node) =>

// list of known static constant variable declarations (in the current file)
const constants = new Map()
let resolvedImports = {}

const getResolvedTag = (node) => {
if (node.type === 'Literal') {
Expand All @@ -44,14 +45,51 @@ const getResolvedTag = (node) => {
const tagValue = constants.get(node.name)
debug('found constant value "%s" for the tag "%s"', tagValue, node.name)
return tagValue
} else if (resolvedImports[node.name]) {
const tagValue = resolvedImports[node.name]
debug('found imported value "%s" for the tag "%s"', tagValue, node.name)
return tagValue
}
} else if (node.type === 'MemberExpression') {
const key = `${node.object.name}.${node.property.name}`
// Handle nested member expressions like TagsModule.TestTags.smoke
// We need to traverse from the root and navigate through the object
let current = resolvedImports
const parts = []

// Build the path by traversing the member expression tree
const buildPath = (expr) => {
if (expr.type === 'Identifier') {
parts.unshift(expr.name)
} else if (expr.type === 'MemberExpression') {
parts.unshift(expr.property.name)
buildPath(expr.object)
}
}

buildPath(node)
const key = parts.join('.')

// First check if it's a constant
if (constants.has(key)) {
const tagValue = constants.get(key)
debug('found constant value "%s" for the tag "%s"', tagValue, key)
return tagValue
}

// Then navigate through the resolved imports
for (const part of parts) {
if (current && typeof current === 'object' && part in current) {
current = current[part]
} else {
current = undefined
break
}
}

if (current !== undefined && typeof current !== 'object') {
debug('found imported value "%s" for the tag "%s"', current, key)
return current
}
}
}

Expand Down Expand Up @@ -701,14 +739,15 @@ function getTestNames(source, withStructure, currentFilename) {
// Tree of describes and tests
let structure = []

debug('clearing local file constants')
debug('clearing local file constants and resolved imports')
constants.clear()
resolvedImports = {}

// first, see if we can resolve any imports
// that resolve as constants
const filePathProvider = relativePathResolver(currentFilename)
debug('constructed file path provider wrt %s', currentFilename)
const resolvedImports = resolveImportsInAst(AST, filePathProvider)
resolvedImports = resolveImportsInAst(AST, filePathProvider)
debug('resolved imports %o', resolvedImports)

walk.ancestor(
Expand Down
20 changes: 15 additions & 5 deletions src/relative-path-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ function relativePathResolver(currentFilename) {
resolved,
)

const exists = fs.existsSync(resolved)
if (!exists) {
debug('"%s" does not exist', resolved)
return
// Try the original path first
if (fs.existsSync(resolved)) {
return fs.readFileSync(resolved, 'utf-8')
}

// Try with .ts and .js extensions
const extensions = ['.js', '.ts']
for (const ext of extensions) {
const resolvedWithExt = resolved + ext
if (fs.existsSync(resolvedWithExt)) {
debug('found "%s" with extension "%s"', resolved, ext)
return fs.readFileSync(resolvedWithExt, 'utf-8')
}
}

return fs.readFileSync(resolved, 'utf-8')
debug('"%s[.js|.ts]" does not exist', resolved)
return
}
}

Expand Down
33 changes: 33 additions & 0 deletions src/resolve-exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ function resolveExportsInAst(AST, proxy) {
}
}
})
} else if (node.declaration.type === 'TSEnumDeclaration') {
const enumName = node.declaration.id.name
const enumObj = {}

node.declaration.members.forEach((member) => {
const key = member.id.name
// Only support string enums, as tags are strings
if (member.initializer && member.initializer.type === 'Literal' && typeof member.initializer.value === 'string') {
enumObj[key] = member.initializer.value
}
})

exportedValues[enumName] = enumObj
}
},
ExportDefaultDeclaration(node) {
if (node.declaration.type === 'Literal') {
exportedValues.default = node.declaration.value
} else if (node.declaration.type === 'ObjectExpression') {
const obj = {}
node.declaration.properties.forEach((prop) => {
const value = prop.value
if (value.type === 'Literal') {
obj[prop.key.name] = value.value
}
})

exportedValues.default = obj
} else if (node.declaration.type === 'Identifier') {
// export default someVariable
// We would need to resolve the variable, which might be complex
// For now, we just mark that a default export exists
exportedValues.default = node.declaration.name
}
},
},
Expand Down
31 changes: 24 additions & 7 deletions src/resolve-imports.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,31 @@ function resolveImportsInAst(AST, fileProvider) {
}

node.specifiers.forEach((specifier) => {
const importedName = specifier.imported.name
const localName = specifier.local.name
debug('importing "%s" as "%s"', importedName, localName)
if (!exportedValues[importedName]) {
debug('could not find export "%s" in "%s"', importedName, fromWhere)
return
if (specifier.type === 'ImportDefaultSpecifier') {
// Handle default imports: import foo from 'module'
const localName = specifier.local.name
debug('importing default as "%s"', localName)
if (exportedValues.default) {
importedValues[localName] = exportedValues.default
} else {
debug('could not find default export in "%s"', fromWhere)
}
} else if (specifier.type === 'ImportNamespaceSpecifier') {
// Handle namespace imports: import * as foo from 'module'
const localName = specifier.local.name
debug('importing namespace as "%s"', localName)
importedValues[localName] = exportedValues
} else if (specifier.type === 'ImportSpecifier') {
// Handle named imports: import { foo } from 'module'
const importedName = specifier.imported.name
const localName = specifier.local.name
debug('importing "%s" as "%s"', importedName, localName)
if (!exportedValues[importedName]) {
debug('could not find export "%s" in "%s"', importedName, fromWhere)
return
}
importedValues[localName] = exportedValues[importedName]
}
importedValues[localName] = exportedValues[importedName]
})
},
},
Expand Down
5 changes: 5 additions & 0 deletions test/fixture-default/roles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
admin: '@admin',
user: '@user-obj',
guest: '@guest'
}
7 changes: 7 additions & 0 deletions test/fixture-default/spec-object.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import roles from './roles'

it('default object test 1', { tags: roles.admin })

it('default object test 2', { tags: roles.user })

it('default object test 3', { tags: roles.guest })
3 changes: 3 additions & 0 deletions test/fixture-default/spec.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import defaultTag from './tag'

it('default import test', { tags: defaultTag })
1 change: 1 addition & 0 deletions test/fixture-default/tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default '@default-tag'
7 changes: 7 additions & 0 deletions test/fixture-enum/spec.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TestTags } from './tags'

it('enum test 1', { tags: TestTags.smoke })

it('enum test 2', { tags: TestTags.regression })

it('enum test 3', { tags: TestTags.sanity })
5 changes: 5 additions & 0 deletions test/fixture-enum/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum TestTags {
smoke = '@smoke-enum',
regression = '@regression-enum',
sanity = '@sanity-enum'
}
2 changes: 2 additions & 0 deletions test/fixture-extensions/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const envTag = '@production'
export const debugTag = '@debug'
5 changes: 5 additions & 0 deletions test/fixture-extensions/spec2.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { smokeTag, regressionTag } from './tags'

it('test 1', { tags: smokeTag })

it('test 2', { tags: regressionTag })
5 changes: 5 additions & 0 deletions test/fixture-extensions/spec3.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { envTag, debugTag } from './config'

it('test with ts import 1', { tags: envTag })

it('test with ts import 2', { tags: debugTag })
2 changes: 2 additions & 0 deletions test/fixture-extensions/tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const smokeTag = '@smoke'
export const regressionTag = '@regression'
2 changes: 2 additions & 0 deletions test/fixture-mixed/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const namedTag = '@named'
export default '@default-mixed'
5 changes: 5 additions & 0 deletions test/fixture-mixed/spec.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import defaultMixed, { namedTag } from './exports'

it('mixed default test', { tags: defaultMixed })

it('mixed named test', { tags: namedTag })
4 changes: 4 additions & 0 deletions test/fixture-namespace/enum-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum TestTags {
smoke = '@smoke-ns-enum',
regression = '@regression-ns-enum'
}
5 changes: 5 additions & 0 deletions test/fixture-namespace/spec-enum.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as TagsModule from './enum-tags'

it('namespace enum test 1', { tags: TagsModule.TestTags.smoke })

it('namespace enum test 2', { tags: TagsModule.TestTags.regression })
7 changes: 7 additions & 0 deletions test/fixture-namespace/spec.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as allTags from './tags'

it('namespace test 1', { tags: allTags.smokeTag })

it('namespace test 2', { tags: allTags.regressionTag })

it('namespace test 3', { tags: allTags.userTag })
3 changes: 3 additions & 0 deletions test/fixture-namespace/tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const smokeTag = '@smoke'
export const regressionTag = '@regression'
export const userTag = '@user'
4 changes: 2 additions & 2 deletions test/fixture1/spec1.cy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { userTag } from './tags.js'

it('works 1')
it('works 1', { tags: userTag })

it('works 2')
it('works 2', { tags: userTag })
Loading