import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { Trash2 } from '@lucide/vue' import Button from '../../src/components/ui/Button.vue' import ConfirmDialog from '../../src/components/ui/ConfirmDialog.vue' import Toast from '../../src/components/ui/Toast.vue' import { useConfirm } from '../../src/components/ui/confirm' import { showToast, toasts } from '../../src/state/useToasts' import { resetState, routerAt, wait } from '../helpers' beforeEach(() => { resetState() }) afterEach(() => { document.body.innerHTML = '' }) enableAutoUnmount(afterEach) describe('button', () => { it('is a type=button outline button by default and can be disabled', () => { const wrapper = mount(Button, { slots: { default: 'Go' } }) const button = wrapper.find('button') expect(button.attributes('type')).toBe('button') expect(button.classes()).toContain('border-border-strong') expect(button.classes()).toContain('h-button') expect(button.text()).toBe('Go') const disabled = mount(Button, { props: { disabled: true, type: 'submit', size: 'sm', variant: 'primary' } }) expect(disabled.find('button').attributes('disabled')).toBeDefined() expect(disabled.find('button').attributes('type')).toBe('submit') expect(disabled.find('button').classes()).toContain('h-[38px]') expect(disabled.find('button').classes()).toContain('bg-primary') }) it('renders its icon hidden from assistive tech and the danger and ghost variants', () => { const danger = mount(Button, { props: { variant: 'danger', icon: Trash2 }, slots: { default: 'Delete' } }) expect(danger.find('svg').attributes('aria-hidden')).toBe('true') expect(danger.find('button').classes()).toContain('text-danger') expect(mount(Button, { props: { variant: 'ghost' } }).find('button').classes()).toContain('text-muted') }) it('renders a router link with the same styling when given a target', async () => { const router = await routerAt('/login') const wrapper = mount(Button, { props: { to: '/settings', variant: 'primary' }, slots: { default: 'Open' }, global: { plugins: [router] } }) const link = wrapper.find('a') expect(link.attributes('href')).toBe('/settings') expect(link.classes()).toContain('bg-primary') expect(wrapper.find('button').exists()).toBe(false) }) }) describe('confirm dialog', () => { function dialog() { return document.body.querySelector('[data-confirm-dialog]') } it('is an alertdialog with the default title and labels, and confirms', async () => { const wrapper = mount(ConfirmDialog, { props: { open: true, message: 'Delete 2 widgets?' }, attachTo: document.body }) await flushPromises() const element = dialog()! expect(element.getAttribute('role')).toBe('alertdialog') expect(document.getElementById(element.getAttribute('aria-labelledby')!)?.textContent?.trim()).toBe('Potwierdź') expect(document.getElementById(element.getAttribute('aria-describedby')!)?.textContent).toBe('Delete 2 widgets?') expect(element.querySelector('[data-action="cancel"]')!.textContent?.trim()).toBe('Anuluj') const confirm = element.querySelector('[data-action="confirm"]')! expect(confirm.textContent?.trim()).toBe('Potwierdź') expect(confirm.className).toContain('bg-primary') confirm.click() await flushPromises() expect(wrapper.emitted('confirm')).toHaveLength(1) expect(wrapper.emitted('update:open')).toEqual([[false]]) expect(wrapper.emitted('cancel')).toBeUndefined() }) it('uses custom labels, the danger style and cancels on Anuluj', async () => { const wrapper = mount(ConfirmDialog, { props: { open: true, message: 'Sure?', title: 'Remove', confirmLabel: 'Remove', cancelLabel: 'Keep', danger: true }, attachTo: document.body, }) await flushPromises() const element = dialog()! expect(element.textContent).toContain('Remove') expect(element.querySelector('[data-action="confirm"]')!.className).toContain('bg-danger') ;(element.querySelector('[data-action="cancel"]') as HTMLElement).click() await flushPromises() expect(wrapper.emitted('cancel')).toHaveLength(1) expect(wrapper.emitted('confirm')).toBeUndefined() }) it('renders a message with markup as text', async () => { mount(ConfirmDialog, { props: { open: true, message: 'bold' }, attachTo: document.body }) await flushPromises() expect(dialog()!.querySelector('b')).toBeNull() expect(dialog()!.textContent).toContain('bold') }) it('renders nothing while closed', async () => { mount(ConfirmDialog, { props: { open: false, message: 'x' }, attachTo: document.body }) await flushPromises() expect(dialog()).toBeNull() }) }) describe('useConfirm', () => { it('resolves true on confirm and false on cancel, and clears the request', async () => { const { request, ask, confirm, cancel } = useConfirm() const yes = ask({ message: 'A?' }) expect(request.value).toEqual({ message: 'A?' }) confirm() expect(await yes).toBe(true) expect(request.value).toBeNull() const no = ask({ message: 'B?', danger: true }) cancel() expect(await no).toBe(false) }) it('cancels a pending request when a second one is asked', async () => { const { request, ask, confirm } = useConfirm() const first = ask({ message: 'first' }) const second = ask({ message: 'second' }) expect(await first).toBe(false) expect(request.value?.message).toBe('second') confirm() expect(await second).toBe(true) confirm() }) }) describe('toast stack', () => { it('shows success as status and danger as alert, newest first, with a labelled close button', async () => { const wrapper = mount(Toast, { attachTo: document.body }) showToast('Saved') showToast('Failed', 'danger') await flushPromises() const items = wrapper.findAll('[data-tone]') expect(items.map((item) => [item.attributes('role'), item.text()])).toEqual([ ['alert', 'Failed'], ['status', 'Saved'], ]) const close = items[0]!.find('button') expect(close.attributes('aria-label')).toBe('Zamknij') await close.trigger('click') await wait(0) expect(toasts.value.map((item) => item.text)).toEqual(['Saved']) }) it('renders toast text without markup', async () => { const wrapper = mount(Toast, { attachTo: document.body }) showToast('') await flushPromises() expect(wrapper.find('img').exists()).toBe(false) }) })