afa1a67ea1
Code-review fixes для commit 209eeac (G1 AdminPricingTiersView errors):
I-1 (mock leak risk): Добавлен afterEach(() => vi.clearAllMocks()) в
новый describe block. Раньше axios.isAxiosError.mockReturnValue(true)
оставался активным после run'а нового describe. Сейчас нет других
тестов после G1 describe в файле — но future-proof против перестановки
test order.
I-2 (coverage gap): Оба success теста (submit + confirmDelete) теперь
assert vm.successToastOpen === true. Раньше тест мог пройти, если
кто-то забыл successToastOpen.value = true в impl — message set, но
snackbar не открыт. Now covered.
Tests 9/9. Регрессий 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
8.7 KiB
TypeScript
166 lines
8.7 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { mount } from '@vue/test-utils';
|
|
import { createVuetify } from 'vuetify';
|
|
import axios from 'axios';
|
|
|
|
import AdminPricingTiersView from '../../resources/js/views/admin/AdminPricingTiersView.vue';
|
|
|
|
vi.mock('axios');
|
|
|
|
// Auto-импорт компонентов/директив Vuetify подхватывает vite-plugin-vuetify
|
|
// из vitest.config.ts (см. AdminBillingView.spec.ts).
|
|
const vuetify = createVuetify();
|
|
|
|
const mockTiers = [
|
|
{ tier_no: 1, leads_in_tier: 100, price_per_lead_kopecks: 50000, effective_from: '1970-01-01' },
|
|
{ tier_no: 2, leads_in_tier: 200, price_per_lead_kopecks: 45000, effective_from: '1970-01-01' },
|
|
{ tier_no: 3, leads_in_tier: 400, price_per_lead_kopecks: 40000, effective_from: '1970-01-01' },
|
|
{ tier_no: 4, leads_in_tier: 800, price_per_lead_kopecks: 35000, effective_from: '1970-01-01' },
|
|
{ tier_no: 5, leads_in_tier: 1500, price_per_lead_kopecks: 30000, effective_from: '1970-01-01' },
|
|
{ tier_no: 6, leads_in_tier: 3000, price_per_lead_kopecks: 27000, effective_from: '1970-01-01' },
|
|
{ tier_no: 7, leads_in_tier: null, price_per_lead_kopecks: 25000, effective_from: '1970-01-01' },
|
|
];
|
|
|
|
describe('AdminPricingTiersView', () => {
|
|
beforeEach(() => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.get as any).mockResolvedValue({ data: { data: { active: mockTiers, scheduled: {} } } });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.post as any).mockResolvedValue({ data: { effective_from: '2026-06-01' } });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.delete as any).mockResolvedValue({ data: { ok: true } });
|
|
});
|
|
|
|
it('renders 7 tier rows from /api/admin/pricing-tiers', async () => {
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
expect(wrapper.text()).toContain('500.00');
|
|
expect(wrapper.text()).toContain('250.00');
|
|
});
|
|
|
|
it('shows "все свыше" for tier 7 with leads_in_tier=null', async () => {
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
expect(wrapper.text()).toContain('все свыше');
|
|
});
|
|
|
|
it('opens editor dialog on button click', async () => {
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((wrapper.vm as any).editorOpen).toBe(false);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(wrapper.vm as any).editorOpen = true;
|
|
await wrapper.vm.$nextTick();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((wrapper.vm as any).editorOpen).toBe(true);
|
|
});
|
|
|
|
it('submits POST with editor payload', async () => {
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).submit();
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
'/api/admin/pricing-tiers',
|
|
expect.objectContaining({
|
|
tiers: expect.arrayContaining([expect.objectContaining({ tier_no: 7, leads_in_tier: null })]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('confirmDelete triggers DELETE to /scheduled/{date}', async () => {
|
|
window.confirm = vi.fn(() => true);
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).confirmDelete('2026-06-01');
|
|
expect(axios.delete).toHaveBeenCalledWith('/api/admin/pricing-tiers/scheduled/2026-06-01');
|
|
});
|
|
});
|
|
|
|
describe('AdminPricingTiersView error handling (Sprint 1 G1)', () => {
|
|
beforeEach(() => {
|
|
// axios.isAxiosError is auto-mocked as vi.fn() by vi.mock('axios').
|
|
// We need it to return true so extractErrorMessage() can read response.data.message.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.isAxiosError as any).mockReturnValue(true);
|
|
});
|
|
afterEach(() => {
|
|
// Cleanup mockReturnValue to prevent leak into other describe blocks (review I-1).
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('submit() shows errorMessage when axios.post rejects with 422', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.get as any).mockResolvedValue({ data: { data: { active: mockTiers, scheduled: {} } } });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.post as any).mockRejectedValue({
|
|
response: { status: 422, data: { message: 'Validation failed: tier 7 leads_in_tier must be null' } },
|
|
});
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// Open editor first (submit is called from dialog) so we can verify it stays open on error.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(wrapper.vm as any).editorOpen = true;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).submit();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
expect(vm.errorMessage).toContain('Validation failed');
|
|
expect(vm.saving).toBe(false);
|
|
// Dialog should remain OPEN so user can fix and retry
|
|
expect(vm.editorOpen).toBe(true);
|
|
});
|
|
|
|
it('submit() shows successMessage on 200', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.get as any).mockResolvedValue({ data: { data: { active: mockTiers, scheduled: {} } } });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.post as any).mockResolvedValue({ data: { effective_from: '2026-06-01' } });
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).submit();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
expect(vm.successMessage).toContain('Сохранено');
|
|
expect(vm.errorMessage).toBe(null);
|
|
expect(vm.saving).toBe(false);
|
|
expect(vm.editorOpen).toBe(false);
|
|
expect(vm.successToastOpen).toBe(true);
|
|
});
|
|
|
|
it('confirmDelete() shows errorMessage when axios.delete rejects', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.get as any).mockResolvedValue({ data: { data: { active: mockTiers, scheduled: {} } } });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.delete as any).mockRejectedValue({
|
|
response: { status: 500, data: { message: 'Database connection failed' } },
|
|
});
|
|
window.confirm = vi.fn(() => true);
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).confirmDelete('2026-06-01');
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((wrapper.vm as any).errorMessage).toContain('Database connection failed');
|
|
});
|
|
|
|
it('confirmDelete() shows successMessage on OK', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.get as any).mockResolvedValue({ data: { data: { active: mockTiers, scheduled: {} } } });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.delete as any).mockResolvedValue({ data: { ok: true } });
|
|
window.confirm = vi.fn(() => true);
|
|
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).confirmDelete('2026-06-01');
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
expect(vm.successMessage).toContain('Удалено');
|
|
expect(vm.successToastOpen).toBe(true);
|
|
});
|
|
});
|