034657788d
- ErrorView универсальный с конфигурацией через route.meta.errorCode
(404/403/500). По v8_errors.html: full-bleed теало-нуар bg, top-brand,
err-code 96px JBM с accent на средней цифре, title/desc, 2 actions,
опциональные status-list (500) и err-id с copy-btn (403/500).
- AppShell: meta.layout='error' → RouterView напрямую (ErrorView сам
предоставляет v-app).
- Router: /403, /500, catch-all /:pathMatch(.*)* → ErrorView с meta.errorCode.
- web.php: явные Route::view + Route::fallback (срабатывает после Pest
runtime-routes, не ломает SetTenantContextTest).
- cspell-words.txt: резолвится, роуты.
Vitest +8 (всего 118/118 за 9.39s):
- 404 default + 403 с REQ-ID + 500 с INC-ID + status-list (API/Telegram/YooKassa) +
404 actions (На дашборд + Назад) + 403 mailto-link + 500 status-link +
brand-блок + 404 НЕ содержит REQ/INC/status-list (regression-guard).
- stubs:{VApp/VMain} как passthrough — обходим Vuetify layout-injection в jsdom.
Регресс: lint+type+format OK; vitest 118/118; vite build (ErrorView lazy-chunk;
main app-chunk 101.01KB упал на 7KB благодаря shared chunk'ам); story:build
19/26 за 30.96s; Pest 48/48 за 4.88s (fallback не сломал runtime-routes).
CLAUDE.md v1.29->v1.30, реестр Открытых_вопросов v1.38->v1.39.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
4.7 KiB
TypeScript
103 lines
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import { mount } from '@vue/test-utils';
|
||
import { createVuetify } from 'vuetify';
|
||
import { createRouter, createMemoryHistory } from 'vue-router';
|
||
import ErrorView from '../../resources/js/views/errors/ErrorView.vue';
|
||
|
||
// ErrorView читает route.meta.errorCode для конфигурации экрана.
|
||
|
||
const mountErrorView = async (errorCode: '404' | '403' | '500') => {
|
||
const router = createRouter({
|
||
history: createMemoryHistory(),
|
||
routes: [
|
||
{ path: '/error', component: ErrorView, meta: { errorCode } },
|
||
{ path: '/dashboard', component: { template: '<div>dashboard stub</div>' } },
|
||
],
|
||
});
|
||
await router.push('/error');
|
||
await router.isReady();
|
||
return mount(ErrorView, {
|
||
global: {
|
||
plugins: [createVuetify(), router],
|
||
// ErrorView содержит v-app + v-main — те же layout-проблемы что у DealDetailDrawer.
|
||
// Stub'им VApp/VMain как passthrough.
|
||
stubs: {
|
||
VApp: { template: '<div class="v-app-stub"><slot /></div>' },
|
||
VMain: { template: '<div class="v-main-stub"><slot /></div>' },
|
||
},
|
||
},
|
||
});
|
||
};
|
||
|
||
describe('ErrorView.vue', () => {
|
||
it('по умолчанию (errorCode=404) показывает «404 / Страница не найдена»', async () => {
|
||
const wrapper = await mountErrorView('404');
|
||
const text = wrapper.text();
|
||
expect(wrapper.find('.err-code').text()).toBe('404');
|
||
expect(text).toContain('Страница не найдена');
|
||
expect(text).toContain('Все рабочие экраны Лидерра доступны через дашборд');
|
||
});
|
||
|
||
it('errorCode=403 показывает «403 / У вас нет доступа» + RequestId', async () => {
|
||
const wrapper = await mountErrorView('403');
|
||
const text = wrapper.text();
|
||
expect(wrapper.find('.err-code').text()).toBe('403');
|
||
expect(text).toContain('У вас нет доступа');
|
||
expect(text).toContain('REQ-3F8A2-0007');
|
||
expect(text).toContain('Запрос');
|
||
});
|
||
|
||
it('errorCode=500 показывает «500 / Что-то пошло не так» + IncidentId + status-list', async () => {
|
||
const wrapper = await mountErrorView('500');
|
||
const text = wrapper.text();
|
||
expect(wrapper.find('.err-code').text()).toBe('500');
|
||
expect(text).toContain('Что-то пошло не так');
|
||
expect(text).toContain('INC-2026-0507-0034');
|
||
expect(text).toContain('Инцидент');
|
||
// status-list только на 500.
|
||
expect(text).toContain('API · OK');
|
||
expect(text).toContain('Telegram · деградация');
|
||
});
|
||
|
||
it('404 содержит «На дашборд» primary + «Назад» secondary', async () => {
|
||
const wrapper = await mountErrorView('404');
|
||
const text = wrapper.text();
|
||
expect(text).toContain('На дашборд');
|
||
expect(text).toContain('Назад');
|
||
});
|
||
|
||
it('403 содержит «На дашборд» + «Написать в поддержку» (mailto)', async () => {
|
||
const wrapper = await mountErrorView('403');
|
||
const text = wrapper.text();
|
||
expect(text).toContain('На дашборд');
|
||
expect(text).toContain('Написать в поддержку');
|
||
const mailtoLink = wrapper.find('a[href^="mailto:"]');
|
||
expect(mailtoLink.exists()).toBe(true);
|
||
expect(mailtoLink.attributes('href')).toBe('mailto:support@liderra.app');
|
||
});
|
||
|
||
it('500 содержит «Попробовать снова» + «Статус сервиса» (external link)', async () => {
|
||
const wrapper = await mountErrorView('500');
|
||
const text = wrapper.text();
|
||
expect(text).toContain('Попробовать снова');
|
||
expect(text).toContain('Статус сервиса');
|
||
const statusLink = wrapper.find('a[href^="https://status."]');
|
||
expect(statusLink.exists()).toBe(true);
|
||
});
|
||
|
||
it('содержит брендовый блок «Лидерра.» в шапке', async () => {
|
||
const wrapper = await mountErrorView('404');
|
||
const brand = wrapper.find('.top-brand');
|
||
expect(brand.exists()).toBe(true);
|
||
expect(brand.text()).toContain('Лидерра');
|
||
});
|
||
|
||
it('404 НЕ содержит RequestId или status-list', async () => {
|
||
const wrapper = await mountErrorView('404');
|
||
const text = wrapper.text();
|
||
expect(text).not.toContain('REQ-');
|
||
expect(text).not.toContain('INC-');
|
||
expect(text).not.toContain('API · OK');
|
||
});
|
||
});
|