diff --git a/npm/vue/CHANGELOG.md b/npm/vue/CHANGELOG.md
index 36a6bc47d375..a9e7775c1e96 100644
--- a/npm/vue/CHANGELOG.md
+++ b/npm/vue/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [@cypress/vue-v3.1.2](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.1.1...@cypress/vue-v3.1.2) (2022-05-03)
+
+
+### Bug Fixes
+
+* head content reset, fix [#19721](https://github.com/cypress-io/cypress/issues/19721) ([#21291](https://github.com/cypress-io/cypress/issues/21291)) ([77ab6a5](https://github.com/cypress-io/cypress/commit/77ab6a51a0de1929171a2275e9cec9580c57241d))
+
# [@cypress/vue-v3.1.1](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.1.0...@cypress/vue-v3.1.1) (2022-02-10)
diff --git a/npm/vue/src/index.ts b/npm/vue/src/index.ts
index 8afb1f4c9423..9516423bed30 100644
--- a/npm/vue/src/index.ts
+++ b/npm/vue/src/index.ts
@@ -39,8 +39,6 @@ export type CyMountOptions = Omit,
}
} & Partial
-let initialInnerHtml = ''
-
Cypress.on('run:start', () => {
// `mount` is designed to work with component testing only.
// it assumes ROOT_SELECTOR exists, which is not the case in e2e.
@@ -52,13 +50,11 @@ Cypress.on('run:start', () => {
return
}
- initialInnerHtml = document.head.innerHTML
Cypress.on('test:before:run', () => {
Cypress.vueWrapper?.unmount()
const el = getContainerEl()
el.innerHTML = ''
- document.head.innerHTML = initialInnerHtml
})
})
diff --git a/package.json b/package.json
index e457b0e64c1f..802b2140e539 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cypress",
- "version": "9.6.0",
+ "version": "9.6.1",
"description": "Cypress.io end to end testing tool",
"private": true,
"scripts": {
diff --git a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts
index aaf312db80b8..64eda51c6b43 100644
--- a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts
+++ b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts
@@ -208,8 +208,10 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100
it(`resets selector playground validity when selecting element with playground selector in ${testingType}`, () => {
startAtSpecsPage(testingType)
- cy.get('[data-cy="spec-item"]').first().click()
- cy.get('#unified-reporter').should('be.visible')
+ const spec = testingType === 'e2e' ? 'dom-content.spec.js' : 'TestComponent.spec.jsx'
+
+ cy.get('[data-cy="spec-item"]').contains(spec).click()
+ cy.get('.passed > .num').should('contain', 1)
cy.get('[data-cy="playground-activator"]').click()
cy.get('[data-cy="playground-selector"]').clear()
diff --git a/packages/app/cypress/e2e/runner/retries.ui.cy.ts b/packages/app/cypress/e2e/runner/retries.ui.cy.ts
index 98252b2ffe7b..fe61882cb15b 100644
--- a/packages/app/cypress/e2e/runner/retries.ui.cy.ts
+++ b/packages/app/cypress/e2e/runner/retries.ui.cy.ts
@@ -122,7 +122,7 @@ describe('runner/cypress retries.ui.spec', {
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/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts
index 0f800073aaf6..2bf3f89b92ce 100644
--- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts
+++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts
@@ -1,10 +1,34 @@
import { loadSpec } from './support/spec-loader'
import { snapshotReporter } from './support/snapshot-reporter'
+const validateSessionsInstrumentPanel = (sessionIds: Array = []) => {
+ cy.get('.sessions-container')
+ .should('contain', `Sessions (${sessionIds.length})`)
+ .click()
+
+ sessionIds.forEach((id) => {
+ cy.contains('.sessions-container', id)
+ })
+}
+
+const validateCreateNewSessionGroup = () => {
+ cy.contains('Create New Session').closest('.command').as('createNewSession')
+
+ cy.get('@createNewSession').find('.command-expander-is-open')
+ cy.get('@createNewSession').find('.command-alias').contains('runSetup')
+
+ return cy.contains('Create New Session')
+ .closest('.command')
+ .find('.command-name-Clear-Page')
+ .should('have.length', 2)
+}
+
describe('runner/cypress sessions.ui.spec', {
// Limiting tests kept in memory due to large memory cost
// of nested spec snapshots
numTestsKeptInMemory: 1,
+ viewportWidth: 1000,
+ viewportHeight: 1000,
}, () => {
afterEach(function () {
// @ts-ignore
@@ -13,59 +37,288 @@ describe('runner/cypress sessions.ui.spec', {
}
})
- it('empty session with no data', () => {
+ it('creates new session', () => {
loadSpec({
- filePath: 'sessions/blank_session.cy.js',
- passCount: 1,
+ filePath: 'sessions/new_session.cy.js',
})
- cy.get('.sessions-container').click()
- .should('contain', 'blank_session')
+ validateSessionsInstrumentPanel(['blank_session'])
+
+ cy.get('.command-name-session')
+ .first()
+ .within(() => {
+ cy.get('i.command-message-indicator-successful')
+ .siblings()
+ .should('contain', '(new) blank_session')
+
+ cy.get('.command-name-session').contains('blank_session')
+ validateCreateNewSessionGroup()
+ })
+
+ cy.percySnapshot()
+
+ cy.get('.command-name-session').find('.command-expander-column').first().click()
+
+ cy.get('.command').should('have.length', 2)
})
- it('shows message for new, saved, and recreated session', () => {
+ it('creates new session with validation', () => {
loadSpec({
- filePath: 'sessions/recreated_session.cy.js',
- passCount: 3,
+ filePath: 'sessions/new_session_with_validation.cy.js',
})
- const clickEl = ($el) => cy.wrap($el).click()
+ validateSessionsInstrumentPanel(['blank_session'])
+
+ cy.get('.command-name-session')
+ .first()
+ .within(() => {
+ cy.get('i.command-message-indicator-successful')
+ .siblings()
+ .should('contain', '(new) blank_session')
+
+ cy.get('.command-name-session').contains('blank_session')
+
+ validateCreateNewSessionGroup()
+
+ cy.contains('Validate Session: valid')
+ .closest('.command').as('validateSession')
+
+ cy.get('@validateSession')
+ .find('.command-expander-is-open')
+
+ cy.get('@validateSession')
+ .find('.command-alias')
+ .contains('runValidation')
+ })
+
+ cy.percySnapshot()
+
+ cy.get('.command-name-session').get('.command-expander').first().click()
+
+ cy.get('.command').should('have.length', 2)
+ })
+
+ it('creates new session and fails validation', () => {
+ loadSpec({
+ filePath: 'sessions/new_session_and_fails_validation.cy.js',
+ })
+
+ validateSessionsInstrumentPanel(['blank_session'])
+
+ cy.get('.command-name-session')
+ .first()
+ .within(() => {
+ cy.get('i.command-message-indicator-successful')
+ .siblings()
+ .should('contain', '(new) blank_session')
+
+ cy.get('.command-name-session').contains('blank_session')
+
+ validateCreateNewSessionGroup()
+
+ cy.contains('Validate Session: invalid')
+ .closest('.command').as('validateSession')
+
+ cy.get('@validateSession')
+ .find('.command-expander')
+ .should('have.class', 'command-expander-is-open')
+
+ cy.get('@validateSession')
+ .find('.command-alias')
+ .contains('runValidation')
+ })
+
+ cy.contains('CypressError')
+
+ cy.percySnapshot()
+ })
+
+ it('restores saved session', () => {
+ loadSpec({
+ filePath: 'sessions/restores_saved_session.cy.js',
+ })
+
+ 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'])
+ 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()
+ .within(() => {
+ cy.get('i.command-message-indicator-pending')
+ .siblings().should('contain', '(saved) user1')
- cy.get('.test').eq(0).then(clickEl)
- cy.get('.test').eq(1).then(clickEl)
- cy.get('.test').eq(2).then(clickEl)
+ cy.get('.command-name-session').contains('user1')
- cy.get('.sessions-container .collapsible-header[role=button]').eq(0).click()
- .should('contain', '1')
+ cy.contains('Restore Saved Session')
+ .closest('.command')
+ .contains('Clear Page')
+ .should('have.length', 1)
- cy.get('.sessions-container .collapsible-header[role=button]').eq(1).click()
- .should('contain', '1')
+ cy.contains('Restore Saved Session')
+ .closest('.command')
+ .contains('runSetup')
+ .should('not.exist')
- cy.get('.test').eq(0)
- .should('contain', 'Sessions (1)')
- .should('contain', 'user1')
- .should('contain', '(new)')
+ cy.contains('Validate Session: valid')
+ .closest('.command').as('validateSession')
- cy.get('.test').eq(1)
- .should('contain', 'Sessions (1)')
- .should('contain', 'user1')
- .should('contain', '(saved)')
+ cy.get('@validateSession')
+ .find('.command-expander')
+ // 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-expander-is-open')
- cy.get('.test').eq(2)
- .should('contain', 'Sessions (1)')
- .should('contain', 'user1')
- .should('contain', '(recreated)')
+ cy.get('@validateSession')
+ .find('.command-alias')
+ .contains('runValidation')
+ })
+
+ cy.get('.command-name-session').get('.command-expander').first().click()
+
+ cy.get('.command').should('have.length', 2)
+ })
+ })
+
+ it('recreates session', () => {
+ loadSpec({
+ filePath: 'sessions/recreates_session.cy.js',
+ })
+
+ 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('validate saved session was used in second test')
+ cy.get('.test').eq(1).within(() => {
+ validateSessionsInstrumentPanel(['user1'])
+
+ cy.get('.command-name-session')
+ .first()
+ .within(() => {
+ cy.get('i.command-message-indicator-bad')
+ .siblings().should('contain', '(recreated) 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').as('validateSession')
+
+ cy.get('@validateSession')
+ .find('.command-expander')
+ // 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-expander-is-open')
+
+ cy.get('@validateSession')
+ .find('.command-alias')
+ .contains('runValidation')
+ })
+ .percySnapshot()
+
+ cy.get('.runnable-err').should('have.length', 1)
+
+ cy.get('.command-name-session').get('.command-expander').first().click()
+
+ cy.get('.command').should('have.length', 2)
+ })
+ })
+
+ it('recreates session and fails validation', () => {
+ loadSpec({
+ filePath: 'sessions/recreates_session_and_fails_validation.cy.js',
+ })
+
+ 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('validate saved session was used in second test')
+ cy.get('.test').eq(1).within(() => {
+ validateSessionsInstrumentPanel(['user1'])
+
+ cy.get('.command-name-session')
+ .first()
+ .within(() => {
+ cy.get('i.command-message-indicator-bad')
+ .siblings().should('contain', '(recreated) 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()
+ .parent()
+ .closest('.command')
+ .next()
+ .contains('Validate Session: invalid')
+ .closest('.command').as('secondValidateSession')
+
+ cy.get('@secondValidateSession')
+ .find('.command-expander')
+ // should be 'not.have.class' to align
+ .should('have.class', 'command-expander-is-open')
+
+ cy.get('@secondValidateSession')
+ .find('.command-alias')
+ .contains('runValidation')
+ })
+ .percySnapshot()
+
+ cy.get('.runnable-err').should('have.length', 2)
+ })
})
it('multiple sessions in a test', () => {
loadSpec({
filePath: 'sessions/multiple_sessions.cy.js',
- passCount: 1,
})
- cy.get('.sessions-container').first().click()
- .should('contain', 'Sessions (2)')
- .should('contain', 'user1')
- .should('contain', 'user2')
+ validateSessionsInstrumentPanel(['user1', 'user2'])
+ cy.percySnapshot()
})
})
diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts
index 0dda1323a40c..6a1da9906917 100644
--- a/packages/app/cypress/e2e/runs.cy.ts
+++ b/packages/app/cypress/e2e/runs.cy.ts
@@ -1,6 +1,27 @@
import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
import type { SinonStub } from 'sinon'
+function scaffoldTestingTypeAndVisitRunsPage (testingType: 'e2e' | 'component') {
+ cy.scaffoldProject('cypress-in-cypress')
+ cy.openProject('cypress-in-cypress')
+ cy.startAppServer(testingType)
+
+ cy.loginUser()
+
+ // make sure there are no runs found for the project ID
+ cy.remoteGraphQLIntercept(async (obj) => {
+ if (obj.result.data?.cloudProjectBySlug) {
+ obj.result.data.cloudProjectBySlug.runs.nodes = []
+ }
+
+ return obj.result
+ })
+
+ cy.visitApp()
+
+ return cy.get('[href="#/runs"]').click()
+}
+
describe('App: Runs', { viewportWidth: 1200 }, () => {
context('Runs Page', () => {
beforeEach(() => {
@@ -398,47 +419,40 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
cy.findByText(defaultMessages.runs.connect.buttonProject).should('exist')
})
- it('displays how to record prompt when connected and no runs', () => {
- cy.scaffoldProject('component-tests')
- cy.openProject('component-tests')
- cy.startAppServer('component')
-
- cy.loginUser()
- cy.remoteGraphQLIntercept(async (obj) => {
- if (obj.result.data?.cloudProjectBySlug?.runs?.nodes) {
- obj.result.data.cloudProjectBySlug.runs.nodes = []
- }
+ it('displays how to record prompt when connected and no runs in Component Testing', () => {
+ scaffoldTestingTypeAndVisitRunsPage('component')
+ cy.contains(defaultMessages.runs.empty.title).should('be.visible')
+ cy.contains(defaultMessages.runs.empty.description).should('be.visible')
+ cy.contains('cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa').should('be.visible')
+ })
- return obj.result
- })
+ it('displays how to record prompt when connected and no runs in E2E', () => {
+ scaffoldTestingTypeAndVisitRunsPage('e2e')
- cy.visitApp()
- cy.get('[href="#/runs"]').click()
- cy.contains(defaultMessages.runs.empty.title)
- cy.contains(defaultMessages.runs.empty.description)
- cy.contains('--record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
+ cy.contains(defaultMessages.runs.empty.title).should('be.visible')
+ cy.contains(defaultMessages.runs.empty.description).should('be.visible')
+ cy.contains('cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa').should('be.visible')
})
- it('displays a copy button', () => {
- cy.scaffoldProject('component-tests')
- cy.openProject('component-tests')
- cy.startAppServer('component')
-
+ it('displays a copy button and copies correct command in Component Testing', () => {
+ scaffoldTestingTypeAndVisitRunsPage('component')
cy.withCtx(async (ctx, o) => {
o.sinon.stub(ctx.electronApi, 'copyTextToClipboard')
})
- cy.loginUser()
- cy.remoteGraphQLIntercept(async (obj) => {
- if (obj.result.data?.cloudProjectBySlug?.runs?.nodes) {
- obj.result.data.cloudProjectBySlug.runs.nodes = []
- }
+ cy.get('[data-cy="copy-button"]').click()
+ cy.contains('Copied!')
+ cy.withRetryableCtx((ctx) => {
+ expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
+ })
+ })
- return obj.result
+ it('displays a copy button and copies correct command in E2E', () => {
+ scaffoldTestingTypeAndVisitRunsPage('e2e')
+ cy.withCtx(async (ctx, o) => {
+ o.sinon.stub(ctx.electronApi, 'copyTextToClipboard')
})
- cy.visitApp()
- cy.get('[href="#/runs"]').click()
cy.get('[data-cy="copy-button"]').click()
cy.contains('Copied!')
cy.withRetryableCtx((ctx) => {
diff --git a/packages/app/cypress/e2e/specs.cy.ts b/packages/app/cypress/e2e/specs.cy.ts
index 08e331ceb09d..d2848915db0a 100644
--- a/packages/app/cypress/e2e/specs.cy.ts
+++ b/packages/app/cypress/e2e/specs.cy.ts
@@ -509,6 +509,8 @@ describe('App: Specs', () => {
it('shows success modal when empty spec is created', () => {
cy.get('@CreateEmptySpecDialog').within(() => {
+ cy.findByLabelText('Enter a relative path...').invoke('val').should('eq', getPathForPlatform('cypress/component/filename.cy.ts'))
+
cy.findByLabelText('Enter a relative path...').clear().type('cypress/my-empty-spec.cy.js')
cy.findByRole('button', { name: 'Create Spec' }).click()
@@ -533,6 +535,8 @@ describe('App: Specs', () => {
it('navigates to spec runner when selected', () => {
cy.get('@CreateEmptySpecDialog').within(() => {
+ cy.findByLabelText('Enter a relative path...').invoke('val').should('eq', getPathForPlatform('cypress/component/filename.cy.ts'))
+
cy.findByLabelText('Enter a relative path...').clear().type('cypress/my-empty-spec.cy.js')
cy.findByRole('button', { name: 'Create Spec' }).click()
@@ -551,6 +555,8 @@ describe('App: Specs', () => {
it('displays alert with docs link on new spec', () => {
cy.get('@CreateEmptySpecDialog').within(() => {
+ cy.findByLabelText('Enter a relative path...').invoke('val').should('eq', getPathForPlatform('cypress/component/filename.cy.ts'))
+
cy.findByLabelText('Enter a relative path...').clear().type('cypress/my-empty-spec.cy.js')
cy.findByRole('button', { name: 'Create Spec' }).click()
diff --git a/packages/app/cypress/e2e/top-nav.cy.ts b/packages/app/cypress/e2e/top-nav.cy.ts
index e1178f35539e..b676381b1e38 100644
--- a/packages/app/cypress/e2e/top-nav.cy.ts
+++ b/packages/app/cypress/e2e/top-nav.cy.ts
@@ -264,34 +264,35 @@ describe('App Top Nav Workflows', () => {
cy.findByRole('heading', { name: 'References', level: 2 })
cy.findByRole('heading', { name: 'Run in CI/CD', level: 2 })
- cy.validateExternalLink({
- name: 'Write your first test',
- href: 'https://on.cypress.io/writing-first-test?utm_medium=Docs+Menu&utm_content=First+Test',
- })
-
- cy.validateExternalLink({
- name: 'Testing your app',
- href: 'https://on.cypress.io/testing-your-app?utm_medium=Docs+Menu&utm_content=Testing+Your+App',
- })
-
- cy.validateExternalLink({
- name: 'Organizing Tests',
- href: 'https://on.cypress.io/writing-and-organizing-tests?utm_medium=Docs+Menu&utm_content=Organizing+Tests',
- })
-
- cy.validateExternalLink({
- name: 'Best Practices',
- href: 'https://on.cypress.io/best-practices?utm_medium=Docs+Menu&utm_content=Best+Practices',
- })
-
- cy.validateExternalLink({
- name: 'Configuration',
- href: 'https://on.cypress.io/configuration?utm_medium=Docs+Menu&utm_content=Configuration',
- })
-
- cy.validateExternalLink({
- name: 'API',
- href: 'https://on.cypress.io/api?utm_medium=Docs+Menu&utm_content=API',
+ const expectedLinks = [
+ {
+ name: 'Write your first test',
+ href: 'https://on.cypress.io/writing-first-test?utm_medium=Docs+Menu&utm_content=First+Test&utm_source=Binary%3A+App',
+ },
+ {
+ name: 'Testing your app',
+ href: 'https://on.cypress.io/testing-your-app?utm_medium=Docs+Menu&utm_content=Testing+Your+App&utm_source=Binary%3A+App',
+ },
+ {
+ name: 'Organizing Tests',
+ href: 'https://on.cypress.io/writing-and-organizing-tests?utm_medium=Docs+Menu&utm_content=Organizing+Tests&utm_source=Binary%3A+App',
+ },
+ {
+ name: 'Best Practices',
+ href: 'https://on.cypress.io/best-practices?utm_medium=Docs+Menu&utm_content=Best+Practices&utm_source=Binary%3A+App',
+ },
+ {
+ name: 'Configuration',
+ href: 'https://on.cypress.io/configuration?utm_medium=Docs+Menu&utm_content=Configuration&utm_source=Binary%3A+App',
+ },
+ {
+ name: 'API',
+ href: 'https://on.cypress.io/api?utm_medium=Docs+Menu&utm_content=API&utm_source=Binary%3A+App',
+ },
+ ]
+
+ expectedLinks.forEach((link) => {
+ cy.validateExternalLink(link)
})
})
diff --git a/packages/app/src/runs/RunsEmpty.vue b/packages/app/src/runs/RunsEmpty.vue
index d1dac4553d7c..318079792135 100644
--- a/packages/app/src/runs/RunsEmpty.vue
+++ b/packages/app/src/runs/RunsEmpty.vue
@@ -33,6 +33,7 @@ fragment RunsEmpty on CurrentProject {
title
projectId
configFile
+ currentTestingType
cloudProject {
__typename
... on CloudProject {
@@ -58,7 +59,9 @@ const firstRecordKey = computed(() => {
: ''
})
const recordCommand = computed(() => {
- return `cypress run --record --key ${firstRecordKey.value}`
+ const componentFlagOrSpace = props.gql.currentTestingType === 'component' ? ' --component ' : ' '
+
+ return `cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
})
diff --git a/packages/app/src/runs/modals/CreateCloudOrgModal.vue b/packages/app/src/runs/modals/CreateCloudOrgModal.vue
index 228198678ee9..664474d5de17 100644
--- a/packages/app/src/runs/modals/CreateCloudOrgModal.vue
+++ b/packages/app/src/runs/modals/CreateCloudOrgModal.vue
@@ -10,7 +10,7 @@
{{ t('runs.connect.modal.createOrg.description') }}
', () => {
gql={{
currentProject: {
id: 'id',
- codeGenGlobs: {
- id: 'super-unique-id',
- __typename: 'CodeGenGlobs',
- component: '**.vue',
- },
currentTestingType: 'component',
configFile: 'cypress.config.js',
configFileAbsolutePath: '/path/to/cypress.config.js',
@@ -79,11 +74,6 @@ describe('playground', () => {
gql={{
currentProject: {
id: 'id',
- codeGenGlobs: {
- id: 'super-unique-id',
- __typename: 'CodeGenGlobs',
- component: '**.vue',
- },
currentTestingType: 'component',
configFile: 'cypress.config.js',
configFileAbsolutePath: '/path/to/cypress.config.js',
diff --git a/packages/app/src/specs/CreateSpecModal.vue b/packages/app/src/specs/CreateSpecModal.vue
index 5ec9bf3ae1f2..e2fad62993fa 100644
--- a/packages/app/src/specs/CreateSpecModal.vue
+++ b/packages/app/src/specs/CreateSpecModal.vue
@@ -20,7 +20,6 @@
v-if="generator"
:key="`${generator.id}-${iteration}`"
v-model:title="title"
- :code-gen-glob="codeGenGlob"
:gql="props.gql.currentProject"
:type="props.gql.currentProject?.currentTestingType"
:spec-file-name="specFileName"
@@ -78,10 +77,6 @@ fragment CreateSpecModal on Query {
id
fileExtensionToUse
defaultSpecFileName
- codeGenGlobs {
- id
- component
- }
...EmptyGenerator
}
}
@@ -114,14 +109,6 @@ const specFileName = computed(() => {
return getPathForPlatform(fileName)
})
-const codeGenGlob = computed(() => {
- if (!generator.value) {
- return null
- }
-
- return props.gql.currentProject?.codeGenGlobs[generator.value.id]
-})
-
const filteredGenerators = getFilteredGeneratorList(props.gql.currentProject?.currentTestingType)
const singleGenerator = computed(() => filteredGenerators.value.length === 1 ? filteredGenerators.value[0] : null)
diff --git a/packages/data-context/__snapshots__/codegen.spec.ts.js b/packages/data-context/__snapshots__/codegen.spec.ts.js
index b4548acacfd6..ec8c9a31947f 100644
--- a/packages/data-context/__snapshots__/codegen.spec.ts.js
+++ b/packages/data-context/__snapshots__/codegen.spec.ts.js
@@ -229,7 +229,6 @@ const { defineConfig } = require('cypress')
module.exports = defineConfig({
component: {
setupNodeEvents(on, config) {},
- componentFolder: '.',
specPattern: './**/*.spec.cy.{js,ts,jsx,tsx}',
},
})
diff --git a/packages/data-context/package.json b/packages/data-context/package.json
index 2b4cf1fc0d45..51c396b6ee0f 100644
--- a/packages/data-context/package.json
+++ b/packages/data-context/package.json
@@ -23,7 +23,6 @@
"@urql/exchange-graphcache": "4.3.6",
"chokidar": "3.5.1",
"common-path-prefix": "3.0.0",
- "create-cypress-tests": "0.0.0-development",
"cross-fetch": "^3.1.4",
"dataloader": "^2.0.0",
"dayjs": "^1.9.3",
diff --git a/packages/data-context/src/actions/MigrationActions.ts b/packages/data-context/src/actions/MigrationActions.ts
index 486aee8281f5..0cb1096a3565 100644
--- a/packages/data-context/src/actions/MigrationActions.ts
+++ b/packages/data-context/src/actions/MigrationActions.ts
@@ -260,9 +260,11 @@ export class MigrationActions {
throw error
})
- // @ts-ignore configFile needs to be updated with the new one, so it finds the correct one
- // with the new file, instead of the deleted one which is not supported anymore
- this.ctx.modeOptions.configFile = this.ctx.migration.configFileNameAfterMigration
+ if (this.ctx.modeOptions.configFile) {
+ // @ts-ignore configFile needs to be updated with the new one, so it finds the correct one
+ // with the new file, instead of the deleted one which is not supported anymore
+ this.ctx.modeOptions.configFile = this.ctx.migration.configFileNameAfterMigration
+ }
}
async setLegacyConfigForMigration (config: LegacyCypressConfigJson) {
diff --git a/packages/data-context/src/sources/ProjectDataSource.ts b/packages/data-context/src/sources/ProjectDataSource.ts
index 91e201f7814d..22b9066f399f 100644
--- a/packages/data-context/src/sources/ProjectDataSource.ts
+++ b/packages/data-context/src/sources/ProjectDataSource.ts
@@ -1,8 +1,6 @@
import os from 'os'
import chokidar from 'chokidar'
-import type { ResolvedFromConfig, RESOLVED_FROM, FoundSpec } from '@packages/types'
-import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
-import { scanFSForAvailableDependency } from 'create-cypress-tests'
+import type { ResolvedFromConfig, RESOLVED_FROM, FoundSpec, TestingType } from '@packages/types'
import minimatch from 'minimatch'
import { debounce, isEqual } from 'lodash'
import path from 'path'
@@ -105,7 +103,7 @@ export function transformSpec ({
}
}
-export function getDefaultSpecFileName (specPattern: string, fileExtensionToUse?: 'js' | 'ts') {
+export function getDefaultSpecFileName (specPattern: string, testingType: TestingType, fileExtensionToUse?: 'js' | 'ts') {
function replaceWildCard (s: string, fallback: string) {
return s.replace(/\*/g, fallback)
}
@@ -122,7 +120,7 @@ export function getDefaultSpecFileName (specPattern: string, fileExtensionToUse?
dirname = dirname.replace('**', 'cypress')
}
- const splittedDirname = dirname.split('/').filter((s) => s !== '**').map((x) => replaceWildCard(x, 'e2e')).join('/')
+ const splittedDirname = dirname.split('/').filter((s) => s !== '**').map((x) => replaceWildCard(x, testingType)).join('/')
const fileName = replaceWildCard(parsedGlob.path.filename, 'filename')
const extnameWithoutExt = parsedGlob.path.extname.replace(parsedGlob.path.ext, '')
@@ -283,13 +281,15 @@ export class ProjectDataSource {
}
async defaultSpecFileName () {
- const defaultFileName = 'cypress/e2e/filename.cy.js'
+ const getDefaultFileName = (testingType: TestingType) => `cypress/${testingType}/filename.cy.${this.ctx.lifecycleManager.fileExtensionToUse}`
try {
if (!this.ctx.currentProject || !this.ctx.coreData.currentTestingType) {
return null
}
+ const defaultFileName = getDefaultFileName(this.ctx.coreData.currentTestingType)
+
let specPatternSet: string | undefined
const { specPattern = [] } = await this.ctx.project.specPatterns()
@@ -301,7 +301,11 @@ export class ProjectDataSource {
return defaultFileName
}
- const specFileName = getDefaultSpecFileName(specPatternSet, this.ctx.lifecycleManager.fileExtensionToUse)
+ if (specPatternSet === defaultSpecPattern[this.ctx.coreData.currentTestingType]) {
+ return defaultFileName
+ }
+
+ const specFileName = getDefaultSpecFileName(specPatternSet, this.ctx.coreData.currentTestingType, this.ctx.lifecycleManager.fileExtensionToUse)
if (!specFileName) {
return defaultFileName
@@ -309,7 +313,7 @@ export class ProjectDataSource {
return specFileName
} catch {
- return defaultFileName
+ return getDefaultFileName(this.ctx.coreData.currentTestingType ?? 'e2e')
}
}
@@ -360,31 +364,6 @@ export class ProjectDataSource {
return preferences[projectTitle] ?? null
}
- private guessFramework (projectRoot: string) {
- const guess = WIZARD_FRAMEWORKS.find((framework) => {
- const lookingForDeps = framework.detectors.map((x) => x.package).reduce(
- (acc, dep) => ({ ...acc, [dep]: '*' }),
- {},
- )
-
- return scanFSForAvailableDependency(projectRoot, lookingForDeps)
- })
-
- return guess ?? null
- }
-
- async getCodeGenGlobs () {
- assert(this.ctx.currentProject, `Cannot find glob without currentProject.`)
-
- const looseComponentGlob = '*.{js,jsx,ts,tsx,.vue}'
-
- const framework = this.guessFramework(this.ctx.currentProject)
-
- return {
- component: framework?.glob ?? looseComponentGlob,
- }
- }
-
async getResolvedConfigFields (): Promise {
const config = this.ctx.lifecycleManager.loadedFullConfig?.resolved ?? {}
diff --git a/packages/data-context/src/sources/migration/codegen.ts b/packages/data-context/src/sources/migration/codegen.ts
index 27130c0e393d..7033103aa571 100644
--- a/packages/data-context/src/sources/migration/codegen.ts
+++ b/packages/data-context/src/sources/migration/codegen.ts
@@ -14,6 +14,8 @@ import { hasDefaultExport } from './parserUtils'
import type { LegacyCypressConfigJson } from '..'
import { parse } from '@babel/parser'
import generate from '@babel/generator'
+import _ from 'lodash'
+import { getBreakingKeys } from '@packages/config'
const debug = Debug('cypress:data-context:sources:migration:codegen')
@@ -383,19 +385,23 @@ export function reduceConfig (cfg: LegacyCypressConfigJson, options: CreateConfi
const isDefaultE2E = key === 'e2e' && specPattern === `cypress/e2e/${ext}`
const isDefaultCT = key === 'component' && specPattern === ext
+ const breakingKeys = getBreakingKeys()
+ const restWithoutBreakingKeys = _.omit(rest, breakingKeys)
+ const existingWithoutBreakingKeys = _.omit(acc[key], breakingKeys)
+
if (isDefaultE2E || isDefaultCT) {
return {
...acc, [key]: {
- ...rest,
- ...acc[key],
+ ...restWithoutBreakingKeys,
+ ...existingWithoutBreakingKeys,
},
}
}
return {
...acc, [key]: {
- ...rest,
- ...acc[key],
+ ...restWithoutBreakingKeys,
+ ...existingWithoutBreakingKeys,
specPattern,
},
}
diff --git a/packages/data-context/test/unit/codegen/code-generator.spec.ts b/packages/data-context/test/unit/codegen/code-generator.spec.ts
index 9f80a4218725..393ebae41b4c 100644
--- a/packages/data-context/test/unit/codegen/code-generator.spec.ts
+++ b/packages/data-context/test/unit/codegen/code-generator.spec.ts
@@ -1,10 +1,8 @@
import { parse } from '@babel/parser'
-import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
import { expect } from 'chai'
import dedent from 'dedent'
import fs from 'fs-extra'
import path from 'path'
-import sinon from 'sinon'
import { DataContext } from '../../../src'
import {
Action, codeGenerator, CodeGenResult, CodeGenResults,
@@ -221,9 +219,6 @@ describe('code-generator', () => {
target,
}
- // @ts-ignore
- sinon.stub(ctx.project, 'guessFramework').returns(WIZARD_FRAMEWORKS[0])
-
const newSpecCodeGenOptions = new SpecOptions(ctx, {
codeGenPath: path.join(__dirname, 'files', 'react', 'Button.jsx'),
codeGenType: 'component',
@@ -244,9 +239,6 @@ describe('code-generator', () => {
target,
}
- // @ts-ignore
- sinon.stub(ctx.project, 'guessFramework').returns(WIZARD_FRAMEWORKS[1])
-
const newSpecCodeGenOptions = new SpecOptions(ctx, {
codeGenPath: path.join(__dirname, 'files', 'vue', 'Button.vue'),
codeGenType: 'component',
diff --git a/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts b/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts
index f393ae74adbb..2a864587227a 100644
--- a/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts
+++ b/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts
@@ -192,31 +192,31 @@ describe('getDefaultSpecFileName', () => {
context('dirname', () => {
it('returns pattern without change if it is do not a glob', () => {
const specPattern = 'cypress/e2e/foo.spec.ts'
- const defaultFileName = getDefaultSpecFileName(specPattern)
+ const defaultFileName = getDefaultSpecFileName(specPattern, 'e2e')
expect(defaultFileName).to.eq(specPattern)
})
it('remove ** from glob if it is not in the beginning', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/**/foo.spec.ts')
+ const defaultFileName = getDefaultSpecFileName('cypress/**/foo.spec.ts', 'e2e')
expect(defaultFileName).to.eq('cypress/foo.spec.ts')
})
it('replace ** for cypress if it starts with **', () => {
- const defaultFileName = getDefaultSpecFileName('**/e2e/foo.spec.ts')
+ const defaultFileName = getDefaultSpecFileName('**/e2e/foo.spec.ts', 'e2e')
expect(defaultFileName).to.eq('cypress/e2e/foo.spec.ts')
})
it('replace ** for cypress if it starts with ** and omit extra **', () => {
- const defaultFileName = getDefaultSpecFileName('**/**/foo.spec.ts')
+ const defaultFileName = getDefaultSpecFileName('**/**/foo.spec.ts', 'e2e')
expect(defaultFileName).to.eq('cypress/foo.spec.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
- const defaultFileName = getDefaultSpecFileName('{cypress,tests}/{integration,e2e}/foo.spec.ts')
+ const defaultFileName = getDefaultSpecFileName('{cypress,tests}/{integration,e2e}/foo.spec.ts', 'e2e')
expect(defaultFileName).to.eq('cypress/integration/foo.spec.ts')
})
@@ -224,13 +224,13 @@ describe('getDefaultSpecFileName', () => {
context('filename', () => {
it('replace * for filename', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/e2e/*.spec.ts')
+ const defaultFileName = getDefaultSpecFileName('cypress/e2e/*.spec.ts', 'e2e')
expect(defaultFileName).to.eq('cypress/e2e/filename.spec.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/e2e/{foo,filename}.spec.ts')
+ const defaultFileName = getDefaultSpecFileName('cypress/e2e/{foo,filename}.spec.ts', 'e2e')
expect(defaultFileName).to.eq('cypress/e2e/foo.spec.ts')
})
@@ -238,13 +238,13 @@ describe('getDefaultSpecFileName', () => {
context('test extension', () => {
it('replace * for filename', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.*.ts')
+ const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.*.ts', 'e2e')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.{spec,cy}.ts')
+ const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.{spec,cy}.ts', 'e2e')
expect(defaultFileName).to.eq('cypress/e2e/filename.spec.ts')
})
@@ -252,25 +252,25 @@ describe('getDefaultSpecFileName', () => {
context('lang extension', () => {
it('if project use TS, set TS as extension if it exists in the glob', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.ts', 'ts')
+ const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.ts', 'e2e', 'ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
})
it('if project use TS, set TS as extension if it exists in the options of extensions', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,ts,tsx}', 'ts')
+ const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,ts,tsx}', 'e2e', 'ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
})
it('if project use TS, do not set TS as extension if it do not exists in the options of extensions', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,jsx}', 'ts')
+ const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,jsx}', 'e2e', 'ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.js')
})
it('selects first option if there are multiples possibilities of values', () => {
- const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{ts,js}')
+ const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{ts,js}', 'e2e')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
})
@@ -278,43 +278,43 @@ describe('getDefaultSpecFileName', () => {
context('extra cases', () => {
it('creates specName for tests/*.js', () => {
- const defaultFileName = getDefaultSpecFileName('tests/*.js')
+ const defaultFileName = getDefaultSpecFileName('tests/*.js', 'e2e')
expect(defaultFileName).to.eq('tests/filename.js')
})
it('creates specName for src/*-test.js', () => {
- const defaultFileName = getDefaultSpecFileName('src/*-test.js')
+ const defaultFileName = getDefaultSpecFileName('src/*-test.js', 'e2e')
expect(defaultFileName).to.eq('src/filename-test.js')
})
it('creates specName for src/*.foo.bar.js', () => {
- const defaultFileName = getDefaultSpecFileName('src/*.foo.bar.js')
+ const defaultFileName = getDefaultSpecFileName('src/*.foo.bar.js', 'e2e')
expect(defaultFileName).to.eq('src/filename.foo.bar.js')
})
it('creates specName for src/prefix.*.test.js', () => {
- const defaultFileName = getDefaultSpecFileName('src/prefix.*.test.js')
+ const defaultFileName = getDefaultSpecFileName('src/prefix.*.test.js', 'e2e')
expect(defaultFileName).to.eq('src/prefix.cy.test.js')
})
it('creates specName for src/*/*.test.js', () => {
- const defaultFileName = getDefaultSpecFileName('src/*/*.test.js')
+ const defaultFileName = getDefaultSpecFileName('src/*/*.test.js', 'e2e')
expect(defaultFileName).to.eq('src/e2e/filename.test.js')
})
it('creates specName for src-*/**/*.test.js', () => {
- const defaultFileName = getDefaultSpecFileName('src-*/**/*.test.js')
+ const defaultFileName = getDefaultSpecFileName('src-*/**/*.test.js', 'e2e')
expect(defaultFileName).to.eq('src-e2e/filename.test.js')
})
it('creates specName for src/*.test.(js|jsx)', () => {
- const defaultFileName = getDefaultSpecFileName('src/*.test.(js|jsx)')
+ const defaultFileName = getDefaultSpecFileName('src/*.test.(js|jsx)', 'e2e')
const possiblesFileNames = ['src/filename.test.jsx', 'src/filename.test.js']
@@ -322,7 +322,7 @@ describe('getDefaultSpecFileName', () => {
})
it('creates specName for (src|components)/**/*.test.js', () => {
- const defaultFileName = getDefaultSpecFileName('(src|components)/**/*.test.js')
+ const defaultFileName = getDefaultSpecFileName('(src|components)/**/*.test.js', 'e2e')
const possiblesFileNames = ['src/filename.test.js', 'components/filename.test.js']
@@ -330,7 +330,7 @@ describe('getDefaultSpecFileName', () => {
})
it('creates specName for e2e/**/*.cy.{js,jsx,ts,tsx}', () => {
- const defaultFileName = getDefaultSpecFileName('e2e/**/*.cy.{js,jsx,ts,tsx}')
+ const defaultFileName = getDefaultSpecFileName('e2e/**/*.cy.{js,jsx,ts,tsx}', 'e2e')
expect(defaultFileName).to.eq('e2e/filename.cy.js')
})
diff --git a/packages/data-context/test/unit/sources/migration/codegen.spec.ts b/packages/data-context/test/unit/sources/migration/codegen.spec.ts
index 09aaf087cd5a..61b68ec2f38b 100644
--- a/packages/data-context/test/unit/sources/migration/codegen.spec.ts
+++ b/packages/data-context/test/unit/sources/migration/codegen.spec.ts
@@ -415,7 +415,7 @@ describe('reduceConfig', () => {
hasE2ESpec: false,
hasPluginsFile: false,
projectRoot: '',
- hasTypescript: false,
+ isUsingTypeScript: false,
isProjectUsingESModules: false,
shouldAddCustomE2ESpecPattern: false,
}
@@ -448,6 +448,7 @@ describe('reduceConfig', () => {
}
const newConfig = reduceConfig(config, options)
+ expect(newConfig.component.componentFolder).to.not.exist
expect(newConfig.component.specPattern).to.eq('src/**/**.cy.js')
expect(newConfig.e2e.specPattern).to.eq(`${config.e2e.integrationFolder}/${config.testFiles}`)
})
diff --git a/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js b/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js
index 6f0b40e60b57..4296e08897ee 100644
--- a/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js
+++ b/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js
@@ -1486,6 +1486,16 @@ describe('src/cy/commands/actions/type - #type special chars', () => {
cy.get('#multiple-inputs-and-button-submit input:first').type('foo{enter}')
})
+ it('triggers form submit when the submit button is outside of the form', function (done) {
+ this.$forms.find('[id="multiple-inputs-and-button-submit.outside-form"]').submit((e) => {
+ e.preventDefault()
+
+ done()
+ })
+
+ cy.get('[id="multiple-inputs-and-button-submit.outside-form"] input:first').type('foo{enter}')
+ })
+
it('causes click event on the button[type=submit]', function (done) {
this.$forms.find('#multiple-inputs-and-button-submit button[type=submit]').click((e) => {
e.preventDefault()
diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js
index 6318a04a4e1b..74a48e597732 100644
--- a/packages/driver/cypress/e2e/commands/assertions.cy.js
+++ b/packages/driver/cypress/e2e/commands/assertions.cy.js
@@ -1151,7 +1151,7 @@ describe('src/cy/commands/assertions', () => {
})
})
- context('format quotation marks', () => {
+ describe('message formatting', () => {
const expectMarkdown = (test, message, done) => {
cy.then(() => {
test()
@@ -1168,70 +1168,83 @@ describe('src/cy/commands/assertions', () => {
})
}
- it('preserves quotation marks in number strings', (done) => {
- expectMarkdown(() => {
- try {
- expect(25).to.eq('25')
- } catch (error) {} /* eslint-disable-line no-empty */
- },
- `expected **25** to equal **'25'**`,
- done)
- })
-
- it('preserves quotation marks in empty string', (done) => {
- expectMarkdown(() => {
- try {
- expect(42).to.eq('')
- } catch (error) {} /* eslint-disable-line no-empty */
- },
- `expected **42** to equal **''**`,
- done)
- })
+ // https://github.com/cypress-io/cypress/issues/19116
+ it('text with backslashes', (done) => {
+ const text = '" {
expectMarkdown(
- () => expect(`\'cypress\'`).to.eq(`\'cypress\'`),
- // ****'cypress'**** -> ** for emphasizing result string + ** for emphasizing the entire result.
- `expected **'cypress'** to equal ****'cypress'****`,
+ () => expect(text).to.equal(text),
+ `expected **" {
- expectMarkdown(
- () => {
- cy.get('body').then(($body) => {
- expect($body).to.contain('div')
- })
+ describe('messages with quotation marks', () => {
+ it('preserves quotation marks in number strings', (done) => {
+ expectMarkdown(() => {
+ try {
+ expect(25).to.eq('25')
+ } catch (error) {} /* eslint-disable-line no-empty */
},
- `expected **** to contain **div**`,
- done,
- )
- })
+ `expected **25** to equal **'25'**`,
+ done)
+ })
- it('removes quotation marks in strings', (done) => {
- expectMarkdown(() => expect('cypress').to.eq('cypress'), `expected **cypress** to equal **cypress**`, done)
- })
+ it('preserves quotation marks in empty string', (done) => {
+ expectMarkdown(() => {
+ try {
+ expect(42).to.eq('')
+ } catch (error) {} /* eslint-disable-line no-empty */
+ },
+ `expected **42** to equal **''**`,
+ done)
+ })
- it('removes quotation marks in objects', (done) => {
- expectMarkdown(
- () => expect({ foo: 'bar' }).to.deep.eq({ foo: 'bar' }),
- `expected **{ foo: bar }** to deeply equal **{ foo: bar }**`,
- done,
- )
- })
+ it('preserves quotation marks if escaped', (done) => {
+ expectMarkdown(
+ () => expect(`\'cypress\'`).to.eq(`\'cypress\'`),
+ // ****'cypress'**** -> ** for emphasizing result string + ** for emphasizing the entire result.
+ `expected **'cypress'** to equal ****'cypress'****`,
+ done,
+ )
+ })
- it('formats keys properly for "have.all.keys"', (done) => {
- const person = {
- name: 'Joe',
- age: 20,
- }
+ it('removes quotation marks in DOM elements', (done) => {
+ expectMarkdown(
+ () => {
+ cy.get('body').then(($body) => {
+ expect($body).to.contain('div')
+ })
+ },
+ `expected **** to contain **div**`,
+ done,
+ )
+ })
- expectMarkdown(
- () => expect(person).to.have.all.keys('name', 'age'),
- `expected **{ name: Joe, age: 20 }** to have keys **name**, and **age**`,
- done,
- )
+ it('removes quotation marks in strings', (done) => {
+ expectMarkdown(() => expect('cypress').to.eq('cypress'), `expected **cypress** to equal **cypress**`, done)
+ })
+
+ it('removes quotation marks in objects', (done) => {
+ expectMarkdown(
+ () => expect({ foo: 'bar' }).to.deep.eq({ foo: 'bar' }),
+ `expected **{ foo: bar }** to deeply equal **{ foo: bar }**`,
+ done,
+ )
+ })
+
+ it('formats keys properly for "have.all.keys"', (done) => {
+ const person = {
+ name: 'Joe',
+ age: 20,
+ }
+
+ expectMarkdown(
+ () => expect(person).to.have.all.keys('name', 'age'),
+ `expected **{ name: Joe, age: 20 }** to have keys **name**, and **age**`,
+ done,
+ )
+ })
})
describe('formats strings with spaces', (done) => {
@@ -1269,36 +1282,6 @@ describe('src/cy/commands/assertions', () => {
})
})
- // TODO: this suite should be merged with the suite above
- describe('message formatting', () => {
- const expectMarkdown = (test, message, done) => {
- cy.then(() => {
- test()
- })
-
- cy.on('log:added', (attrs, log) => {
- if (attrs.name === 'assert') {
- cy.removeAllListeners('log:added')
-
- expect(log.get('message')).to.eq(message)
-
- done()
- }
- })
- }
-
- // https://github.com/cypress-io/cypress/issues/19116
- it('text with backslashes', (done) => {
- const text = '" expect(text).to.equal(text),
- `expected **" {
beforeEach(function () {
this.$body = cy.$$('body')
diff --git a/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts b/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts
new file mode 100644
index 000000000000..be10eb7625df
--- /dev/null
+++ b/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts
@@ -0,0 +1,355 @@
+const SessionsManager = require('../../../../src/cy/commands/sessions/manager').default
+const $Cypress = require('../../../../src/cypress').default
+
+describe('src/cy/commands/sessions/manager.ts', () => {
+ let CypressInstance
+ let baseUrl
+
+ beforeEach(function () {
+ // @ts-ignore
+ CypressInstance = new $Cypress()
+ baseUrl = Cypress.config('baseUrl')
+ })
+
+ it('creates SessionsManager instance', () => {
+ const sessionsManager = new SessionsManager(CypressInstance, () => {})
+
+ expect(sessionsManager).to.haveOwnProperty('cy')
+ expect(sessionsManager).to.haveOwnProperty('Cypress')
+ expect(sessionsManager).to.haveOwnProperty('currentTestRegisteredSessions')
+ expect(sessionsManager.currentTestRegisteredSessions).to.be.instanceOf(Map)
+ })
+
+ describe('.setActiveSession()', () => {
+ it('adds session when none were previously added', () => {
+ const cySpy = cy.spy(cy, 'state').withArgs('activeSessions')
+
+ const activeSession: Cypress.Commands.Session.ActiveSessions = {
+ 'session_1': {
+ id: 'session_1',
+ setup: () => {},
+ hydrated: true,
+ },
+ }
+
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ sessionsManager.setActiveSession(activeSession)
+ const calls = cySpy.getCalls()
+
+ expect(cySpy).to.be.calledTwice
+ expect(calls[0].args[1]).to.be.undefined
+ expect(calls[1].args[1]).to.haveOwnProperty('session_1')
+ })
+
+ it('adds session when other sessions were previously added', () => {
+ const existingSessions: Cypress.Commands.Session.ActiveSessions = {
+ 'session_1': {
+ id: 'session_1',
+ setup: () => {},
+ hydrated: false,
+ },
+ 'session_2': {
+ id: 'session_2',
+ setup: () => {},
+ hydrated: true,
+ },
+ }
+
+ const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(existingSessions)
+
+ const activeSession: Cypress.Commands.Session.ActiveSessions = {
+ 'session_3': {
+ id: 'session_3',
+ setup: () => {},
+ hydrated: true,
+ },
+ }
+
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ sessionsManager.setActiveSession(activeSession)
+ const calls = cySpy.getCalls()
+
+ expect(cySpy).to.be.calledTwice
+ expect(calls[0].args[1]).to.be.undefined
+ expect(calls[1].args[1]).to.haveOwnProperty('session_1')
+ expect(calls[1].args[1]).to.haveOwnProperty('session_2')
+ expect(calls[1].args[1]).to.haveOwnProperty('session_3')
+ })
+ })
+
+ describe('.getActiveSession()', () => {
+ it('returns undefined when no active sessions', () => {
+ const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions')
+
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ const activeSession = sessionsManager.getActiveSession('session_1')
+
+ expect(cySpy).to.be.calledOnce
+ expect(activeSession).to.be.undefined
+ })
+
+ it('returns session when found', () => {
+ const activeSessions: Cypress.Commands.Session.ActiveSessions = {
+ 'session_1': {
+ id: 'session_1',
+ setup: () => {},
+ hydrated: false,
+ },
+ 'session_2': {
+ id: 'session_2',
+ setup: () => {},
+ hydrated: true,
+ },
+ }
+
+ const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(activeSessions)
+
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ let activeSession = sessionsManager.getActiveSession('session_1')
+
+ expect(cySpy).to.be.calledOnce
+ expect(activeSession).to.deep.eq(activeSessions['session_1'])
+ })
+ })
+
+ describe('.clearActiveSessions()', () => {
+ it('handles when no active sessions have been set', () => {
+ const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions')
+
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ sessionsManager.clearActiveSessions()
+ const calls = cySpy.getCalls()
+
+ expect(cySpy).to.be.calledTwice
+ expect(calls[1].args[1]).to.be.instanceOf(Object)
+ expect(calls[1].args[1]).to.deep.eq({})
+ })
+
+ it('updates the existing active sessions to "hydrated: false"', () => {
+ const existingSessions: Cypress.Commands.Session.ActiveSessions = {
+ 'session_1': {
+ id: 'session_1',
+ setup: () => {},
+ hydrated: false,
+ },
+ 'session_2': {
+ id: 'session_2',
+ setup: () => {},
+ hydrated: true,
+ },
+ }
+
+ const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(existingSessions)
+
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ sessionsManager.clearActiveSessions()
+ const calls = cySpy.getCalls()
+
+ expect(cySpy).to.be.calledTwice
+ expect(calls[1].args[1]).to.be.instanceOf(Object)
+ expect(calls[1].args[1]).to.haveOwnProperty('session_1')
+ expect(calls[1].args[1].session_1).to.haveOwnProperty('hydrated', false)
+ expect(calls[1].args[1]).to.haveOwnProperty('session_2')
+ expect(calls[1].args[1].session_2).to.haveOwnProperty('hydrated', false)
+ })
+ })
+
+ describe('.mapOrigins()', () => {
+ it('maps when requesting all origins', async () => {
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com']
+ const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins').resolves(allOrigins)
+
+ const origins = await sessionsManager.mapOrigins('*')
+
+ expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com'])
+ expect(sessionsSpy).to.be.calledOnce
+ })
+
+ it('maps when requesting the current origin', async () => {
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+ const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins')
+ const origins = await sessionsManager.mapOrigins('currentOrigin')
+
+ expect(origins).to.deep.eq([baseUrl])
+ expect(sessionsSpy).not.to.be.called
+ })
+
+ it('maps when requesting a specific origin', async () => {
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+ const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins')
+ const origins = await sessionsManager.mapOrigins('https://example.com/random_page?1')
+
+ expect(origins).to.deep.eq(['https://example.com'])
+ expect(sessionsSpy).not.to.be.called
+ })
+
+ it('maps when requesting a list of origins', async () => {
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com']
+ const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins').resolves(allOrigins)
+
+ const origins = await sessionsManager.mapOrigins(['*', 'https://other.com'])
+
+ expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com', 'https://other.com'])
+ expect(sessionsSpy).to.be.calledOnce
+ })
+ })
+
+ // TODO:
+ describe('._setStorageOnOrigins()', () => {})
+
+ it('.getAllHtmlOrigins()', async () => {
+ const storedOrigins = {
+ 'https://example.com': {},
+ 'https://foobar.com': {},
+ }
+
+ storedOrigins[`${baseUrl}`] = {}
+ const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins').resolves(storedOrigins)
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+
+ const origins = await sessionsManager.getAllHtmlOrigins()
+
+ expect(cypressSpy).have.been.calledOnce
+ expect(origins).to.have.lengthOf(3)
+ expect(origins).to.deep.eq(['https://example.com', 'https://foobar.com', baseUrl])
+ })
+
+ describe('.sessions', () => {
+ it('sessions.defineSession()', () => {
+ const sessionsManager = new SessionsManager(CypressInstance, cy)
+ const sessionsSpy = cy.stub(sessionsManager, 'setActiveSession')
+ const setup = cy.stub()
+ const sess = sessionsManager.sessions.defineSession({ id: '1', setup })
+
+ expect(sess).to.deep.eq({
+ id: '1',
+ setup,
+ validate: undefined,
+ cookies: null,
+ localStorage: null,
+ hydrated: false,
+ })
+
+ expect(sessionsSpy).to.be.calledOnce
+ expect(sessionsSpy.getCall(0).args[0]).to.deep.eq({ 1: sess })
+ })
+
+ it('sessions.clearAllSavedSessions()', async () => {
+ const cypressSpy = cy.stub(CypressInstance, 'backend').withArgs('clear:session').resolves(null)
+
+ const sessionsManager = new SessionsManager(CypressInstance, () => {})
+ const sessionsSpy = cy.stub(sessionsManager, 'clearActiveSessions')
+
+ await sessionsManager.sessions.clearAllSavedSessions()
+
+ expect(sessionsSpy).to.be.calledOnce
+ expect(cypressSpy).to.be.calledOnceWith('clear:session', null)
+ })
+
+ it('.clearCurrentSessionData()', async () => {
+ // Unable to cleanly mock localStorage or sessionStorage on Firefox,
+ // so add dummy values and ensure they are cleared as expected.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1141698
+ window.localStorage.foo = 'bar'
+ window.sessionStorage.jazzy = 'music'
+
+ expect(window.localStorage).of.have.lengthOf(1)
+ expect(window.sessionStorage).of.have.lengthOf(1)
+
+ const sessionsManager = new SessionsManager(CypressInstance, () => {})
+
+ const clearStorageSpy = cy.stub(sessionsManager.sessions, 'clearStorage')
+ const clearCookiesSpy = cy.stub(sessionsManager.sessions, 'clearCookies')
+
+ await sessionsManager.sessions.clearCurrentSessionData()
+
+ expect(clearStorageSpy).to.be.calledOnce
+ expect(clearCookiesSpy).to.be.calledOnce
+ expect(window.localStorage).of.have.lengthOf(0)
+ expect(window.sessionStorage).of.have.lengthOf(0)
+ })
+
+ // TODO:
+ describe('sessions.setSessionData', () => {})
+
+ it('sessions.getCookies()', async () => {
+ const cookies = [{ id: 'cookie' }]
+ const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('get:cookies').resolves(cookies)
+
+ const sessionsManager = new SessionsManager(CypressInstance, () => {})
+
+ const sessionCookies = await sessionsManager.sessions.getCookies()
+
+ expect(cypressSpy).to.be.calledOnceWith('get:cookies', {})
+ expect(sessionCookies).to.deep.eq(cookies)
+ })
+
+ it('sessions.setCookies()', async () => {
+ const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('set:cookies')
+
+ const sessionsManager = new SessionsManager(CypressInstance, () => {})
+
+ await sessionsManager.sessions.setCookies({})
+
+ expect(cypressSpy).to.be.calledOnceWith('set:cookies', {})
+ })
+
+ it('sessions.clearCookies()', async () => {
+ const cookies = [{ id: 'cookie' }]
+ const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('clear:cookies').resolves([])
+
+ const sessionsManager = new SessionsManager(CypressInstance, () => {})
+ const sessionsSpy = cy.stub(sessionsManager.sessions, 'getCookies').resolves(cookies)
+
+ await sessionsManager.sessions.clearCookies()
+
+ expect(sessionsSpy).to.be.calledOnce
+ expect(cypressSpy).to.be.calledOnceWith('clear:cookies', cookies)
+ })
+
+ it('sessions.getCurrentSessionData', async () => {
+ const sessionsManager = new SessionsManager(CypressInstance, () => {})
+ const getStorageSpy = cy.stub(sessionsManager.sessions, 'getStorage').resolves({ localStorage: [] })
+ const cookiesSpy = cy.stub(sessionsManager.sessions, 'getCookies').resolves([{ id: 'cookie' }])
+
+ const sessData = await sessionsManager.sessions.getCurrentSessionData()
+
+ expect(sessData).to.deep.eq({
+ localStorage: [],
+ cookies: [{ id: 'cookie' }],
+ })
+
+ expect(getStorageSpy).to.be.calledOnce
+ expect(cookiesSpy).to.be.calledOnce
+ })
+
+ it('sessions.getSession()', () => {
+ const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:session')
+
+ const sessionsManager = new SessionsManager(CypressInstance, () => {})
+
+ sessionsManager.sessions.getSession('session_1')
+
+ expect(cypressSpy).to.be.calledOnceWith('get:session', 'session_1')
+ })
+
+ // TODO:
+ describe('sessions.getStorage', () => {})
+
+ // TODO:
+ describe('sessions.clearStorage', () => {})
+
+ // TODO:
+ describe('sessions.setStorage', () => {})
+ })
+})
diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js
new file mode 100644
index 000000000000..fd42f3ed8ce5
--- /dev/null
+++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js
@@ -0,0 +1,956 @@
+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('session flows', () => {
+ let logs = []
+ let clearPageCount = 0
+ let sessionGroupId
+ let setup
+ let validate
+
+ const handleSetup = () => {
+ cy.then(() => {
+ expect(clearPageCount, 'cleared page before executing session setup').to.eq(1)
+ })
+
+ cy.contains('This is a blank page')
+ cy.contains('We always navigate you here after')
+ cy.contains('cy.session(...)')
+ }
+
+ before(() => {
+ setup = cy.stub().callsFake(handleSetup).as('setupSession')
+ validate = cy.stub().as('validateSession')
+ })
+
+ const resetMocks = () => {
+ logs = []
+ clearPageCount = 0
+ sessionGroupId = undefined
+ setup.reset()
+ validate.reset()
+ }
+
+ const setupTestContext = () => {
+ resetMocks()
+
+ // clear all sessions only sets hydrated: false and re-using a session id
+ // with new setup / validation fn isn't updated/applied
+ Cypress.state('activeSessions', {})
+ cy.log('Cypress.session.clearAllSavedSessions()')
+ Cypress.session.clearAllSavedSessions()
+
+ cy.on('log:added', (attrs, log) => {
+ if (attrs.name === 'session' || attrs.name === 'page load' || attrs.alias?.includes('setupSession') || attrs.alias?.includes('validateSession')) {
+ logs.push(log)
+ if (!sessionGroupId) {
+ sessionGroupId = attrs.id
+ }
+ }
+ })
+
+ cy.on('log:changed', (attrs, log) => {
+ const index = logs.findIndex((l) => l.id === attrs.id)
+
+ if (index) {
+ logs[index] = log
+ }
+ })
+
+ cy.on('internal:window:load', (args) => {
+ if (args.window.location.href === 'about:blank') {
+ clearPageCount++
+ }
+ })
+ }
+
+ describe('create session flow', () => {
+ before(() => {
+ setupTestContext()
+ cy.log('create new session to test against')
+ cy.session('session-1', setup)
+ cy.url().should('eq', 'about:blank')
+ })
+
+ it('successfully creates new session', () => {
+ expect(setup).to.be.calledOnce
+ // FIXME: currently page is cleared 3 times when it should clear 2 times
+ expect(clearPageCount, 'total times session cleared the page').to.eq(3)
+ })
+
+ it('groups session logs correctly', () => {
+ expect(logs[0].get()).to.contain({
+ name: 'session',
+ id: sessionGroupId,
+ })
+
+ expect(logs[0].get('renderProps')()).to.contain({
+ indicator: 'successful',
+ message: '(new) session-1',
+ })
+
+ expect(logs[1].get()).to.contain({
+ name: 'session',
+ message: 'session-1',
+ group: sessionGroupId,
+ })
+
+ const createNewSessionGroup = logs[2].get()
+
+ expect(createNewSessionGroup).to.contain({
+ displayName: 'Create New Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[3].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[4].get()).to.deep.contain({
+ alias: ['setupSession'],
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[5].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[6].get()).to.contain({
+ name: 'Clear Page',
+ group: sessionGroupId,
+ })
+ })
+
+ it('creates new session instrument with session details', () => {
+ const sessionInfo = logs[1].get('sessionInfo')
+
+ expect(sessionInfo).to.deep.eq({
+ id: 'session-1',
+ data: {},
+ })
+ })
+
+ it('has session details in the consoleProps', () => {
+ const consoleProps = logs[1].get('consoleProps')()
+
+ expect(consoleProps).to.deep.eq({
+ Command: 'session',
+ id: 'session-1',
+ table: [],
+ })
+ })
+ })
+
+ describe('create session with validation flow', () => {
+ before(() => {
+ setupTestContext()
+ cy.log('create new session with validation to test against')
+
+ cy.session('session-1', setup, { validate })
+ cy.url().should('eq', 'about:blank')
+ })
+
+ it('successfully creates new session and validates it', () => {
+ expect(setup).to.be.calledOnce
+ expect(validate).to.be.calledOnce
+ // FIXME: currently page is cleared 3 times when it should clear twice
+ expect(clearPageCount, 'total times session cleared the page').to.eq(3)
+ })
+
+ it('groups session logs correctly', () => {
+ expect(logs[0].get()).to.contain({
+ name: 'session',
+ id: sessionGroupId,
+ })
+
+ expect(logs[0].get('renderProps')()).to.contain({
+ indicator: 'successful',
+ message: '(new) session-1',
+ })
+
+ expect(logs[1].get()).to.contain({
+ name: 'session',
+ message: 'session-1',
+ group: sessionGroupId,
+ })
+
+ const createNewSessionGroup = logs[2].get()
+
+ expect(createNewSessionGroup).to.contain({
+ displayName: 'Create New Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[3].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[4].get()).to.deep.contain({
+ alias: ['setupSession'],
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[5].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ const validateSessionGroup = logs[6].get()
+
+ expect(validateSessionGroup).to.contain({
+ displayName: 'Validate Session: valid',
+ group: sessionGroupId,
+ })
+
+ expect(logs[7].get()).to.deep.contain({
+ alias: ['validateSession'],
+ group: validateSessionGroup.id,
+ })
+
+ expect(logs[8].get()).to.contain({
+ name: 'Clear Page',
+ group: sessionGroupId,
+ })
+ })
+ })
+
+ describe('create session with failed validation flow', () => {
+ it('fails validation and logs correctly', function (done) {
+ setupTestContext()
+ cy.log('create new session with validation to test against')
+
+ cy.once('fail', (err) => {
+ expect(setup).to.be.calledOnce
+ expect(validate).to.be.calledOnce
+ expect(clearPageCount, 'total times session cleared the page').to.eq(2)
+ expect(err.message).to.contain('Your `cy.session` **validate** callback returned false')
+
+ expect(logs[0].get()).to.contain({
+ name: 'session',
+ id: sessionGroupId,
+ })
+
+ expect(logs[0].get('renderProps')()).to.contain({
+ indicator: 'successful',
+ message: '(new) session-1',
+ })
+
+ expect(logs[1].get()).to.contain({
+ name: 'session',
+ message: 'session-1',
+ group: sessionGroupId,
+ })
+
+ const createNewSessionGroup = logs[2].get()
+
+ expect(createNewSessionGroup).to.contain({
+ displayName: 'Create New Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[3].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[4].get()).to.deep.contain({
+ alias: ['setupSession'],
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[5].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ const validateSessionGroup = logs[6].get()
+
+ expect(validateSessionGroup).to.contain({
+ displayName: 'Validate Session: invalid',
+ group: sessionGroupId,
+ })
+
+ done()
+ })
+
+ validate.callsFake(() => false)
+
+ cy.session('session-1', setup, { validate })
+ })
+ })
+
+ describe('restores saved session flow', () => {
+ before(() => {
+ setupTestContext()
+ cy.log('create new session for test')
+ cy.session('session-1', setup)
+ .then(() => {
+ // reset and only test restored session
+ resetMocks()
+ })
+
+ cy.log('restore session to test against')
+ cy.session('session-1', setup)
+ cy.url().should('eq', 'about:blank')
+ })
+
+ it('successfully restores saved session', () => {
+ expect(setup).to.not.be.called
+ expect(validate).to.not.be.called
+ expect(clearPageCount, 'total times session cleared the page').to.eq(2)
+ })
+
+ it('groups session logs correctly', () => {
+ expect(logs[0].get()).to.contain({
+ name: 'session',
+ id: sessionGroupId,
+ })
+
+ expect(logs[0].get('renderProps')()).to.contain({
+ indicator: 'pending',
+ message: '(saved) session-1',
+ })
+
+ expect(logs[1].get()).to.contain({
+ name: 'session',
+ message: 'session-1',
+ group: sessionGroupId,
+ })
+
+ const restoreSavedSessionGroup = logs[2].get()
+
+ expect(restoreSavedSessionGroup).to.contain({
+ displayName: 'Restore Saved Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[3].get()).to.contain({
+ name: 'Clear Page',
+ group: restoreSavedSessionGroup.id,
+ })
+
+ expect(logs[4].get()).to.contain({
+ name: 'Clear Page',
+ group: sessionGroupId,
+ })
+ })
+ })
+
+ describe('restores saved session with validation flow', () => {
+ before(() => {
+ setupTestContext()
+ cy.log('create new session for test')
+ cy.session('session-1', setup, { validate })
+ .then(() => {
+ // reset and only test restored session
+ resetMocks()
+ })
+
+ cy.log('restore session to test against')
+ cy.session('session-1', setup, { validate })
+ cy.url().should('eq', 'about:blank')
+ })
+
+ it('successfully restores saved session', () => {
+ expect(setup).to.not.be.called
+ expect(validate).to.be.calledOnce
+ expect(clearPageCount, 'total times session cleared the page').to.eq(2)
+ })
+
+ it('groups session logs correctly', () => {
+ expect(logs[0].get()).to.contain({
+ name: 'session',
+ id: sessionGroupId,
+ })
+
+ expect(logs[0].get('renderProps')()).to.contain({
+ indicator: 'pending',
+ message: '(saved) session-1',
+ })
+
+ expect(logs[1].get()).to.contain({
+ name: 'session',
+ message: 'session-1',
+ group: sessionGroupId,
+ })
+
+ const restoreSavedSessionGroup = logs[2].get()
+
+ expect(restoreSavedSessionGroup).to.contain({
+ displayName: 'Restore Saved Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[3].get()).to.contain({
+ name: 'Clear Page',
+ group: restoreSavedSessionGroup.id,
+ })
+
+ const validateSessionGroup = logs[4].get()
+
+ expect(validateSessionGroup).to.contain({
+ displayName: 'Validate Session: valid',
+ group: sessionGroupId,
+ })
+
+ expect(logs[5].get()).to.deep.contain({
+ alias: ['validateSession'],
+ group: validateSessionGroup.id,
+ })
+
+ expect(logs[6].get()).to.contain({
+ name: 'Clear Page',
+ group: sessionGroupId,
+ })
+ })
+ })
+
+ describe('recreates existing session flow', () => {
+ before(() => {
+ setupTestContext()
+ cy.log('create new session for test')
+ cy.session('session-1', setup, { validate })
+ .then(() => {
+ // reset and only test restored session
+ resetMocks()
+ validate.callsFake(() => {
+ if (validate.callCount === 1) {
+ return false
+ }
+ })
+ })
+
+ cy.log('restore session to test against')
+ cy.session('session-1', setup, { validate })
+ cy.url().should('eq', 'about:blank')
+ })
+
+ it('successfully recreates session', () => {
+ expect(setup).to.be.calledOnce
+ expect(validate).to.be.calledTwice
+ expect(clearPageCount, 'total times session cleared the page').to.eq(4)
+ })
+
+ it('groups session logs correctly', () => {
+ expect(logs[0].get()).to.contain({
+ name: 'session',
+ id: sessionGroupId,
+ })
+
+ expect(logs[0].get('renderProps')()).to.contain({
+ indicator: 'bad',
+ message: '(recreated) session-1',
+ })
+
+ expect(logs[1].get()).to.contain({
+ name: 'session',
+ message: 'session-1',
+ group: sessionGroupId,
+ })
+
+ const recreatedSavedSessionGroup = logs[2].get()
+
+ expect(recreatedSavedSessionGroup).to.contain({
+ displayName: 'Restore Saved Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[3].get()).to.contain({
+ name: 'Clear Page',
+ group: recreatedSavedSessionGroup.id,
+ })
+
+ const validateSessionGroup = logs[4].get()
+
+ expect(validateSessionGroup).to.contain({
+ displayName: 'Validate Session: invalid',
+ group: sessionGroupId,
+ })
+
+ expect(logs[5].get()).to.deep.contain({
+ alias: ['validateSession'],
+ group: validateSessionGroup.id,
+ })
+
+ expect(logs[6].get()).to.deep.contain({
+ showError: true,
+ group: validateSessionGroup.id,
+ })
+
+ expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.')
+
+ const createNewSessionGroup = logs[7].get()
+
+ expect(createNewSessionGroup).to.contain({
+ displayName: 'Create New Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[8].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[9].get()).to.deep.contain({
+ alias: ['setupSession'],
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[10].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ const secondValidateSessionGroup = logs[11].get()
+
+ expect(secondValidateSessionGroup).to.contain({
+ displayName: 'Validate Session: valid',
+ group: sessionGroupId,
+ })
+
+ expect(logs[12].get()).to.deep.contain({
+ alias: ['validateSession'],
+ group: secondValidateSessionGroup.id,
+ })
+
+ expect(logs[13].get()).to.contain({
+ name: 'Clear Page',
+ group: sessionGroupId,
+ })
+ })
+ })
+
+ describe('recreates existing session with failed validation flow', () => {
+ it('fails to recreate session and logs correctly', function (done) {
+ setupTestContext()
+ cy.log('create new session for test')
+ cy.session('session-1', setup, { validate })
+ .then(() => {
+ // reset and only test restored session
+ resetMocks()
+ validate.callsFake(() => false)
+ })
+
+ cy.once('fail', (err) => {
+ expect(err.message).to.contain('Your `cy.session` **validate** callback returned false')
+ expect(setup).to.be.calledOnce
+ expect(validate).to.be.calledTwice
+ expect(clearPageCount, 'total times session cleared the page').to.eq(3)
+
+ expect(logs[0].get()).to.contain({
+ name: 'session',
+ id: sessionGroupId,
+ })
+
+ expect(logs[0].get('renderProps')()).to.contain({
+ indicator: 'bad',
+ message: '(recreated) session-1',
+ })
+
+ expect(logs[1].get()).to.contain({
+ name: 'session',
+ message: 'session-1',
+ group: sessionGroupId,
+ })
+
+ const recreatedSavedSessionGroup = logs[2].get()
+
+ expect(recreatedSavedSessionGroup).to.contain({
+ displayName: 'Restore Saved Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[3].get()).to.contain({
+ name: 'Clear Page',
+ group: recreatedSavedSessionGroup.id,
+ })
+
+ const validateSessionGroup = logs[4].get()
+
+ expect(validateSessionGroup).to.contain({
+ displayName: 'Validate Session: invalid',
+ group: sessionGroupId,
+ })
+
+ expect(logs[5].get()).to.deep.contain({
+ alias: ['validateSession'],
+ group: validateSessionGroup.id,
+ })
+
+ expect(logs[6].get()).to.deep.contain({
+ showError: true,
+ group: validateSessionGroup.id,
+ })
+
+ expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.')
+
+ const createNewSessionGroup = logs[7].get()
+
+ expect(createNewSessionGroup).to.contain({
+ displayName: 'Create New Session',
+ groupStart: true,
+ group: sessionGroupId,
+ })
+
+ expect(logs[8].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[9].get()).to.deep.contain({
+ alias: ['setupSession'],
+ group: createNewSessionGroup.id,
+ })
+
+ expect(logs[10].get()).to.contain({
+ name: 'Clear Page',
+ group: createNewSessionGroup.id,
+ })
+
+ const secondValidateSessionGroup = logs[11].get()
+
+ expect(secondValidateSessionGroup).to.contain({
+ displayName: 'Validate Session: invalid',
+ group: sessionGroupId,
+ })
+
+ expect(logs[12].get()).to.deep.contain({
+ alias: ['validateSession'],
+ group: secondValidateSessionGroup.id,
+ })
+
+ done()
+ })
+
+ cy.log('restore session to test against')
+ cy.session('session-1', setup, { validate })
+ })
+ })
+ })
+
+ 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/cypress/fixtures/dom.html b/packages/driver/cypress/fixtures/dom.html
index 6347924ceccf..819fc05167a9 100644
--- a/packages/driver/cypress/fixtures/dom.html
+++ b/packages/driver/cypress/fixtures/dom.html
@@ -323,6 +323,12 @@
+
+
+