fix: login/utm param bug fixes and test coverage (#23787)

This commit is contained in:
Mark Noonan
2022-09-12 16:38:37 -04:00
committed by GitHub
parent 7c6c231322
commit 282e6c07b3
19 changed files with 153 additions and 33 deletions
+25 -5
View File
@@ -75,9 +75,20 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
it('clicking the login button will open the login modal', () => {
cy.visitApp()
moveToRunsPage()
cy.contains('Log In').click()
cy.contains(defaultMessages.runs.connect.buttonUser).click()
cy.withCtx((ctx, o) => {
o.sinon.spy(ctx._apis.authApi, 'logIn')
})
cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => {
cy.get('button').contains('Log In')
cy.contains('button', 'Log In').click()
})
cy.withCtx((ctx, o) => {
// validate utmSource
expect((ctx._apis.authApi.logIn as SinonStub).lastCall.args[1]).to.eq('Binary: App')
// validate utmMedium
expect((ctx._apis.authApi.logIn as SinonStub).lastCall.args[2]).to.eq('Runs Tab')
})
})
@@ -239,6 +250,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
moveToRunsPage()
cy.withCtx(async (ctx, options) => {
ctx.coreData.app.browserStatus = 'open'
options.sinon.stub(ctx._apis.electronApi, 'isMainWindowFocused').returns(false)
options.sinon.stub(ctx._apis.authApi, 'logIn').callsFake(async (onMessage) => {
setTimeout(() => {
@@ -272,8 +284,8 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
})
context('Runs - Create Project', () => {
it('when a project is created, injects new projectId into the config file', () => {
cy.remoteGraphQLIntercept(async (obj) => {
it('when a project is created, injects new projectId into the config file, and sends expected UTM params', () => {
cy.remoteGraphQLIntercept((obj) => {
if (obj.operationName === 'SelectCloudProjectModal_CreateCloudProject_cloudProjectCreate') {
obj.result.data!.cloudProjectCreate = {
slug: 'newProjectId',
@@ -290,7 +302,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
cy.loginUser()
cy.visitApp()
cy.withCtx(async (ctx) => {
cy.withCtx(async (ctx, o) => {
o.sinon.spy(ctx.cloud, 'executeRemoteGraphQL')
const config = await ctx.project.getConfig()
expect(config.projectId).to.not.equal('newProjectId')
@@ -305,6 +319,12 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
const config = await ctx.project.getConfig()
expect(config.projectId).to.equal('newProjectId')
expect(ctx.cloud.executeRemoteGraphQL).to.have.been.calledWithMatch({
fieldName: 'cloudProjectCreate',
operationVariables: {
medium: 'Runs Tab',
source: 'Binary: App',
} })
})
})
+20 -11
View File
@@ -16,17 +16,12 @@ describe('App: Settings', () => {
cy.visitApp()
cy.get(SidebarSettingsLinkSelector).click()
cy.get('div[data-cy="app-header-bar"]').should('contain', 'Settings')
cy.contains('[data-cy="app-header-bar"]', 'Settings')
cy.contains('[data-cy="app-header-bar"] button', 'Log In').should('be.visible')
cy.findByText('Device Settings').should('be.visible')
cy.findByText('Project Settings').should('be.visible')
})
it('shows a button to log in if user is not connected', () => {
cy.startAppServer('e2e')
cy.visitApp()
cy.get(SidebarSettingsLinkSelector).click()
cy.findByText('Project Settings').click()
cy.get('button').contains('Log In')
cy.findByText('Dashboard Settings').should('be.visible')
})
describe('Cloud Settings', () => {
@@ -406,7 +401,7 @@ describe('App: Settings', () => {
})
describe('App: Settings without cloud', () => {
it('the projectId section shows a prompt to connect when there is no projectId', () => {
it('the projectId section shows a prompt to log in when there is no projectId, and uses correct UTM params', () => {
cy.scaffoldProject('simple-ct')
cy.openProject('simple-ct')
cy.startAppServer('component')
@@ -415,7 +410,21 @@ describe('App: Settings without cloud', () => {
cy.get(SidebarSettingsLinkSelector).click()
cy.findByText('Dashboard Settings').click()
cy.findByText('Project ID').should('exist')
cy.contains('button', 'Log in to the Cypress Dashboard').should('be.visible')
cy.withCtx((ctx, o) => {
o.sinon.spy(ctx._apis.authApi, 'logIn')
})
cy.contains('button', 'Log in to the Cypress Dashboard').click()
cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => {
cy.contains('button', 'Log In').click()
})
cy.withCtx((ctx, o) => {
// validate utmSource
expect((ctx._apis.authApi.logIn as SinonStub).lastCall.args[1]).to.eq('Binary: App')
// validate utmMedium
expect((ctx._apis.authApi.logIn as SinonStub).lastCall.args[2]).to.eq('Settings Tab')
})
})
it('have returned browsers', () => {
@@ -17,6 +17,37 @@ function averageDurationSelector (specFileName: string) {
return `${specRowSelector(specFileName)} [data-cy="average-duration"]`
}
function makeTestingCloudLink (status: string) {
return `https://google.com?utm_medium=Specs+Latest+Runs+Dots&utm_campaign=${status.toUpperCase()}&utm_source=Binary%3A+App`
}
function assertCorrectRunsLink (specFileName: string, status: string) {
// we avoid the full `cy.validateExternalLink` here because that command
// clicks the link, which focuses the link causing tooltips to appear,
// which produces problems elsewhere testing tooltip behavior
cy.findByRole('link', { name: specFileName })
.should('have.attr', 'href', makeTestingCloudLink(status))
.should('have.attr', 'data-cy', 'external') // to confirm the ExternalLink component is used
}
function validateTooltip (status: string) {
cy.validateExternalLink({
// TODO: (#23778) This name is so long because the entire tooltip is wrapped in a link,
// we can make this more accessible by having the name of the link describe the destination
// (which is currently not described) and keeping the other content separate.
name: `accounts_new.spec.js ${status} 4 months ago 2:23 - 2:39 skipped pending passed failed`,
// the main thing about testing this link is that is gets composed with the expected UTM params
href: makeTestingCloudLink(status),
})
.should('contain.text', 'accounts_new.spec.js')
.and('contain.text', '4 months ago')
.and('contain.text', '2:23 - 2:39')
.and('contain.text', 'skipped 0')
.and('contain.text', 'pending 1-2')
.and('contain.text', `passed 22-23`)
.and('contain.text', 'failed 1-2')
}
function specShouldShow (specFileName: string, runDotsClasses: string[], latestRunStatus: CloudRunStatus|'PLACEHOLDER') {
const latestStatusSpinning = latestRunStatus === 'RUNNING'
@@ -31,10 +62,11 @@ function specShouldShow (specFileName: string, runDotsClasses: string[], latestR
.should(`${latestStatusSpinning ? '' : 'not.'}have.class`, 'animate-spin')
.and('have.attr', 'data-cy-run-status', latestRunStatus)
// TODO: add link verification
// if (latestRunStatus !== 'PLACEHOLDER') {
// cy.get(`${specRowSelector(specFileName)} [data-cy="run-status-dots"]`).validateExternalLink('https://google.com')
// }
if (runDotsClasses?.length) {
assertCorrectRunsLink(`${specFileName} test results`, latestRunStatus)
} else {
cy.findByRole('link', { name: `${specFileName} test results` }).should('not.exist')
}
}
function simulateRunData () {
@@ -330,7 +362,9 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
specShouldShow('accounts_new.spec.js', ['gray-300', 'gray-300', 'jade-400'], 'RUNNING')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter')
cy.get('.v-popper__popper--shown').should('exist')
// TODO: verify the contents of the tooltip
validateTooltip('Running')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave')
cy.get(averageDurationSelector('accounts_new.spec.js')).contains('2:03')
})
@@ -601,7 +635,8 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter')
cy.get('.v-popper__popper--shown').should('exist')
// TODO: verify the contents of the tooltip
validateTooltip('Passed')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave')
cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12')
@@ -611,7 +646,8 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter')
cy.get('.v-popper__popper--shown').should('exist')
// TODO: verify the contents of the tooltip
validateTooltip('Passed')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave')
cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12')
})
+8
View File
@@ -407,6 +407,7 @@ describe('App Top Nav Workflows', () => {
const mockLogInActionsForUser = (user) => {
cy.withCtx(async (ctx, options) => {
ctx.coreData.app.browserStatus = 'open'
options.sinon.stub(ctx._apis.electronApi, 'isMainWindowFocused').returns(false)
options.sinon.stub(ctx._apis.authApi, 'logIn').callsFake(async (onMessage) => {
setTimeout(() => {
@@ -455,6 +456,13 @@ describe('App Top Nav Workflows', () => {
mockLogInActionsForUser(mockUser)
logIn({ expectedNextStepText: 'Connect project', displayName: mockUser.name })
cy.withCtx((ctx, o) => {
// validate utmSource
expect((ctx._apis.authApi.logIn as SinonStub).lastCall.args[1]).to.eq('Binary: App')
// validate utmMedium
expect((ctx._apis.authApi.logIn as SinonStub).lastCall.args[2]).to.eq('Nav')
})
cy.findByRole('dialog', { name: 'Create project' }).should('be.visible')
})
})
+3
View File
@@ -49,10 +49,13 @@
<component :is="Component" />
</transition>
</router-view>
<!-- "Nav" is the correct UTM medium below because
this is only opened by event emitted from the header bar-->
<CloudConnectModals
v-if="showConnectDialog && cloudModalQuery.data.value"
:show="showConnectDialog"
:gql="cloudModalQuery.data.value"
utm-medium="Nav"
@cancel="showConnectDialog = false"
@success="showConnectDialog = false"
/>
@@ -9,7 +9,7 @@ describe('<CloudConnectButton />', () => {
result.cloudViewer = null
},
render (gqlVal) {
return <div class="h-screen"><CloudConnectButton gql={gqlVal} /></div>
return <div class="h-screen"><CloudConnectButton utmMedium="testing" gql={gqlVal} /></div>
},
})
@@ -56,7 +56,7 @@ describe('<CloudConnectButton />', () => {
result.cloudViewer = cloudViewer
},
render (gqlVal) {
return <div class="h-screen"><CloudConnectButton gql={gqlVal} /></div>
return <div class="h-screen"><CloudConnectButton utmMedium="testing" gql={gqlVal} /></div>
},
})
@@ -69,7 +69,7 @@ describe('<CloudConnectButton />', () => {
result.cloudViewer = cloudViewer
},
render (gqlVal) {
return <div class="h-screen"><CloudConnectButton gql={gqlVal} /></div>
return <div class="h-screen"><CloudConnectButton utmMedium="testing" gql={gqlVal} /></div>
},
})
+3 -1
View File
@@ -10,7 +10,7 @@
<LoginModal
v-model="isLoginOpen"
:gql="props.gql"
utm-medium="Runs Tab"
:utm-medium="props.utmMedium"
:show-connect-button-after-login="!cloudProjectId"
@connect-project="isProjectConnectOpen = true"
/>
@@ -18,6 +18,7 @@
v-if="isProjectConnectOpen"
:show="isProjectConnectOpen"
:gql="props.gql"
:utm-medium="props.utmMedium"
@cancel="isProjectConnectOpen = false"
@success="isProjectConnectOpen = false; emit('success')"
/>
@@ -54,6 +55,7 @@ const emit = defineEmits<{
const props = defineProps<{
gql: CloudConnectButtonFragment
class?: string
utmMedium: string
}>()
const isLoginOpen = ref(false)
+1
View File
@@ -20,6 +20,7 @@
<CloudConnectButton
:gql="props.gql"
class="mx-auto mt-40px"
utm-medium="Runs Tab"
@success="emit('success')"
/>
</div>
@@ -62,6 +62,7 @@
v-if="showConnectDialog"
:show="showConnectDialog"
:gql="props.gql"
utm-medium="Runs Tab"
@cancel="showConnectDialog = false"
@success="showConnectDialog = false"
/>
@@ -38,7 +38,7 @@ describe('<CloudConnectModals />', () => {
},
render (gql) {
return (<div class="h-screen">
<CloudConnectModals gql={gql}/>
<CloudConnectModals utmMedium="testing" gql={gql}/>
</div>)
},
})
@@ -11,6 +11,7 @@
v-else-if="props.gql.cloudViewer?.organizations?.nodes.length ?? 0 > 0"
:gql="props.gql"
show
:utm-medium="props.utmMedium"
@update-project-id-failed="showManualUpdate"
@success="emit('success')"
@cancel="emit('cancel')"
@@ -71,6 +72,7 @@ const emit = defineEmits<{
const props = defineProps<{
gql: CloudConnectModalsFragment
utmMedium: string
}>()
const newProjectId = ref('')
@@ -25,7 +25,7 @@ describe('<SelectCloudProjectModal />', () => {
},
render (gql) {
return (<div class="h-screen">
<SelectCloudProjectModal gql={gql}/>
<SelectCloudProjectModal utmMedium="test" gql={gql}/>
</div>)
},
})
@@ -144,7 +144,7 @@ describe('<SelectCloudProjectModal />', () => {
cy.get('@createMutation').should('have.been.calledOnceWith', {
campaign: 'Create project',
cohort: '',
medium: 'Specs Create Project Banner',
medium: 'test',
name: 'Test Project',
orgId: '1',
public: false,
@@ -274,6 +274,7 @@ mutation SelectCloudProjectModal_CreateCloudProject( $name: String!, $orgId: ID!
const props = defineProps<{
gql: SelectCloudProjectModalFragment
utmMedium: string
}>()
const emit = defineEmits<{
@@ -346,7 +347,7 @@ async function createOrConnectProject () {
public: projectAccess.value === 'public',
campaign: 'Create project',
cohort: '',
medium: 'Specs Create Project Banner',
medium: props.utmMedium,
source: getUtmSource(),
})
@@ -34,6 +34,7 @@
<CloudConnectButton
v-else
:gql="props.gql"
utm-medium="Settings Tab"
/>
</SettingsSection>
</template>
+4
View File
@@ -43,6 +43,10 @@
class="ml-4px"
/>
</div>
<span
v-if="latestRun"
class="sr-only"
>{{ props.specFileName }}{{ props.specFileExtension }} test results</span>
</div>
</component>
<template
+1
View File
@@ -182,6 +182,7 @@
<CloudConnectModals
v-if="isProjectConnectOpen"
:gql="props.gql"
:utm-medium="loginUtmMedium"
@cancel="isProjectConnectOpen = false"
@success="refreshPage"
/>
@@ -32,6 +32,7 @@
<CloudConnectModals
v-if="isProjectConnectOpen && cloudModalsQuery.data.value"
:gql="cloudModalsQuery.data.value"
utm-medium="Specs Create Project Banner"
@cancel="handleModalClose"
@success="handleModalClose"
/>
@@ -75,7 +75,9 @@ export class AuthActions {
const isBrowserFocusSupported = this.ctx.coreData.activeBrowser
&& await this.ctx.browser.isFocusSupported(this.ctx.coreData.activeBrowser)
if (!isBrowserFocusSupported) {
const isBrowserOpen = this.ctx.coreData.app.browserStatus === 'open'
if (!isBrowserFocusSupported || !isBrowserOpen) {
this.ctx._apis.electronApi.focusMainWindow()
} else {
await this.ctx.actions.browser.focusActiveBrowserWindow()
@@ -49,9 +49,37 @@ describe('AuthActions', () => {
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.not.be.called
})
it('focuses the browser if the activeBrowser does support focus', async () => {
it('focuses the main window if the activeBrowser supports focus, but browser is closed', async () => {
const browser = ctx.coreData.activeBrowser = { name: 'foo' } as FoundBrowser
ctx.coreData.app.browserStatus = 'closed'
sinon.stub(ctx.browser, 'isFocusSupported').withArgs(browser).resolves(true)
await actions.login()
expect(ctx._apis.electronApi.focusMainWindow).to.be.calledOnce
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.not.be.called
})
it('focuses the main window if the activeBrowser supports focus, but browser is opening', async () => {
const browser = ctx.coreData.activeBrowser = { name: 'foo' } as FoundBrowser
ctx.coreData.app.browserStatus = 'opening'
sinon.stub(ctx.browser, 'isFocusSupported').withArgs(browser).resolves(true)
await actions.login()
expect(ctx._apis.electronApi.focusMainWindow).to.be.calledOnce
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.not.be.called
})
it('focuses the browser if the activeBrowser does support focus and browser is open', async () => {
const browser = ctx.coreData.activeBrowser = { name: 'foo' } as FoundBrowser
ctx.coreData.app.browserStatus = 'open'
sinon.stub(ctx.browser, 'isFocusSupported').withArgs(browser).resolves(true)
await actions.login()