Files
portal/app/tests/Frontend/AdminTenantsView.spec.ts
T
Дмитрий da65cf4bf7 phase2(admin): AdminLayout + AdminTenantsView - админка SaaS (12/13 концептов)
- 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>
2026-05-08 19:23:28 +03:00

104 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('Тариф: Все');
});
});