diff --git a/components/pro/__tests__/pro-interface-redirect.test.tsx b/components/pro/__tests__/pro-interface-redirect.test.tsx index 7b91ccdbf..e16c7b7da 100644 --- a/components/pro/__tests__/pro-interface-redirect.test.tsx +++ b/components/pro/__tests__/pro-interface-redirect.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react'; +import { cleanup, render } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; // The Pro shell takes over the standard routes and hands their deep link to @@ -11,7 +11,9 @@ vi.mock('@/i18n/navigation', () => ({ usePathname: () => nav.pathname, useRouter: () => ({ replace: nav.replace }), })); -vi.mock('@/hooks/use-media-query', () => ({ useIsDesktop: () => true })); +const device = vi.hoisted(() => ({ desktop: true })); +vi.mock('@/hooks/use-media-query', () => ({ useIsDesktop: () => device.desktop })); +vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key })); vi.mock('@/stores/settings-store', () => ({ useSettingsStore: (selector: (s: { proInterface: boolean }) => unknown) => selector({ proInterface: true }), })); @@ -25,6 +27,7 @@ const { ProInterfaceRedirect } = await import('../pro-interface-redirect'); describe('ProInterfaceRedirect', () => { beforeEach(() => { vi.clearAllMocks(); + device.desktop = true; }); function visit(path: string, search: string) { @@ -56,4 +59,24 @@ describe('ProInterfaceRedirect', () => { expect(setPendingDeepLink).not.toHaveBeenCalled(); expect(openTab).toHaveBeenLastCalledWith('files'); }); + + it('covers the standard page with the Pro skeleton until /pro renders', () => { + visit('/calendar/week/2026-11-04', ''); + const cover = document.querySelector('[data-pro-takeover]'); + expect(cover?.querySelector('[data-boot-skeleton="pro"]')).not.toBeNull(); + expect(cover?.className).toContain('fixed'); + }); + + it('draws nothing on /pro itself, below desktop width, or on other pages', () => { + visit('/pro', ''); + expect(document.querySelector('[data-pro-takeover]')).toBeNull(); + cleanup(); + visit('/login', ''); + expect(document.querySelector('[data-pro-takeover]')).toBeNull(); + cleanup(); + device.desktop = false; + visit('/mail', ''); + expect(document.querySelector('[data-pro-takeover]')).toBeNull(); + expect(nav.replace).not.toHaveBeenCalled(); + }); }); diff --git a/components/pro/pro-interface-redirect.tsx b/components/pro/pro-interface-redirect.tsx index 5f30c8348..fe7ea4fc2 100644 --- a/components/pro/pro-interface-redirect.tsx +++ b/components/pro/pro-interface-redirect.tsx @@ -7,6 +7,8 @@ import { useIsDesktop } from "@/hooks/use-media-query"; import { useProTabStore, type ProAppTabKind } from "@/stores/pro-tab-store"; import { matchSurface, type AppSurface } from "@/lib/deep-links"; import { setPendingDeepLink } from "@/lib/deep-link-handoff"; +import { ProSkeleton } from "@/components/boot-skeleton/pro-skeleton"; +import { useSkeletonLabels } from "@/components/boot-skeleton/use-skeleton-labels"; const SURFACE_TO_TAB: Record = { mail: 'mail', @@ -37,12 +39,21 @@ export function useProInterfaceActive(): boolean { * segments are handed to the tab that opens (#733), so `/mail/message/` * opens that message in a Pro mail tab instead of silently landing on the * inbox. + * + * Until /pro has rendered, the Pro skeleton covers the standard page, which + * may finish loading first (a fast session restore, a slow /pro chunk): the + * visitor sees the Pro frame from the first paint to the loaded Pro shell, + * never the standard page in between. The server HTML and the hydration pass + * draw nothing here (isDesktop is false there); the page's own skeleton is + * already the Pro frame then (components/boot-skeleton/shell.tsx). */ export function ProInterfaceRedirect() { const router = useRouter(); const pathname = usePathname(); const proInterface = useSettingsStore((s) => s.proInterface); const isDesktop = useIsDesktop(); + const { label } = useSkeletonLabels(); + const takingOver = proInterface && isDesktop && matchSurface(pathname) !== null; useEffect(() => { if (!proInterface || !isDesktop) return; @@ -63,5 +74,10 @@ export function ProInterfaceRedirect() { router.replace('/pro'); }, [proInterface, isDesktop, pathname, router]); - return null; + if (!takingOver) return null; + return ( +
+ +
+ ); }