diff --git a/graphql-codegen.yml b/graphql-codegen.yml index c540da7d56..496238eb8d 100644 --- a/graphql-codegen.yml +++ b/graphql-codegen.yml @@ -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. diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index bd449e52ac..7f8d378631 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -94,6 +94,11 @@ export type MountResponse = { 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 * diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts index a3f0545489..9a79cb4aec 100644 --- a/packages/app/cypress/e2e/runs.cy.ts +++ b/packages/app/cypress/e2e/runs.cy.ts @@ -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') }) }) }) diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 35bf46468e..a07e8dab98 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -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 diff --git a/packages/app/src/runs/RunsEmpty.vue b/packages/app/src/runs/RunsEmpty.vue index 3180797921..617fa8a25c 100644 --- a/packages/app/src/runs/RunsEmpty.vue +++ b/packages/app/src/runs/RunsEmpty.vue @@ -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}` }) diff --git a/packages/app/src/specs/SpecsListBanners.cy.tsx b/packages/app/src/specs/SpecsListBanners.cy.tsx index 0cc1da8131..deb7fb3ce1 100644 --- a/packages/app/src/specs/SpecsListBanners.cy.tsx +++ b/packages/app/src/specs/SpecsListBanners.cy.tsx @@ -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('', () => { }) 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') diff --git a/packages/app/src/specs/SpecsListBanners.vue b/packages/app/src/specs/SpecsListBanners.vue index 71e60af141..bade2d118e 100644 --- a/packages/app/src/specs/SpecsListBanners.vue +++ b/packages/app/src/specs/SpecsListBanners.vue @@ -116,19 +116,22 @@ :has-banner-been-shown="hasRecordBannerBeenShown" /> @@ -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>> = {} + +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 diff --git a/packages/app/src/specs/banners/ConnectProjectBanner.cy.tsx b/packages/app/src/specs/banners/ConnectProjectBanner.cy.tsx index 0fc078252a..497b256eed 100644 --- a/packages/app/src/specs/banners/ConnectProjectBanner.cy.tsx +++ b/packages/app/src/specs/banners/ConnectProjectBanner.cy.tsx @@ -3,32 +3,44 @@ import ConnectProjectBanner from './ConnectProjectBanner.vue' import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql' describe('', () => { + const cohortOption = { cohort: 'A', value: defaultMessages.specPage.banners.connectProject.contentA } + it('should render expected content', () => { - cy.mount({ render: () => }) + cy.mount({ render: () => }) 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: () => }) + it('should record expected event on mount', () => { + cy.mount({ render: () => }) - 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: () => }) + + cy.get('@recordEvent').should('not.have.been.called') }) }) }) diff --git a/packages/app/src/specs/banners/ConnectProjectBanner.vue b/packages/app/src/specs/banners/ConnectProjectBanner.vue index 97315fd732..efdb693a0f 100644 --- a/packages/app/src/specs/banners/ConnectProjectBanner.vue +++ b/packages/app/src/specs/banners/ConnectProjectBanner.vue @@ -1,6 +1,6 @@