From dbbad318aa9649d9254b831a845cbc3b1628c48b Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 5 May 2022 18:21:15 -0500 Subject: [PATCH] chore(sessions): add additional tests (#21338) --- .../commands/sessions/sessions.spec.js | 318 +++++++++++++++++ packages/driver/package.json | 2 +- .../driver/src/cy/commands/sessions/index.ts | 6 +- packages/driver/src/cypress.ts | 2 +- packages/driver/src/cypress/error_messages.ts | 7 +- .../cypress/integration/test_errors_spec.ts | 4 +- packages/reporter/src/attempts/attempts.tsx | 12 +- packages/reporter/src/test/test.tsx | 2 +- .../cypress/integration/retries.ui.spec.js | 2 +- .../cypress/integration/sessions.ui.spec.js | 322 ++++++++++++------ system-tests/__snapshots__/session_spec.ts.js | 29 +- system-tests/package.json | 3 +- .../e2e/cypress/integration/session_spec.js | 218 ------------ .../e2e/sessions/multiple_sessions.cy.js | 10 + .../cypress/e2e/sessions/new_session.cy.js | 6 + .../new_session_and_fails_validation.cy.js | 8 + .../new_session_with_validation.cy.js | 10 + .../e2e/sessions/recreates_session.cy.js | 27 ++ ...creates_session_and_fails_validation.cy.js | 27 ++ .../e2e/sessions/restores_saved_session.cy.js | 23 ++ 20 files changed, 682 insertions(+), 356 deletions(-) create mode 100644 packages/driver/cypress/integration/commands/sessions/sessions.spec.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy.js diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js new file mode 100644 index 000000000000..f1b93ac82295 --- /dev/null +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -0,0 +1,318 @@ +const baseUrl = Cypress.config('baseUrl') + +before(() => { + // sessions has logic built in to persists sessions on UI refresh + Cypress.session.clearAllSavedSessions() +}) + +const expectCurrentSessionData = async (obj) => { + return Cypress.session.getCurrentSessionData() + .then((result) => { + cy.log(result) + expect(result.cookies.map((v) => v.name)).members(obj.cookies || []) + expect(result.localStorage).deep.members(obj.localStorage || []) + expect(result.sessionStorage).deep.members(obj.sessionStorage || []) + }) +} + +describe('cy.session', { retries: 0 }, () => { + describe('args', () => { + it('accepts string as id', () => { + cy.session('session-id', () => {}) + cy.session({ name: 'session-id', zkey: 'val' }, () => {}) + }) + + it('accepts array as id', () => { + cy.session('session-id', () => {}) + }) + + it('accepts object as id', () => { + cy.session('session-id', () => {}) + }) + + // redundant? + it('accepts options as third argument', () => { + const setup = cy.stub().as('setupSession') + const validate = cy.stub().as('validateSession') + + cy.session('session-id', setup, { validate }) + cy.then(() => { + expect(setup).to.be.calledOnce + expect(validate).to.be.calledOnce + }) + }) + }) + + describe('errors', () => { + let lastLog = null + let logs = [] + + beforeEach(() => { + cy.on('log:added', (attrs, log) => { + if (attrs.name === 'session') { + lastLog = log + logs.push(log) + } + }) + + return null + }) + + it('throws error when experimentalSessionAndOrigin not enabled', { experimentalSessionAndOrigin: false, experimentalSessionSupport: false }, (done) => { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` requires enabling the `experimentalSessionAndOrigin` flag.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('sessions-not-enabled') + }) + + it('throws error when experimentalSessionSupport is enabled through test config', { experimentalSessionAndOrigin: false, experimentalSessionSupport: true }, (done) => { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('sessions-not-enabled') + }) + + it('throws error when experimentalSessionSupport is enabled through Cypress.config', { experimentalSessionAndOrigin: false }, (done) => { + Cypress.config('experimentalSessionSupport', true) + + cy.on('fail', (err) => { + Cypress.config('experimentalSessionSupport', false) + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + done() + }) + + cy.session('sessions-not-enabled') + }) + + it('throws when sessionId argument was not provided', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid argument. The first argument `id` must be an string or serializable object.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session() + }) + + it('throws when sessionId argument is not an object', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid argument. The first argument `id` must be an string or serializable object.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session(1) + }) + + it('throws when options argument is provided and is not an object', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid argument. The optional third argument `options` must be an object.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session', () => {}, 'invalid_arg') + }) + + it('throws when options argument has an invalid option', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid option: **invalid_key**\nAvailable options are: `validate`') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session', () => {}, { invalid_key: 2 }) + }) + + it('throws when options argument has an option with an invalid type', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid option value. **validate** must be of type **function** but was **number**.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session', () => {}, { validate: 2 }) + }) + + it('throws when setup function is not provided and existing session is not found', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('No session is defined with the name\n **some-session**\nIn order to use `cy.session()`, provide a `setup` as the second argument:\n\n`cy.session(id, setup)`') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session') + }) + + it('throws when multiple session calls with same sessionId but different options', function (done) { + cy.on('fail', async (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('You may not call `cy.session()` with a previously used name and different options. If you want to specify different options, please use a unique name other than **duplicate-session**.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + await expectCurrentSessionData({ + localStorage: [{ origin: baseUrl, value: { one: 'value' } }], + }) + + done() + }) + + cy.session('duplicate-session', () => { + // function content + window.localStorage.one = 'value' + }) + + cy.session('duplicate-session', () => { + // different function content + window.localStorage.two = 'value' + }) + }) + + describe('options.validate failures', () => { + const errorHookMessage = 'This error occurred in a session validate hook after initializing the session. Because validation failed immediately after session setup we failed the test.' + + it('throws when options.validate has a failing Cypress command', (done) => { + cy.on('fail', (err) => { + expect(err.message).contain('Expected to find element: `#does_not_exist`') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + + done() + }) + + cy.session(['mock-session', 'command'], () => { + cy.log('setup') + }, { + validate () { + cy.get('#does_not_exist', { timeout: 20 }) + }, + }) + }) + + it('throws when options.validate throws an error', (done) => { + cy.on('fail', (err) => { + expect(err.message).contain('validate error') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + done() + }) + + cy.session(['mock-session', 'throws'], () => { + cy.log('setup') + }, { + validate () { + throw new Error('validate error') + }, + }) + }) + + it('throws when options.validate rejects', (done) => { + cy.on('fail', (err) => { + expect(err.message).contain('validate error') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + + done() + }) + + cy.session(['mock-session', 'rejects'], () => { + cy.log('setup') + }, { + validate () { + return Promise.reject(new Error('validate error')) + }, + }) + }) + + it('throws when options.validate returns false', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.contain('Your `cy.session` **validate** callback returned false.') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + + done() + }) + + cy.session(['mock-session', 'return false'], () => { + cy.log('setup') + }, { + validate () { + return false + }, + }) + }) + + it('throws when options.validate resolves false', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.contain('Your `cy.session` **validate** callback resolved false.') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + done() + }) + + cy.session(['mock-session', 'resolves false'], () => { + cy.log('setup') + }, { + validate () { + return Promise.resolve(false) + }, + }) + }) + + // TODO: emilyrohrbough - 4/3/2022 - figure out what the below comment means + // TODO: cy.validate that will fail, hook into event, soft-reload inside and test everything is halted + // Look at other tests for cancellation + // make error collapsible by default + + it('throws when options.validate returns Chainer', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.contain('Your `cy.session` **validate** callback resolved false.') + expect(err.message).contain(errorHookMessage) + done() + }) + + cy.session(['mock-session', 'Chainer'], () => { + cy.log('setup') + }, { + validate () { + return cy.wrap(false) + }, + }) + }) + }) + }) +}) diff --git a/packages/driver/package.json b/packages/driver/package.json index 3f6bc03f24eb..fa8541de4439 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -5,7 +5,7 @@ "scripts": { "clean-deps": "rm -rf node_modules", "cypress:open": "node ../../scripts/cypress open", - "cypress:run": "node ../../scripts/cypress run --spec \"cypress/integration/*/*\",\"cypress/integration/*/!(origin)/**/*\"", + "cypress:run": "node ../../scripts/cypress run --spec \"cypress/integration/*/*\",\"cypress/integration/*/!(origin|sessions)/**/*\"", "cypress:open-experimentalSessionAndOrigin": "node ../../scripts/cypress open --config experimentalSessionAndOrigin=true", "cypress:run-experimentalSessionAndOrigin": "node ../../scripts/cypress run --config experimentalSessionAndOrigin=true", "postinstall": "patch-package", diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index ad998d43dd4b..d57dfdb87fce 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -66,7 +66,7 @@ export default function (Commands, Cypress, cy) { const sessionCommand = cy.state('current') // stringify deterministically if we were given an object - id = typeof id === 'string' ? id : stringifyStable(id) + id = _.isString(id) ? id : stringifyStable(id) if (options) { if (!_.isObject(options)) { @@ -77,14 +77,14 @@ export default function (Commands, Cypress, cy) { 'validate': 'function', } - Object.keys(options).forEach((key) => { + Object.entries(options).forEach(([key, value]) => { const expectedType = validOpts[key] if (!expectedType) { $errUtils.throwErrByPath('sessions.session.wrongArgOptionUnexpected', { args: { key } }) } - const actualType = typeof options[key] + const actualType = typeof value if (actualType !== expectedType) { $errUtils.throwErrByPath('sessions.session.wrongArgOptionInvalid', { args: { key, expected: expectedType, actual: actualType } }) diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index cfb8d1319f49..79857ab0bc6a 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -672,7 +672,7 @@ class $Cypress { // clone the error object // and set stack cleaned // to prevent bluebird from - // attaching long stace traces + // attaching long stack traces // which otherwise make this err // unusably long const err = $errUtils.makeErrFromObj(e) as BackendError diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 4c41358263e4..3c8d21ef1ae3 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1651,18 +1651,19 @@ export default { sessions: { validate_callback_false: { - message: 'Your `cy.session` **validate** callback {{reason}}', + message: 'Your `cy.session` **validate** callback {{reason}}.', }, - experimentNotEnabled (experimentalSessionSupport) { + experimentNotEnabled ({ experimentalSessionSupport }) { if (experimentalSessionSupport) { return { message: stripIndent` ${cmd('session')} requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.`, + docsUrl: 'https://on.cypress.io/session', } } return { - message: `${cmd('session')} requires enabling the \`experimentalSessionAndOrigin\` flag`, + message: `${cmd('session')} requires enabling the \`experimentalSessionAndOrigin\` flag.`, docsUrl: 'https://on.cypress.io/session', } }, diff --git a/packages/reporter/cypress/integration/test_errors_spec.ts b/packages/reporter/cypress/integration/test_errors_spec.ts index 0c1f6337421b..9cede7b50ef3 100644 --- a/packages/reporter/cypress/integration/test_errors_spec.ts +++ b/packages/reporter/cypress/integration/test_errors_spec.ts @@ -295,13 +295,13 @@ describe('test errors', () => { }) it('is not visible by default', () => { - cy.get('.studio-err-wrapper').should('not.be.visible') + cy.get('.studio-err-wrapper').should('not.exist') }) it('is visible when studio is active', () => { runner.emit('reporter:start', { studioActive: true }) - cy.get('.studio-err-wrapper').should('be.visible') + cy.get('.studio-err-wrapper').should('exist').should('be.visible') cy.percySnapshot() }) diff --git a/packages/reporter/src/attempts/attempts.tsx b/packages/reporter/src/attempts/attempts.tsx index 7a8b6f13798c..fe7f05882305 100644 --- a/packages/reporter/src/attempts/attempts.tsx +++ b/packages/reporter/src/attempts/attempts.tsx @@ -41,7 +41,7 @@ const StudioError = () => ( ) -function renderAttemptContent (model: AttemptModel) { +function renderAttemptContent (model: AttemptModel, studioActive: boolean) { // performance optimization - don't render contents if not open return ( @@ -55,7 +55,7 @@ function renderAttemptContent (model: AttemptModel) {
- + {studioActive && }
) @@ -64,6 +64,7 @@ function renderAttemptContent (model: AttemptModel) { interface AttemptProps { model: AttemptModel scrollIntoView: Function + studioActive: boolean } @observer @@ -73,7 +74,7 @@ class Attempt extends Component { } render () { - const { model } = this.props + const { model, studioActive } = this.props // HACK: causes component update when command log is added model.commands.length @@ -91,14 +92,14 @@ class Attempt extends Component { headerClass='attempt-name' isOpen={model.isOpen} > - {renderAttemptContent(model)} + {renderAttemptContent(model, studioActive)} ) } } -const Attempts = observer(({ test, scrollIntoView }: {test: TestModel, scrollIntoView: Function}) => { +const Attempts = observer(({ test, scrollIntoView, studioActive }: {test: TestModel, scrollIntoView: Function, studioActive: boolean}) => { return (
    @@ -107,6 +108,7 @@ const Attempts = observer(({ test, scrollIntoView }: {test: TestModel, scrollInt ) diff --git a/packages/reporter/src/test/test.tsx b/packages/reporter/src/test/test.tsx index bc3ec513e306..4345ddcfe021 100644 --- a/packages/reporter/src/test/test.tsx +++ b/packages/reporter/src/test/test.tsx @@ -182,7 +182,7 @@ class Test extends Component { return (
    - this._scrollIntoView()} /> + this._scrollIntoView()} /> { appState.studioActive && }
    ) diff --git a/packages/runner/cypress/integration/retries.ui.spec.js b/packages/runner/cypress/integration/retries.ui.spec.js index b7cb5802afbf..a64c1bd18374 100644 --- a/packages/runner/cypress/integration/retries.ui.spec.js +++ b/packages/runner/cypress/integration/retries.ui.spec.js @@ -148,7 +148,7 @@ describe('runner/cypress retries.ui.spec', { viewportWidth: 600, viewportHeight: cy.get(attemptTag(3)).parentsUntil('.collapsible').last().parent().within(() => { cy.get('.instruments-container').should('contain', 'Spies / Stubs (2)') cy.get('.instruments-container').should('contain', 'Routes (2)') - cy.get('.runnable-err').should('not.be.visible') + cy.get('.runnable-err').should('not.exist') }) }) diff --git a/packages/runner/cypress/integration/sessions.ui.spec.js b/packages/runner/cypress/integration/sessions.ui.spec.js index 3c9d4561ace5..67c516799d61 100644 --- a/packages/runner/cypress/integration/sessions.ui.spec.js +++ b/packages/runner/cypress/integration/sessions.ui.spec.js @@ -1,146 +1,276 @@ const helpers = require('../support/helpers') +const path = require('path') const { runIsolatedCypress } = helpers.createCypress({ config: { experimentalSessionAndOrigin: true } }) -describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 660 }, () => { - it('empty session with no data', () => { - runIsolatedCypress(() => { - it('t1', () => { - cy.session('blank_session', () => {}) - assert(true) - }) - }) +const validateSessionsInstrumentPanel = (sessionIds = []) => { + cy.get('.sessions-container') + .should('contain', `Sessions (${sessionIds.length})`) + .click() - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'blank_session') + sessionIds.forEach((id) => { + cy.contains('.sessions-container', id) + }) +} + +const validateCreateNewSessionGroup = () => { + cy.contains('Create New Session') + .closest('.command') + .should('have.class', 'command-is-open') + .contains('runSetup') + + cy.contains('Create New Session') + .closest('.command') + .find('.command-name-Clear-Page') + .should('have.length', 2) +} + +describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 1000 }, () => { + it('creates new session', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy')) + + validateSessionsInstrumentPanel(['blank_session']) cy.get('.command-name-session') .first() - .find('i.successful') - .siblings() - .should('contain', '(new) blank_session') + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') + + cy.get('.command-name-session').contains('blank_session') + validateCreateNewSessionGroup() + }) + + cy.percySnapshot() + + cy.get('.command-name-session').first().click('top') + + // FIXME: this should be length 2, not 3 + // the 'Clear Page' log should be nested under session group + cy.get('.command').should('have.length', 3) + }) + + it('creates new session with validation', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy')) + + validateSessionsInstrumentPanel(['blank_session']) cy.get('.command-name-session') - .last() - .contains('blank_session') - .click() + .first() + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') + + cy.get('.command-name-session').contains('blank_session') + + validateCreateNewSessionGroup() + }) cy.percySnapshot() + + // FIXME: this should be nested within the session command + cy.contains('Validate Session: valid') + .closest('.command') + .should('not.have.class', 'command-is-open') + .click() + .contains('runValidation') + + cy.get('.command-name-session').first().click('top') + + // FIXME: this should be length 2, not 5 + // the validate session group should be nested under session group + cy.get('.command').should('have.length', 5) }) - it('shows message for new, saved, and recreated session', () => { - runIsolatedCypress(() => { - const stub = Cypress.sinon.stub().callsFake(() => { - if (stub.callCount === 3) { - return false - } - }) + it('creates new session and fails validation', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy')) - beforeEach(() => { - cy.session('user1', () => { - window.localStorage.foo = 'val' - }, { - validate: stub, - }) - }) + validateSessionsInstrumentPanel(['blank_session']) - it('t1', () => { - expect(window.localStorage.foo).to.eq('val') - }) + cy.get('.command-name-session') + .first() + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') - it('t2', () => { - expect(window.localStorage.foo).to.eq('val') - }) + cy.get('.command-name-session').contains('blank_session') - it('t3', () => { - expect(window.localStorage.foo).to.eq('val') - }) + validateCreateNewSessionGroup() }) + // FIXME: this should be nested within the session command + // FIXME: this should be Validate Session: invalid + cy.contains('Validate Session') + .closest('.command') + .should('have.class', 'command-is-open') + .contains('runValidation') + + cy.contains('CypressError') + + cy.percySnapshot() + }) + + it('restores saved session', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy')) + cy.get('.test').each(($el) => cy.wrap($el).click()) - cy.log('validating new session was created') + cy.log('validate new session was created in first test') cy.get('.test').eq(0).within(() => { - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'user1') + validateSessionsInstrumentPanel(['user1']) + validateCreateNewSessionGroup() + }) + + cy.log('validate saved session was used in second test') + cy.get('.test').eq(1).within(() => { + validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session') .first() - .find('i.successful') - .siblings() - .should('contain', '(new) user1') + .within(() => { + cy.get('i.pending').siblings().should('contain', '(saved) user1') - cy.get('.command-name-session') - .last() - .contains('user1') - .click() + cy.get('.command-name-session').contains('user1') + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('Clear Page') + .should('have.length', 1) + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('runSetup') + .should('not.exist') + + cy.contains('Validate Session: valid') + .closest('.command') + // FIXME: this validation group does not align with the + // with Create New Session's validation group behavior + // should be 'not.have.class' to align + .should('have.class', 'command-is-open') + .contains('runValidation') + }) + + cy.get('.command-name-session').first().click('top') - cy.get('.command-name-assert') - .should('have.class', 'command-state-passed') + cy.get('.command').should('have.length', 2) }) + }) + + it('recreates session', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy')) + + cy.get('.test').each(($el) => cy.wrap($el).click()) + + cy.log('validate new session was created in first test') + cy.get('.test').eq(0).within(() => { + validateSessionsInstrumentPanel(['user1']) - cy.log('validating previous session was used') + cy.contains('Create New Session') + }) + + cy.log('validate saved session was used in second test') cy.get('.test').eq(1).within(() => { - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'user1') + validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session') .first() - .find('i.pending') - .siblings() - .should('contain', '(saved) user1') + .within(() => { + cy.get('i.bad').siblings().should('contain', '(recreated) user1') - cy.get('.command-name-session') - .last() - .contains('user1') + cy.get('.command-name-session').contains('user1') + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('Clear Page') + .should('have.length', 1) + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('runSetup') + .should('not.exist') + + cy.contains('Validate Session: invalid') + + validateCreateNewSessionGroup() + + cy.contains('Validate Session: valid') + .closest('.command') + // FIXME: this validation group does not align with the + // with Create New Session's validation group behavior + // should be 'not.have.class' to align + .should('have.class', 'command-is-open') + .contains('runValidation') + }) + .percySnapshot() + + cy.get('.runnable-err').should('have.length', 1) + + cy.get('.command-name-session').first().click('top') + + cy.get('.command').should('have.length', 2) + }) + }) + + it('recreates session and fails validation', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy')) + + cy.get('.test').each(($el) => cy.wrap($el).click()) + + cy.log('validate new session was created in first test') + cy.get('.test').eq(0).within(() => { + validateSessionsInstrumentPanel(['user1']) + + cy.contains('Create New Session') }) - cy.log('validating session was recreated after it failed to verify') - cy.get('.test').eq(2).within(() => { - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'user1') + cy.log('validate saved session was used in second test') + cy.get('.test').eq(1).within(() => { + validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session') .first() - .find('i.bad') - .siblings() - .should('contain', '(recreated) user1') + .within(() => { + cy.get('i.bad').siblings().should('contain', '(recreated) user1') - cy.get('.command-name-session') - .last() - .contains('user1') - }) + cy.get('.command-name-session').contains('user1') - cy.percySnapshot() - }) + cy.contains('Restore Saved Session') + .closest('.command') + .contains('Clear Page') + .should('have.length', 1) - it('multiple sessions in a test', () => { - runIsolatedCypress(() => { - it('t1', () => { - cy.session('user1', () => { - window.localStorage.foo = 'val' - }) - - cy.session('user2', () => { - window.localStorage.foo = 'val' - window.localStorage.bar = 'val' - }) + cy.contains('Restore Saved Session') + .closest('.command') + .contains('runSetup') + .should('not.exist') + + cy.contains('Validate Session: invalid') + + validateCreateNewSessionGroup() + + // FIXME: this validation group should say 'Validate Session: valid' + cy.contains('Validate Session') + .closest('.command') + // FIXME: this validation group does not align with the + // with Create New Session's validation group behavior + //' should be 'not.have.class' to align + .should('have.class', 'command-is-open') + .contains('runValidation') }) + .percySnapshot() + + cy.get('.runnable-err').should('have.length', 2) }) + }) - cy.get('.sessions-container').first().click() - .should('contain', 'Sessions (2)') - .should('contain', 'user1') - .should('contain', 'user2') + it('multiple sessions in a test', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy')) + validateSessionsInstrumentPanel(['user1', 'user2']) cy.percySnapshot() }) }) diff --git a/system-tests/__snapshots__/session_spec.ts.js b/system-tests/__snapshots__/session_spec.ts.js index 4535f63e09b6..4c69d23d5025 100644 --- a/system-tests/__snapshots__/session_spec.ts.js +++ b/system-tests/__snapshots__/session_spec.ts.js @@ -25,10 +25,6 @@ exports['e2e sessions / session tests'] = ` ✓ get localStorage from all origins ✓ only gets localStorage from origins visited in test - args - ✓ accepts string or object as id - ✓ uses sorted stringify and rejects duplicate registrations - with a blank session ✓ t1 ✓ t2 @@ -87,14 +83,6 @@ exports['e2e sessions / session tests'] = ` ✓ t1 ✓ t2 - options.validate failing test - ✓ test fails when options.validate after setup fails command - ✓ test fails when options.validate after setup throws - ✓ test fails when options.validate after setup rejects - ✓ test fails when options.validate after setup returns false - ✓ test fails when options.validate after setup resolves false - ✓ test fails when options.validate after setup returns Chainer - can wait for login redirect automatically ✓ t1 @@ -118,23 +106,16 @@ exports['e2e sessions / session tests'] = ` ✓ switches to secure context - clears only secure context data - 1/2 ✓ clears only secure context data - 2/2 - errors - ✓ throws error when experimentalSessionAndOrigin not enabled - ✓ throws error when experimentalSessionSupport is enabled through test config - ✓ throws error when experimentalSessionSupport is enabled through Cypress.config - ✓ throws if session has not been defined during current test - ✓ throws if multiple session calls with same name but different options - - 56 passing + 43 passing 1 pending (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 57 │ - │ Passing: 56 │ + │ Tests: 44 │ + │ Passing: 43 │ │ Failing: 0 │ │ Pending: 1 │ │ Skipped: 0 │ @@ -152,9 +133,9 @@ exports['e2e sessions / session tests'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ session_spec.js XX:XX 57 56 - 1 - │ + │ ✔ session_spec.js XX:XX 44 43 - 1 - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 57 56 - 1 - + ✔ All specs passed! XX:XX 44 43 - 1 - ` diff --git a/system-tests/package.json b/system-tests/package.json index 41c1298afd62..0595544821f3 100644 --- a/system-tests/package.json +++ b/system-tests/package.json @@ -8,7 +8,8 @@ "type-check": "tsc --project .", "projects:yarn:install": "node ./scripts/projects-yarn-install.js", "test": "node ./scripts/run.js --glob-in-dir='{test,test-binary}'", - "test:ci": "node ./scripts/run.js" + "test:ci": "node ./scripts/run.js", + "update:snapshots": "SNAPSHOT_UPDATE=1 npm run test" }, "devDependencies": { "@babel/core": "7.9.0", diff --git a/system-tests/projects/e2e/cypress/integration/session_spec.js b/system-tests/projects/e2e/cypress/integration/session_spec.js index a5de24bbeb1d..565e23912876 100644 --- a/system-tests/projects/e2e/cypress/integration/session_spec.js +++ b/system-tests/projects/e2e/cypress/integration/session_spec.js @@ -24,10 +24,7 @@ before(() => { }) const sessionUser = (name = 'user0') => { - console.log('session User') - return cy.session(name, () => { - console.log('cyvisit') cy.visit(`https://localhost:4466/cross_origin_iframe/${name}`) cy.window().then((win) => { win.localStorage.username = name @@ -131,29 +128,6 @@ describe('cross origin automations', function () { }) }) -describe('args', () => { - it('accepts string or object as id', () => { - cy.session('some-name', () => {}) - cy.session({ name: 'some-name', zkey: 'val' }, () => {}) - }) - - it('uses sorted stringify and rejects duplicate registrations', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('previously used name') - expect(err.message).contain('{"key":"val"') - done() - }) - - cy.session({ name: 'bob', key: 'val' }, () => { - // foo - }) - - cy.session({ key: 'val', name: 'bob' }, () => { - // bar - }) - }) -}) - describe('with a blank session', () => { beforeEach(() => { cy.session('sess1', @@ -181,7 +155,6 @@ describe('with a blank session', () => { expectCurrentSessionData({ cookies: ['/form'], - }) }) }) @@ -480,123 +453,6 @@ describe('options.validate reruns steps when failing cy.request', () => { }) }) -describe('options.validate failing test', () => { - it('test fails when options.validate after setup fails command', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('foo') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_1', () => { - cy.log('setup') - }, { - validate () { - cy.wrap('foo', { timeout: 30 }).should('eq', 'bar') - }, - }) - }) - - it('test fails when options.validate after setup throws', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_2', () => { - cy.log('setup') - }, { - validate () { - throw new Error('validate error') - }, - }) - }) - - it('test fails when options.validate after setup rejects', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('validate error') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_3', () => { - cy.log('setup') - }, { - validate () { - return Promise.reject(new Error('validate error')) - }, - }) - }) - - it('test fails when options.validate after setup returns false', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('returned false') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_4', () => { - cy.log('setup') - }, { - validate () { - return false - }, - }) - }) - - it('test fails when options.validate after setup resolves false', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('callback resolved false') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_5', () => { - cy.log('setup') - }, { - validate () { - return Promise.resolve(false) - }, - }) - }) - - // TODO: cy.validate that will fail, hook into event, soft-reload inside and test everything is halted - // Look at other tests for cancellation - // make error collapsible by default - - it('test fails when options.validate after setup returns Chainer', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('callback resolved false') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - done() - }) - - cy.session('user_validate_fails_after_setup', () => { - cy.log('setup') - }, { - validate () { - return cy.wrap(false) - }, - }) - }) -}) - describe('can wait for login redirect automatically', () => { it('t1', () => { cy.session('redirect-login', () => { @@ -760,77 +616,3 @@ describe('ignores setting insecure context data when on secure context', () => { }) }) }) - -describe('errors', () => { - it('throws error when experimentalSessionAndOrigin not enabled', { experimentalSessionAndOrigin: false }, (done) => { - cy.on('fail', ({ message }) => { - expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag') - done() - }) - - cy.session('sessions-not-enabled') - }) - - it('throws error when experimentalSessionSupport is enabled through test config', { experimentalSessionAndOrigin: false, experimentalSessionSupport: true }, (done) => { - cy.on('fail', ({ message }) => { - expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') - done() - }) - - cy.session('sessions-not-enabled') - }) - - it('throws error when experimentalSessionSupport is enabled through Cypress.config', { experimentalSessionAndOrigin: false }, (done) => { - Cypress.config('experimentalSessionSupport', true) - - cy.on('fail', ({ message }) => { - expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') - done() - }) - - cy.session('sessions-not-enabled') - }) - - it('throws if session has not been defined during current test', (done) => { - cy.on('fail', (err) => { - expect(err.message) - .contain('session') - .contain('No session is defined with') - .contain('**bob**') - - expect(err.docsUrl).eq('https://on.cypress.io/session') - expect(err.codeFrame.frame, 'has accurate codeframe').contain('session') - - done() - }) - - cy.session('bob') - }) - - it('throws if multiple session calls with same name but different options', (done) => { - cy.on('fail', (err) => { - expect(err.message) - expect(err.message).contain('previously used name') - .contain('**duplicate-session**') - - expect(err.docsUrl).eq('https://on.cypress.io/session') - expect(err.codeFrame.frame, 'has accurate codeframe').contain('session') - - done() - }) - - cy.session('duplicate-session', () => { - // function content - window.localStorage.one = 'value' - }) - - cy.session('duplicate-session', () => { - // different function content - window.localStorage.two = 'value' - }) - - expectCurrentSessionData({ - localStorage: [{ origin: 'https://localhost:4466', value: { two: 'value' } }], - }) - }) -}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy.js new file mode 100644 index 000000000000..347eab320df9 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy.js @@ -0,0 +1,10 @@ +it('t1', () => { + cy.session('user1', () => { + window.localStorage.foo = 'val' + }) + + cy.session('user2', () => { + window.localStorage.foo = 'val' + window.localStorage.bar = 'val' + }) +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy.js new file mode 100644 index 000000000000..c8acc81bfe82 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy.js @@ -0,0 +1,6 @@ +it('t1', () => { + const setupFn = cy.stub().as('runSetup') + + cy.session('blank_session', setupFn) + cy.log('after') +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy.js new file mode 100644 index 000000000000..27db500fa423 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy.js @@ -0,0 +1,8 @@ +it('t1', () => { + const setupFn = cy.stub().as('runSetup') + const validateFn = cy.stub().returns(false).as('runValidation') + + cy.session('blank_session', setupFn, { + validate: validateFn, + }) +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy.js new file mode 100644 index 000000000000..9e16375511ed --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy.js @@ -0,0 +1,10 @@ +it('t1', () => { + const setupFn = cy.stub().as('runSetup') + const validateFn = cy.stub().as('runValidation') + + cy.session('blank_session', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy.js new file mode 100644 index 000000000000..3a5389920b16 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy.js @@ -0,0 +1,27 @@ +let setupFn +let validateFn + +before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().callsFake(() => { + if (validateFn.callCount === 2) { + return false + } + }).as('runValidation') +}) + +it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) + +it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy.js new file mode 100644 index 000000000000..11201aa6b3d1 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy.js @@ -0,0 +1,27 @@ +let setupFn +let validateFn + +before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().callsFake(() => { + if (validateFn.callCount >= 2) { + return false + } + }).as('runValidation') +}) + +it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) + +it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy.js new file mode 100644 index 000000000000..4640c687c030 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy.js @@ -0,0 +1,23 @@ +let setupFn +let validateFn + +before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().as('runValidation') +}) + +it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) + +it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +})