Files
scrabble-game/ui/src/lib/vk.test.ts
T
Ilia Denisov db17287113 fix(ui): route external links out of the Android VK WebView
The Android VK client's WebView ignores target=_blank and navigates the
Mini App's own window to the target, stranding the player outside the
game with no way back (the dictionary lookup, About/Feedback links, the
ad banner and the bot-link modal fallbacks). vk-bridge 3.x has no method
to open an external URL, so external links are routed through VK's own
leave-VK redirect (vk.com/away.php), which the client intercepts
natively and hands to the system browser.

onExternalLinkClick moves from lib/telegram to a new lib/links that
composes the Telegram and VK routers; iOS and desktop VK open _blank
correctly and are left alone.
2026-07-02 00:09:52 +02:00

143 lines
5.0 KiB
TypeScript

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');
});
});