- 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui}
covering states and a11y roles; every src module is imported by a test
- typed fixture helper assigns each JSON fixture to its generated OpenAPI type
- fix: iconFor ignores inherited object members such as "constructor"
- fix: field controls import ./control instead of the registry (import cycle
left a renderer unregistered depending on module load order)
- fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption
- fix: list announces a failed schema load even when the rows arrive after it
- tailwind no longer scans admin/tests; boardwalk/dist rebuilt
156 lines
6.5 KiB
TypeScript
156 lines
6.5 KiB
TypeScript
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<HTMLElement>('[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<HTMLElement>('[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: '<b>bold</b>' }, attachTo: document.body })
|
|
await flushPromises()
|
|
expect(dialog()!.querySelector('b')).toBeNull()
|
|
expect(dialog()!.textContent).toContain('<b>bold</b>')
|
|
})
|
|
|
|
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('<img src=x onerror=alert(1)>')
|
|
await flushPromises()
|
|
expect(wrapper.find('img').exists()).toBe(false)
|
|
})
|
|
})
|