From 687eb6029f4a0cb22d411b12b3fd16df973c4da0 Mon Sep 17 00:00:00 2001 From: Cacie Prins Date: Mon, 3 Jun 2024 15:54:39 -0400 Subject: [PATCH] fix: Launchpad perpetual loading state, in certain circumstances (#29597) * fix: refetch current project on geometric delay in Main.vue if config has not yet loaded * adds test for config file load behavior in Main.vue * changelog * fix extraneous refetches when config is in error state * Update packages/launchpad/src/Main.vue Co-authored-by: Ryan Manuel * Update packages/launchpad/src/Main.vue Co-authored-by: Bill Glesias * Update cli/CHANGELOG.md Co-authored-by: Bill Glesias * Update packages/launchpad/cypress/e2e/project-setup.cy.ts Co-authored-by: Bill Glesias * Update packages/launchpad/cypress/e2e/project-setup.cy.ts Co-authored-by: Bill Glesias --------- Co-authored-by: Ryan Manuel Co-authored-by: Jennifer Shehane Co-authored-by: Bill Glesias --- cli/CHANGELOG.md | 1 + .../launchpad/cypress/e2e/project-setup.cy.ts | 52 +++++++++++++++++++ packages/launchpad/src/Main.vue | 52 ++++++++++++++++++- 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index a3952f027c7d..303532e40f8a 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -13,6 +13,7 @@ _Released 6/4/2024 (PENDING)_ **Bugfixes:** +- Fixed a situation where the Launchpad would hang if the project config had not been loaded when the Launchpad first queries the current project. Fixes [#29486](https://github.com/cypress-io/cypress/issues/29486). - Pre-emptively fix behavior with Chrome for when `unload` events are forcefully deprecated by using `pagehide` as a proxy. Fixes [#29241](https://github.com/cypress-io/cypress/issues/29241). ## 13.10.0 diff --git a/packages/launchpad/cypress/e2e/project-setup.cy.ts b/packages/launchpad/cypress/e2e/project-setup.cy.ts index 9a1bb69b436d..a9e7ae1637c3 100644 --- a/packages/launchpad/cypress/e2e/project-setup.cy.ts +++ b/packages/launchpad/cypress/e2e/project-setup.cy.ts @@ -694,4 +694,56 @@ describe('Launchpad: Setup Project', () => { cy.contains('h1', 'Project setup') }) }) + + describe('config loading state', () => { + describe('when currentProject config loading state changes from loading to loaded after the first query', () => { + beforeEach(() => { + let responseCount = 0 + + cy.intercept('POST', '/__launchpad/graphql/query-MainLaunchpadQuery', (req) => { + req.reply((res) => { + responseCount++ + if (responseCount === 2) { + res.body.data.currentProject.isLoadingConfigFile = false + } else if (responseCount === 1) { + res.body.data.currentProject.isLoadingConfigFile = true + } else { + throw new Error('Too many calls to MainLaunchpadQuery') + } + }) + }) + }) + + it('eventually displays the launchpad', () => { + scaffoldAndOpenProject('pristine') + cy.visitLaunchpad() + }) + }) + + describe('when the initial config is loading, but eventually fails', () => { + it('shows the error message, and only calls the endpoint enough times to receive the baseError', () => { + let callCount = 0 + let resWithBaseError: number | undefined + + cy.intercept('POST', '/__launchpad/graphql/query-MainLaunchpadQuery', (req) => { + if (resWithBaseError && callCount >= resWithBaseError) { + throw new Error('Too many calls to MainLaunchpadQuery') + } + + callCount++ + req.reply((res) => { + res.body.data.currentProject.isLoadingConfigFile = true + if (res.body.data.baseError) { + resWithBaseError = callCount + } + }) + }) + + scaffoldAndOpenProject('config-with-ts-syntax-error') + cy.visitLaunchpad() + cy.get('[data-cy=error-header]').contains('Cypress configuration error') + cy.wait(1000) + }) + }) + }) }) diff --git a/packages/launchpad/src/Main.vue b/packages/launchpad/src/Main.vue index 23d761e09122..a624ffbb16f4 100644 --- a/packages/launchpad/src/Main.vue +++ b/packages/launchpad/src/Main.vue @@ -108,7 +108,7 @@ import MigrationWizard from './migration/MigrationWizard.vue' import ScaffoldedFiles from './setup/ScaffoldedFiles.vue' import MajorVersionWelcome from './migration/MajorVersionWelcome.vue' import { useI18n } from '@cy/i18n' -import { computed, ref } from 'vue' +import { computed, ref, watch } from 'vue' import LaunchpadHeader from './setup/LaunchpadHeader.vue' import OpenBrowser from './setup/OpenBrowser.vue' import LoginConnectModals from '@cy/gql-components/LoginConnectModals.vue' @@ -193,6 +193,56 @@ const resetErrorAndLoadConfig = (id: string) => { } const query = useQuery({ query: MainLaunchpadQueryDocument }) const currentProject = computed(() => query.data.value?.currentProject) +const hasBaseError = computed(() => !!query.data.value?.baseError) + +const refetchDelaying = ref(false) +const refetchCount = ref(0) + +/* + * Sometimes the config file has not been loaded by the DataContext's config manager by the + * time the MainLaunchpadQueryDocument request is sent off. The server ends up resolving + * the isLoadingConfigFile field as false. In certain situations, there can be a race between + * opening the project and the DataContext completing its retrieval of the configuration. + * In these cases, we want to retry the query until the config file is fully loaded. + * + * If the ProjectConfigIPC encounters an error while loading the config, it will update the + * baseError field via subscription, so there is not a limit set here on retries. + */ + +watch( + [currentProject, query.fetching], + ([currentProject, isFetchingProject]) => { + const isLoadingConfig = currentProject?.isLoadingConfigFile + + /* + * conditions for refetch are: + * - There is a current project, but Config file has not yet loaded + * - There are no pending (delayed) refetches, or fetches in progress + * - There is no baseError - we don't want to continue to refetch if + * things have errored out. + */ + if ( + currentProject && + isLoadingConfig && + !isFetchingProject && + !refetchDelaying.value && + !hasBaseError.value + ) { + refetchDelaying.value = true + refetchCount.value++ + setTimeout(() => { + refetchDelaying.value = false + if ( + (currentProject && !isLoadingConfig) || hasBaseError.value + ) { + return + } + + query.executeQuery({ requestPolicy: 'network-only' }) + }, (refetchCount.value + 1) * 500) + } + }, +) function handleClearLandingPage () { setMajorVersionWelcomeDismissed(MAJOR_VERSION_FOR_CONTENT)