diff --git a/app/resources/js/components/layout/GuidedTour.vue b/app/resources/js/components/layout/GuidedTour.vue new file mode 100644 index 00000000..02356d5a --- /dev/null +++ b/app/resources/js/components/layout/GuidedTour.vue @@ -0,0 +1,186 @@ + + + + + + + + Шаг {{ stepIndex + 1 }} из {{ steps.length }} + {{ currentStep.title }} + {{ currentStep.text }} + + Закрыть + + {{ isLast ? 'Готово' : 'Далее' }} + + + + + + + diff --git a/app/tests/Frontend/GuidedTour.spec.ts b/app/tests/Frontend/GuidedTour.spec.ts new file mode 100644 index 00000000..762d7578 --- /dev/null +++ b/app/tests/Frontend/GuidedTour.spec.ts @@ -0,0 +1,55 @@ +import { describe, expect, it, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import GuidedTour from '../../resources/js/components/layout/GuidedTour.vue'; +import type { TourStep } from '../../resources/js/tours/catalog'; + +const steps: TourStep[] = [ + { route: '/projects', target: '[data-tour="a"]', title: 'Шаг 1', text: 'т1' }, + { route: '/projects', target: '[data-tour="b"]', title: 'Шаг 2', text: 'т2' }, +]; + +function mountTour() { + return mount(GuidedTour, { + props: { steps, active: true }, + global: { stubs: { 'v-btn': { template: '' } } }, + }); +} + +describe('GuidedTour', () => { + it('показывает первый шаг и счётчик', () => { + const w = mountTour(); + expect(w.text()).toContain('Шаг 1'); + expect(w.text()).toContain('1 из 2'); + }); + + it('Далее ведёт по шагам, на последнем — Готово и finish', async () => { + const w = mountTour(); + await w.find('[data-testid="tour-next"]').trigger('click'); + expect(w.text()).toContain('Шаг 2'); + await w.find('[data-testid="tour-next"]').trigger('click'); + expect(w.emitted('finish')).toBeTruthy(); + }); + + it('Пропустить завершает тур сразу', async () => { + const w = mountTour(); + await w.find('[data-testid="tour-skip"]').trigger('click'); + expect(w.emitted('finish')).toBeTruthy(); + }); + + it('цель не найдена → карточка по центру (targetRect null), без падения', () => { + const w = mountTour(); + expect(w.find('[data-testid="guided-tour"]').exists()).toBe(true); + }); + + it('ретрай измерения: цель появляется позже — подсветка находит её', async () => { + vi.useFakeTimers(); + const w = mountTour(); + const el = document.createElement('div'); + el.setAttribute('data-tour', 'a'); + document.body.appendChild(el); + await vi.advanceTimersByTimeAsync(1000); + expect((w.vm as any).targetRect).not.toBeNull(); + el.remove(); + vi.useRealTimers(); + }); +});
{{ currentStep.text }}