diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 29467b57c53c..c9a09bc1386f 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -7,6 +7,7 @@ _Released 03/1/2023 (PENDING)_ - It is now possible to set `hostOnly` cookies with [`cy.setCookie()`](https://docs.cypress.io/api/commands/setcookie) for a given domain. Addresses [#16856](https://github.com/cypress-io/cypress/issues/16856) and [#17527](https://github.com/cypress-io/cypress/issues/17527). - Added a Public API for third party component libraries to define a Framework Definition, embedding their library into the Cypress onboarding workflow. Learn more [here](https://docs.cypress.io/guides/component-testing/third-party-definitions). Implemented in [#25780](https://github.com/cypress-io/cypress/pull/25780) and closes [#25638](https://github.com/cypress-io/cypress/issues/25638). +- Added a Debug Page tutorial slideshow for projects that are not connected to Cypress Cloud. Addresses [#25768](https://github.com/cypress-io/cypress/issues/25768). **Bugfixes:** diff --git a/packages/app/package.json b/packages/app/package.json index f3617fcc74a1..b86150e520f4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -20,8 +20,8 @@ }, "dependencies": {}, "devDependencies": { - "@cypress-design/vue-icon": "^0.15.0", - "@cypress-design/vue-statusicon": "0.2.1", + "@cypress-design/vue-icon": "^0.18.0", + "@cypress-design/vue-statusicon": "0.2.4", "@graphql-typed-document-node/core": "^3.1.0", "@headlessui/vue": "1.4.0", "@iconify/iconify": "2.1.2", diff --git a/packages/app/src/assets/debug-guide-skeleton-1.png b/packages/app/src/assets/debug-guide-skeleton-1.png new file mode 100644 index 000000000000..2abb50e0bb50 Binary files /dev/null and b/packages/app/src/assets/debug-guide-skeleton-1.png differ diff --git a/packages/app/src/assets/debug-guide-skeleton-2.png b/packages/app/src/assets/debug-guide-skeleton-2.png new file mode 100644 index 000000000000..44d36ccbe60b Binary files /dev/null and b/packages/app/src/assets/debug-guide-skeleton-2.png differ diff --git a/packages/app/src/assets/debug-guide-skeleton-3.png b/packages/app/src/assets/debug-guide-skeleton-3.png new file mode 100644 index 000000000000..9b7957130f2f Binary files /dev/null and b/packages/app/src/assets/debug-guide-skeleton-3.png differ diff --git a/packages/app/src/assets/debug-guide-text-1.png b/packages/app/src/assets/debug-guide-text-1.png new file mode 100644 index 000000000000..0c3dbeecffda Binary files /dev/null and b/packages/app/src/assets/debug-guide-text-1.png differ diff --git a/packages/app/src/assets/debug-guide-text-2.png b/packages/app/src/assets/debug-guide-text-2.png new file mode 100644 index 000000000000..4a7dd371a1cd Binary files /dev/null and b/packages/app/src/assets/debug-guide-text-2.png differ diff --git a/packages/app/src/assets/debug-guide-text-3.png b/packages/app/src/assets/debug-guide-text-3.png new file mode 100644 index 000000000000..3a1928737037 Binary files /dev/null and b/packages/app/src/assets/debug-guide-text-3.png differ diff --git a/packages/app/src/components/Slideshow.vue b/packages/app/src/components/Slideshow.vue new file mode 100644 index 000000000000..2d08e039316c --- /dev/null +++ b/packages/app/src/components/Slideshow.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/app/src/debug/DebugContainer.cy.tsx b/packages/app/src/debug/DebugContainer.cy.tsx index 797b029faad5..9c2b5c2869fe 100644 --- a/packages/app/src/debug/DebugContainer.cy.tsx +++ b/packages/app/src/debug/DebugContainer.cy.tsx @@ -1,9 +1,10 @@ -import { DebugSpecListGroupsFragment, DebugSpecListSpecFragment, DebugSpecListTestsFragment, DebugSpecsFragmentDoc } from '../generated/graphql-test' +import { DebugSpecListGroupsFragment, DebugSpecListSpecFragment, DebugSpecListTestsFragment, DebugSpecsFragmentDoc, UseCohorts_DetermineCohortDocument } from '../generated/graphql-test' import DebugContainer from './DebugContainer.vue' import { defaultMessages } from '@cy/i18n' import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' import { specsList } from './utils/DebugMapping' import { CloudRunStubs } from '@packages/graphql/test/stubCloudTypes' +import { DEBUG_SLIDESHOW } from './utils/constants' const DebugSpecVariableTypes = { hasNextRun: 'Boolean', @@ -22,6 +23,10 @@ describe('', () => { describe('empty states', () => { const validateEmptyState = (expectedMessages: string[]) => { + cy.stubMutationResolver(UseCohorts_DetermineCohortDocument, (defineResult) => { + return defineResult({ determineCohort: { __typename: 'Cohort', name: DEBUG_SLIDESHOW.id, cohort: 'A' } }) + }) + cy.mountFragment(DebugSpecsFragmentDoc, { variableTypes: DebugSpecVariableTypes, variables: { @@ -29,6 +34,13 @@ describe('', () => { runNumber: 1, nextRunNumber: -1, }, + onResult: (res) => { + if (res.currentProject) { + res.currentProject.savedState = { + debugSlideshowComplete: true, + } + } + }, render: (gqlVal) => , }) diff --git a/packages/app/src/debug/DebugContainer.vue b/packages/app/src/debug/DebugContainer.vue index 01cbacbdc756..bf393c9d1b4a 100644 --- a/packages/app/src/debug/DebugContainer.vue +++ b/packages/app/src/debug/DebugContainer.vue @@ -163,6 +163,7 @@ fragment DebugSpecs on Query { } currentTestingType } + ..._DebugEmptyView } ` diff --git a/packages/app/src/debug/empty/DebugEmptyStates.cy.tsx b/packages/app/src/debug/empty/DebugEmptyStates.cy.tsx index 15339fe3adfe..f07e42d1b02d 100644 --- a/packages/app/src/debug/empty/DebugEmptyStates.cy.tsx +++ b/packages/app/src/debug/empty/DebugEmptyStates.cy.tsx @@ -4,6 +4,58 @@ import DebugNoRuns from './DebugNoRuns.vue' import DebugLoading from './DebugLoading.vue' import DebugError from './DebugError.vue' import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' +import { DebugEmptyView_RecordEventDocument, DebugEmptyView_SetPreferencesDocument, UseCohorts_DetermineCohortDocument, _DebugEmptyViewFragment, _DebugEmptyViewFragmentDoc } from '../../generated/graphql-test' +import { DEBUG_SLIDESHOW } from '../utils/constants' + +function mountWithGql (component: JSX.Element, gqlOptions?: { debugSlideshowComplete?: boolean, cohort?: 'A' | 'B' }) { + let gql: _DebugEmptyViewFragment + const opts = { debugSlideshowComplete: true, cohort: 'A', ...gqlOptions } + + cy.stubMutationResolver(UseCohorts_DetermineCohortDocument, (defineResult) => { + return defineResult({ determineCohort: { __typename: 'Cohort', name: DEBUG_SLIDESHOW.id, cohort: opts.cohort } }) + }) + + const recordEvent = cy.stub().as('recordEvent') + const storeSlideshowComplete = cy.stub().as('storeSlideshowComplete') + + cy.stubMutationResolver(DebugEmptyView_RecordEventDocument, (defineResult, args) => { + recordEvent(args) + + return defineResult({ recordEvent: true }) + }) + + cy.stubMutationResolver(DebugEmptyView_SetPreferencesDocument, (defineResult, args) => { + storeSlideshowComplete(args) + + return defineResult({ + setPreferences: { + __typename: 'Query', + currentProject: { + __typename: 'CurrentProject', + id: gql.currentProject?.id!, + savedState: { + debugSlideshowComplete: true, + }, + }, + }, + }) + }) + + cy.mountFragment(_DebugEmptyViewFragmentDoc, { + onResult: (res) => { + if (res.currentProject) { + res.currentProject.savedState = { + debugSlideshowComplete: opts.debugSlideshowComplete, + } + + gql = res + } + }, + render: () => { + return component + }, + }) +} describe('Debug page empty states', () => { context('not logged in', () => { @@ -13,12 +65,18 @@ describe('Debug page empty states', () => { // We need to set isLoggedIn so that CloudConnectButton shows the correct state loginConnectStore.setUserFlag('isLoggedIn', false) - cy.mount() + mountWithGql() cy.findByRole('link', { name: 'Learn about debugging CI failures in Cypress' }).should('have.attr', 'href', 'https://on.cypress.io/debug-page?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More') cy.percySnapshot() }) + + it('sends record event upon seeing slideshow', () => { + useLoginConnectStore().setUserFlag('isLoggedIn', false) + mountWithGql(, { debugSlideshowComplete: false }) + cy.get('@recordEvent').should('have.been.calledWithMatch', { campaign: DEBUG_SLIDESHOW.campaigns.login }) + }) }) context('no project', () => { @@ -28,31 +86,43 @@ describe('Debug page empty states', () => { // We need to set isLoggedIn so that CloudConnectButton shows the correct state loginConnectStore.setUserFlag('isLoggedIn', true) - cy.mount() + mountWithGql() cy.findByRole('link', { name: 'Learn about project setup in Cypress' }).should('have.attr', 'href', 'https://on.cypress.io/adding-new-project?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More') cy.percySnapshot() - cy.viewport(600, 600) + cy.viewport(700, 700) cy.percySnapshot('responsive') }) + + it('sends record event upon seeing slideshow', () => { + useLoginConnectStore().setUserFlag('isLoggedIn', false) + mountWithGql(, { debugSlideshowComplete: false }) + cy.get('@recordEvent').should('have.been.calledWithMatch', { campaign: DEBUG_SLIDESHOW.campaigns.connectProject }) + }) }) context('no runs', () => { it('renders', () => { - cy.mount() + mountWithGql() cy.findByRole('link', { name: 'Learn about recording a run to Cypress Cloud' }).should('have.attr', 'href', 'https://on.cypress.io/cypress-run-record-key?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More') cy.percySnapshot() }) + + it('sends record event upon seeing slideshow', () => { + useLoginConnectStore().setUserFlag('isLoggedIn', false) + mountWithGql(, { debugSlideshowComplete: false }) + cy.get('@recordEvent').should('have.been.calledWithMatch', { campaign: DEBUG_SLIDESHOW.campaigns.recordRun }) + }) }) context('loading', () => { it('renders', () => { - cy.mount() + mountWithGql() cy.percySnapshot() }) @@ -60,11 +130,63 @@ describe('Debug page empty states', () => { context('error', () => { it('renders', () => { - cy.mount() + mountWithGql() cy.findByRole('link', { name: 'Learn about debugging CI failures in Cypress' }).should('have.attr', 'href', 'https://on.cypress.io/debug-page?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More') cy.percySnapshot() }) + + it('does not render slideshow on error page', () => { + mountWithGql(, { debugSlideshowComplete: false }) + cy.get('@recordEvent').should('not.have.been.called') + cy.findByTestId('debug-default-empty-state').should('be.visible') + cy.findByTestId('debug-slideshow-slide').should('not.exist') + }) + }) + + context('slideshow', () => { + function moveThroughSlideshow (options: { cohort: 'A' | 'B', percy?: boolean }) { + for (const step of [1, 2, 3]) { + const { title, description } = cy.i18n.debugPage.emptyStates.slideshow[`step${step}`] + const imageSrc = `debug-guide-${options.cohort === 'A' ? 'skeleton' : 'text'}-${step}.png` + + cy.findByAltText('Debug tutorial').should('have.attr', 'src').should('include', imageSrc) + cy.contains('h2', title) + cy.contains('p', description) + cy.findByTestId('debug-slideshow-step').contains(`${step}/3`) + cy.contains('button', 'Previous').should(step === 1 ? 'not.exist' : 'be.visible') + if (options.percy) { + cy.percySnapshot(`slideshow step ${step}`) + } + + cy.contains('button', step === 3 ? 'Done' : 'Next').click() + } + + cy.findByTestId('debug-slideshow-slide').should('not.exist') + } + + it('renders slideshow if debugSlideshowComplete = false', () => { + useLoginConnectStore().setUserFlag('isLoggedIn', false) + mountWithGql(, { cohort: 'B', debugSlideshowComplete: false }) + cy.get('@recordEvent').should('have.been.calledWithMatch', { campaign: DEBUG_SLIDESHOW.campaigns.recordRun }) + moveThroughSlideshow({ cohort: 'B', percy: true }) + cy.get('@storeSlideshowComplete').should('have.been.called') + + // Can still move through slideshow, does not record events after completion + cy.contains('button', 'Info').click() + cy.get('@recordEvent').should('not.have.been.calledTwice') + moveThroughSlideshow({ cohort: 'B' }) + cy.get('@storeSlideshowComplete').should('not.have.been.calledTwice') + }) + + it('renders default empty state if debugSlideshowComplete = true', () => { + useLoginConnectStore().setUserFlag('isLoggedIn', false) + mountWithGql(, { cohort: 'A', debugSlideshowComplete: true }) + cy.findByTestId('debug-default-empty-state') + + cy.contains('button', 'Info').click() + moveThroughSlideshow({ cohort: 'A', percy: true }) + }) }) }) diff --git a/packages/app/src/debug/empty/DebugEmptyView.vue b/packages/app/src/debug/empty/DebugEmptyView.vue index e83be1670183..b169f3fd826b 100644 --- a/packages/app/src/debug/empty/DebugEmptyView.vue +++ b/packages/app/src/debug/empty/DebugEmptyView.vue @@ -1,5 +1,5 @@ + + diff --git a/packages/app/src/debug/empty/DebugNoProject.vue b/packages/app/src/debug/empty/DebugNoProject.vue index 8689b49889a1..125351dde6ea 100644 --- a/packages/app/src/debug/empty/DebugNoProject.vue +++ b/packages/app/src/debug/empty/DebugNoProject.vue @@ -4,6 +4,7 @@ :description="t('debugPage.emptyStates.reviewRerunAndDebug')" :example-test-name="t('debugPage.emptyStates.noProjectTestMessage')" :help-link-text="t('debugPage.emptyStates.learnAboutProjectSetup')" + :slideshow-campaign="DEBUG_SLIDESHOW.campaigns.connectProject" help-link-href="https://on.cypress.io/adding-new-project" >