import { GeneratedFolder, ProjectUIDL } from '@teleporthq/teleport-types' import { PageTransition } from '@teleporthq/teleport-shared' import uidlSample from '../../../examples/test-samples/project-sample.json' import { createNextProjectGenerator } from '../src' import NextTemplate from '../src/project-template' import { pageTransitionCover, pageTransitionCoverColor, pageTransitionSkipPattern, pageTransitionVariants, resolvePageTransitionOptions, reversePageTransitionPreset, } from '../src/page-transition/page-transition-variants' import { revealOnScreen } from '../src/page-transition/reveal-on-screen' import { REVEAL_ON_SCREEN_SOURCES } from '../src/page-transition/reveal-on-screen-source.generated' const template = () => JSON.parse(JSON.stringify(NextTemplate)) as GeneratedFolder const buildUidl = (pageTransition?: { preset: string duration: number easing: string skipRoutes?: string[] options?: Record custom?: { arrive: Record; leave: Record } flyingPictures?: boolean }) => { const uidl = JSON.parse(JSON.stringify(uidlSample)) as ProjectUIDL if (pageTransition) { ;(uidl.globals.settings as { pageTransition?: unknown }).pageTransition = pageTransition } return uidl } const findFile = (folder: GeneratedFolder, folderName: string, fileName: string) => folder.subFolders .find((sub) => sub.name === folderName) ?.files.find((file) => file.name === fileName) describe('Next generator page transition', () => { const generator = createNextProjectGenerator() it('leaves a project without a transition exactly as before', async () => { const output = await generator.generateProject(buildUidl(), template()) expect(findFile(output, 'components', 'tq-page-transition')).toBeUndefined() const app = findFile(output, 'pages', '_app') expect(app?.content).toContain('') expect(app?.content).not.toContain('TqPageTransition') const pkg = output.files.find((file) => file.name === 'package') expect(pkg?.content).not.toContain('framer-motion') }) it('wraps the page mount and ships the wrapper when the editor chose one', async () => { const output = await generator.generateProject( buildUidl({ preset: 'slide-up', duration: 0.25, easing: 'ease-out' }), template() ) const app = findFile(output, 'pages', '_app') expect(app?.content).toContain( "import TqPageTransition from '../components/tq-page-transition'" ) expect(app?.content).toContain( '' ) const wrapper = findFile(output, 'components', 'tq-page-transition') expect(wrapper?.content).toContain("const PRESET = 'slide-up'") expect(wrapper?.content).toContain('function reversePageTransitionPreset(') expect(wrapper?.content).toContain('router.beforePopState(') expect(wrapper?.content).toContain("window.addEventListener('pointerdown'") expect(wrapper?.content).toContain('const DURATION = 0.25') expect(wrapper?.content).toContain('const EASE = [0, 0, 0.58, 1]') expect(wrapper?.content).toContain('function pageTransitionVariants(') expect(wrapper?.content).toContain( "AnimatePresence mode={context.morph || !REVEAL ? 'wait' : 'sync'} initial={false}" ) expect(wrapper?.content).toContain("router.events.on('routeChangeStart'") const pkg = output.files.find((file) => file.name === 'package') expect(pkg?.content).toContain('"framer-motion"') expect(pkg?.content).toContain('"react": "^18.3.1"') }) // The server cannot know that a visitor asks for less motion. A wrapper that // rendered the bare page for them made React throw the server's page away on // every load (error 418) and draw it again in the browser. it('renders the same page for every visitor and gives less motion each page at once', async () => { const output = await generator.generateProject( buildUidl({ preset: 'slide-up', duration: 0.25, easing: 'ease-out' }), template() ) const wrapper = findFile(output, 'components', 'tq-page-transition')?.content || '' expect(wrapper).not.toContain('useReducedMotion') expect(wrapper).toContain('if (!variants) {\n return children\n }') expect(wrapper).toContain('const skip = prefersLessMotion() || isSkippedRoute(routeKeyOf(url))') }) it('ignores a transition it does not know and a preset of none', async () => { const output = await generator.generateProject( buildUidl({ preset: 'none', duration: 0.35, easing: 'ease-out' }), template() ) expect(findFile(output, 'components', 'tq-page-transition')).toBeUndefined() expect(findFile(output, 'pages', '_app')?.content).not.toContain('TqPageTransition') }) it('falls back to the site curve when the easing name is unknown', async () => { const output = await generator.generateProject( buildUidl({ preset: 'fade', duration: 0.45, easing: 'wobble' }), template() ) expect(findFile(output, 'components', 'tq-page-transition')?.content).toContain( 'const EASE = [0, 0, 0.58, 1]' ) }) it('plays an arriving page once the pictures on its first screen are decoded, 300 ms at most', async () => { const output = await generator.generateProject( buildUidl({ preset: 'fade', duration: 0.35, easing: 'ease-out' }), template() ) const wrapper = findFile(output, 'components', 'tq-page-transition')?.content || '' expect(wrapper).toContain('const PICTURE_WAIT_MS = 300') expect(wrapper).toContain('image.decode().catch(() => {})') // the first page is ready at once; a later one waits at its starting state expect(wrapper).toContain('const [readyKey, setReadyKey] = React.useState(routeKey)') expect(wrapper).toContain("animate={readyKey === routeKey ? 'animate' : 'initial'}") // a page that arrived after it takes the turn: a late answer changes nothing expect(wrapper).toContain('if (readyKeyRef.current === key) {') }) }) describe('Next generator per-page skip', () => { const generator = createNextProjectGenerator() it('carries the opted-out routes into the wrapper as patterns', async () => { const output = await generator.generateProject( buildUidl({ preset: 'fade', duration: 0.35, easing: 'ease-out', skipRoutes: ['/checkout', '/products/[slug]'], }), template() ) const wrapper = findFile(output, 'components', 'tq-page-transition')?.content || '' expect(wrapper).toContain('const SKIP_ROUTES = ["/checkout","/products/[slug]"]') expect(wrapper).toContain('const skip = prefersLessMotion() || isSkippedRoute(routeKeyOf(url))') expect(wrapper).not.toContain('isSkippedRoute(routeKeyRef.current)') }) it('emits an empty list when no page opted out', async () => { const output = await generator.generateProject( buildUidl({ preset: 'fade', duration: 0.35, easing: 'ease-out' }), template() ) expect(findFile(output, 'components', 'tq-page-transition')?.content).toContain( 'const SKIP_ROUTES = []' ) }) }) describe('Next generator cover presets', () => { const generator = createNextProjectGenerator() it('ships the overlay panels, painted in the theme colour, only for the cover family', async () => { const covered = await generator.generateProject( buildUidl({ preset: 'curtain', duration: 0.35, easing: 'ease-out' }), template() ) const wrapper = findFile(covered, 'components', 'tq-page-transition')?.content || '' expect(wrapper).toContain('function pageTransitionCover(') expect(wrapper).toContain('--dl-color-theme-primary1') expect(wrapper).toContain('data-tq-page-transition-panel') expect(wrapper).toContain("const PRESET = 'curtain'") // the panels stay closed until the arriving page's pictures are ready expect(wrapper).toContain("setPanelPhase('covered')") expect(wrapper).toContain( "setPanelPhase((phase) => (phase === 'covered' ? 'uncovering' : phase))" ) }) }) describe('Next generator flying picture', () => { const generator = createNextProjectGenerator() const config = { preset: 'slide-up', duration: 0.3, easing: 'ease-out' } it('plays the static export’s stylesheet, without opting every page load in', async () => { const output = await generator.generateProject(buildUidl(config), template()) const wrapper = findFile(output, 'components', 'tq-page-transition')?.content || '' const plan = PageTransition.viewTransitionPlan(config, { crossDocument: false }) expect(wrapper).toContain(`const VT_CSS = ${JSON.stringify(plan?.css)}`) expect(plan?.css).toContain('::view-transition-group(tq-morph)') expect(plan?.css).not.toContain('@view-transition') expect(wrapper).toContain('