import { afterEach, describe, expect, it, vi } from 'vitest'; import { insideVK, onVKPath, routeExternalLinkInVK, vkAndroidWebView, vkAppId, vkExternalBrowserUrl, 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('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'); }); });