From 9b5ddcb6cd0ee3226ddbfada9c47898e71472ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 3 Jul 2026 03:23:59 +0300 Subject: [PATCH] =?UTF-8?q?feat(tours):=20GuidedTour=20=E2=80=94=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BE=D0=B1=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D0=B5=D1=80=20=D1=81=20=D0=BE=D0=B6=D0=B8?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20=D1=86=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/components/layout/GuidedTour.vue | 186 ++++++++++++++++++ app/tests/Frontend/GuidedTour.spec.ts | 55 ++++++ 2 files changed, 241 insertions(+) create mode 100644 app/resources/js/components/layout/GuidedTour.vue create mode 100644 app/tests/Frontend/GuidedTour.spec.ts 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 @@ + + + + + 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(); + }); +});