UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

181 lines (146 loc) 5.04 kB
const debug = require('debug')('codeceptjs:heal') const colors = require('chalk') const Container = require('./container') const recorder = require('./recorder') const output = require('./output') const event = require('./event') /** * @class */ class Heal { constructor() { this.recipes = {} this.fixes = [] this.prepareFns = [] this.contextName = null this.numHealed = 0 } clear() { this.recipes = {} this.fixes = [] this.prepareFns = [] this.contextName = null this.numHealed = 0 } addRecipe(name, opts = {}) { if (!opts.priority) opts.priority = 0 if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`) this.recipes[name] = opts } connectToEvents() { event.dispatcher.on(event.suite.before, suite => { this.contextName = suite.title }) event.dispatcher.on(event.test.started, test => { this.contextName = test.fullTitle() }) event.dispatcher.on(event.test.finished, () => { this.contextName = null }) } hasCorrespondingRecipes(step) { return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0 } async getCodeSuggestions(context) { const suggestions = [] const recipes = matchRecipes(this.recipes, this.contextName) debug('Recipes', recipes) const currentOutputLevel = output.level() output.level(0) for (const [property, prepareFn] of Object.entries( recipes .map(r => r.prepare) .filter(p => !!p) .reduce((acc, obj) => ({ ...acc, ...obj }), {}), )) { if (!prepareFn) continue if (context[property]) continue context[property] = await prepareFn(Container.support()) } output.level(currentOutputLevel) for (const recipe of recipes) { let snippets = await recipe.fn(context) if (!Array.isArray(snippets)) snippets = [snippets] suggestions.push({ name: recipe.name, snippets, }) } return suggestions.filter(s => !isBlank(s.snippets)) } async healStep(failedStep, error, failureContext = {}) { output.debug(`Trying to heal ${failedStep.toCode()} step`) Object.assign(failureContext, { error, step: failedStep, prevSteps: failureContext?.test?.steps?.slice(0, -1) || [], }) const suggestions = await this.getCodeSuggestions(failureContext) if (suggestions.length === 0) { debug('No healing suggestions found') throw error } output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`) debug(suggestions) for (const suggestion of suggestions) { for (const codeSnippet of suggestion.snippets) { try { debug('Executing', codeSnippet) recorder.catch(e => { debug(e) }) if (typeof codeSnippet === 'string') { const I = Container.support('I') await eval(codeSnippet) } else if (typeof codeSnippet === 'function') { await codeSnippet(Container.support()) } this.fixes.push({ recipe: suggestion.name, test: failureContext?.test, step: failedStep, snippet: codeSnippet, }) if (failureContext?.test) { const test = failureContext.test let note = `This test was healed by '${suggestion.name}'` note += `\n\nReplace the failed code:\n\n` note += colors.red(`- ${failedStep.toCode()}\n`) note += colors.green(`+ ${codeSnippet}\n`) test.addNote('heal', note) test.meta.healed = true } recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)'))) this.numHealed++ // recorder.session.restore(); return } catch (err) { debug('Failed to execute code', err) recorder.ignoreErr(err) // healing did not help recorder.catchWithoutStop(err) await recorder.promise() // wait for all promises to resolve } } } output.debug(`Couldn't heal the code for ${failedStep.toCode()}`) recorder.throw(error) } static setDefaultHealers() { require('./template/heal') } } const heal = new Heal() module.exports = heal function matchRecipes(recipes, contextName) { return Object.entries(recipes) .filter(([, recipe]) => !contextName || !recipe.grep || new RegExp(recipe.grep).test(contextName)) .sort(([, a], [, b]) => a.priority - b.priority) .map(([name, recipe]) => { recipe.name = name return recipe }) .filter(r => !!r.fn) } function isBlank(value) { return value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim() === '') }