UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

632 lines (507 loc) 16.3 kB
/* eslint prefer-rest-params: "off", no-console: "off", arrow-body-style: "off"*/ const { _ } = Cypress const debug = require('debug')('spec') const snapshotCommand = require('../plugins/snapshot/snapshotCommand') /** * @type {sinon.SinonMatch} */ const match = Cypress.sinon.match const { stringifyShort } = snapshotCommand const eventCleanseMap = { snapshots: stringifyShort, parent: stringifyShort, tests: stringifyShort, commands: stringifyShort, invocationDetails: stringifyShort, body: '[body]', wallClockStartedAt: match.date, lifecycle: match.number, fnDuration: match.number, duration: match.number, afterFnDuration: match.number, wallClockDuration: match.number, stack: match.string, message: '[error message]', sourceMappedStack: match.string, parsedStack: match.array, } const mochaEventCleanseMap = { ...eventCleanseMap, start: match.date, end: match.date, } const cleanseRunStateMap = { ...eventCleanseMap, 'err.stack': '[err stack]', wallClockStartedAt: new Date(0), wallClockDuration: 1, fnDuration: 1, afterFnDuration: 1, lifecycle: 1, duration: 1, startTime: new Date(0), } const spyOn = (obj, prop, fn) => { const _fn = obj[prop] obj[prop] = function () { fn.apply(this, arguments) const ret = _fn.apply(this, arguments) return ret } } function createCypress (defaultOptions = {}) { /** * @type {sinon.SinonStub} */ let allStubs /** * @type {sinon.SinonStub} */ let mochaStubs /** * @type {sinon.SinonStub} */ let setRunnablesStub const enableStubSnapshots = false // const enableStubSnapshots = true let autCypress const getAutCypress = () => autCypress const snapshotMochaEvents = () => { expect(mochaStubs.args).to.matchSnapshot(mochaEventCleanseMap, name.mocha) } snapshotCommand.registerInCypress() const backupCy = window.cy const backupCypress = window.Cypress beforeEach(() => { window.cy = backupCy window.Cypress = backupCypress }) /** * Spawns an isolated Cypress runner as the AUT, with provided spec/fixture and optional state/config * @param {string | ()=>void | {[key:string]: any}} mochaTestsOrFile * @param {{state?: any, config?: any}} opts */ const runIsolatedCypress = (mochaTestsOrFile, opts = {}) => { opts = _.defaultsDeep(opts, defaultOptions, { state: {}, config: { video: false }, onBeforeRun () {}, visitUrl: 'http://localhost:3500/fixtures/dom.html', visitSuccess: true, }) return cy.visit('/fixtures/isolated-runner.html#/tests/cypress/fixtures/empty_spec.js') .then({ timeout: 60000 }, (win) => { win.runnerWs.destroy() allStubs = cy.stub().snapshot(enableStubSnapshots).log(false) mochaStubs = cy.stub().snapshot(enableStubSnapshots).log(false) setRunnablesStub = cy.stub().snapshot(enableStubSnapshots).log(false) return new Promise((resolve) => { const runIsolatedCypress = () => { autCypress.run.restore() const emit = autCypress.emit const emitMap = autCypress.emitMap const emitThen = autCypress.emitThen cy.stub(autCypress, 'automation').log(false).snapshot(enableStubSnapshots) .callThrough() .withArgs('clear:cookies') .resolves({ foo: 'bar', }) .withArgs('take:screenshot') .resolves({ path: '/path/to/screenshot', size: 12, dimensions: { width: 20, height: 20 }, multipart: false, pixelRatio: 1, takenAt: new Date().toISOString(), name: 'name', blackout: ['.foo'], duration: 100, }) cy.stub(autCypress, 'emit').snapshot(enableStubSnapshots).log(false) .callsFake(function () { const noLog = _.includes([ 'navigation:changed', 'stability:changed', 'window:load', 'url:changed', 'log:added', 'page:loading', 'window:unload', 'newListener', ], arguments[0]) const noCall = _.includes(['window:before:unload', 'mocha'], arguments[0]) const isMocha = _.includes(['mocha'], arguments[0]) if (isMocha) { mochaStubs.apply(this, arguments) } noLog || allStubs.apply(this, ['emit'].concat([].slice.call(arguments))) return noCall || emit.apply(this, arguments) }) cy.stub(autCypress, 'emitMap').snapshot(enableStubSnapshots).log(false) .callsFake(function () { allStubs.apply(this, ['emitMap'].concat([].slice.call(arguments))) return emitMap.apply(this, arguments) }) cy.stub(autCypress, 'emitThen').snapshot(enableStubSnapshots).log(false) .callsFake(function () { allStubs.apply(this, ['emitThen'].concat([].slice.call(arguments))) return emitThen.apply(this, arguments) }) spyOn(autCypress.mocha.getRunner(), 'fail', (...args) => { Cypress.log({ name: 'Runner (fail event)', ended: true, event: true, message: `${args[1]}`, state: 'failed', consoleProps: () => { return { Error: args[1], } }, }) }) // TODO: clean this up, sinon doesn't like wrapping things multiple times // and this catches that error try { cy.spy(cy.state('window').console, 'log').as('console_log').log(false) cy.spy(cy.state('window').console, 'error').as('console_error').log(false) } catch (_e) { // console was already wrapped, noop } autCypress.run((failed) => { resolve({ failed, mochaStubs, autCypress, win }) }) } cy.spy(win.eventManager.reporterBus, 'emit').snapshot(enableStubSnapshots).log(false).as('reporterBus') cy.spy(win.eventManager.localBus, 'emit').snapshot(enableStubSnapshots).log(false).as('localBus') cy.stub(win.runnerWs, 'emit').snapshot(enableStubSnapshots).log(false) .withArgs('watch:test:file') .callsFake(() => { autCypress = win.Cypress cy.stub(autCypress, 'onSpecWindow').snapshot(enableStubSnapshots).log(false).callsFake((specWindow) => { autCypress.onSpecWindow.restore() opts.onBeforeRun({ specWindow, win, autCypress }) const testsInOwnFile = _.isString(mochaTestsOrFile) const relativeFile = testsInOwnFile ? mochaTestsOrFile : 'cypress/fixtures/empty_spec.js' autCypress.onSpecWindow(specWindow, [ { absolute: relativeFile, relative: relativeFile, relativeUrl: `/__cypress/tests?p=${relativeFile}`, }, ]) if (testsInOwnFile) return generateMochaTestsForWin(specWindow, mochaTestsOrFile) }) cy.stub(autCypress, 'run').snapshot(enableStubSnapshots).log(false).callsFake(runIsolatedCypress) }) .withArgs('is:automation:client:connected') .yieldsAsync(true) .withArgs('get:existing:run:state') .callsFake((evt, cb) => { cb(opts.state) }) .withArgs('backend:request', 'reset:server:state') .yieldsAsync({}) .withArgs('backend:request', 'resolve:url') .yieldsAsync({ response: { isOkStatusCode: opts.visitSuccess, isHtml: true, url: opts.visitUrl, } }) .withArgs('set:runnables:and:maybe:record:tests') .callsFake((...args) => { setRunnablesStub(...args) _.last(args)() }) // .withArgs('preserve:run:state') // .callsFake() .withArgs('automation:request') .yieldsAsync({ response: {} }) const c = _.extend({}, Cypress.config(), { isTextTerminal: false, spec: { relative: 'relative/path/to/spec.js', absolute: '/absolute/path/to/spec.js', name: 'empty_spec.js', }, }, opts.config) c.state = {} cy.stub(win.runnerWs, 'on').snapshot(enableStubSnapshots).log(false) win.Runner.start(win.document.getElementById('app'), window.btoa(JSON.stringify(c))) }) }) } const createVerifyTest = (modifier) => { return (title, opts, props) => { if (!props) { props = opts opts = null } const verifyFn = props.verifyFn || verifyFailure const args = _.compact([title, opts, () => { return runIsolatedCypress(`cypress/fixtures/errors/${props.file}`, { onBeforeRun ({ specWindow, win, autCypress }) { specWindow.testToRun = title specWindow.autWindow = win specWindow.autCypress = autCypress if (props.onBeforeRun) { props.onBeforeRun({ specWindow, win }) } }, }) .then(({ win }) => { props.codeFrameText = props.codeFrameText || title props.win = win verifyFn(props) }) }]) ;(modifier ? it[modifier] : it)(...args) } } const verify = { it: createVerifyTest(), } verify.it['only'] = createVerifyTest('only') verify.it['skip'] = createVerifyTest('skip') return { runIsolatedCypress, snapshotMochaEvents, getAutCypress, verify, } } const createHooks = (win, hooks = []) => { _.each(hooks, (hook) => { if (_.isString(hook)) { hook = { type: hook } } let { type, fail, fn, agents } = hook if (fn) { if (hook.eval) { const fnStr = fn.toString() const newFn = function () { return win.eval(`(${fnStr})`).call(this) } Object.defineProperty(newFn, 'length', { value: fn.length }) fn = newFn } return win[type](fn) } if (fail) { const numFailures = fail return win[type](function () { const message = `${type} - ${this._runnable.parent.title || 'root'}` if (agents) { registerAgents(win) } if (_.isNumber(fail) && fail-- <= 0) { debug(`hook pass after (${numFailures}) failures: ${type}`) win.assert(true, message) return } if (agents) { failCypressCommand(win, type) } else { debug(`hook fail: ${type}`) win.assert(false, message) throw new Error(`hook failed: ${type}`) } }) } return win[type](function () { win.assert(true, `${type} - ${this._runnable.parent.title || 'root'}`) debug(`hook pass: ${type}`) }) }) } const createTests = (win, tests = []) => { _.each(tests, (test) => { if (_.isString(test)) { test = { name: test } } let { name, pending, fail, fn, only, agents } = test let it = win.it if (only) { it = it['only'] } if (fn) { if (test.eval) { const fnStr = fn.toString() const newFn = function () { return win.eval(`(${fnStr})`).call(this) } Object.defineProperty(newFn, 'length', { value: fn.length }) fn = newFn } return it(name, fn) } if (pending) { return it(name) } if (fail) { return it(name, () => { if (agents) { registerAgents(win) } if (_.isNumber(fail) && fail-- === 0) { debug(`test pass after retry: ${name}`) win.assert(true, name) return } if (agents) { failCypressCommand(win, name) } else { debug(`test fail: ${name}`) win.assert(false, name) throw new Error(`test fail: ${name}`) } }) } return it(name, () => { debug(`test pass: ${name}`) win.assert(true, name) }) }) } const failCypressCommand = (win, name) => win.cy.wrap(name).then(() => win.assert(false, name)) const registerAgents = (win) => { const obj = { foo: 'bar' } win.cy.stub(obj, 'foo') win.cy.wrap(obj).should('exist') win.cy.server() win.cy.route('https://example.com') } const createSuites = (win, suites = {}) => { _.each(suites, (obj, suiteName) => { let fn = () => { createHooks(win, obj.hooks) createTests(win, obj.tests) createSuites(win, obj.suites) } if (_.isFunction(obj)) { fn = evalFn(win, obj) } win.describe(suiteName, fn) }) } const generateMochaTestsForWin = (win, obj) => { if (typeof obj === 'function') { win.eval(`( ${obj.toString()})()`) return } createHooks(win, obj.hooks) createTests(win, obj.tests) createSuites(win, obj.suites) } const evalFn = (win, fn) => { return function () { return win.eval(`(${fn.toString()})`).call(this) } } const shouldHaveTestResults = (expPassed, expFailed, expPending) => { return () => { expPassed = expPassed || '--' expFailed = expFailed || '--' cy.get('header .passed .num').should('have.text', `${expPassed}`) cy.get('header .failed .num').should('have.text', `${expFailed}`) if (expPending) cy.get('header .pending .num').should('have.text', `${expPending}`) } } const containText = (text) => { return (($el) => { expect($el[0]).property('innerText').contain(text) }) } const getRunState = (Cypress) => { const currentRunnable = Cypress.cy.state('runnable') const currentId = currentRunnable && currentRunnable.id const s = { currentId, tests: Cypress.runner.getTestsState(), startTime: Cypress.runner.getStartTime(), emissions: Cypress.runner.getEmissions(), } s.passed = Cypress.runner.countByTestState(s.tests, 'passed') s.failed = Cypress.runner.countByTestState(s.tests, 'failed') s.pending = Cypress.runner.countByTestState(s.tests, 'pending') s.numLogs = Cypress.Log.countLogsByTests(s.tests) return _.cloneDeep(s) } const verifyFailure = (options) => { const { hasCodeFrame = true, verifyOpenInIde = true, column, codeFrameText, message, stack, file, win, } = options let { regex, line } = options regex = regex || new RegExp(`${file}:${line || '\\d+'}:${column}`) const testOpenInIde = () => { expect(win.runnerWs.emit.withArgs('open:file').lastCall.args[1].file).to.include(file) } win.runnerWs.emit.withArgs('get:user:editor') .yields({ preferredOpener: { id: 'foo-editor', name: 'Foo', openerId: 'foo-editor', isOther: false, }, }) win.runnerWs.emit.withArgs('open:file') cy.contains('View stack trace').click() _.each([].concat(message), (msg) => { cy.get('.runnable-err-message') .should('include.text', msg) cy.get('.runnable-err-stack-trace') .should('not.include.text', msg) }) cy.get('.runnable-err-stack-trace') .invoke('text') .should('match', regex) if (stack) { _.each([].concat(stack), (stackLine) => { cy.get('.runnable-err-stack-trace') .should('include.text', stackLine) }) } cy.get('.runnable-err-stack-trace') .should('not.include.text', '__stackReplacementMarker') if (verifyOpenInIde) { cy.contains('.runnable-err-stack-trace .runnable-err-file-path a', file) .click('left') .should(() => { testOpenInIde() }) } if (!hasCodeFrame) return cy .get('.test-err-code-frame .runnable-err-file-path') .invoke('text') .should('match', regex) cy.get('.test-err-code-frame pre span').should('include.text', codeFrameText) if (verifyOpenInIde) { cy.contains('.test-err-code-frame .runnable-err-file-path a', file) .click() .should(() => { expect(win.runnerWs.emit.withArgs('open:file')).to.be.calledTwice testOpenInIde() }) } } module.exports = { generateMochaTestsForWin, createCypress, containText, cleanseRunStateMap, shouldHaveTestResults, getRunState, }