import { describe, expect, test } from 'vitest'; import { HOST_BRIDGE_PUBLIC_WEB_ORIGIN, HOST_BRIDGE_PUBLIC_WEB_URL, HOST_BRIDGE_VERSION, } from '../../../../packages/shared/src/contracts/hostBridge'; import { ALLOWED_PRODUCTION_WEB_ORIGIN, DEFAULT_MOBILE_SHELL_WEB_URL, buildMobileShellUrl, resolveMobileShellBaseWebUrl, } from './url'; describe('buildMobileShellUrl', () => { test('默认 H5 地址指向真实主站', () => { expect(DEFAULT_MOBILE_SHELL_WEB_URL).toBe(HOST_BRIDGE_PUBLIC_WEB_URL); expect(ALLOWED_PRODUCTION_WEB_ORIGIN).toBe(HOST_BRIDGE_PUBLIC_WEB_ORIGIN); }); test('为 H5 附加原生移动壳上下文', () => { const url = new URL( buildMobileShellUrl('https://www.genarrative.world/works/detail?work=PZ-1', { platform: 'ios', hostVersion: '0.1.0', capabilities: ['host.getRuntime', 'share.open'], }), ); expect(url.searchParams.get('clientRuntime')).toBe('native_app'); expect(url.searchParams.get('clientType')).toBe('native_app'); expect(url.searchParams.get('hostShell')).toBe('expo_mobile'); expect(url.searchParams.get('hostPlatform')).toBe('ios'); expect(url.searchParams.get('hostVersion')).toBe('0.1.0'); expect(url.searchParams.get('bridgeVersion')).toBe( HOST_BRIDGE_VERSION.toString(), ); expect(url.searchParams.get('hostCapabilities')).toBe( 'host.getRuntime,share.open', ); expect(url.searchParams.get('work')).toBe('PZ-1'); }); test('附加宿主上下文前会清理旧移动壳 query', () => { const rawUrl = 'https://www.genarrative.world/works/detail?clientRuntime=browser&hostShell=old_shell&hostCapabilities=old&work=PZ-1'; const builtUrl = buildMobileShellUrl(rawUrl, { platform: 'android', hostVersion: '0.1.0', capabilities: ['host.getRuntime', 'share.open'], }); const url = new URL(builtUrl); expect(url.searchParams.get('work')).toBe('PZ-1'); expect(url.searchParams.get('clientRuntime')).toBe('native_app'); expect(url.searchParams.get('hostShell')).toBe('expo_mobile'); expect(url.searchParams.get('hostCapabilities')).toBe( 'host.getRuntime,share.open', ); expect(builtUrl.match(/clientRuntime=/g)).toHaveLength(1); expect(builtUrl.match(/hostShell=/g)).toHaveLength(1); expect(builtUrl.match(/hostCapabilities=/g)).toHaveLength(1); expect(builtUrl).not.toContain('clientRuntime=browser'); expect(builtUrl).not.toContain('hostShell=old_shell'); expect(builtUrl).not.toContain('hostCapabilities=old'); }); test('支持按平台注入不同能力清单', () => { const iosUrl = new URL( buildMobileShellUrl('https://www.genarrative.world/', { platform: 'ios', hostVersion: '0.1.0', capabilities: ['host.getRuntime', 'app.setBadgeCount'], }), ); const androidUrl = new URL( buildMobileShellUrl('https://www.genarrative.world/', { platform: 'android', hostVersion: '0.1.0', capabilities: ['host.getRuntime'], }), ); expect(iosUrl.searchParams.get('hostCapabilities')).toBe( 'host.getRuntime,app.setBadgeCount', ); expect(androidUrl.searchParams.get('hostCapabilities')).toBe( 'host.getRuntime', ); }); test('移动壳基准 URL 默认只接受生产主站', () => { expect( resolveMobileShellBaseWebUrl('https://www.genarrative.world/path'), ).toBe( 'https://www.genarrative.world/path', ); expect(resolveMobileShellBaseWebUrl('http://127.0.0.1:3000/')).toBe( DEFAULT_MOBILE_SHELL_WEB_URL, ); expect(resolveMobileShellBaseWebUrl('http://localhost:3000/')).toBe( DEFAULT_MOBILE_SHELL_WEB_URL, ); expect(resolveMobileShellBaseWebUrl('https://example.com/path')).toBe( DEFAULT_MOBILE_SHELL_WEB_URL, ); expect(resolveMobileShellBaseWebUrl('http://192.168.1.2:3000/')).toBe( DEFAULT_MOBILE_SHELL_WEB_URL, ); expect(resolveMobileShellBaseWebUrl('javascript:alert(1)')).toBe( DEFAULT_MOBILE_SHELL_WEB_URL, ); expect(resolveMobileShellBaseWebUrl('/relative/path')).toBe( DEFAULT_MOBILE_SHELL_WEB_URL, ); expect(resolveMobileShellBaseWebUrl('')).toBe(DEFAULT_MOBILE_SHELL_WEB_URL); expect(resolveMobileShellBaseWebUrl(null)).toBe( DEFAULT_MOBILE_SHELL_WEB_URL, ); }); test('移动壳基准 URL 只在开发模式接受本机入口', () => { const options = { allowLocalDevelopment: true, }; expect( resolveMobileShellBaseWebUrl(' http://127.0.0.1:3000/ ', options), ).toBe('http://127.0.0.1:3000/'); expect(resolveMobileShellBaseWebUrl('http://localhost:3000/', options)).toBe( 'http://localhost:3000/', ); expect(resolveMobileShellBaseWebUrl('http://[::1]:3000/', options)).toBe( 'http://[::1]:3000/', ); }); test('移动壳 URL 构建默认不会接受本机入口', () => { const url = new URL( buildMobileShellUrl('http://127.0.0.1:3000/works/detail?work=PZ-1', { platform: 'android', hostVersion: '0.1.0', capabilities: ['host.getRuntime'], }), ); expect(url.origin).toBe(HOST_BRIDGE_PUBLIC_WEB_ORIGIN); expect(url.pathname).toBe('/'); expect(url.searchParams.get('clientRuntime')).toBe('native_app'); }); test('移动壳 URL 构建只在显式开发模式接受本机入口', () => { const url = new URL( buildMobileShellUrl( 'http://127.0.0.1:3000/works/detail?work=PZ-1', { platform: 'android', hostVersion: '0.1.0', capabilities: ['host.getRuntime'], }, { allowLocalDevelopment: true, }, ), ); expect(url.origin).toBe('http://127.0.0.1:3000'); expect(url.pathname).toBe('/works/detail'); expect(url.searchParams.get('work')).toBe('PZ-1'); expect(url.searchParams.get('clientRuntime')).toBe('native_app'); }); test('非法启动 URL 回退到默认 H5 地址并继续附加宿主上下文', () => { const url = new URL( buildMobileShellUrl('javascript:alert(1)', { platform: 'android', hostVersion: '0.1.0', capabilities: ['host.getRuntime'], }), ); expect(url.toString().startsWith(DEFAULT_MOBILE_SHELL_WEB_URL)).toBe(true); expect(url.searchParams.get('clientRuntime')).toBe('native_app'); expect(url.searchParams.get('hostShell')).toBe('expo_mobile'); }); });