import { describe, expect, test } from 'vitest'; import { buildMobileShellUrlFromDeepLink, resolveMobileShellUrlFromDeepLink, } from './deepLink'; const options = { platform: 'ios' as const, hostVersion: '0.1.0', capabilities: ['host.getRuntime' as const], }; describe('buildMobileShellUrlFromDeepLink', () => { test('把原生 scheme deep link 映射成同源 H5 目标页', () => { const url = new URL( buildMobileShellUrlFromDeepLink( 'genarrative://open/works/detail?work=PZ-1', 'https://www.genarrative.world/', options, ), ); expect(url.origin).toBe('https://www.genarrative.world'); expect(url.pathname).toBe('/works/detail'); expect(url.searchParams.get('work')).toBe('PZ-1'); expect(url.searchParams.get('clientRuntime')).toBe('native_app'); expect(url.searchParams.get('hostShell')).toBe('expo_mobile'); }); test('把同源 universal link 映射成带宿主上下文的 H5 URL', () => { const url = new URL( buildMobileShellUrlFromDeepLink( 'https://www.genarrative.world/creation/puzzle#draft', 'https://www.genarrative.world/', options, ), ); expect(url.pathname).toBe('/creation/puzzle'); expect(url.hash).toBe('#draft'); expect(url.searchParams.get('hostCapabilities')).toBe('host.getRuntime'); }); test('外域和危险协议回退到默认首页', () => { const external = new URL( buildMobileShellUrlFromDeepLink( 'https://example.com/works/detail?work=PZ-1', 'https://www.genarrative.world/', options, ), ); const unsafe = new URL( buildMobileShellUrlFromDeepLink( 'javascript:alert(1)', 'https://www.genarrative.world/', options, ), ); expect(external.origin).toBe('https://www.genarrative.world'); expect(external.pathname).toBe('/'); expect(unsafe.origin).toBe('https://www.genarrative.world'); expect(unsafe.pathname).toBe('/'); }); test('协议相对外域 URL 不会被装进移动壳 WebView', () => { const url = new URL( buildMobileShellUrlFromDeepLink( '//example.com/works/detail?work=PZ-1', 'https://www.genarrative.world/', options, ), ); expect(url.origin).toBe('https://www.genarrative.world'); expect(url.pathname).toBe('/'); }); test('基准 H5 URL 配置非法或外域时回退到默认启动地址', () => { const url = new URL( buildMobileShellUrlFromDeepLink( 'genarrative://open/works/detail?work=PZ-1', 'https://example.com/app', options, ), ); expect(url.origin).toBe('https://www.genarrative.world'); expect(url.pathname).toBe('/works/detail'); expect(url.searchParams.get('work')).toBe('PZ-1'); expect(url.searchParams.get('clientRuntime')).toBe('native_app'); }); test('基准 H5 URL 只在显式开发模式保留本机 deep link 入口', () => { const productionUrl = new URL( buildMobileShellUrlFromDeepLink( 'http://127.0.0.1:3000/works/detail?work=PZ-1', 'http://127.0.0.1:3000/', options, ), ); const developmentUrl = new URL( buildMobileShellUrlFromDeepLink( 'http://127.0.0.1:3000/works/detail?work=PZ-1', 'http://127.0.0.1:3000/', options, { allowLocalDevelopment: true, }, ), ); expect(productionUrl.origin).toBe('https://www.genarrative.world'); expect(productionUrl.pathname).toBe('/'); expect(developmentUrl.origin).toBe('http://127.0.0.1:3000'); expect(developmentUrl.pathname).toBe('/works/detail'); expect(developmentUrl.searchParams.get('work')).toBe('PZ-1'); expect(developmentUrl.searchParams.get('clientRuntime')).toBe('native_app'); }); test('返回 deep link 解析状态用于壳层记录拒绝路径', () => { const mapped = resolveMobileShellUrlFromDeepLink( 'genarrative://open/works/detail?work=PZ-1', 'https://www.genarrative.world/', options, ); const rejected = resolveMobileShellUrlFromDeepLink( 'https://example.com/works/detail?work=PZ-1', 'https://www.genarrative.world/', options, ); const empty = resolveMobileShellUrlFromDeepLink( null, 'https://www.genarrative.world/', options, ); expect(mapped.status).toBe('mapped'); expect(new URL(mapped.url).pathname).toBe('/works/detail'); expect(rejected.status).toBe('rejected'); expect(new URL(rejected.url).pathname).toBe('/'); expect(empty.status).toBe('default'); expect(new URL(empty.url).pathname).toBe('/'); }); });