Skip to content
Merged
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
3 changes: 1 addition & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@ export default [
{
ignores: ['test/data/output', 'lib/css2xpath/*'],
},
...compat.extends('airbnb-base'),
{
languageOptions: {
globals: {
...globals.node,
},

ecmaVersion: 2020,
sourceType: 'commonjs',
sourceType: 'module',
},

rules: {
Expand Down
138 changes: 69 additions & 69 deletions lib/heal.js
Original file line number Diff line number Diff line change
@@ -1,170 +1,170 @@
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');
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;
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;
this.recipes = {}
this.fixes = []
this.prepareFns = []
this.contextName = null
this.numHealed = 0
}

addRecipe(name, opts = {}) {
if (!opts.priority) opts.priority = 0;
if (!opts.priority) opts.priority = 0

if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`);
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)

this.recipes[name] = opts;
this.recipes[name] = opts
}

connectToEvents() {
event.dispatcher.on(event.suite.before, suite => {
this.contextName = suite.title;
});
this.contextName = suite.title
})

event.dispatcher.on(event.test.started, test => {
this.contextName = test.fullTitle();
});
this.contextName = test.fullTitle()
})

event.dispatcher.on(event.test.finished, () => {
this.contextName = null;
});
this.contextName = null
})
}

hasCorrespondingRecipes(step) {
return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0;
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);
const suggestions = []
const recipes = matchRecipes(this.recipes, this.contextName)

debug('Recipes', recipes);
debug('Recipes', recipes)

const currentOutputLevel = output.level();
output.level(0);
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 (!prepareFn) continue

if (context[property]) continue;
context[property] = await prepareFn(Container.support());
if (context[property]) continue
context[property] = await prepareFn(Container.support())
}

output.level(currentOutputLevel);
output.level(currentOutputLevel)

for (const recipe of recipes) {
let snippets = await recipe.fn(context);
if (!Array.isArray(snippets)) snippets = [snippets];
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));
return suggestions.filter(s => !isBlank(s.snippets))
}

async healStep(failedStep, error, failureContext = {}) {
output.debug(`Trying to heal ${failedStep.toCode()} step`);
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);
const suggestions = await this.getCodeSuggestions(failureContext)

if (suggestions.length === 0) {
debug('No healing suggestions found');
throw error;
debug('No healing suggestions found')
throw error
}

output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`);
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)

debug(suggestions);
debug(suggestions)

for (const suggestion of suggestions) {
for (const codeSnippet of suggestion.snippets) {
try {
debug('Executing', codeSnippet);
debug('Executing', codeSnippet)
recorder.catch(e => {
debug(e);
});
debug(e)
})

if (typeof codeSnippet === 'string') {
const I = Container.support('I');
await eval(codeSnippet); // eslint-disable-line
const I = Container.support('I')
await eval(codeSnippet)
} else if (typeof codeSnippet === 'function') {
await codeSnippet(Container.support());
await codeSnippet(Container.support())
}

this.fixes.push({
recipe: suggestion.name,
test: failureContext?.test,
step: failedStep,
snippet: codeSnippet,
});
})

recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')));
this.numHealed++;
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;
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
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);
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
recorder.throw(error)
}

static setDefaultHealers() {
require('./template/heal');
require('./template/heal')
}
}

const heal = new Heal();
const heal = new Heal()

module.exports = 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;
recipe.name = name
return recipe
})
.filter(r => !!r.fn);
.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() === '');
return value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim() === '')
}
14 changes: 6 additions & 8 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -2182,7 +2182,6 @@ class Playwright extends Helper {
let chunked = chunkArray(props, values.length)
chunked = chunked.filter(val => {
for (let i = 0; i < val.length; ++i) {
// eslint-disable-next-line eqeqeq
if (val[i] != values[i]) return false
}
return true
Expand Down Expand Up @@ -2451,7 +2450,7 @@ class Playwright extends Helper {
waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
} else {
const enabledFn = function ([locator, $XPath]) {
eval($XPath) // eslint-disable-line no-eval
eval($XPath)
return $XPath(null, locator).filter(el => !el.disabled).length > 0
}
waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
Expand All @@ -2477,7 +2476,7 @@ class Playwright extends Helper {
waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout })
} else {
const disabledFn = function ([locator, $XPath]) {
eval($XPath) // eslint-disable-line no-eval
eval($XPath)
return $XPath(null, locator).filter(el => el.disabled).length > 0
}
waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
Expand All @@ -2503,7 +2502,7 @@ class Playwright extends Helper {
waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout })
} else {
const valueFn = function ([locator, $XPath, value]) {
eval($XPath) // eslint-disable-line no-eval
eval($XPath)
return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
}
waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], {
Expand Down Expand Up @@ -2537,7 +2536,7 @@ class Playwright extends Helper {
waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout })
} else {
const visibleFn = function ([locator, $XPath, num]) {
eval($XPath) // eslint-disable-line no-eval
eval($XPath)
return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
}
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], {
Expand Down Expand Up @@ -2771,7 +2770,7 @@ class Playwright extends Helper {
if (locator.isXPath()) {
return contextObject.waitForFunction(
([locator, text, $XPath]) => {
eval($XPath) // eslint-disable-line no-eval
eval($XPath)
const el = $XPath(null, locator)
if (!el.length) return false
return el[0].innerText.indexOf(text) > -1
Expand Down Expand Up @@ -2975,7 +2974,7 @@ class Playwright extends Helper {
}
} else {
const visibleFn = function ([locator, $XPath]) {
eval($XPath) // eslint-disable-line no-eval
eval($XPath)
return $XPath(null, locator).length === 0
}
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout })
Expand Down Expand Up @@ -3214,7 +3213,6 @@ class Playwright extends Helper {
}

for (const i in this.requests) {
// eslint-disable-next-line no-prototype-builtins
if (this.requests.hasOwnProperty(i)) {
const request = this.requests[i]

Expand Down
4 changes: 2 additions & 2 deletions lib/helper/Protractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,7 @@ class Protractor extends Helper {
const stream = fs.createWriteStream(outputFile)
stream.write(Buffer.from(png, 'base64'))
stream.end()
return new Promise(resolve => stream.on('finish', resolve)) // eslint-disable-line no-promise-executor-return
return new Promise(resolve => stream.on('finish', resolve))
}

const res = await this._locate(locator)
Expand All @@ -1077,7 +1077,7 @@ class Protractor extends Helper {
const stream = fs.createWriteStream(outputFile)
stream.write(Buffer.from(png, 'base64'))
stream.end()
return new Promise(resolve => stream.on('finish', resolve)) // eslint-disable-line no-promise-executor-return
return new Promise(resolve => stream.on('finish', resolve))
}

if (!fullPage) {
Expand Down
Loading
Loading