da65cf4bf7
- AdminLayout: отдельный sidebar теало-нуар с под-брендом ADMIN (red error 10px JBM uppercase) + 4 nav (Тенанты 142 / Биллинг / Инциденты 3 / Система) + topbar с crumb «Админка → currentPage» + admin-user-chip с error-color avatar. - AdminTenantsView (/admin/tenants): page-head + 5-stats + Экспорт + search/Статус/Тариф фильтры + v-data-table 7 колонок (Тенант с двухстрочным name+inn / Статус-chip 4 цвета / Тариф / Баланс ₽ с error-color при <0 / Желаем×факт / MRR с «—» / Активность). - mockTenants.ts соответствует schema v8.7 §3: 4 статуса × 5 тарифов, 7 mock с разнообразием (active/trial/overdue/suspended) + AdminStats (142/128/9/5/ 1 248 600 ₽). - AdminPlaceholderView универсальный для Биллинг/Инциденты/Система с описаниями ссылающимися на schema v8.7 (incidents_log §9 / system_settings §10). - AppShell расширен meta.layout='admin'. Router: /admin redirect на /tenants + 4 admin-route'а с lazy-imports. Web.php fallback покрывает /admin/*. - cspell-words.txt: Екб. Vitest +11 (всего 129/129 за 10.02s): - заголовок + 5 stats (regex nbsp в 1 248 600 ₽) + 7 columns + 7 rows + Окна Москва ИНН + overdue −1 200 + trial 4 дня + suspended + search filter «Натяжные» → 1 row + Экспорт/Статус/Тариф кнопки. Регресс: lint+type+format OK; vitest 129/129; vite build (admin views в lazy-chunks; main 104.99 KB); story:build 21/28 за 30.32s; Pest 48/48 за 4.89s. CLAUDE.md v1.30->v1.31, реестр Открытых_вопросов v1.39->v1.40. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
104 lines
4.7 KiB
TypeScript
104 lines
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import { mount } from '@vue/test-utils';
|
||
import { createVuetify } from 'vuetify';
|
||
import AdminTenantsView from '../../resources/js/views/admin/AdminTenantsView.vue';
|
||
import { MOCK_STATS, MOCK_TENANTS } from '../../resources/js/composables/mockTenants';
|
||
|
||
describe('AdminTenantsView.vue', () => {
|
||
const factory = () =>
|
||
mount(AdminTenantsView, {
|
||
global: { plugins: [createVuetify()] },
|
||
});
|
||
|
||
it('монтируется и содержит заголовок «Тенанты»', () => {
|
||
const wrapper = factory();
|
||
expect(wrapper.find('h1').text()).toBe('Тенанты');
|
||
});
|
||
|
||
it('показывает 5 stats: всего/активны/trial/просрочка/выручка', () => {
|
||
const wrapper = factory();
|
||
const text = wrapper.text();
|
||
expect(text).toContain(`${MOCK_STATS.total}`); // 142
|
||
expect(text).toContain('всего');
|
||
expect(text).toContain(`${MOCK_STATS.active}`); // 128
|
||
expect(text).toContain('активны');
|
||
expect(text).toContain(`${MOCK_STATS.trial}`); // 9
|
||
expect(text).toContain('trial');
|
||
expect(text).toContain(`${MOCK_STATS.overdue}`); // 5
|
||
expect(text).toContain('просрочка');
|
||
expect(text).toContain('выручка месяц');
|
||
// 1 248 600 ₽
|
||
expect(text).toMatch(/1\s+248\s+600\s*₽/);
|
||
});
|
||
|
||
it('таблица содержит 7 колонок (Тенант/Статус/Тариф/Баланс/Желаем×факт/MRR/Активность)', () => {
|
||
const wrapper = factory();
|
||
const headers = wrapper.findAll('thead th').map((h) => h.text());
|
||
['Тенант', 'Статус', 'Тариф', 'Баланс', 'Желаем×факт', 'MRR', 'Активность'].forEach((label) => {
|
||
expect(headers.some((h) => h.includes(label))).toBe(true);
|
||
});
|
||
});
|
||
|
||
it('рендерит все 7 mock-tenants', () => {
|
||
const wrapper = factory();
|
||
const rows = wrapper.findAll('tbody tr');
|
||
expect(rows.length).toBe(MOCK_TENANTS.length);
|
||
});
|
||
|
||
it('первая строка — Окна Москва ООО + ИНН + Активен + Команда', () => {
|
||
const wrapper = factory();
|
||
const text = wrapper.text();
|
||
expect(text).toContain('Окна Москва ООО');
|
||
expect(text).toContain('ИНН 7724444444');
|
||
expect(text).toContain('Активен');
|
||
expect(text).toContain('Команда');
|
||
});
|
||
|
||
it('overdue-тенант (Двери Премиум) показывает «Просрочка 3 дня» + отрицательный баланс', () => {
|
||
const wrapper = factory();
|
||
const text = wrapper.text();
|
||
expect(text).toContain('Двери Премиум');
|
||
expect(text).toContain('Просрочка 3 дня');
|
||
expect(text).toMatch(/−1\s+200/); // -1200 без 0 ₽
|
||
});
|
||
|
||
it('trial-тенант (Ремонт под ключ) показывает «Trial · 4 дня» + MRR=—', () => {
|
||
const wrapper = factory();
|
||
const text = wrapper.text();
|
||
expect(text).toContain('Ремонт под ключ');
|
||
expect(text).toContain('Trial · 4 дня');
|
||
});
|
||
|
||
it('suspended-тенант (Оконные системы РФ) показывает «Приостановлен»', () => {
|
||
const wrapper = factory();
|
||
const text = wrapper.text();
|
||
expect(text).toContain('Оконные системы РФ');
|
||
expect(text).toContain('Приостановлен');
|
||
});
|
||
|
||
it('содержит search-input с placeholder «ИНН, юр. лицо, email админа…»', () => {
|
||
const wrapper = factory();
|
||
const input = wrapper.find('input[type="text"]');
|
||
expect(input.exists()).toBe(true);
|
||
expect(input.attributes('placeholder')).toContain('ИНН');
|
||
});
|
||
|
||
it('фильтр по search оставляет только matching-tenants', async () => {
|
||
const wrapper = factory();
|
||
const input = wrapper.find('input[type="text"]');
|
||
await input.setValue('Натяжные');
|
||
await wrapper.vm.$nextTick();
|
||
const rows = wrapper.findAll('tbody tr');
|
||
expect(rows.length).toBe(1);
|
||
expect(rows[0].text()).toContain('Натяжные потолки СПб');
|
||
});
|
||
|
||
it('содержит Экспорт-кнопку и фильтры Статус/Тариф', () => {
|
||
const wrapper = factory();
|
||
const text = wrapper.text();
|
||
expect(text).toContain('Экспорт');
|
||
expect(text).toContain('Статус: Все');
|
||
expect(text).toContain('Тариф: Все');
|
||
});
|
||
});
|