diff --git a/app/resources/js/components/AppShell.vue b/app/resources/js/components/AppShell.vue index b2f84455..393dfd30 100644 --- a/app/resources/js/components/AppShell.vue +++ b/app/resources/js/components/AppShell.vue @@ -9,7 +9,7 @@ * Источник дизайна: liderra_v8_handoff/concepts/v8_login.html (auth), * v8_dashboard.html (app), v8_errors.html (error). */ -import { computed } from 'vue'; +import { computed, defineAsyncComponent, type Component } from 'vue'; import { RouterView, useRoute } from 'vue-router'; import AdminLayout from '../layouts/AdminLayout.vue'; import AppLayout from '../layouts/AppLayout.vue'; @@ -17,6 +17,11 @@ import AuthLayout from '../layouts/AuthLayout.vue'; const route = useRoute(); const layoutName = computed(() => route.meta.layout ?? 'app'); + +// Dev-only overlay: tree-shaken from production bundle via import.meta.env.DEV guard. +const DevIndexOverlay: Component | null = import.meta.env.DEV + ? defineAsyncComponent(() => import('./DevIndexOverlay.vue')) + : null; @@ -24,4 +29,5 @@ const layoutName = computed(() => route.meta.layout ?? 'app'); + diff --git a/app/resources/js/components/DevIndexOverlay.vue b/app/resources/js/components/DevIndexOverlay.vue new file mode 100644 index 00000000..1ef80443 --- /dev/null +++ b/app/resources/js/components/DevIndexOverlay.vue @@ -0,0 +1,125 @@ + + + + #{{ currentId }} + {{ tagLabel }} · "{{ textPreview }}" + + + + + + + diff --git a/app/tests/Frontend/DevIndexOverlay.spec.ts b/app/tests/Frontend/DevIndexOverlay.spec.ts new file mode 100644 index 00000000..c43b944a --- /dev/null +++ b/app/tests/Frontend/DevIndexOverlay.spec.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import DevIndexOverlay from '../../resources/js/components/DevIndexOverlay.vue'; +import { useDevIndices } from '../../resources/js/composables/useDevIndices'; + +describe('DevIndexOverlay', () => { + beforeEach(() => useDevIndices().reset()); + + it('hidden when no current target', () => { + mount(DevIndexOverlay, { attachTo: document.body }); + expect(document.querySelector('.dx-badge')).toBeNull(); + }); + + it('shows badge with id + tag when target is set', async () => { + const el = document.createElement('button'); + el.setAttribute('data-dx', '1030'); + el.textContent = 'Создать'; + document.body.appendChild(el); + + const dx = useDevIndices(); + dx.setTarget(el); + + mount(DevIndexOverlay, { attachTo: document.body }); + await nextTick(); + const badge = document.querySelector('.dx-badge'); + expect(badge).not.toBeNull(); + expect(badge!.textContent).toContain('1030'); + expect(badge!.textContent!.toLowerCase()).toContain('button'); + + document.body.removeChild(el); + }); + + it('Esc clears the target', async () => { + const el = document.createElement('div'); + el.setAttribute('data-dx', '5'); + document.body.appendChild(el); + const dx = useDevIndices(); + dx.setTarget(el); + + mount(DevIndexOverlay, { attachTo: document.body }); + await nextTick(); + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); + await nextTick(); + + expect(dx.currentId.value).toBeNull(); + document.body.removeChild(el); + }); + + it('clicking the badge copies "#" to clipboard', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { + value: { writeText }, + configurable: true, + }); + + const el = document.createElement('button'); + el.setAttribute('data-dx', '1030'); + document.body.appendChild(el); + useDevIndices().setTarget(el); + + mount(DevIndexOverlay, { attachTo: document.body }); + await nextTick(); + + const badge = document.querySelector('.dx-badge') as HTMLElement; + expect(badge).not.toBeNull(); + badge.click(); + await nextTick(); + expect(writeText).toHaveBeenCalledWith('#1030'); + + document.body.removeChild(el); + }); +});