import { afterEach, describe, expect, it, vi } from 'vitest'; import { insideVK, onVKPath, routeExternalLinkInVK, vkAndroidWebView, vkAppId, vkDiagLines, vkExternalBrowserUrl, vkLaunchDiag, vkLaunchParams, vkStartParam, appearanceForBg, } from './vk'; describe('vk launch detection', () => { afterEach(() => vi.unstubAllGlobals()); it('is not inside VK and not on the VK path without a location (node / SSR)', () => { expect(onVKPath()).toBe(false); expect(insideVK()).toBe(false); expect(vkLaunchParams()).toBe(''); }); it('detects the dedicated /vk/ entry path', () => { vi.stubGlobal('location', { pathname: '/vk/', search: '' }); expect(onVKPath()).toBe(true); vi.stubGlobal('location', { pathname: '/app/', search: '' }); expect(onVKPath()).toBe(false); }); it('returns the signed launch query only when a sign is present', () => { vi.stubGlobal('location', { pathname: '/vk/', search: '?vk_user_id=1&vk_ts=2&sign=abc' }); expect(vkLaunchParams()).toBe('vk_user_id=1&vk_ts=2&sign=abc'); expect(insideVK()).toBe(true); }); it('treats a URL carrying no sign as not a VK launch', () => { vi.stubGlobal('location', { pathname: '/vk/', search: '?utm_source=catalog' }); expect(vkLaunchParams()).toBe(''); expect(insideVK()).toBe(false); }); }); describe('vk launch params', () => { afterEach(() => vi.unstubAllGlobals()); it('reads the VK app id from the launch query', () => { vi.stubGlobal('location', { pathname: '/vk/', search: '?vk_app_id=6736218&sign=x' }); expect(vkAppId()).toBe('6736218'); }); it('reads the direct-link deep-link payload from the hash query param', () => { vi.stubGlobal('location', { pathname: '/vk/', search: '?vk_user_id=1&sign=x&hash=f123456' }); expect(vkStartParam()).toBe('f123456'); }); it('returns empty app id / start param when absent', () => { vi.stubGlobal('location', { pathname: '/vk/', search: '?vk_user_id=1&sign=x' }); expect(vkAppId()).toBe(''); expect(vkStartParam()).toBe(''); }); }); describe('vk external links', () => { afterEach(() => vi.unstubAllGlobals()); function stubVK(platform: string) { const open = vi.fn(); vi.stubGlobal('location', { pathname: '/vk/', search: `?vk_user_id=1&vk_platform=${platform}&sign=x`, href: 'https://app.example/vk/', origin: 'https://app.example', }); vi.stubGlobal('window', { open }); return open; } it('wraps a URL in the away.php redirect with the target encoded', () => { const url = 'https://gramota.ru/poisk?query=кот&mode=slovari'; expect(vkExternalBrowserUrl(url)).toBe(`https://vk.com/away.php?to=${encodeURIComponent(url)}`); }); it('detects the Android VK WebView from vk_platform', () => { stubVK('mobile_android'); expect(vkAndroidWebView()).toBe(true); stubVK('mobile_android_messenger'); expect(vkAndroidWebView()).toBe(true); stubVK('mobile_iphone'); expect(vkAndroidWebView()).toBe(false); stubVK('desktop_web'); expect(vkAndroidWebView()).toBe(false); }); it('is not the Android WebView without a signed VK launch', () => { vi.stubGlobal('location', { pathname: '/vk/', search: '?vk_platform=mobile_android' }); expect(vkAndroidWebView()).toBe(false); }); it('routes an external _blank link through the away redirect on Android', () => { const open = stubVK('mobile_android'); expect(routeExternalLinkInVK({ href: 'https://gramota.ru/poisk?query=кот', target: '_blank' })).toBe(true); expect(open).toHaveBeenCalledWith( `https://vk.com/away.php?to=${encodeURIComponent('https://gramota.ru/poisk?query=кот')}`, '_blank', ); }); it('leaves same-origin and non-_blank links to the browser', () => { const open = stubVK('mobile_android'); expect(routeExternalLinkInVK({ href: 'https://app.example/lobby', target: '_blank' })).toBe(false); expect(routeExternalLinkInVK({ href: 'https://gramota.ru', target: '' })).toBe(false); expect(open).not.toHaveBeenCalled(); }); it('does nothing outside the Android VK client (iOS / desktop open _blank fine)', () => { const open = stubVK('mobile_iphone'); expect(routeExternalLinkInVK({ href: 'https://gramota.ru', target: '_blank' })).toBe(false); expect(open).not.toHaveBeenCalled(); }); }); describe('vkDiagLines (launch diagnostic)', () => { it('reports an unsigned browser open: no sign, every VK param missing', () => { const lines = vkDiagLines('utm_source=catalog', false, ''); expect(lines[0]).toBe('launch: unsigned iframe: no'); expect(lines[1]).toBe('vk-platform: —'); expect(lines[2]).toBe('params: utm_source'); expect(lines[3]).toBe('missing: vk_app_id,vk_user_id,vk_ts,vk_platform,sign'); expect(lines[4]).toBe('referrer: —'); }); it('reports a signed launch and never leaks the sign value, only its name', () => { const lines = vkDiagLines( 'vk_app_id=6736218&vk_platform=mobile_android&vk_user_id=42&vk_ts=1700000000&sign=SECRETSIG', true, 'vk.com', ); expect(lines[0]).toBe('launch: signed iframe: yes'); expect(lines[1]).toBe('vk-platform: mobile_android'); expect(lines[3]).toBe('missing: —'); expect(lines[4]).toBe('referrer: vk.com'); // The signature is auth material: its NAME may appear, its VALUE must never. const report = lines.join('\n'); expect(report).toContain('sign'); expect(report).not.toContain('SECRETSIG'); }); }); describe('vkLaunchDiag', () => { afterEach(() => vi.unstubAllGlobals()); it('builds a VK launch diagnostic with no Retry (VK signs the URL at load, nothing to wait for)', () => { vi.stubGlobal('location', { pathname: '/vk/', search: '?utm_source=catalog' }); const d = vkLaunchDiag(); expect(d.platform).toBe('vk'); expect(d.retry).toBe(false); expect(d.report).toContain('launch: unsigned'); }); }); describe('appearanceForBg', () => { it('maps a dark background to the dark appearance (light status-bar icons)', () => { expect(appearanceForBg('#0f1115')).toBe('dark'); expect(appearanceForBg('#171a21')).toBe('dark'); }); it('maps a light background to the light appearance (dark status-bar icons)', () => { expect(appearanceForBg('#f3f4f6')).toBe('light'); expect(appearanceForBg('#ffffff')).toBe('light'); }); it('accepts shorthand hex and surrounding whitespace (a raw CSS custom-property value)', () => { expect(appearanceForBg('#000')).toBe('dark'); expect(appearanceForBg(' #fff ')).toBe('light'); }); it('defaults to light for an unparseable colour', () => { expect(appearanceForBg('')).toBe('light'); expect(appearanceForBg('rgb(0, 0, 0)')).toBe('light'); }); });