diff --git a/lib/els.js b/lib/els.js index 31242a969..1f319df63 100644 --- a/lib/els.js +++ b/lib/els.js @@ -1,115 +1,124 @@ -const output = require('./output'); -const store = require('./store'); -const recorder = require('./recorder'); -const container = require('./container'); -const event = require('./event'); -const Step = require('./step'); -const { truth } = require('./assert/truth'); -const { isAsyncFunction, humanizeFunction } = require('./utils'); +const output = require('./output') +const store = require('./store') +const container = require('./container') +const StepConfig = require('./step/config') +const recordStep = require('./step/record') +const FuncStep = require('./step/func') +const { truth } = require('./assert/truth') +const { isAsyncFunction, humanizeFunction } = require('./utils') function element(purpose, locator, fn) { - if (!fn) { - fn = locator; - locator = purpose; - purpose = 'first element'; + let stepConfig + if (arguments[arguments.length - 1] instanceof StepConfig) { + stepConfig = arguments[arguments.length - 1] } - const step = prepareStep(purpose, locator, fn); - if (!step) return; + if (!fn || fn === stepConfig) { + fn = locator + locator = purpose + purpose = 'first element' + } - return executeStep(step, async () => { - const els = await step.helper._locate(locator); - output.debug(`Found ${els.length} elements, using first element`); + const step = prepareStep(purpose, locator, fn) + if (!step) return - return fn(els[0]); - }); + return executeStep( + step, + async () => { + const els = await step.helper._locate(locator) + output.debug(`Found ${els.length} elements, using first element`) + + return fn(els[0]) + }, + stepConfig, + ) } function eachElement(purpose, locator, fn) { if (!fn) { - fn = locator; - locator = purpose; - purpose = 'for each element'; + fn = locator + locator = purpose + purpose = 'for each element' } - const step = prepareStep(purpose, locator, fn); - if (!step) return; + const step = prepareStep(purpose, locator, fn) + if (!step) return return executeStep(step, async () => { - const els = await step.helper._locate(locator); - output.debug(`Found ${els.length} elements for each elements to iterate`); + const els = await step.helper._locate(locator) + output.debug(`Found ${els.length} elements for each elements to iterate`) - const errs = []; - let i = 0; + const errs = [] + let i = 0 for (const el of els) { try { - await fn(el, i); + await fn(el, i) } catch (err) { - output.error(`eachElement: failed operation on element #${i} ${el}`); - errs.push(err); + output.error(`eachElement: failed operation on element #${i} ${el}`) + errs.push(err) } - i++; + i++ } if (errs.length) { - throw errs[0]; + throw errs[0] } - }); + }) } function expectElement(locator, fn) { - const step = prepareStep('expect element to be', locator, fn); - if (!step) return; + const step = prepareStep('expect element to be', locator, fn) + if (!step) return return executeStep(step, async () => { - const els = await step.helper._locate(locator); - output.debug(`Found ${els.length} elements, first will be used for assertion`); + const els = await step.helper._locate(locator) + output.debug(`Found ${els.length} elements, first will be used for assertion`) - const result = await fn(els[0]); - const assertion = truth(`element (${locator})`, fn.toString()); - assertion.assert(result); - }); + const result = await fn(els[0]) + const assertion = truth(`element (${locator})`, fn.toString()) + assertion.assert(result) + }) } function expectAnyElement(locator, fn) { - const step = prepareStep('expect any element to be', locator, fn); - if (!step) return; + const step = prepareStep('expect any element to be', locator, fn) + if (!step) return return executeStep(step, async () => { - const els = await step.helper._locate(locator); - output.debug(`Found ${els.length} elements, at least one should pass the assertion`); + const els = await step.helper._locate(locator) + output.debug(`Found ${els.length} elements, at least one should pass the assertion`) - const assertion = truth(`any element of (${locator})`, fn.toString()); + const assertion = truth(`any element of (${locator})`, fn.toString()) - let found = false; + let found = false for (const el of els) { - const result = await fn(el); + const result = await fn(el) if (result) { - found = true; - break; + found = true + break } } - if (!found) throw assertion.getException(); - }); + if (!found) throw assertion.getException() + }) } function expectAllElements(locator, fn) { - const step = prepareStep('expect all elements', locator, fn); - if (!step) return; + const step = prepareStep('expect all elements', locator, fn) + if (!step) return return executeStep(step, async () => { - const els = await step.helper._locate(locator); - output.debug(`Found ${els.length} elements, all should pass the assertion`); + const els = await step.helper._locate(locator) + output.debug(`Found ${els.length} elements, all should pass the assertion`) - let i = 1; + let i = 1 for (const el of els) { - output.debug(`checking element #${i}: ${el}`); - const result = await fn(el); - const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn)); - assertion.assert(result); - i++; + output.debug(`checking element #${i}: ${el}`) + const result = await fn(el) + const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn)) + assertion.assert(result) + i++ } - }); + }) } module.exports = { @@ -118,60 +127,32 @@ module.exports = { expectElement, expectAnyElement, expectAllElements, -}; +} function prepareStep(purpose, locator, fn) { - if (store.dryRun) return; - const helpers = Object.values(container.helpers()); + if (store.dryRun) return + const helpers = Object.values(container.helpers()) - const helper = helpers.filter(h => !!h._locate)[0]; + const helper = helpers.filter(h => !!h._locate)[0] if (!helper) { - throw new Error('No helper enabled with _locate method with returns a list of elements.'); + throw new Error('No helper enabled with _locate method with returns a list of elements.') } if (!isAsyncFunction(fn)) { - throw new Error('Async function should be passed into each element'); + throw new Error('Async function should be passed into each element') } - const isAssertion = purpose.startsWith('expect'); + const isAssertion = purpose.startsWith('expect') - const step = new Step(helper, `${purpose} within "${locator}" ${isAssertion ? 'to be' : 'to'}`); - step.setActor('EL'); - step.setArguments([humanizeFunction(fn)]); - step.helperMethod = '_locate'; + const step = new FuncStep(`${purpose} within "${locator}" ${isAssertion ? 'to be' : 'to'}`) + step.setHelper(helper) + step.setArguments([humanizeFunction(fn)]) // user defined function is a passed argument - return step; + return step } -async function executeStep(step, action) { - let error; - const promise = recorder.add('register element wrapper', async () => { - event.emit(event.step.started, step); - - try { - await action(); - } catch (err) { - recorder.throw(err); - event.emit(event.step.failed, step, err); - event.emit(event.step.finished, step); - // event.emit(event.step.after, step) - error = err; - // await recorder.promise(); - return; - } - - event.emit(event.step.after, step); - event.emit(event.step.passed, step); - event.emit(event.step.finished, step); - }); - - // await recorder.promise(); - - // if (error) { - // console.log('error', error.inspect()) - // return recorder.throw(error); - // } - - return promise; +async function executeStep(step, action, stepConfig = {}) { + step.setCallable(action) + return recordStep(step, [stepConfig]) } diff --git a/lib/step/func.js b/lib/step/func.js new file mode 100644 index 000000000..ab437e9dc --- /dev/null +++ b/lib/step/func.js @@ -0,0 +1,46 @@ +const BaseStep = require('./base') +const store = require('../store') + +/** + * Function executed as a step + */ +class FuncStep extends BaseStep { + // this is actual function that should be executed within step + setCallable(fn) { + this.fn = fn + } + + // helper is optional, if we need to allow step to access helper methods + setHelper(helper) { + this.helper = helper + } + + run() { + if (!this.fn) throw new Error('Function is not set') + + // we wrap that function to track time and status + // and disable it in dry run mode + this.args = Array.prototype.slice.call(arguments) + this.startTime = +Date.now() + + if (store.dryRun) { + this.setStatus('success') + // should we add Proxy and dry run resolver here? + return Promise.resolve(true) + } + + let result + try { + result = this.fn.apply(this.helper, this.args) + this.setStatus('success') + this.endTime = +Date.now() + } catch (err) { + this.endTime = +Date.now() + this.setStatus('failed') + throw err + } + return result + } +} + +module.exports = FuncStep diff --git a/test/unit/els_test.js b/test/unit/els_test.js index 2b128f778..09f2081c0 100644 --- a/test/unit/els_test.js +++ b/test/unit/els_test.js @@ -1,178 +1,217 @@ -const assert = require('assert'); -const { expect } = require('chai'); -const els = require('../../lib/els'); -const recorder = require('../../lib/recorder'); -const Container = require('../../lib/container'); -const Helper = require('../../lib/helper'); +const assert = require('assert') +const { expect } = require('chai') +const els = require('../../lib/els') +const recorder = require('../../lib/recorder') +const Container = require('../../lib/container') +const Helper = require('../../lib/helper') +const StepConfig = require('../../lib/step/config') class TestHelper extends Helper { constructor() { - super(); - this.elements = []; + super() + this.elements = [] } async _locate(locator) { - return this.elements; + return this.elements } } -describe('els', () => { - let helper; +describe('els', function () { + let helper beforeEach(() => { - helper = new TestHelper(); - Container.clear(); + helper = new TestHelper() + Container.clear() Container.append({ helpers: { test: helper, }, - }); - recorder.reset(); - recorder.startUnlessRunning(); - }); + }) + recorder.reset() + recorder.startUnlessRunning() + }) describe('#element', () => { it('should execute function on first found element', async () => { - helper.elements = ['el1', 'el2', 'el3']; - let elementUsed; + helper.elements = ['el1', 'el2', 'el3'] + let elementUsed await els.element('my test', '.selector', async el => { - elementUsed = el; - }); + elementUsed = el + }) - await recorder.promise(); + await recorder.promise() - assert.equal(elementUsed, 'el1'); - }); + assert.equal(elementUsed, 'el1') + }) it('should work without purpose parameter', async () => { - helper.elements = ['el1', 'el2']; - let elementUsed; + helper.elements = ['el1', 'el2'] + let elementUsed await els.element('.selector', async el => { - elementUsed = el; - }); + elementUsed = el + }) - assert.equal(elementUsed, 'el1'); - }); + assert.equal(elementUsed, 'el1') + }) it('should throw error when no helper with _locate available', async () => { - Container.clear(); + Container.clear() try { - await els.element('.selector', async () => {}); - throw new Error('should have thrown error'); + await els.element('.selector', async () => {}) + throw new Error('should have thrown error') } catch (e) { - expect(e.message).to.include('No helper enabled with _locate method'); + expect(e.message).to.include('No helper enabled with _locate method') } - }); - }); + }) + + it('should fail on timeout if timeout is set', async () => { + helper.elements = ['el1', 'el2'] + try { + await els.element( + '.selector', + async () => { + await new Promise(resolve => setTimeout(resolve, 1000)) + }, + new StepConfig().timeout(0.01), + ) + await recorder.promise() + throw new Error('should have thrown error') + } catch (e) { + recorder.catch() + expect(e.message).to.include('was interrupted on timeout 10ms') + } + }) + + it('should retry until timeout when retries are set', async () => { + helper.elements = ['el1', 'el2'] + let attempts = 0 + await els.element( + '.selector', + async els => { + attempts++ + if (attempts < 2) { + throw new Error('keep retrying') + } + return els.slice(0, attempts) + }, + new StepConfig().retry(2), + ) + + await recorder.promise() + expect(attempts).to.be.at.least(2) + expect(helper.elements).to.deep.equal(['el1', 'el2']) + }) + }) describe('#eachElement', () => { it('should execute function on each element', async () => { - helper.elements = ['el1', 'el2', 'el3']; - const usedElements = []; + helper.elements = ['el1', 'el2', 'el3'] + const usedElements = [] await els.eachElement('.selector', async el => { - usedElements.push(el); - }); + usedElements.push(el) + }) - assert.deepEqual(usedElements, ['el1', 'el2', 'el3']); - }); + assert.deepEqual(usedElements, ['el1', 'el2', 'el3']) + }) it('should provide index as second parameter', async () => { - helper.elements = ['el1', 'el2']; - const indices = []; + helper.elements = ['el1', 'el2'] + const indices = [] await els.eachElement('.selector', async (el, i) => { - indices.push(i); - }); + indices.push(i) + }) - assert.deepEqual(indices, [0, 1]); - }); + assert.deepEqual(indices, [0, 1]) + }) it('should work without purpose parameter', async () => { - helper.elements = ['el1', 'el2']; - const usedElements = []; + helper.elements = ['el1', 'el2'] + const usedElements = [] await els.eachElement('.selector', async el => { - usedElements.push(el); - }); + usedElements.push(el) + }) - assert.deepEqual(usedElements, ['el1', 'el2']); - }); + assert.deepEqual(usedElements, ['el1', 'el2']) + }) it('should throw first error if operation fails', async () => { - helper.elements = ['el1', 'el2']; + helper.elements = ['el1', 'el2'] try { await els.eachElement('.selector', async el => { - throw new Error(`failed on ${el}`); - }); - await recorder.promise(); - throw new Error('should have thrown error'); + throw new Error(`failed on ${el}`) + }) + await recorder.promise() + throw new Error('should have thrown error') } catch (e) { - expect(e.message).to.equal('failed on el1'); + expect(e.message).to.equal('failed on el1') } - }); - }); + }) + }) describe('#expectElement', () => { it('should pass when condition is true', async () => { - helper.elements = ['el1']; + helper.elements = ['el1'] - await els.expectElement('.selector', async () => true); - }); + await els.expectElement('.selector', async () => true) + }) it('should fail when condition is false', async () => { - helper.elements = ['el1']; + helper.elements = ['el1'] try { - await els.expectElement('.selector', async () => false); - await recorder.promise(); - throw new Error('should have thrown error'); + await els.expectElement('.selector', async () => false) + await recorder.promise() + throw new Error('should have thrown error') } catch (e) { - expect(e.cliMessage()).to.include('element (.selector)'); + expect(e.cliMessage()).to.include('element (.selector)') } - }); - }); + }) + }) describe('#expectAnyElement', () => { it('should pass when any element matches condition', async () => { - helper.elements = ['el1', 'el2', 'el3']; + helper.elements = ['el1', 'el2', 'el3'] - await els.expectAnyElement('.selector', async el => el === 'el2'); - }); + await els.expectAnyElement('.selector', async el => el === 'el2') + }) it('should fail when no element matches condition', async () => { - helper.elements = ['el1', 'el2']; + helper.elements = ['el1', 'el2'] try { - await els.expectAnyElement('.selector', async () => false); - await recorder.promise(); - throw new Error('should have thrown error'); + await els.expectAnyElement('.selector', async () => false) + await recorder.promise() + throw new Error('should have thrown error') } catch (e) { - expect(e.cliMessage()).to.include('any element of (.selector)'); + expect(e.cliMessage()).to.include('any element of (.selector)') } - }); - }); + }) + }) describe('#expectAllElements', () => { it('should pass when all elements match condition', async () => { - helper.elements = ['el1', 'el2']; + helper.elements = ['el1', 'el2'] - await els.expectAllElements('.selector', async () => true); - }); + await els.expectAllElements('.selector', async () => true) + }) it('should fail when any element does not match condition', async () => { - helper.elements = ['el1', 'el2', 'el3']; + helper.elements = ['el1', 'el2', 'el3'] try { - await els.expectAllElements('.selector', async el => el !== 'el2'); - await recorder.promise(); - throw new Error('should have thrown error'); + await els.expectAllElements('.selector', async el => el !== 'el2') + await recorder.promise() + throw new Error('should have thrown error') } catch (e) { - expect(e.cliMessage()).to.include('element #2 of (.selector)'); + expect(e.cliMessage()).to.include('element #2 of (.selector)') } - }); - }); -}); + }) + }) +})