diff --git a/circle.yml b/circle.yml index 6966d8f3b0f5..75c7f61db869 100644 --- a/circle.yml +++ b/circle.yml @@ -124,6 +124,25 @@ commands: path: /tmp/artifacts - store-npm-logs + run-runner-integration-tests: + parameters: + browser: + description: browser shortname to target + type: string + steps: + - attach_workspace: + at: ~/ + - run: + command: | + CYPRESS_KONFIG_ENV=production \ + CYPRESS_RECORD_KEY=$PACKAGES_RECORD_KEY \ + yarn workspace @packages/runner cypress:run --record --parallel --group runner-integration-<< parameters.browser >> --browser <> + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + run-e2e-tests: parameters: browser: @@ -653,6 +672,20 @@ jobs: browser: firefox chunk: "8" + "runner-integration-tests-chrome": + <<: *defaults + parallelism: 2 + steps: + - run-runner-integration-tests: + browser: chrome + + "runner-integration-tests-firefox": + <<: *defaults + parallelism: 2 + steps: + - run-runner-integration-tests: + browser: firefox + "driver-integration-tests-chrome": <<: *defaults parallelism: 5 @@ -1486,6 +1519,13 @@ linux-workflow: &linux-workflow - driver-integration-tests-firefox: requires: - build + - runner-integration-tests-chrome: + requires: + - build + - runner-integration-tests-firefox: + requires: + - build + ## TODO: add these back in when flaky tests are fixed # - driver-integration-tests-electron: # requires: diff --git a/packages/driver/cypress/fixtures/isolated-runner.html b/packages/driver/cypress/fixtures/isolated-runner.html new file mode 100644 index 000000000000..20b9e67021ec --- /dev/null +++ b/packages/driver/cypress/fixtures/isolated-runner.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + +
+ + + + + diff --git a/packages/driver/cypress/plugins/server.js b/packages/driver/cypress/plugins/server.js index 7c0bbfddbaad..6b756f85db34 100644 --- a/packages/driver/cypress/plugins/server.js +++ b/packages/driver/cypress/plugins/server.js @@ -35,6 +35,9 @@ ports.forEach((port) => { }) }) + // allows us to serve the testrunner into an iframe for testing + app.use('/isolated-runner', express.static(path.join(__dirname, '../../../runner/dist'))) + app.get('/node_modules/*', (req, res) => { return res.sendFile(path.join('node_modules', req.params[0]), { root: path.join(__dirname, '../..'), diff --git a/packages/driver/src/cy/aliases.js b/packages/driver/src/cy/aliases.js index 2500567adffb..8bf2dff1c6ab 100644 --- a/packages/driver/src/cy/aliases.js +++ b/packages/driver/src/cy/aliases.js @@ -1,4 +1,3 @@ -/* globals cy */ const _ = require('lodash') const $errUtils = require('../cypress/error_utils') @@ -44,14 +43,14 @@ const validateAlias = (alias) => { } } -const create = (state) => { +const create = (cy) => { const addAlias = (ctx, aliasObj) => { const { alias, subject } = aliasObj - const aliases = state('aliases') || {} + const aliases = cy.state('aliases') || {} aliases[alias] = aliasObj - state('aliases', aliases) + cy.state('aliases', aliases) const remoteSubject = cy.getRemotejQueryInstance(subject) @@ -59,7 +58,7 @@ const create = (state) => { } const getNextAlias = () => { - const next = state('current').get('next') + const next = cy.state('current').get('next') if (next && (next.get('name') === 'as')) { return next.get('args')[0] @@ -67,7 +66,7 @@ const create = (state) => { } const getAlias = (name, cmd, log) => { - const aliases = state('aliases') || {} + const aliases = cy.state('aliases') || {} // bail if the name doesnt reference an alias if (!aliasRe.test(name)) { @@ -85,7 +84,7 @@ const create = (state) => { } const getAvailableAliases = () => { - const aliases = state('aliases') + const aliases = cy.state('aliases') if (!aliases) { return [] @@ -108,7 +107,7 @@ const create = (state) => { }) } - cmd = cmd ?? ((log && log.get('name')) || state('current').get('name')) + cmd = cmd ?? ((log && log.get('name')) || cy.state('current').get('name')) displayName = aliasDisplayName(name) const errPath = availableAliases.length diff --git a/packages/driver/src/cy/assertions.js b/packages/driver/src/cy/assertions.js index 658a7a476204..4a66bf22a75a 100644 --- a/packages/driver/src/cy/assertions.js +++ b/packages/driver/src/cy/assertions.js @@ -1,4 +1,3 @@ -/* global cy Cypress */ const _ = require('lodash') const Promise = require('bluebird') @@ -70,14 +69,14 @@ const parseValueActualAndExpected = (value, actual, expected) => { return obj } -const create = function (state, queue, retryFn) { +const create = function (Cypress, cy) { const getUpcomingAssertions = () => { - const index = state('index') + 1 + const index = cy.state('index') + 1 const assertions = [] // grab the rest of the queue'd commands - for (let cmd of queue.slice(index).get()) { + for (let cmd of cy.queue.slice(index).get()) { // don't break on utilities, just skip over them if (cmd.is('utility')) { continue @@ -111,12 +110,12 @@ const create = function (state, queue, retryFn) { // them up with existing ones cmd.set('assertionIndex', 0) - if (state('current') != null) { - state('current').set('currentAssertionCommand', cmd) + if (cy.state('current') != null) { + cy.state('current').set('currentAssertionCommand', cmd) } return cmd.get('fn').originalFn.apply( - state('ctx'), + cy.state('ctx'), [subject].concat(cmd.get('args')), ) }) @@ -139,7 +138,7 @@ const create = function (state, queue, retryFn) { const verifyUpcomingAssertions = function (subject, options = {}, callbacks = {}) { const cmds = getUpcomingAssertions() - state('upcomingAssertions', cmds) + cy.state('upcomingAssertions', cmds) // we're applying the default assertion in the // case where there are no upcoming assertion commands @@ -234,7 +233,7 @@ const create = function (state, queue, retryFn) { } if (_.isFunction(onRetry)) { - return retryFn(onRetry, options) + return cy.retry(onRetry, options) } } @@ -264,7 +263,7 @@ const create = function (state, queue, retryFn) { } // when we do immediately unbind this function - state('onBeforeLog', null) + cy.state('onBeforeLog', null) const insertNewLog = (log) => { cmd.log(log) @@ -347,7 +346,7 @@ const create = function (state, queue, retryFn) { return insertNewLog(log) } - state('onBeforeLog', setCommandLog) + cy.state('onBeforeLog', setCommandLog) // send verify=true as the last arg return assertFn.apply(this, args.concat(true)) @@ -387,16 +386,16 @@ const create = function (state, queue, retryFn) { } const restore = () => { - state('upcomingAssertions', []) + cy.state('upcomingAssertions', []) // no matter what we need to // restore the assert fn - return state('overrideAssert', undefined) + return cy.state('overrideAssert', undefined) } // store this in case our test ends early // and we reset between tests - state('overrideAssert', overrideAssert) + cy.state('overrideAssert', overrideAssert) return Promise .reduce(fns, assertions, [subject]) @@ -465,7 +464,7 @@ const create = function (state, queue, retryFn) { // if our current command is an assertion type isAssertionType(current) || // are we currently verifying assertions? - (state('upcomingAssertions') && state('upcomingAssertions').length > 0) || + (cy.state('upcomingAssertions') && cy.state('upcomingAssertions').length > 0) || // did the function have arguments functionHadArguments(current) } @@ -509,7 +508,7 @@ const create = function (state, queue, retryFn) { const assert = function (...args) { // if we've temporarily overriden assertions // then just bail early with this function - const fn = state('overrideAssert') || assertFn + const fn = cy.state('overrideAssert') || assertFn return fn.apply(this, args) } diff --git a/packages/driver/src/cy/commands/agents.js b/packages/driver/src/cy/commands/agents.js index f9c6db8bf6fe..3ac6e5561410 100644 --- a/packages/driver/src/cy/commands/agents.js +++ b/packages/driver/src/cy/commands/agents.js @@ -63,7 +63,7 @@ const onInvoke = function (Cypress, obj, args) { error: obj.error, type: 'parent', end: true, - snapshot: true, + snapshot: !agent._noSnapshot, event: true, consoleProps () { const consoleObj = {} @@ -218,6 +218,13 @@ module.exports = function (Commands, Cypress, cy, state) { return agent } + // disable DOM snapshots during log for this agent + agent.snapshot = (bool = true) => { + agent._noSnapshot = !bool + + return agent + } + agent.as = (alias) => { cy.validateAlias(alias) cy.addAlias(ctx, { diff --git a/packages/driver/src/cy/commands/navigation.js b/packages/driver/src/cy/commands/navigation.js index db48f407528d..db05767a7545 100644 --- a/packages/driver/src/cy/commands/navigation.js +++ b/packages/driver/src/cy/commands/navigation.js @@ -107,7 +107,7 @@ const specifyFileByRelativePath = (url, log) => { }) } -const aboutBlank = (win) => { +const aboutBlank = (cy, win) => { return new Promise((resolve) => { cy.once('window:load', resolve) @@ -198,7 +198,7 @@ const formSubmitted = (Cypress, e) => { }) } -const pageLoading = (bool, state) => { +const pageLoading = (bool, Cypress, state) => { if (state('pageLoading') === bool) { return } @@ -215,7 +215,7 @@ const stabilityChanged = (Cypress, state, config, stable) => { // if we're currently visiting about blank // and becoming unstable for the first time // notifiy that we're page loading - pageLoading(true, state) + pageLoading(true, Cypress, state) return } @@ -226,7 +226,7 @@ const stabilityChanged = (Cypress, state, config, stable) => { } // let the world know that the app is page:loading - pageLoading(!stable, state) + pageLoading(!stable, Cypress, state) // if we aren't becoming unstable // then just return now @@ -1021,7 +1021,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { hasVisitedAboutBlank = true currentlyVisitingAboutBlank = true - return aboutBlank(win) + return aboutBlank(cy, win) .then(() => { currentlyVisitingAboutBlank = false diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index 838b2f844bb4..9ffd59999ecb 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -78,10 +78,18 @@ const setTopOnError = function (cy) { curCy = cy + // prevent overriding top.onerror twice when loading more than one + // instance of test runner. + if (top.onerror && top.onerror.isCypressHandler) { + return + } + const onTopError = function () { return curCy.onUncaughtException.apply(curCy, arguments) } + onTopError.isCypressHandler = true + top.onerror = onTopError // Prevent Mocha from setting top.onerror which would override our handler @@ -96,7 +104,12 @@ const setTopOnError = function (cy) { }) } +// NOTE: this makes the cy object an instance +// TODO: refactor the 'create' method below into this class +class $Cy {} + const create = function (specWindow, Cypress, Cookies, state, config, log) { + let cy = new $Cy() let stopped = false const commandFns = {} @@ -130,7 +143,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const timeouts = $Timeouts.create(state) const stability = $Stability.create(Cypress, state) const retries = $Retries.create(Cypress, state, timeouts.timeout, timeouts.clearTimeout, stability.whenStable, onFinishAssertions) - const assertions = $Assertions.create(state, queue, retries.retry) + const assertions = $Assertions.create(Cypress, cy) const jquery = $jQuery.create(state) const location = $Location.create(state) @@ -142,7 +155,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const { expect } = $Chai.create(specWindow, state, assertions.assert) const xhrs = $Xhrs.create(state) - const aliases = $Aliases.create(state) + const aliases = $Aliases.create(cy) const errors = $Errors.create(state, config, log) const ensures = $Ensures.create(state, expect) @@ -788,7 +801,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { return finish(err) } - const cy = { + _.extend(cy, { id: _.uniqueId('cy'), // synchrounous querying @@ -1377,7 +1390,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { } } }, - } + }) _.each(privateProps, (obj, key) => { return Object.defineProperty(cy, key, { diff --git a/packages/reporter/src/main.tsx b/packages/reporter/src/main.tsx index 319858cc6a88..079845b7d6a5 100644 --- a/packages/reporter/src/main.tsx +++ b/packages/reporter/src/main.tsx @@ -113,9 +113,11 @@ declare global { } } +// NOTE: this is for testing Cypress-in-Cypress if (window.Cypress) { window.state = appState window.render = (props) => { + // @ts-ignore render(} />, document.getElementById('app')) } } diff --git a/packages/runner/README.md b/packages/runner/README.md index d731244fd23d..e36c0fd2fb3e 100644 --- a/packages/runner/README.md +++ b/packages/runner/README.md @@ -39,6 +39,20 @@ yarn workspace @packages/runner build-prod ## Testing +### Node Unit Tests + ```bash yarn workspace @packages/runner test ``` + +### Cypress Tests + +You can run Cypress tests found in [`cypress/integration`](./cypress/integration): +```bash +yarn workspace @packages/runner cypress:open +``` + +To watch and reload changes to the runner while testing you'll want to run: +```bash +yarn workspace @packages/runner watch +``` diff --git a/packages/runner/__snapshots__/runner.mochaEvents.spec.js b/packages/runner/__snapshots__/runner.mochaEvents.spec.js new file mode 100644 index 000000000000..e57eb46540f5 --- /dev/null +++ b/packages/runner/__snapshots__/runner.mochaEvents.spec.js @@ -0,0 +1,2470 @@ +exports['src/cypress/runner tests finish with correct state hook failures fail in [before] #1'] = [ + [ + "mocha", + "start", + { + "start": "match.date" + } + ], + [ + "mocha", + "suite", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "suite", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r3", + "title": "\"before all\" hook", + "hookName": "before all", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "fail", + { + "id": "r3", + "title": "\"before all\" hook for \"test 1\"", + "hookName": "before all", + "hookId": "h1", + "err": "{Object 9}", + "state": "failed", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null, + "originalTitle": "\"before all\" hook" + }, + { + "message": "[error message]", + "name": "AssertionError", + "stack": "match.string", + "sourceMappedStack": "match.string", + "parsedStack": "match.array", + "actual": null, + "showDiff": false + } + ], + [ + "mocha", + "suite end", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r3", + "order": 1, + "title": "test 1", + "hookName": "before all", + "err": "{Object 9}", + "state": "failed", + "failedFromHookId": "h1", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "end", + { + "end": "match.date" + } + ] +] + +exports['src/cypress/runner tests finish with correct state hook failures fail in [beforeEach] #1'] = [ + [ + "mocha", + "start", + { + "start": "match.date" + } + ], + [ + "mocha", + "suite", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "suite", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r3", + "order": 1, + "title": "test 1", + "body": "[body]", + "type": "test", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r3", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "fail", + { + "id": "r3", + "title": "\"before each\" hook for \"test 1\"", + "hookName": "before each", + "hookId": "h1", + "err": "{Object 9}", + "state": "failed", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null, + "originalTitle": "\"before each\" hook" + }, + { + "message": "[error message]", + "name": "AssertionError", + "stack": "match.string", + "sourceMappedStack": "match.string", + "parsedStack": "match.array", + "actual": null, + "showDiff": false + } + ], + [ + "mocha", + "suite end", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r3", + "order": 1, + "title": "test 1", + "hookName": "before each", + "err": "{Object 9}", + "state": "failed", + "failedFromHookId": "h1", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "before each": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "end", + { + "end": "match.date" + } + ] +] + +exports['src/cypress/runner tests finish with correct state hook failures fail in [afterEach] #1'] = [ + [ + "mocha", + "start", + { + "start": "match.date" + } + ], + [ + "mocha", + "suite", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "suite", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r3", + "order": 1, + "title": "test 1", + "body": "[body]", + "type": "test", + "file": null + } + ], + [ + "mocha", + "pass", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "test end", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r3", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "fail", + { + "id": "r3", + "title": "\"after each\" hook for \"test 1\"", + "hookName": "after each", + "hookId": "h1", + "err": "{Object 9}", + "state": "failed", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null, + "originalTitle": "\"after each\" hook" + }, + { + "message": "[error message]", + "name": "AssertionError", + "stack": "match.string", + "sourceMappedStack": "match.string", + "parsedStack": "match.array", + "actual": null, + "showDiff": false + } + ], + [ + "mocha", + "suite end", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r3", + "order": 1, + "title": "test 1", + "hookName": "after each", + "err": "{Object 9}", + "state": "failed", + "failedFromHookId": "h1", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "end", + { + "end": "match.date" + } + ] +] + +exports['src/cypress/runner tests finish with correct state hook failures fail in [after] #1'] = [ + [ + "mocha", + "start", + { + "start": "match.date" + } + ], + [ + "mocha", + "suite", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "suite", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r3", + "order": 1, + "title": "test 1", + "body": "[body]", + "type": "test", + "file": null + } + ], + [ + "mocha", + "pass", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + }, + "file": null + } + ], + [ + "mocha", + "test end", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + }, + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + }, + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r4", + "order": 2, + "title": "test 2", + "body": "[body]", + "type": "test", + "file": null + } + ], + [ + "mocha", + "pass", + { + "id": "r4", + "order": 2, + "title": "test 2", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "test end", + { + "id": "r4", + "order": 2, + "title": "test 2", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r4", + "title": "\"after all\" hook", + "hookName": "after all", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "fail", + { + "id": "r4", + "title": "\"after all\" hook for \"test 2\"", + "hookName": "after all", + "hookId": "h1", + "err": "{Object 9}", + "state": "failed", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null, + "originalTitle": "\"after all\" hook" + }, + { + "message": "[error message]", + "name": "AssertionError", + "stack": "match.string", + "sourceMappedStack": "match.string", + "parsedStack": "match.array", + "actual": null, + "showDiff": false + } + ], + [ + "mocha", + "suite end", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r4", + "order": 2, + "title": "test 2", + "hookName": "after all", + "err": "{Object 9}", + "state": "failed", + "failedFromHookId": "h1", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "end", + { + "end": "match.date" + } + ] +] + +exports['src/cypress/runner tests finish with correct state mocha grep fail with [only] #1'] = [ + [ + "mocha", + "start", + { + "start": "match.date" + } + ], + [ + "mocha", + "suite", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "suite", + { + "id": "r4", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"before all\" hook", + "hookName": "before all", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"before all\" hook", + "hookName": "before all", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r5", + "title": "test 2", + "body": "[body]", + "type": "test", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "fail", + { + "id": "r5", + "title": "test 2", + "err": "{Object 9}", + "state": "failed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + }, + { + "message": "[error message]", + "name": "AssertionError", + "stack": "match.string", + "sourceMappedStack": "match.string", + "parsedStack": "match.array", + "actual": null, + "showDiff": false + } + ], + [ + "mocha", + "test end", + { + "id": "r5", + "title": "test 2", + "err": "{Object 9}", + "state": "failed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"after all\" hook", + "hookName": "after all", + "hookId": "h4", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"after all\" hook", + "hookName": "after all", + "hookId": "h4", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r5", + "title": "test 2", + "err": "{Object 9}", + "state": "failed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r4", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "end", + { + "end": "match.date" + } + ] +] + +exports['src/cypress/runner tests finish with correct state mocha grep pass with [only] #1'] = [ + [ + "mocha", + "start", + { + "start": "match.date" + } + ], + [ + "mocha", + "suite", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "suite", + { + "id": "r4", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"before all\" hook", + "hookName": "before all", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"before all\" hook", + "hookName": "before all", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r5", + "title": "test 2", + "body": "[body]", + "type": "test", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "pass", + { + "id": "r5", + "title": "test 2", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "test end", + { + "id": "r5", + "title": "test 2", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"after all\" hook", + "hookName": "after all", + "hookId": "h4", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"after all\" hook", + "hookName": "after all", + "hookId": "h4", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r5", + "title": "test 2", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r4", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "end", + { + "end": "match.date" + } + ] +] + +exports['serialize state - hooks'] = { + "currentId": "r6", + "tests": { + "r3": { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "stub", + "type": "test", + "duration": 1, + "wallClockStartedAt": "1970-01-01T00:00:00.000Z", + "wallClockDuration": 1, + "timings": { + "lifecycle": 1, + "before all": [ + { + "hookId": "h1", + "fnDuration": 1, + "afterFnDuration": 1 + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": 1, + "afterFnDuration": 1 + } + ], + "test": { + "fnDuration": 1, + "afterFnDuration": 1 + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": 1, + "afterFnDuration": 1 + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": 1, + "afterFnDuration": 1 + } + ] + }, + "file": null + }, + "r5": { + "id": "r5", + "order": 2, + "title": "test 1", + "state": "passed", + "body": "stub", + "type": "test", + "duration": 1, + "wallClockStartedAt": "1970-01-01T00:00:00.000Z", + "wallClockDuration": 1, + "timings": { + "lifecycle": 1, + "test": { + "fnDuration": 1, + "afterFnDuration": 1 + } + }, + "file": null + } + }, + "startTime": "1970-01-01T00:00:00.000Z", + "emissions": { + "started": { + "r1": true, + "r2": true, + "r3": true, + "r4": true, + "r5": true, + "r6": true + }, + "ended": { + "r3": true, + "r2": true, + "r5": true + } + }, + "passed": 2, + "failed": 0, + "pending": 0, + "numLogs": 0 +} + +exports['src/cypress/runner other specs screenshots screenshot after failed test #1'] = [ + [ + "take:screenshot", + { + "titles": [ + "suite 1", + "test 1" + ], + "testId": "r3", + "simple": true, + "testFailure": true, + "capture": "runner", + "clip": { + "x": 0, + "y": 0, + "width": 1000, + "height": 660 + }, + "viewport": { + "width": 1000, + "height": 660 + }, + "scaled": true, + "blackout": [], + "startTime": "1970-01-01T00:00:00.000Z" + } + ] +] + +exports['src/cypress/runner mocha events simple single test #1'] = [ + [ + "mocha", + "start", + { + "start": "match.date" + } + ], + [ + "mocha", + "suite", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "suite", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r3", + "order": 1, + "title": "test 1", + "body": "[body]", + "type": "test", + "file": null + } + ], + [ + "mocha", + "pass", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + }, + "file": null + } + ], + [ + "mocha", + "test end", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "end", + { + "end": "match.date" + } + ] +] + +exports['src/cypress/runner mocha events simple three tests #1'] = [ + [ + "mocha", + "start", + { + "start": "match.date" + } + ], + [ + "mocha", + "suite", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "suite", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r3", + "title": "\"before all\" hook", + "hookName": "before all", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r3", + "title": "\"before all\" hook", + "hookName": "before all", + "hookId": "h1", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r3", + "order": 1, + "title": "test 1", + "body": "[body]", + "type": "test", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r3", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r3", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "pass", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "test end", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r3", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r3", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r3", + "order": 1, + "title": "test 1", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "before all": [ + { + "hookId": "h1", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r4", + "order": 2, + "title": "test 2", + "body": "[body]", + "type": "test", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r4", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r4", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "pass", + { + "id": "r4", + "order": 2, + "title": "test 2", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "test end", + { + "id": "r4", + "order": 2, + "title": "test 2", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r4", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r4", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r4", + "order": 2, + "title": "test 2", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "test", + { + "id": "r5", + "order": 3, + "title": "test 3", + "body": "[body]", + "type": "test", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"before each\" hook", + "hookName": "before each", + "hookId": "h2", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "pass", + { + "id": "r5", + "order": 3, + "title": "test 3", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "test end", + { + "id": "r5", + "order": 3, + "title": "test 3", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "timings": { + "lifecycle": "match.number", + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"after each\" hook", + "hookName": "after each", + "hookId": "h3", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "hook", + { + "id": "r5", + "title": "\"after all\" hook", + "hookName": "after all", + "hookId": "h4", + "body": "[body]", + "type": "hook", + "file": null + } + ], + [ + "mocha", + "hook end", + { + "id": "r5", + "title": "\"after all\" hook", + "hookName": "after all", + "hookId": "h4", + "body": "[body]", + "type": "hook", + "duration": "match.number", + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r2", + "title": "suite 1", + "root": false, + "type": "suite", + "file": null + } + ], + [ + "mocha", + "test:after:run", + { + "id": "r5", + "order": 3, + "title": "test 3", + "state": "passed", + "body": "[body]", + "type": "test", + "duration": "match.number", + "wallClockStartedAt": "match.date", + "wallClockDuration": "match.number", + "timings": { + "lifecycle": "match.number", + "before each": [ + { + "hookId": "h2", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "test": { + "fnDuration": "match.number", + "afterFnDuration": "match.number" + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": "match.number", + "afterFnDuration": "match.number" + } + ] + }, + "file": null + } + ], + [ + "mocha", + "suite end", + { + "id": "r1", + "title": "", + "root": true, + "type": "suite", + "file": "relative/path/to/spec.js" + } + ], + [ + "mocha", + "end", + { + "end": "match.date" + } + ] +] diff --git a/packages/runner/cypress.json b/packages/runner/cypress.json new file mode 100644 index 000000000000..0ddefb02cf0f --- /dev/null +++ b/packages/runner/cypress.json @@ -0,0 +1,4 @@ +{ + "projectId": "ypt4pf", + "baseUrl": "http://localhost:3500" +} diff --git a/packages/runner/cypress/.eslintrc.json b/packages/runner/cypress/.eslintrc.json new file mode 100644 index 000000000000..16184dd4a3e4 --- /dev/null +++ b/packages/runner/cypress/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "plugin:@cypress/dev/tests" + ], + "plugins": [ + "cypress" + ], + "env": { + "cypress/globals": true + } +} diff --git a/packages/runner/cypress/fixtures/empty_spec.js b/packages/runner/cypress/fixtures/empty_spec.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/runner/cypress/integration/runner.mochaEvents.spec.js b/packages/runner/cypress/integration/runner.mochaEvents.spec.js new file mode 100644 index 000000000000..a55892fba141 --- /dev/null +++ b/packages/runner/cypress/integration/runner.mochaEvents.spec.js @@ -0,0 +1,302 @@ +const { _ } = Cypress +const sinon = require('sinon') +const helpers = require('../support/helpers') + +const { cleanseRunStateMap, shouldHaveTestResults, getRunState } = helpers +const { runIsolatedCypress, snapshotMochaEvents, onInitialized, getAutCypress } = helpers.createCypress() + +const simpleSingleTest = { + suites: { 'suite 1': { tests: [{ name: 'test 1' }] } }, +} + +const threeTestsWithHooks = { + suites: { 'suite 1': { hooks: ['before', 'beforeEach', 'afterEach', 'after'], tests: ['test 1', 'test 2', 'test 3'] } }, +} + +describe('src/cypress/runner', () => { + describe('tests finish with correct state', () => { + describe('hook failures', () => { + it('fail in [before]', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: [ + { + type: 'before', + fail: true, + }, + ], + tests: [{ name: 'test 1' }], + }, + }, + }) + .then(shouldHaveTestResults(0, 1)) + .then(() => { + cy.get('.runnable-err:visible').invoke('text').should('contain', 'Because this error occurred during a before all hook') + }) + .then(() => { + snapshotMochaEvents() + }) + }) + + it('fail in [beforeEach]', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: [ + { + type: 'beforeEach', + fail: true, + }, + ], + tests: [{ name: 'test 1' }], + }, + }, + }) + .then(shouldHaveTestResults(0, 1)) + .then(() => { + snapshotMochaEvents() + }) + }) + + it('fail in [afterEach]', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: [ + { + type: 'afterEach', + fail: true, + }, + ], + tests: [{ name: 'test 1' }], + }, + }, + }) + .then(shouldHaveTestResults(0, 1)) + .then(() => { + snapshotMochaEvents() + }) + }) + + it('fail in [after]', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: [ + { + type: 'after', + fail: true, + }, + ], + tests: ['test 1', 'test 2'], + }, + }, + }) + .then(shouldHaveTestResults(1, 1)) + .then(() => { + expect('foo').contain('f') + cy.get('.runnable-err:visible').invoke('text').should('contain', 'Because this error occurred during a after all hook') + }) + .then(() => { + snapshotMochaEvents() + }) + }) + }) + + describe('mocha grep', () => { + it('fail with [only]', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: ['before', 'beforeEach', 'afterEach', 'after'], + tests: [ + { name: 'test 1', fail: true }, + { name: 'test 2', fail: true, only: true }, + { name: 'test 3', fail: true }, + ], + }, + }, + }) + .then(shouldHaveTestResults(0, 1)) + .then(() => { + snapshotMochaEvents() + }) + }) + + it('pass with [only]', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: ['before', 'beforeEach', 'afterEach', 'after'], + tests: [ + { name: 'test 1' }, + { name: 'test 2', only: true }, + { name: 'test 3' }, + ], + }, + }, + }) + .then(shouldHaveTestResults(1, 0)) + .then(() => { + snapshotMochaEvents() + }) + }) + }) + }) + + describe('save/reload state on top navigation', () => { + describe('serialize / load from state', () => { + const serializeState = () => { + return getRunState(getAutCypress()) + } + + const loadStateFromSnapshot = (cypressConfig, name) => { + cy.task('getSnapshot', { + file: Cypress.spec.name, + exactSpecName: name, + }) + .then((state) => { + cypressConfig[1].state = state + }) + } + + describe('hooks', () => { + let realState + const stub1 = sinon.stub() + const stub2 = sinon.stub() + const stub3 = sinon.stub().callsFake(() => realState = serializeState()) + let cypressConfig = [ + { + suites: { + 'suite 1': { + hooks: [ + 'before', + 'beforeEach', + 'afterEach', + 'after', + ], + tests: [{ name: 'test 1', fn: stub1 }], + }, + 'suite 2': { + tests: [ + { name: 'test 1', fn: stub2 }, + { name: 'test 2', fn: stub3 }, + 'test 3', + ], + }, + }, + }, {}, + ] + + // TODO: make this one test with multiple visits + it('serialize state', () => { + runIsolatedCypress(...cypressConfig) + .then(shouldHaveTestResults(4, 0)) + .then(() => { + expect(realState).to.matchSnapshot(cleanseRunStateMap, 'serialize state - hooks') + }) + }) + + it('load state', () => { + loadStateFromSnapshot(cypressConfig, 'serialize state - hooks') + + runIsolatedCypress(...cypressConfig) + .then(shouldHaveTestResults(4, 0)) + .then(() => { + expect(stub1).to.calledOnce + expect(stub2).to.calledOnce + expect(stub3).to.calledTwice + }) + }) + }) + }) + }) + + describe('other specs', () => { + it('mocha suite:end fire before test:pass event', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + suites: { + 'suite 1-1': { + tests: ['test 1', 'test 2'], + }, + }, + }, + }, + }).then(({ mochaStubs }) => { + const getOrderFired = (eventProps) => { + const event = _.find(mochaStubs.args, eventProps) + + expect(event).ok + + return _.indexOf(mochaStubs.args, event) + } + + expect(getOrderFired({ 1: 'pass', 2: { title: 'test 2' } })) + .to.be.lt(getOrderFired({ 1: 'suite end', 2: { title: 'suite 1-1' } })) + }) + }) + + describe('screenshots', () => { + let onAfterScreenshotListener + + beforeEach(() => { + onInitialized((autCypress) => { + autCypress.Screenshot.onAfterScreenshot = cy.stub() + onAfterScreenshotListener = cy.stub() + autCypress.on('after:screenshot', onAfterScreenshotListener) + }) + }) + + it('screenshot after failed test', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + tests: [ + { + name: 'test 1', + fn: () => { + assert(false, 'some error') + }, + eval: true, + }, + ], + }, + }, + }) + .then(({ autCypress }) => { + // sent to server + expect(autCypress.automation.withArgs('take:screenshot').args).to.matchSnapshot(cleanseRunStateMap) + + //// on('after:screenshot') + // TODO: for some reason snapshot is not properly saved + // expect(onAfterScreenshotListener.args).to.matchSnapshot(cleanseRunStateMap) + + //// Screenshot.onAfterScreenshot + // TODO: for some reason snapshot is not properly saved + // expect(autCypress.Screenshot.onAfterScreenshot.args).to.matchSnapshot( + // { '^.0.0': stringifyShort, 'test': stringifyShort, takenAt: match.string }, + // ) + }) + }) + }) + }) + + describe('mocha events', () => { + it('simple single test', () => { + runIsolatedCypress(simpleSingleTest) + .then(() => { + snapshotMochaEvents() + }) + }) + + it('simple three tests', () => { + runIsolatedCypress(threeTestsWithHooks) + .then(() => { + snapshotMochaEvents() + }) + }) + }) +}) diff --git a/packages/runner/cypress/integration/runner.ui.spec.js b/packages/runner/cypress/integration/runner.ui.spec.js new file mode 100644 index 000000000000..ba63c81f9f1f --- /dev/null +++ b/packages/runner/cypress/integration/runner.ui.spec.js @@ -0,0 +1,267 @@ +const helpers = require('../support/helpers') + +const { shouldHaveTestResults, createCypress } = helpers +const { runIsolatedCypress } = createCypress() + +const fx_simpleSingleTest = { + suites: { 'suite 1': { tests: [{ name: 'test 1' }] } }, +} + +const fx_failPass = { + suites: { + 'suite 1': { + tests: [ + { + name: 'test 1', + fail: true, + }, + { name: 'test 2' }, + ], + }, + }, +} + +const fx_passFailPassFail = { + suites: { + 'suite 1': { + tests: [ + 'test 1', + { + name: 'test 2', + fail: true, + }, + ], + }, + 'suite 2': { + tests: [ + 'test 1', + { + name: 'test 2', + fail: true, + }, + ], + }, + }, +} + +describe('src/cypress/runner', () => { + describe('tests finish with correct state', () => { + it('simple 1 test', () => { + runIsolatedCypress(fx_simpleSingleTest) + .then(shouldHaveTestResults(1, 0)) + }) + + it('simple 1 global test', () => { + runIsolatedCypress(() => { + it('foo', () => { + expect(true).is.true + }) + }) + .then(shouldHaveTestResults(1, 0)) + }) + + it('simple 3 tests', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { tests: ['test 1', 'test 2', 'test 3'] }, + }, + }) + .then(shouldHaveTestResults(3, 0)) + }) + + it('simple fail', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + tests: [ + { + name: 'test 1', + fail: true, + }, + ], + }, + }, + }) + .then(shouldHaveTestResults(0, 1)) + .then(() => { + // render exactly one error + cy.get('.runnable-err:contains(AssertionError)').should('have.length', 1) + }) + }) + + it('pass fail pass fail', () => { + runIsolatedCypress(fx_passFailPassFail) + .then(shouldHaveTestResults(2, 2)) + }) + + it('fail pass', () => { + runIsolatedCypress(fx_failPass) + .then(shouldHaveTestResults(1, 1)) + }) + + it('no tests', () => { + runIsolatedCypress({}) + .then(shouldHaveTestResults(0, 0)) + + cy.contains('No tests found in your file').should('be.visible') + cy.get('.error-message p').invoke('text').should('eq', 'We could not detect any tests in the above file. Write some tests and re-run.') + }) + + it('ends test before nested suite', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { tests: ['test 1', 'test 2'], + suites: { + 'suite 1-1': { + tests: ['test 1'], + }, + } }, + }, + }, {}) + .then(shouldHaveTestResults(3, 0)) + }) + + it('simple fail, catch cy.on(fail)', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + tests: [ + { + name: 'test 1', + fn: () => { + cy.on('fail', () => { + return false + }) + + expect(false).ok + throw new Error('error in test') + }, + eval: true, + }, + ], + }, + }, + }) + .then(shouldHaveTestResults(1, 0)) + }) + + describe('hook failures', () => { + describe('test failures w/ hooks', () => { + it('fail with [before]', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: ['before'], + tests: [ + { + name: 'test 1', + fail: true, + }, + { name: 'test 2' }, + ], + }, + }, + }) + .then(shouldHaveTestResults(1, 1)) + }) + + it('fail with [after]', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: [{ type: 'after' }], + tests: [{ name: 'test 1', fail: true }, 'test 2'], + }, + }, + }) + .then(shouldHaveTestResults(1, 1)) + }) + + it('fail with all hooks', () => { + runIsolatedCypress({ + suites: { + 'suite 1': { + hooks: ['before', 'beforeEach', 'afterEach', 'after'], + tests: [{ name: 'test 1', fail: true }], + }, + }, + }) + .then(shouldHaveTestResults(0, 1)) + }) + }) + }) + }) + + describe('other specs', () => { + it('simple failing hook spec', () => { + const mochaTests = { + suites: { + 'simple failing hook spec': { + suites: { + 'beforeEach hooks': { + hooks: [{ type: 'beforeEach', fail: true }], + tests: ['never gets here'], + }, + 'pending': { + tests: [{ name: 'is pending', pending: true }], + }, + 'afterEach hooks': { + hooks: [{ type: 'afterEach', fail: true }], + tests: ['fails this', 'does not run this'], + }, + 'after hooks': { + hooks: [{ type: 'after', fail: true }] + , tests: ['runs this', 'fails on this'], + }, + }, + }, + + }, + } + + runIsolatedCypress(mochaTests) + .then(shouldHaveTestResults(1, 3)) + .then(() => { + cy.contains('.test', 'never gets here').should('have.class', 'runnable-failed') + cy.contains('.command', 'beforeEach').should('have.class', 'command-state-failed') + cy.contains('.runnable-err', 'beforeEach').scrollIntoView().should('be.visible') + + cy.contains('.test', 'is pending').should('have.class', 'runnable-pending') + + cy.contains('.test', 'fails this').should('have.class', 'runnable-failed') + cy.contains('.command', 'afterEach').should('have.class', 'command-state-failed') + cy.contains('.runnable-err', 'afterEach').should('be.visible') + + cy.contains('.test', 'does not run this').should('have.class', 'runnable-processing') + + cy.contains('.test', 'runs this').should('have.class', 'runnable-passed') + + cy.contains('.test', 'fails on this').should('have.class', 'runnable-failed') + cy.contains('.command', 'after').should('have.class', 'command-state-failed') + cy.contains('.runnable-err', 'after').should('be.visible') + }) + }) + + it('async timeout spec', () => { + runIsolatedCypress({ + suites: { + 'async': { + tests: [ + { name: 'bar fails', + // eslint-disable-next-line + fn (done) { + this.timeout(100) + cy.on('fail', () => {}) + // eslint-disable-next-line + foo.bar() + }, + eval: true, + }, + ], + }, + }, + }) + .then(shouldHaveTestResults(0, 1)) + }) + }) +}) diff --git a/packages/runner/cypress/plugins/index.js b/packages/runner/cypress/plugins/index.js new file mode 100644 index 000000000000..62be55014b82 --- /dev/null +++ b/packages/runner/cypress/plugins/index.js @@ -0,0 +1,15 @@ +// static file server that serves fixtures needed for testing +require('@packages/driver/cypress/plugins/server') +const { getSnapshot, saveSnapshot } = require('./snapshot/snapshotPlugin') + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + on('task', { + getSnapshot, + saveSnapshot, + }) +} diff --git a/packages/runner/cypress/plugins/snapshot/index.d.ts b/packages/runner/cypress/plugins/snapshot/index.d.ts new file mode 100644 index 000000000000..56dfa5abd634 --- /dev/null +++ b/packages/runner/cypress/plugins/snapshot/index.d.ts @@ -0,0 +1,13 @@ +declare namespace Chai { + interface Assertion { + matchSnapshot: { + (name?: string) + (replacers: object) + (replacers: object, name?: string) + } + matchDeep: { + (replacers: object, expected: object) + (expected: object) + } + } +} diff --git a/packages/runner/cypress/plugins/snapshot/snapshotCommand.js b/packages/runner/cypress/plugins/snapshot/snapshotCommand.js new file mode 100644 index 000000000000..4e021a871bb7 --- /dev/null +++ b/packages/runner/cypress/plugins/snapshot/snapshotCommand.js @@ -0,0 +1,443 @@ +const _ = require('lodash') +// if we're in Cypress, we'll need to swap this with Cypress.sinon later +let sinon = require('sinon') +const Debug = require('debug') +const chalk = require('chalk') +const stripAnsi = require('strip-ansi') +const { stripIndent } = require('common-tags') +const { printVar, stringifyShort, isObject, addPluginButton, fmt, typeColors } = require('./snapshotUtils') + +const debug = Debug('plugin:snapshot') + +/** + * prints nice assertion error in command log with modified error message + */ +function throwErr (e, message, exp, ctx) { + try { + ctx.assert(false, message, 'sdf', exp, e.act, true) + } catch (err) { + err.message += `\n\n**- expected + actual:**\n${e.message}` + throw err + } +} + +function getMatchDeepMessage ({ act, exp }) { + return `Expected **${chai.util.objDisplay(act)}** to deep match: **${chai.util.objDisplay(exp)}**` +} + +function saveSnapshot (ctx, exactSpecName, file, exp, act) { + const message = !exp ? 'new snapshot saved' : 'snapshot updated' + + ctx.assert(true, `📸 ${message}: **${exactSpecName}**`, '', exp, act) + + return cy.task('saveSnapshot', { + file, + what: act, + exactSpecName, + }, { log: false }) +} + +const registerInCypress = () => { + // need to use correct sinon version for matcher.isMatcher to work + sinon = Cypress.sinon + const $ = Cypress.$ + + let snapshotIndex = {} + + chai = window.chai + chai.Assertion.addMethod('matchDeep', matchDeepCypress) + chai.Assertion.addMethod('matchSnapshot', matchSnapshotCypress) + + after(() => { + snapshotIndex = {} + }) + + before(() => { + addPluginButton($, 'toggle-snapshot-update', '', { + render () { + const btnIcon = $(this).children().first() + + return btnIcon.text(top.SNAPSHOT_UPDATE ? 'snapshot\nupdate\non' : 'snapshot\nupdate\noff') + .css({ 'font-size': '10px', 'line-height': '0.9' }) + .html(btnIcon.html().replace(/\n/g, '
')) + }, + click () { + top.SNAPSHOT_UPDATE = !top.SNAPSHOT_UPDATE + }, + }) + }) + + function matchDeepCypress (...args) { + const exp = args[1] || args[0] + const ctx = this + + try { + const res = matchDeep.apply(this, [args[0], args[1], { Cypress, expectedOnly: true }]) + + const message = getMatchDeepMessage(res.act, exp) + + ctx.assert(true, message) + Cypress.log({ + name: 'assert', + message, + state: 'passed', + consoleProps: () => { + return { + Actual: res.act, + } + }, + }) + } catch (e) { + throwErr( + e, + getMatchDeepMessage(e.act, args[1] || args[0]), + exp, + ctx, + ) + } + } + + function matchSnapshotCypress (m, snapshotName) { + const ctx = this + const file = Cypress.spec.name + const testName = Cypress.mocha.getRunner().test.fullTitle() + + return cy.then(() => { + snapshotIndex[testName] = (snapshotIndex[testName] || 1) + const exactSpecName = snapshotName || `${testName} #${snapshotIndex[testName]}` + + return cy.task('getSnapshot', { + file, + exactSpecName, + }, { log: false }) + .then(function (exp) { + try { + snapshotIndex[testName] = snapshotIndex[testName] + 1 + const res = matchDeep.call(ctx, m, exp, { message: 'to match snapshot', Cypress, isSnapshot: true, sinon }) + + ctx.assert(true, `snapshot matched: **${exactSpecName}**`, res.act) + } catch (e) { + if (!e.known) { + throw e + } + + // save snapshot if env var or no previously saved snapshot (and no failed matcher assertions) + if ((top.SNAPSHOT_UPDATE || !exp) && !e.failedMatcher && e.act) { + return saveSnapshot(ctx, exactSpecName, file, exp, e.act) + } + + throwErr(e, `**snapshot failed to match**: ${exactSpecName}`, exp, ctx) + } + }) + }) + } +} + +const matcherStringToObj = (mes) => { + const res = mes.replace(/typeOf\("(\w+)"\)/, '$1') + + const ret = {} + + ret.toString = () => { + return `${res}` + } + + ret.toJSON = () => { + return `match.${res}` + } + + return ret +} + +const matchDeep = function (matchers, exp, optsArg) { + let m = matchers + + if (exp === undefined) { + exp = m + m = {} + } + + const opts = _.defaults(optsArg, { + message: 'to match', + Cypress: false, + diff: true, + expectedOnly: false, + sinon: null, + }) + + if (!opts.sinon) { + opts.sinon = sinon + } + + const match = opts.sinon.match + const isAnsi = !opts.Cypress + + const act = this._obj + + m = _.map(m, (val, key) => { + return [key.split('.'), val] + }) + + const diffStr = withMatchers(m, match, opts.expectedOnly)(exp, act) + + if (diffStr.changed) { + let e = _.extend(new Error(), { known: true, act: diffStr.act, failedMatcher: diffStr.opts.failedMatcher }) + + e.message = isAnsi ? `\n${diffStr.text}` : stripAnsi(diffStr.text) + + if (_.isString(act)) { + e.message = `\n${stripIndent` + SnapshotError: Failed to match snapshot + Expected:\n---\n${printVar(exp)}\n--- + Actual:\n---\n${printVar(diffStr.act)}\n--- + `}` + } + + throw e + } + + return diffStr +} + +const parseMatcherFromString = (matcher) => { + const regex = /match\.(.*)/ + + if (_.isString(matcher)) { + const parsed = regex.exec(matcher) + + if (parsed) { + return parsed[1] + } + } +} + +function parseMatcherFromObj (obj, match) { + if (match.isMatcher(obj)) { + return obj + } + + const objStr = (_.isString(obj) && obj) || (obj && obj.toJSON && obj.toJSON()) + + if (objStr) { + const parsed = parseMatcherFromString(objStr) + + if (parsed) { + return match[parsed] + } + } + + return obj +} + +function setReplacement (act, val, path) { + if (_.isFunction(val)) { + return val(act, path) + } + + return val +} + +const withMatchers = (matchers, match, expectedOnly = false) => { + const getReplacementFor = (path = [], m) => { + for (let rep of m) { + const wildCards = _.keys(_.pickBy(rep[0], (value) => { + return value === '*' + })) + + const _path = _.map(path, (value, key) => { + if (_.includes(wildCards, `${key}`)) { + return '*' + } + + return value + }) + + const matched = _path.join('.').endsWith(rep[0].join('.')) + + if (matched) { + return rep[1] + } + } + + return NO_REPLACEMENT + } + + const testValue = (matcher, value) => { + if (matcher.test(value)) { + return true + } + + return false + } + + const NO_REPLACEMENT = {} + + /** + * diffing function that produces human-readable diff output. + * unfortunately it is also unreadable code in itself. + */ + const diff = (exp, act, path = ['^'], optsArg) => { + const opts = _.defaults({}, optsArg, { + expectedOnly, + }) + + if (path.length > 15) { + throw new Error(`exceeded max depth on ${path.slice(0, 4)} ... ${path.slice(-4)}`) + } + + let text = '' + let changed = false + let itemDiff + let keys + let subOutput = '' + + let replacement = getReplacementFor(path, matchers) + + if (replacement !== NO_REPLACEMENT) { + if (match.isMatcher(replacement)) { + if (testValue(replacement, act)) { + act = matcherStringToObj(replacement.message).toJSON() + } else { + opts.failedMatcher = true + if (!_.isFunction(act)) { + act = _.clone(act) + } + + exp = replacement + } + } else { + act = setReplacement(act, replacement, path) + } + } else { + if (!_.isFunction(act) && !_.isFunction(_.get(act, 'toJSON'))) { + act = _.clone(act) + } + + exp = parseMatcherFromObj(exp, match) + if (match.isMatcher(exp)) { + if (testValue(exp, act)) { + act = matcherStringToObj(exp.message).toJSON() + + return { + text: '', + changed: false, + act, + } + } + + return { + text: fmt.wrap('failed', `${chalk.green(printVar(act))} ⛔ ${matcherStringToObj(exp.message).toJSON()}`), + changed: true, + act, + } + } + } + + if (_.isFunction(_.get(act, 'toJSON'))) { + act = act.toJSON() + } + + if (isObject(exp) && isObject(act) && !match.isMatcher(exp)) { + keys = _.keysIn(exp) + let actObj = _.extend({}, act) + let key + + if (_.isArray(exp)) { + keys.sort((a, b) => +a - +b) + } else { + keys.sort() + } + + for (let i = 0; i < keys.length; i++) { + key = keys[i] + const isUndef = exp[key] === undefined + + if (_.hasIn(act, key) || isUndef) { + itemDiff = diff(exp[key], act[key], path.concat([key])) + _.defaults(opts, itemDiff.opts) + act[key] = itemDiff.act + if (itemDiff.changed) { + subOutput += fmt.keyChanged(key, itemDiff.text) + changed = true + } + } else { + subOutput += fmt.keyRemoved(key, exp[key]) + changed = true + } + + delete actObj[key] + } + + let addedKeys = _.keysIn(actObj) + + if (!opts.expectedOnly) { + for (let i = 0; i < addedKeys.length; i++) { + const key = addedKeys[i] + const val = act[key] + + const addDiff = diff(val, val, path.concat([key])) + + _.defaults(opts, addDiff.opts) + + act[key] = addDiff.act + if (act[key] === undefined) continue + + if (opts.failedMatcher) { + subOutput += addDiff.text + } else { + subOutput += fmt.keyAdded(key, act[key]) + } + + changed = true + } + } + + if (changed) { + text = fmt.wrapObjectLike(exp, act, subOutput) + } + } else if (match.isMatcher(exp)) { + debug('is matcher') + if (!testValue(exp, act)) { + text = fmt.wrap('failed', `${chalk.green(printVar(act))} ⛔ ${matcherStringToObj(exp.message).toJSON()}`) + changed = true + } + } else if (isObject(act)) { + debug('only act is obj') + + const addDiff = diff({}, act, path, { expectedOnly: false }) + + _.defaults(opts, addDiff.opts) + + return _.extend({}, + addDiff, { + changed: true, + text: fmt.wrap('removed', `${printVar(exp)}\n${fmt.wrap('added', addDiff.text)}`), + }) + } else { + debug('neither is obj') + exp = printVar(exp) + act = printVar(act) + + if (exp !== act) { + text = fmt.wrap('modified', `${exp} ${typeColors['normal']('⮕')} ${act}`) + changed = true + } + } + + return { + changed, + text, + act, + opts, + } + } + + return diff +} + +module.exports = { + registerInCypress, + matchDeep, + stringifyShort, + parseMatcherFromString, +} diff --git a/packages/runner/cypress/plugins/snapshot/snapshotPlugin.js b/packages/runner/cypress/plugins/snapshot/snapshotPlugin.js new file mode 100644 index 000000000000..c74703658dd1 --- /dev/null +++ b/packages/runner/cypress/plugins/snapshot/snapshotPlugin.js @@ -0,0 +1,57 @@ +const snapshotCore = require('snap-shot-core') +const _ = require('lodash') + +// TODO: prune snapshots + +/** + * + * @param {{what: any, file: string, exactSpecName: string, store?: Function compare?: Function}} + */ +const getSnapshot = (opts) => { + let result = null + + // HACK: in order to read the snapshot from disk using snap-shot-core + // we have to 'fake save' a snapshot, and intercept the stored value via custom compare fn + opts = _.defaults(opts, { + what: '[placeholder]', + }) + + opts = _.assign(opts, { + compare: ({ expected }) => { + result = expected + throw new Error('bail') + }, + ext: '.js', + opts: { + update: false, + ci: true, + }, + }) + + try { + snapshotCore.core({ ...opts }) + } catch (e) { + null + } + + return result +} + +const saveSnapshot = (opts) => { + opts = _.defaults(opts, { + }) + + return snapshotCore.core(_.extend({}, + opts, + { + ext: '.js', + opts: { + update: true, + }, + })) +} + +module.exports = { + saveSnapshot, + getSnapshot, +} diff --git a/packages/runner/cypress/plugins/snapshot/snapshotUtils.js b/packages/runner/cypress/plugins/snapshot/snapshotUtils.js new file mode 100644 index 000000000000..17bfc8de5657 --- /dev/null +++ b/packages/runner/cypress/plugins/snapshot/snapshotUtils.js @@ -0,0 +1,146 @@ +const _ = require('lodash') +const chalk = require('chalk') + +function printVar (variable) { + switch (getType(variable)) { + case 'Null': + return variable + case 'Undefined': + return variable + case 'Boolean': + return variable + case 'Number': + return variable + case 'Function': + return `[Function${variable.name ? ` ${variable.name}` : ''}]` + + case 'Array': + case 'Object': + + if (variable.toJSON) { + return variable.toJSON() + } + + return stringifyShort(variable) + + case 'String': + return `${variable}` + + default: return `${variable}` + } +} + +function getType (obj) { + return Object.prototype.toString.call(obj).split('[object ').join('').slice(0, -1) +} + +const stringifyShort = (obj) => { + const constructorName = _.get(obj, 'constructor.name') + + if (constructorName && !_.includes(['Object', 'Array'], constructorName)) { + return `{${constructorName}}` + } + + if (_.isArray(obj)) { + return `[Array ${obj.length}]` + } + + if (_.isObject(obj)) { + return `{Object ${Object.keys(obj).length}}` + } + + return obj +} + +function isObject (obj) { + return typeof obj === 'object' && obj && getType(obj) !== 'RegExp' +} + +function addPluginButton ($, name, faClass, { render, click }) { + $(`#${name}`, window.top.document).remove() + + const btn = $(``, window.top.document) + const container = $( + '.toggle-auto-scrolling.auto-scrolling-enabled', + window.top.document, + ).closest('.controls') + + container.prepend(btn) + + btn.on('click', () => { + click.apply(btn[0]) + render.apply(btn[0]) + }) + + render.apply(btn[0]) + + return btn +} + +const typeColors = { + modified: chalk.yellow, + added: chalk.green, + removed: chalk.red, + normal: chalk.gray, + failed: chalk.redBright, +} + +const fmtOpts = { + indent: ' ', + newLineChar: '\n', +} + +const fmt = { + wrap: function wrap (type, text) { + if (this.Cypress) { + text = `**${text}**` + } + + return typeColors[type](text) + }, + + wrapObjectLike (exp, act, subOutput) { + let renderBracket = false + + if (_.isArray(act) && _.isArray(exp)) { + renderBracket = true + } + + const _O = renderBracket ? '[' : '{' + const _C = renderBracket ? ']' : '}' + + return fmt.wrap('normal', `${_O}${fmtOpts.newLineChar}${subOutput}${_C}`) + }, + + indentSubItem (text) { + return text.split(fmtOpts.newLineChar).map(function (line, index) { + if (index === 0) { + return line + } + + return fmtOpts.indent + line + }).join(fmtOpts.newLineChar) + }, + + keyChanged (key, text) { + return `${fmtOpts.indent + key}: ${fmt.indentSubItem(text)}${fmtOpts.newLineChar}` + }, + + keyRemoved (key, variable) { + return fmt.wrap('removed', `- ${key}: ${printVar(variable)}`) + fmtOpts.newLineChar + }, + + keyAdded (key, variable) { + return fmt.wrap('added', `+ ${key}: ${printVar(variable)}`) + fmtOpts.newLineChar + }, +} + +module.exports = { + printVar, + stringifyShort, + isObject, + addPluginButton, + fmt, + fmtOpts, + typeColors, +} diff --git a/packages/runner/cypress/support/helpers.js b/packages/runner/cypress/support/helpers.js new file mode 100644 index 000000000000..de6c2cc15285 --- /dev/null +++ b/packages/runner/cypress/support/helpers.js @@ -0,0 +1,479 @@ +/* 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, + err: 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 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 () { + /** + * @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 + }) + + let onInitializedListeners = [] + + const onInitialized = function (fn) { + onInitializedListeners.push(fn) + } + + /** + * Spawns an isolated Cypress runner as the AUT, with provided spec/fixture and optional state/config + * @param {()=>void | {[key:string]: any}} mochaTests + * @param {{state?: any, config?: any}} opts + */ + const runIsolatedCypress = (mochaTests, opts = {}) => { + _.defaultsDeep(opts, { + state: {}, + config: { video: false }, + }) + + 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) + mochaStubs = cy.stub().snapshot(enableStubSnapshots) + setRunnablesStub = cy.stub().snapshot(enableStubSnapshots) + + 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').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', + message: `${args[1]}`, + state: 'failed', + consoleProps: () => { + return { + Error: args[1], + } + }, + }) + }) + + cy.spy(cy.state('window').console, 'log').as('console_log') + cy.spy(cy.state('window').console, 'error').as('console_error') + + onInitializedListeners.forEach((fn) => fn(autCypress)) + onInitializedListeners = [] + + autCypress.run((failed) => { + resolve({ failed, mochaStubs, autCypress }) + }) + } + + cy.spy(win.eventManager.reporterBus, 'emit').snapshot(enableStubSnapshots).as('reporterBus') + cy.spy(win.eventManager.localBus, 'emit').snapshot(enableStubSnapshots).as('localBus') + + cy.stub(win.runnerWs, 'emit').snapshot(enableStubSnapshots) + .withArgs('watch:test:file') + .callsFake(() => { + autCypress = win.Cypress + + cy.stub(autCypress, 'onSpecWindow').snapshot(enableStubSnapshots).callsFake((specWindow) => { + autCypress.onSpecWindow.restore() + + autCypress.onSpecWindow(specWindow, [ + { + absolute: 'cypress/fixtures/empty_spec.js', + relative: 'cypress/fixtures/empty_spec.js', + relativeUrl: '/__cypress/tests?p=cypress/fixtures/empty_spec.js', + }, + ]) + + generateMochaTestsForWin(specWindow, mochaTests) + specWindow.before = () => {} + specWindow.beforeEach = () => {} + specWindow.afterEach = () => {} + specWindow.after = () => {} + specWindow.describe = () => {} + }) + + cy.stub(autCypress, 'run').snapshot(enableStubSnapshots).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: true, + isHtml: true, + url: 'http://localhost:3500/fixtures/generic.html', + } }) + + .withArgs('set:runnables') + .callsFake((...args) => { + setRunnablesStub(...args) + _.last(args)() + }) + + // .withArgs('preserve:run:state') + // .callsFake() + + .withArgs('automation:request') + .yieldsAsync({ response: {} }) + + const c = _.extend({}, Cypress.config(), { + isTextTerminal: true, + spec: { + relative: 'relative/path/to/spec.js', + absolute: '/absolute/path/to/spec.js', + }, + }, opts.config) + + c.state = {} + + cy.stub(win.runnerWs, 'on').snapshot(enableStubSnapshots) + + win.Runner.start(win.document.getElementById('app'), window.btoa(JSON.stringify(c))) + }) + }) + } + + return { + runIsolatedCypress, + snapshotMochaEvents, + onInitialized, + getAutCypress, + + } +} + +const createHooks = (win, hooks = []) => { + _.each(hooks, (hook) => { + if (_.isString(hook)) { + hook = { type: hook } + } + + let { type, fail, fn } = 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](() => { + if (_.isNumber(fail) && fail-- <= 0) { + debug(`hook pass after (${numFailures}) failures: ${type}`) + win.assert(true, type) + + return + } + + debug(`hook fail: ${type}`) + + win.assert(false, type) + + throw new Error(`hook failed: ${type}`) + }) + } + + return win[type](() => { + win.assert(true, type) + debug(`hook pass: ${type}`) + }) + }) +} + +const createTests = (win, tests = []) => { + _.each(tests, (test) => { + if (_.isString(test)) { + test = { name: test } + } + + let { name, pending, fail, fn, only } = 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 (_.isNumber(fail) && fail-- === 0) { + debug(`test pass after retry: ${name}`) + win.assert(true, name) + + return + } + + 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 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 cleanseRunStateMap = { + wallClockStartedAt: new Date(0), + wallClockDuration: 1, + fnDuration: 1, + afterFnDuration: 1, + lifecycle: 1, + duration: 1, + startTime: new Date(0), + 'err.stack': '[err stack]', + sourceMappedStack: match.string, + parsedStack: match.array, +} + +const shouldHaveTestResults = (expPassed, expFailed) => { + return ({ failed }) => { + expect(failed, 'resolve with failure count').eq(failed) + expPassed = expPassed || '--' + expFailed = expFailed || '--' + cy.get('header .passed .num').should('have.text', `${expPassed}`) + cy.get('header .failed .num').should('have.text', `${expFailed}`) + } +} + +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) +} + +module.exports = { + generateMochaTestsForWin, + createCypress, + containText, + cleanseRunStateMap, + shouldHaveTestResults, + getRunState, +} diff --git a/packages/runner/cypress/support/index.js b/packages/runner/cypress/support/index.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/runner/package.json b/packages/runner/package.json index 956563e7ac7f..37519d2b79b2 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -7,6 +7,8 @@ "build": "webpack", "build-prod": "cross-env NODE_ENV=production yarn build", "clean-deps": "rm -rf node_modules", + "cypress:open": "node ../../scripts/cypress open", + "cypress:run": "node ../../scripts/cypress run", "postinstall": "echo '@packages/runner needs: yarn build'", "test": "yarn test-unit", "test-debug": "yarn test-unit --inspect-brk=5566", @@ -42,6 +44,7 @@ "react-input-autosize": "2.2.2", "sinon": "7.5.0", "sinon-chai": "3.3.0", + "snap-shot-core": "10.2.1", "webpack": "4.35.3", "webpack-cli": "3.3.2" }, diff --git a/packages/runner/src/lib/event-manager.js b/packages/runner/src/lib/event-manager.js index d8fd1ea9e4db..35ae20006be0 100644 --- a/packages/runner/src/lib/event-manager.js +++ b/packages/runner/src/lib/event-manager.js @@ -16,8 +16,6 @@ const ws = client.connect({ parser: circularParser, }) -window.runnerWs = ws - ws.on('connect', () => { ws.emit('runner:connected') }) @@ -32,6 +30,15 @@ const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ') const localBus = new EventEmitter() const reporterBus = new EventEmitter() +// NOTE: this is exposed for testing, ideally we should only expose this if a test flag is set +window.runnerWs = ws + +// NOTE: this is for testing Cypress-in-Cypress, window.Cypress is undefined here +// unless Cypress has been loaded into the AUT frame +if (window.Cypress) { + window.eventManager = { reporterBus, localBus } +} + /** * @type {Cypress.Cypress} */ diff --git a/yarn.lock b/yarn.lock index f878a3b77e18..d68c2959e4a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,36 +1918,21 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.12.1.tgz#2a98fea9fbb8a606ddc79a4680034e9d5591c550" integrity sha512-ZtjIIFplxncqxvogq148C3hBLQE+W3iJ8E4UvJ09zIJUgzwLcROsWwFDErVSXY2Plzao5J9KUYNHKHMEUYDMKw== -"@hapi/address@2.x.x", "@hapi/address@^2.1.2": +"@hapi/address@^2.1.2": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== -"@hapi/bourne@1.x.x": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a" - integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA== - "@hapi/formula@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-1.2.0.tgz#994649c7fea1a90b91a0a1e6d983523f680e10cd" integrity sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA== -"@hapi/hoek@8.x.x", "@hapi/hoek@^8.2.4", "@hapi/hoek@^8.3.0": +"@hapi/hoek@^8.2.4", "@hapi/hoek@^8.3.0": version "8.5.1" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== -"@hapi/joi@^15.0.3": - version "15.1.1" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7" - integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ== - dependencies: - "@hapi/address" "2.x.x" - "@hapi/bourne" "1.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/topo" "3.x.x" - "@hapi/joi@^16.1.8": version "16.1.8" resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.8.tgz#84c1f126269489871ad4e2decc786e0adef06839" @@ -1964,7 +1949,7 @@ resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-1.0.2.tgz#025b7a36dbbf4d35bf1acd071c26b20ef41e0d13" integrity sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ== -"@hapi/topo@3.x.x", "@hapi/topo@^3.1.3": +"@hapi/topo@^3.1.3": version "3.1.6" resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== @@ -17465,7 +17450,7 @@ mkdirp-promise@^5.0.1: dependencies: mkdirp "*" -mkdirp@*: +mkdirp@*, mkdirp@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -21928,11 +21913,6 @@ rx-lite@*, rx-lite@^4.0.8: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= -rx@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" - integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= - rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.3, rxjs@^6.5.4: version "6.5.5" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" @@ -22716,6 +22696,25 @@ snap-shot-core@10.2.0: quote "0.4.0" ramda "0.26.1" +snap-shot-core@10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/snap-shot-core/-/snap-shot-core-10.2.1.tgz#2926c8692acf984095c274b97068456d1a540db7" + integrity sha512-fApr6/GQi7kfLyperpF70SxG9mp9L3gQgb3czXH97iuix1QrfThS9kATx43r3wIl0Ik5sWmWvor/Xcb3+2cteQ== + dependencies: + arg "4.1.0" + check-more-types "2.24.0" + common-tags "1.8.0" + debug "4.1.1" + escape-quotes "1.0.2" + folktale "2.3.2" + is-ci "2.0.0" + jsesc "2.5.2" + lazy-ass "1.6.0" + mkdirp "1.0.4" + pluralize "8.0.0" + quote "0.4.0" + ramda "0.26.1" + snap-shot-it@7.9.3: version "7.9.3" resolved "https://registry.yarnpkg.com/snap-shot-it/-/snap-shot-it-7.9.3.tgz#7eadbe78e2f1e180998d50620290840d2cb532cf" @@ -25242,17 +25241,6 @@ w3c-xmlserializer@^1.0.1, w3c-xmlserializer@^1.1.2: webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" -wait-on@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.3.0.tgz#9940981d047a72a9544a97b8b5fca45b2170a082" - integrity sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ== - dependencies: - "@hapi/joi" "^15.0.3" - core-js "^2.6.5" - minimist "^1.2.0" - request "^2.88.0" - rx "^4.1.0" - wait-on@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-4.0.0.tgz#4d7e4485ca759968897fd3b0cc50720c0b4ca959"