chore: Remove unused testing dependencies and configuration files to streamline the project and reduce clutter in the codebase.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m33s

This commit is contained in:
2025-12-24 16:22:57 +07:00
parent bda9b4fc27
commit d7c769c655
7 changed files with 2 additions and 594 deletions
+2 -9
View File
@@ -9,10 +9,7 @@
"build:check": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@tabler/core": "^1.3.2",
@@ -27,8 +24,6 @@
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2",
@@ -36,10 +31,8 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"typescript": "^5.7.2",
"vite": "^7.0.0",
"vitest": "^2.1.8"
"vite": "^7.0.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-musl": "4.40.0"
-141
View File
@@ -1,141 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import {
useDomains,
useIpRanges,
useAsns,
useServers,
useCommunities,
queryKeys,
} from './useApiQuery';
// Мокаем api
vi.mock('../lib/api', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
},
}));
import api from '../lib/api';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
return ({ children }: { children: React.ReactNode }) => (
React.createElement(QueryClientProvider, { client: queryClient }, children)
);
};
describe('useApiQuery hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useDomains', () => {
it('загружает домены успешно', async () => {
const mockDomains = [
{ domain: 'example.com', type: '120' },
{ domain: 'test.com', type: '130' },
];
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
data: { items: mockDomains },
headers: { etag: 'abc123', 'last-modified': '2024-01-01' },
});
const { result } = renderHook(() => useDomains(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.items).toEqual(mockDomains);
expect(result.current.data?.etag).toBe('abc123');
});
it('обрабатывает пустой ответ', async () => {
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
data: { items: null },
headers: {},
});
const { result } = renderHook(() => useDomains(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.items).toEqual([]);
});
});
describe('useServers', () => {
it('загружает серверы успешно', async () => {
const mockServers = [
{ id: '1', ip: '192.168.1.1', dns: 'srv1.example.com' },
{ id: '2', ip: '192.168.1.2', dns: 'srv2.example.com' },
];
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
data: mockServers,
headers: { etag: 'xyz789' },
});
const { result } = renderHook(() => useServers(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.items).toEqual(mockServers);
});
});
describe('useCommunities', () => {
it('загружает справочник communities', async () => {
const mockCommunities = [
{ value: '120', name: 'Cloudflare' },
{ value: '130', name: 'Telegram' },
];
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
data: mockCommunities,
});
const { result } = renderHook(() => useCommunities(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockCommunities);
});
});
describe('queryKeys', () => {
it('генерирует правильные ключи', () => {
expect(queryKeys.domains).toEqual(['domains']);
expect(queryKeys.servers).toEqual(['servers']);
expect(queryKeys.serverFilters('srv-1')).toEqual(['serverFilters', 'srv-1']);
});
});
});
-122
View File
@@ -1,122 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
formatDateTime,
formatDateTimeShort,
formatTime,
formatRelative,
formatDateTimeWithRelative,
now,
} from './datetime';
describe('datetime utilities', () => {
beforeEach(() => {
// Фиксируем время для предсказуемых тестов
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-12-24T15:30:45.000Z'));
});
afterEach(() => {
vi.useRealTimers();
});
describe('formatDateTime', () => {
it('форматирует Date объект', () => {
const date = new Date('2024-12-24T15:30:45.000Z');
// Результат зависит от часового пояса, проверяем формат
const result = formatDateTime(date);
expect(result).toMatch(/^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2}$/);
});
it('форматирует строку даты', () => {
const result = formatDateTime('2024-12-24T15:30:45.000Z');
expect(result).toMatch(/^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2}$/);
});
it('возвращает пустую строку для null/undefined', () => {
expect(formatDateTime(null)).toBe('');
expect(formatDateTime(undefined)).toBe('');
expect(formatDateTime('')).toBe('');
});
it('возвращает пустую строку для невалидной даты', () => {
expect(formatDateTime('invalid-date')).toBe('');
});
});
describe('formatDateTimeShort', () => {
it('форматирует без секунд', () => {
const date = new Date('2024-12-24T15:30:45.000Z');
const result = formatDateTimeShort(date);
expect(result).toMatch(/^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}$/);
expect(result).not.toContain(':45');
});
it('возвращает пустую строку для null', () => {
expect(formatDateTimeShort(null)).toBe('');
});
});
describe('formatTime', () => {
it('возвращает только время', () => {
const date = new Date('2024-12-24T15:30:45.000Z');
const result = formatTime(date);
expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/);
});
it('возвращает пустую строку для null', () => {
expect(formatTime(null)).toBe('');
});
});
describe('formatRelative', () => {
it('возвращает "только что" для недавних дат', () => {
const now = new Date();
expect(formatRelative(now)).toBe('только что');
});
it('возвращает секунды', () => {
const date = new Date(Date.now() - 30 * 1000); // 30 секунд назад
expect(formatRelative(date)).toBe('30 сек назад');
});
it('возвращает минуты', () => {
const date = new Date(Date.now() - 5 * 60 * 1000); // 5 минут назад
expect(formatRelative(date)).toBe('5 мин назад');
});
it('возвращает часы', () => {
const date = new Date(Date.now() - 3 * 60 * 60 * 1000); // 3 часа назад
expect(formatRelative(date)).toBe('3 ч назад');
});
it('возвращает дни', () => {
const date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); // 2 дня назад
expect(formatRelative(date)).toBe('2 дн назад');
});
it('возвращает пустую строку для null', () => {
expect(formatRelative(null)).toBe('');
});
});
describe('formatDateTimeWithRelative', () => {
it('комбинирует абсолютное и относительное время', () => {
const date = new Date(Date.now() - 5 * 60 * 1000); // 5 минут назад
const result = formatDateTimeWithRelative(date);
expect(result).toContain('(5 мин назад)');
expect(result).toMatch(/^\d{2}\.\d{2}\.\d{4}/);
});
it('возвращает пустую строку для null', () => {
expect(formatDateTimeWithRelative(null)).toBe('');
});
});
describe('now', () => {
it('возвращает текущую дату в правильном формате', () => {
const result = now();
expect(result).toMatch(/^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2}$/);
});
});
});
-49
View File
@@ -1,49 +0,0 @@
import '@testing-library/jest-dom';
import { afterEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
// Очистка после каждого теста
afterEach(() => {
cleanup();
});
// Мок для localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
// Мок для matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Мок для ResizeObserver
class ResizeObserverMock {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
Object.defineProperty(window, 'ResizeObserver', { value: ResizeObserverMock });
// Мок для clipboard
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: vi.fn().mockResolvedValue(undefined),
readText: vi.fn().mockResolvedValue(''),
},
});
-110
View File
@@ -1,110 +0,0 @@
import { describe, it, expect } from 'vitest';
import {
isValidIPv4,
isValidIPv6,
isValidIP,
isValidCIDR,
isValidIPRange,
normalizeIP,
ipToNumber,
compareIPs,
} from './ipUtils';
describe('ipUtils', () => {
describe('isValidIPv4', () => {
it('валидирует корректные IPv4 адреса', () => {
expect(isValidIPv4('192.168.1.1')).toBe(true);
expect(isValidIPv4('10.0.0.0')).toBe(true);
expect(isValidIPv4('255.255.255.255')).toBe(true);
expect(isValidIPv4('0.0.0.0')).toBe(true);
});
it('отклоняет некорректные IPv4 адреса', () => {
expect(isValidIPv4('256.1.1.1')).toBe(false);
expect(isValidIPv4('1.1.1')).toBe(false);
expect(isValidIPv4('1.1.1.1.1')).toBe(false);
expect(isValidIPv4('abc.def.ghi.jkl')).toBe(false);
expect(isValidIPv4('')).toBe(false);
});
});
describe('isValidIPv6', () => {
it('валидирует корректные IPv6 адреса', () => {
expect(isValidIPv6('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(true);
expect(isValidIPv6('2001:db8:85a3::8a2e:370:7334')).toBe(true);
expect(isValidIPv6('::1')).toBe(true);
expect(isValidIPv6('::')).toBe(true);
expect(isValidIPv6('fe80::1')).toBe(true);
});
it('отклоняет некорректные IPv6 адреса', () => {
expect(isValidIPv6('gggg::1')).toBe(false);
expect(isValidIPv6('2001:db8')).toBe(false);
expect(isValidIPv6('')).toBe(false);
});
});
describe('isValidIP', () => {
it('валидирует и IPv4 и IPv6', () => {
expect(isValidIP('192.168.1.1')).toBe(true);
expect(isValidIP('::1')).toBe(true);
expect(isValidIP('invalid')).toBe(false);
});
});
describe('isValidCIDR', () => {
it('валидирует корректные CIDR нотации', () => {
expect(isValidCIDR('192.168.1.0/24')).toBe(true);
expect(isValidCIDR('10.0.0.0/8')).toBe(true);
expect(isValidCIDR('2001:db8::/32')).toBe(true);
});
it('отклоняет некорректные CIDR нотации', () => {
expect(isValidCIDR('192.168.1.0/33')).toBe(false);
expect(isValidCIDR('192.168.1.0')).toBe(false);
expect(isValidCIDR('invalid/24')).toBe(false);
});
});
describe('isValidIPRange', () => {
it('валидирует IP адреса и CIDR', () => {
expect(isValidIPRange('192.168.1.1')).toBe(true);
expect(isValidIPRange('192.168.1.0/24')).toBe(true);
});
it('отклоняет некорректный ввод', () => {
expect(isValidIPRange('invalid')).toBe(false);
});
});
describe('normalizeIP', () => {
it('нормализует IPv4', () => {
expect(normalizeIP('192.168.001.001')).toBe('192.168.1.1');
});
it('возвращает как есть при ошибке', () => {
expect(normalizeIP('invalid')).toBe('invalid');
});
});
describe('ipToNumber', () => {
it('конвертирует IPv4 в число', () => {
expect(ipToNumber('0.0.0.1')).toBe(1);
expect(ipToNumber('0.0.1.0')).toBe(256);
expect(ipToNumber('192.168.1.1')).toBe(3232235777);
});
it('возвращает 0 для невалидного IP', () => {
expect(ipToNumber('invalid')).toBe(0);
});
});
describe('compareIPs', () => {
it('сравнивает IP адреса', () => {
expect(compareIPs('192.168.1.1', '192.168.1.2')).toBeLessThan(0);
expect(compareIPs('192.168.1.2', '192.168.1.1')).toBeGreaterThan(0);
expect(compareIPs('192.168.1.1', '192.168.1.1')).toBe(0);
});
});
});
-129
View File
@@ -1,129 +0,0 @@
import { describe, it, expect } from 'vitest';
import {
makeGateway,
normalizeGateways,
countryToFlag,
needsGateways,
SERVER_TYPE_OPTIONS,
normalizeCommunityValue,
getShortCommunityValue,
} from './serverUtils';
describe('serverUtils', () => {
describe('makeGateway', () => {
it('создаёт gateway с дефолтными значениями', () => {
const gw = makeGateway();
expect(gw).toHaveProperty('id');
expect(gw.name).toBe('');
expect(gw.ip).toBe('');
expect(gw.comment).toBe('');
expect(gw.primary).toBe(false);
});
it('создаёт gateway с переопределёнными значениями', () => {
const gw = makeGateway({ name: 'Test', primary: true });
expect(gw.name).toBe('Test');
expect(gw.primary).toBe(true);
});
it('генерирует уникальные id', () => {
const gw1 = makeGateway();
const gw2 = makeGateway();
expect(gw1.id).not.toBe(gw2.id);
});
});
describe('normalizeGateways', () => {
it('возвращает пустой массив для undefined', () => {
expect(normalizeGateways(undefined, '')).toEqual([]);
});
it('возвращает массив как есть, если валидный', () => {
const gateways = [{ id: '1', name: 'GW1', ip: '10.0.0.1', primary: true }];
const result = normalizeGateways(gateways, '');
expect(result).toHaveLength(1);
expect(result[0].name).toBe('GW1');
});
it('добавляет fallbackName если name пустое', () => {
const gateways = [{ id: '1', name: '', ip: '10.0.0.1', primary: true }];
const result = normalizeGateways(gateways, 'Fallback');
expect(result[0].name).toBe('Fallback');
});
});
describe('countryToFlag', () => {
it('конвертирует код страны в эмодзи флаг', () => {
expect(countryToFlag('RU')).toBe('🇷🇺');
expect(countryToFlag('US')).toBe('🇺🇸');
expect(countryToFlag('DE')).toBe('🇩🇪');
});
it('обрабатывает нижний регистр', () => {
expect(countryToFlag('ru')).toBe('🇷🇺');
});
it('возвращает пустую строку для пустого ввода', () => {
expect(countryToFlag('')).toBe('');
expect(countryToFlag(null as any)).toBe('');
});
});
describe('needsGateways', () => {
it('возвращает true для jumphost', () => {
expect(needsGateways('jumphost')).toBe(true);
});
it('возвращает true для exit', () => {
expect(needsGateways('exit')).toBe(true);
});
it('возвращает false для других типов', () => {
expect(needsGateways('unknown')).toBe(false);
expect(needsGateways('')).toBe(false);
});
it('работает независимо от регистра', () => {
expect(needsGateways('JUMPHOST')).toBe(true);
expect(needsGateways('Exit')).toBe(true);
});
});
describe('SERVER_TYPE_OPTIONS', () => {
it('содержит jumphost и exit', () => {
expect(SERVER_TYPE_OPTIONS).toContainEqual({ value: 'jumphost', label: 'Jumphost' });
expect(SERVER_TYPE_OPTIONS).toContainEqual({ value: 'exit', label: 'Выходная нода' });
});
});
describe('normalizeCommunityValue', () => {
it('добавляет baseAS если нет двоеточия', () => {
expect(normalizeCommunityValue('120', '65001')).toBe('65001:120');
});
it('оставляет как есть если уже есть двоеточие', () => {
expect(normalizeCommunityValue('65000:120', '65001')).toBe('65000:120');
});
it('возвращает пустую строку для пустого ввода', () => {
expect(normalizeCommunityValue('', '65001')).toBe('');
expect(normalizeCommunityValue(' ', '65001')).toBe('');
});
});
describe('getShortCommunityValue', () => {
it('извлекает часть после двоеточия', () => {
expect(getShortCommunityValue('65001:120')).toBe('120');
});
it('возвращает как есть если нет двоеточия', () => {
expect(getShortCommunityValue('120')).toBe('120');
});
it('возвращает пустую строку для пустого ввода', () => {
expect(getShortCommunityValue('')).toBe('');
expect(getShortCommunityValue(' ')).toBe('');
});
});
});
-34
View File
@@ -1,34 +0,0 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{js,jsx,ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.{js,jsx,ts,tsx}'],
exclude: [
'src/test/**',
'src/**/*.d.ts',
'src/main.jsx',
'src/types/**',
],
},
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@components': resolve(__dirname, './src/components'),
'@hooks': resolve(__dirname, './src/hooks'),
'@lib': resolve(__dirname, './src/lib'),
'@utils': resolve(__dirname, './src/utils'),
},
},
});