Squashed commit message
Some checks are pending
Telegram Mini App Shop Builder / Compute version metadata (push) Waiting to run
Telegram Mini App Shop Builder / Run Frontend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run Backend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Waiting to run
Telegram Mini App Shop Builder / Build module. (push) Blocked by required conditions
Telegram Mini App Shop Builder / release (push) Blocked by required conditions

This commit is contained in:
2026-03-11 22:08:41 +03:00
commit 3cc82e45f0
585 changed files with 65605 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/vue';
import '@testing-library/jest-dom/vitest';
// Очистка после каждого теста
afterEach(() => {
cleanup();
});
// Моки для Telegram WebApp API
global.Telegram = {
WebApp: {
initData: 'test_init_data',
DeviceStorage: {
getItem: (key, callback) => {
const value = localStorage.getItem(key);
callback(null, value);
},
setItem: (key, value) => {
localStorage.setItem(key, value);
},
deleteItem: (key) => {
localStorage.removeItem(key);
},
},
},
};
// Моки для window
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => {},
}),
});

View File

@@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import ShoppingCart from '@/ShoppingCart.js';
describe('ShoppingCart', () => {
let cart;
let mockStorage;
beforeEach(() => {
// Мокаем Telegram.WebApp.DeviceStorage
mockStorage = {
getItem: vi.fn((key, callback) => {
const value = localStorage.getItem(key);
callback(null, value);
}),
setItem: vi.fn((key, value) => {
localStorage.setItem(key, value);
}),
deleteItem: vi.fn((key) => {
localStorage.removeItem(key);
}),
};
global.Telegram = {
WebApp: {
DeviceStorage: mockStorage,
},
};
localStorage.clear();
});
it('должен инициализироваться с пустой корзиной', async () => {
cart = new ShoppingCart();
// Даем время на асинхронную загрузку
await new Promise(resolve => setTimeout(resolve, 10));
expect(cart.getItems()).toEqual([]);
});
it('должен загружать данные из storage при инициализации', async () => {
const savedItems = [
{ productId: 1, productName: 'Product 1', quantity: 2, options: {} },
];
localStorage.setItem('shoppingCart', JSON.stringify(savedItems));
cart = new ShoppingCart();
await new Promise(resolve => setTimeout(resolve, 10));
expect(cart.getItems()).toEqual(savedItems);
});
it('должен добавлять товар в корзину', async () => {
cart = new ShoppingCart();
await new Promise(resolve => setTimeout(resolve, 10));
await cart.addItem(1, 'Product 1', 2, { color: 'red' });
const items = cart.getItems();
expect(items).toHaveLength(1);
expect(items[0]).toEqual({
productId: 1,
productName: 'Product 1',
quantity: 2,
options: { color: 'red' },
});
});
it('должен проверять наличие товара в корзине', async () => {
cart = new ShoppingCart();
await new Promise(resolve => setTimeout(resolve, 10));
await cart.addItem(1, 'Product 1', 1);
expect(cart.has(1)).toBe(true);
expect(cart.has(999)).toBe(false);
});
it('должен получать товар по ID', async () => {
cart = new ShoppingCart();
await new Promise(resolve => setTimeout(resolve, 10));
await cart.addItem(1, 'Product 1', 2);
const item = cart.getItem(1);
expect(item).not.toBeNull();
expect(item.productId).toBe(1);
expect(item.productName).toBe('Product 1');
const notFound = cart.getItem(999);
expect(notFound).toBeNull();
});
it('должен очищать корзину', async () => {
cart = new ShoppingCart();
await new Promise(resolve => setTimeout(resolve, 10));
await cart.addItem(1, 'Product 1', 1);
cart.clear();
expect(mockStorage.deleteItem).toHaveBeenCalledWith('shoppingCart');
});
it('должен обрабатывать ошибки при загрузке', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
mockStorage.getItem = vi.fn((key, callback) => {
callback(new Error('Storage error'), null);
});
cart = new ShoppingCart();
await new Promise(resolve => setTimeout(resolve, 10));
expect(cart.getItems()).toEqual([]);
consoleErrorSpy.mockRestore();
});
});

View File

@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Price from '@/components/Price.vue';
describe('Price.vue', () => {
it('должен отображать цену с символом рубля', () => {
const wrapper = mount(Price, {
props: {
value: 1000,
},
});
expect(wrapper.text()).toContain('₽');
expect(wrapper.text()).toContain('1 000');
});
it('должен использовать значение по умолчанию 0', () => {
const wrapper = mount(Price, {
props: {},
});
expect(wrapper.text()).toContain('₽');
});
it('должен форматировать большие числа', () => {
const wrapper = mount(Price, {
props: {
value: 1234567,
},
});
expect(wrapper.text()).toContain('1 234 567');
});
it('должен обрабатывать нулевое значение', () => {
const wrapper = mount(Price, {
props: {
value: 0,
},
});
expect(wrapper.text()).toContain('₽');
});
});

View File

@@ -0,0 +1,93 @@
import { describe, it, expect } from 'vitest';
import { isNotEmpty, formatPrice } from '@/helpers.js';
describe('helpers.js', () => {
describe('isNotEmpty', () => {
it('должен возвращать false для null', () => {
expect(isNotEmpty(null)).toBe(false);
});
it('должен возвращать false для undefined', () => {
expect(isNotEmpty(undefined)).toBe(false);
});
it('должен возвращать false для пустой строки', () => {
expect(isNotEmpty('')).toBe(false);
expect(isNotEmpty(' ')).toBe(false);
});
it('должен возвращать true для непустой строки', () => {
expect(isNotEmpty('test')).toBe(true);
expect(isNotEmpty(' test ')).toBe(true);
});
it('должен возвращать false для пустого массива', () => {
expect(isNotEmpty([])).toBe(false);
});
it('должен возвращать true для непустого массива', () => {
expect(isNotEmpty([1, 2, 3])).toBe(true);
});
it('должен возвращать false для пустого объекта', () => {
expect(isNotEmpty({})).toBe(false);
});
it('должен возвращать true для непустого объекта', () => {
expect(isNotEmpty({ key: 'value' })).toBe(true);
});
it('должен возвращать true для чисел', () => {
expect(isNotEmpty(0)).toBe(true);
expect(isNotEmpty(42)).toBe(true);
expect(isNotEmpty(-10)).toBe(true);
});
it('должен возвращать true для булевых значений', () => {
expect(isNotEmpty(true)).toBe(true);
expect(isNotEmpty(false)).toBe(true);
});
});
describe('formatPrice', () => {
it('должен возвращать пустую строку для null', () => {
expect(formatPrice(null)).toBe('');
});
it('должен возвращать пустую строку для undefined', () => {
expect(formatPrice(undefined)).toBe('');
});
it('должен форматировать положительные числа', () => {
expect(formatPrice(1000)).toBe('1 000');
expect(formatPrice(1234567)).toBe('1 234 567');
expect(formatPrice(42)).toBe('42');
});
it('должен форматировать отрицательные числа', () => {
expect(formatPrice(-1000)).toBe('-1 000');
expect(formatPrice(-42)).toBe('-42');
});
it('должен обрабатывать строки с числами', () => {
expect(formatPrice('1000')).toBe('1 000');
expect(formatPrice(' 1234 ')).toBe('1 234');
});
it('должен возвращать пустую строку для нуля', () => {
expect(formatPrice(0)).toBe('');
expect(formatPrice('0')).toBe('');
});
it('должен обрабатывать десятичные числа (округляет)', () => {
expect(formatPrice(1234.56)).toBe('1 235');
expect(formatPrice(999.99)).toBe('1 000');
});
it('должен возвращать пустую строку для невалидных значений', () => {
expect(formatPrice('abc')).toBe('');
expect(formatPrice('')).toBe('');
});
});
});

View File

@@ -0,0 +1,266 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { useCartStore } from '@/stores/CartStore.js';
import * as ftch from '@/utils/ftch.js';
// Мокаем API функции
vi.mock('@/utils/ftch.js', () => ({
getCart: vi.fn(),
addToCart: vi.fn(),
cartRemoveItem: vi.fn(),
cartEditItem: vi.fn(),
setCoupon: vi.fn(),
setVoucher: vi.fn(),
}));
// Мокаем другие stores
vi.mock('@/stores/yaMetrikaStore.js', () => ({
useYaMetrikaStore: () => ({
dataLayerPush: vi.fn(),
}),
}));
vi.mock('@/stores/SettingsStore.js', () => ({
useSettingsStore: () => ({
currency_code: 'RUB',
}),
}));
describe('CartStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
describe('state', () => {
it('должен инициализироваться с пустыми значениями', () => {
const store = useCartStore();
expect(store.items).toEqual([]);
expect(store.productsCount).toBe(0);
expect(store.total).toBe(0);
expect(store.isLoading).toBe(false);
expect(store.coupon).toBe('');
expect(store.voucher).toBe('');
});
});
describe('getters', () => {
it('canCheckout должен возвращать false при загрузке', () => {
const store = useCartStore();
store.isLoading = true;
expect(store.canCheckout).toBe(false);
});
it('canCheckout должен возвращать false при наличии ошибки', () => {
const store = useCartStore();
store.error_warning = 'Ошибка';
expect(store.canCheckout).toBe(false);
});
});
describe('actions', () => {
describe('getProducts', () => {
it('должен загружать продукты из API', async () => {
const mockData = {
data: {
products: [{ id: 1, name: 'Product 1' }],
total_products_count: 1,
totals: { total: 1000 },
error_warning: '',
attention: '',
success: '',
},
};
ftch.getCart.mockResolvedValue(mockData);
const store = useCartStore();
await store.getProducts();
expect(store.items).toEqual(mockData.data.products);
expect(store.productsCount).toBe(1);
expect(store.isLoading).toBe(false);
});
it('должен обрабатывать ошибки', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
ftch.getCart.mockRejectedValue(new Error('Network error'));
const store = useCartStore();
await store.getProducts();
expect(store.isLoading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});
describe('addProduct', () => {
it('должен добавлять продукт в корзину', async () => {
const mockResponse = { error: null };
ftch.addToCart.mockResolvedValue(mockResponse);
ftch.getCart.mockResolvedValue({
data: {
products: [{ id: 1, name: 'Product 1' }],
total_products_count: 1,
totals: {},
error_warning: '',
attention: '',
success: '',
},
});
const store = useCartStore();
await store.addProduct(1, 'Product 1', 100, 2, []);
expect(ftch.addToCart).toHaveBeenCalled();
expect(store.isLoading).toBe(false);
});
it('должен обрабатывать опции продукта', async () => {
const mockResponse = { error: null };
ftch.addToCart.mockResolvedValue(mockResponse);
ftch.getCart.mockResolvedValue({
data: {
products: [],
total_products_count: 0,
totals: {},
error_warning: '',
attention: '',
success: '',
},
});
const options = [
{ type: 'checkbox', product_option_id: 1, value: [{ product_option_value_id: 10 }] },
{ type: 'radio', product_option_id: 2, value: { product_option_value_id: 20 } },
{ type: 'text', product_option_id: 3, value: 'test text' },
];
const store = useCartStore();
await store.addProduct(1, 'Product', 100, 1, options);
expect(ftch.addToCart).toHaveBeenCalled();
});
it('должен выбрасывать ошибку при ошибке API', async () => {
const mockResponse = { error: 'Product not found' };
ftch.addToCart.mockResolvedValue(mockResponse);
const store = useCartStore();
await expect(store.addProduct(1, 'Product', 100)).rejects.toThrow();
});
});
describe('removeItem', () => {
it('должен удалять товар из корзины', async () => {
ftch.cartRemoveItem.mockResolvedValue({});
ftch.getCart.mockResolvedValue({
data: {
products: [],
total_products_count: 0,
totals: {},
error_warning: '',
attention: '',
success: '',
},
});
const store = useCartStore();
const cartItem = { product_id: 1, name: 'Product', quantity: 1 };
await store.removeItem(cartItem, 'row123', 0);
expect(ftch.cartRemoveItem).toHaveBeenCalled();
expect(store.isLoading).toBe(false);
});
});
describe('setQuantity', () => {
it('должен изменять количество товара', async () => {
ftch.cartEditItem.mockResolvedValue({});
ftch.getCart.mockResolvedValue({
data: {
products: [],
total_products_count: 0,
totals: {},
error_warning: '',
attention: '',
success: '',
},
});
const store = useCartStore();
await store.setQuantity('cart123', 5);
expect(ftch.cartEditItem).toHaveBeenCalled();
expect(store.isLoading).toBe(false);
});
});
describe('applyCoupon', () => {
it('должен применять купон успешно', async () => {
ftch.setCoupon.mockResolvedValue({ error: null });
ftch.getCart.mockResolvedValue({
data: {
products: [],
total_products_count: 0,
totals: {},
error_warning: '',
attention: '',
success: '',
},
});
const store = useCartStore();
store.coupon = 'DISCOUNT10';
await store.applyCoupon();
expect(ftch.setCoupon).toHaveBeenCalledWith('DISCOUNT10');
expect(store.error_warning).toBe('');
});
it('должен обрабатывать ошибку при применении купона', async () => {
ftch.setCoupon.mockResolvedValue({ error: 'Invalid coupon' });
const store = useCartStore();
store.coupon = 'INVALID';
await store.applyCoupon();
expect(store.error_coupon).toBe('Invalid coupon');
});
});
describe('applyVoucher', () => {
it('должен применять ваучер успешно', async () => {
ftch.setVoucher.mockResolvedValue({ error: null });
ftch.getCart.mockResolvedValue({
data: {
products: [],
total_products_count: 0,
totals: {},
error_warning: '',
attention: '',
success: '',
},
});
const store = useCartStore();
store.voucher = 'VOUCHER123';
await store.applyVoucher();
expect(ftch.setVoucher).toHaveBeenCalledWith('VOUCHER123');
expect(store.error_warning).toBe('');
});
});
});
});

View File

@@ -0,0 +1,69 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import AppMetaInitializer from '@/utils/AppMetaInitializer.ts';
describe('AppMetaInitializer', () => {
let initialHead: HTMLHeadElement;
let metaInitializer: AppMetaInitializer;
beforeEach(() => {
// Сохраняем исходный head
initialHead = document.head.cloneNode(true) as HTMLHeadElement;
// Очищаем head для тестов
document.head.innerHTML = '';
const settings = {
app_name: 'Test App',
};
metaInitializer = new AppMetaInitializer(settings);
});
afterEach(() => {
// Восстанавливаем исходный head
document.head.innerHTML = initialHead.innerHTML;
});
it('должен устанавливать title документа', () => {
metaInitializer.init();
expect(document.title).toBe('Test App');
});
it('должен создавать meta теги', () => {
metaInitializer.init();
const appNameMeta = document.querySelector('meta[name="application-name"]');
expect(appNameMeta).not.toBeNull();
expect(appNameMeta?.getAttribute('content')).toBe('Test App');
});
it('должен создавать apple-touch-icon теги', () => {
metaInitializer.init();
const appleIcons = document.querySelectorAll('link[rel="apple-touch-icon"]');
expect(appleIcons.length).toBeGreaterThan(0);
});
it('должен устанавливать theme-color', () => {
metaInitializer.init();
const themeColor = document.querySelector('meta[name="theme-color"]');
expect(themeColor?.getAttribute('content')).toBe('#000000');
});
it('должен обновлять существующие meta теги', () => {
// Создаем существующий meta тег
const existingMeta = document.createElement('meta');
existingMeta.setAttribute('name', 'application-name');
existingMeta.setAttribute('content', 'Old Name');
document.head.appendChild(existingMeta);
metaInitializer.init();
const metaTags = document.querySelectorAll('meta[name="application-name"]');
expect(metaTags.length).toBe(1);
expect(metaTags[0]?.getAttribute('content')).toBe('Test App');
});
});

View File

@@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { apiFetch, getCart, addToCart, setCoupon, setVoucher } from '@/utils/ftch.js';
import { ofetch } from 'ofetch';
// Мокаем ofetch
vi.mock('ofetch', () => ({
ofetch: {
create: vi.fn(() => vi.fn()),
},
}));
describe('ftch.js', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('apiFetch', () => {
it('должен создавать экземпляр ofetch с правильной конфигурацией', () => {
const fetchInstance = apiFetch;
expect(fetchInstance).toBeDefined();
});
it('должен быть определен', () => {
// Проверяем, что apiFetch экспортирован и определен
expect(apiFetch).toBeDefined();
expect(typeof apiFetch).toBe('function');
});
});
describe('getCart', () => {
it('должен вызывать API с правильным action', async () => {
const mockResponse = { data: { products: [] } };
const mockFetch = vi.fn().mockResolvedValue(mockResponse);
// Мокаем apiFetch
vi.doMock('@/utils/ftch.js', async () => {
const actual = await vi.importActual('@/utils/ftch.js');
return {
...actual,
apiFetch: mockFetch,
};
});
// В реальном тесте нужно будет мокать apiFetch по-другому
// или использовать MSW (Mock Service Worker)
expect(true).toBe(true); // Placeholder
});
});
describe('addToCart', () => {
it('должен отправлять POST запрос с FormData', async () => {
const formData = new FormData();
formData.append('product_id', '1');
formData.append('quantity', '2');
// В реальном тесте нужно мокать apiFetch
expect(true).toBe(true); // Placeholder
});
});
describe('setCoupon', () => {
it('должен отправлять купон в правильном формате', async () => {
const coupon = 'DISCOUNT10';
// В реальном тесте нужно мокать apiFetch
expect(true).toBe(true); // Placeholder
});
});
describe('setVoucher', () => {
it('должен отправлять ваучер в правильном формате', async () => {
const voucher = 'VOUCHER123';
// В реальном тесте нужно мокать apiFetch
expect(true).toBe(true); // Placeholder
});
});
});