feat: Component Testing banner (#26625)
Co-authored-by: elevatebart <bart@cypress.io>
@@ -3,6 +3,10 @@
|
||||
|
||||
_Released 05/09/2023 (PENDING)_
|
||||
|
||||
**Features:**
|
||||
|
||||
- Added a new informational banner to help get started with component testing from an existing end-to-end test suite. Addresses [#26511](https://github.com/cypress-io/cypress/issues/26511).
|
||||
|
||||
**Bugfixes:**
|
||||
|
||||
- Fixed an issue in Electron where devtools gets out of sync with the DOM occasionally. Addresses [#15932](https://github.com/cypress-io/cypress/issues/15932).
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@cypress-design/vue-icon": "0.20.0",
|
||||
"@cypress-design/vue-statusicon": "0.3.0",
|
||||
"@cypress-design/vue-button": "0.9.2",
|
||||
"@cypress-design/vue-icon": "0.22.1",
|
||||
"@cypress-design/vue-statusicon": "0.4.3",
|
||||
"@graphql-typed-document-node/core": "^3.1.0",
|
||||
"@headlessui/vue": "1.4.0",
|
||||
"@iconify/iconify": "2.1.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { gql, useSubscription } from '@urql/vue'
|
||||
import { Debug_RelevantRuns_SubscriptionDocument, Sidebar_RelevantRuns_SubscriptionDocument } from '@packages/app/src/generated/graphql'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { uniq } from 'lodash'
|
||||
@@ -43,10 +43,10 @@ gql`
|
||||
`
|
||||
|
||||
export function useRelevantRun (location: 'SIDEBAR' | 'DEBUG') {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
const shouldPause = computed(() => {
|
||||
return !loginConnectStore.project.isProjectConnected
|
||||
return !userProjectStatusStore.project.isProjectConnected
|
||||
})
|
||||
|
||||
//Switch the subscription query depending on where it was registered from
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DebugSpecListGroupsFragment, DebugSpecListSpecFragment, DebugSpecListTestsFragment, DebugSpecsFragment, 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 { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
import { specsList } from './utils/DebugMapping'
|
||||
import { CloudRunStubs, createCloudRun } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { DEBUG_SLIDESHOW } from './utils/constants'
|
||||
@@ -51,31 +51,31 @@ describe('<DebugContainer />', () => {
|
||||
}
|
||||
|
||||
it('shows not logged in', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setHasInitiallyLoaded()
|
||||
userProjectStatusStore.setHasInitiallyLoaded()
|
||||
|
||||
validateEmptyState([defaultMessages.debugPage.emptyStates.connectToCypressCloud, defaultMessages.debugPage.emptyStates.debugDirectlyInCypress, defaultMessages.debugPage.emptyStates.notLoggedInTestMessage])
|
||||
cy.findByRole('button', { name: 'Connect to Cypress Cloud' }).should('be.visible')
|
||||
})
|
||||
|
||||
it('is logged in with no project', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setProjectFlag('isProjectConnected', false)
|
||||
loginConnectStore.setHasInitiallyLoaded()
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', false)
|
||||
userProjectStatusStore.setHasInitiallyLoaded()
|
||||
|
||||
validateEmptyState([defaultMessages.debugPage.emptyStates.debugDirectlyInCypress, defaultMessages.debugPage.emptyStates.reviewRerunAndDebug, defaultMessages.debugPage.emptyStates.noProjectTestMessage])
|
||||
cy.findByRole('button', { name: 'Connect a Cypress Cloud project' }).should('be.visible')
|
||||
})
|
||||
|
||||
it('has no runs', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setProjectFlag('isProjectConnected', true)
|
||||
loginConnectStore.setHasInitiallyLoaded()
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', true)
|
||||
userProjectStatusStore.setHasInitiallyLoaded()
|
||||
cy.mountFragment(DebugSpecsFragmentDoc, {
|
||||
variableTypes: DebugSpecVariableTypes,
|
||||
variables: defaultVariables,
|
||||
@@ -87,11 +87,11 @@ describe('<DebugContainer />', () => {
|
||||
})
|
||||
|
||||
it('errors', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setProjectFlag('isProjectConnected', true)
|
||||
loginConnectStore.setHasInitiallyLoaded()
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', true)
|
||||
userProjectStatusStore.setHasInitiallyLoaded()
|
||||
cy.mountFragment(DebugSpecsFragmentDoc, {
|
||||
variableTypes: DebugSpecVariableTypes,
|
||||
variables: defaultVariables,
|
||||
@@ -105,11 +105,11 @@ describe('<DebugContainer />', () => {
|
||||
|
||||
describe('run states', { viewportWidth: 900 }, () => {
|
||||
beforeEach(() => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setProjectFlag('isProjectConnected', true)
|
||||
loginConnectStore.setHasInitiallyLoaded()
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', true)
|
||||
userProjectStatusStore.setHasInitiallyLoaded()
|
||||
})
|
||||
|
||||
function mountTestRun (runName: string) {
|
||||
@@ -229,14 +229,14 @@ describe('<DebugContainer />', () => {
|
||||
})
|
||||
|
||||
describe('when logged in and connected', () => {
|
||||
let loginConnectStore
|
||||
let userProjectStatusStore
|
||||
|
||||
beforeEach(() => {
|
||||
loginConnectStore = useLoginConnectStore()
|
||||
userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setProjectFlag('isProjectConnected', true)
|
||||
loginConnectStore.setHasInitiallyLoaded()
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', true)
|
||||
userProjectStatusStore.setHasInitiallyLoaded()
|
||||
})
|
||||
|
||||
it('renders running run', () => {
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
<NoInternetConnection v-if="!online">
|
||||
{{ t('launchpadErrors.noInternet.connectProject') }}
|
||||
</NoInternetConnection>
|
||||
<DebugLoading v-else-if="!loginConnectStore.hasInitiallyLoaded || loginConnectStore.project.isProjectConnected && isLoading" />
|
||||
<DebugLoading v-else-if="!userProjectStatusStore.hasInitiallyLoaded || userProjectStatusStore.project.isProjectConnected && isLoading" />
|
||||
<DebugError
|
||||
v-else-if="showError"
|
||||
/>
|
||||
<DebugNotLoggedIn
|
||||
v-else-if="!loginConnectStore.user.isLoggedIn"
|
||||
v-else-if="!userProjectStatusStore.user.isLoggedIn"
|
||||
data-cy="debug-empty"
|
||||
/>
|
||||
<DebugNoProject
|
||||
v-else-if="!loginConnectStore.project.isProjectConnected"
|
||||
v-else-if="!userProjectStatusStore.project.isProjectConnected"
|
||||
data-cy="debug-empty"
|
||||
/>
|
||||
<DebugNoRuns
|
||||
@@ -85,7 +85,7 @@
|
||||
import { gql } from '@urql/vue'
|
||||
import { computed } from 'vue'
|
||||
import type { CloudRunStatus, DebugSpecsFragment, TestingTypeEnum } from '../generated/graphql'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
import NoInternetConnection from '@packages/frontend-shared/src/components/NoInternetConnection.vue'
|
||||
import DebugLoading from '../debug/empty/DebugLoading.vue'
|
||||
import DebugPageHeader from './DebugPageHeader.vue'
|
||||
@@ -205,7 +205,7 @@ const props = withDefaults(defineProps<{
|
||||
currentCommitInfo: undefined,
|
||||
})
|
||||
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
const cloudProject = computed(() => {
|
||||
return props.gql?.currentProject?.cloudProject?.__typename === 'CloudProject'
|
||||
|
||||
@@ -4,7 +4,7 @@ import DebugNoRuns from './DebugNoRuns.vue'
|
||||
import DebugLoading from './DebugLoading.vue'
|
||||
import DebugError from './DebugError.vue'
|
||||
import DebugEmptyView from './DebugEmptyView.vue'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
import { DebugEmptyView_RecordEventDocument, DebugEmptyView_SetPreferencesDocument, UseCohorts_DetermineCohortDocument, _DebugEmptyViewFragment, _DebugEmptyViewFragmentDoc } from '../../generated/graphql-test'
|
||||
import { DEBUG_SLIDESHOW } from '../utils/constants'
|
||||
|
||||
@@ -77,10 +77,10 @@ describe('Debug page empty states', () => {
|
||||
|
||||
context('not logged in', () => {
|
||||
it('renders', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
// We need to set isLoggedIn so that CloudConnectButton shows the correct state
|
||||
loginConnectStore.setUserFlag('isLoggedIn', false)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', false)
|
||||
|
||||
mountWithGql(<DebugNotLoggedIn />)
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('Debug page empty states', () => {
|
||||
})
|
||||
|
||||
it('sends record event upon seeing slideshow', () => {
|
||||
useLoginConnectStore().setUserFlag('isLoggedIn', false)
|
||||
useUserProjectStatusStore().setUserFlag('isLoggedIn', false)
|
||||
mountWithGql(<DebugNotLoggedIn />, { debugSlideshowComplete: false })
|
||||
cy.get('@recordEvent').should('have.been.calledWithMatch', { campaign: DEBUG_SLIDESHOW.campaigns.login, messageId: Cypress.sinon.match.string, medium: DEBUG_SLIDESHOW.medium, cohort: Cypress.sinon.match(/A|B/) })
|
||||
})
|
||||
@@ -98,10 +98,10 @@ describe('Debug page empty states', () => {
|
||||
|
||||
context('no project', () => {
|
||||
it('renders', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
// We need to set isLoggedIn so that CloudConnectButton shows the correct state
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
|
||||
mountWithGql(<DebugNoProject />)
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('Debug page empty states', () => {
|
||||
})
|
||||
|
||||
it('sends record event upon seeing slideshow', () => {
|
||||
useLoginConnectStore().setUserFlag('isLoggedIn', false)
|
||||
useUserProjectStatusStore().setUserFlag('isLoggedIn', false)
|
||||
mountWithGql(<DebugNoProject />, { debugSlideshowComplete: false })
|
||||
cy.get('@recordEvent').should('have.been.calledWithMatch', { campaign: DEBUG_SLIDESHOW.campaigns.connectProject, messageId: Cypress.sinon.match.string, medium: DEBUG_SLIDESHOW.medium, cohort: Cypress.sinon.match(/A|B/) })
|
||||
})
|
||||
@@ -131,7 +131,7 @@ describe('Debug page empty states', () => {
|
||||
})
|
||||
|
||||
it('sends record event upon seeing slideshow', () => {
|
||||
useLoginConnectStore().setUserFlag('isLoggedIn', false)
|
||||
useUserProjectStatusStore().setUserFlag('isLoggedIn', false)
|
||||
mountWithGql(<DebugNoRuns />, { debugSlideshowComplete: false })
|
||||
cy.get('@recordEvent').should('have.been.calledWithMatch', { campaign: DEBUG_SLIDESHOW.campaigns.recordRun, messageId: Cypress.sinon.match.string, medium: DEBUG_SLIDESHOW.medium, cohort: Cypress.sinon.match(/A|B/) })
|
||||
})
|
||||
@@ -184,7 +184,7 @@ describe('Debug page empty states', () => {
|
||||
}
|
||||
|
||||
it('renders slideshow if debugSlideshowComplete = false', () => {
|
||||
useLoginConnectStore().setUserFlag('isLoggedIn', false)
|
||||
useUserProjectStatusStore().setUserFlag('isLoggedIn', false)
|
||||
mountWithGql(<DebugNoRuns />, { cohort: 'B', debugSlideshowComplete: false })
|
||||
cy.get('@recordEvent').should('have.been.calledWithMatch', { campaign: DEBUG_SLIDESHOW.campaigns.recordRun, messageId: Cypress.sinon.match.string, medium: DEBUG_SLIDESHOW.medium, cohort: Cypress.sinon.match(/A|B/) })
|
||||
moveThroughSlideshow({ cohort: 'B', percy: true })
|
||||
@@ -198,7 +198,7 @@ describe('Debug page empty states', () => {
|
||||
})
|
||||
|
||||
it('renders default empty state if debugSlideshowComplete = true', () => {
|
||||
useLoginConnectStore().setUserFlag('isLoggedIn', false)
|
||||
useUserProjectStatusStore().setUserFlag('isLoggedIn', false)
|
||||
mountWithGql(<DebugNoRuns />, { cohort: 'A', debugSlideshowComplete: true })
|
||||
cy.findByTestId('debug-default-empty-state')
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defaultMessages } from '@cy/i18n'
|
||||
import { CloudRunStatus, SidebarNavigationFragment, SidebarNavigationFragmentDoc, SideBarNavigation_SetPreferencesDocument } from '../generated/graphql-test'
|
||||
import { CloudRunStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
function mountComponent (props: { initialNavExpandedVal?: boolean, cloudProject?: { status: CloudRunStatus, numFailedTests: number }, isLoading?: boolean, online?: boolean} = {}) {
|
||||
const withDefaults = { initialNavExpandedVal: false, isLoading: false, online: true, ...props }
|
||||
@@ -186,9 +186,9 @@ describe('SidebarNavigation', () => {
|
||||
})
|
||||
|
||||
it('renders no badge when query is loading', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setProjectFlag('isProjectConnected', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', true)
|
||||
|
||||
mountComponent({ isLoading: true })
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ import { useI18n } from '@cy/i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SidebarNavigationHeader from './SidebarNavigationHeader.vue'
|
||||
import { useDebounceFn, useWindowSize } from '@vueuse/core'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -162,7 +162,7 @@ const props = defineProps<{
|
||||
|
||||
const NAV_EXPAND_MIN_SCREEN_WIDTH = 1024
|
||||
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
const debugBadge = ref<Badge | undefined>()
|
||||
|
||||
@@ -171,7 +171,7 @@ const setDebugBadge = useDebounceFn((badge) => {
|
||||
}, 500)
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.isLoading && loginConnectStore.project.isProjectConnected) {
|
||||
if (props.isLoading && userProjectStatusStore.project.isProjectConnected) {
|
||||
setDebugBadge(undefined)
|
||||
|
||||
return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import CloudConnectButton from './CloudConnectButton.vue'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
describe('<CloudConnectButton />', { viewportHeight: 60, viewportWidth: 400 }, () => {
|
||||
context('not logged in ', () => {
|
||||
@@ -11,12 +11,12 @@ describe('<CloudConnectButton />', { viewportHeight: 60, viewportWidth: 400 }, (
|
||||
})
|
||||
|
||||
context('logged in', () => {
|
||||
let loginConnectStore
|
||||
let userProjectStatusStore
|
||||
|
||||
beforeEach(() => {
|
||||
loginConnectStore = useLoginConnectStore()
|
||||
userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
})
|
||||
|
||||
it('show project connect if not connected', () => {
|
||||
@@ -26,7 +26,7 @@ describe('<CloudConnectButton />', { viewportHeight: 60, viewportWidth: 400 }, (
|
||||
})
|
||||
|
||||
it('uses the store to open the Login Connect modal', () => {
|
||||
loginConnectStore.openLoginConnectModal = cy.spy().as('openLoginConnectModal')
|
||||
userProjectStatusStore.openLoginConnectModal = cy.spy().as('openLoginConnectModal')
|
||||
cy.mount(() => <div class="h-screen"><CloudConnectButton utmMedium="testing" /></div>)
|
||||
|
||||
cy.contains('button', 'Connect a Cypress Cloud project').click()
|
||||
@@ -35,7 +35,7 @@ describe('<CloudConnectButton />', { viewportHeight: 60, viewportWidth: 400 }, (
|
||||
})
|
||||
|
||||
it('uses the store to open the Login Connect modal with utmContent', () => {
|
||||
loginConnectStore.openLoginConnectModal = cy.spy().as('openLoginConnectModal')
|
||||
userProjectStatusStore.openLoginConnectModal = cy.spy().as('openLoginConnectModal')
|
||||
cy.mount(() => <div class="h-screen"><CloudConnectButton utmMedium="testing" utmContent="content"/></div>)
|
||||
|
||||
cy.contains('button', 'Connect a Cypress Cloud project').click()
|
||||
|
||||
@@ -15,9 +15,9 @@ import ChainIcon from '~icons/cy/chain-link_x16.svg'
|
||||
import CypressIcon from '~icons/cy/cypress-logo_x16.svg'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const { openLoginConnectModal, user } = useLoginConnectStore()
|
||||
const { openLoginConnectModal, user } = useUserProjectStatusStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import RunsContainer from './RunsContainer.vue'
|
||||
import { RunsContainerFragmentDoc } from '../generated/graphql-test'
|
||||
import { CloudUserStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
|
||||
@@ -16,9 +16,9 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
|
||||
|
||||
context('when the user is logged in', () => {
|
||||
beforeEach(() => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
})
|
||||
|
||||
it('renders with expected runs if there is a cloud project id', () => {
|
||||
|
||||
@@ -52,7 +52,7 @@ import RunsEmpty from './RunsEmpty.vue'
|
||||
import { RunsContainerFragment, RunsContainer_FetchNewerRunsDocument } from '../generated/graphql'
|
||||
import Warning from '@packages/frontend-shared/src/warning/Warning.vue'
|
||||
import RunsErrorRenderer from './RunsErrorRenderer.vue'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -188,9 +188,9 @@ const props = defineProps<{
|
||||
const showConnectSuccessAlert = ref(false)
|
||||
const connectionFailed = computed(() => !props.gql.currentProject?.cloudProject && props.online)
|
||||
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
watch(() => loginConnectStore.project.isProjectConnected, (newVal, oldVal) => {
|
||||
watch(() => userProjectStatusStore.project.isProjectConnected, (newVal, oldVal) => {
|
||||
if (newVal && oldVal === false) {
|
||||
// only show this alert if we have just connected
|
||||
showConnectSuccessAlert.value = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RunsErrorRendererFragmentDoc } from '../generated/graphql-test'
|
||||
import RunsErrorRenderer from './RunsErrorRenderer.vue'
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const text = defaultMessages.runs.errors
|
||||
|
||||
@@ -34,9 +34,9 @@ describe('<RunsErrorRenderer />', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
cy.spy(loginConnectStore, 'openLoginConnectModal').as('loginConnectSpy')
|
||||
cy.spy(userProjectStatusStore, 'openLoginConnectModal').as('loginConnectSpy')
|
||||
|
||||
cy.contains(text.notFound.title).should('be.visible')
|
||||
cy.contains(text.notFound.description.replace('{0}', 'projectId: "test-project-id"')).should('be.visible')
|
||||
|
||||
@@ -71,9 +71,9 @@ import SendIcon from '~icons/cy/paper-airplane_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import CodeTag from '../../../frontend-shared/src/components/CodeTag.vue'
|
||||
import ExternalLink from '@cy/gql-components/ExternalLink.vue'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const { openLoginConnectModal } = useLoginConnectStore()
|
||||
const { openLoginConnectModal } = useUserProjectStatusStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SpecHeaderCloudDataTooltipFragmentDoc } from '../generated/graphql-test
|
||||
import SpecHeaderCloudDataTooltip from './SpecHeaderCloudDataTooltip.vue'
|
||||
import { get, set } from 'lodash'
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const tooltipContentSelector = '.v-popper'
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('<SpecHeaderCloudDataTooltip />', () => {
|
||||
docs: string
|
||||
},
|
||||
) {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
cy.mountFragment(SpecHeaderCloudDataTooltipFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
@@ -26,22 +26,22 @@ describe('<SpecHeaderCloudDataTooltip />', () => {
|
||||
|
||||
switch (status) {
|
||||
case 'LOGGED_OUT':
|
||||
loginConnectStore.setUserFlag('isLoggedIn', false)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', false)
|
||||
break
|
||||
case 'NOT_CONNECTED':
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setUserFlag('isOrganizationLoaded', true)
|
||||
loginConnectStore.setUserFlag('isMemberOfOrganization', true)
|
||||
loginConnectStore.setProjectFlag('isProjectConnected', false)
|
||||
loginConnectStore.setProjectFlag('isConfigLoaded', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setUserFlag('isOrganizationLoaded', true)
|
||||
userProjectStatusStore.setUserFlag('isMemberOfOrganization', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', false)
|
||||
userProjectStatusStore.setProjectFlag('isConfigLoaded', true)
|
||||
break
|
||||
case 'NOT_FOUND':
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setProjectFlag('isNotFound', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setProjectFlag('isNotFound', true)
|
||||
break
|
||||
case 'ACCESS_REQUESTED':
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setProjectFlag('isNotAuthorized', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setProjectFlag('isNotAuthorized', true)
|
||||
|
||||
set(result, 'currentProject.cloudProject', {
|
||||
__typename: 'CloudProjectUnauthorized',
|
||||
@@ -51,16 +51,16 @@ describe('<SpecHeaderCloudDataTooltip />', () => {
|
||||
|
||||
break
|
||||
case 'UNAUTHORIZED':
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setProjectFlag('isNotAuthorized', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setProjectFlag('isNotAuthorized', true)
|
||||
|
||||
break
|
||||
case 'CONNECTED':
|
||||
default:
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
loginConnectStore.setUserFlag('isOrganizationLoaded', true)
|
||||
loginConnectStore.setUserFlag('isMemberOfOrganization', true)
|
||||
loginConnectStore.setProjectFlag('isProjectConnected', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setUserFlag('isOrganizationLoaded', true)
|
||||
userProjectStatusStore.setUserFlag('isMemberOfOrganization', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', true)
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
v-if="userStatusMatches('isLoggedOut')"
|
||||
v-if="cloudStatusMatches('isLoggedOut')"
|
||||
:prefix-icon="UserOutlineIcon"
|
||||
prefix-icon-class="icon-dark-white icon-light-transparent"
|
||||
data-cy="login-button"
|
||||
@@ -53,7 +53,7 @@
|
||||
{{ t('specPage.cloudLoginButton') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="userStatusMatches('needsProjectConnect')"
|
||||
v-else-if="cloudStatusMatches('needsProjectConnect')"
|
||||
:prefix-icon="ConnectIcon"
|
||||
prefix-icon-class="icon-dark-white icon-light-transparent"
|
||||
data-cy="connect-button"
|
||||
@@ -92,9 +92,9 @@ import type { SpecHeaderCloudDataTooltipFragment } from '../generated/graphql'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { computed } from 'vue'
|
||||
import { gql } from '@urql/vue'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const { userStatusMatches, project } = useLoginConnectStore()
|
||||
const { cloudStatusMatches, project } = useUserProjectStatusStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -201,13 +201,13 @@ import { useRoute } from 'vue-router'
|
||||
import FlakyInformation from './flaky-badge/FlakyInformation.vue'
|
||||
import { useCloudSpecData } from '../composables/useCloudSpecData'
|
||||
import { useSpecFilter } from '../composables/useSpecFilter'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
import SpecsRunAllSpecs from './SpecsRunAllSpecs.vue'
|
||||
import { useRunAllSpecsStore } from '../store/run-all-specs-store'
|
||||
import { posixify } from '../paths'
|
||||
import { useSubscription } from '../graphql'
|
||||
|
||||
const { openLoginConnectModal } = useLoginConnectStore()
|
||||
const { openLoginConnectModal } = useUserProjectStatusStore()
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -6,15 +6,16 @@ import interval from 'human-interval'
|
||||
import { CloudUserStubs, CloudProjectStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { AllowedState, BannerIds } from '@packages/types'
|
||||
import { assignIn, set } from 'lodash'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import type { LoginConnectState } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { UserProjectStatusStore, useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
import type { UserProjectStatusState } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const AlertSelector = 'alert-header'
|
||||
const AlertCloseBtnSelector = 'alert-suffix-icon'
|
||||
|
||||
type BannerKey = keyof typeof BannerIds
|
||||
type BannerId = typeof BannerIds[BannerKey]
|
||||
|
||||
describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
describe('<SpecsListBanners />', { viewportHeight: 260, defaultCommandTimeout: 1000 }, () => {
|
||||
const validateBaseRender = () => {
|
||||
it('should render as expected', () => {
|
||||
cy.findByTestId(AlertSelector).should('be.visible')
|
||||
@@ -68,9 +69,9 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
|
||||
const validateSmartNotificationBehaviors = (bannerId: BannerId, bannerTestId: string, gql: Partial<SpecsListBannersFragment>) => {
|
||||
it('should not render when using cypress < 4 days', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
userProjectStatusStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, {
|
||||
render: (gqlVal) => <SpecsListBanners gql={gqlVal} />,
|
||||
@@ -80,9 +81,9 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
})
|
||||
|
||||
it('should not render when previously-dismissed', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setBannersState({
|
||||
userProjectStatusStore.setBannersState({
|
||||
[bannerId]: {
|
||||
dismissed: Date.now(),
|
||||
},
|
||||
@@ -97,9 +98,9 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
|
||||
context('banner conditions are met and when cypress use >= 4 days', () => {
|
||||
beforeEach(() => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setCypressFirstOpened(Date.now() - interval('4 days'))
|
||||
userProjectStatusStore.setCypressFirstOpened(Date.now() - interval('4 days'))
|
||||
|
||||
cy.stubMutationResolver(UseCohorts_DetermineCohortDocument, (defineResult) => {
|
||||
return defineResult({ determineCohort: { __typename: 'Cohort', name: 'foo', cohort: 'A' } })
|
||||
@@ -113,27 +114,42 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
/>),
|
||||
})
|
||||
|
||||
const bannerTrueUserConditions = {
|
||||
'login-banner': [],
|
||||
'create-organization-banner': ['isLoggedIn', 'isOrganizationLoaded'],
|
||||
'connect-project-banner': ['isLoggedIn', 'isMemberOfOrganization'],
|
||||
'record-banner': ['isLoggedIn', 'isMemberOfOrganization'],
|
||||
} as const
|
||||
type DeepPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
|
||||
}
|
||||
|
||||
const bannerTrueProjectConditions = {
|
||||
'login-banner': [],
|
||||
'create-organization-banner': [],
|
||||
'connect-project-banner': ['isConfigLoaded'],
|
||||
'record-banner': ['isProjectConnected', 'hasNoRecordedRuns', 'hasNonExampleSpec', 'isConfigLoaded'],
|
||||
} as const
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const bannerTrueConditions: Record<string, DeepPartial<UserProjectStatusState>> = {
|
||||
'login-banner': {},
|
||||
'create-organization-banner': {
|
||||
user: { isLoggedIn: true, isOrganizationLoaded: true },
|
||||
},
|
||||
'connect-project-banner': {
|
||||
user: { isLoggedIn: true, isMemberOfOrganization: true },
|
||||
project: { isConfigLoaded: true },
|
||||
},
|
||||
'record-banner': {
|
||||
user: { isLoggedIn: true, isMemberOfOrganization: true },
|
||||
project: { isProjectConnected: true, hasNoRecordedRuns: true, hasNonExampleSpec: true, isConfigLoaded: true },
|
||||
},
|
||||
'component-testing-banner': {
|
||||
testingType: 'e2e',
|
||||
user: { isLoggedIn: true, isMemberOfOrganization: true },
|
||||
project: { isProjectConnected: true, hasNonExampleSpec: true, isConfigLoaded: true, hasDetectedCtFramework: true },
|
||||
},
|
||||
}
|
||||
|
||||
bannerTrueUserConditions[bannerTestId].forEach((status: keyof LoginConnectState['user']) => {
|
||||
loginConnectStore.setUserFlag(status, true)
|
||||
})
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
bannerTrueProjectConditions[bannerTestId].forEach((status: keyof LoginConnectState['project']) => {
|
||||
loginConnectStore.setProjectFlag(status, true)
|
||||
const stateToSet = bannerTrueConditions[bannerTestId]
|
||||
|
||||
Object.entries(stateToSet).forEach(([key, value]) => {
|
||||
if (key === 'user') {
|
||||
Object.entries(value).forEach(([key, value]) => userProjectStatusStore.setUserFlag(key as any, value))
|
||||
} else if (key === 'project') {
|
||||
Object.entries(value).forEach(([key, value]) => userProjectStatusStore.setProjectFlag(key as any, value))
|
||||
} else if (key === 'testingType') {
|
||||
userProjectStatusStore.setTestingType(value as any)
|
||||
}
|
||||
})
|
||||
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('be.visible')
|
||||
@@ -142,26 +158,31 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
it('should be preempted by spec not found banner', () => {
|
||||
mountWithState(gql, {}, { isSpecNotFound: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
cy.contains('Spec not found').should('exist')
|
||||
})
|
||||
|
||||
it('should be preempted by offline warning banner', () => {
|
||||
mountWithState(gql, {}, { isOffline: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
cy.contains('No internet connection').should('exist')
|
||||
})
|
||||
|
||||
it('should be preempted by fetch error banner', () => {
|
||||
mountWithState(gql, {}, { isFetchError: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
cy.contains('Lost connection').should('exist')
|
||||
})
|
||||
|
||||
it('should be preempted by project not found banner', () => {
|
||||
mountWithState(gql, {}, { isProjectNotFound: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
cy.contains('Couldn\'t find your project').should('exist')
|
||||
})
|
||||
|
||||
it('should be preempted by request access banner', () => {
|
||||
mountWithState(gql, {}, { isProjectUnauthorized: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
cy.contains('Request access').should('exist')
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -171,9 +192,9 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
visible.value = true
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
userProjectStatusStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, { render: (gql) => <SpecsListBanners gql={gql} isSpecNotFound={visible} /> })
|
||||
})
|
||||
|
||||
@@ -188,9 +209,9 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
visible.value = true
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
userProjectStatusStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, { render: (gql) => <SpecsListBanners gql={gql} isOffline={visible} /> })
|
||||
})
|
||||
@@ -208,9 +229,9 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
beforeEach(() => {
|
||||
visible.value = true
|
||||
refetchCallback = cy.stub()
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
userProjectStatusStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, { render: (gql) => <SpecsListBanners gql={gql} onRefetchFailedCloudData={refetchCallback} isFetchError={visible} /> })
|
||||
})
|
||||
@@ -234,11 +255,11 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
visible.value = true
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
reconnectCallback = cy.stub(loginConnectStore, 'openLoginConnectModal')
|
||||
reconnectCallback = cy.stub(userProjectStatusStore, 'openLoginConnectModal')
|
||||
|
||||
loginConnectStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
userProjectStatusStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, { render: (gql) => <SpecsListBanners gql={gql} isProjectNotFound={visible} /> })
|
||||
})
|
||||
@@ -261,9 +282,9 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
visible.value = true
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
userProjectStatusStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
@@ -277,6 +298,8 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
hasRequestedAccess: false,
|
||||
},
|
||||
savedState: {},
|
||||
currentTestingType: 'e2e',
|
||||
config: {},
|
||||
}
|
||||
},
|
||||
render: (gql) => <SpecsListBanners gql={gql} isProjectUnauthorized={visible} />,
|
||||
@@ -294,9 +317,9 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
visible.value = true
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
userProjectStatusStore.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
@@ -310,6 +333,8 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
hasRequestedAccess: true,
|
||||
},
|
||||
savedState: {},
|
||||
currentTestingType: 'e2e',
|
||||
config: {},
|
||||
}
|
||||
},
|
||||
render: (gql) => <SpecsListBanners gql={gql} isProjectUnauthorized={visible} hasRequestedAccess />,
|
||||
@@ -408,4 +433,87 @@ describe('<SpecsListBanners />', { viewportHeight: 260 }, () => {
|
||||
|
||||
validateSmartNotificationBehaviors(BannerIds.ACI_082022_RECORD, 'record-banner', gql)
|
||||
})
|
||||
|
||||
describe('component testing', () => {
|
||||
const gql: Partial<SpecsListBannersFragment> = {
|
||||
cloudViewer: {
|
||||
...CloudUserStubs.me,
|
||||
firstOrganization: {
|
||||
__typename: 'CloudOrganizationConnection',
|
||||
nodes: [{ __typename: 'CloudOrganization', id: '987' }],
|
||||
},
|
||||
},
|
||||
currentProject: {
|
||||
__typename: 'CurrentProject',
|
||||
id: 'abc123',
|
||||
title: 'my-test-project',
|
||||
currentTestingType: 'e2e',
|
||||
projectId: 'abcd',
|
||||
cloudProject: {
|
||||
...CloudProjectStubs.componentProject,
|
||||
runs: {
|
||||
__typename: 'CloudRunConnection',
|
||||
nodes: [{ __typename: 'CloudRun', id: 111 }],
|
||||
},
|
||||
},
|
||||
config: {
|
||||
component: {},
|
||||
},
|
||||
} as any,
|
||||
wizard: {
|
||||
__typename: 'Wizard',
|
||||
framework: {
|
||||
id: 'react',
|
||||
name: 'React',
|
||||
type: 'react',
|
||||
} as any,
|
||||
bundler: {
|
||||
id: 'bundler',
|
||||
name: 'webpack',
|
||||
},
|
||||
},
|
||||
}
|
||||
let userProjectStatusStore: UserProjectStatusStore
|
||||
|
||||
beforeEach(() => {
|
||||
cy.gqlStub.Query.currentProject = gql.currentProject as any
|
||||
cy.gqlStub.Query.cloudViewer = gql.cloudViewer as any
|
||||
cy.gqlStub.Query.wizard = gql.wizard as any
|
||||
|
||||
userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setUserFlag('isMemberOfOrganization', true)
|
||||
userProjectStatusStore.setProjectFlag('isProjectConnected', true)
|
||||
userProjectStatusStore.setProjectFlag('hasDetectedCtFramework', true)
|
||||
userProjectStatusStore.setProjectFlag('isCTConfigured', false)
|
||||
userProjectStatusStore.setTestingType('e2e')
|
||||
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, {
|
||||
render: (gqlVal) => <SpecsListBanners gql={gqlVal} />,
|
||||
})
|
||||
})
|
||||
|
||||
validateBaseRender()
|
||||
validateCloseControl()
|
||||
validateSmartNotificationBehaviors(BannerIds.CT_052023_AVAILABLE, 'component-testing-banner', gql)
|
||||
|
||||
it('should not render when another smart banner has been dismissed within two days', () => {
|
||||
userProjectStatusStore.setBannersState({
|
||||
[BannerIds.ACI_082022_CONNECT_PROJECT]: {
|
||||
dismissed: Date.now() - interval('3 days'),
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByTestId('component-testing-banner').should('be.visible').then(() => {
|
||||
userProjectStatusStore.setBannersState({
|
||||
[BannerIds.ACI_082022_CONNECT_PROJECT]: {
|
||||
dismissed: Date.now() - interval('1 day'),
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByTestId('component-testing-banner').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
:prefix-icon="ConnectIcon"
|
||||
class="mt-[24px]"
|
||||
data-cy="reconnect-button"
|
||||
@click="loginConnectStore.openLoginConnectModal({utmMedium: 'Tests Tab'})"
|
||||
@click="userProjectStatusStore.openLoginConnectModal({utmMedium: 'Tests Tab'})"
|
||||
>
|
||||
{{ t('runs.errors.notFound.button') }}
|
||||
</Button>
|
||||
@@ -112,10 +112,13 @@
|
||||
</Alert>
|
||||
|
||||
<component
|
||||
:is="bannerToShow"
|
||||
v-else-if="isBannerAllowed && bannerToShow"
|
||||
:is="bannerComponentToShow"
|
||||
v-else-if="bannerComponentToShow"
|
||||
:has-banner-been-shown="hasCurrentBannerBeenShown"
|
||||
:cohort-option="currentCohortOption.cohort"
|
||||
:framework="ctFramework"
|
||||
:bundler="ctBundler"
|
||||
:machine-id="props.gql.machineId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -135,15 +138,15 @@ import RequestAccessButton from './RequestAccessButton.vue'
|
||||
import { gql } from '@urql/vue'
|
||||
import { SpecsListBannersFragment, SpecsListBanners_CheckCloudOrgMembershipDocument } from '../generated/graphql'
|
||||
import { AllowedState, BannerIds } from '@packages/types'
|
||||
import { LoginBanner, CreateOrganizationBanner, ConnectProjectBanner, RecordBanner } from './banners'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { LoginBanner, ComponentTestingAvailableBanner, CreateOrganizationBanner, ConnectProjectBanner, RecordBanner } from './banners'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
import { usePromptManager } from '@packages/frontend-shared/src/gql-components/composables/usePromptManager'
|
||||
import { CohortConfig, CohortOption, useCohorts } from '@packages/frontend-shared/src/gql-components/composables/useCohorts'
|
||||
import { useSubscription } from '../graphql'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
gql`
|
||||
fragment SpecsListBanners on Query {
|
||||
@@ -163,6 +166,21 @@ fragment SpecsListBanners on Query {
|
||||
id
|
||||
projectId
|
||||
savedState
|
||||
currentTestingType
|
||||
config
|
||||
}
|
||||
machineId
|
||||
wizard {
|
||||
framework {
|
||||
id
|
||||
name
|
||||
icon
|
||||
type
|
||||
}
|
||||
bundler {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -203,13 +221,12 @@ const showFetchError = ref(props.isFetchError)
|
||||
const showProjectNotFound = ref(props.isProjectNotFound)
|
||||
const showProjectRequestAccess = ref(props.isProjectUnauthorized)
|
||||
|
||||
const isBannerAllowed = ref(false)
|
||||
|
||||
const bannerIds = {
|
||||
isLoggedOut: BannerIds.ACI_082022_LOGIN,
|
||||
needsOrgConnect: BannerIds.ACI_082022_CREATE_ORG,
|
||||
needsProjectConnect: BannerIds.ACI_082022_CONNECT_PROJECT,
|
||||
needsRecordedRun: BannerIds.ACI_082022_RECORD,
|
||||
isComponentTestingCandidate: BannerIds.CT_052023_AVAILABLE,
|
||||
} as const
|
||||
|
||||
watch(
|
||||
@@ -223,32 +240,30 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
const cloudData = computed(() => ([props.gql.cloudViewer, props.gql.cachedUser, props.gql.currentProject] as const))
|
||||
const bannerToShow = computed(() => {
|
||||
const { getEffectiveBannerState } = usePromptManager()
|
||||
|
||||
const bannerComponentToShow = computed(() => {
|
||||
const componentsByStatus = {
|
||||
isLoggedOut: LoginBanner,
|
||||
needsOrgConnect: CreateOrganizationBanner,
|
||||
needsProjectConnect: ConnectProjectBanner,
|
||||
needsRecordedRun: RecordBanner,
|
||||
isComponentTestingCandidate: ComponentTestingAvailableBanner,
|
||||
}
|
||||
|
||||
return componentsByStatus[loginConnectStore.userStatus] ?? null
|
||||
const bannerStateToShow = getEffectiveBannerState('specsListBanner')
|
||||
|
||||
return bannerStateToShow ? componentsByStatus[bannerStateToShow] : null
|
||||
})
|
||||
|
||||
const hasCurrentBannerBeenShown = computed(() => {
|
||||
const bannerStateToShow = getEffectiveBannerState('specsListBanner')
|
||||
const bannersState = (props.gql.currentProject?.savedState as AllowedState)?.banners
|
||||
const bannerId = bannerStateToShow && bannerIds[bannerStateToShow]
|
||||
|
||||
return !!bannersState?._disabled || !!bannersState?.[bannerIds[loginConnectStore.userStatus]]?.lastShown
|
||||
return !!bannersState?._disabled || (!!bannerId && !!bannersState?.[bannerId]?.lastShown)
|
||||
})
|
||||
|
||||
const { isAllowedFeature } = usePromptManager()
|
||||
|
||||
watch(cloudData, () => {
|
||||
// when cloud data updates, recheck if banner is allowed
|
||||
isBannerAllowed.value = isAllowedFeature('specsListBanner')
|
||||
},
|
||||
{ immediate: true })
|
||||
|
||||
type BannerKeys = keyof typeof BannerIds
|
||||
type BannerId = typeof BannerIds[BannerKeys]
|
||||
type BannerCohortOptions = Partial<Record<BannerId, CohortOption[]>>
|
||||
@@ -280,11 +295,21 @@ const getCohortForBanner = (bannerId: BannerId): Ref<CohortOption | undefined> =
|
||||
}
|
||||
|
||||
const currentCohortOption = computed(() => {
|
||||
if (!bannerCohortOptions[bannerIds[loginConnectStore.userStatus]]) {
|
||||
if (!bannerCohortOptions[bannerIds[userProjectStatusStore.cloudStatus]]) {
|
||||
return { cohort: null }
|
||||
}
|
||||
|
||||
return reactive({ cohort: getCohortForBanner(bannerIds[loginConnectStore.userStatus]) })
|
||||
return reactive({ cohort: getCohortForBanner(bannerIds[userProjectStatusStore.cloudStatus]) })
|
||||
})
|
||||
|
||||
const ctFramework = computed(() => {
|
||||
return {
|
||||
name: props.gql.wizard?.framework?.name,
|
||||
type: props.gql.wizard?.framework?.type,
|
||||
icon: props.gql.wizard?.framework?.icon,
|
||||
}
|
||||
})
|
||||
|
||||
const ctBundler = computed(() => props.gql.wizard?.bundler?.name)
|
||||
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import ComponentTestingAvailableBanner from './ComponentTestingAvailableBanner.vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument, TrackedBanner_SetProjectStateDocument } from '../../generated/graphql'
|
||||
import type Sinon from 'sinon'
|
||||
|
||||
const frameworks = [
|
||||
{ name: 'React', type: 'react' },
|
||||
{ name: 'Create React App', type: 'reactscripts' },
|
||||
{ name: 'Nuxt.js (v2)', type: 'nuxtjs' },
|
||||
{ name: 'Vue', type: 'vue3' },
|
||||
{ name: 'Angular', type: 'angular' },
|
||||
{ name: 'Next.js', type: 'nextjs' },
|
||||
{ name: 'Svelte.js', type: 'svelte' },
|
||||
]
|
||||
|
||||
describe('<ComponentTestingBanner />', { viewportWidth: 1200 }, () => {
|
||||
it('should render expected content', () => {
|
||||
cy.mount(<ComponentTestingAvailableBanner hasBannerBeenShown={true} framework={frameworks[0]} machineId="abc" />)
|
||||
})
|
||||
|
||||
frameworks.map((framework) => {
|
||||
it(`should render expected content for ${framework.name}`, () => {
|
||||
cy.mount(
|
||||
<ComponentTestingAvailableBanner hasBannerBeenShown={true} framework={framework} machineId="abc" />,
|
||||
)
|
||||
|
||||
cy.findByTestId('alert-prefix-icon').should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.ct.title.replace('{0}', framework.name)).should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
context('events', () => {
|
||||
beforeEach(() => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
const setPrefs = cy.stub().as('setPrefs')
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
|
||||
return defineResult({ recordEvent: true })
|
||||
})
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_SetProjectStateDocument, (defineResult, event) => {
|
||||
setPrefs(event)
|
||||
|
||||
return defineResult({ __typename: 'Mutation', setPreferences: { __typename: 'Query' } as any })
|
||||
})
|
||||
})
|
||||
|
||||
it('should record expected event on mount', () => {
|
||||
cy.mount(
|
||||
<ComponentTestingAvailableBanner hasBannerBeenShown={false} framework={frameworks[0]} machineId="abc" />,
|
||||
)
|
||||
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'CT Available',
|
||||
medium: 'Specs CT Available Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not record event on mount if already shown', () => {
|
||||
cy.mount(
|
||||
<ComponentTestingAvailableBanner hasBannerBeenShown={true} framework={frameworks[0]} machineId="abc" />,
|
||||
)
|
||||
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
|
||||
it('should record dismissal event when clicking survey link', () => {
|
||||
cy.mount(
|
||||
<ComponentTestingAvailableBanner hasBannerBeenShown={true} framework={frameworks[0]} machineId="abc" />,
|
||||
)
|
||||
|
||||
cy.findByTestId('survey-link').click()
|
||||
|
||||
cy.get('@setPrefs').should('have.been.calledTwice')
|
||||
cy.get('@setPrefs').should(($stub) => {
|
||||
const arg = ($stub as unknown as Sinon.SinonStub).getCall(1).args[0]
|
||||
|
||||
expect(arg.value).to.contain('ct_052023_available')
|
||||
expect(arg.value).to.contain('dismissed')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="bannerId"
|
||||
data-cy="component-testing-banner"
|
||||
status="promo"
|
||||
:title="title"
|
||||
class="mb-[16px]"
|
||||
dismissible
|
||||
:icon="iconFromType"
|
||||
:has-banner-been-shown="hasBannerBeenShown"
|
||||
:event-data="{
|
||||
campaign: 'CT Available',
|
||||
medium: 'Specs CT Available Banner',
|
||||
cohort: ''
|
||||
}"
|
||||
>
|
||||
<template #default="{ dismiss, bannerInstanceId }">
|
||||
<p class="pb-[16px] text-gray-700">
|
||||
{{ t('specPage.banners.ct.content') }}
|
||||
</p>
|
||||
<div class="flex flex-row items-center text-sm border-t border-gray-100 pt-[8px] -mb-[8px] -mx-[16px] px-[16px]">
|
||||
<Button
|
||||
data-cy="setup-button"
|
||||
variant="outline-indigo"
|
||||
size="32"
|
||||
class="mr-[16px]"
|
||||
@click="handlePrimary(bannerInstanceId)"
|
||||
>
|
||||
{{ t('specPage.banners.ct.primaryAction') }}
|
||||
</Button>
|
||||
<ExternalLink
|
||||
data-cy="docs-link"
|
||||
:href="docsLink"
|
||||
@click="handleDocsClick(bannerInstanceId)"
|
||||
>
|
||||
<span class="font-medium">{{ t('specPage.banners.ct.secondaryAction') }}</span>
|
||||
</ExternalLink>
|
||||
<span class="flex-grow" />
|
||||
<ExternalLink
|
||||
data-cy="survey-link"
|
||||
:href="surveyLink"
|
||||
@click="handleSurveyClick(bannerInstanceId, dismiss)"
|
||||
>
|
||||
<span class="font-medium">{{ t('specPage.banners.ct.dismissAction') }}</span>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</template>
|
||||
</TrackedBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import Button from '@cypress-design/vue-button'
|
||||
import { FrameworkBundlerLogos } from '@packages/frontend-shared/src/utils/icons'
|
||||
import ExternalLink from '@cy/gql-components/ExternalLink.vue'
|
||||
import { getUrlWithParams } from '@packages/frontend-shared/src/utils/getUrlWithParams'
|
||||
import { SwitchToComponentTestingDocument, ComponentTestingAvailable_RecordEventDocument } from '../../generated/graphql'
|
||||
|
||||
gql`
|
||||
mutation SwitchToComponentTesting {
|
||||
switchTestingTypeAndRelaunch(testingType: component)
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation ComponentTestingAvailable_RecordEvent($campaign: String!, $messageId: String!, $payload: String!) {
|
||||
recordEvent(campaign: $campaign, medium: "CT Available Banner", messageId: $messageId, includeMachineId: true, payload: $payload)
|
||||
}
|
||||
`
|
||||
|
||||
const switchToCtAndRelaunch = useMutation(SwitchToComponentTestingDocument)
|
||||
const recordEvent = useMutation(ComponentTestingAvailable_RecordEventDocument)
|
||||
|
||||
const props = defineProps<{
|
||||
hasBannerBeenShown: boolean
|
||||
framework: {
|
||||
name: string
|
||||
icon?: string | null
|
||||
type: string
|
||||
}
|
||||
bundler?: 'vite' | 'webpack'
|
||||
machineId: string | undefined
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.CT_052023_AVAILABLE
|
||||
|
||||
const title = computed(() => t('specPage.banners.ct.title', [props.framework?.name]))
|
||||
const iconFromType = computed(() => FrameworkBundlerLogos[props.framework?.type])
|
||||
|
||||
const handlePrimary = async (bannerInstanceId: string) => {
|
||||
await recordCampaignEvent(bannerInstanceId, 'Quick setup')
|
||||
|
||||
await switchToCtAndRelaunch.executeMutation({})
|
||||
}
|
||||
|
||||
const handleDocsClick = async (bannerInstanceId: string) => {
|
||||
await recordCampaignEvent(bannerInstanceId, 'Read our guide')
|
||||
}
|
||||
|
||||
const handleSurveyClick = async (bannerInstanceId: string, dismiss: () => Promise<void>) => {
|
||||
await recordCampaignEvent(bannerInstanceId, 'Give feedback')
|
||||
|
||||
await dismiss()
|
||||
}
|
||||
|
||||
const recordCampaignEvent = async (bannerInstanceId: string, campaign: string) => {
|
||||
await recordEvent.executeMutation({
|
||||
campaign,
|
||||
messageId: bannerInstanceId,
|
||||
payload: JSON.stringify({
|
||||
framework: props.framework.name,
|
||||
bundler: props.bundler,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const docsLink = computed(() => {
|
||||
return getUrlWithParams({
|
||||
url: 'https://on.cypress.io/component',
|
||||
params: {
|
||||
utm_medium: 'CT Available Banner',
|
||||
utm_campaign: 'Read the Docs',
|
||||
utm_content: [props.framework.name, props.bundler].filter((val) => !!val).join('-'),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const surveyLink = computed(() => {
|
||||
return getUrlWithParams({
|
||||
url: 'https://on.cypress.io/component-survey-q2-23',
|
||||
params: {
|
||||
utm_medium: 'CT Available Banner',
|
||||
utm_campaign: 'Give feedback',
|
||||
utm_content: [props.framework.name, props.bundler].filter((val) => !!val).join('-'),
|
||||
machine_id: props.machineId ?? '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -37,8 +37,8 @@ import Button from '@cy/components/Button.vue'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import type { CohortOption } from '@packages/frontend-shared/src/gql-components/composables/useCohorts'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
const { openLoginConnectModal } = useLoginConnectStore()
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
const { openLoginConnectModal } = useUserProjectStatusStore()
|
||||
|
||||
defineProps<{
|
||||
hasBannerBeenShown: boolean
|
||||
|
||||
@@ -37,8 +37,8 @@ import Button from '@cy/components/Button.vue'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import type { CohortOption } from '@packages/frontend-shared/src/gql-components/composables/useCohorts'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
const { openLoginConnectModal } = useLoginConnectStore()
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
const { openLoginConnectModal } = useUserProjectStatusStore()
|
||||
|
||||
defineProps<{
|
||||
hasBannerBeenShown: boolean
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
v-model="isAlertDisplayed"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
<slot
|
||||
:dismiss="dismiss"
|
||||
:bannerInstanceId="bannerInstanceId"
|
||||
/>
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
@@ -71,22 +74,22 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => isAlertDisplayed.value, (newVal) => {
|
||||
watch(() => isAlertDisplayed.value, async (newVal) => {
|
||||
if (!newVal) {
|
||||
updateBannerState('dismissed')
|
||||
await updateBannerState('dismissed')
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateBannerState('lastShown')
|
||||
onMounted(async () => {
|
||||
await updateBannerState('lastShown')
|
||||
})
|
||||
|
||||
function updateBannerState (field: 'lastShown' | 'dismissed') {
|
||||
async function updateBannerState (field: 'lastShown' | 'dismissed') {
|
||||
const savedBannerState = stateQuery.data.value?.currentProject?.savedState?.banners ?? {}
|
||||
|
||||
set(savedBannerState, [props.bannerId, field], Date.now())
|
||||
|
||||
setStateMutation.executeMutation({ value: JSON.stringify({ banners: savedBannerState }) })
|
||||
await setStateMutation.executeMutation({ value: JSON.stringify({ banners: savedBannerState }) })
|
||||
}
|
||||
|
||||
function recordBannerShown ({ campaign, medium, cohort }: EventData): void {
|
||||
@@ -98,4 +101,8 @@ function recordBannerShown ({ campaign, medium, cohort }: EventData): void {
|
||||
})
|
||||
}
|
||||
|
||||
async function dismiss (): Promise<void> {
|
||||
await updateBannerState('dismissed')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -5,3 +5,5 @@ export { default as CreateOrganizationBanner } from './CreateOrganizationBanner.
|
||||
export { default as ConnectProjectBanner } from './ConnectProjectBanner.vue'
|
||||
|
||||
export { default as RecordBanner } from './RecordBanner.vue'
|
||||
|
||||
export { default as ComponentTestingAvailableBanner } from './ComponentTestingAvailableBanner.vue'
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
const config = require('@packages/frontend-shared/tailwind.config.cjs')
|
||||
|
||||
config.content.files = [
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx,scss,css}', //
|
||||
'../frontend-shared/src/**/*.{vue,js,ts,jsx,tsx,scss,css}',
|
||||
],
|
||||
config.content.files.push('../frontend-shared/src/**/*.{vue,js,ts,jsx,tsx,scss,css}')
|
||||
|
||||
module.exports = config
|
||||
|
||||
@@ -5,11 +5,13 @@ const pkg = require('@packages/root')
|
||||
|
||||
const debug = Debug('cypress:data-context:actions:EventCollectorActions')
|
||||
|
||||
interface CollectableEvent {
|
||||
interface CollectibleEvent {
|
||||
campaign: string
|
||||
messageId: string
|
||||
medium: string
|
||||
cohort?: string
|
||||
payload?: object
|
||||
machineId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,23 +25,29 @@ export class EventCollectorActions {
|
||||
debug('Using %s environment for Event Collection', cloudEnv)
|
||||
}
|
||||
|
||||
async recordEvent (event: CollectableEvent): Promise<boolean> {
|
||||
async recordEvent (event: CollectibleEvent, includeMachineId: boolean): Promise<boolean> {
|
||||
try {
|
||||
const cloudUrl = this.ctx.cloud.getCloudUrl(cloudEnv)
|
||||
const eventUrl = includeMachineId ? `${cloudUrl}/machine-collect` : `${cloudUrl}/anon-collect`
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-cypress-version': pkg.version,
|
||||
}
|
||||
|
||||
if (includeMachineId) {
|
||||
event.machineId = (await this.ctx.coreData.machineId) || undefined
|
||||
}
|
||||
|
||||
await this.ctx.util.fetch(
|
||||
`${cloudUrl}/anon-collect`,
|
||||
eventUrl,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-cypress-version': pkg.version,
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify(event),
|
||||
},
|
||||
)
|
||||
|
||||
debug(`Recorded event: %o`, event)
|
||||
debug(`Recorded %s event: %o`, includeMachineId ? 'machine-linked' : 'anonymous', event)
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
|
||||
@@ -100,6 +100,14 @@ export class WizardActions {
|
||||
|
||||
this.resetWizard()
|
||||
|
||||
await this.initializeFramework()
|
||||
}
|
||||
|
||||
async initializeFramework () {
|
||||
if (!this.ctx.currentProject) {
|
||||
return
|
||||
}
|
||||
|
||||
const detected = await detectFramework(this.ctx.currentProject, this.ctx.coreData.wizard.frameworks)
|
||||
|
||||
debug('detected %o', detected)
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { SocketIONamespace, SocketIOServer } from '@packages/socket'
|
||||
import type { Server } from 'http'
|
||||
import type { ErrorWrapperSource } from '@packages/errors'
|
||||
import type { GitDataSource, LegacyCypressConfigJson } from '../sources'
|
||||
import { machineId as getMachineId } from 'node-machine-id'
|
||||
|
||||
export type Maybe<T> = T | null | undefined
|
||||
|
||||
@@ -123,6 +124,7 @@ export interface CoreDataShape {
|
||||
cliBrowser: string | null
|
||||
cliTestingType: string | null
|
||||
activeBrowser: FoundBrowser | null
|
||||
machineId: Promise<string | null>
|
||||
machineBrowsers: Promise<FoundBrowser[]> | null
|
||||
allBrowsers: Promise<FoundBrowser[]> | null
|
||||
servers: {
|
||||
@@ -166,6 +168,7 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
|
||||
servers: {},
|
||||
cliBrowser: modeOptions.browser ?? null,
|
||||
cliTestingType: modeOptions.testingType ?? null,
|
||||
machineId: machineId(),
|
||||
machineBrowsers: null,
|
||||
allBrowsers: null,
|
||||
hasInitializedMode: null,
|
||||
@@ -233,4 +236,12 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
|
||||
testsForRunResults: {},
|
||||
},
|
||||
}
|
||||
|
||||
async function machineId (): Promise<string | null> {
|
||||
try {
|
||||
return await getMachineId()
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import semver from 'semver'
|
||||
const debug = Debug('cypress:data-context:sources:VersionsDataSource')
|
||||
|
||||
const pkg = require('@packages/root')
|
||||
const nmi = require('node-machine-id')
|
||||
|
||||
interface Version {
|
||||
id: string
|
||||
@@ -131,7 +130,7 @@ export class VersionsDataSource {
|
||||
return pkg.version
|
||||
}
|
||||
|
||||
const id = await VersionsDataSource.machineId()
|
||||
const id = (await this.ctx.coreData.machineId) || undefined
|
||||
|
||||
const manifestHeaders: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -226,14 +225,6 @@ export class VersionsDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
private static async machineId (): Promise<string | undefined> {
|
||||
try {
|
||||
return await nmi.machineId()
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private isFulfilled<R> (item: PromiseSettledResult<R>): item is PromiseFulfilledResult<R> {
|
||||
return item.status === 'fulfilled'
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ describe('EventCollectorActions', () => {
|
||||
})
|
||||
|
||||
context('.recordEvent', () => {
|
||||
it('makes expected request', async () => {
|
||||
it('makes expected request for anonymous event', async () => {
|
||||
await actions.recordEvent({
|
||||
campaign: 'abc',
|
||||
medium: 'def',
|
||||
messageId: 'ghi',
|
||||
cohort: '123',
|
||||
})
|
||||
}, false)
|
||||
|
||||
expect(ctx.util.fetch).to.have.been.calledOnceWith(
|
||||
sinon.match(/anon-collect$/), // Verify URL ends with expected 'anon-collect' path
|
||||
@@ -38,10 +38,26 @@ describe('EventCollectorActions', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('makes expected request for machine-linked event', async () => {
|
||||
ctx.coreData.machineId = Promise.resolve('xyz')
|
||||
|
||||
await actions.recordEvent({
|
||||
campaign: 'abc',
|
||||
medium: 'def',
|
||||
messageId: 'ghi',
|
||||
cohort: '123',
|
||||
}, true)
|
||||
|
||||
expect(ctx.util.fetch).to.have.been.calledOnceWith(
|
||||
sinon.match(/machine-collect$/), // Verify URL ends with expected 'machine-collect' path
|
||||
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'x-cypress-version': pkg.version }, body: '{"campaign":"abc","medium":"def","messageId":"ghi","cohort":"123","machineId":"xyz"}' },
|
||||
)
|
||||
})
|
||||
|
||||
it('resolve true if request succeeds', async () => {
|
||||
(ctx.util.fetch as SinonStub).resolves({} as any)
|
||||
|
||||
const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' })
|
||||
const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }, false)
|
||||
|
||||
expect(result).to.eql(true)
|
||||
})
|
||||
@@ -49,7 +65,7 @@ describe('EventCollectorActions', () => {
|
||||
it('resolves false if request fails', async () => {
|
||||
(ctx.util.fetch as SinonStub).rejects({} as any)
|
||||
|
||||
const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' })
|
||||
const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }, false)
|
||||
|
||||
expect(result).to.eql(false)
|
||||
})
|
||||
|
||||
@@ -9,12 +9,10 @@ import { createTestDataContext } from '../helper'
|
||||
import { CYPRESS_REMOTE_MANIFEST_URL, NPM_CYPRESS_REGISTRY_URL } from '@packages/types'
|
||||
|
||||
const pkg = require('@packages/root')
|
||||
const nmi = require('node-machine-id')
|
||||
|
||||
describe('VersionsDataSource', () => {
|
||||
context('.versions', () => {
|
||||
let ctx: DataContext
|
||||
let nmiStub: sinon.SinonStub
|
||||
let fetchStub: sinon.SinonStub
|
||||
let isDependencyInstalledStub: sinon.SinonStub
|
||||
let mockNow: Date = new Date()
|
||||
@@ -33,6 +31,7 @@ describe('VersionsDataSource', () => {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.coreData.machineId = Promise.resolve('abcd123')
|
||||
ctx.coreData.currentProject = '/abc'
|
||||
ctx.coreData.currentTestingType = 'e2e'
|
||||
|
||||
@@ -41,7 +40,6 @@ describe('VersionsDataSource', () => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
nmiStub = sinon.stub(nmi, 'machineId')
|
||||
sinon.stub(ctx.util, 'fetch').callsFake(fetchStub)
|
||||
sinon.stub(ctx.util, 'isDependencyInstalled').callsFake(isDependencyInstalledStub)
|
||||
sinon.stub(os, 'platform').returns('darwin')
|
||||
@@ -54,8 +52,6 @@ describe('VersionsDataSource', () => {
|
||||
})
|
||||
|
||||
it('loads the manifest for the latest version with all headers and queries npm for release dates', async () => {
|
||||
nmiStub.resolves('abcd123')
|
||||
|
||||
fetchStub
|
||||
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
|
||||
headers: sinon.match({
|
||||
@@ -107,7 +103,7 @@ describe('VersionsDataSource', () => {
|
||||
it('resets telemetry data triggering a new call to get the latest version', async () => {
|
||||
const currentCypressVersion = pkg.version
|
||||
|
||||
nmiStub.rejects('Error while obtaining machine id')
|
||||
ctx.coreData.machineId = Promise.resolve(null)
|
||||
ctx.coreData.currentTestingType = 'component'
|
||||
|
||||
fetchStub
|
||||
@@ -140,8 +136,6 @@ describe('VersionsDataSource', () => {
|
||||
})
|
||||
|
||||
it('handles errors fetching version data', async () => {
|
||||
nmiStub.resolves('abcd123')
|
||||
|
||||
fetchStub
|
||||
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
|
||||
headers: sinon.match({
|
||||
@@ -167,8 +161,6 @@ describe('VersionsDataSource', () => {
|
||||
})
|
||||
|
||||
it('handles invalid response errors', async () => {
|
||||
nmiStub.resolves('abcd123')
|
||||
|
||||
fetchStub
|
||||
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
|
||||
headers: sinon.match({
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@antfu/utils": "^0.3.0",
|
||||
"@cypress-design/css": "^0.11.0",
|
||||
"@cypress-design/css": "^0.13.1",
|
||||
"@graphql-typed-document-node/core": "^3.1.0",
|
||||
"@headlessui/vue": "1.4.0",
|
||||
"@iconify/json": "1.1.368",
|
||||
|
||||
|
Before Width: | Height: | Size: 788 B After Width: | Height: | Size: 788 B |
|
Before Width: | Height: | Size: 688 B After Width: | Height: | Size: 688 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 486 B |
|
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
@@ -23,7 +23,7 @@
|
||||
:title="title"
|
||||
:header-class="`${props.headerClass} ${canCollapse ? 'group-hocus:underline' : ''}`"
|
||||
:prefix-icon="prefix?.icon"
|
||||
:prefix-icon-class="open ? prefix?.classes + ' rotate-180' : prefix?.classes"
|
||||
:prefix-icon-class="(open && collapsible) ? prefix?.classes + ' rotate-180' : prefix?.classes"
|
||||
:suffix-icon-aria-label="props.dismissible ? t('components.alert.dismissAriaLabel') : ''"
|
||||
:suffix-icon="props.dismissible ? DeleteIcon : null"
|
||||
:suffix-button-class="classes.suffixButtonClass"
|
||||
@@ -73,7 +73,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type AlertStatus = 'error' | 'warning' | 'info' | 'default' | 'success'
|
||||
export type AlertStatus = 'error' | 'warning' | 'info' | 'default' | 'success' | 'promo'
|
||||
|
||||
export type AlertClasses = {
|
||||
headerClass: string
|
||||
@@ -162,6 +162,13 @@ const alertStyles: Record<AlertStatus, AlertClasses> = {
|
||||
bodyClass: 'bg-success-50',
|
||||
ring: 'hocus:ring-success-200 hocus:border-success-300',
|
||||
},
|
||||
promo: {
|
||||
headerClass: 'text-gray-900 bg-white border border-gray-100',
|
||||
suffixIconClass: 'icon-dark-gray-700',
|
||||
suffixButtonClass: 'text-gray-700',
|
||||
bodyClass: 'bg-white border-t border-gray-100',
|
||||
ring: 'hocus:ring-gray-100 hocus:border-gray-100',
|
||||
},
|
||||
}
|
||||
|
||||
const classes = computed(() => {
|
||||
|
||||
@@ -82,9 +82,9 @@ import type {
|
||||
} from '../generated/graphql'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -178,7 +178,7 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
const showConnectButton = computed(() => {
|
||||
return loginConnectStore.project.isConfigLoaded && loginConnectStore.userStatusMatches('needsProjectConnect')
|
||||
return userProjectStatusStore.project.isConfigLoaded && userProjectStatusStore.cloudStatusMatches('needsProjectConnect')
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -205,7 +205,7 @@ const loginMutationIsPending = computed(() => {
|
||||
})
|
||||
|
||||
const handleLoginOrContinue = async () => {
|
||||
if (loginConnectStore.user.isLoggedIn) {
|
||||
if (userProjectStatusStore.user.isLoggedIn) {
|
||||
// user is already logged in, just emit close event & return early
|
||||
emit('close')
|
||||
|
||||
@@ -244,7 +244,7 @@ const buttonText = computed(() => {
|
||||
return strings.connectProject
|
||||
}
|
||||
|
||||
if (loginConnectStore.user.isLoggedIn) {
|
||||
if (userProjectStatusStore.user.isLoggedIn) {
|
||||
return strings.continue
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watchEffect } from 'vue'
|
||||
import { gql, useQuery, useSubscription } from '@urql/vue'
|
||||
import { CloudViewerAndProject_RequiredDataDocument, CloudViewerAndProject_CheckCloudOrgMembershipDocument } from '../generated/graphql'
|
||||
import { useLoginConnectStore } from '../store/login-connect-store'
|
||||
import { watchEffect, ref } from 'vue'
|
||||
import { gql, useMutation, useQuery, useSubscription } from '@urql/vue'
|
||||
import { CloudViewerAndProject_RequiredDataDocument, CloudViewerAndProject_CheckCloudOrgMembershipDocument, CloudViewerAndProject_DetectCtFrameworksDocument } from '../generated/graphql'
|
||||
import { useUserProjectStatusStore } from '../store/user-project-status-store'
|
||||
|
||||
gql`
|
||||
fragment CloudViewerAndProject on Query {
|
||||
@@ -35,7 +35,9 @@ fragment CloudViewerAndProject on Query {
|
||||
currentProject {
|
||||
id
|
||||
config
|
||||
currentTestingType
|
||||
isFullConfigReady
|
||||
isCTConfigured
|
||||
hasNonExampleSpec
|
||||
savedState
|
||||
cloudProject {
|
||||
@@ -52,6 +54,14 @@ fragment CloudViewerAndProject on Query {
|
||||
}
|
||||
}
|
||||
}
|
||||
wizard {
|
||||
framework {
|
||||
id
|
||||
name
|
||||
icon
|
||||
isDetected
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -69,7 +79,14 @@ subscription CloudViewerAndProject_CheckCloudOrgMembership {
|
||||
}
|
||||
`
|
||||
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
gql`
|
||||
mutation CloudViewerAndProject_DetectCtFrameworks {
|
||||
initializeCtFrameworks
|
||||
}
|
||||
`
|
||||
|
||||
const hasDetectedFrameworks = ref(false)
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
const {
|
||||
setHasInitiallyLoaded,
|
||||
setUserFlag,
|
||||
@@ -77,18 +94,29 @@ const {
|
||||
setUserData,
|
||||
setPromptShown,
|
||||
setCypressFirstOpened,
|
||||
setTestingType,
|
||||
setBannersState,
|
||||
} = loginConnectStore
|
||||
} = userProjectStatusStore
|
||||
|
||||
useSubscription({ query: CloudViewerAndProject_CheckCloudOrgMembershipDocument })
|
||||
|
||||
const query = useQuery({ query: CloudViewerAndProject_RequiredDataDocument })
|
||||
|
||||
watchEffect(() => {
|
||||
const detectCtFrameworks = useMutation(CloudViewerAndProject_DetectCtFrameworksDocument)
|
||||
|
||||
watchEffect(async () => {
|
||||
if (!query.data.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasDetectedFrameworks.value && query.data.value.currentProject?.currentTestingType === 'e2e') {
|
||||
await detectCtFrameworks.executeMutation({})
|
||||
|
||||
hasDetectedFrameworks.value = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the CloudViewerAndProject has received its initial data response to use to set flags. It can be used
|
||||
* to detect that the app or launchpad has completed the first initialization of determining if the user is logged in,
|
||||
@@ -101,6 +129,7 @@ watchEffect(() => {
|
||||
cachedUser,
|
||||
cloudViewer,
|
||||
authState,
|
||||
wizard,
|
||||
} = query.data.value
|
||||
|
||||
const savedState = currentProject?.savedState
|
||||
@@ -119,6 +148,8 @@ watchEffect(() => {
|
||||
setBannersState(savedState.banners)
|
||||
}
|
||||
|
||||
setTestingType(currentProject?.currentTestingType ?? undefined)
|
||||
|
||||
const AUTH_STATE_ERRORS = ['AUTH_COULD_NOT_LAUNCH_BROWSER', 'AUTH_ERROR_DURING_LOGIN', 'AUTH_COULD_NOT_LAUNCH_BROWSER']
|
||||
|
||||
// 1. set user-related information in store
|
||||
@@ -140,9 +171,12 @@ watchEffect(() => {
|
||||
setProjectFlag('hasNonExampleSpec', !!currentProject?.hasNonExampleSpec)
|
||||
setProjectFlag('hasNoRecordedRuns', currentProject?.cloudProject?.__typename === 'CloudProject' && (currentProject.cloudProject?.runs?.nodes?.length ?? 0) === 0)
|
||||
|
||||
if (currentProject?.cloudProject || !loginConnectStore.user.isLoggedIn) {
|
||||
if (currentProject?.cloudProject || !userProjectStatusStore.user.isLoggedIn) {
|
||||
setProjectFlag('isProjectConnected', currentProject?.cloudProject?.__typename === 'CloudProject')
|
||||
}
|
||||
|
||||
setProjectFlag('isCTConfigured', !!currentProject?.isCTConfigured)
|
||||
setProjectFlag('hasDetectedCtFramework', wizard?.framework?.isDetected ?? false)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HeaderBar_HeaderBarContentFragmentDoc } from '../generated/graphql-test
|
||||
import HeaderBarContent from './HeaderBarContent.vue'
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import { CloudUserStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { useLoginConnectStore } from '../store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '../store/user-project-status-store'
|
||||
|
||||
const text = defaultMessages.topNav
|
||||
|
||||
@@ -297,9 +297,9 @@ describe('<HeaderBarContent />', { viewportWidth: 1000, viewportHeight: 750 }, (
|
||||
})
|
||||
|
||||
it('the logged in state is correctly presented in header', () => {
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
loginConnectStore.setUserFlag('isLoggedIn', true)
|
||||
userProjectStatusStore.setUserFlag('isLoggedIn', true)
|
||||
|
||||
const cloudViewer = {
|
||||
...CloudUserStubs.me,
|
||||
@@ -313,7 +313,7 @@ describe('<HeaderBarContent />', { viewportWidth: 1000, viewportHeight: 750 }, (
|
||||
fullName: 'Tester Test',
|
||||
}
|
||||
|
||||
loginConnectStore.setUserData(cloudViewer)
|
||||
userProjectStatusStore.setUserData(cloudViewer)
|
||||
|
||||
cy.mountFragment(HeaderBar_HeaderBarContentFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
|
||||
@@ -94,18 +94,18 @@
|
||||
@clear-force-open="isForceOpenAllowed = false"
|
||||
>
|
||||
<template
|
||||
v-if="loginConnectStore.user.isLoggedIn"
|
||||
v-if="userProjectStatusStore.user.isLoggedIn"
|
||||
#login-title
|
||||
>
|
||||
<UserAvatar
|
||||
:email="loginConnectStore.userData?.email"
|
||||
:email="userProjectStatusStore.userData?.email"
|
||||
class="h-[24px] w-[24px]"
|
||||
data-cy="user-avatar-title"
|
||||
/>
|
||||
<span class="sr-only">{{ t('topNav.login.profileMenuLabel') }}</span>
|
||||
</template>
|
||||
<template
|
||||
v-if="loginConnectStore.userData"
|
||||
v-if="userProjectStatusStore.userData"
|
||||
#login-panel
|
||||
>
|
||||
<div
|
||||
@@ -114,14 +114,14 @@
|
||||
>
|
||||
<div class="border-b flex border-b-gray-100 p-[16px]">
|
||||
<UserAvatar
|
||||
:email="loginConnectStore.userData?.email"
|
||||
:email="userProjectStatusStore.userData?.email"
|
||||
class="h-[48px] mr-[16px] w-[48px]"
|
||||
data-cy="user-avatar-panel"
|
||||
/>
|
||||
<div>
|
||||
<span class="text-gray-800">{{ loginConnectStore.userData?.fullName }}</span>
|
||||
<span class="text-gray-800">{{ userProjectStatusStore.userData?.fullName }}</span>
|
||||
<br>
|
||||
<span class="text-gray-600">{{ loginConnectStore.userData?.email }}</span>
|
||||
<span class="text-gray-600">{{ userProjectStatusStore.userData?.email }}</span>
|
||||
<br>
|
||||
<ExternalLink
|
||||
href="https://on.cypress.io/dashboard/profile"
|
||||
@@ -141,10 +141,10 @@
|
||||
</div>
|
||||
</template>
|
||||
</TopNav>
|
||||
<div v-if="!loginConnectStore.user.isLoggedIn">
|
||||
<div v-if="!userProjectStatusStore.user.isLoggedIn">
|
||||
<button
|
||||
class="flex text-gray-600 items-center group focus:outline-transparent"
|
||||
@click="loginConnectStore.openLoginConnectModal({ utmMedium: 'Nav' })"
|
||||
@click="userProjectStatusStore.openLoginConnectModal({ utmMedium: 'Nav' })"
|
||||
>
|
||||
<i-cy-profile_x16
|
||||
class="h-[16px] mr-[8px] w-[16px] block icon-dark-gray-500 icon-light-gray-100 group-hocus:icon-dark-indigo-500 group-hocus:icon-light-indigo-50"
|
||||
@@ -174,9 +174,9 @@ import interval from 'human-interval'
|
||||
import { sortBy } from 'lodash'
|
||||
import Tooltip from '../components/Tooltip.vue'
|
||||
import type { AllowedState } from '@packages/types'
|
||||
import { useLoginConnectStore } from '../store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '../store/user-project-status-store'
|
||||
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
gql`
|
||||
fragment HeaderBarContent_Auth on Query {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<LoginConnectModalsContent
|
||||
v-if="loginConnectStore.isLoginConnectOpen"
|
||||
v-if="userProjectStatusStore.isLoginConnectOpen"
|
||||
:gql="query.data.value"
|
||||
/>
|
||||
</template>
|
||||
@@ -30,9 +30,9 @@
|
||||
import LoginConnectModalsContent from './LoginConnectModalsContent.vue'
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import { LoginConnectModals_LoginConnectModalsQueryDocument } from '../generated/graphql'
|
||||
import { useLoginConnectStore } from '../store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '../store/user-project-status-store'
|
||||
import { whenever } from '@vueuse/core'
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
gql`
|
||||
query LoginConnectModals_LoginConnectModalsQuery {
|
||||
@@ -46,6 +46,6 @@ const executeQuery = async () => {
|
||||
await query.executeQuery()
|
||||
}
|
||||
|
||||
whenever(() => loginConnectStore.isLoginConnectOpen, executeQuery)
|
||||
whenever(() => userProjectStatusStore.isLoginConnectOpen, executeQuery)
|
||||
|
||||
</script>
|
||||
|
||||
@@ -3,13 +3,13 @@ import LoginConnectModalsContent from './LoginConnectModalsContent.vue'
|
||||
import { CloudUserStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { SelectCloudProjectModal_CreateCloudProjectDocument } from '../generated/graphql'
|
||||
|
||||
import { useLoginConnectStore } from '../store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '../store/user-project-status-store'
|
||||
|
||||
describe('<LoginConnectModalsContent />', () => {
|
||||
context('when user is logged out', () => {
|
||||
[undefined, 'testContent'].forEach((content) => {
|
||||
it(`shows login modal with utmContent: ${content}`, () => {
|
||||
const { openLoginConnectModal } = useLoginConnectStore()
|
||||
const { openLoginConnectModal } = useUserProjectStatusStore()
|
||||
|
||||
cy.mountFragment(LoginConnectModalsContentFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
@@ -52,7 +52,7 @@ describe('<LoginConnectModalsContent />', () => {
|
||||
context('when user is logged in', () => {
|
||||
[undefined, 'testContent'].forEach((content) => {
|
||||
it('shows "Create Project" state if project is not set up', () => {
|
||||
const { openLoginConnectModal, setUserFlag, setProjectFlag } = useLoginConnectStore()
|
||||
const { openLoginConnectModal, setUserFlag, setProjectFlag } = useUserProjectStatusStore()
|
||||
|
||||
setUserFlag('isLoggedIn', true)
|
||||
setUserFlag('isMemberOfOrganization', true)
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<template>
|
||||
<template v-if="loginConnectStore.isLoginConnectOpen && gqlRef">
|
||||
<template v-if="userProjectStatusStore.isLoginConnectOpen && gqlRef">
|
||||
<LoginModal
|
||||
v-if="userStatusMatches('isLoggedOut') || keepLoginOpen"
|
||||
v-if="cloudStatusMatches('isLoggedOut') || keepLoginOpen"
|
||||
:gql="gqlRef"
|
||||
:utm-medium="loginConnectStore.utmMedium"
|
||||
:utm-content="loginConnectStore.utmContent"
|
||||
:utm-medium="userProjectStatusStore.utmMedium"
|
||||
:utm-content="userProjectStatusStore.utmContent"
|
||||
@cancel="closeLoginConnectModal"
|
||||
@close="handleCloseLogin"
|
||||
/>
|
||||
<RecordRunModal
|
||||
v-else-if="userStatusMatches('needsRecordedRun')"
|
||||
:utm-medium="loginConnectStore.utmMedium"
|
||||
:utm-content="loginConnectStore.utmContent"
|
||||
v-else-if="cloudStatusMatches('needsRecordedRun')"
|
||||
:utm-medium="userProjectStatusStore.utmMedium"
|
||||
:utm-content="userProjectStatusStore.utmContent"
|
||||
@cancel="closeLoginConnectModal"
|
||||
/>
|
||||
<CloudConnectModals
|
||||
v-else-if="userStatusMatches('needsProjectConnect') || userStatusMatches('needsOrgConnect')"
|
||||
:show="loginConnectStore.user.isLoggedIn"
|
||||
v-else-if="cloudStatusMatches('needsProjectConnect') || cloudStatusMatches('needsOrgConnect')"
|
||||
:show="userProjectStatusStore.user.isLoggedIn"
|
||||
:gql="gqlRef"
|
||||
:utm-medium="loginConnectStore.utmMedium"
|
||||
:utm-content="loginConnectStore.utmContent"
|
||||
:utm-medium="userProjectStatusStore.utmMedium"
|
||||
:utm-content="userProjectStatusStore.utmContent"
|
||||
@cancel="closeLoginConnectModal"
|
||||
@success="closeLoginConnectModal"
|
||||
/>
|
||||
@@ -30,7 +30,7 @@ import { gql } from '@urql/vue'
|
||||
import type { LoginConnectModalsContentFragment } from '../generated/graphql'
|
||||
import LoginModal from './modals/LoginModal.vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useLoginConnectStore } from '../store/login-connect-store'
|
||||
import { useUserProjectStatusStore } from '../store/user-project-status-store'
|
||||
import CloudConnectModals from './modals/CloudConnectModals.vue'
|
||||
import RecordRunModal from './RecordRunModal.vue'
|
||||
import { debouncedWatch } from '@vueuse/core'
|
||||
@@ -50,15 +50,15 @@ const props = defineProps<{
|
||||
gql?: LoginConnectModalsContentFragment
|
||||
}>()
|
||||
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const { closeLoginConnectModal, userStatusMatches } = loginConnectStore
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
const { closeLoginConnectModal, cloudStatusMatches } = userProjectStatusStore
|
||||
|
||||
// use this to hold login open after the transition between logged out and logged in
|
||||
// this is to show the temporary "continue" state and its variations
|
||||
// that only exist if you have used the modal to log in
|
||||
const keepLoginOpen = ref(false)
|
||||
|
||||
watch(() => loginConnectStore.userStatus, (newVal, oldVal) => {
|
||||
watch(() => userProjectStatusStore.cloudStatus, (newVal, oldVal) => {
|
||||
if (oldVal === 'isLoggedOut' && newVal !== 'isLoggedOut') {
|
||||
keepLoginOpen.value = true
|
||||
}
|
||||
@@ -67,7 +67,7 @@ watch(() => loginConnectStore.userStatus, (newVal, oldVal) => {
|
||||
})
|
||||
|
||||
const handleCloseLogin = () => {
|
||||
if (userStatusMatches('allTasksCompleted')) {
|
||||
if (cloudStatusMatches('allTasksCompleted')) {
|
||||
closeLoginConnectModal()
|
||||
} else {
|
||||
keepLoginOpen.value = false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { UsePromptManager_SetProjectPreferencesDocument, UsePromptManager_SetGlobalPreferencesDocument } from '../../generated/graphql'
|
||||
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
|
||||
import { CloudStatus, ProjectStatus, useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
|
||||
import { isAllowedFeature } from '../../utils/isAllowedFeature'
|
||||
|
||||
gql`
|
||||
@@ -26,10 +26,12 @@ mutation UsePromptManager_SetGlobalPreferences($value: String!) {
|
||||
}
|
||||
`
|
||||
|
||||
type FeatureName = Parameters<typeof isAllowedFeature>[0]
|
||||
|
||||
export function usePromptManager () {
|
||||
const setProjectPreferencesMutation = useMutation(UsePromptManager_SetProjectPreferencesDocument)
|
||||
const setGlobalPreferencesMutation = useMutation(UsePromptManager_SetGlobalPreferencesDocument)
|
||||
const loginConnectStore = useLoginConnectStore()
|
||||
const userProjectStatusStore = useUserProjectStatusStore()
|
||||
|
||||
// TODO: get Nav CI prompts using this in #23768 and retire the old setPromptShown mutation
|
||||
function setPromptShown (slug: 'ci1' | 'orchestration1' | 'loginModalRecord') {
|
||||
@@ -40,13 +42,31 @@ export function usePromptManager () {
|
||||
return setGlobalPreferencesMutation.executeMutation({ value: JSON.stringify({ majorVersionWelcomeDismissed: { [majorVersion]: Date.now() } }) })
|
||||
}
|
||||
|
||||
const wrappedIsAllowedFeature = (featureName: 'specsListBanner' | 'docsCiPrompt') => {
|
||||
return isAllowedFeature(featureName, loginConnectStore)
|
||||
const wrappedIsAllowedFeature = (featureName: FeatureName, state: CloudStatus | ProjectStatus) => {
|
||||
return isAllowedFeature(featureName, userProjectStatusStore, state)
|
||||
}
|
||||
|
||||
const getEffectiveBannerState = (featureName: FeatureName) => {
|
||||
const cloudStatus = userProjectStatusStore.cloudStatus
|
||||
const projectStatus = userProjectStatusStore.projectStatus
|
||||
|
||||
if (featureName === 'specsListBanner') {
|
||||
if (cloudStatus !== 'allTasksCompleted' && wrappedIsAllowedFeature('specsListBanner', cloudStatus)) {
|
||||
return cloudStatus
|
||||
}
|
||||
|
||||
if (projectStatus !== 'allTasksCompleted' && wrappedIsAllowedFeature('specsListBanner', projectStatus)) {
|
||||
return projectStatus
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
setPromptShown,
|
||||
isAllowedFeature: wrappedIsAllowedFeature,
|
||||
setMajorVersionWelcomeDismissed,
|
||||
getEffectiveBannerState,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defaultMessages } from '@cy/i18n'
|
||||
import Tooltip from '../../components/Tooltip.vue'
|
||||
import { ref } from 'vue'
|
||||
import { CloudUserStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { useLoginConnectStore } from '../../store'
|
||||
import { useUserProjectStatusStore } from '../../store'
|
||||
|
||||
const text = defaultMessages.topNav
|
||||
|
||||
@@ -38,7 +38,7 @@ const mountSuccess = (viewer: TestCloudViewer = cloudViewer) => {
|
||||
...viewer,
|
||||
}
|
||||
|
||||
const { setUserFlag } = useLoginConnectStore()
|
||||
const { setUserFlag } = useUserProjectStatusStore()
|
||||
|
||||
setUserFlag('isLoggedIn', true)
|
||||
cy.mountFragment(LoginModalFragmentDoc, {
|
||||
|
||||
@@ -215,6 +215,13 @@
|
||||
"record": {
|
||||
"title": "Record your first run",
|
||||
"content": "Record a run to see your test results in Cypress Cloud. You can then optimize your test suite, debug failing and flaky tests, and integrate with your favorite tools."
|
||||
},
|
||||
"ct": {
|
||||
"title": "{0} component testing is available for this project",
|
||||
"content": "You can now use Cypress to develop and test individual components without running your whole application. Generate the config in just a few clicks.",
|
||||
"primaryAction": "Quick setup",
|
||||
"secondaryAction": "Read our guide",
|
||||
"dismissAction": "Give feedback"
|
||||
}
|
||||
},
|
||||
"runSelectedSpecs": "Run {n} spec | Run {n} specs"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './login-connect-store'
|
||||
export * from './user-project-status-store'
|
||||
|
||||
import { createPinia as _createPinia } from 'pinia'
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ interface LoginUserData {
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export interface LoginConnectState {
|
||||
export interface UserProjectStatusState {
|
||||
hasInitiallyLoaded: boolean
|
||||
isLoginConnectOpen: boolean
|
||||
utmMedium: string
|
||||
utmContent?: string
|
||||
cypressFirstOpened?: number
|
||||
testingType?: 'e2e' | 'component'
|
||||
user: {
|
||||
isLoggedIn: boolean
|
||||
loginError: boolean
|
||||
@@ -25,6 +26,8 @@ export interface LoginConnectState {
|
||||
hasNonExampleSpec: boolean
|
||||
isNotAuthorized: boolean
|
||||
isNotFound: boolean
|
||||
isCTConfigured: boolean
|
||||
hasDetectedCtFramework: boolean
|
||||
}
|
||||
userData?: LoginUserData
|
||||
promptsShown: {
|
||||
@@ -35,10 +38,7 @@ export interface LoginConnectState {
|
||||
_latestBannerShownTimeForTesting?: number
|
||||
}
|
||||
|
||||
// The user can be in only one status at a time.
|
||||
// These are specifically related to Cypress Cloud
|
||||
// and the progress from logging in to recording a run.
|
||||
export const userStatuses = [
|
||||
export const CLOUD_STATUSES = [
|
||||
'isLoggedOut',
|
||||
'needsOrgConnect',
|
||||
'needsProjectConnect',
|
||||
@@ -46,12 +46,19 @@ export const userStatuses = [
|
||||
'allTasksCompleted',
|
||||
] as const
|
||||
|
||||
export type UserStatus = typeof userStatuses[number]
|
||||
export const PROJECT_STATUSES = [
|
||||
'isComponentTestingCandidate',
|
||||
'allTasksCompleted',
|
||||
] as const
|
||||
|
||||
export const useLoginConnectStore = defineStore({
|
||||
id: 'loginConnect',
|
||||
export type CloudStatus = typeof CLOUD_STATUSES[number]
|
||||
|
||||
state (): LoginConnectState {
|
||||
export type ProjectStatus = typeof PROJECT_STATUSES[number]
|
||||
|
||||
export const useUserProjectStatusStore = defineStore({
|
||||
id: 'userProjectStatus',
|
||||
|
||||
state (): UserProjectStatusState {
|
||||
return {
|
||||
hasInitiallyLoaded: false,
|
||||
utmMedium: '',
|
||||
@@ -59,6 +66,7 @@ export const useLoginConnectStore = defineStore({
|
||||
isLoginConnectOpen: false,
|
||||
cypressFirstOpened: undefined,
|
||||
userData: undefined,
|
||||
testingType: undefined,
|
||||
user: {
|
||||
isLoggedIn: false,
|
||||
loginError: false,
|
||||
@@ -72,6 +80,8 @@ export const useLoginConnectStore = defineStore({
|
||||
hasNonExampleSpec: false,
|
||||
isNotAuthorized: false,
|
||||
isNotFound: false,
|
||||
isCTConfigured: false,
|
||||
hasDetectedCtFramework: false,
|
||||
},
|
||||
promptsShown: {},
|
||||
bannersState: {},
|
||||
@@ -92,10 +102,10 @@ export const useLoginConnectStore = defineStore({
|
||||
this.utmMedium = ''
|
||||
this.utmContent = undefined
|
||||
},
|
||||
setUserFlag (name: keyof LoginConnectState['user'], newVal: boolean) {
|
||||
setUserFlag (name: keyof UserProjectStatusState['user'], newVal: boolean) {
|
||||
this.user[name] = newVal
|
||||
},
|
||||
setProjectFlag (name: keyof LoginConnectState['project'], newVal: boolean) {
|
||||
setProjectFlag (name: keyof UserProjectStatusState['project'], newVal: boolean) {
|
||||
this.project[name] = newVal
|
||||
},
|
||||
setLoginError (error: boolean) {
|
||||
@@ -110,6 +120,9 @@ export const useLoginConnectStore = defineStore({
|
||||
setCypressFirstOpened (timestamp: number) {
|
||||
this.cypressFirstOpened = timestamp
|
||||
},
|
||||
setTestingType (testingType: 'e2e' | 'component' | undefined) {
|
||||
this.testingType = testingType
|
||||
},
|
||||
setBannersState (banners: BannersState) {
|
||||
this.bannersState = banners
|
||||
},
|
||||
@@ -118,34 +131,38 @@ export const useLoginConnectStore = defineStore({
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
userStatus (state): UserStatus {
|
||||
cloudStatus (state): CloudStatus {
|
||||
const { user, project } = state
|
||||
let userStatus: UserStatus
|
||||
|
||||
switch (true) {
|
||||
// the switch here ensures the uniqueness of states as we don't allow duplicate case labels
|
||||
// https://eslint.org/docs/latest/rules/no-duplicate-case
|
||||
case !user.isLoggedIn:
|
||||
userStatus = 'isLoggedOut'
|
||||
break
|
||||
return 'isLoggedOut'
|
||||
case user.isLoggedIn && user.isOrganizationLoaded && !user.isMemberOfOrganization:
|
||||
userStatus = 'needsOrgConnect'
|
||||
break
|
||||
return 'needsOrgConnect'
|
||||
case user.isLoggedIn && user.isMemberOfOrganization && !project.isProjectConnected && project.isConfigLoaded:
|
||||
userStatus = 'needsProjectConnect'
|
||||
break
|
||||
return 'needsProjectConnect'
|
||||
case user.isLoggedIn && user.isMemberOfOrganization && project.isProjectConnected && project.hasNoRecordedRuns && project.hasNonExampleSpec && project.isConfigLoaded:
|
||||
userStatus = 'needsRecordedRun'
|
||||
break
|
||||
return 'needsRecordedRun'
|
||||
default:
|
||||
userStatus = 'allTasksCompleted'
|
||||
return 'allTasksCompleted'
|
||||
}
|
||||
},
|
||||
projectStatus (state): ProjectStatus {
|
||||
const { project } = state
|
||||
|
||||
if (state.testingType === 'e2e' && !project.isCTConfigured && project.hasDetectedCtFramework) {
|
||||
return 'isComponentTestingCandidate'
|
||||
}
|
||||
|
||||
return userStatus
|
||||
return 'allTasksCompleted'
|
||||
},
|
||||
userStatusMatches () {
|
||||
// casting here since ts seems to need a little extra help in this 'return a function from a getter' situation
|
||||
return (status: UserStatus) => this.userStatus as unknown as UserStatus === status
|
||||
cloudStatusMatches () {
|
||||
return (status: CloudStatus) => this.cloudStatus === status
|
||||
},
|
||||
projectStatusMatches () {
|
||||
return (status: ProjectStatus) => this.projectStatus === status
|
||||
},
|
||||
latestBannerShownTime (state) {
|
||||
return state._latestBannerShownTimeForTesting
|
||||
@@ -155,4 +172,4 @@ export const useLoginConnectStore = defineStore({
|
||||
},
|
||||
})
|
||||
|
||||
export type LoginConnectStore = ReturnType<typeof useLoginConnectStore>
|
||||
export type UserProjectStatusStore = ReturnType<typeof useUserProjectStatusStore>
|
||||
23
packages/frontend-shared/src/utils/icons.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import LogoWebpack from '../assets/logos/webpack.svg'
|
||||
import LogoVite from '../assets/logos/vite.svg'
|
||||
import LogoNext from '../assets/logos/nextjs.svg'
|
||||
import LogoNuxt from '../assets/logos/nuxt.svg'
|
||||
import LogoVue from '../assets/logos/vue.svg'
|
||||
import LogoReact from '../assets/logos/react.svg'
|
||||
import LogoAngular from '../assets/logos/angular.svg'
|
||||
import LogoSvelte from '../assets/logos/svelte.svg'
|
||||
|
||||
export const FrameworkBundlerLogos = {
|
||||
webpack: LogoWebpack,
|
||||
vite: LogoVite,
|
||||
vue2: LogoVue,
|
||||
vue3: LogoVue,
|
||||
vueclivue2: LogoVue,
|
||||
vueclivue3: LogoVue,
|
||||
nextjs: LogoNext,
|
||||
nuxtjs: LogoNuxt,
|
||||
react: LogoReact,
|
||||
reactscripts: LogoReact,
|
||||
angular: LogoAngular,
|
||||
svelte: LogoSvelte,
|
||||
} as const
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isAllowedFeature } from './isAllowedFeature'
|
||||
import { LoginConnectStore, useLoginConnectStore, userStatuses } from '../store'
|
||||
import type { UserStatus } from '../store'
|
||||
import { UserProjectStatusStore, useUserProjectStatusStore, CLOUD_STATUSES, ProjectStatus } from '../store'
|
||||
import type { CloudStatus } from '../store'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import interval from 'human-interval'
|
||||
|
||||
@@ -12,14 +12,14 @@ const bannerIds = {
|
||||
} as const
|
||||
|
||||
describe('isAllowedFeature', () => {
|
||||
let store: LoginConnectStore
|
||||
let store: UserProjectStatusStore
|
||||
|
||||
// this setup function acts as a test of the userStatus
|
||||
// getter in loginConnectStore, since we set the individual flags here
|
||||
// getter in userProjectStatusStore, since we set the individual flags here
|
||||
// and assert on the expected user status derived from those flags
|
||||
// and provided by loginConnectStore.userStatus
|
||||
const setUpStatus = (status: UserStatus) => {
|
||||
const { setCypressFirstOpened, setPromptShown, setUserFlag, setProjectFlag } = store
|
||||
// and provided by userProjectStatusStore.userStatus
|
||||
const setUpStatus = (status: CloudStatus | ProjectStatus) => {
|
||||
const { setCypressFirstOpened, setPromptShown, setTestingType, setUserFlag, setProjectFlag } = store
|
||||
|
||||
// set a default valid number of days since first open & nav prompt shown
|
||||
// individual tests may override
|
||||
@@ -29,12 +29,12 @@ describe('isAllowedFeature', () => {
|
||||
switch (status) {
|
||||
case 'isLoggedOut':
|
||||
setUserFlag('isLoggedIn', false)
|
||||
expect(store.userStatus).to.eq('isLoggedOut')
|
||||
expect(store.cloudStatus).to.eq('isLoggedOut')
|
||||
break
|
||||
case 'needsOrgConnect':
|
||||
setUserFlag('isLoggedIn', true)
|
||||
setUserFlag('isOrganizationLoaded', true)
|
||||
expect(store.userStatus).to.eq('needsOrgConnect')
|
||||
expect(store.cloudStatus).to.eq('needsOrgConnect')
|
||||
break
|
||||
case 'needsProjectConnect':
|
||||
setUserFlag('isLoggedIn', true)
|
||||
@@ -42,7 +42,7 @@ describe('isAllowedFeature', () => {
|
||||
setUserFlag('isOrganizationLoaded', true)
|
||||
setProjectFlag('isConfigLoaded', true)
|
||||
setProjectFlag('isProjectConnected', false)
|
||||
expect(store.userStatus).to.eq('needsProjectConnect')
|
||||
expect(store.cloudStatus).to.eq('needsProjectConnect')
|
||||
break
|
||||
case 'needsRecordedRun':
|
||||
setUserFlag('isLoggedIn', true)
|
||||
@@ -52,7 +52,7 @@ describe('isAllowedFeature', () => {
|
||||
setProjectFlag('isConfigLoaded', true)
|
||||
setProjectFlag('hasNonExampleSpec', true)
|
||||
|
||||
expect(store.userStatus).to.eq('needsRecordedRun')
|
||||
expect(store.cloudStatus).to.eq('needsRecordedRun')
|
||||
break
|
||||
case 'allTasksCompleted':
|
||||
setUserFlag('isLoggedIn', true)
|
||||
@@ -60,7 +60,18 @@ describe('isAllowedFeature', () => {
|
||||
setProjectFlag('isProjectConnected', true)
|
||||
setProjectFlag('hasNoRecordedRuns', false)
|
||||
|
||||
expect(store.userStatus).to.eq('allTasksCompleted')
|
||||
expect(store.cloudStatus).to.eq('allTasksCompleted')
|
||||
break
|
||||
case 'isComponentTestingCandidate':
|
||||
setTestingType('e2e')
|
||||
setUserFlag('isLoggedIn', true)
|
||||
setUserFlag('isMemberOfOrganization', true)
|
||||
setProjectFlag('isProjectConnected', true)
|
||||
setProjectFlag('hasNoRecordedRuns', false)
|
||||
setProjectFlag('isCTConfigured', false)
|
||||
setProjectFlag('hasDetectedCtFramework', true)
|
||||
|
||||
expect(store.projectStatus).to.eq('isComponentTestingCandidate')
|
||||
break
|
||||
default:
|
||||
return
|
||||
@@ -68,25 +79,25 @@ describe('isAllowedFeature', () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
store = useLoginConnectStore()
|
||||
store = useUserProjectStatusStore()
|
||||
store.setProjectFlag('hasNonExampleSpec', true)
|
||||
})
|
||||
|
||||
describe('specsListBanner', () => {
|
||||
context('at least one non-example spec has been written', () => {
|
||||
context('banners HAVE NOT been dismissed', () => {
|
||||
userStatuses.forEach((status) => {
|
||||
CLOUD_STATUSES.forEach((status) => {
|
||||
if (status === 'allTasksCompleted') {
|
||||
it('returns false when user has no actions to take', () => {
|
||||
setUpStatus('allTasksCompleted')
|
||||
const result = isAllowedFeature('specsListBanner', store)
|
||||
const result = isAllowedFeature('specsListBanner', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
} else {
|
||||
it(`returns true for status ${status}`, () => {
|
||||
setUpStatus(status)
|
||||
const result = isAllowedFeature('specsListBanner', store)
|
||||
const result = isAllowedFeature('specsListBanner', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.true
|
||||
})
|
||||
@@ -107,7 +118,7 @@ describe('isAllowedFeature', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const result = isAllowedFeature('specsListBanner', store)
|
||||
const result = isAllowedFeature('specsListBanner', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
@@ -115,7 +126,7 @@ describe('isAllowedFeature', () => {
|
||||
})
|
||||
|
||||
context('banners have been disabled for testing', () => {
|
||||
userStatuses.forEach((status) => {
|
||||
CLOUD_STATUSES.forEach((status) => {
|
||||
it(`returns false for status ${ status }`, () => {
|
||||
setUpStatus(status)
|
||||
|
||||
@@ -123,7 +134,7 @@ describe('isAllowedFeature', () => {
|
||||
_disabled: true,
|
||||
})
|
||||
|
||||
const result = isAllowedFeature('specsListBanner', store)
|
||||
const result = isAllowedFeature('specsListBanner', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
@@ -131,12 +142,12 @@ describe('isAllowedFeature', () => {
|
||||
})
|
||||
|
||||
context('cypress was first opened less than 4 days ago', () => {
|
||||
userStatuses.forEach((status) => {
|
||||
CLOUD_STATUSES.forEach((status) => {
|
||||
it(`returns false for status ${status}`, () => {
|
||||
setUpStatus(status)
|
||||
store.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
|
||||
const result = isAllowedFeature('specsListBanner', store)
|
||||
const result = isAllowedFeature('specsListBanner', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
@@ -144,12 +155,12 @@ describe('isAllowedFeature', () => {
|
||||
})
|
||||
|
||||
context('nav CI prompt was shown less than one day ago', () => {
|
||||
userStatuses.forEach((status) => {
|
||||
CLOUD_STATUSES.forEach((status) => {
|
||||
it(`returns false for status ${status}`, () => {
|
||||
setUpStatus(status)
|
||||
store.setPromptShown('ci1', Date.now() - interval('23 hours'))
|
||||
|
||||
const result = isAllowedFeature('specsListBanner', store)
|
||||
const result = isAllowedFeature('specsListBanner', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
@@ -158,13 +169,13 @@ describe('isAllowedFeature', () => {
|
||||
})
|
||||
|
||||
context('no non-example specs have been written', () => {
|
||||
userStatuses.forEach((status) => {
|
||||
CLOUD_STATUSES.forEach((status) => {
|
||||
if (status === 'allTasksCompleted' || status === 'needsRecordedRun') {
|
||||
it(`returns false for status ${status}`, () => {
|
||||
setUpStatus(status)
|
||||
store.setProjectFlag('hasNonExampleSpec', false)
|
||||
|
||||
const result = isAllowedFeature('specsListBanner', store)
|
||||
const result = isAllowedFeature('specsListBanner', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
@@ -173,7 +184,7 @@ describe('isAllowedFeature', () => {
|
||||
setUpStatus(status)
|
||||
store.setProjectFlag('hasNonExampleSpec', false)
|
||||
|
||||
const result = isAllowedFeature('specsListBanner', store)
|
||||
const result = isAllowedFeature('specsListBanner', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.true
|
||||
})
|
||||
@@ -184,10 +195,10 @@ describe('isAllowedFeature', () => {
|
||||
|
||||
describe('docsCiPrompt', () => {
|
||||
context('a banner WAS NOT shown in the last day', () => {
|
||||
userStatuses.forEach((status) => {
|
||||
CLOUD_STATUSES.forEach((status) => {
|
||||
it(`returns true with status ${ status } `, () => {
|
||||
setUpStatus(status)
|
||||
const result = isAllowedFeature('docsCiPrompt', store)
|
||||
const result = isAllowedFeature('docsCiPrompt', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.true
|
||||
})
|
||||
@@ -195,11 +206,11 @@ describe('isAllowedFeature', () => {
|
||||
})
|
||||
|
||||
context('a banner WAS shown in the last day', () => {
|
||||
userStatuses.forEach((status) => {
|
||||
CLOUD_STATUSES.forEach((status) => {
|
||||
it(`returns false with status ${ status } `, () => {
|
||||
setUpStatus(status)
|
||||
store.setLatestBannerShownTime(Date.now() - interval('23 hours'))
|
||||
const result = isAllowedFeature('docsCiPrompt', store)
|
||||
const result = isAllowedFeature('docsCiPrompt', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
@@ -207,11 +218,11 @@ describe('isAllowedFeature', () => {
|
||||
})
|
||||
|
||||
context('cypress was first opened less than 4 days ago', () => {
|
||||
userStatuses.forEach((status) => {
|
||||
CLOUD_STATUSES.forEach((status) => {
|
||||
it(`returns false for status ${ status } `, () => {
|
||||
setUpStatus(status)
|
||||
store.setCypressFirstOpened(Date.now() - interval('3 days'))
|
||||
const result = isAllowedFeature('docsCiPrompt', store)
|
||||
const result = isAllowedFeature('docsCiPrompt', store, store.cloudStatus)
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import interval from 'human-interval'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import type { LoginConnectStore, UserStatus } from '../store'
|
||||
import type { UserProjectStatusStore, CloudStatus, ProjectStatus } from '../store'
|
||||
|
||||
type Feature = 'specsListBanner' | 'docsCiPrompt'
|
||||
type RulesSet = { base: boolean[] } & Partial<Record<UserStatus, boolean[]>>
|
||||
type RulesSet = { base: boolean[] } & Partial<Record<CloudStatus | ProjectStatus, boolean[]>>
|
||||
type Rules = Record<Feature, RulesSet>
|
||||
type BannerId = typeof BannerIds[keyof typeof BannerIds]
|
||||
|
||||
const bannerIds = {
|
||||
const BANNER_ID_BY_STATE: Partial<Record<CloudStatus | ProjectStatus, BannerId>> = {
|
||||
isLoggedOut: BannerIds.ACI_082022_LOGIN,
|
||||
needsOrgConnect: BannerIds.ACI_082022_CREATE_ORG,
|
||||
needsProjectConnect: BannerIds.ACI_082022_CONNECT_PROJECT,
|
||||
needsRecordedRun: BannerIds.ACI_082022_RECORD,
|
||||
isComponentTestingCandidate: BannerIds.CT_052023_AVAILABLE,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,20 +35,18 @@ const minTimeSinceEvent = (eventTime: number | undefined, waitTime: string) => {
|
||||
return (Date.now() - eventTime) > waitTimestamp
|
||||
}
|
||||
|
||||
export const IATR_RELEASE = new Date('2023-01-31T00:00:00').getTime()
|
||||
|
||||
export const isAllowedFeature = (
|
||||
featureName: Feature,
|
||||
loginConnectStore: LoginConnectStore,
|
||||
userProjectStatusStore: UserProjectStatusStore,
|
||||
state: CloudStatus | ProjectStatus = 'allTasksCompleted',
|
||||
) => {
|
||||
const {
|
||||
cypressFirstOpened,
|
||||
promptsShown,
|
||||
latestBannerShownTime,
|
||||
bannersState,
|
||||
userStatus,
|
||||
project,
|
||||
} = loginConnectStore
|
||||
} = userProjectStatusStore
|
||||
|
||||
const events = {
|
||||
cypressFirstOpened,
|
||||
@@ -56,16 +56,34 @@ export const isAllowedFeature = (
|
||||
}
|
||||
|
||||
function bannerForCurrentStatusWasNotDismissed () {
|
||||
const bannerId = bannerIds[userStatus]
|
||||
if (!state) {
|
||||
return true
|
||||
}
|
||||
|
||||
const bannerId = BANNER_ID_BY_STATE[state]
|
||||
|
||||
if (!bannerId) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !bannersState?.[bannerId]?.dismissed
|
||||
}
|
||||
|
||||
function noOtherSmartBannerShownWithin (interval: string) {
|
||||
const currentBannerId = BANNER_ID_BY_STATE[state]
|
||||
|
||||
return Object.entries(BannerIds)
|
||||
.map(([_, bannerId]) => bannerId)
|
||||
.filter((bannerId) => bannerId !== currentBannerId)
|
||||
.map((bannerId) => bannersState[bannerId]?.dismissed)
|
||||
.every((bannerDismissed) => minTimeSinceEvent(bannerDismissed, interval))
|
||||
}
|
||||
|
||||
function bannersAreNotDisabledForTesting () {
|
||||
return !bannersState?._disabled
|
||||
}
|
||||
|
||||
// For each feature, we define an array of rules for every `UserStatus`.
|
||||
// For each feature, we define an array of rules for every `BannerStatus`.
|
||||
// The `base` rule is applied to all statuses, additional rules are
|
||||
// nested in their respective statuses.
|
||||
const rules: Rules = {
|
||||
@@ -83,6 +101,9 @@ export const isAllowedFeature = (
|
||||
needsOrgConnect: [],
|
||||
needsProjectConnect: [],
|
||||
isLoggedOut: [],
|
||||
isComponentTestingCandidate: [
|
||||
noOtherSmartBannerShownWithin('2 days'),
|
||||
],
|
||||
},
|
||||
docsCiPrompt: {
|
||||
base: [
|
||||
@@ -94,6 +115,7 @@ export const isAllowedFeature = (
|
||||
needsProjectConnect: [],
|
||||
isLoggedOut: [],
|
||||
allTasksCompleted: [],
|
||||
isComponentTestingCandidate: [],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -102,7 +124,7 @@ export const isAllowedFeature = (
|
||||
// if the `userStatus` is not explicitly listed for a feature, then
|
||||
// we don't have anything that we are allowed to show for that status
|
||||
// so the fallback rules array of [false] is used
|
||||
const statusSpecificRules = rules[featureName][userStatus] ?? [false]
|
||||
const statusSpecificRules = (state && rules[featureName][state]) ?? [false]
|
||||
|
||||
const rulesToCheck = baseRules.concat(statusSpecificRules)
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ const cypressCSS = require('@cypress-design/css')
|
||||
module.exports = {
|
||||
presets: [cypressCSS.TailwindConfig()],
|
||||
content: {
|
||||
files: ['./src/**/*.{vue,js,ts,jsx,tsx,scss,css}'],
|
||||
files: [
|
||||
'../../node_modules/@cypress-design/vue-*/dist/*.js',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx,scss,css}',
|
||||
],
|
||||
extract: ['vue', 'js', 'tsx'].reduce((acc, ext) => {
|
||||
acc[ext] = cypressCSS.TailwindIconExtractor
|
||||
|
||||
|
||||
@@ -1624,6 +1624,11 @@ type Mutation {
|
||||
Parse a JS or TS file to see any exported React components that are defined in the file
|
||||
"""
|
||||
getReactComponentsFromFile(filePath: String!): ReactComponentResponse
|
||||
|
||||
"""
|
||||
Scan dependencies to determine what, if any, CT frameworks are installed
|
||||
"""
|
||||
initializeCtFrameworks: Boolean
|
||||
internal_clearAllProjectPreferencesCache: Boolean
|
||||
internal_clearLatestProjectCache: Boolean
|
||||
internal_clearProjectPreferencesCache(projectTitle: String!): Boolean
|
||||
@@ -1705,9 +1710,18 @@ type Mutation {
|
||||
reconfigureProject: Boolean!
|
||||
|
||||
"""
|
||||
Dispatch an event to Cypress Cloud to be recorded. Events are completely anonymous and are only used to identify aggregate usage patterns across all Cypress users.
|
||||
Dispatch an event to Cypress Cloud to be recorded. Events are used only to derive aggregate usage patterns across all Cypress instances.
|
||||
"""
|
||||
recordEvent(campaign: String!, cohort: String, medium: String!, messageId: String!): Boolean
|
||||
recordEvent(
|
||||
campaign: String!
|
||||
cohort: String
|
||||
includeMachineId: Boolean
|
||||
medium: String!
|
||||
messageId: String!
|
||||
|
||||
"""(optional) stringified JSON object with supplemental data"""
|
||||
payload: String
|
||||
): Boolean
|
||||
|
||||
"""
|
||||
Signal that we are explicitly refetching remote data and should not use the server cache
|
||||
@@ -1959,6 +1973,11 @@ type Query {
|
||||
"""local settings on a device-by-device basis"""
|
||||
localSettings: LocalSettings!
|
||||
|
||||
"""
|
||||
Unique node machine identifier for this instance - may be nil if unable to resolve
|
||||
"""
|
||||
machineId: String
|
||||
|
||||
"""Metadata about the migration, null if we aren't showing it"""
|
||||
migration: Migration
|
||||
node(id: ID!): Node
|
||||
|
||||
@@ -709,12 +709,16 @@ export const mutation = mutationType({
|
||||
|
||||
t.field('recordEvent', {
|
||||
type: 'Boolean',
|
||||
description: 'Dispatch an event to Cypress Cloud to be recorded. Events are completely anonymous and are only used to identify aggregate usage patterns across all Cypress users.',
|
||||
description: 'Dispatch an event to Cypress Cloud to be recorded. Events are used only to derive aggregate usage patterns across all Cypress instances.',
|
||||
args: {
|
||||
includeMachineId: booleanArg({}),
|
||||
campaign: nonNull(stringArg({})),
|
||||
messageId: nonNull(stringArg({})),
|
||||
medium: nonNull(stringArg({})),
|
||||
cohort: stringArg({}),
|
||||
payload: stringArg({
|
||||
description: '(optional) stringified JSON object with supplemental data',
|
||||
}),
|
||||
},
|
||||
resolve: (source, args, ctx) => {
|
||||
return ctx.actions.eventCollector.recordEvent({
|
||||
@@ -722,7 +726,8 @@ export const mutation = mutationType({
|
||||
messageId: args.messageId,
|
||||
medium: args.medium,
|
||||
cohort: args.cohort || undefined,
|
||||
})
|
||||
payload: (args.payload && JSON.parse(args.payload)) || undefined,
|
||||
}, args.includeMachineId ?? false)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -804,5 +809,16 @@ export const mutation = mutationType({
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
t.field('initializeCtFrameworks', {
|
||||
description: 'Scan dependencies to determine what, if any, CT frameworks are installed',
|
||||
type: 'Boolean',
|
||||
resolve: async (source, args, ctx) => {
|
||||
await ctx.actions.wizard.detectFrameworks()
|
||||
await ctx.actions.wizard.initializeFramework()
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -141,6 +141,11 @@ export const Query = objectType({
|
||||
return ctx.graphql.resolveNode(args.id, ctx, info) as any
|
||||
},
|
||||
})
|
||||
|
||||
t.string('machineId', {
|
||||
description: 'Unique node machine identifier for this instance - may be nil if unable to resolve',
|
||||
resolve: async (source, args, ctx) => await ctx.coreData.machineId,
|
||||
})
|
||||
},
|
||||
sourceType: {
|
||||
module: '@packages/graphql',
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { FrameworkBundlerLogos } from '../utils/icons'
|
||||
import { FrameworkBundlerLogos } from '@packages/frontend-shared/src/utils/icons'
|
||||
import type { Option, FrameworkOption } from './types'
|
||||
import Select from '@cy/components/Select.vue'
|
||||
import type {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import LogoWebpack from '../images/logos/webpack.svg'
|
||||
import LogoVite from '../images/logos/vite.svg'
|
||||
import LogoNext from '../images/logos/nextjs.svg'
|
||||
import LogoNuxt from '../images/logos/nuxt.svg'
|
||||
import LogoVue from '../images/logos/vue.svg'
|
||||
import LogoReact from '../images/logos/react.svg'
|
||||
import LogoAngular from '../images/logos/angular.svg'
|
||||
import LogoSvelte from '../images/logos/svelte.svg'
|
||||
|
||||
import type { SupportedBundlers } from '../generated/graphql'
|
||||
|
||||
export const FrameworkBundlerLogos: Record<string | SupportedBundlers, string> = {
|
||||
webpack: LogoWebpack,
|
||||
vite: LogoVite,
|
||||
vue2: LogoVue,
|
||||
vue3: LogoVue,
|
||||
vueclivue2: LogoVue,
|
||||
vueclivue3: LogoVue,
|
||||
nextjs: LogoNext,
|
||||
nuxtjs: LogoNuxt,
|
||||
react: LogoReact,
|
||||
reactscripts: LogoReact,
|
||||
angular: LogoAngular,
|
||||
svelte: LogoSvelte,
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
const config = require('@packages/frontend-shared/tailwind.config.cjs')
|
||||
|
||||
config.content.files = [
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx,scss,css}', //
|
||||
'../frontend-shared/src/**/*.{vue,js,ts,jsx,tsx,scss,css}',
|
||||
],
|
||||
config.content.files.push('../frontend-shared/src/**/*.{vue,js,ts,jsx,tsx,scss,css}')
|
||||
|
||||
module.exports = config
|
||||
|
||||
@@ -47,6 +47,7 @@ export const BannerIds = {
|
||||
ACI_082022_CREATE_ORG: 'aci_082022_createOrganization',
|
||||
ACI_082022_CONNECT_PROJECT: 'aci_082022_connectProject',
|
||||
ACI_082022_RECORD: 'aci_082022_record',
|
||||
CT_052023_AVAILABLE: 'ct_052023_available',
|
||||
} as const
|
||||
|
||||
type BannerKeys = keyof typeof BannerIds
|
||||
|
||||
58
yarn.lock
@@ -2222,10 +2222,20 @@
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@cypress-design/css@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/css/-/css-0.11.0.tgz#61d6c0a58dfbce610df5d992d1fe310e4110ac56"
|
||||
integrity sha512-kZDjTfSTnzvROBYtCTTqf6/s0VBUroXx9O9P8jM5SCjpE221wZa3pFyPt78iCQ00E/AyIt0yUlb5mz22QRRKCQ==
|
||||
"@cypress-design/constants-button@*":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/constants-button/-/constants-button-0.1.0.tgz#d32788484eda0a05ce72a53d9529ec7284174e77"
|
||||
integrity sha512-HE5QQT6T8GDSgBDUBywOdnTvRXErrvGHm8YQuvj2iqzCTv9B0RhNQUVKSoyN9/LKyVcIXoa/ZaJLeTMVTlf+pg==
|
||||
|
||||
"@cypress-design/constants-statusicon@*":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/constants-statusicon/-/constants-statusicon-0.1.0.tgz#2b4f56bb0838af8635fb655d4136a0a5980c6f9a"
|
||||
integrity sha512-xp1iGxJAV5Qj+FWcZ4mpQ00UqD5n1syFbnqOvm8WLuRie2lw0hyz5/lncJMMU7mheMyRtM+KU9+VL0+JB8mkLw==
|
||||
|
||||
"@cypress-design/css@*", "@cypress-design/css@^0.13.1":
|
||||
version "0.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/css/-/css-0.13.1.tgz#6144c38167ed4d514135257b5d942c52c359e8ab"
|
||||
integrity sha512-wWlDIzk6NL3/a2xBHi5XaJ5P/Y+YVC9r02ZpSueSwZviAklV02bKcKjHTDFd6OdsEKTRwj8bEzmItwFi3sNvpg==
|
||||
dependencies:
|
||||
"@windicss/plugin-interaction-variants" "^1.0.0"
|
||||
lodash "^4.17.21"
|
||||
@@ -2235,24 +2245,34 @@
|
||||
windicss "^3.5.6"
|
||||
windicss-webpack-plugin "^1.7.8"
|
||||
|
||||
"@cypress-design/icon-registry@^0.21.0":
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/icon-registry/-/icon-registry-0.21.0.tgz#bddb07da4af6ae6e1883056d86809edf8348343b"
|
||||
integrity sha512-aPB/BjZNGWewf6YX9htgISZ69hIVn/YppNxC4hp1v8I4ZYxkIaOHvd1Q7HR9hxXdLwo8YL1d9DAJPqKGxfYCPg==
|
||||
|
||||
"@cypress-design/vue-icon@*", "@cypress-design/vue-icon@0.20.0":
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/vue-icon/-/vue-icon-0.20.0.tgz#eaf029a877459524c7ff259c19e88422606d3058"
|
||||
integrity sha512-Gc7V73hOWDwAaCWRMTCFGQYQ8V6ILb3kJtRcPK8u1UlYcNbfv9/oZoHZpgwm71J9VArHHUa9RBey6cp1BsT+gA==
|
||||
"@cypress-design/icon-registry@^0.22.0":
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/icon-registry/-/icon-registry-0.22.0.tgz#cdaa45c32ebe2d5cb0df6c3220bac735d8c3ee0f"
|
||||
integrity sha512-VyH7khgBSOJfJCTyMfDoB0YReyiQUtD/VyCEjmkiZOCjPcVF0LEH3lc3zzz+tAXjM+VJtx2TIJi5uOs3mDtRlQ==
|
||||
dependencies:
|
||||
"@cypress-design/icon-registry" "^0.21.0"
|
||||
"@cypress-design/css" "*"
|
||||
|
||||
"@cypress-design/vue-statusicon@0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/vue-statusicon/-/vue-statusicon-0.3.0.tgz#f43c42c0053c49b089693f0a1462441f789ffb3c"
|
||||
integrity sha512-992Onatd2ZTi+tocV7eEgufz1K5deG9lLO3RRryuiy5LNBNmhb/STrWGKopqL4G/L6F9polf0nWMBmzYEVwoWg==
|
||||
"@cypress-design/vue-button@0.9.2":
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/vue-button/-/vue-button-0.9.2.tgz#8445282e9740124f9ff366ae3ebc4dc731757a12"
|
||||
integrity sha512-Zcs50D/VwwthXF6aqVDz1f/Z1csbu8q5SzGAqRNxlLhgGR1kQS+0WgA2Q+nj4OHfL/Jh6uGTqFZrdNXVMRLtbA==
|
||||
dependencies:
|
||||
"@cypress-design/icon-registry" "^0.21.0"
|
||||
"@cypress-design/constants-button" "*"
|
||||
|
||||
"@cypress-design/vue-icon@*", "@cypress-design/vue-icon@0.22.1":
|
||||
version "0.22.1"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/vue-icon/-/vue-icon-0.22.1.tgz#f415221fae929b94ad75444860e7ee1d317c9dc4"
|
||||
integrity sha512-SurEkyRHsQjNeCxhcXujOiAr8+puU+fC0w9fqgVgNxPffelvcXOKgGnalk/FFD6bN9T5QCZSA0yNprOzp9PxSw==
|
||||
dependencies:
|
||||
"@cypress-design/icon-registry" "^0.22.0"
|
||||
|
||||
"@cypress-design/vue-statusicon@0.4.3":
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@cypress-design/vue-statusicon/-/vue-statusicon-0.4.3.tgz#39ade5e5e0f3b11337d4e07a91f5302fe257e3fa"
|
||||
integrity sha512-cTr7BSmhgZt2Ka0+Qa+QsKYcu4TikQ1B+iBGNVGEZFBE+o417mFHLyDLcqE3r9IC3zx8efI4VuKTxJMaYYyXRA==
|
||||
dependencies:
|
||||
"@cypress-design/constants-statusicon" "*"
|
||||
"@cypress-design/icon-registry" "^0.22.0"
|
||||
"@cypress-design/vue-icon" "*"
|
||||
|
||||
"@cypress/commit-info@2.2.0":
|
||||
|
||||