Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement new login and "connect project" logic #23762

Merged
merged 174 commits into from
Oct 24, 2022

Conversation

marktnoonan
Copy link
Contributor

@marktnoonan marktnoonan commented Sep 9, 2022

Co-authored with @warrensplayer

There are lots of changes in this PR, though many of the are minor changes that come from using the new Login Connect store, moving files around, and simplifying some component APIs.

This PR:

  • displays the "connect project" button after login in the Launchpad
  • displays a "record run" modal prompt if the user is ready to record their first run, when the "continue" button is pressed after login (if a testing type is chosen and the project is connected)
  • renders the login-connect modals flow as a single component at the root of each app
  • wires up the Smart Banners with their new rules via the login-connect-store and prompt manger
  • wires up the new Login Connect modal itself to be driven by the new store
  • add as a CloudViewerAndProject component to consolidate much of the GQL-querying needed to support the new Login Connect store
  • fixes some flake, adds some missing e2e coverage for login from launchpad
  • adds a couple of missing/incorrect UTM params

Allowing users to connect projects from the Launchpad login flow required moving many components and queries into frontend-shared, which had various implications for tests.

Developer facing improvements

Through incremental changes introduced since Cypress 10.0.0, the implementation for ways to trigger modals and track state related to logging in and connecting projects has become unwieldy, with similar ideas implemented in multiple places, in slightly different ways. The complexity of the current implementation exceeds the complexity of the underlying ideas, and this PR attempts to bring them closer together by using the logic merged in #24101 to drive the UI.

  • Previously we had 4 LoginModal components and 5 CloudConnectModals being rendered throughout the app, with each parent component managing opening and closed states & user progress. Now all those parent components trigger the single LoginConnectModal component for that app, which opens in the correct state based on the store and looks after itself once open.
  • This cleans up lots of events and prop drilling how in the Login, CloudConnect, and Auth components work together
  • Got rid of a lot of modelValue bindings where we were not leveraging v-model behavior.
  • Driving more things from the store makes component tests a little easier to manage as it's all clientside.
  • The setPreferences action in data-context will now do a lodash merge instead of a simple loop resetting properties that were passed to it, this avoids some reading/merging of deep preferences that we were having to do on the client now that we have nested values in there like banners, which was causing a problem in this PR as we'd step on freshly saved data with some other new sibling data.

Further improvements

We could always go further with refactoring, but had to draw the line somewhere to merge this. Things this PR doesn't address:

  • quite a bit of repetition in GQL queries, I think we can use fragments more to avoid this?
  • GQL queries requesting things (like firstOrganization in this PR) that aren't used directly by the component but need to stay up to date, and if left out can cause problems - this feels like a bit of a bad pattern, it's easy to remove something that's not obviously being used, only to find out that it can throw off some other component.
  • There are more things we could hook up to the new LoginConnect store,
  • Thought about pulling out the setUpStatus function from isAllowedFeature.cy.ts, if all of our tests use this, it will simplify them and they won't need to know details of what values in the store lead to each state.

User facing changelog

Features

  • Users can now connect a project to the Dashboard when logging in from the Launchpad, if a testing type has been selected.
  • In both the App and the Launchpad, users whose projects are connect to the dashboard, and who are ready to record their first, run can see the appropriate prompt after login

Minor Bugfixes

  • Within the Cloud Connect modal, user can no longer click the button to create or connect a project unless a project name has been chosen or entered.
  • Within the Cloud Connect modal, background polling of cloud data will no longer cause a chosen project to become unselected
  • The Create Organization and Select Project modals will now only render external links when the URL is present

Additional details

Steps to test

Launchpad - Login/Connect/Record flow

Loom video of expectations for these steps in the launchpad

  • Global Mode

    • Log in via the top nav
    • CTA should be "Continue" after login
    • Login modal should close after clicking continue
  • With any project open

    • on "Choose testing type" screen, log in via the top nav
    • CTA should be "Continue" after login
    • Login modal should close after clicking continue
  • With a project that has no project id

    • on "Choose a Browser" screen, log in via the top nav
    • CTA should be "Connect Project" after login
    • Connect/Create Project modal should open after clicking "Connect Project"
    • After connecting project, config should reload, closing modal and causing a spinner
  • With a project that has a project id, but no recorded runs in the dashboard

    • on "Choose a Browser" screen, log in via the top nav
    • CTA should be "Continue" after login
    • "Record Your First Run" modal should open after clicking "Continue"
    • Record command should be copyable
    • Modal should be dismissable

App - Login/Connect/Record flow

  • With an unconnected project open (no project ID in config) (loom video)

    • setup - remove promptsShown.loginModalRecord from state.json for this project if it is present
    • on the specs list page, log in via the top nav
    • CTA should be "Connect Project" after login
    • Connect/Create Project modal should open after clicking "Connect Project"
    • Choose a project that has no runs recorded in the dashboard (you might need to make an empty project in your personal org for this)
    • After connecting project, config should reload, closing modal and causing a spinner
    • "Record Your First Run" smart banner should be displayed
  • With a connected project that has no recorded runs (Loom video]

    • setup - remove promptsShown.loginModalRecord from state.json for this project if it is present
    • on the specs list page, log in via the top nav
    • CTA should be "Continue" after login
    • After clicking "Continue", the "record first run" modal should appear
    • Record command should be copyable
    • Modal should be dismissable
    • After dismissing modal, "Record Your First Run" smart banner should not be displayed at the top of the specs list
    • promptsShown.loginModalRecord should exist in state.json for this project
  • With a connected project that has recorded runs

    • on the specs list page, log in via the top nav
    • CTA should be "Continue" after login
    • After clicking "Continue", the modal should simply dismiss

How has the user experience changed?

PR Tasks

  • Have tests been added/updated?
  • Has the original issue (or this PR, if no issue exists) been tagged with a release in ZenHub? (user-facing changes only)
  • Has a PR for user-facing changes been opened in cypress-documentation?
  • Have API changes been updated in the type definitions?

Copy link
Contributor

@warrensplayer warrensplayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marktnoonan Still going through items, but wanted to post this since you were still working in this area. I hope my suggestions with using reactive in SpecsListBanners simplifies things.

@@ -116,6 +116,10 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
obj.result.data.cloudViewer.organizations.nodes = []
}

if (obj.result.data?.cloudViewer?.firstOrganization?.nodes) {
obj.result.data.cloudViewer.firstOrganization.nodes = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that the need for firstOrganization was introduced in previous work, but I hope we can take some more time soon to find out why we have to rely on this trick while we also have the organizations being returned at the same time.

@@ -181,7 +191,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
cy.startAppServer('component')

cy.remoteGraphQLIntercept(async (obj, testState) => {
if (obj.operationName === 'CloudConnectModals_RefreshCloudViewer_refreshCloudViewer_cloudViewer') {
if (obj.operationName === 'LoginConnectModals_LoginConnectModalsQuery_cloudViewer') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was just updating existing code, but looking at the rest of the code base, this spec file is the only place that there is conditional logic based on operationName. This is a fragile pattern to have look at this string. This is definitely a code smell to me but I have not looked at what the alternative would be yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These mutation names do get into a lot of places, and since their naming convention depends on our component architecture, we end up expressing that architecture in our tests, which yeah does feel a bit off, but also not a huge burden in practice imo, since we rarely change the architecture.

Comment on lines 233 to 236
return {
component: componentsByStatus[loginConnectStore.userStatus] ?? null,
cohortOption: getCohortForBanner(bannerIds[loginConnectStore.userStatus]),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return {
component: componentsByStatus[loginConnectStore.userStatus] ?? null,
cohortOption: getCohortForBanner(bannerIds[loginConnectStore.userStatus]),
}
return reactive({
component: componentsByStatus[loginConnectStore.userStatus] ?? null,
cohortOption: getCohortForBanner(bannerIds[loginConnectStore.userStatus]),
})

Wrapping the return in reactive should allow for the un-nesting of the refs but still allow them to be reactive. That way, all the nested .value calls can be removed. I have not fully tested this yet, but I believe this should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, I will check that out, I had drafted a comment about this but your review beat me to it 🙏

@@ -294,6 +268,10 @@ const bannerCohortOptions = {
const cohortBuilder = useCohorts()

const getCohortForBanner = (bannerId: string) => {
if (!bannerCohortOptions[bannerIds[loginConnectStore.userStatus]]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!bannerCohortOptions[bannerIds[loginConnectStore.userStatus]]) {
if (!bannerCohortOptions[bannerId]) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha, yes 🤦

Comment on lines 45 to 55
watch(() => loginConnectStore.isLoginConnectOpen, (newVal) => {
if (newVal) {
executeQuery()
}
}, {
immediate: true,
})

const executeQuery = async () => {
await query.executeQuery()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
watch(() => loginConnectStore.isLoginConnectOpen, (newVal) => {
if (newVal) {
executeQuery()
}
}, {
immediate: true,
})
const executeQuery = async () => {
await query.executeQuery()
}
const executeQuery = async () => {
await query.executeQuery()
}
whenever(() => loginConnectStore.isLoginConnectOpen, executeQuery)

Short and sweet? Didn't test this though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, updated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I did not know about whenever. I need to spend more time looking through the vueUse library!

@@ -159,3 +162,22 @@ async function fileExists (absolute: string) {
return false
}
}

export async function hasNonExampleSpec (testTemplateDir: string, specs: string[]): Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code feels like it doesn't belong here and would make more sense in ProjectActions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @warrensplayer had a reason for keeping it here & calling it from ProjectActions, maybe he can refresh my memory?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ZachJW34 @marktnoonan I felt a little weird about putting it there, but kept it in this file because I was using internal methods (allFilesInDir and fileExists) that I did not want to refactor at the time I created that method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with where it is then, could do with a comment saying a refactor/move would be good.

Copy link
Contributor

@ZachJW34 ZachJW34 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some questions and nits, everything works great! Will approve once the questions are responded too, nothing was blocking.

Copy link
Contributor

@warrensplayer warrensplayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the code is great. The part I love most is all the deletion of code! That was a great update to SpecsListBanners to consolidate the creation of the cohort with the determination of the banner to show.

Only one strange thing I am seeing during testing and not sure if it is an existing issue or not. Logging in with the menu item in the header seems fine. However, using the tooltip or the LoginBanner, sometimes causes things to get "stuck". I have attached a screen recording to show one example. In this example, one item has the "loading" skeleton stuck showing. Also, when logging back out, the LoginBanner does not reappear. Not sure what is happening here yet.

Screen.Recording.2022-10-21.at.3.16.28.PM.mov

cy.get(`[data-cy="${bannerTestId}"]`).should('be.visible')
})

it('should be preempted by spec not found banner', () => {
mountWithState(gql, stateWithFirstOpenedDaysAgo(4), { isSpecNotFound: true })
mountWithState(gql, undefined, { isSpecNotFound: true })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the undefined be an empty object instead? {}


_.merge(this.ctx.coreData.localSettings.preferences, toJson)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that LocalSettingsActions does not have any unit tests. Can you see if it would be easy to add a unit test for the method this is in?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just commenting for the record that these have been added!

Comment on lines 45 to 55
watch(() => loginConnectStore.isLoginConnectOpen, (newVal) => {
if (newVal) {
executeQuery()
}
}, {
immediate: true,
})

const executeQuery = async () => {
await query.executeQuery()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I did not know about whenever. I need to spend more time looking through the vueUse library!

Copy link
Contributor

@warrensplayer warrensplayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great to me! Great job @marktnoonan

component: componentsByStatus[loginConnectStore.userStatus] ?? null,
cohortOption: getCohortForBanner(bannerIds[loginConnectStore.userStatus]),
}
return componentsByStatus[loginConnectStore.userStatus] ?? null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason you split the cohortOption out of this method again? I liked that the bannerToShow encapsulated both the banner and the props it needed, but I do not think it is worth refactoring again. It works this way as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked it too, but it turned out Vue was logging warnings about wrapping the components in a reactive object. I don't recall the exact warning, but splitting them up seemed the best way to get what we wanted, at least for now.

@marktnoonan
Copy link
Contributor Author

@ZachJW34 I think I responded to your comments, let me know if there's anything else that sticks out.

Copy link
Contributor

@ZachJW34 ZachJW34 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants