From fa2937087b2b81fd96350088961ec97e23bf91b8 Mon Sep 17 00:00:00 2001 From: Peter Buglavecz Date: Tue, 14 Jan 2025 12:37:24 +0100 Subject: [PATCH] fix(nuxt): call `page:loading:end` only once with nested pages (#29009) --- packages/nuxt/src/pages/runtime/page.ts | 12 +++++- test/basic.test.ts | 40 ++++++++++++++++++- test/fixtures/basic/app.vue | 6 +++ test/fixtures/basic/pages/index.vue | 3 ++ test/fixtures/basic/pages/page-load-hook.vue | 9 +++++ .../basic/pages/page-load-hook/[slug].vue | 7 ++++ .../basic/plugins/page-hook-plugin.ts | 8 ++++ 7 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/basic/app.vue create mode 100644 test/fixtures/basic/pages/page-load-hook.vue create mode 100644 test/fixtures/basic/pages/page-load-hook/[slug].vue create mode 100644 test/fixtures/basic/plugins/page-hook-plugin.ts diff --git a/packages/nuxt/src/pages/runtime/page.ts b/packages/nuxt/src/pages/runtime/page.ts index 5ba304cd23..3f82ed8393 100644 --- a/packages/nuxt/src/pages/runtime/page.ts +++ b/packages/nuxt/src/pages/runtime/page.ts @@ -65,7 +65,7 @@ export default defineComponent({ if (import.meta.dev) { nuxtApp._isNuxtPageUsed = true } - + let pageLoadingEndHookAlreadyCalled = false return () => { return h(RouterView, { name: props.name, route: props.route, ...attrs }, { default: (routeProps: RouterViewSlotProps) => { @@ -99,6 +99,7 @@ export default defineComponent({ const key = generateRouteKey(routeProps, props.pageKey) if (!nuxtApp.isHydrating && !hasChildrenRoutes(forkRoute, routeProps.route, routeProps.Component) && previousPageKey === key) { nuxtApp.callHook('page:loading:end') + pageLoadingEndHookAlreadyCalled = true } previousPageKey = key @@ -115,7 +116,14 @@ export default defineComponent({ wrapInKeepAlive(keepaliveConfig, h(Suspense, { suspensible: true, onPending: () => nuxtApp.callHook('page:start', routeProps.Component), - onResolve: () => { nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).then(() => nuxtApp.callHook('page:loading:end')).finally(done)) }, + onResolve: () => { + nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).then(() => { + if (!pageLoadingEndHookAlreadyCalled) { + return nuxtApp.callHook('page:loading:end') + } + pageLoadingEndHookAlreadyCalled = false + }).finally(done)) + }, }, { default: () => { const providerVNode = h(RouteProvider, { diff --git a/test/basic.test.ts b/test/basic.test.ts index da0441d6ab..360bfadf9b 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -623,6 +623,44 @@ describe('pages', () => { const html = await $fetch('/prerender/test') expect(html).toContain('should be prerendered: true') }) + + it('should trigger page:loading:end only once', async () => { + const { page, consoleLogs } = await renderPage('/') + + await page.getByText('to page load hook').click() + await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/page-load-hook') + const loadingEndLogs = consoleLogs.filter(c => c.text.includes('page:loading:end')) + expect(loadingEndLogs.length).toBe(1) + + await page.close() + }) + + it('should hide nuxt page load indicator after navigate back from nested page', async () => { + const LOAD_INDICATOR_SELECTOR = '.nuxt-loading-indicator' + const { page } = await renderPage('/page-load-hook') + await page.getByText('To sub page').click() + await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/page-load-hook/subpage') + + await page.waitForSelector(LOAD_INDICATOR_SELECTOR) + let isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR) + expect(isVisible).toBe(true) + + await page.waitForSelector(LOAD_INDICATOR_SELECTOR, { state: 'hidden' }) + isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR) + expect(isVisible).toBe(false) + + await page.goBack() + + await page.waitForSelector(LOAD_INDICATOR_SELECTOR) + isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR) + expect(isVisible).toBe(true) + + await page.waitForSelector(LOAD_INDICATOR_SELECTOR, { state: 'hidden' }) + isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR) + expect(isVisible).toBe(false) + + await page.close() + }) }) describe('nuxt composables', () => { @@ -2741,7 +2779,7 @@ describe('teleports', () => { const html = await $fetch('/nuxt-teleport') // Teleport is appended to body, after the __nuxt div - expect(html).toContain('

Normal content

Nuxt Teleport

Normal content

Nuxt Teleport
+ + + + + diff --git a/test/fixtures/basic/pages/index.vue b/test/fixtures/basic/pages/index.vue index d0fcd1513c..84edf9d105 100644 --- a/test/fixtures/basic/pages/index.vue +++ b/test/fixtures/basic/pages/index.vue @@ -94,6 +94,9 @@ to server page + + to page load hook + diff --git a/test/fixtures/basic/pages/page-load-hook.vue b/test/fixtures/basic/pages/page-load-hook.vue new file mode 100644 index 0000000000..867cf5f097 --- /dev/null +++ b/test/fixtures/basic/pages/page-load-hook.vue @@ -0,0 +1,9 @@ + diff --git a/test/fixtures/basic/pages/page-load-hook/[slug].vue b/test/fixtures/basic/pages/page-load-hook/[slug].vue new file mode 100644 index 0000000000..6bb96bf43e --- /dev/null +++ b/test/fixtures/basic/pages/page-load-hook/[slug].vue @@ -0,0 +1,7 @@ + diff --git a/test/fixtures/basic/plugins/page-hook-plugin.ts b/test/fixtures/basic/plugins/page-hook-plugin.ts new file mode 100644 index 0000000000..880f7f1ac4 --- /dev/null +++ b/test/fixtures/basic/plugins/page-hook-plugin.ts @@ -0,0 +1,8 @@ +export default defineNuxtPlugin((nuxtApp) => { + const route = useRoute() + nuxtApp.hook('page:loading:end', () => { + if (route.path === '/page-load-hook') { + console.log('page:loading:end') + } + }) +})