UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

178 lines (158 loc) 5.54 kB
import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import * as acorn from 'acorn' import { getConfig, getTestRoot } from './utils.js' import Codecept from '../codecept.js' import container from '../container.js' import { getParamsToString } from '../parser.js' import { methodsOfObject } from '../utils.js' import output from '../output.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const helperDir = path.resolve(__dirname, '..', 'helper') const webapiDir = path.resolve(__dirname, '..', '..', 'docs', 'webapi') let partialsCache = null function loadWebApiPartials() { if (partialsCache) return partialsCache const map = new Map() if (fs.existsSync(webapiDir)) { for (const file of fs.readdirSync(webapiDir)) { if (path.extname(file) !== '.mustache') continue const name = path.basename(file, '.mustache') map.set(name, fs.readFileSync(path.join(webapiDir, file), 'utf8')) } } partialsCache = map return map } function resolveHelperSource(helper, helperName, config, testsPath) { const builtin = path.join(helperDir, `${helper.constructor.name}.js`) if (fs.existsSync(builtin)) return builtin const requirePath = config?.helpers?.[helperName]?.require if (requirePath) { const resolved = path.isAbsolute(requirePath) ? requirePath : path.resolve(testsPath, requirePath) if (fs.existsSync(resolved)) return resolved } return null } function findClassNode(ast) { for (const node of ast.body) { if (node.type === 'ClassDeclaration') return node if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'ClassDeclaration') return node.declaration if (node.type === 'ExportDefaultDeclaration' && node.declaration?.type === 'ClassDeclaration') return node.declaration } return null } function stripJsDoc(value) { return value .split('\n') .map(line => line.replace(/^\s*\* ?/, '')) .join('\n') .trim() } function resolvePartials(text, partials) { return text.replace(/\{\{>\s*([\w-]+)\s*\}\}/g, (match, name) => { return partials.has(name) ? partials.get(name) : match }) } function extractMethodDocs(helper, helperName, config, testsPath, partials) { const result = new Map() const sourceFile = resolveHelperSource(helper, helperName, config, testsPath) if (!sourceFile) return result let source try { source = fs.readFileSync(sourceFile, 'utf8') } catch { return result } const comments = [] let ast try { ast = acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module', locations: true, onComment: comments, }) } catch { return result } const classNode = findClassNode(ast) if (!classNode) return result const blockComments = comments .filter(c => c.type === 'Block' && c.value.startsWith('*')) .sort((a, b) => a.start - b.start) let cursor = 0 for (const member of classNode.body.body) { if (member.type !== 'MethodDefinition') continue if (member.kind === 'constructor' || member.static) continue const name = member.key?.name if (!name || name.startsWith('_')) continue let attached = null let attachedIdx = -1 for (let i = cursor; i < blockComments.length; i++) { const c = blockComments[i] if (c.end > member.start) break attached = c attachedIdx = i } if (attached) { cursor = attachedIdx + 1 const stripped = stripJsDoc(attached.value) const resolved = resolvePartials(stripped, partials) result.set(name, resolved) } } return result } function printDocBlock(doc) { if (!doc) return for (const line of doc.split('\n')) { output.print(` ${line}`) } output.print('') } export default async function (path, options = {}) { const configFile = options.config const testsPath = getTestRoot(configFile || path) const config = await getConfig(configFile || testsPath) const codecept = new Codecept(config, {}) await codecept.init(testsPath) await container.started() const filter = options.action ? options.action.replace(/^I\./, '') : null const showDocs = !!(options.docs || filter) const partials = showDocs ? loadWebApiPartials() : null if (!filter) output.print('List of test actions: -- ') const helpers = container.helpers() const supportI = container.support('I') const actions = [] let matched = false for (const name in helpers) { const helper = helpers[name] const docs = showDocs ? extractMethodDocs(helper, name, config, testsPath, partials) : null methodsOfObject(helper).forEach(action => { actions[action] = 1 if (filter && action !== filter) return const params = getParamsToString(helper[action]) output.print(` ${output.colors.grey(name)} I.${output.colors.bold(action)}(${params})`) if (docs && docs.has(action)) printDocBlock(docs.get(action)) matched = true }) } for (const name in supportI) { if (actions[name]) continue if (filter && name !== filter) continue const actor = supportI[name] const params = getParamsToString(actor) output.print(` I.${output.colors.bold(name)}(${params})`) matched = true } if (filter && !matched) { output.print(`No action named ${output.colors.bold(filter)} found in enabled helpers or support objects.`) return } if (!filter) { output.print('PS: Actions are retrieved from enabled helpers. ') output.print('Implement custom actions in your helper classes.') } }