mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-01 20:39:57 -05:00
Merge branch 'develop' into add_to_triage_project_workflow
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cypress",
|
||||
"version": "10.5.0",
|
||||
"version": "10.6.0",
|
||||
"description": "Cypress.io end to end testing tool",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// <reference path="../../../../frontend-shared/cypress/support/component.ts" />
|
||||
import '../../../../frontend-shared/cypress/support/component.ts'
|
||||
import { registerMountFn } from '@packages/frontend-shared/cypress/support/common'
|
||||
// ***********************************************************
|
||||
// This example support/index.ts is processed and
|
||||
|
||||
@@ -14,14 +14,7 @@ describe('App: Spec List - Flaky Indicator', () => {
|
||||
|
||||
cy.remoteGraphQLIntercept(async (obj) => {
|
||||
await new Promise((r) => setTimeout(r, 20))
|
||||
if (obj.result.data && 'cloudProjectBySlug' in obj.result.data) {
|
||||
obj.result.data.cloudProjectBySlug = {
|
||||
__typename: 'CloudProject',
|
||||
retrievedAt: new Date().toISOString(),
|
||||
id: `id${obj.variables.slug}`,
|
||||
projectId: 'abc123',
|
||||
}
|
||||
} else if (obj.result.data && 'cloudSpecByPath' in obj.result.data) {
|
||||
if (obj.result.data && 'cloudSpecByPath' in obj.result.data) {
|
||||
if (obj.variables.specPath.includes('123.spec.js')) {
|
||||
obj.result.data.cloudSpecByPath = {
|
||||
__typename: 'CloudProjectSpec',
|
||||
|
||||
@@ -314,7 +314,7 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
|
||||
cy.get(averageDurationSelector('accounts_new.spec.js')).contains('2:03')
|
||||
})
|
||||
|
||||
it('lazily loads data for off-screen specs', () => {
|
||||
it('lazily loads data for off-screen specs', { viewportHeight: 500 }, () => {
|
||||
// make sure the virtualized list didn't load z008.spec.js
|
||||
cy.get(specRowSelector('z008.spec.js')).should('not.exist')
|
||||
|
||||
|
||||
@@ -655,6 +655,7 @@ describe('Growth Prompts Can Open Automatically', () => {
|
||||
firstOpened: 1609459200000,
|
||||
lastOpened: 1609459200000,
|
||||
promptsShown: {},
|
||||
banners: { _disabled: true },
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -672,6 +673,7 @@ describe('Growth Prompts Can Open Automatically', () => {
|
||||
firstOpened: 1609459200000,
|
||||
lastOpened: 1609459200000,
|
||||
promptsShown: { ci1: 1609459200000 },
|
||||
banners: { _disabled: true },
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import SpecsListBanners from './SpecsListBanners.vue'
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { SpecsListBannersFragmentDoc } from '../generated/graphql-test'
|
||||
import { SpecsListBannersFragment, SpecsListBannersFragmentDoc } from '../generated/graphql-test'
|
||||
import interval from 'human-interval'
|
||||
import { CloudUserStubs, CloudProjectStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { AllowedState, BannerIds } from '@packages/types'
|
||||
import { assignIn, set } from 'lodash'
|
||||
|
||||
const AlertSelector = 'alert-header'
|
||||
const AlertCloseBtnSelector = 'alert-suffix-icon'
|
||||
|
||||
type BannerKey = keyof typeof BannerIds
|
||||
type BannerId = typeof BannerIds[BannerKey]
|
||||
|
||||
describe('<SpecsListBanners />', () => {
|
||||
const validateBaseRender = () => {
|
||||
it('should render as expected', () => {
|
||||
@@ -48,6 +55,75 @@ describe('<SpecsListBanners />', () => {
|
||||
})
|
||||
}
|
||||
|
||||
const stateWithFirstOpenedDaysAgo = (days: number) => {
|
||||
return {
|
||||
firstOpened: Date.now() - interval(`${days} days`),
|
||||
}
|
||||
}
|
||||
|
||||
const mountWithState = (query: Partial<SpecsListBannersFragment>, state?: Partial<AllowedState>, props?: object) => {
|
||||
cy.mountFragment(SpecsListBannersFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
assignIn(result, query)
|
||||
set(result, 'currentProject.savedState', state)
|
||||
},
|
||||
render: (gql) => <SpecsListBanners gql={gql} {...props} />,
|
||||
})
|
||||
}
|
||||
|
||||
const validateSmartNotificationBehaviors = (bannerId: BannerId, bannerTestId: string, gql: Partial<SpecsListBannersFragment>) => {
|
||||
it('should not render when using cypress < 4 days', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(3))
|
||||
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
})
|
||||
|
||||
it('should not render when previously-dismissed', () => {
|
||||
mountWithState(gql, {
|
||||
...stateWithFirstOpenedDaysAgo(4),
|
||||
banners: {
|
||||
[bannerId]: {
|
||||
dismissed: Date.now(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
})
|
||||
|
||||
context('banner conditions are met and when cypress use >= 4 days', () => {
|
||||
it('should render when not previously-dismissed', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(4))
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('be.visible')
|
||||
})
|
||||
|
||||
it('should be preempted by spec not found banner', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(4), { isSpecNotFound: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
})
|
||||
|
||||
it('should be preempted by offline warning banner', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(4), { isOffline: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
})
|
||||
|
||||
it('should be preempted by fetch error banner', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(4), { isFetchError: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
})
|
||||
|
||||
it('should be preempted by project not found banner', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(4), { isProjectNotFound: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
})
|
||||
|
||||
it('should be preempted by request access banner', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(4), { isProjectUnauthorized: true })
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('not.exist')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('spec not found', () => {
|
||||
const visible: any = ref(true)
|
||||
|
||||
@@ -141,6 +217,7 @@ describe('<SpecsListBanners />', () => {
|
||||
message: 'test',
|
||||
hasRequestedAccess: false,
|
||||
},
|
||||
savedState: {},
|
||||
}
|
||||
},
|
||||
render: (gql) => <SpecsListBanners gql={gql} isProjectUnauthorized={visible} />,
|
||||
@@ -170,6 +247,7 @@ describe('<SpecsListBanners />', () => {
|
||||
message: 'test',
|
||||
hasRequestedAccess: true,
|
||||
},
|
||||
savedState: {},
|
||||
}
|
||||
},
|
||||
render: (gql) => <SpecsListBanners gql={gql} isProjectUnauthorized={visible} hasRequestedAccess />,
|
||||
@@ -181,4 +259,91 @@ describe('<SpecsListBanners />', () => {
|
||||
validateCloseOnPropChange(visible)
|
||||
validateReopenOnPropChange(visible)
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
const gql: Partial<SpecsListBannersFragment> = {
|
||||
cloudViewer: null,
|
||||
cachedUser: null,
|
||||
currentProject: {
|
||||
__typename: 'CurrentProject',
|
||||
id: 'abc123',
|
||||
} as any,
|
||||
}
|
||||
|
||||
validateSmartNotificationBehaviors(BannerIds.ACI_082022_LOGIN, 'login-banner', gql)
|
||||
})
|
||||
|
||||
describe('create organization', () => {
|
||||
const gql: Partial<SpecsListBannersFragment> = {
|
||||
cloudViewer: {
|
||||
...CloudUserStubs.me,
|
||||
firstOrganization: {
|
||||
__typename: 'CloudOrganizationConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
currentProject: {
|
||||
__typename: 'CurrentProject',
|
||||
id: 'abc123',
|
||||
} as any,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.gqlStub.Query.cloudViewer = gql.cloudViewer as any
|
||||
})
|
||||
|
||||
validateSmartNotificationBehaviors(BannerIds.ACI_082022_CREATE_ORG, 'create-organization-banner', gql)
|
||||
})
|
||||
|
||||
describe('connect project', () => {
|
||||
const gql: Partial<SpecsListBannersFragment> = {
|
||||
cloudViewer: {
|
||||
...CloudUserStubs.me,
|
||||
firstOrganization: {
|
||||
__typename: 'CloudOrganizationConnection',
|
||||
nodes: [{ __typename: 'CloudOrganization', id: '987' }],
|
||||
},
|
||||
},
|
||||
currentProject: {
|
||||
__typename: 'CurrentProject',
|
||||
id: 'abc123',
|
||||
projectId: null,
|
||||
} as any,
|
||||
}
|
||||
|
||||
validateSmartNotificationBehaviors(BannerIds.ACI_082022_CONNECT_PROJECT, 'connect-project-banner', gql)
|
||||
})
|
||||
|
||||
describe('record', () => {
|
||||
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: 'component',
|
||||
projectId: 'abcd',
|
||||
cloudProject: {
|
||||
...CloudProjectStubs.componentProject,
|
||||
runs: {
|
||||
__typename: 'CloudRunConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.gqlStub.Query.currentProject = gql.currentProject as any
|
||||
cy.gqlStub.Query.cloudViewer = gql.cloudViewer as any
|
||||
})
|
||||
|
||||
validateSmartNotificationBehaviors(BannerIds.ACI_082022_RECORD, 'record-banner', gql)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<p>{{ t('specPage.noSpecError.explainer') }}</p>
|
||||
</Alert>
|
||||
<Alert
|
||||
v-if="showOffline"
|
||||
v-else-if="showOffline"
|
||||
v-model="showOffline"
|
||||
data-cy="offline-alert"
|
||||
status="warning"
|
||||
@@ -30,7 +30,7 @@
|
||||
</p>
|
||||
</Alert>
|
||||
<Alert
|
||||
v-if="showFetchError"
|
||||
v-else-if="showFetchError"
|
||||
v-model="showFetchError"
|
||||
status="warning"
|
||||
:title="t('specPage.fetchFailedWarning.title')"
|
||||
@@ -64,7 +64,7 @@
|
||||
</Button>
|
||||
</Alert>
|
||||
<Alert
|
||||
v-if="showProjectNotFound"
|
||||
v-else-if="showProjectNotFound"
|
||||
v-model="showProjectNotFound"
|
||||
data-cy="project-not-found-alert"
|
||||
status="warning"
|
||||
@@ -96,7 +96,7 @@
|
||||
</Button>
|
||||
</Alert>
|
||||
<Alert
|
||||
v-if="showProjectRequestAccess"
|
||||
v-else-if="showProjectRequestAccess"
|
||||
v-model="showProjectRequestAccess"
|
||||
data-cy="project-request-access-alert"
|
||||
status="warning"
|
||||
@@ -110,6 +110,22 @@
|
||||
</p>
|
||||
<RequestAccessButton :gql="props.gql" />
|
||||
</Alert>
|
||||
<RecordBanner
|
||||
v-else-if="showRecordBanner"
|
||||
v-model="showRecordBanner"
|
||||
/>
|
||||
<ConnectProjectBanner
|
||||
v-else-if="showConnectBanner"
|
||||
v-model="showConnectBanner"
|
||||
/>
|
||||
<CreateOrganizationBanner
|
||||
v-else-if="showCreateOrganizationBanner"
|
||||
v-model="showCreateOrganizationBanner"
|
||||
/>
|
||||
<LoginBanner
|
||||
v-else-if="showLoginBanner"
|
||||
v-model="showLoginBanner"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -123,10 +139,13 @@ import ConnectIcon from '~icons/cy/chain-link_x16.svg'
|
||||
import WarningIcon from '~icons/cy/warning_x16.svg'
|
||||
import RefreshIcon from '~icons/cy/action-restart_x16'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import RequestAccessButton from './RequestAccessButton.vue'
|
||||
import { gql } from '@urql/vue'
|
||||
import type { SpecsListBannersFragment } from '../generated/graphql'
|
||||
import { gql, useSubscription } from '@urql/vue'
|
||||
import { SpecsListBannersFragment, SpecsListBanners_CheckCloudOrgMembershipDocument } from '../generated/graphql'
|
||||
import interval from 'human-interval'
|
||||
import { AllowedState, BannerIds } from '@packages/types'
|
||||
import { LoginBanner, CreateOrganizationBanner, ConnectProjectBanner, RecordBanner } from './banners'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
@@ -134,6 +153,41 @@ const { t } = useI18n()
|
||||
gql`
|
||||
fragment SpecsListBanners on Query {
|
||||
...RequestAccessButton
|
||||
cloudViewer {
|
||||
id
|
||||
firstOrganization: organizations(first: 1) {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedUser {
|
||||
id
|
||||
}
|
||||
currentProject {
|
||||
id
|
||||
projectId
|
||||
savedState
|
||||
cloudProject {
|
||||
__typename
|
||||
... on CloudProject {
|
||||
id
|
||||
runs(first: 1) {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
subscription SpecsListBanners_CheckCloudOrgMembership {
|
||||
cloudViewerChange {
|
||||
...SpecsListBanners
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -158,11 +212,17 @@ const emit = defineEmits<{
|
||||
(e: 'reconnectProject'): void
|
||||
}>()
|
||||
|
||||
useSubscription({ query: SpecsListBanners_CheckCloudOrgMembershipDocument })
|
||||
|
||||
const showSpecNotFound = ref(props.isSpecNotFound)
|
||||
const showOffline = ref(props.isOffline)
|
||||
const showFetchError = ref(props.isFetchError)
|
||||
const showProjectNotFound = ref(props.isProjectNotFound)
|
||||
const showProjectRequestAccess = ref(props.isProjectUnauthorized)
|
||||
const showRecordBanner = ref(false)
|
||||
const showConnectBanner = ref(false)
|
||||
const showCreateOrganizationBanner = ref(false)
|
||||
const showLoginBanner = ref(false)
|
||||
|
||||
watch(
|
||||
() => ([props.isSpecNotFound, props.isOffline, props.isFetchError, props.isProjectNotFound, props.isProjectUnauthorized]),
|
||||
@@ -175,4 +235,33 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
const cloudData = computed(() => ([props.gql.cloudViewer, props.gql.cachedUser, props.gql.currentProject] as const))
|
||||
|
||||
watch(
|
||||
cloudData,
|
||||
([cloudViewer, cachedUser, currentProject]) => {
|
||||
// Cached user covers state where we're authenticated but data isn't loaded yet
|
||||
const isLoggedIn = !!cachedUser?.id || !!cloudViewer?.id
|
||||
// Need to be able to tell whether the lack of `firstOrganization` means they don't have an org or whether it just hasn't loaded yet
|
||||
// Not having this check can cause a brief flicker of the 'Create Org' banner while org data is loading
|
||||
const isOrganizationLoaded = !!cloudViewer?.firstOrganization
|
||||
const isMemberOfOrganization = (cloudViewer?.firstOrganization?.nodes?.length ?? 0) > 0
|
||||
const isProjectConnected = !!currentProject?.projectId && currentProject.cloudProject?.__typename === 'CloudProject'
|
||||
const hasNoRecordedRuns = currentProject?.cloudProject?.__typename === 'CloudProject' && (currentProject.cloudProject?.runs?.nodes?.length ?? 0) === 0
|
||||
const hasFourDaysOfCypressUse = (Date.now() - currentProject?.savedState?.firstOpened) > interval('4 days')
|
||||
|
||||
showRecordBanner.value = !hasBannerBeenDismissed(BannerIds.ACI_082022_RECORD) && isLoggedIn && isProjectConnected && isMemberOfOrganization && isProjectConnected && hasNoRecordedRuns && hasFourDaysOfCypressUse
|
||||
showConnectBanner.value = !hasBannerBeenDismissed(BannerIds.ACI_082022_CONNECT_PROJECT) && isLoggedIn && isMemberOfOrganization && !isProjectConnected && hasFourDaysOfCypressUse
|
||||
showCreateOrganizationBanner.value = !hasBannerBeenDismissed(BannerIds.ACI_082022_CREATE_ORG) && isLoggedIn && isOrganizationLoaded && !isMemberOfOrganization && hasFourDaysOfCypressUse
|
||||
showLoginBanner.value = !hasBannerBeenDismissed(BannerIds.ACI_082022_LOGIN) && !isLoggedIn && hasFourDaysOfCypressUse
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function hasBannerBeenDismissed (bannerId: string) {
|
||||
const bannersState = (props.gql.currentProject?.savedState as AllowedState)?.banners
|
||||
|
||||
return !!bannersState?._disabled || !!bannersState?.[bannerId]?.dismissed
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import ConnectProjectBanner from './ConnectProjectBanner.vue'
|
||||
|
||||
describe('<ConnectProjectBanner />', () => {
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} /> })
|
||||
|
||||
cy.contains(defaultMessages.specPage.banners.connectProject.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.connectProject.content).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.connectProject.buttonLabel).should('be.visible')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_CONNECT_PROJECT"
|
||||
:model-value="modelValue"
|
||||
data-cy="connect-project-banner"
|
||||
status="info"
|
||||
:title="t('specPage.banners.connectProject.title')"
|
||||
class="mb-16px"
|
||||
:icon="ConnectIcon"
|
||||
dismissible
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<p class="mb-24px">
|
||||
{{ t('specPage.banners.connectProject.content') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
:prefix-icon="ConnectIcon"
|
||||
class="mt-24px"
|
||||
data-cy="connect-project-button"
|
||||
@click="handleButtonClick"
|
||||
>
|
||||
{{ t('specPage.banners.connectProject.buttonLabel') }}
|
||||
</Button>
|
||||
|
||||
<CloudConnectModals
|
||||
v-if="isProjectConnectOpen && cloudModalsQuery.data.value"
|
||||
:gql="cloudModalsQuery.data.value"
|
||||
@cancel="handleModalClose"
|
||||
@success="handleModalClose"
|
||||
/>
|
||||
</TrackedBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import ConnectIcon from '~icons/cy/chain-link_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { ref } from 'vue'
|
||||
import { ConnectProjectBannerDocument } from '../../generated/graphql'
|
||||
import CloudConnectModals from '../../runs/modals/CloudConnectModals.vue'
|
||||
|
||||
gql`
|
||||
query ConnectProjectBanner {
|
||||
...CloudConnectModals
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
modelValue: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isProjectConnectOpen = ref(false)
|
||||
|
||||
const cloudModalsQuery = useQuery({ query: ConnectProjectBannerDocument, pause: true })
|
||||
|
||||
async function handleButtonClick () {
|
||||
await cloudModalsQuery.executeQuery()
|
||||
|
||||
isProjectConnectOpen.value = true
|
||||
}
|
||||
|
||||
function handleModalClose () {
|
||||
isProjectConnectOpen.value = false
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import CreateOrganizationBanner from './CreateOrganizationBanner.vue'
|
||||
|
||||
describe('<CreateOrganizationBanner />', () => {
|
||||
it('should render expected content', () => {
|
||||
const linkHref = 'http://dummy.cypress.io/organizations/create'
|
||||
|
||||
cy.gqlStub.Query.cloudViewer = {
|
||||
__typename: 'CloudUser',
|
||||
id: 'test123',
|
||||
cloudOrganizationsUrl: linkHref,
|
||||
} as any
|
||||
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} /> })
|
||||
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.content).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.buttonLabel).should('be.visible')
|
||||
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', linkHref)
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_CREATE_ORG"
|
||||
:model-value="modelValue"
|
||||
data-cy="create-organization-banner"
|
||||
status="info"
|
||||
:title="t('specPage.banners.createOrganization.title')"
|
||||
class="mb-16px"
|
||||
:icon="OrganizationIcon"
|
||||
dismissible
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<p class="mb-24px">
|
||||
{{ t('specPage.banners.createOrganization.content') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
:href="createOrganizationUrl"
|
||||
:include-graphql-port="true"
|
||||
data-cy="create-organization-button"
|
||||
:prefix-icon="OrganizationIcon"
|
||||
prefix-icon-class="icon-dark-white icon-light-indigo-500"
|
||||
>
|
||||
{{ t('specPage.banners.createOrganization.buttonLabel') }}
|
||||
</Button>
|
||||
</TrackedBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import OrganizationIcon from '~icons/cy/office-building_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { CreateOrganizationBannerDocument } from '../../generated/graphql'
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import { getUrlWithParams } from '@packages/frontend-shared/src/utils/getUrlWithParams'
|
||||
import { computed } from 'vue'
|
||||
import Button from '@packages/frontend-shared/src/components/Button.vue'
|
||||
|
||||
gql`
|
||||
query CreateOrganizationBanner {
|
||||
cloudViewer {
|
||||
id
|
||||
createCloudOrganizationUrl
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
modelValue: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const query = useQuery({ query: CreateOrganizationBannerDocument })
|
||||
|
||||
const createOrganizationUrl = computed(() => {
|
||||
const baseUrl = query.data?.value?.cloudViewer?.createCloudOrganizationUrl
|
||||
|
||||
if (!baseUrl) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return getUrlWithParams({
|
||||
url: baseUrl,
|
||||
params: {
|
||||
utm_medium: 'Specs Create Organization Banner',
|
||||
utm_campaign: 'Set up your organization',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import LoginBanner from './LoginBanner.vue'
|
||||
|
||||
describe('<LoginBanner />', () => {
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} /> })
|
||||
|
||||
cy.contains(defaultMessages.specPage.banners.login.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.login.content).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.login.buttonLabel).should('be.visible')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_LOGIN"
|
||||
:model-value="modelValue"
|
||||
data-cy="login-banner"
|
||||
status="info"
|
||||
:title="t('specPage.banners.login.title')"
|
||||
class="mb-16px"
|
||||
:icon="ConnectIcon"
|
||||
dismissible
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<p class="mb-24px">
|
||||
{{ t('specPage.banners.login.content') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
:prefix-icon="ConnectIcon"
|
||||
class="mt-24px"
|
||||
data-cy="login-button"
|
||||
@click="handleButtonClick"
|
||||
>
|
||||
{{ t('specPage.banners.login.buttonLabel') }}
|
||||
</Button>
|
||||
<LoginModal
|
||||
v-if="loginModalQuery.data.value"
|
||||
v-model="isLoginOpen"
|
||||
:gql="loginModalQuery.data.value"
|
||||
utm-medium="Specs Login Banner"
|
||||
/>
|
||||
</TrackedBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import ConnectIcon from '~icons/cy/chain-link_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import { LoginBannerDocument } from '../../generated/graphql'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import LoginModal from '@cy/gql-components/topnav/LoginModal.vue'
|
||||
|
||||
gql`
|
||||
query LoginBanner {
|
||||
...LoginModal
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
modelValue: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoginOpen = ref(false)
|
||||
const loginModalQuery = useQuery({ query: LoginBannerDocument, pause: true })
|
||||
|
||||
async function handleButtonClick () {
|
||||
await loginModalQuery.executeQuery()
|
||||
isLoginOpen.value = true
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import RecordBanner from './RecordBanner.vue'
|
||||
|
||||
describe('<RecordBanner />', () => {
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <RecordBanner modelValue={true} /> })
|
||||
|
||||
cy.gqlStub.Query.currentProject = {
|
||||
id: 'test_id',
|
||||
title: 'project_title',
|
||||
currentTestingType: 'component',
|
||||
cloudProject: {
|
||||
__typename: 'CloudProject',
|
||||
id: 'cloud_id',
|
||||
recordKeys: [{
|
||||
__typename: 'CloudRecordKey',
|
||||
id: 'recordKey1',
|
||||
key: 'abcd-efg-1234',
|
||||
}],
|
||||
} as any,
|
||||
} as any
|
||||
|
||||
cy.contains(defaultMessages.specPage.banners.record.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.record.content).should('be.visible')
|
||||
|
||||
cy.findByText('cypress run --component --record --key abcd-efg-1234')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_RECORD"
|
||||
:model-value="modelValue"
|
||||
data-cy="record-banner"
|
||||
status="info"
|
||||
:title="t('specPage.banners.record.title')"
|
||||
class="mb-16px"
|
||||
:icon="RecordIcon"
|
||||
dismissible
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<p class="mb-24px">
|
||||
{{ t('specPage.banners.record.content') }}
|
||||
</p>
|
||||
|
||||
<TerminalPrompt
|
||||
:command="recordCommand"
|
||||
:project-folder-name="query.data?.value?.currentProject?.title"
|
||||
class="bg-white max-w-900px"
|
||||
/>
|
||||
</TrackedBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import RecordIcon from '~icons/cy/action-record_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import TerminalPrompt from '@cy/components/TerminalPrompt.vue'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { RecordBannerDocument } from '../../generated/graphql'
|
||||
import { computed } from 'vue'
|
||||
|
||||
gql`
|
||||
query RecordBanner {
|
||||
currentProject {
|
||||
id
|
||||
title
|
||||
currentTestingType
|
||||
cloudProject {
|
||||
__typename
|
||||
... on CloudProject {
|
||||
id
|
||||
recordKeys {
|
||||
id
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
modelValue: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const query = useQuery({ query: RecordBannerDocument })
|
||||
|
||||
const firstRecordKey = computed(() => {
|
||||
return (query.data?.value?.currentProject?.cloudProject?.__typename === 'CloudProject' && query.data.value.currentProject.cloudProject.recordKeys?.[0]?.key) ?? '<record-key>'
|
||||
})
|
||||
const recordCommand = computed(() => {
|
||||
const componentFlagOrSpace = query.data?.value?.currentProject?.currentTestingType === 'component' ? ' --component ' : ' '
|
||||
|
||||
return `cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,64 @@
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import { ref } from 'vue'
|
||||
import { TrackedBanner_SetProjectStateDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<TrackedBanner />', () => {
|
||||
it('should pass through props and child content', () => {
|
||||
cy.mount({ render: () => <TrackedBanner bannerId="test-banner" dismissible modelValue={true}>Test Content</TrackedBanner> })
|
||||
|
||||
cy.findByText('Test Content').should('be.visible')
|
||||
cy.findByTestId('alert-suffix-icon').should('be.visible')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('should record when banner is made visible', () => {
|
||||
cy.clock(1234)
|
||||
const recordStub = cy.stub()
|
||||
const shown = ref(true)
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_SetProjectStateDocument, (defineResult, { value }) => {
|
||||
recordStub(value)
|
||||
|
||||
return defineResult({ setPreferences: {} as any })
|
||||
})
|
||||
|
||||
// Initially mount as visible
|
||||
// @ts-ignore
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} /> })
|
||||
|
||||
cy.get('[data-cy="banner"]').as('banner')
|
||||
|
||||
cy.get('@banner').should('be.visible')
|
||||
.then(() => {
|
||||
expect(recordStub).to.have.been.calledWith('{"banners":{"test-banner":{"lastShown":1234}}}')
|
||||
})
|
||||
})
|
||||
|
||||
it('should record when banner is dismissed', () => {
|
||||
cy.clock(1234)
|
||||
const recordStub = cy.stub()
|
||||
const shown = ref(true)
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_SetProjectStateDocument, (defineResult, { value }) => {
|
||||
recordStub(value)
|
||||
|
||||
return defineResult({ setPreferences: {} as any })
|
||||
})
|
||||
|
||||
// Initially mount as visible
|
||||
// @ts-ignore
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} dismissible /> })
|
||||
|
||||
cy.get('[data-cy="banner"]').as('banner')
|
||||
|
||||
cy.get('@banner').should('be.visible')
|
||||
|
||||
cy.get('@banner').findByTestId('alert-suffix-icon').click()
|
||||
|
||||
cy.get('@banner').should('not.exist')
|
||||
.then(() => {
|
||||
expect(recordStub).to.have.been.calledWith('{"banners":{"test-banner":{"dismissed":1234}}}')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<Alert
|
||||
v-bind="$attrs"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="handleBannerDismissed"
|
||||
>
|
||||
<slot />
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Alert from '@packages/frontend-shared/src/components/Alert.vue'
|
||||
import { watchEffect } from 'vue'
|
||||
import { gql, useMutation, useQuery } from '@urql/vue'
|
||||
import { TrackedBanner_ProjectStateDocument, TrackedBanner_SetProjectStateDocument } from '../../generated/graphql'
|
||||
import { set } from 'lodash'
|
||||
|
||||
type AlertComponentProps = InstanceType<typeof Alert>['$props']
|
||||
type AlertComponentEmits = InstanceType<typeof Alert>['$emit']
|
||||
interface TrackedBannerComponentProps extends AlertComponentProps {
|
||||
bannerId: string
|
||||
modelValue: boolean
|
||||
}
|
||||
interface TrackedBannerComponentEmits extends AlertComponentEmits {
|
||||
(e: 'update:modelValue'): void
|
||||
}
|
||||
|
||||
gql`
|
||||
query TrackedBanner_ProjectState {
|
||||
currentProject {
|
||||
id
|
||||
savedState
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation TrackedBanner_SetProjectState($value: String!) {
|
||||
setPreferences(type: project, value: $value) {
|
||||
...TestingPreferences
|
||||
...SpecRunner_Preferences
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const props = withDefaults(defineProps<TrackedBannerComponentProps>(), {})
|
||||
|
||||
const emit = defineEmits<TrackedBannerComponentEmits>()
|
||||
|
||||
const stateQuery = useQuery({ query: TrackedBanner_ProjectStateDocument })
|
||||
const setStateMutation = useMutation(TrackedBanner_SetProjectStateDocument)
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.modelValue) {
|
||||
updateBannerState('lastShown')
|
||||
}
|
||||
})
|
||||
|
||||
function handleBannerDismissed (visible: boolean) {
|
||||
if (!visible) {
|
||||
updateBannerState('dismissed')
|
||||
}
|
||||
|
||||
emit('update:modelValue', visible)
|
||||
}
|
||||
|
||||
function updateBannerState (field: 'lastShown' | 'dismissed') {
|
||||
const savedState = stateQuery.data.value?.currentProject?.savedState ?? {}
|
||||
|
||||
set(savedState, ['banners', props.bannerId, field], Date.now())
|
||||
|
||||
setStateMutation.executeMutation({ value: JSON.stringify(savedState) })
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as LoginBanner } from './LoginBanner.vue'
|
||||
|
||||
export { default as CreateOrganizationBanner } from './CreateOrganizationBanner.vue'
|
||||
|
||||
export { default as ConnectProjectBanner } from './ConnectProjectBanner.vue'
|
||||
|
||||
export { default as RecordBanner } from './RecordBanner.vue'
|
||||
@@ -35,7 +35,7 @@ export interface ProjectApiShape {
|
||||
getConfig(): ReceivedCypressOptions | undefined
|
||||
getRemoteStates(): { reset(): void, getPrimary(): Cypress.RemoteState } | undefined
|
||||
getCurrentBrowser: () => Cypress.Browser | undefined
|
||||
getCurrentProjectSavedState(): {} | undefined
|
||||
getCurrentProjectSavedState(): AllowedState | undefined
|
||||
setPromptShown(slug: string): void
|
||||
setProjectPreferences(stated: AllowedState): void
|
||||
makeProjectSavedState(projectRoot: string): void
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep } from '@packages/types'
|
||||
import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep, BannerState } from '@packages/types'
|
||||
import type { WizardFrontendFramework, WizardBundler } from '@packages/scaffold-config'
|
||||
import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { App, BrowserWindow } from 'electron'
|
||||
@@ -35,6 +35,7 @@ export interface SavedStateShape {
|
||||
firstOpened?: number | null
|
||||
lastOpened?: number | null
|
||||
promptsShown?: object | null
|
||||
banners?: BannerState | null
|
||||
lastProjectId?: string | null
|
||||
specFilter?: string | null
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@ function startAppServer (mode: 'component' | 'e2e' = 'e2e', options: { skipMocki
|
||||
firstOpened: 1609459200000,
|
||||
lastOpened: 1609459200000,
|
||||
promptsShown: { ci1: 1609459200000 },
|
||||
banners: { _disabled: true },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cy.i18n = defaultMessages
|
||||
cy.gqlStub = GQLStubRegistry
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 9C8.10457 9 9 8.10457 9 7C9 5.89543 8.10457 5 7 5C5.89543 5 5 5.89543 5 7C5 8.10457 5.89543 9 7 9Z" fill="#fff" class="icon-dark"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 7C9 8.10457 8.10457 9 7 9C5.89543 9 5 8.10457 5 7C5 5.89543 5.89543 5 7 5C8.10457 5 9 5.89543 9 7Z" fill="#fff" class="icon-dark"/>
|
||||
<path d="M7 9C8.10457 9 9 8.10457 9 7C9 5.89543 8.10457 5 7 5C5.89543 5 5 5.89543 5 7C5 8.10457 5.89543 9 7 9Z" stroke="#1B1E2E" stroke-width="2" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 7C9 8.10457 8.10457 9 7 9C5.89543 9 5 8.10457 5 7C5 5.89543 5.89543 5 7 5C8.10457 5 9 5.89543 9 7Z" stroke="#1B1E2E" stroke-width="2"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" fill="#1B1E2E" class="icon-dark" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 8C10 9.10457 9.10457 10 8 10C6.89543 10 6 9.10457 6 8C6 6.89543 6.89543 6 8 6C9.10457 6 10 6.89543 10 8Z" fill="#1B1E2E" class="icon-dark" />
|
||||
<path d="M14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8ZM10 8C10 9.10457 9.10457 10 8 10C6.89543 10 6 9.10457 6 8C6 6.89543 6.89543 6 8 6C9.10457 6 10 6.89543 10 8Z" stroke="#1B1E2E" stroke-width="2" class="icon-dark"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 734 B |
@@ -357,7 +357,7 @@ describe('<HeaderBarContent />', { viewportWidth: 1000, viewportHeight: 750 }, (
|
||||
})
|
||||
|
||||
function mountWithSavedState (options?: {state?: object, projectId?: string }) {
|
||||
return cy.mountFragment(HeaderBar_HeaderBarContentFragmentDoc, {
|
||||
const mountResult = cy.mountFragment(HeaderBar_HeaderBarContentFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
if (!result.currentProject) {
|
||||
return
|
||||
@@ -378,6 +378,12 @@ describe('<HeaderBarContent />', { viewportWidth: 1000, viewportHeight: 750 }, (
|
||||
},
|
||||
render: (gqlVal) => <div class="border-current border-1 h-700px resize overflow-auto"><HeaderBarContent gql={gqlVal} show-browsers={true} allowAutomaticPromptOpen={true} /></div>,
|
||||
})
|
||||
|
||||
// Auto-opening prompts wait 2000ms after mount before opening
|
||||
// Advance to that point so that prompts will have had a chance to open
|
||||
cy.tick(2000)
|
||||
|
||||
return mountResult
|
||||
}
|
||||
|
||||
it('opens when after 4 days from first open, no projectId, and not already shown', () => {
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { gql, useMutation, useSubscription } from '@urql/vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type { HeaderBar_HeaderBarContentFragment } from '../generated/graphql'
|
||||
import {
|
||||
GlobalPageHeader_ClearCurrentProjectDocument,
|
||||
@@ -181,6 +181,7 @@ import ExternalLink from './ExternalLink.vue'
|
||||
import interval from 'human-interval'
|
||||
import { sortBy } from 'lodash'
|
||||
import Tooltip from '../components/Tooltip.vue'
|
||||
import type { AllowedState } from '@packages/types'
|
||||
|
||||
gql`
|
||||
fragment HeaderBarContent_Auth on Query {
|
||||
@@ -247,7 +248,7 @@ const userData = computed(() => {
|
||||
})
|
||||
|
||||
const savedState = computed(() => {
|
||||
return props.gql?.currentProject?.savedState
|
||||
return props.gql?.currentProject?.savedState as AllowedState
|
||||
})
|
||||
|
||||
const currentProject = computed(() => props.gql.currentProject)
|
||||
@@ -299,7 +300,20 @@ const prompts = sortBy([
|
||||
},
|
||||
], 'interval')
|
||||
const isForceOpenAllowed = ref(true)
|
||||
const isOpenDelayElapsed = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => isOpenDelayElapsed.value = true, 2000)
|
||||
})
|
||||
|
||||
const isShowablePromptInSavedState = computed(() => {
|
||||
// We do not want to show a prompt if a banner is going to be shown, but some banners rely on cloud data
|
||||
// getting loaded before deciding whether to display. Add a delay here of a few seconds to give banners
|
||||
// a chance to display before deciding whether to show a prompt.
|
||||
if (!isOpenDelayElapsed.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (savedState.value) {
|
||||
for (const prompt of prompts) {
|
||||
if (shouldShowPrompt(prompt)) {
|
||||
@@ -318,8 +332,9 @@ function shouldShowPrompt (prompt: { slug: string, noProjectId: boolean, interva
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const timeSinceOpened = now - savedState.value?.firstOpened
|
||||
const timeSinceOpened = now - (savedState.value?.firstOpened ?? now)
|
||||
const allPromptShownTimes: number[] = Object.values(savedState.value?.promptsShown ?? {})
|
||||
const bannersLastShown = Object.values(savedState.value?.banners ?? {}).map((banner) => typeof banner === 'object' && banner?.lastShown).filter((val): val is number => !!val)
|
||||
|
||||
// prompt has been shown
|
||||
if (savedState.value?.promptsShown?.[prompt.slug]) {
|
||||
@@ -331,6 +346,11 @@ function shouldShowPrompt (prompt: { slug: string, noProjectId: boolean, interva
|
||||
return false
|
||||
}
|
||||
|
||||
// If any tracked banners have been shown in the last 24 hours
|
||||
if (bannersLastShown.some((bannerLastShown) => (now - bannerLastShown) < interval('24 hours'))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// enough time has passed
|
||||
// no interval indicates *never* being shown automatically, so don't show if there's no interval
|
||||
if (!prompt.interval || timeSinceOpened < prompt.interval) {
|
||||
|
||||
@@ -184,6 +184,27 @@
|
||||
"explainer1": "The request timed out or failed when trying to retrieve the recorded run metrics from the Cypress Dashboard. The information that you're seeing in the table below may be incomplete as a result.",
|
||||
"explainer2": "Please refresh the page to try again and visit our {0} if this behavior continues.",
|
||||
"refreshButton": "Try again"
|
||||
},
|
||||
"banners": {
|
||||
"login": {
|
||||
"title": "Optimize and record your CI test runs with Cypress Dashboard",
|
||||
"content": "Parallelize your tests in CI and visualize every error by watching full video recordings of each test you run.",
|
||||
"buttonLabel": "Get started with Cypress Dashboard"
|
||||
},
|
||||
"createOrganization": {
|
||||
"title": "Finish setting up Cypress Dashboard",
|
||||
"content": "Join or create an organization in Cypress Dashboard to access your projects and recorded test runs.",
|
||||
"buttonLabel": "Set up your organization"
|
||||
},
|
||||
"connectProject": {
|
||||
"title": "Connect your project to Cypress Dashboard",
|
||||
"content": "View recorded test runs directly in the Cypress app to monitor, run, and fix tests locally.",
|
||||
"buttonLabel": "Connect your project"
|
||||
},
|
||||
"record": {
|
||||
"title": "Record your first run",
|
||||
"content": "Record a run to see your test results in Cypress Dashboard. You can then optimize your test suite, debug failing and flaky tests, and integrate with your favorite tools."
|
||||
}
|
||||
}
|
||||
},
|
||||
"noResults": {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { SocketE2E } from './socket-e2e'
|
||||
import { ensureProp } from './util/class-helpers'
|
||||
|
||||
import system from './util/system'
|
||||
import type { FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, TestingType } from '@packages/types'
|
||||
import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, TestingType } from '@packages/types'
|
||||
import { DataContext, getCtx } from '@packages/data-context'
|
||||
import { createHmac } from 'crypto'
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface Cfg extends ReceivedCypressOptions {
|
||||
firstOpened?: number | null
|
||||
lastOpened?: number | null
|
||||
promptsShown?: object | null
|
||||
banners?: BannersState | null
|
||||
}
|
||||
e2e: Partial<Cfg>
|
||||
component: Partial<Cfg>
|
||||
|
||||
@@ -36,3 +36,21 @@ export interface SettingsOptions {
|
||||
testingType?: 'component' |'e2e'
|
||||
args?: AllModeOptions
|
||||
}
|
||||
|
||||
export type BannerState = {
|
||||
lastShown?: number
|
||||
dismissed?: number
|
||||
}
|
||||
|
||||
export const BannerIds = {
|
||||
ACI_082022_LOGIN: 'aci_082022_login',
|
||||
ACI_082022_CREATE_ORG: 'aci_082022_createOrganization',
|
||||
ACI_082022_CONNECT_PROJECT: 'aci_082022_connectProject',
|
||||
ACI_082022_RECORD: 'aci_082022_record',
|
||||
} as const
|
||||
|
||||
type BannerKeys = keyof typeof BannerIds
|
||||
type BannerId = typeof BannerIds[BannerKeys]
|
||||
export type BannersState = {
|
||||
[bannerId in BannerId]?: BannerState
|
||||
} & { _disabled?: boolean }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Editor } from '.'
|
||||
import type { BannersState, Editor } from '.'
|
||||
|
||||
export const defaultPreferences: AllowedState = {
|
||||
autoScrollingEnabled: true,
|
||||
@@ -12,6 +12,7 @@ export const allowedKeys: Readonly<Array<keyof AllowedState>> = [
|
||||
'appX',
|
||||
'appY',
|
||||
'autoScrollingEnabled',
|
||||
'banners',
|
||||
'browserWidth',
|
||||
'browserHeight',
|
||||
'browserX',
|
||||
@@ -47,6 +48,7 @@ export type AllowedState = Partial<{
|
||||
appY: Maybe<number>
|
||||
isSpecsListOpen: Maybe<boolean>
|
||||
autoScrollingEnabled: Maybe<boolean>
|
||||
banners: Maybe<BannersState>
|
||||
browserWidth: Maybe<number>
|
||||
browserHeight: Maybe<number>
|
||||
browserX: Maybe<number>
|
||||
|
||||
Reference in New Issue
Block a user