mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-06 07:10:12 -06:00
Merge branch 'develop' into sessions-instrument-panel
This commit is contained in:
@@ -106,7 +106,7 @@ generates:
|
||||
<<: *vueOperations
|
||||
|
||||
'./packages/frontend-shared/src/generated/graphql.ts':
|
||||
documents: './packages/frontend-shared/src/{gql-components,graphql}/**/*.{vue,ts,tsx,js,jsx}'
|
||||
documents: './packages/frontend-shared/src/{gql-components,graphql,composables}/**/*.{vue,ts,tsx,js,jsx}'
|
||||
<<: *vueOperations
|
||||
###
|
||||
# All GraphQL documents imported into the .spec.tsx files for component testing.
|
||||
|
||||
@@ -94,6 +94,11 @@ export type MountResponse<T> = {
|
||||
component: T
|
||||
};
|
||||
|
||||
// 'zone.js/testing' is not properly aliasing `it.skip` but it does provide `xit`/`xspecify`
|
||||
// Written up under https://github.com/angular/angular/issues/46297 but is not seeing movement
|
||||
// so we'll patch here pending a fix in that library
|
||||
globalThis.it.skip = globalThis.xit
|
||||
|
||||
/**
|
||||
* Bootstraps the TestModuleMetaData passed to the TestBed
|
||||
*
|
||||
|
||||
@@ -611,7 +611,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
cy.get('[data-cy="copy-button"]').click()
|
||||
cy.contains('Copied!')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('npx cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -624,7 +624,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
cy.get('[data-cy="copy-button"]').click()
|
||||
cy.contains('Copied!')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('npx cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Bluebird from 'bluebird'
|
||||
import { EventEmitter } from 'events'
|
||||
import type { MobxRunnerStore } from '@packages/app/src/store/mobx-runner-store'
|
||||
import type { RunState } from '@packages/types/src/driver'
|
||||
import type MobX from 'mobx'
|
||||
import type { LocalBusEmitsMap, LocalBusEventMap, DriverToLocalBus, SocketToDriverMap } from './event-manager-types'
|
||||
import type { AutomationElementId, FileDetails } from '@packages/types'
|
||||
|
||||
import type { RunState, CachedTestState, AutomationElementId, FileDetails, ReporterStartInfo, ReporterRunState } from '@packages/types'
|
||||
|
||||
import { logger } from './logger'
|
||||
import type { Socket } from '@packages/socket/lib/browser'
|
||||
@@ -87,8 +87,7 @@ export class EventManager {
|
||||
|
||||
const rerun = () => {
|
||||
if (!this) {
|
||||
// if the tests have been reloaded
|
||||
// then there is nothing to rerun
|
||||
// if the tests have been reloaded then there is nothing to rerun
|
||||
return
|
||||
}
|
||||
|
||||
@@ -252,7 +251,7 @@ export class EventManager {
|
||||
this.reporterBus.on('clear:all:sessions', () => {
|
||||
if (!Cypress) return
|
||||
|
||||
Cypress.backend('clear:session')
|
||||
Cypress.backend('clear:sessions', true)
|
||||
.then(rerun)
|
||||
})
|
||||
|
||||
@@ -336,8 +335,8 @@ export class EventManager {
|
||||
// @ts-ignore
|
||||
const $window = this.$CypressDriver.$(window)
|
||||
|
||||
// This is a test-only even. It's used to
|
||||
// trigger a re-reun for the drive rerun.cy.js spec.
|
||||
// This is a test-only event. It's used to
|
||||
// trigger a rerun for the driver rerun.cy.js spec.
|
||||
$window.on('test:trigger:rerun', rerun)
|
||||
|
||||
// when we actually unload then
|
||||
@@ -395,9 +394,9 @@ export class EventManager {
|
||||
return Cypress.initialize({
|
||||
$autIframe,
|
||||
onSpecReady: () => {
|
||||
// get the current runnable in case we reran mid-test due to a visit
|
||||
// to a new domain
|
||||
this.ws.emit('get:existing:run:state', (state: RunState = {}) => {
|
||||
// get the current runnable states and cached test state
|
||||
// in case we reran mid-test due to a visit to a new domain
|
||||
this.ws.emit('get:cached:test:state', (runState: RunState = {}, testState: CachedTestState) => {
|
||||
if (!Cypress.runner) {
|
||||
// the tests have been reloaded
|
||||
return
|
||||
@@ -405,40 +404,40 @@ export class EventManager {
|
||||
|
||||
const hideCommandLog = window.__CYPRESS_CONFIG__.hideCommandLog
|
||||
|
||||
this.studioStore.initialize(config, state)
|
||||
this.studioStore.initialize(config, runState)
|
||||
|
||||
const runnables = Cypress.runner.normalizeAll(state.tests, hideCommandLog)
|
||||
const runnables = Cypress.runner.normalizeAll(runState.tests, hideCommandLog)
|
||||
|
||||
const run = () => {
|
||||
performance.mark('initialize-end')
|
||||
performance.measure('initialize', 'initialize-start', 'initialize-end')
|
||||
|
||||
this._runDriver(state)
|
||||
this._runDriver(runState, testState)
|
||||
}
|
||||
|
||||
if (!hideCommandLog) {
|
||||
this.reporterBus.emit('runnables:ready', runnables)
|
||||
}
|
||||
|
||||
if (state?.numLogs) {
|
||||
Cypress.runner.setNumLogs(state.numLogs)
|
||||
if (runState?.numLogs) {
|
||||
Cypress.runner.setNumLogs(runState.numLogs)
|
||||
}
|
||||
|
||||
if (state.startTime) {
|
||||
Cypress.runner.setStartTime(state.startTime)
|
||||
if (runState.startTime) {
|
||||
Cypress.runner.setStartTime(runState.startTime)
|
||||
}
|
||||
|
||||
if (config.isTextTerminal && !state.currentId) {
|
||||
if (config.isTextTerminal && !runState.currentId) {
|
||||
// we are in run mode and it's the first load
|
||||
// store runnables in backend and maybe send to dashboard
|
||||
return this.ws.emit('set:runnables:and:maybe:record:tests', runnables, run)
|
||||
}
|
||||
|
||||
if (state.currentId) {
|
||||
if (runState.currentId) {
|
||||
// if we have a currentId it means
|
||||
// we need to tell the Cypress to skip
|
||||
// ahead to that test
|
||||
Cypress.runner.resumeAtTest(state.currentId, state.emissions)
|
||||
Cypress.runner.resumeAtTest(runState.currentId, runState.emissions)
|
||||
}
|
||||
|
||||
return run()
|
||||
@@ -464,7 +463,7 @@ export class EventManager {
|
||||
}
|
||||
|
||||
return new Bluebird((resolve) => {
|
||||
this.reporterBus.emit('reporter:collect:run:state', (reporterState) => {
|
||||
this.reporterBus.emit('reporter:collect:run:state', (reporterState: ReporterRunState) => {
|
||||
resolve({
|
||||
...reporterState,
|
||||
studio: {
|
||||
@@ -749,9 +748,9 @@ export class EventManager {
|
||||
window.top.addEventListener('message', crossOriginOnMessageRef, false)
|
||||
}
|
||||
|
||||
_runDriver (state) {
|
||||
_runDriver (runState: RunState, testState: CachedTestState) {
|
||||
performance.mark('run-s')
|
||||
Cypress.run(() => {
|
||||
Cypress.run(testState, () => {
|
||||
performance.mark('run-e')
|
||||
performance.measure('run', 'run-s', 'run-e')
|
||||
})
|
||||
@@ -760,14 +759,14 @@ export class EventManager {
|
||||
|
||||
this.reporterBus.emit('reporter:start', {
|
||||
startTime: Cypress.runner.getStartTime(),
|
||||
numPassed: state.passed,
|
||||
numFailed: state.failed,
|
||||
numPending: state.pending,
|
||||
autoScrollingEnabled: state.autoScrollingEnabled,
|
||||
isSpecsListOpen: state.isSpecsListOpen,
|
||||
scrollTop: state.scrollTop,
|
||||
numPassed: runState.passed,
|
||||
numFailed: runState.failed,
|
||||
numPending: runState.pending,
|
||||
autoScrollingEnabled: runState.autoScrollingEnabled,
|
||||
isSpecsListOpen: runState.isSpecsListOpen,
|
||||
scrollTop: runState.scrollTop,
|
||||
studioActive: hasRunnableId,
|
||||
})
|
||||
} as ReporterStartInfo)
|
||||
}
|
||||
|
||||
stop () {
|
||||
@@ -783,8 +782,8 @@ export class EventManager {
|
||||
state.setIsLoading(true)
|
||||
|
||||
if (!isRerun) {
|
||||
// only clear session state when a new spec is selected
|
||||
Cypress.backend('reset:session:state')
|
||||
// only clear test state when a new spec is selected
|
||||
Cypress.backend('reset:cached:test:state')
|
||||
}
|
||||
|
||||
// when we are re-running we first need to stop cypress always
|
||||
|
||||
@@ -61,7 +61,7 @@ const firstRecordKey = computed(() => {
|
||||
const recordCommand = computed(() => {
|
||||
const componentFlagOrSpace = props.gql.currentTestingType === 'component' ? ' --component ' : ' '
|
||||
|
||||
return `cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
|
||||
return `npx cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SpecsListBanners from './SpecsListBanners.vue'
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { SpecsListBannersFragment, SpecsListBannersFragmentDoc } from '../generated/graphql-test'
|
||||
import { SpecsListBannersFragment, SpecsListBannersFragmentDoc, UseCohorts_DetermineCohortDocument } from '../generated/graphql-test'
|
||||
import interval from 'human-interval'
|
||||
import { CloudUserStubs, CloudProjectStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { AllowedState, BannerIds } from '@packages/types'
|
||||
@@ -92,6 +92,12 @@ describe('<SpecsListBanners />', () => {
|
||||
})
|
||||
|
||||
context('banner conditions are met and when cypress use >= 4 days', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubMutationResolver(UseCohorts_DetermineCohortDocument, (defineResult) => {
|
||||
return defineResult({ determineCohort: { __typename: 'Cohort', name: 'foo', cohort: 'A' } })
|
||||
})
|
||||
})
|
||||
|
||||
it('should render when not previously-dismissed', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(4))
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('be.visible')
|
||||
|
||||
@@ -116,19 +116,22 @@
|
||||
:has-banner-been-shown="hasRecordBannerBeenShown"
|
||||
/>
|
||||
<ConnectProjectBanner
|
||||
v-else-if="showConnectBanner"
|
||||
v-else-if="showConnectBanner && cohorts.connectProject?.value"
|
||||
v-model="showConnectBanner"
|
||||
:has-banner-been-shown="hasConnectBannerBeenShown"
|
||||
:cohort-option="cohorts.connectProject.value"
|
||||
/>
|
||||
<CreateOrganizationBanner
|
||||
v-else-if="showCreateOrganizationBanner"
|
||||
v-else-if="showCreateOrganizationBanner && cohorts.organization?.value"
|
||||
v-model="showCreateOrganizationBanner"
|
||||
:has-banner-been-shown="hasCreateOrganizationBannerBeenShown"
|
||||
:cohort-option="cohorts.organization.value"
|
||||
/>
|
||||
<LoginBanner
|
||||
v-else-if="showLoginBanner"
|
||||
v-else-if="showLoginBanner && cohorts.login?.value"
|
||||
v-model="showLoginBanner"
|
||||
:has-banner-been-shown="hasLoginBannerBeenShown"
|
||||
:cohort-option="cohorts.login.value"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -143,13 +146,14 @@ 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 { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch, watchEffect } from 'vue'
|
||||
import RequestAccessButton from './RequestAccessButton.vue'
|
||||
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'
|
||||
import { CohortConfig, useCohorts } from '@packages/frontend-shared/src/composables/useCohorts'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
@@ -228,10 +232,10 @@ const showConnectBanner = ref(false)
|
||||
const showCreateOrganizationBanner = ref(false)
|
||||
const showLoginBanner = ref(false)
|
||||
|
||||
const hasRecordBannerBeenShown = ref(true)
|
||||
const hasConnectBannerBeenShown = ref(true)
|
||||
const hasCreateOrganizationBannerBeenShown = ref(true)
|
||||
const hasLoginBannerBeenShown = ref(true)
|
||||
const hasRecordBannerBeenShown = ref(false)
|
||||
const hasConnectBannerBeenShown = ref(false)
|
||||
const hasCreateOrganizationBannerBeenShown = ref(false)
|
||||
const hasLoginBannerBeenShown = ref(false)
|
||||
|
||||
watch(
|
||||
() => ([props.isSpecNotFound, props.isOffline, props.isFetchError, props.isProjectNotFound, props.isProjectUnauthorized]),
|
||||
@@ -272,6 +276,50 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const bannerCohortOptions = {
|
||||
[BannerIds.ACI_082022_LOGIN]: [
|
||||
{ cohort: 'A', value: t('specPage.banners.login.contentA') },
|
||||
{ cohort: 'B', value: t('specPage.banners.login.contentB') },
|
||||
],
|
||||
[BannerIds.ACI_082022_CREATE_ORG]: [
|
||||
{ cohort: 'A', value: t('specPage.banners.createOrganization.titleA') },
|
||||
{ cohort: 'B', value: t('specPage.banners.createOrganization.titleB') },
|
||||
],
|
||||
[BannerIds.ACI_082022_CONNECT_PROJECT]: [
|
||||
{ cohort: 'A', value: t('specPage.banners.connectProject.contentA') },
|
||||
{ cohort: 'B', value: t('specPage.banners.connectProject.contentB') },
|
||||
],
|
||||
}
|
||||
|
||||
const cohortBuilder = useCohorts()
|
||||
|
||||
const getCohortForBanner = (bannerId: string) => {
|
||||
const cohortConfig: CohortConfig = {
|
||||
name: bannerId,
|
||||
options: bannerCohortOptions[bannerId],
|
||||
}
|
||||
|
||||
return cohortBuilder.getCohort(cohortConfig)
|
||||
}
|
||||
|
||||
type BannerType = 'login' | 'connectProject' | 'organization'
|
||||
|
||||
const cohorts: Partial<Record<BannerType, ReturnType<typeof getCohortForBanner>>> = {}
|
||||
|
||||
watchEffect(() => {
|
||||
if (!cohorts.login && showLoginBanner.value) {
|
||||
cohorts.login = getCohortForBanner(BannerIds.ACI_082022_LOGIN)
|
||||
}
|
||||
|
||||
if (!cohorts.organization && showCreateOrganizationBanner.value) {
|
||||
cohorts.organization = getCohortForBanner(BannerIds.ACI_082022_CREATE_ORG)
|
||||
}
|
||||
|
||||
if (!cohorts.connectProject && showConnectBanner.value) {
|
||||
cohorts.connectProject = getCohortForBanner(BannerIds.ACI_082022_CONNECT_PROJECT)
|
||||
}
|
||||
})
|
||||
|
||||
function hasBannerBeenDismissed (bannerId: string) {
|
||||
const bannersState = (props.gql.currentProject?.savedState as AllowedState)?.banners
|
||||
|
||||
|
||||
@@ -3,32 +3,44 @@ import ConnectProjectBanner from './ConnectProjectBanner.vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<ConnectProjectBanner />', () => {
|
||||
const cohortOption = { cohort: 'A', value: defaultMessages.specPage.banners.connectProject.contentA }
|
||||
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} /> })
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
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.contentA).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.connectProject.buttonLabel).should('be.visible')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('should record expected event on mount', () => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
context('events', () => {
|
||||
beforeEach(() => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
|
||||
return defineResult({ recordEvent: true })
|
||||
return defineResult({ recordEvent: true })
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} hasBannerBeenShown={false} /> })
|
||||
it('should record expected event on mount', () => {
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} hasBannerBeenShown={false} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Create project',
|
||||
medium: 'Specs Create Project Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Create project',
|
||||
medium: 'Specs Create Project Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: 'A',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not record event on mount if already shown', () => {
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_CONNECT_PROJECT"
|
||||
:banner-id="bannerId"
|
||||
:model-value="modelValue"
|
||||
data-cy="connect-project-banner"
|
||||
status="info"
|
||||
@@ -12,12 +12,12 @@
|
||||
:event-data="{
|
||||
campaign: 'Create project',
|
||||
medium: 'Specs Create Project Banner',
|
||||
cohort: '' // TODO Connect cohort
|
||||
cohort: cohortOption.cohort
|
||||
}"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<p class="mb-24px">
|
||||
{{ t('specPage.banners.connectProject.content') }}
|
||||
{{ cohortOption.value }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
@@ -45,6 +45,7 @@ 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 type { CohortOption } from '@packages/frontend-shared/src/composables/useCohorts'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { ref } from 'vue'
|
||||
import { ConnectProjectBannerDocument } from '../../generated/graphql'
|
||||
@@ -56,19 +57,18 @@ query ConnectProjectBanner {
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
hasBannerBeenShown: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
hasBannerBeenShown: true,
|
||||
})
|
||||
cohortOption: CohortOption
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.ACI_082022_CONNECT_PROJECT
|
||||
const isProjectConnectOpen = ref(false)
|
||||
|
||||
const cloudModalsQuery = useQuery({ query: ConnectProjectBannerDocument, pause: true })
|
||||
|
||||
@@ -3,6 +3,8 @@ import CreateOrganizationBanner from './CreateOrganizationBanner.vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<CreateOrganizationBanner />', () => {
|
||||
const cohortOption = { cohort: 'A', value: defaultMessages.specPage.banners.createOrganization.titleA }
|
||||
|
||||
it('should render expected content', () => {
|
||||
const linkHref = 'http://dummy.cypress.io/organizations/create'
|
||||
|
||||
@@ -12,9 +14,9 @@ describe('<CreateOrganizationBanner />', () => {
|
||||
cloudOrganizationsUrl: linkHref,
|
||||
} as any
|
||||
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} /> })
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.titleA).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.content).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.buttonLabel).should('be.visible')
|
||||
|
||||
@@ -25,22 +27,32 @@ describe('<CreateOrganizationBanner />', () => {
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('should record expected event on mount', () => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
context('events', () => {
|
||||
beforeEach(() => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
|
||||
return defineResult({ recordEvent: true })
|
||||
return defineResult({ recordEvent: true })
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} hasBannerBeenShown={false} /> })
|
||||
it('should record expected event on mount', () => {
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} hasBannerBeenShown={false} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Set up your organization',
|
||||
medium: 'Specs Create Organization Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Set up your organization',
|
||||
medium: 'Specs Create Organization Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: 'A',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not record event on mount if already shown', () => {
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_CREATE_ORG"
|
||||
:banner-id="bannerId"
|
||||
:model-value="modelValue"
|
||||
data-cy="create-organization-banner"
|
||||
status="info"
|
||||
:title="t('specPage.banners.createOrganization.title')"
|
||||
:title="cohortOption.value"
|
||||
class="mb-16px"
|
||||
:icon="OrganizationIcon"
|
||||
dismissible
|
||||
@@ -12,7 +12,7 @@
|
||||
:event-data="{
|
||||
campaign: 'Set up your organization',
|
||||
medium: 'Specs Create Organization Banner',
|
||||
cohort: '' // TODO Connect cohort
|
||||
cohort: cohortOption.cohort
|
||||
}"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
@@ -36,6 +36,7 @@
|
||||
import OrganizationIcon from '~icons/cy/office-building_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import type { CohortOption } from '@packages/frontend-shared/src/composables/useCohorts'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { CreateOrganizationBannerDocument } from '../../generated/graphql'
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
@@ -52,19 +53,18 @@ query CreateOrganizationBanner {
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
hasBannerBeenShown: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
hasBannerBeenShown: true,
|
||||
})
|
||||
cohortOption: CohortOption
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.ACI_082022_CREATE_ORG
|
||||
|
||||
const query = useQuery({ query: CreateOrganizationBannerDocument })
|
||||
|
||||
@@ -80,6 +80,7 @@ const createOrganizationUrl = computed(() => {
|
||||
params: {
|
||||
utm_medium: 'Specs Create Organization Banner',
|
||||
utm_campaign: 'Set up your organization',
|
||||
utm_content: props.cohortOption.cohort,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,32 +3,44 @@ import LoginBanner from './LoginBanner.vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<LoginBanner />', () => {
|
||||
const cohortOption = { cohort: 'A', value: defaultMessages.specPage.banners.login.contentA }
|
||||
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} /> })
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
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.contentA).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.login.buttonLabel).should('be.visible')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('should record expected event on mount', () => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
context('events', () => {
|
||||
beforeEach(() => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
|
||||
return defineResult({ recordEvent: true })
|
||||
return defineResult({ recordEvent: true })
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} hasBannerBeenShown={false} /> })
|
||||
it('should record expected event on mount', () => {
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} hasBannerBeenShown={false} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Log In',
|
||||
medium: 'Specs Login Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Log In',
|
||||
medium: 'Specs Login Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: cohortOption.cohort,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not record event on mount if already shown', () => {
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_LOGIN"
|
||||
:banner-id="bannerId"
|
||||
:model-value="modelValue"
|
||||
data-cy="login-banner"
|
||||
status="info"
|
||||
@@ -12,12 +12,12 @@
|
||||
:event-data="{
|
||||
campaign: 'Log In',
|
||||
medium: 'Specs Login Banner',
|
||||
cohort: '' // TODO Connect cohort
|
||||
cohort: cohortOption.cohort
|
||||
}"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<p class="mb-24px">
|
||||
{{ t('specPage.banners.login.content') }}
|
||||
{{ cohortOption.value }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
@@ -33,6 +33,7 @@
|
||||
v-model="isLoginOpen"
|
||||
:gql="loginModalQuery.data.value"
|
||||
utm-medium="Specs Login Banner"
|
||||
:utm-content="cohortOption.cohort"
|
||||
/>
|
||||
</TrackedBanner>
|
||||
</template>
|
||||
@@ -45,6 +46,7 @@ import { useI18n } from '@cy/i18n'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import { LoginBannerDocument } from '../../generated/graphql'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import type { CohortOption } from '@packages/frontend-shared/src/composables/useCohorts'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import LoginModal from '@cy/gql-components/topnav/LoginModal.vue'
|
||||
|
||||
@@ -54,19 +56,18 @@ query LoginBanner {
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
hasBannerBeenShown: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
hasBannerBeenShown: true,
|
||||
})
|
||||
cohortOption: CohortOption
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.ACI_082022_LOGIN
|
||||
const isLoginOpen = ref(false)
|
||||
const loginModalQuery = useQuery({ query: LoginBannerDocument, pause: true })
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<RecordBanner />', () => {
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <RecordBanner modelValue={true} /> })
|
||||
cy.mount({ render: () => <RecordBanner modelValue={true} hasBannerBeenShown={false} /> })
|
||||
|
||||
cy.gqlStub.Query.currentProject = {
|
||||
id: 'test_id',
|
||||
@@ -24,7 +24,7 @@ describe('<RecordBanner />', () => {
|
||||
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.findByText('npx cypress run --component --record --key abcd-efg-1234')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
@@ -44,7 +44,7 @@ describe('<RecordBanner />', () => {
|
||||
campaign: 'Record Runs',
|
||||
medium: 'Specs Record Runs Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
cohort: 'n/a',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_RECORD"
|
||||
:banner-id="bannerId"
|
||||
:model-value="modelValue"
|
||||
data-cy="record-banner"
|
||||
status="info"
|
||||
@@ -12,7 +12,7 @@
|
||||
:event-data="{
|
||||
campaign: 'Record Runs',
|
||||
medium: 'Specs Record Runs Banner',
|
||||
cohort: '' // TODO Connect cohort
|
||||
cohort: 'n/a'
|
||||
}"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
@@ -58,29 +58,28 @@ query RecordBanner {
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
hasBannerBeenShown: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
hasBannerBeenShown: true,
|
||||
})
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.ACI_082022_RECORD
|
||||
|
||||
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}`
|
||||
return `npx cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument, TrackedBanner_SetProjectStateDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<TrackedBanner />', () => {
|
||||
@@ -25,7 +25,7 @@ describe('<TrackedBanner />', () => {
|
||||
|
||||
// Initially mount as visible
|
||||
// @ts-ignore
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} hasBannerBeenShown={false} /> })
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} hasBannerBeenShown={false} eventData={{} as any}/> })
|
||||
|
||||
cy.get('[data-cy="banner"]').as('banner')
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('<TrackedBanner />', () => {
|
||||
|
||||
// Initially mount as visible
|
||||
// @ts-ignore
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} dismissible hasBannerBeenShown={false} /> })
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} dismissible hasBannerBeenShown={false} eventData={{} as any} /> })
|
||||
|
||||
cy.get('[data-cy="banner"]').as('banner')
|
||||
|
||||
@@ -74,32 +74,55 @@ describe('<TrackedBanner />', () => {
|
||||
})
|
||||
|
||||
context('when banner not previously shown', () => {
|
||||
let eventData
|
||||
|
||||
beforeEach(() => {
|
||||
const setProjectStateStub = cy.stub().as('setProjectState')
|
||||
const hasBannerBeenShown = ref(false)
|
||||
|
||||
// mock setting the project state which would reactively set the hasBannerBeenShown ref
|
||||
cy.stubMutationResolver(TrackedBanner_SetProjectStateDocument, (defineResult, event) => {
|
||||
setProjectStateStub(event)
|
||||
const preference = JSON.parse(event.value)
|
||||
|
||||
expect(preference).to.have.nested.property('banners.test-banner.lastShown')
|
||||
hasBannerBeenShown.value = true
|
||||
|
||||
return defineResult({ setPreferences: null }) // do not care about return value here
|
||||
})
|
||||
|
||||
eventData = reactive({ campaign: 'CAM', medium: 'MED', cohort: 'COH' })
|
||||
|
||||
cy.mount({
|
||||
render: () => <TrackedBanner bannerId="test-banner" modelValue={true} hasBannerBeenShown={false} eventData={{ campaign: 'CAM', medium: 'MED', cohort: 'COH' }} />,
|
||||
render: () => <TrackedBanner bannerId="test-banner" modelValue={true} hasBannerBeenShown={hasBannerBeenShown.value} eventData={eventData} />,
|
||||
})
|
||||
})
|
||||
|
||||
it('should record event', () => {
|
||||
cy.get('@recordEvent').should('have.been.calledOnce')
|
||||
eventData.cohort = 'COH2' //Change reactive variable to confirm the record event is not recorded a second time
|
||||
cy.get('@recordEvent').should(
|
||||
'have.been.calledWith',
|
||||
'have.been.calledOnceWith',
|
||||
Cypress.sinon.match({ campaign: 'CAM', messageId: Cypress.sinon.match.string, medium: 'MED', cohort: 'COH' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should debounce event recording', () => {
|
||||
eventData.cohort = 'COH'
|
||||
cy.wait(250)
|
||||
cy.get('@recordEvent').should('have.been.calledOnce')
|
||||
})
|
||||
})
|
||||
|
||||
context('when banner has been previously shown', () => {
|
||||
let eventData
|
||||
|
||||
beforeEach(() => {
|
||||
cy.mount({ render: () => <TrackedBanner bannerId="test-banner" modelValue={true} hasBannerBeenShown={true} eventData={{} as any} /> })
|
||||
eventData = reactive({ campaign: 'CAM', medium: 'MED', cohort: undefined })
|
||||
cy.mount({ render: () => <TrackedBanner bannerId="test-banner" modelValue={true} hasBannerBeenShown={true} eventData={eventData} /> })
|
||||
})
|
||||
|
||||
it('should not record event', () => {
|
||||
eventData.cohort = 'COH'
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
BrowserActions,
|
||||
DevActions,
|
||||
AuthActions,
|
||||
CohortsActions,
|
||||
} from './actions'
|
||||
import { ErrorActions } from './actions/ErrorActions'
|
||||
import { EventCollectorActions } from './actions/EventCollectorActions'
|
||||
@@ -83,4 +84,9 @@ export class DataActions {
|
||||
get eventCollector () {
|
||||
return new EventCollectorActions(this.ctx)
|
||||
}
|
||||
|
||||
@cached
|
||||
get cohorts () {
|
||||
return new CohortsActions(this.ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import _ from 'lodash'
|
||||
|
||||
import 'server-destroy'
|
||||
|
||||
import { AppApiShape, DataEmitterActions, LocalSettingsApiShape, ProjectApiShape } from './actions'
|
||||
import { AppApiShape, CohortsApiShape, DataEmitterActions, LocalSettingsApiShape, ProjectApiShape } from './actions'
|
||||
import type { NexusGenAbstractTypeMembers } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { AuthApiShape } from './actions/AuthActions'
|
||||
import type { ElectronApiShape } from './actions/ElectronActions'
|
||||
@@ -70,6 +70,7 @@ export interface DataContextConfig {
|
||||
projectApi: ProjectApiShape
|
||||
electronApi: ElectronApiShape
|
||||
browserApi: BrowserApiShape
|
||||
cohortsApi: CohortsApiShape
|
||||
}
|
||||
|
||||
export interface GraphQLRequestInfo {
|
||||
@@ -131,6 +132,10 @@ export class DataContext {
|
||||
return this._config.localSettingsApi
|
||||
}
|
||||
|
||||
get cohortsApi () {
|
||||
return this._config.cohortsApi
|
||||
}
|
||||
|
||||
get isGlobalMode () {
|
||||
return this.appData.isGlobalMode
|
||||
}
|
||||
@@ -324,6 +329,7 @@ export class DataContext {
|
||||
projectApi: this._config.projectApi,
|
||||
electronApi: this._config.electronApi,
|
||||
localSettingsApi: this._config.localSettingsApi,
|
||||
cohortsApi: this._config.cohortsApi,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
packages/data-context/src/actions/CohortsActions.ts
Normal file
54
packages/data-context/src/actions/CohortsActions.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Cohort } from '@packages/types'
|
||||
import type { DataContext } from '..'
|
||||
import { WEIGHTED, WEIGHTED_EVEN } from '../util/weightedChoice'
|
||||
const debug = require('debug')('cypress:data-context:actions:CohortActions')
|
||||
|
||||
export interface CohortsApiShape {
|
||||
getCohorts(): Promise<Record<string, Cohort> | undefined>
|
||||
|
||||
getCohort(name: string): Promise<Cohort | undefined>
|
||||
|
||||
insertCohort (cohort: Cohort): Promise<void>
|
||||
}
|
||||
|
||||
export class CohortsActions {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
async getCohorts () {
|
||||
debug('Getting all cohorts')
|
||||
|
||||
return this.ctx._apis.cohortsApi.getCohorts()
|
||||
}
|
||||
|
||||
async getCohort (name: string) {
|
||||
debug('Getting cohort for %s', name)
|
||||
|
||||
return this.ctx._apis.cohortsApi.getCohort(name)
|
||||
}
|
||||
|
||||
async determineCohort (name: string, cohorts: string[], weights?: number[]) {
|
||||
debug('Determining cohort', name, cohorts)
|
||||
|
||||
const cohortFromCache = await this.getCohort(name)
|
||||
|
||||
let cohortSelected: Cohort
|
||||
|
||||
if (!cohortFromCache || !cohorts.includes(cohortFromCache.cohort)) {
|
||||
const algorithm = weights ? WEIGHTED(weights) : WEIGHTED_EVEN(cohorts)
|
||||
const pickedCohort = {
|
||||
name,
|
||||
cohort: algorithm.pick(cohorts),
|
||||
}
|
||||
|
||||
debug('Inserting cohort for %o', pickedCohort)
|
||||
await this.ctx._apis.cohortsApi.insertCohort(pickedCohort)
|
||||
cohortSelected = pickedCohort
|
||||
} else {
|
||||
cohortSelected = cohortFromCache
|
||||
}
|
||||
|
||||
debug('Selecting cohort', cohortSelected)
|
||||
|
||||
return cohortSelected
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
export * from './AppActions'
|
||||
export * from './AuthActions'
|
||||
export * from './BrowserActions'
|
||||
export * from './CohortsActions'
|
||||
export * from './DataEmitterActions'
|
||||
export * from './DevActions'
|
||||
export * from './ElectronActions'
|
||||
|
||||
@@ -527,6 +527,7 @@ export class ProjectConfigManager {
|
||||
}
|
||||
|
||||
async getFullInitialConfig (options: Partial<AllModeOptions> = this.options.ctx.modeOptions, withBrowsers = true): Promise<FullConfig> {
|
||||
// return cached configuration for new spec and/or new navigating load when Cypress is running tests
|
||||
if (this._cachedFullConfig) {
|
||||
return this._cachedFullConfig
|
||||
}
|
||||
|
||||
@@ -120,10 +120,7 @@ export class HtmlDataSource {
|
||||
window.__CYPRESS_CONFIG__ = ${JSON.stringify(serveConfig)};
|
||||
window.__CYPRESS_TESTING_TYPE__ = '${this.ctx.coreData.currentTestingType}'
|
||||
window.__CYPRESS_BROWSER__ = ${JSON.stringify(this.ctx.coreData.activeBrowser)}
|
||||
${process.env.CYPRESS_INTERNAL_GQL_NO_SOCKET
|
||||
? `window.__CYPRESS_GQL_NO_SOCKET__ = 'true';`
|
||||
: ''
|
||||
}
|
||||
${process.env.CYPRESS_INTERNAL_GQL_NO_SOCKET ? `window.__CYPRESS_GQL_NO_SOCKET__ = 'true';` : ''}
|
||||
</script>
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './file'
|
||||
export * from './hasTypescript'
|
||||
export * from './pluginHandlers'
|
||||
export * from './urqlCacheKeys'
|
||||
export * from './weightedChoice'
|
||||
|
||||
57
packages/data-context/src/util/weightedChoice.ts
Normal file
57
packages/data-context/src/util/weightedChoice.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export type WeightedAlgorithm = {
|
||||
pick: (values: string[]) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomly choose an index from an array based on weights
|
||||
*
|
||||
* Based on algorithm found here: https://dev.to/trekhleb/weighted-random-algorithm-in-javascript-1pdc
|
||||
*
|
||||
* @param weights array of numbered weights that correspond to the indexed values
|
||||
* @param values array of values to choose from
|
||||
*/
|
||||
const weightedChoice = (weights: number[], values: any[]) => {
|
||||
if (weights.length === 0 || values.length === 0 || weights.length !== values.length) {
|
||||
throw new Error('The length of the weights and values must be the same and greater than zero')
|
||||
}
|
||||
|
||||
const cumulativeWeights = weights.reduce<number[]>((acc, curr) => {
|
||||
if (acc.length === 0) {
|
||||
return [curr]
|
||||
}
|
||||
|
||||
const last = acc[acc.length - 1]
|
||||
|
||||
if (!last) {
|
||||
return acc
|
||||
}
|
||||
|
||||
return [...acc, last + curr]
|
||||
}, [])
|
||||
|
||||
const randomNumber = Math.random() * (cumulativeWeights[cumulativeWeights.length - 1] ?? 1)
|
||||
|
||||
const choice = _.transform(cumulativeWeights, (result, value, index) => {
|
||||
if (value >= randomNumber) {
|
||||
result.chosenIndex = index
|
||||
}
|
||||
|
||||
return result.chosenIndex === -1
|
||||
}, { chosenIndex: -1 })
|
||||
|
||||
return values[choice.chosenIndex]
|
||||
}
|
||||
|
||||
export const WEIGHTED = (weights: number[]): WeightedAlgorithm => {
|
||||
return {
|
||||
pick: (values: any[]): string => {
|
||||
return weightedChoice(weights, values)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const WEIGHTED_EVEN = (values: any[]): WeightedAlgorithm => {
|
||||
return WEIGHTED(_.fill(Array(values.length), 1))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { DataContext } from '../../../src'
|
||||
import { CohortsActions } from '../../../src/actions/CohortsActions'
|
||||
import { createTestDataContext } from '../helper'
|
||||
import { expect } from 'chai'
|
||||
import sinon, { SinonStub, match } from 'sinon'
|
||||
|
||||
describe('CohortsActions', () => {
|
||||
let ctx: DataContext
|
||||
let actions: CohortsActions
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.restore()
|
||||
|
||||
ctx = createTestDataContext('open')
|
||||
|
||||
actions = new CohortsActions(ctx)
|
||||
})
|
||||
|
||||
context('getCohort', () => {
|
||||
it('should return null if name not found', async () => {
|
||||
const name = '123'
|
||||
|
||||
const cohort = await actions.getCohort(name)
|
||||
|
||||
expect(cohort).to.be.undefined
|
||||
expect(ctx.cohortsApi.getCohort).to.have.been.calledWith(name)
|
||||
})
|
||||
|
||||
it('should return cohort if in cache', async () => {
|
||||
const cohort = {
|
||||
name: 'loginBanner',
|
||||
cohort: 'A',
|
||||
}
|
||||
|
||||
;(ctx._apis.cohortsApi.getCohort as SinonStub).resolves(cohort)
|
||||
|
||||
const cohortReturned = await actions.getCohort(cohort.name)
|
||||
|
||||
expect(cohortReturned).to.eq(cohort)
|
||||
expect(ctx.cohortsApi.getCohort).to.have.been.calledWith(cohort.name)
|
||||
})
|
||||
})
|
||||
|
||||
context('determineCohort', () => {
|
||||
it('should determine cohort', async () => {
|
||||
const cohortConfig = {
|
||||
name: 'loginBanner',
|
||||
cohorts: ['A', 'B'],
|
||||
}
|
||||
|
||||
const pickedCohort = await actions.determineCohort(cohortConfig.name, cohortConfig.cohorts)
|
||||
|
||||
expect(ctx.cohortsApi.insertCohort).to.have.been.calledOnceWith({ name: cohortConfig.name, cohort: match.string })
|
||||
expect(cohortConfig.cohorts.includes(pickedCohort.cohort)).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@ import { DataContext, DataContextConfig } from '../../src'
|
||||
import { graphqlSchema } from '@packages/graphql/src/schema'
|
||||
import { remoteSchemaWrapped as schemaCloud } from '@packages/graphql/src/stitching/remoteSchemaWrapped'
|
||||
import type { BrowserApiShape } from '../../src/sources/BrowserDataSource'
|
||||
import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape } from '../../src/actions'
|
||||
import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape, CohortsApiShape } from '../../src/actions'
|
||||
import sinon from 'sinon'
|
||||
import { execute, parse } from 'graphql'
|
||||
import { getOperationName } from '@urql/core'
|
||||
@@ -63,6 +63,12 @@ export function createTestDataContext (mode: DataContextConfig['mode'] = 'run',
|
||||
focusActiveBrowserWindow: sinon.stub(),
|
||||
getBrowsers: sinon.stub().resolves([]),
|
||||
} as unknown as BrowserApiShape,
|
||||
cohortsApi: {
|
||||
getCohorts: sinon.stub().resolves(),
|
||||
getCohort: sinon.stub().resolves(),
|
||||
insertCohort: sinon.stub(),
|
||||
determineCohort: sinon.stub().resolves(),
|
||||
} as unknown as CohortsApiShape,
|
||||
})
|
||||
|
||||
const origFetch = ctx.util.fetch
|
||||
|
||||
75
packages/data-context/test/unit/util/weightedChoice.spec.ts
Normal file
75
packages/data-context/test/unit/util/weightedChoice.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { WEIGHTED, WEIGHTED_EVEN } from '../../../src/util/weightedChoice'
|
||||
|
||||
describe('weightedChoice', () => {
|
||||
context('WeightedAlgorithm', () => {
|
||||
it('should error if invalid arguments', () => {
|
||||
const weights = [25, 75, 45]
|
||||
const options = ['A', 'B']
|
||||
|
||||
const func = () => {
|
||||
WEIGHTED(weights).pick(options)
|
||||
}
|
||||
|
||||
expect(func).to.throw()
|
||||
})
|
||||
|
||||
it('should error if weights is empty', () => {
|
||||
const weights = []
|
||||
const options = ['A', 'B']
|
||||
|
||||
const func = () => {
|
||||
WEIGHTED(weights).pick(options)
|
||||
}
|
||||
|
||||
expect(func).to.throw()
|
||||
})
|
||||
|
||||
it('should error if options is empty', () => {
|
||||
const weights = [25, 75, 45]
|
||||
const options = []
|
||||
|
||||
const func = () => {
|
||||
WEIGHTED(weights).pick(options)
|
||||
}
|
||||
|
||||
expect(func).to.throw()
|
||||
})
|
||||
|
||||
it('should return an option', () => {
|
||||
const weights = [25, 75]
|
||||
const options = ['A', 'B']
|
||||
const selected = WEIGHTED(weights).pick(options)
|
||||
|
||||
expect(options.includes(selected)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
context('WEIGHTED_EVEN', () => {
|
||||
it('should return an option', () => {
|
||||
const options = ['A', 'B']
|
||||
const selected = WEIGHTED_EVEN(options).pick(options)
|
||||
|
||||
expect(options.includes(selected)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
context('randomness', () => {
|
||||
it('should return values close to supplied weights', () => {
|
||||
const results = {}
|
||||
const options = ['A', 'B']
|
||||
const algorithm = WEIGHTED_EVEN(options)
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const selected = algorithm.pick(options)
|
||||
|
||||
results[selected] ? results[selected]++ : results[selected] = 1
|
||||
}
|
||||
|
||||
Object.keys(results).forEach((key) => {
|
||||
expect(Math.round(results[key] / 100)).to.equal(5)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -237,6 +237,7 @@ describe('src/cy/commands/sessions/manager.ts', () => {
|
||||
validate: undefined,
|
||||
cookies: null,
|
||||
localStorage: null,
|
||||
sessionStorage: null,
|
||||
hydrated: false,
|
||||
})
|
||||
|
||||
@@ -245,7 +246,7 @@ describe('src/cy/commands/sessions/manager.ts', () => {
|
||||
})
|
||||
|
||||
it('sessions.clearAllSavedSessions()', async () => {
|
||||
const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('clear:session').resolves(null)
|
||||
const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('clear:sessions', true).resolves(null)
|
||||
|
||||
const sessionsManager = new SessionsManager(CypressInstance, () => {})
|
||||
const sessionsSpy = cy.stub(sessionsManager, 'clearActiveSessions')
|
||||
@@ -253,7 +254,7 @@ describe('src/cy/commands/sessions/manager.ts', () => {
|
||||
await sessionsManager.sessions.clearAllSavedSessions()
|
||||
|
||||
expect(sessionsSpy).to.be.calledOnce
|
||||
expect(cypressSpy).to.be.calledOnceWith('clear:session', null)
|
||||
expect(cypressSpy).to.be.calledOnceWith('clear:sessions', true)
|
||||
})
|
||||
|
||||
describe('.clearCurrentSessionData()', () => {
|
||||
|
||||
@@ -10,6 +10,8 @@ import { bothUrlsMatchAndOneHasHash } from '../navigation'
|
||||
import { $Location, LocationObject } from '../../cypress/location'
|
||||
import { isRunnerAbleToCommunicateWithAut } from '../../util/commandAUTCommunication'
|
||||
|
||||
import type { RunState } from '@packages/types'
|
||||
|
||||
import debugFn from 'debug'
|
||||
const debug = debugFn('cypress:driver:navigation')
|
||||
|
||||
@@ -1116,26 +1118,26 @@ export default (Commands, Cypress, cy, state, config) => {
|
||||
// tell our backend we're changing origins
|
||||
// TODO: add in other things we want to preserve
|
||||
// state for like scrollTop
|
||||
let s: Record<string, any> = {
|
||||
let runState: RunState = {
|
||||
currentId: id,
|
||||
tests: Cypress.runner.getTestsState(),
|
||||
startTime: Cypress.runner.getStartTime(),
|
||||
emissions: Cypress.runner.getEmissions(),
|
||||
}
|
||||
|
||||
s.passed = Cypress.runner.countByTestState(s.tests, 'passed')
|
||||
s.failed = Cypress.runner.countByTestState(s.tests, 'failed')
|
||||
s.pending = Cypress.runner.countByTestState(s.tests, 'pending')
|
||||
s.numLogs = LogUtils.countLogsByTests(s.tests)
|
||||
runState.passed = Cypress.runner.countByTestState(runState.tests, 'passed')
|
||||
runState.failed = Cypress.runner.countByTestState(runState.tests, 'failed')
|
||||
runState.pending = Cypress.runner.countByTestState(runState.tests, 'pending')
|
||||
runState.numLogs = LogUtils.countLogsByTests(runState.tests)
|
||||
|
||||
return Cypress.action('cy:collect:run:state')
|
||||
.then((a = []) => {
|
||||
.then((otherRunStates = []) => {
|
||||
// merge all the states together holla'
|
||||
s = _.reduce(a, (memo, obj) => {
|
||||
runState = _.reduce(otherRunStates, (memo, obj) => {
|
||||
return _.extend(memo, obj)
|
||||
}, s)
|
||||
}, runState)
|
||||
|
||||
return Cypress.backend('preserve:run:state', s)
|
||||
return Cypress.backend('preserve:run:state', runState)
|
||||
})
|
||||
.then(() => {
|
||||
// and now we must change the url to be the new
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash'
|
||||
import { $Location } from '../../../cypress/location'
|
||||
|
||||
import type { ServerSessionData } from '@packages/types'
|
||||
import {
|
||||
getCurrentOriginStorage,
|
||||
setPostMessageLocalStorage,
|
||||
@@ -120,6 +120,7 @@ export default class SessionsManager {
|
||||
id: options.id,
|
||||
cookies: null,
|
||||
localStorage: null,
|
||||
sessionStorage: null,
|
||||
setup: options.setup,
|
||||
hydrated: false,
|
||||
validate: options.validate,
|
||||
@@ -132,8 +133,9 @@ export default class SessionsManager {
|
||||
|
||||
clearAllSavedSessions: async () => {
|
||||
this.clearActiveSessions()
|
||||
const clearAllSessions = true
|
||||
|
||||
return this.Cypress.backend('clear:session', null)
|
||||
return this.Cypress.backend('clear:sessions', clearAllSessions)
|
||||
},
|
||||
|
||||
clearCurrentSessionData: async () => {
|
||||
@@ -205,7 +207,7 @@ export default class SessionsManager {
|
||||
}
|
||||
},
|
||||
|
||||
getSession: (id: string) => {
|
||||
getSession: (id: string): Promise<ServerSessionData> => {
|
||||
return this.Cypress.backend('get:session', id)
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash'
|
||||
import $ from 'jquery'
|
||||
import { $Location } from '../../../cypress/location'
|
||||
import Bluebird from 'bluebird'
|
||||
import { $Location } from '../../../cypress/location'
|
||||
|
||||
type SessionData = Cypress.Commands.Session.SessionData
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ import * as resolvers from './cypress/resolvers'
|
||||
import { PrimaryOriginCommunicator, SpecBridgeCommunicator } from './cross-origin/communicator'
|
||||
import { setupAutEventHandlers } from './cypress/aut_event_handlers'
|
||||
|
||||
import type { CachedTestState } from '@packages/types'
|
||||
|
||||
const debug = debugFn('cypress:driver:cypress')
|
||||
|
||||
declare global {
|
||||
@@ -280,11 +282,13 @@ class $Cypress {
|
||||
}
|
||||
}
|
||||
|
||||
run (fn) {
|
||||
run (cachedTestState: CachedTestState, fn) {
|
||||
if (!this.runner) {
|
||||
$errUtils.throwErrByPath('miscellaneous.no_runner')
|
||||
}
|
||||
|
||||
this.state(cachedTestState)
|
||||
|
||||
return this.runner.run(fn)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ declare namespace Cypress {
|
||||
type SessionSetup = (log: Cypress.Log) => Chainable<S>
|
||||
type SessionValidation = (log: Cypress.Log) => Chainable<S>
|
||||
|
||||
interface LocalStorage {
|
||||
interface Storage {
|
||||
origin: string
|
||||
value: Record<string, any>
|
||||
}
|
||||
@@ -14,7 +14,8 @@ declare namespace Cypress {
|
||||
interface SessionData {
|
||||
id: string
|
||||
cookies?: Array<Cypress.Cookie> | null
|
||||
localStorage?: Array<LocalStorage> | null
|
||||
localStorage?: Array<Storage> | null
|
||||
sessionStorage?: Array<Storage> | null
|
||||
setup: () => void
|
||||
hydrated: boolean
|
||||
validate?: Cypress.SessionOptions['validate']
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import UseCohortsExample, { CopyOption } from './UseCohortsExample.vue'
|
||||
import { UseCohorts_DetermineCohortDocument } from '../../generated/graphql'
|
||||
|
||||
describe('useCohorts example', () => {
|
||||
const copyOptions: CopyOption[] = [
|
||||
{ cohort: 'A', value: 'Notification Title A' },
|
||||
{ cohort: 'B', value: 'Notification Title B' },
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
cy.stubMutationResolver(UseCohorts_DetermineCohortDocument, (defineResult) => {
|
||||
return defineResult({ determineCohort: { __typename: 'Cohort', name: 'foo', cohort: 'A' } })
|
||||
})
|
||||
})
|
||||
|
||||
it('should show value for one cohort with default algorithm', () => {
|
||||
cy.mount(() => <UseCohortsExample copyOptions={copyOptions}/>)
|
||||
cy.findByTestId('result').then((elem) => {
|
||||
expect(copyOptions.map((option) => option.value)).to.include(elem.text())
|
||||
})
|
||||
})
|
||||
|
||||
it('should show value for one cohort with supplied algorithm', () => {
|
||||
const weighted25_75 = [25, 75]
|
||||
|
||||
cy.mount(() => <UseCohortsExample copyOptions={copyOptions} weights={weighted25_75}/>)
|
||||
cy.findByTestId('result').then((elem) => {
|
||||
expect(copyOptions.map((option) => option.value)).to.include(elem.text())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div data-cy="result">
|
||||
{{ cohortChoice?.value }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type CopyOption = {
|
||||
cohort: string
|
||||
value: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CohortConfig, useCohorts } from '../useCohorts'
|
||||
|
||||
const props = defineProps<{
|
||||
weights?: number[]
|
||||
copyOptions: CopyOption[]
|
||||
}>()
|
||||
|
||||
const cohortConfig: CohortConfig = {
|
||||
name: 'login',
|
||||
options: props.copyOptions,
|
||||
weights: props.weights,
|
||||
}
|
||||
|
||||
const cohortChoice = useCohorts().getCohort(cohortConfig)
|
||||
|
||||
</script>
|
||||
83
packages/frontend-shared/src/composables/useCohorts.ts
Normal file
83
packages/frontend-shared/src/composables/useCohorts.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useMutation, gql } from '@urql/vue'
|
||||
import { UseCohorts_DetermineCohortDocument } from '../generated/graphql'
|
||||
import { ref } from 'vue'
|
||||
|
||||
gql`
|
||||
mutation UseCohorts_DetermineCohort ($name: String!, $cohorts: [String!]!) {
|
||||
determineCohort(cohortConfig: { name: $name, cohorts: $cohorts } ) {
|
||||
__typename
|
||||
name
|
||||
cohort
|
||||
}
|
||||
}`
|
||||
|
||||
/**
|
||||
* An option to use for a given cohort selection.
|
||||
*/
|
||||
export type CohortOption = {
|
||||
/** The individual cohort identifier. Example: 'A' or 'B' */
|
||||
cohort: string
|
||||
|
||||
/** The value to be used by the calling code for the given cohort. The algorithm for selecting the cohort does not care about this value, but it will return the entire CohortOption that is selected so that this value can be used by the calling code. */
|
||||
value: any
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration of the cohort to be used to select a cohort.
|
||||
*/
|
||||
export type CohortConfig = {
|
||||
/** The name of the feature the cohort will be calculated for. This will be used as a key in the cache file for storing the selected option. */
|
||||
name: string
|
||||
|
||||
/** Array of options to pick from when selecting the cohort */
|
||||
options: CohortOption[]
|
||||
|
||||
/** Optional array of weights to use for selecting the cohort. If not supplied, an even weighting algorithm will be used. */
|
||||
weights?: number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable that encapsulates the logic for choosing a cohort from a list of configured options.
|
||||
*
|
||||
* @remarks
|
||||
* The logic for this composable will first check the cache file to determine if a cohort has already been saved for the given cohort `name`. If found, that cohort will be returned. If not found or the option found does not match an existing option, a weighted algorithm will be used to pick from the list of CohortOptions. The picked value will be stored in the cache and returned.
|
||||
*
|
||||
* @returns object with getCohort function for returning the cohort
|
||||
*/
|
||||
export const useCohorts = () => {
|
||||
const determineCohortMutation = useMutation(UseCohorts_DetermineCohortDocument)
|
||||
|
||||
const determineCohort = async (name: string, cohorts: string[]) => {
|
||||
return await determineCohortMutation.executeMutation({
|
||||
name,
|
||||
cohorts,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cohort from the list of configured options
|
||||
*
|
||||
* @param config - cohort configuration that contains the options to choose from and optionally the algorithm to use. Defaults to using the WEIGHTED_EVEN algorithm
|
||||
*
|
||||
* @returns a reactive reference to the cohort option that is selected
|
||||
*/
|
||||
const getCohort = (config: CohortConfig) => {
|
||||
const cohortOptionSelected = ref<CohortOption>()
|
||||
|
||||
const cohortIds = config.options.map((option) => option.cohort)
|
||||
|
||||
const fetchCohort = async () => {
|
||||
const cohortSelected = await determineCohort(config.name, cohortIds)
|
||||
|
||||
cohortOptionSelected.value = config.options.find((option) => option.cohort === cohortSelected.data?.determineCohort?.cohort)
|
||||
}
|
||||
|
||||
fetchCohort()
|
||||
|
||||
return cohortOptionSelected
|
||||
}
|
||||
|
||||
return {
|
||||
getCohort,
|
||||
}
|
||||
}
|
||||
@@ -197,17 +197,20 @@
|
||||
"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.",
|
||||
"contentA": "Parallelize your tests in CI and visualize every error by watching full video recordings of each test you run.",
|
||||
"contentB": "When you configure Cypress to record tests to the Cypress Dashboard, you'll see data from your latest recorded runs in the Cypress app. This increased visibility into your test history allows you to debug your tests faster and more effectively, all within your local workflow.",
|
||||
"buttonLabel": "Get started with Cypress Dashboard"
|
||||
},
|
||||
"createOrganization": {
|
||||
"title": "Finish setting up Cypress Dashboard",
|
||||
"titleA": "Finish setting up Cypress Dashboard",
|
||||
"titleB": "Create or join an organization",
|
||||
"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.",
|
||||
"contentA": "View recorded test runs directly in the Cypress app to monitor, run, and fix tests locally.",
|
||||
"contentB": "Bring your recorded test results into your local development workflow to monitor, run, and fix tests all in the Cypress app.",
|
||||
"buttonLabel": "Connect your project"
|
||||
},
|
||||
"record": {
|
||||
|
||||
@@ -555,6 +555,28 @@ enum CodeLanguageEnum {
|
||||
ts
|
||||
}
|
||||
|
||||
"""used to distinguish one group of users from another"""
|
||||
type Cohort {
|
||||
"""value used to indicate the cohort (e.g. "A" or "B")"""
|
||||
cohort: String!
|
||||
|
||||
"""name used to identify the cohort topic (e.g. "LoginBanner" ) """
|
||||
name: String!
|
||||
}
|
||||
|
||||
input CohortInput {
|
||||
"""Array of cohort options to choose from. Ex: A or B """
|
||||
cohorts: [String!]!
|
||||
|
||||
"""Name of the cohort"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Optional array of integer weights to use for determining cohort. Defaults to even weighting
|
||||
"""
|
||||
weights: [Int!]
|
||||
}
|
||||
|
||||
"""
|
||||
The currently opened Cypress project, represented by a cypress.config.{js,ts,mjs,cjs} file
|
||||
"""
|
||||
@@ -1198,6 +1220,11 @@ type Mutation {
|
||||
"""add the passed text to the local clipboard"""
|
||||
copyTextToClipboard(text: String!): Boolean
|
||||
|
||||
"""
|
||||
Determine the cohort based on the given configuration. This will either return the cached cohort for a given name or choose a new one and store it.
|
||||
"""
|
||||
determineCohort(cohortConfig: CohortInput!): Cohort
|
||||
|
||||
"""
|
||||
Development only: Triggers or dismisses a prompted refresh by touching the file watched by our development scripts
|
||||
"""
|
||||
@@ -1494,6 +1521,12 @@ type Query {
|
||||
"""A user within the Cypress Cloud"""
|
||||
cloudViewer: CloudUser
|
||||
|
||||
"""Return the cohort for the given name"""
|
||||
cohort(
|
||||
"""the name of the cohort to find"""
|
||||
name: String!
|
||||
): Cohort
|
||||
|
||||
"""The currently opened project"""
|
||||
currentProject: CurrentProject
|
||||
|
||||
|
||||
19
packages/graphql/src/schemaTypes/objectTypes/gql-Cohorts.ts
Normal file
19
packages/graphql/src/schemaTypes/objectTypes/gql-Cohorts.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { inputObjectType, objectType } from 'nexus'
|
||||
|
||||
export const Cohort = objectType({
|
||||
name: 'Cohort',
|
||||
description: 'used to distinguish one group of users from another',
|
||||
definition (t) {
|
||||
t.nonNull.string('name', { description: 'name used to identify the cohort topic (e.g. "LoginBanner" ) ' })
|
||||
t.nonNull.string('cohort', { description: 'value used to indicate the cohort (e.g. "A" or "B")' })
|
||||
},
|
||||
})
|
||||
|
||||
export const CohortInput = inputObjectType({
|
||||
name: 'CohortInput',
|
||||
definition (t) {
|
||||
t.nonNull.string('name', { description: 'Name of the cohort' })
|
||||
t.nonNull.list.nonNull.string('cohorts', { description: 'Array of cohort options to choose from. Ex: A or B ' })
|
||||
t.list.nonNull.int('weights', { description: 'Optional array of integer weights to use for determining cohort. Defaults to even weighting' })
|
||||
},
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import { FileDetailsInput } from '../inputTypes/gql-FileDetailsInput'
|
||||
import { WizardUpdateInput } from '../inputTypes/gql-WizardUpdateInput'
|
||||
import { CurrentProject } from './gql-CurrentProject'
|
||||
import { GenerateSpecResponse } from './gql-GenerateSpecResponse'
|
||||
import { Cohort, CohortInput } from './gql-Cohorts'
|
||||
import { Query } from './gql-Query'
|
||||
import { ScaffoldedFile } from './gql-ScaffoldedFile'
|
||||
import { WIZARD_BUNDLERS, WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
@@ -684,6 +685,17 @@ export const mutation = mutationType({
|
||||
},
|
||||
})
|
||||
|
||||
t.field('determineCohort', {
|
||||
type: Cohort,
|
||||
description: 'Determine the cohort based on the given configuration. This will either return the cached cohort for a given name or choose a new one and store it.',
|
||||
args: {
|
||||
cohortConfig: nonNull(CohortInput),
|
||||
},
|
||||
resolve: async (source, args, ctx) => {
|
||||
return ctx.actions.cohorts.determineCohort(args.cohortConfig.name, args.cohortConfig.cohorts, args.cohortConfig.weights || undefined)
|
||||
},
|
||||
})
|
||||
|
||||
t.field('recordEvent', {
|
||||
type: 'Boolean',
|
||||
description: 'Dispatch an event to the dashboard to be recorded. Events are completely anonymous and are only used to identify aggregate usage patterns across all Cypress users.',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { idArg, nonNull, objectType } from 'nexus'
|
||||
import { idArg, stringArg, nonNull, objectType } from 'nexus'
|
||||
import { ProjectLike, ScaffoldedFile } from '..'
|
||||
import { CurrentProject } from './gql-CurrentProject'
|
||||
import { DevState } from './gql-DevState'
|
||||
@@ -9,6 +9,7 @@ import { VersionData } from './gql-VersionData'
|
||||
import { Wizard } from './gql-Wizard'
|
||||
import { ErrorWrapper } from './gql-ErrorWrapper'
|
||||
import { CachedUser } from './gql-CachedUser'
|
||||
import { Cohort } from './gql-Cohorts'
|
||||
|
||||
export const Query = objectType({
|
||||
name: 'Query',
|
||||
@@ -107,6 +108,17 @@ export const Query = objectType({
|
||||
resolve: (source, args, ctx) => Boolean(ctx.modeOptions.invokedFromCli),
|
||||
})
|
||||
|
||||
t.field('cohort', {
|
||||
description: 'Return the cohort for the given name',
|
||||
type: Cohort,
|
||||
args: {
|
||||
name: nonNull(stringArg({ description: 'the name of the cohort to find' })),
|
||||
},
|
||||
resolve: async (source, args, ctx) => {
|
||||
return await ctx.cohortsApi.getCohort(args.name) ?? null
|
||||
},
|
||||
})
|
||||
|
||||
t.field('node', {
|
||||
type: 'Node',
|
||||
args: {
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from './gql-Browser'
|
||||
export * from './gql-CachedUser'
|
||||
export * from './gql-CodeFrame'
|
||||
export * from './gql-CodeGenGlobs'
|
||||
export * from './gql-Cohorts'
|
||||
export * from './gql-CurrentProject'
|
||||
export * from './gql-DevState'
|
||||
export * from './gql-Editor'
|
||||
|
||||
@@ -36,8 +36,8 @@ const debug = null
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/1756
|
||||
const zlibOptions = {
|
||||
flush: zlib.Z_SYNC_FLUSH,
|
||||
finishFlush: zlib.Z_SYNC_FLUSH,
|
||||
flush: zlib.constants.Z_SYNC_FLUSH,
|
||||
finishFlush: zlib.constants.Z_SYNC_FLUSH,
|
||||
}
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/1543
|
||||
|
||||
@@ -3,12 +3,7 @@ import { action, computed, observable } from 'mobx'
|
||||
import { TestState } from '../test/test-model'
|
||||
import { IntervalID } from '../lib/types'
|
||||
|
||||
export interface StatsStoreStartInfo {
|
||||
startTime: string
|
||||
numPassed?: number
|
||||
numFailed?: number
|
||||
numPending?: number
|
||||
}
|
||||
import type { StatsStoreStartInfo } from '@packages/types'
|
||||
|
||||
const defaults = {
|
||||
numPassed: 0,
|
||||
|
||||
@@ -2,10 +2,12 @@ import { EventEmitter } from 'events'
|
||||
import { action } from 'mobx'
|
||||
import appState, { AppState } from './app-state'
|
||||
import runnablesStore, { RunnablesStore, RootRunnable, LogProps } from '../runnables/runnables-store'
|
||||
import statsStore, { StatsStore, StatsStoreStartInfo } from '../header/stats-store'
|
||||
import statsStore, { StatsStore } from '../header/stats-store'
|
||||
import scroller, { Scroller } from './scroller'
|
||||
import TestModel, { UpdatableTestProps, UpdateTestCallback, TestProps } from '../test/test-model'
|
||||
|
||||
import type { ReporterStartInfo, ReporterRunState } from '@packages/types'
|
||||
|
||||
const localBus = new EventEmitter()
|
||||
|
||||
interface InitEvent {
|
||||
@@ -32,16 +34,7 @@ export interface Events {
|
||||
__off: (() => void)
|
||||
}
|
||||
|
||||
interface StartInfo extends StatsStoreStartInfo {
|
||||
autoScrollingEnabled: boolean
|
||||
scrollTop: number
|
||||
studioActive: boolean
|
||||
}
|
||||
|
||||
type CollectRunStateCallback = (arg: {
|
||||
autoScrollingEnabled: boolean
|
||||
scrollTop: number
|
||||
}) => void
|
||||
type CollectRunStateCallback = (arg: ReporterRunState) => void
|
||||
|
||||
const events: Events = {
|
||||
appState,
|
||||
@@ -88,7 +81,7 @@ const events: Events = {
|
||||
}
|
||||
}))
|
||||
|
||||
runner.on('reporter:start', action('start', (startInfo: StartInfo) => {
|
||||
runner.on('reporter:start', action('start', (startInfo: ReporterStartInfo) => {
|
||||
appState.temporarilySetAutoScrolling(startInfo.autoScrollingEnabled)
|
||||
runnablesStore.setInitialScrollTop(startInfo.scrollTop)
|
||||
appState.setStudioActive(startInfo.studioActive)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// Studio tests have been removed with v10 update.
|
||||
// You can find the tests in the PR below.
|
||||
// @see https://github.com/cypress-io/cypress/pull/9542
|
||||
|
||||
export * from './studio-recorder'
|
||||
@@ -45,6 +45,7 @@ module.exports = {
|
||||
PROJECTS: [],
|
||||
PROJECT_PREFERENCES: {},
|
||||
PROJECTS_CONFIG: {},
|
||||
COHORTS: {},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -184,6 +185,21 @@ module.exports = {
|
||||
return fileUtil.set({ PROJECT_PREFERENCES: updatedPreferences })
|
||||
},
|
||||
|
||||
getCohorts () {
|
||||
return fileUtil.get('COHORTS', {})
|
||||
},
|
||||
|
||||
insertCohort (cohort) {
|
||||
return fileUtil.transaction((tx) => {
|
||||
return tx.get('COHORTS', {}).then((cohorts) => {
|
||||
return tx.set('COHORTS', {
|
||||
...cohorts,
|
||||
[cohort.name]: cohort,
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
remove () {
|
||||
return fileUtil.remove()
|
||||
},
|
||||
|
||||
25
packages/server/lib/cohorts.ts
Normal file
25
packages/server/lib/cohorts.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
const cache = require('./cache')
|
||||
import type { Cohort } from '@packages/types'
|
||||
const debug = require('debug')('cypress:server:cohorts')
|
||||
|
||||
export = {
|
||||
get: (): Promise<Record<string, Cohort>> => {
|
||||
debug('Get cohorts')
|
||||
|
||||
return cache.getCohorts()
|
||||
},
|
||||
getByName: (name: string): Promise<Cohort> => {
|
||||
debug('Get cohort name:', name)
|
||||
|
||||
return cache.getCohorts().then((cohorts) => {
|
||||
debug('Get cohort returning:', cohorts[name])
|
||||
|
||||
return cohorts[name]
|
||||
})
|
||||
},
|
||||
set: (cohort: Cohort) => {
|
||||
debug('Set cohort', cohort)
|
||||
|
||||
return cache.insertCohort(cohort)
|
||||
},
|
||||
}
|
||||
@@ -2,10 +2,6 @@ import _ from 'lodash'
|
||||
import type { ResolvedFromConfig } from '@packages/types'
|
||||
import * as configUtils from '@packages/config'
|
||||
|
||||
export const setupFullConfigWithDefaults = configUtils.setupFullConfigWithDefaults
|
||||
|
||||
export const updateWithPluginValues = configUtils.updateWithPluginValues
|
||||
|
||||
export const setUrls = configUtils.setUrls
|
||||
|
||||
export function getResolvedRuntimeConfig (config, runtimeConfig) {
|
||||
|
||||
@@ -140,7 +140,7 @@ export const getExperiments = (project: CypressProject, names = experimental.nam
|
||||
}
|
||||
|
||||
/**
|
||||
* Whilelist known experiments here to avoid accidentally showing
|
||||
* Allow known experiments here to avoid accidentally showing
|
||||
* any config key that starts with "experimental" prefix
|
||||
*/
|
||||
// @ts-ignore
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
import browserUtils from './browsers/utils'
|
||||
import auth from './gui/auth'
|
||||
import user from './user'
|
||||
import cohorts from './cohorts'
|
||||
import { openProject } from './open_project'
|
||||
import cache from './cache'
|
||||
import { graphqlSchema } from '@packages/graphql/src/schema'
|
||||
@@ -195,6 +196,17 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
return availableEditors
|
||||
},
|
||||
},
|
||||
cohortsApi: {
|
||||
async getCohorts () {
|
||||
return cohorts.get()
|
||||
},
|
||||
async getCohort (name: string) {
|
||||
return cohorts.getByName(name)
|
||||
},
|
||||
async insertCohort (cohort) {
|
||||
cohorts.set(cohort)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
|
||||
@@ -718,7 +718,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea
|
||||
|
||||
async function runEachSpec (spec: SpecWithRelativeRoot, index: number, length: number, estimated: number) {
|
||||
if (!options.quiet) {
|
||||
printResults.displaySpecHeader(spec.baseName, index + 1, length, estimated)
|
||||
printResults.displaySpecHeader(spec.relativeToCommonRoot, index + 1, length, estimated)
|
||||
}
|
||||
|
||||
const { results } = await runSpec(config, spec, options, estimated, isFirstSpec, index === length - 1)
|
||||
|
||||
@@ -1,33 +1,47 @@
|
||||
import type { CyCookie } from './browsers/cdp_automation'
|
||||
import type { ServerSessionData, StoredSessions } from '@packages/types'
|
||||
|
||||
interface SessionData {
|
||||
cookies: CyCookie[]
|
||||
id: string
|
||||
localStorage: object
|
||||
sessionStorage: object
|
||||
}
|
||||
const state = {
|
||||
sessions: {},
|
||||
type State = {
|
||||
globalSessions: StoredSessions
|
||||
specSessions: StoredSessions
|
||||
}
|
||||
|
||||
export function saveSession (data: SessionData) {
|
||||
const state: State = {
|
||||
globalSessions: {},
|
||||
specSessions: {},
|
||||
}
|
||||
|
||||
export function saveSession (data: ServerSessionData): void {
|
||||
if (!data.id) throw new Error('session data had no id')
|
||||
|
||||
state.sessions[data.id] = data
|
||||
if (data.cacheAcrossSpecs) {
|
||||
state.globalSessions[data.id] = data
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
state.specSessions[data.id] = data
|
||||
}
|
||||
|
||||
export function getSession (id: string): SessionData {
|
||||
const session = state.sessions[id]
|
||||
export function getActiveSessions (): StoredSessions {
|
||||
return state.globalSessions
|
||||
}
|
||||
|
||||
export function getSession (id: string): ServerSessionData {
|
||||
const session = state.globalSessions[id] || state.specSessions[id]
|
||||
|
||||
if (!session) throw new Error(`session with id "${id}" not found`)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
export function getState () {
|
||||
export function getState (): State {
|
||||
return state
|
||||
}
|
||||
|
||||
export function clearSessions () {
|
||||
state.sessions = {}
|
||||
export function clearSessions (clearAllSessions: boolean = false): void {
|
||||
state.specSessions = {}
|
||||
|
||||
if (clearAllSessions) {
|
||||
state.globalSessions = {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,22 +26,13 @@ import runEvents from './plugins/run_events'
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import type { Socket } from '@packages/socket'
|
||||
|
||||
import type { RunState, CachedTestState } from '@packages/types'
|
||||
|
||||
type StartListeningCallbacks = {
|
||||
onSocketConnection: (socket: any) => void
|
||||
}
|
||||
|
||||
type RunnerEvent =
|
||||
'reporter:restart:test:run'
|
||||
| 'runnables:ready'
|
||||
| 'run:start'
|
||||
| 'test:before:run:async'
|
||||
| 'reporter:log:add'
|
||||
| 'reporter:log:state:changed'
|
||||
| 'paused'
|
||||
| 'test:after:hooks'
|
||||
| 'run:end'
|
||||
|
||||
const runnerEvents: RunnerEvent[] = [
|
||||
const runnerEvents = [
|
||||
'reporter:restart:test:run',
|
||||
'runnables:ready',
|
||||
'run:start',
|
||||
@@ -51,18 +42,9 @@ const runnerEvents: RunnerEvent[] = [
|
||||
'paused',
|
||||
'test:after:hooks',
|
||||
'run:end',
|
||||
]
|
||||
] as const
|
||||
|
||||
type ReporterEvent =
|
||||
'runner:restart'
|
||||
| 'runner:abort'
|
||||
| 'runner:console:log'
|
||||
| 'runner:console:error'
|
||||
| 'runner:show:snapshot'
|
||||
| 'runner:hide:snapshot'
|
||||
| 'reporter:restarted'
|
||||
|
||||
const reporterEvents: ReporterEvent[] = [
|
||||
const reporterEvents = [
|
||||
// "go:to:file"
|
||||
'runner:restart',
|
||||
'runner:abort',
|
||||
@@ -71,7 +53,7 @@ const reporterEvents: ReporterEvent[] = [
|
||||
'runner:show:snapshot',
|
||||
'runner:hide:snapshot',
|
||||
'reporter:restarted',
|
||||
]
|
||||
] as const
|
||||
|
||||
const debug = Debug('cypress:server:socket-base')
|
||||
|
||||
@@ -156,7 +138,7 @@ export class SocketBase {
|
||||
options,
|
||||
callbacks: StartListeningCallbacks,
|
||||
) {
|
||||
let existingState = null
|
||||
let runState: RunState | undefined = undefined
|
||||
|
||||
_.defaults(options, {
|
||||
socketId: null,
|
||||
@@ -350,7 +332,6 @@ export class SocketBase {
|
||||
})
|
||||
|
||||
// TODO: what to do about runner disconnections?
|
||||
|
||||
socket.on('spec:changed', (spec) => {
|
||||
return options.onSpecChanged(spec)
|
||||
})
|
||||
@@ -402,8 +383,7 @@ export class SocketBase {
|
||||
})
|
||||
}
|
||||
|
||||
// retry for up to data.timeout
|
||||
// or 1 second
|
||||
// retry for up to data.timeout or 1 second
|
||||
return Bluebird
|
||||
.try(tryConnected)
|
||||
.timeout(data.timeout != null ? data.timeout : 1000)
|
||||
@@ -428,7 +408,7 @@ export class SocketBase {
|
||||
|
||||
switch (eventName) {
|
||||
case 'preserve:run:state':
|
||||
existingState = args[0]
|
||||
runState = args[0]
|
||||
|
||||
return null
|
||||
case 'resolve:url': {
|
||||
@@ -467,21 +447,20 @@ export class SocketBase {
|
||||
return task.run(cfgFile ?? null, args[0])
|
||||
case 'save:session':
|
||||
return session.saveSession(args[0])
|
||||
case 'clear:session':
|
||||
return session.clearSessions()
|
||||
case 'clear:sessions':
|
||||
return session.clearSessions(args[0])
|
||||
case 'get:session':
|
||||
return session.getSession(args[0])
|
||||
case 'reset:session:state':
|
||||
case 'reset:cached:test:state':
|
||||
runState = undefined
|
||||
cookieJar.removeAllCookies()
|
||||
session.clearSessions()
|
||||
resetRenderedHTMLOrigins()
|
||||
|
||||
return
|
||||
return resetRenderedHTMLOrigins()
|
||||
case 'get:rendered:html:origins':
|
||||
return options.getRenderedHTMLOrigins()
|
||||
case 'reset:rendered:html:origins': {
|
||||
case 'reset:rendered:html:origins':
|
||||
return resetRenderedHTMLOrigins()
|
||||
}
|
||||
case 'cross:origin:automation:cookies:received':
|
||||
return this.localBus.emit('cross:origin:automation:cookies:received')
|
||||
default:
|
||||
@@ -497,16 +476,18 @@ export class SocketBase {
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('get:existing:run:state', (cb) => {
|
||||
const s = existingState
|
||||
socket.on('get:cached:test:state', (cb: (runState: RunState | null, testState: CachedTestState) => void) => {
|
||||
const s = runState
|
||||
|
||||
if (s) {
|
||||
existingState = null
|
||||
|
||||
return cb(s)
|
||||
const cachedTestState: CachedTestState = {
|
||||
activeSessions: session.getActiveSessions(),
|
||||
}
|
||||
|
||||
return cb()
|
||||
if (s) {
|
||||
runState = undefined
|
||||
}
|
||||
|
||||
return cb(s || {}, cachedTestState)
|
||||
})
|
||||
|
||||
socket.on('save:app:state', (state, cb) => {
|
||||
@@ -547,7 +528,7 @@ export class SocketBase {
|
||||
// todo(lachlan): post 10.0 we should not pass the
|
||||
// editor (in the `fileDetails.where` key) from the
|
||||
// front-end, but rather rely on the server context
|
||||
// to grab the prefered editor, like I'm doing here,
|
||||
// to grab the preferred editor, like I'm doing here,
|
||||
// so we do not need to
|
||||
// maintain two sources of truth for the preferred editor
|
||||
// adding this conditional to maintain backwards compat with
|
||||
|
||||
@@ -113,7 +113,7 @@ function macOSRemovePrivate (str: string) {
|
||||
function collectTestResults (obj: { video?: boolean, screenshots?: Screenshot[], spec?: any, stats?: any }, estimated: number) {
|
||||
return {
|
||||
name: _.get(obj, 'spec.name'),
|
||||
baseName: _.get(obj, 'spec.baseName'),
|
||||
relativeToCommonRoot: _.get(obj, 'spec.relativeToCommonRoot'),
|
||||
tests: _.get(obj, 'stats.tests'),
|
||||
passes: _.get(obj, 'stats.passes'),
|
||||
pending: _.get(obj, 'stats.pending'),
|
||||
@@ -203,7 +203,7 @@ export function displayRunStarting (options: { browser: Browser, config: Cfg, gr
|
||||
|
||||
const formatSpecs = (specs) => {
|
||||
// 25 found: (foo.spec.js, bar.spec.js, baz.spec.js)
|
||||
const names = _.map(specs, 'baseName')
|
||||
const names = _.map(specs, 'relativeToCommonRoot')
|
||||
const specsTruncated = _.truncate(names.join(', '), { length: 250 })
|
||||
|
||||
const stringifiedSpecs = [
|
||||
@@ -325,7 +325,7 @@ export function renderSummaryTable (runUrl: string | undefined, results: any) {
|
||||
|
||||
const ms = duration.format(stats.wallClockDuration || 0)
|
||||
|
||||
const formattedSpec = formatPath(spec.baseName, getWidth(table2, 1))
|
||||
const formattedSpec = formatPath(spec.relativeToCommonRoot, getWidth(table2, 1))
|
||||
|
||||
if (run.skippedSpec) {
|
||||
return table2.push([
|
||||
@@ -398,7 +398,7 @@ export function displayResults (obj: { screenshots?: Screenshot[] }, estimated:
|
||||
['Video:', results.video],
|
||||
['Duration:', results.duration],
|
||||
estimated ? ['Estimated:', results.estimated] : undefined,
|
||||
['Spec Ran:', formatPath(results.baseName, getWidth(table, 1), c)],
|
||||
['Spec Ran:', formatPath(results.relativeToCommonRoot, getWidth(table, 1), c)],
|
||||
])
|
||||
.compact()
|
||||
.map((arr) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
|
||||
const SseStream = require('ssestream')
|
||||
const EventSource = require('eventsource')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const pluginsModule = require(`../../lib/plugins`)
|
||||
@@ -107,7 +108,7 @@ describe('Routes', () => {
|
||||
// get all the config defaults
|
||||
// and allow us to override them
|
||||
// for each test
|
||||
return config.setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
// use a jar for each test
|
||||
// but reset it automatically
|
||||
|
||||
@@ -5,6 +5,7 @@ const http = require('http')
|
||||
const rp = require('@cypress/request-promise')
|
||||
const Promise = require('bluebird')
|
||||
const evilDns = require('evil-dns')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
@@ -47,7 +48,7 @@ describe('Server', () => {
|
||||
// get all the config defaults
|
||||
// and allow us to override them
|
||||
// for each test
|
||||
return config.setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
// use a jar for each test
|
||||
// but reset it automatically
|
||||
|
||||
@@ -21,7 +21,7 @@ const { createRoutes } = require(`../../lib/routes`)
|
||||
process.env.CYPRESS_INTERNAL_ENV = 'development'
|
||||
|
||||
const CA = require('@packages/https-proxy').CA
|
||||
const Config = require('../../lib/config')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const { ServerE2E } = require('../../lib/server-e2e')
|
||||
const { SocketE2E } = require('../../lib/socket-e2e')
|
||||
const { _getArgs } = require('../../lib/browsers/chrome')
|
||||
@@ -350,7 +350,7 @@ describe('Proxy Performance', function () {
|
||||
https: { cert, key },
|
||||
}).start(HTTPS_PROXY_PORT),
|
||||
|
||||
Config.setupFullConfigWithDefaults({
|
||||
setupFullConfigWithDefaults({
|
||||
projectRoot: '/tmp/a',
|
||||
config: {
|
||||
supportFile: false,
|
||||
|
||||
@@ -256,6 +256,28 @@ describe('lib/cache', () => {
|
||||
PROJECTS: ['foo'],
|
||||
PROJECT_PREFERENCES: {},
|
||||
PROJECTS_CONFIG: {},
|
||||
COHORTS: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('cohorts', () => {
|
||||
it('should get no cohorts when empty', () => {
|
||||
return cache.getCohorts().then((cohorts) => {
|
||||
expect(cohorts).to.deep.eq({})
|
||||
})
|
||||
})
|
||||
|
||||
it('should insert a cohort', () => {
|
||||
const cohort = {
|
||||
name: 'cohort_id',
|
||||
cohort: 'A',
|
||||
}
|
||||
|
||||
return cache.insertCohort(cohort).then(() => {
|
||||
return cache.getCohorts().then((cohorts) => {
|
||||
expect(cohorts).to.deep.eq({ [cohort.name]: cohort })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
63
packages/server/test/unit/cohort_spec.ts
Normal file
63
packages/server/test/unit/cohort_spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
require('../spec_helper')
|
||||
|
||||
import { Cohort } from '@packages/types'
|
||||
import cache from '../../lib/cache'
|
||||
import cohorts from '../../lib/cohorts'
|
||||
|
||||
describe('lib/cohort', () => {
|
||||
context('.get', () => {
|
||||
it('calls cache.get', async () => {
|
||||
const cohortTest: Cohort = {
|
||||
name: 'testName',
|
||||
cohort: 'A',
|
||||
}
|
||||
const cohortTest2: Cohort = {
|
||||
name: 'testName2',
|
||||
cohort: 'B',
|
||||
}
|
||||
|
||||
const allCohorts = {
|
||||
[cohortTest.name]: cohortTest,
|
||||
[cohortTest2.name]: cohortTest2,
|
||||
}
|
||||
|
||||
sinon.stub(cache, 'getCohorts').resolves(allCohorts)
|
||||
|
||||
return cohorts.get().then((cohorts) => {
|
||||
expect(cohorts).to.eq(allCohorts)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.getByName', () => {
|
||||
it('calls cache.getByName', async () => {
|
||||
const cohortTest: Cohort = {
|
||||
name: 'testName',
|
||||
cohort: 'A',
|
||||
}
|
||||
|
||||
sinon.stub(cache, 'getCohorts').resolves({
|
||||
[cohortTest.name]: cohortTest,
|
||||
})
|
||||
|
||||
return cohorts.getByName(cohortTest.name).then((cohort) => {
|
||||
expect(cohort).to.eq(cohortTest)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.set', () => {
|
||||
it('calls cache.set', async () => {
|
||||
const cohortTest: Cohort = {
|
||||
name: 'testName',
|
||||
cohort: 'A',
|
||||
}
|
||||
|
||||
return cohorts.set(cohortTest).then(() => {
|
||||
return cohorts.getByName(cohortTest.name).then((cohort) => {
|
||||
expect(cohort).to.eq(cohortTest)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ require('../spec_helper')
|
||||
const path = require('path')
|
||||
const chokidar = require('chokidar')
|
||||
const pkg = require('@packages/root')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const Fixtures = require('@tooling/system-tests')
|
||||
const { sinon } = require('../spec_helper')
|
||||
const user = require(`../../lib/user`)
|
||||
@@ -44,7 +45,7 @@ describe.skip('lib/project-base', () => {
|
||||
.then((obj = {}) => {
|
||||
({ projectId: this.projectId } = obj)
|
||||
|
||||
return config.setupFullConfigWithDefaults({ projectName: 'project', projectRoot: '/foo/bar' }, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults({ projectName: 'project', projectRoot: '/foo/bar' }, getCtx().file.getFilesByGlob)
|
||||
.then((config1) => {
|
||||
this.config = config1
|
||||
this.project = new ProjectBase({ projectRoot: this.todosPath, testingType: 'e2e' })
|
||||
|
||||
@@ -5,7 +5,7 @@ const os = require('os')
|
||||
const express = require('express')
|
||||
const Promise = require('bluebird')
|
||||
const { connect } = require('@packages/network')
|
||||
const config = require(`../../lib/config`)
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const fileServer = require(`../../lib/file_server`)
|
||||
@@ -22,7 +22,7 @@ describe('lib/server', () => {
|
||||
beforeEach(function () {
|
||||
this.server = new ServerE2E()
|
||||
|
||||
return config.setupFullConfigWithDefaults({ projectRoot: '/foo/bar/', config: { supportFile: false } }, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults({ projectRoot: '/foo/bar/', config: { supportFile: false } }, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
this.config = cfg
|
||||
})
|
||||
@@ -38,7 +38,7 @@ describe('lib/server', () => {
|
||||
})
|
||||
|
||||
// TODO: Figure out correct configuration to run these tests and/or which ones we need to keep.
|
||||
// The introducion of server-base/socket-base and the `ensureProp` function made unit testing
|
||||
// The introduction of server-base/socket-base and the `ensureProp` function made unit testing
|
||||
// the server difficult.
|
||||
describe.skip('lib/server', () => {
|
||||
beforeEach(function () {
|
||||
@@ -51,7 +51,7 @@ describe.skip('lib/server', () => {
|
||||
|
||||
sinon.stub(fileServer, 'create').returns(this.fileServer)
|
||||
|
||||
return config.setupFullConfigWithDefaults({ projectRoot: '/foo/bar/' }, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults({ projectRoot: '/foo/bar/' }, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
this.config = cfg
|
||||
this.server = new ServerE2E()
|
||||
|
||||
@@ -3,21 +3,22 @@ require('../spec_helper')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const Promise = require('bluebird')
|
||||
const socketIo = require('@packages/socket/lib/browser')
|
||||
const httpsAgent = require('https-proxy-agent')
|
||||
|
||||
const errors = require(`../../lib/errors`)
|
||||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { Automation } = require(`../../lib/automation`)
|
||||
const exec = require(`../../lib/exec`)
|
||||
const preprocessor = require(`../../lib/plugins/preprocessor`)
|
||||
const { fs } = require(`../../lib/util/fs`)
|
||||
|
||||
const socketIo = require('@packages/socket/lib/browser')
|
||||
const Fixtures = require('@tooling/system-tests')
|
||||
const firefoxUtil = require(`../../lib/browsers/firefox-util`).default
|
||||
const { createRoutes } = require(`../../lib/routes`)
|
||||
const { getCtx } = require(`../../lib/makeDataContext`)
|
||||
|
||||
const errors = require('../../lib/errors')
|
||||
const { SocketE2E } = require('../../lib/socket-e2e')
|
||||
const { ServerE2E } = require('../../lib/server-e2e')
|
||||
const { Automation } = require('../../lib/automation')
|
||||
const exec = require('../../lib/exec')
|
||||
const preprocessor = require('../../lib/plugins/preprocessor')
|
||||
const { fs } = require('../../lib/util/fs')
|
||||
const session = require('../../lib/session')
|
||||
|
||||
const firefoxUtil = require('../../lib/browsers/firefox-util').default
|
||||
const { createRoutes } = require('../../lib/routes')
|
||||
const { getCtx } = require('../../lib/makeDataContext')
|
||||
const { sinon } = require('../spec_helper')
|
||||
|
||||
let ctx
|
||||
@@ -33,6 +34,7 @@ describe('lib/socket', () => {
|
||||
sinon.stub(ctx.actions.project, 'initializeActiveProject')
|
||||
|
||||
Fixtures.scaffold()
|
||||
session.clearSessions(true)
|
||||
|
||||
this.todosPath = Fixtures.projectPath('todos')
|
||||
|
||||
@@ -453,7 +455,7 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(get:fixture)', () => {
|
||||
context('on(backend:request, get:fixture)', () => {
|
||||
it('returns the fixture object', function (done) {
|
||||
const cb = function (resp) {
|
||||
expect(resp.response).to.deep.eq([
|
||||
@@ -488,7 +490,7 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(http:request)', () => {
|
||||
context('on(backend:request, http:request)', () => {
|
||||
it('calls socket#onRequest', function (done) {
|
||||
sinon.stub(this.options, 'onRequest').resolves({ foo: 'bar' })
|
||||
|
||||
@@ -512,7 +514,7 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(exec)', () => {
|
||||
context('on(backend:request, exec)', () => {
|
||||
it('calls exec#run with project root and options', function (done) {
|
||||
const run = sinon.stub(exec, 'run').returns(Promise.resolve('Desktop Music Pictures'))
|
||||
|
||||
@@ -539,7 +541,7 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(firefox:force:gc)', () => {
|
||||
context('on(backend:request, firefox:force:gc)', () => {
|
||||
it('calls firefoxUtil#collectGarbage', function (done) {
|
||||
sinon.stub(firefoxUtil, 'collectGarbage').resolves()
|
||||
|
||||
@@ -598,6 +600,180 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, save:session)', () => {
|
||||
it('saves spec sessions', function (done) {
|
||||
const sessionData = {
|
||||
id: 'spec',
|
||||
cacheAcrossSpecs: false,
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'save:session', sessionData, () => {
|
||||
const state = session.getState()
|
||||
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {},
|
||||
specSessions: {
|
||||
'spec': sessionData,
|
||||
},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('saves global sessions', function (done) {
|
||||
const sessionData = {
|
||||
id: 'global',
|
||||
cacheAcrossSpecs: true,
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'save:session', sessionData, () => {
|
||||
const state = session.getState()
|
||||
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {
|
||||
'global': sessionData,
|
||||
},
|
||||
specSessions: {},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if session data has no id', function (done) {
|
||||
const sessionData = {}
|
||||
|
||||
this.client.emit('backend:request', 'save:session', sessionData, ({ error }) => {
|
||||
expect(error.message).to.eq('session data had no id')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, clear:sessions)', () => {
|
||||
it('clears spec sessions', function (done) {
|
||||
let state = session.getState()
|
||||
|
||||
state.globalSessions = {
|
||||
global: { id: 'global' },
|
||||
}
|
||||
|
||||
state.specSessions = {
|
||||
spec: { id: 'spec' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'clear:sessions', false, () => {
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {
|
||||
'global': { id: 'global' },
|
||||
},
|
||||
specSessions: {},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('clears all sessions', function (done) {
|
||||
let state = session.getState()
|
||||
|
||||
state.globalSessions = {
|
||||
global: { id: 'global' },
|
||||
}
|
||||
|
||||
state.specSessions = {
|
||||
spec: { id: 'spec' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'clear:sessions', true, () => {
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {},
|
||||
specSessions: {},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, get:session)', () => {
|
||||
it('returns global session', function (done) {
|
||||
const state = session.getState()
|
||||
|
||||
state.globalSessions = {
|
||||
global: { id: 'global' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'get:session', 'global', ({ response, error }) => {
|
||||
expect(error).to.be.undefined
|
||||
expect(response).deep.eq({
|
||||
id: 'global',
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns spec session', function (done) {
|
||||
const state = session.getState()
|
||||
|
||||
state.globalSessions = {}
|
||||
state.specSessions = {
|
||||
'spec': { id: 'spec' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'get:session', 'spec', ({ response, error }) => {
|
||||
expect(error).to.be.undefined
|
||||
expect(response).deep.eq({
|
||||
id: 'spec',
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error when session does not exist', function (done) {
|
||||
const state = session.getState()
|
||||
|
||||
state.globalSessions = {}
|
||||
state.specSessions = {}
|
||||
this.client.emit('backend:request', 'get:session', 1, ({ response, error }) => {
|
||||
expect(response).to.be.undefined
|
||||
expect(error.message).to.eq('session with id "1" not found')
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, reset:cached:test:state)', () => {
|
||||
it('clears spec sessions', function (done) {
|
||||
const state = session.getState()
|
||||
|
||||
state.globalSessions = {
|
||||
global: { id: 'global' },
|
||||
}
|
||||
|
||||
state.specSessions = {
|
||||
local: { id: 'local' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'reset:cached:test:state', ({ error }) => {
|
||||
expect(error).to.be.undefined
|
||||
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {
|
||||
'global': { id: 'global' },
|
||||
},
|
||||
specSessions: {},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('unit', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface Cache {
|
||||
PROJECTS: string[]
|
||||
PROJECT_PREFERENCES: Record<string, Preferences>
|
||||
USER: CachedUser
|
||||
COHORTS: Record<string, Cohort>
|
||||
}
|
||||
|
||||
import type { AllowedState } from './preferences'
|
||||
@@ -13,3 +14,8 @@ export interface CachedUser {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface Cohort {
|
||||
name: string
|
||||
cohort: string
|
||||
}
|
||||
|
||||
@@ -1,19 +1,46 @@
|
||||
export interface RunState {
|
||||
import type { ReporterRunState, StudioRecorderState } from './reporter'
|
||||
|
||||
interface MochaRunnerState {
|
||||
startTime?: number
|
||||
currentId?: number
|
||||
currentId?: number | null
|
||||
emissions?: Emissions
|
||||
tests?: unknown
|
||||
tests?: Record<string, Cypress.ObjectLike>
|
||||
passed?: number
|
||||
failed?: number
|
||||
pending?: number
|
||||
numLogs?: number
|
||||
}
|
||||
|
||||
export type RunState = MochaRunnerState & ReporterRunState & {
|
||||
studio?: StudioRecorderState
|
||||
isSpecsListOpen?: boolean
|
||||
}
|
||||
|
||||
export interface Emissions {
|
||||
started: Record<string, boolean>
|
||||
ended: Record<string, boolean>
|
||||
}
|
||||
|
||||
interface HtmlWebStorage {
|
||||
origin: string
|
||||
value: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ServerSessionData {
|
||||
id: string
|
||||
cacheAcrossSpecs: boolean
|
||||
cookies: Cypress.Cookie[] | null
|
||||
localStorage: Array<HtmlWebStorage> | null
|
||||
sessionStorage: Array<HtmlWebStorage> | null
|
||||
setup: string
|
||||
}
|
||||
|
||||
export type StoredSessions = Record<string, ServerSessionData>
|
||||
|
||||
export interface CachedTestState {
|
||||
activeSessions: StoredSessions
|
||||
}
|
||||
|
||||
export type Instrument = 'agent' | 'command' | 'route'
|
||||
|
||||
export type TestState = 'active' | 'failed' | 'pending' | 'passed' | 'processing'
|
||||
|
||||
@@ -28,6 +28,8 @@ export {
|
||||
RESOLVED_FROM,
|
||||
} from './config'
|
||||
|
||||
export * from './reporter'
|
||||
|
||||
export * from './server'
|
||||
|
||||
export * from './util'
|
||||
|
||||
@@ -25,10 +25,7 @@ export const allowedKeys: Readonly<Array<keyof AllowedState>> = [
|
||||
'firstOpenedCypress',
|
||||
'showedStudioModal',
|
||||
'preferredOpener',
|
||||
'ctReporterWidth',
|
||||
'ctIsSpecsListOpen',
|
||||
'isSpecsListOpen',
|
||||
'ctSpecListWidth',
|
||||
'firstOpened',
|
||||
'lastOpened',
|
||||
'lastProjectId',
|
||||
@@ -61,9 +58,6 @@ export type AllowedState = Partial<{
|
||||
firstOpenedCypress: Maybe<number>
|
||||
showedStudioModal: Maybe<boolean>
|
||||
preferredOpener: Editor | undefined
|
||||
ctReporterWidth: Maybe<number>
|
||||
ctIsSpecsListOpen: Maybe<boolean>
|
||||
ctSpecListWidth: Maybe<number>
|
||||
lastProjectId: Maybe<string>
|
||||
firstOpened: Maybe<number>
|
||||
lastOpened: Maybe<number>
|
||||
|
||||
24
packages/types/src/reporter.ts
Normal file
24
packages/types/src/reporter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface StudioRecorderState {
|
||||
suiteId?: string
|
||||
testId?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface ReporterRunState {
|
||||
autoScrollingEnabled?: boolean
|
||||
scrollTop?: number
|
||||
}
|
||||
|
||||
export interface StatsStoreStartInfo {
|
||||
startTime: string
|
||||
numPassed?: number
|
||||
numFailed?: number
|
||||
numPending?: number
|
||||
}
|
||||
|
||||
export interface ReporterStartInfo extends StatsStoreStartInfo {
|
||||
isSpecsListOpen: boolean
|
||||
autoScrollingEnabled: boolean
|
||||
scrollTop: number
|
||||
studioActive: boolean
|
||||
}
|
||||
@@ -275,15 +275,15 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Cypress: 1.2.3 │
|
||||
│ Browser: FooBrowser 88 │
|
||||
│ Specs: 3 found (spec.cy.js, stdout_specfile.cy.js, stdout_specfile_display_spec_with_a_re │
|
||||
│ ally_long_name_that_never_has_a_line_break_or_new_line.cy.js) │
|
||||
│ Searched: cypress/e2e/nested-1/nested-2/nested-3/* │
|
||||
│ Specs: 4 found (spec.cy.js, stdout_specfile.cy.js, stdout_specfile_display_spec_with_a_re │
|
||||
│ ally_long_name_that_never_has_a_line_break_or_new_line.cy.js, nested-4/spec.cy.js) │
|
||||
│ Searched: cypress/e2e/nested-1/nested-2/nested-3/**/* │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: spec.cy.js (1 of 3)
|
||||
Running: spec.cy.js (1 of 4)
|
||||
|
||||
|
||||
stdout_specfile_display_spec
|
||||
@@ -316,7 +316,7 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: stdout_specfile.cy.js (2 of 3)
|
||||
Running: stdout_specfile.cy.js (2 of 4)
|
||||
|
||||
|
||||
stdout_specfile_display_spec
|
||||
@@ -349,7 +349,7 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: stdout_specfile_display_spec_with_a_really_long_name_that_never_has_ (3 of 3)
|
||||
Running: stdout_specfile_display_spec_with_a_really_long_name_that_never_has_ (3 of 4)
|
||||
a_line_break_or_new_line.cy.js
|
||||
|
||||
|
||||
@@ -391,6 +391,39 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
ne.cy.js.mp4
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: nested-4/spec.cy.js (4 of 4)
|
||||
|
||||
|
||||
stdout_specfile_display_spec
|
||||
✓ passes
|
||||
|
||||
|
||||
1 passing
|
||||
|
||||
|
||||
(Results)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tests: 1 │
|
||||
│ Passing: 1 │
|
||||
│ Failing: 0 │
|
||||
│ Pending: 0 │
|
||||
│ Skipped: 0 │
|
||||
│ Screenshots: 0 │
|
||||
│ Video: true │
|
||||
│ Duration: X seconds │
|
||||
│ Spec Ran: nested-4/spec.cy.js │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
(Video)
|
||||
|
||||
- Started processing: Compressing to 32 CRF
|
||||
- Finished processing: /XXX/XXX/XXX/cypress/videos/nested-4/spec.cy.js.mp4 (X second)
|
||||
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Finished)
|
||||
@@ -405,8 +438,10 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
│ ✔ stdout_specfile_display_spec_with_a XX:XX 1 1 - - - │
|
||||
│ _really_long_name_that_never_has_a_ │
|
||||
│ line_break_or_new_line.cy.js │
|
||||
├────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✔ nested-4/spec.cy.js XX:XX 1 1 - - - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
✔ All specs passed! XX:XX 3 3 - - -
|
||||
✔ All specs passed! XX:XX 4 4 - - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { AppComponent } from './app.component'
|
||||
|
||||
const ExcludedTestTitle = 'should not exist'
|
||||
|
||||
// Validating Mocha syntax and behavior of *.only is still valid after being patched by `zone.js/testing`
|
||||
// Github Issue: https://github.com/cypress-io/cypress/issues/23409
|
||||
describe('only', () => {
|
||||
context.only('01 - executions', () => {
|
||||
describe('suite', () => {
|
||||
suite.only('should exist on "suite"', () => {
|
||||
it('succeeds', () => {})
|
||||
})
|
||||
|
||||
it(ExcludedTestTitle, () => {})
|
||||
})
|
||||
|
||||
describe('describe', () => {
|
||||
describe.only('should exist on "describe"', () => {
|
||||
it('succeeds', () => {})
|
||||
})
|
||||
|
||||
it(ExcludedTestTitle, () => {})
|
||||
})
|
||||
|
||||
describe('context', () => {
|
||||
context.only('should exist on "context"', () => {
|
||||
it('succeeds', () => {})
|
||||
})
|
||||
|
||||
it(ExcludedTestTitle, () => {})
|
||||
})
|
||||
|
||||
describe('specify', () => {
|
||||
specify.only('should exist on "specify"', () => {})
|
||||
it(ExcludedTestTitle, () => {})
|
||||
})
|
||||
|
||||
describe('test', () => {
|
||||
test.only('should exist on "test"', () => {})
|
||||
it(ExcludedTestTitle, () => {})
|
||||
})
|
||||
|
||||
describe('it', () => {
|
||||
it.only('should exist on "it"', () => {})
|
||||
it(ExcludedTestTitle, () => {})
|
||||
})
|
||||
})
|
||||
|
||||
context.only('02 - validations', () => {
|
||||
const verifyNotPresent = (title: string) => {
|
||||
cy.wrap(Cypress.$(window.top!.document.body)).within(() =>
|
||||
cy
|
||||
.contains(title)
|
||||
.should('not.exist')
|
||||
)
|
||||
}
|
||||
|
||||
describe('suite', () => {
|
||||
it('should not include other test', () => {
|
||||
verifyNotPresent(ExcludedTestTitle)
|
||||
})
|
||||
})
|
||||
|
||||
describe('describe', () => {
|
||||
it('should not include other test', () => {
|
||||
verifyNotPresent(ExcludedTestTitle)
|
||||
})
|
||||
})
|
||||
|
||||
describe('context', () => {
|
||||
it('should not include other test', () => {
|
||||
verifyNotPresent(ExcludedTestTitle)
|
||||
})
|
||||
})
|
||||
|
||||
describe('specify', () => {
|
||||
it('should not include other test', () => {
|
||||
verifyNotPresent(ExcludedTestTitle)
|
||||
})
|
||||
})
|
||||
|
||||
describe('test', () => {
|
||||
it('should not include other test', () => {
|
||||
verifyNotPresent(ExcludedTestTitle)
|
||||
})
|
||||
})
|
||||
|
||||
describe('it', () => {
|
||||
it('should not include other test', () => {
|
||||
verifyNotPresent(ExcludedTestTitle)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('empty passing test', () => {})
|
||||
@@ -0,0 +1,86 @@
|
||||
import { AppComponent } from './app.component'
|
||||
|
||||
// Validating Mocha syntax and behavior of *.skip is still valid after being patched by `zone.js/testing`
|
||||
// Github Issue: https://github.com/cypress-io/cypress/issues/23409
|
||||
describe('skip', () => {
|
||||
context('01 - executions', () => {
|
||||
describe('suite', () => {
|
||||
suite.skip('should exist on "suite"', () => {
|
||||
it('skipped', () => {})
|
||||
})
|
||||
})
|
||||
|
||||
describe('describe', () => {
|
||||
describe.skip('should exist on "describe"', () => {
|
||||
it('skipped', () => {})
|
||||
})
|
||||
})
|
||||
|
||||
describe('context', () => {
|
||||
context.skip('should exist on "context"', () => {
|
||||
it('skipped', () => {})
|
||||
})
|
||||
})
|
||||
|
||||
describe('specify', () => {
|
||||
specify.skip('should exist on "specify"', () => {})
|
||||
})
|
||||
|
||||
describe('test', () => {
|
||||
test.skip('should exist on "test"', () => {})
|
||||
})
|
||||
|
||||
describe('it', () => {
|
||||
it.skip('should exist on "it"', () => {})
|
||||
})
|
||||
})
|
||||
|
||||
context('02 - validations', () => {
|
||||
const verifyWasSkipped = (title: string) => {
|
||||
cy.wrap(Cypress.$(window.top!.document.body)).within(() =>
|
||||
cy
|
||||
.contains(title)
|
||||
.parents('[data-model-state="pending"]') // Find parent row with class indicating test was skipped
|
||||
.should('be.visible')
|
||||
)
|
||||
}
|
||||
|
||||
describe('suite', () => {
|
||||
it('should have been skipped', () => {
|
||||
verifyWasSkipped('should exist on "suite"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('describe', () => {
|
||||
it('should have been skipped', () => {
|
||||
verifyWasSkipped('should exist on "describe"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('context', () => {
|
||||
it('should have been skipped', () => {
|
||||
verifyWasSkipped('should exist on "context"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('specify', () => {
|
||||
it('should have been skipped', () => {
|
||||
verifyWasSkipped('should exist on "specify"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('test', () => {
|
||||
it('should have been skipped', () => {
|
||||
verifyWasSkipped('should exist on "test"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('it', () => {
|
||||
it('should have been skipped', () => {
|
||||
verifyWasSkipped('should exist on "it"')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('empty passing test', () => {})
|
||||
@@ -0,0 +1,3 @@
|
||||
describe('stdout_specfile_display_spec', () => {
|
||||
it('passes', () => {})
|
||||
})
|
||||
@@ -113,7 +113,11 @@ describe(`Angular CLI major versions`, () => {
|
||||
systemTests.setup()
|
||||
|
||||
for (const majorVersion of ANGULAR_MAJOR_VERSIONS) {
|
||||
const spec = `${majorVersion === '14' ? 'src/app/components/standalone.component.cy.ts,src/app/mount.cy.ts' : 'src/app/mount.cy.ts'}`
|
||||
let spec = 'src/**/*.cy.ts'
|
||||
|
||||
if (majorVersion === '13') {
|
||||
spec = `${spec},!src/app/components/standalone.component.cy.ts`
|
||||
}
|
||||
|
||||
systemTests.it(`v${majorVersion} with mount tests`, {
|
||||
project: `angular-${majorVersion}`,
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('e2e stdout', () => {
|
||||
return systemTests.exec(this, {
|
||||
port: 2020,
|
||||
snapshot: true,
|
||||
spec: 'nested-1/nested-2/nested-3/*',
|
||||
spec: 'nested-1/nested-2/nested-3/**/*',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user