import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' import { cleanup, render, screen } from '@testing-library/react' import { Router } from '../components/router' vi.mock('next/router', () => ({ useRouter: () => router, })) vi.mock('../use-admin', () => ({ useAdmin: () => ctx, })) vi.mock('../router', () => ({ useNavigation: () => ({ navigate }), })) const navigate = vi.fn() let router: { asPath: string push: ReturnType replace: ReturnType beforePopState: ReturnType } let ctx: { path: string | null basePath: string prodUrl: string devUrl: string | null production: boolean } function routerFixture(asPath = '/') { return { asPath, push: vi.fn(), replace: vi.fn(), beforePopState: vi.fn(), } } /** The app is mounted at the root of its own domain. */ function ctxFixture(path: string | null) { return { path, basePath: '/admin/rocket', prodUrl: 'https://rocket.vtex.app', devUrl: 'https://rocket.vtex.app', production: true, } } /** Runs the `beforePopState` handler the Router registered with Next.js. */ function popState(as: string) { const [handler] = router.beforePopState.mock.calls[0] return handler({ as }) } beforeEach(() => { vi.clearAllMocks() router = routerFixture() ctx = ctxFixture('/admin/rocket') }) afterEach(() => { cleanup() vi.restoreAllMocks() }) describe('Router', () => { test('renders the app once the internal path matches the admin path', () => { render( app , ) expect(screen.getByTestId('app')).toBeDefined() expect(router.replace).not.toHaveBeenCalled() }) test('holds the app back until the routing matches', () => { ctx = ctxFixture('/admin/rocket/launches') render( app , ) expect(screen.queryByTestId('app')).toBeNull() }) // Regression: the iframe used to `push` here, adding a second entry to the // browser's joint session history for a single top window navigation. test('replaces instead of pushing when syncing to the admin path', () => { ctx = ctxFixture('/admin/rocket/launches') render( app , ) expect(router.replace).toHaveBeenCalledWith('/launches') expect(router.push).not.toHaveBeenCalled() }) test('replaces with the remaining path when the app has its own base route', () => { ctx = { ...ctxFixture('/admin/rocket/launches'), basePath: '/admin/rocket' } // The app is served from https://host/rocket, so its Next.js base route // has to be prepended to the remaining path. ctx.prodUrl = 'https://rocket.vtex.app/rocket' ctx.devUrl = 'https://rocket.vtex.app/rocket' render( app , ) expect(router.replace).toHaveBeenCalledWith('/rocket/launches') }) test('mounts outdated apps that do not report a path', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) ctx = ctxFixture(null) render( app , ) expect(screen.getByTestId('app')).toBeDefined() expect(router.replace).not.toHaveBeenCalled() expect(warn).toHaveBeenCalled() }) describe('on in-iframe pop navigation', () => { test('forwards the navigation to the Admin Shell and stops Next.js', () => { render( app , ) expect(popState('/launches')).toBe(false) expect(navigate).toHaveBeenCalledWith('/launches') }) test('lets Next.js handle a pop back to the current admin path', () => { render( app , ) expect(popState('/rocket')).toBe(true) expect(navigate).not.toHaveBeenCalled() }) test('lets Next.js handle the pop when the app reports no path', () => { vi.spyOn(console, 'warn').mockImplementation(() => {}) ctx = ctxFixture(null) render( app , ) expect(popState('/launches')).toBe(true) expect(navigate).not.toHaveBeenCalled() }) }) })