feat: keep the Pro skeleton over a standard page until the Pro workspace takes it over

This commit is contained in:
Linus Rath
2026-11-01 19:47:30 +01:00
parent 7efae37645
commit 73916cf42e
2 changed files with 42 additions and 3 deletions
@@ -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();
});
});
+17 -1
View File
@@ -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<AppSurface, ProAppTabKind> = {
mail: 'mail',
@@ -37,12 +39,21 @@ export function useProInterfaceActive(): boolean {
* segments are handed to the tab that opens (#733), so `/mail/message/<id>`
* 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 (
<div className="fixed inset-0 z-50" data-pro-takeover="">
<ProSkeleton label={label} />
</div>
);
}