From 086c26005a317b687b6f8f75fb2f57dcfaa4c6b9 Mon Sep 17 00:00:00 2001 From: wdoconnell <91283923+wdoconnell@users.noreply.github.com> Date: Fri, 12 Aug 2022 09:07:24 -0400 Subject: [PATCH 1/2] feat: add e2e tests for change-account and change-org dropdowns --- cypress/e2e/cloud/globalHeader.ts | 279 ++++++++++++++++-- .../GlobalHeader/AccountDropdown.tsx | 3 + .../components/GlobalHeader/GlobalHeader.tsx | 41 ++- .../components/GlobalHeader/OrgDropdown.tsx | 3 + 4 files changed, 288 insertions(+), 38 deletions(-) diff --git a/cypress/e2e/cloud/globalHeader.ts b/cypress/e2e/cloud/globalHeader.ts index f2a4646f3c..67fd9c48e2 100644 --- a/cypress/e2e/cloud/globalHeader.ts +++ b/cypress/e2e/cloud/globalHeader.ts @@ -1,58 +1,293 @@ -describe('multi-account multi-org global header', () => { +describe('change-account change-org global header', () => { const globalHeaderFeatureFlags = { quartzIdentity: true, multiOrg: true, } - beforeEach(() => { - // Maintain the same session for all tests so that further logins aren't required. - Cypress.Cookies.preserveOnce('sid') - }) + let idpeOrgID: string + + const interceptPageReload = () => { + cy.intercept('GET', 'api/v2/orgs').as('getOrgs') + cy.intercept('GET', 'api/v2/flags').as('getFlags') + cy.intercept('GET', 'api/v2/quartz/accounts/**/orgs').as('getQuartzOrgs') + } + + const makeQuartzUseIDPEOrgID = () => { + cy.intercept('GET', 'api/v2/quartz/identity', req => { + req.continue(res => { + res.body.org.id = idpeOrgID + }) + }).as('getQuartzIdentity') + + cy.intercept('GET', '/api/v2/quartz/accounts/**/orgs', req => { + req.continue(res => { + res.body[0].id = idpeOrgID + }) + }).as('getQuartzOrgs') + + cy.intercept('GET', 'api/v2/quartz/orgs/*', req => { + req.continue(res => { + res.body.id = idpeOrgID + }) + }).as('getQuartzOrgDetails') + } + + const mockQuartzOutage = () => { + const quartzFailure = { + statusCode: 503, + body: 'Service Unavailable', + } + + cy.intercept('GET', 'api/v2/quartz/identity', quartzFailure).as( + 'getQuartzIdentity' + ) + cy.intercept('GET', 'api/v2/quartz/accounts', quartzFailure).as( + 'getQuartzAccounts' + ) + } before(() => { cy.flush().then(() => cy.signin().then(() => { - cy.get('@org').then(() => { - cy.getByTestID('home-page--header').should('be.visible') - cy.setFeatureFlags(globalHeaderFeatureFlags).then(() => { - // cy.wait is necessary to ensure sufficient time for the feature flag override. - // We cannot cy.wait an intercepted route because this isn't a network request. - cy.wait(400).then(() => { - cy.visit('/') - }) - }) + cy.request({ + method: 'GET', + url: '/api/v2/orgs', + }).then(res => { + // Store the IDPE org ID so that it can be cloned when intercepting quartz. + idpeOrgID = res.body.orgs[0].id }) }) ) }) - describe('user profile avatar', () => { + beforeEach(() => { + // Preserve one session throughout. + Cypress.Cookies.preserveOnce('sid') + }) + + describe('global change-account and change-org header', () => { + it('does not render when API requests to quartz fail', () => { + mockQuartzOutage() + interceptPageReload() + cy.setFeatureFlags(globalHeaderFeatureFlags).then(() => { + cy.visit('/') + cy.wait(['@getQuartzIdentity', '@getQuartzAccounts']) + cy.getByTestID('global-header--container').should('not.exist') + }) + }) + + describe('change org dropdown', () => { + before(() => { + cy.request({ + method: 'GET', + url: '/api/v2/orgs', + }).then(res => { + // Retrieve the user's org ID from IDPE. + idpeOrgID = res.body.orgs[0].id + + cy.setFeatureFlags(globalHeaderFeatureFlags) + cy.visit('/') + }) + }) + + beforeEach(() => { + // For each test, replace the org id served by quartz-mock with the IDPE org id. + // This ensures that routes based on the current org id are compatible with quartz-mock. + makeQuartzUseIDPEOrgID() + // A short wait is needed to ensure we've completed trailing API calls. Can't consistently cy.wait one route, + // as the problem call varies. This adds < 1s to a 35+ second run, which seems acceptable to combat flake. + cy.wait(200) + }) + + it('navigates to the org settings page', () => { + cy.getByTestID('globalheader--org-dropdown') + .should('be.visible') + .click() + cy.getByTestID('globalheader--org-dropdown-main').should('be.visible') + cy.getByTestID('globalheader--org-dropdown-main-Settings') + .should('be.visible') + .click() + + cy.location('pathname').should('eq', `/orgs/${idpeOrgID}/about`) + cy.getByTestID('org-profile--panel') + .should('be.visible') + .and('contain', 'Organization Profile') + }) + + it('navigates to the org members page', () => { + cy.getByTestID('globalheader--org-dropdown') + .should('be.visible') + .click() + cy.getByTestID('globalheader--org-dropdown-main').should('be.visible') + cy.getByTestID('globalheader--org-dropdown-main-Members') + .should('be.visible') + .click() + + cy.location('pathname').should('eq', `/orgs/${idpeOrgID}/users`) + cy.getByTestID('tabs--container') + .should('be.visible') + .and('contain', 'Add a new user to your organization') + }) + + it('navigates to the org usage page', () => { + cy.getByTestID('globalheader--org-dropdown') + .should('exist') + .click() + + cy.getByTestID('globalheader--org-dropdown-main').should('be.visible') + cy.getByTestID('globalheader--org-dropdown-main-Usage') + .should('be.visible') + .click() + cy.location('pathname').should('eq', `/orgs/${idpeOrgID}/usage`) + cy.getByTestID('tabs--container') + .should('be.visible') + .and('contain', 'Billing Stats') + }) + + it('can change change the active org', () => { + cy.intercept('GET', 'auth/orgs/58fafbb4f68e05e5', { + statusCode: 200, + body: 'Reaching this page serves an org change in prod.', + }).as('getNewOrg') + + cy.getByTestID('globalheader--org-dropdown') + .should('exist') + .click() + + cy.getByTestID('globalheader--org-dropdown-main').should('be.visible') + cy.getByTestID('dropdown-item') + .contains('Switch Organization') + .should('be.visible') + .click() + + cy.getByTestID('globalheader--org-dropdown-typeahead') + .should('be.visible') + .type('g 5') + + cy.getByTestID('globalheader--org-dropdown-main--contents') + .contains('Org 5') + .should('be.visible') + .click() + + cy.contains('Reaching this page serves an org change in prod.').should( + 'be.visible' + ) + }) + }) + + describe('change account dropdown', () => { + beforeEach(() => { + makeQuartzUseIDPEOrgID() + cy.setFeatureFlags(globalHeaderFeatureFlags) + }) + + before(() => { + cy.visit('/') + }) + + it('navigates to the account settings page', () => { + cy.getByTestID('globalheader--account-dropdown') + .should('exist') + .click() + + cy.getByTestID('globalheader--account-dropdown-main').should( + 'be.visible' + ) + + cy.getByTestID('globalheader--account-dropdown-main-Settings') + .should('be.visible') + .click() + + cy.getByTestID('account-settings--header').should('be.visible') + }) + + it('navigates to the account billing page', () => { + cy.getByTestID('globalheader--account-dropdown') + .should('exist') + .click() + + cy.getByTestID('globalheader--account-dropdown-main').should( + 'be.visible' + ) + + cy.getByTestID('globalheader--account-dropdown-main-Billing') + .should('be.visible') + .click() + }) + + it('can change change the active account', () => { + cy.intercept('GET', 'auth/accounts/415', { + statusCode: 200, + body: 'Reaching this page serves an account change in prod.', + }).as('getNewAccount') + + cy.getByTestID('globalheader--account-dropdown') + .should('exist') + .click() + + cy.getByTestID('globalheader--account-dropdown-main').should( + 'be.visible' + ) + cy.getByTestID('dropdown-item') + .contains('Switch Account') + .should('be.visible') + .click() + + cy.getByTestID('globalheader--account-dropdown-typeahead') + .should('be.visible') + .type('gan') + + cy.getByTestID('globalheader--account-dropdown-main--contents') + .contains('Veganomicon') + .should('be.visible') + .click() + + cy.contains( + 'Reaching this page serves an account change in prod.' + ).should('be.visible') + }) + }) + }) + + describe('user profile avatar', {scrollBehavior: false}, () => { + before(() => { + interceptPageReload() + makeQuartzUseIDPEOrgID() + // A reset is required here because the prior test ends on a mocked-up page served by cy.intercept, + // which stands in for the 'change account' page actually served by quartz in prod. + cy.visit('/').then(() => { + cy.wait(['@getOrgs', '@getFlags']) + cy.setFeatureFlags(globalHeaderFeatureFlags).then(() => { + cy.wait('@getQuartzOrgs') + cy.visit('/') + }) + }) + }) + it('navigates to the `user profile` page', () => { cy.getByTestID('global-header--user-avatar') .should('be.visible') - .click({scrollBehavior: false}) + .click() cy.getByTestID('global-header--user-popover-profile-button') .should('be.visible') - .click({scrollBehavior: false}) + .click() cy.getByTestID('user-profile--page').should('be.visible') cy.getByTestID('global-header--user-avatar') .should('be.visible') - .click({scrollBehavior: false}) + .click() }) it('allows the user to log out', () => { cy.getByTestID('global-header--user-avatar') .should('be.visible') - .click({scrollBehavior: false}) + .click() cy.getByTestID('global-header--user-popover-logout-button') .should('be.visible') - .click({ - scrollBehavior: false, - }) + .click() // Logout in remocal looks like a 404 because there is no quartz. This tests the logout URL. cy.location('pathname').should('eq', '/logout') }) diff --git a/src/identity/components/GlobalHeader/AccountDropdown.tsx b/src/identity/components/GlobalHeader/AccountDropdown.tsx index 985b2269cb..7e0d458209 100644 --- a/src/identity/components/GlobalHeader/AccountDropdown.tsx +++ b/src/identity/components/GlobalHeader/AccountDropdown.tsx @@ -59,11 +59,14 @@ export const AccountDropdown: FC = ({ mainMenuHeaderIcon={IconFont.Switch_New} mainMenuHeaderText="Switch Account" mainMenuOptions={accountMainMenu} + mainMenuTestID="globalheader--account-dropdown-main" style={style} + testID="globalheader--account-dropdown" typeAheadInputPlaceholder="Search Accounts" typeAheadMenuOptions={accountsList} typeAheadOnSelectOption={switchAccount} typeAheadSelectedOption={selectedAccount} + typeAheadTestID="globalheader--account-dropdown-typeahead" /> ) } diff --git a/src/identity/components/GlobalHeader/GlobalHeader.tsx b/src/identity/components/GlobalHeader/GlobalHeader.tsx index 170ca9c9d7..9a35a06979 100644 --- a/src/identity/components/GlobalHeader/GlobalHeader.tsx +++ b/src/identity/components/GlobalHeader/GlobalHeader.tsx @@ -77,17 +77,23 @@ export const GlobalHeader: FC = () => { } }, [orgsList]) + const shouldLoadDropdowns = activeOrg?.id && activeAccount?.id + const shouldLoadAvatar = + user.firstName && user.lastName && user.email && org.id + const shouldLoadGlobalHeader = shouldLoadDropdowns || shouldLoadAvatar + const caretStyle = {fontSize: '18px', color: InfluxColors.Grey65} return ( - - - {activeOrg && activeAccount && ( - <> + shouldLoadGlobalHeader && ( + + {shouldLoadDropdowns && ( + { /> - + + )} + + {shouldLoadAvatar && ( + )} - - + ) ) } diff --git a/src/identity/components/GlobalHeader/OrgDropdown.tsx b/src/identity/components/GlobalHeader/OrgDropdown.tsx index 6796adad1e..390f0846bc 100644 --- a/src/identity/components/GlobalHeader/OrgDropdown.tsx +++ b/src/identity/components/GlobalHeader/OrgDropdown.tsx @@ -58,6 +58,9 @@ export const OrgDropdown: FC = ({activeOrg, orgsList}) => { typeAheadMenuOptions={orgsList} typeAheadOnSelectOption={switchOrg} typeAheadSelectedOption={activeOrg} + testID="globalheader--org-dropdown" + mainMenuTestID="globalheader--org-dropdown-main" + typeAheadTestID="globalheader--org-dropdown-typeahead" /> ) } From 761054afb5e0e37fd57c92d02a05ca0443b1b9fa Mon Sep 17 00:00:00 2001 From: wdoconnell <91283923+wdoconnell@users.noreply.github.com> Date: Wed, 17 Aug 2022 09:45:14 -0400 Subject: [PATCH 2/2] chore: reorder variable names in global header --- .../components/GlobalHeader/GlobalHeader.tsx | 18 +++++++++--------- .../components/GlobalHeader/OrgDropdown.tsx | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/identity/components/GlobalHeader/GlobalHeader.tsx b/src/identity/components/GlobalHeader/GlobalHeader.tsx index 9a35a06979..09fbf793cb 100644 --- a/src/identity/components/GlobalHeader/GlobalHeader.tsx +++ b/src/identity/components/GlobalHeader/GlobalHeader.tsx @@ -1,13 +1,13 @@ // Library imports -import React, {useContext, useEffect, useState, FC} from 'react' -import {useSelector, useDispatch} from 'react-redux' +import React, {FC, useContext, useEffect, useState} from 'react' +import {useDispatch, useSelector} from 'react-redux' import { ComponentSize, FlexBox, - IconFont, Icon, - JustifyContent, + IconFont, InfluxColors, + JustifyContent, } from '@influxdata/clockface' // Selectors and Context @@ -16,8 +16,8 @@ import {selectQuartzIdentity} from 'src/identity/selectors' import {UserAccountContext} from 'src/accounts/context/userAccount' // Components -import {OrgDropdown} from 'src/identity/components/GlobalHeader/OrgDropdown' import {AccountDropdown} from 'src/identity/components/GlobalHeader/AccountDropdown' +import {OrgDropdown} from 'src/identity/components/GlobalHeader/OrgDropdown' // Thunks import {getQuartzOrganizationsThunk} from 'src/identity/quartzOrganizations/actions/thunks' @@ -87,16 +87,16 @@ export const GlobalHeader: FC = () => { return ( shouldLoadGlobalHeader && ( {shouldLoadDropdowns && ( @@ -106,9 +106,9 @@ export const GlobalHeader: FC = () => { {shouldLoadAvatar && ( )} diff --git a/src/identity/components/GlobalHeader/OrgDropdown.tsx b/src/identity/components/GlobalHeader/OrgDropdown.tsx index 390f0846bc..9d37cef847 100644 --- a/src/identity/components/GlobalHeader/OrgDropdown.tsx +++ b/src/identity/components/GlobalHeader/OrgDropdown.tsx @@ -4,8 +4,8 @@ import {IconFont} from '@influxdata/clockface' // Components import { - TypeAheadMenuItem, GlobalHeaderDropdown, + TypeAheadMenuItem, } from 'src/identity/components/GlobalHeader/GlobalHeaderDropdown' // Types @@ -25,8 +25,8 @@ interface Props { orgsList: OrganizationSummaries } -const style = {width: 'auto'} const menuStyle = {width: '250px'} +const style = {width: 'auto'} export const OrgDropdown: FC = ({activeOrg, orgsList}) => { const orgMainMenu = [ @@ -53,13 +53,13 @@ export const OrgDropdown: FC = ({activeOrg, orgsList}) => { mainMenuHeaderIcon={IconFont.Switch_New} mainMenuHeaderText="Switch Organization" mainMenuOptions={orgMainMenu} + mainMenuTestID="globalheader--org-dropdown-main" style={style} typeAheadInputPlaceholder="Search Organizations" typeAheadMenuOptions={orgsList} typeAheadOnSelectOption={switchOrg} typeAheadSelectedOption={activeOrg} testID="globalheader--org-dropdown" - mainMenuTestID="globalheader--org-dropdown-main" typeAheadTestID="globalheader--org-dropdown-typeahead" /> )