Files
portal/app/tests/Frontend/AdminSupplierIntegrationView.spec.ts
T
Дмитрий 1d18933d9e feat/supplier: экран отчёта о вечерней заливке (Эпик 5)
Владелец выбрал формат «экран в админке» (не письмо).
- SyncSupplierProjectsJob по завершении пишет строку-сводку в новую supplier_sync_runs
  (групп/синк/ручная/отложено/упало + status ok|partial|failed|aborted) через finally —
  пишется и при раннем abort (time-budget/mass-fail/auth).
- Эндпоинт GET /api/admin/supplier-integration/sync-runs + метод syncRuns.
- Экран SaaS-admin «Интеграция с поставщиком» → карточка «Вечерняя заливка проектов
  поставщику»: таблица заливок со статусом человеческим языком (Всё ровно/Частично/Сбой).
- Схема v8.55 +1 таблица (SaaS-level без RLS как supplier_csv_reconcile_log), миграция
  2026_06_25_130000, RLS-ревью 7/7. Проверено глазами в браузере (epic5-sync-runs-admin-screen.png).

Тесты: бэк 24/25 (1 skip) + фронт-экран 5/5 зелёные. Под LEFTHOOK=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:52:39 +03:00

106 lines
3.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import axios from 'axios';
import AdminSupplierIntegrationView from '../../resources/js/views/admin/AdminSupplierIntegrationView.vue';
vi.mock('axios');
const vuetify = createVuetify();
const healthPayload = {
health: { last_run_at: '2026-05-18T12:00:00Z', last_status: 'ok', drift_ratio: 0.02, webhook_state: 'live' },
history: [
{
started_at: '2026-05-18T12:00:00Z',
finished_at: '2026-05-18T12:01:00Z',
window_start: '2026-05-17T00:00:00Z',
window_end: '2026-05-18T12:00:00Z',
status: 'ok',
total_csv_rows: 100,
matched_count: 98,
recovered_count: 2,
drift_ratio: 0.02,
},
],
};
function mountView() {
return mount(AdminSupplierIntegrationView, { global: { plugins: [vuetify] } });
}
const syncRunsPayload = {
runs: [
{
started_at: '2026-06-25T15:05:00Z',
finished_at: '2026-06-25T15:47:00Z',
groups_total: 180,
synced_ok: 312,
manual_queued: 1,
deferred: 0,
failed: 0,
status: 'partial',
},
],
};
beforeEach(() => {
vi.clearAllMocks();
(axios.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
if (url === '/api/admin/supplier-integration/sync-runs') {
return Promise.resolve({ data: syncRunsPayload });
}
if (url === '/api/admin/supplier-integration/manual-queue') {
return Promise.resolve({ data: { queue: [] } });
}
if (url === '/api/admin/supplier-integration/export-mode') {
return Promise.resolve({ data: { mode: 'batch' } });
}
return Promise.resolve({ data: healthPayload });
});
(axios.post as ReturnType<typeof vi.fn>).mockResolvedValue({ data: { dispatched: true } });
});
describe('AdminSupplierIntegrationView', () => {
it('loads channel health on mount', async () => {
const wrapper = mountView();
await new Promise((r) => setTimeout(r, 0));
expect(axios.get).toHaveBeenCalledWith('/api/admin/supplier-integration');
expect(wrapper.text()).toContain('live');
});
it('renders reconcile history rows', async () => {
const wrapper = mountView();
await new Promise((r) => setTimeout(r, 0));
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain('100');
});
it('triggers manual reconcile on button click', async () => {
const wrapper = mountView();
await new Promise((r) => setTimeout(r, 0));
await wrapper.find('[data-test="reconcile-now"]').trigger('click');
expect(axios.post).toHaveBeenCalledWith('/api/admin/supplier-integration/reconcile');
});
// --- Эпик 5: история вечерних заливок ---
it('loads evening-upload history on mount', async () => {
mountView();
await new Promise((r) => setTimeout(r, 0));
expect(axios.get).toHaveBeenCalledWith('/api/admin/supplier-integration/sync-runs');
});
it('renders sync-runs table with totals + human status', async () => {
const wrapper = mountView();
await new Promise((r) => setTimeout(r, 0));
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="sync-runs-table"]').exists()).toBe(true);
const rows = wrapper.findAll('[data-testid="sync-run-row"]');
expect(rows.length).toBe(1);
expect(wrapper.text()).toContain('180'); // групп
expect(wrapper.text()).toContain('312'); // готово
expect(wrapper.text()).toContain('Частично'); // human status (partial)
});
});