Unit tests
Overview
Unit tests isolate and verify individual components and composables in isolation. In Vue.js with Vitest:
- Vitest: Lightning-fast unit test runner compatible with Vite
- Vue Test Utils: Mount Vue components in a test environment, interact with them, and assert behavior
- Focus: Test component logic (props, emits, state, lifecycle) and composable hooks without DOM rendering
- Isolation: Mock external dependencies (API calls, third-party libraries) to test only the code you wrote
Run tests with:
bash
pnpm test:unitComponents
VOvChart.test.ts
ts
import { describe, it, expect, vi, beforeAll } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import type { ChartData } from 'chart.js'
import VOvChart from '../VOvChart.vue'
// Mock ResizeObserver globally for all tests
beforeAll(() => {
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
})) as never
})
const mockBarChartData: ChartData<'bar'> = {
labels: ['January', 'February', 'March'],
datasets: [
{
label: 'Sales',
data: [65, 59, 80],
backgroundColor: 'rgba(255, 99, 132, 0.5)',
},
],
}
const mockLineChartData: ChartData<'line'> = {
labels: ['Jan', 'Feb', 'Mar', 'Apr'],
datasets: [
{
label: 'Dataset 1',
data: [10, 20, 30, 40],
borderColor: 'rgb(75, 192, 192)',
},
],
}
const mockPieChartData: ChartData<'pie'> = {
labels: ['Red', 'Blue', 'Yellow'],
datasets: [
{
label: 'Votes',
data: [12, 19, 3],
backgroundColor: ['red', 'blue', 'yellow'],
},
],
}
// Helper to provide chart component stubs for all tests
const chartStubs = {
Bar: true,
Line: true,
Pie: true,
Doughnut: true,
Radar: true,
PolarArea: true,
Scatter: true,
}
describe('VOvChart', () => {
describe('Rendering', () => {
it('mounts without error when bar prop is true', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('mounts without error when line prop is true', () => {
const wrapper = mount(VOvChart, {
props: {
line: true,
chartData: mockLineChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('mounts without error when pie prop is true', () => {
const wrapper = mount(VOvChart, {
props: {
pie: true,
chartData: mockPieChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('mounts without error when doughnut prop is true', () => {
const wrapper = mount(VOvChart, {
props: {
doughnut: true,
chartData: mockPieChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('mounts without error when radar prop is true', () => {
const wrapper = mount(VOvChart, {
props: {
radar: true,
chartData: mockLineChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('mounts without error when polarArea prop is true', () => {
const wrapper = mount(VOvChart, {
props: {
polarArea: true,
chartData: mockLineChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('mounts without error when scatter prop is true', () => {
const wrapper = mount(VOvChart, {
props: {
scatter: true,
chartData: mockLineChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders nothing when no chart type is specified', () => {
const wrapper = mount(VOvChart, {
props: {
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
})
it('renders only first chart when multiple types are specified', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
line: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
})
describe('Data Props', () => {
it('accepts chart data prop', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('chartData')).toEqual(mockBarChartData)
})
it('handles empty datasets', () => {
const emptyData: ChartData<'bar'> = {
labels: ['test'],
datasets: [],
}
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: emptyData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('chartData')).toEqual(emptyData)
})
it('handles line chart data', () => {
const wrapper = mount(VOvChart, {
props: {
line: true,
chartData: mockLineChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('chartData')).toEqual(mockLineChartData)
})
it('handles pie chart data', () => {
const wrapper = mount(VOvChart, {
props: {
pie: true,
chartData: mockPieChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('chartData')).toEqual(mockPieChartData)
})
})
describe('Options Props', () => {
it('accepts custom chart options', () => {
const customOptions = {
responsive: true,
maintainAspectRatio: false,
}
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
chartOptions: customOptions,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('chartOptions')).toEqual(customOptions)
})
it('defaults to empty options object', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('chartOptions')).toEqual({})
})
})
describe('Plugins Props', () => {
it('accepts plugin array prop', () => {
const mockPlugin = { id: 'test-plugin' }
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
chartPlugins: [mockPlugin],
},
global: { stubs: chartStubs },
})
expect(wrapper.props('chartPlugins')).toEqual([mockPlugin])
})
it('defaults to empty plugins array', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('chartPlugins')).toEqual([])
})
})
describe('Click Handling', () => {
it('accepts clickable prop as true', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
clickable: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('clickable')).toBe(true)
})
it('accepts clickable prop as false', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
clickable: false,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('clickable')).toBe(false)
})
it('defaults clickable to false', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('clickable')).toBe(false)
})
})
describe('Chart Type Selection', () => {
it('mounts correctly when bar is true', () => {
const wrapper = mount(VOvChart, {
props: { bar: true, chartData: mockBarChartData },
global: { stubs: chartStubs },
})
expect(wrapper.exists()).toBe(true)
})
it('mounts correctly when line is true', () => {
const wrapper = mount(VOvChart, {
props: { line: true, chartData: mockLineChartData },
global: { stubs: chartStubs },
})
expect(wrapper.exists()).toBe(true)
})
it('mounts correctly when pie is true', () => {
const wrapper = mount(VOvChart, {
props: { pie: true, chartData: mockPieChartData },
global: { stubs: chartStubs },
})
expect(wrapper.exists()).toBe(true)
})
it('mounts correctly when doughnut is true', () => {
const wrapper = mount(VOvChart, {
props: { doughnut: true, chartData: mockPieChartData },
global: { stubs: chartStubs },
})
expect(wrapper.exists()).toBe(true)
})
it('mounts correctly when radar is true', () => {
const wrapper = mount(VOvChart, {
props: { radar: true, chartData: mockLineChartData },
global: { stubs: chartStubs },
})
expect(wrapper.exists()).toBe(true)
})
it('mounts correctly when polarArea is true', () => {
const wrapper = mount(VOvChart, {
props: { polarArea: true, chartData: mockLineChartData },
global: { stubs: chartStubs },
})
expect(wrapper.exists()).toBe(true)
})
it('mounts correctly when scatter is true', () => {
const wrapper = mount(VOvChart, {
props: { scatter: true, chartData: mockLineChartData },
global: { stubs: chartStubs },
})
expect(wrapper.exists()).toBe(true)
})
})
describe('ResizeObserver', () => {
it('mounts with ResizeObserver mock available', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(global.ResizeObserver).toBeDefined()
expect(wrapper.vm).toBeDefined()
})
it('can be unmounted without errors', async () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
await wrapper.unmount()
expect(wrapper.vm).toBeDefined()
})
})
describe('Edge Cases', () => {
it('handles no chart type props', () => {
const wrapper = mount(VOvChart, {
props: {
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.vm).toBeDefined()
})
it('handles multiple chart types (first wins)', () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
line: true,
pie: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
expect(wrapper.props('bar')).toBe(true)
expect(wrapper.props('line')).toBe(true)
expect(wrapper.props('pie')).toBe(true)
})
it('handles prop changes without errors', async () => {
const wrapper = mount(VOvChart, {
props: {
bar: true,
chartData: mockBarChartData,
},
global: { stubs: chartStubs },
})
await wrapper.setProps({ line: true, bar: false })
expect(wrapper.props('line')).toBe(true)
expect(wrapper.props('bar')).toBe(false)
await flushPromises()
})
})
describe('All Chart Types Integration', () => {
it('renders all 7 chart types without errors', async () => {
const wrappers = [
mount(VOvChart, {
props: { bar: true, chartData: mockBarChartData },
global: { stubs: chartStubs },
}),
mount(VOvChart, {
props: { line: true, chartData: mockLineChartData },
global: { stubs: chartStubs },
}),
mount(VOvChart, {
props: { pie: true, chartData: mockPieChartData },
global: { stubs: chartStubs },
}),
mount(VOvChart, {
props: { doughnut: true, chartData: mockPieChartData },
global: { stubs: chartStubs },
}),
mount(VOvChart, {
props: { radar: true, chartData: mockLineChartData },
global: { stubs: chartStubs },
}),
mount(VOvChart, {
props: { polarArea: true, chartData: mockLineChartData },
global: { stubs: chartStubs },
}),
mount(VOvChart, {
props: { scatter: true, chartData: mockLineChartData },
global: { stubs: chartStubs },
}),
]
wrappers.forEach((wrapper) => {
expect(wrapper.exists()).toBe(true)
})
})
})
})VOvDialog.test.ts
ts
import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import type { OvAction } from '../index'
import VOvDialog from '../VOvDialog.vue'
// Mock visualViewport for Vuetify VOverlay with proper event handling
if (typeof global.visualViewport === 'undefined') {
const mockViewport: Partial<VisualViewport> & {
addEventListener: ReturnType<typeof vi.fn>
removeEventListener: ReturnType<typeof vi.fn>
} = {
width: 1024,
height: 768,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
global.visualViewport = mockViewport as VisualViewport
}
// Helper to open dialog and wait for render
async function openDialog(wrapper: ReturnType<typeof mount>) {
await wrapper.setProps({ modelValue: true })
await wrapper.vm.$nextTick()
await flushPromises()
}
describe('VOvDialog', () => {
describe('Rendering', () => {
it('mounts without error', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Test Dialog',
},
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders VDialog component', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Test Dialog',
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.exists()).toBe(true)
})
it('renders VCard component inside dialog', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Test Dialog',
},
})
await openDialog(wrapper)
const card = wrapper.findComponent({ name: 'VCard' })
expect(card.exists()).toBe(true)
})
it('renders VCardText component for content', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Test Dialog',
content: 'Test content',
},
})
await openDialog(wrapper)
const cardText = wrapper.findComponent({ name: 'VCardText' })
expect(cardText.exists()).toBe(true)
})
it('renders VCardActions component for buttons', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Test Dialog',
actions: ['ok'],
},
})
await openDialog(wrapper)
const cardActions = wrapper.findComponent({ name: 'VCardActions' })
expect(cardActions.exists()).toBe(true)
})
})
describe('Dialog Properties', () => {
it('applies title prop to card', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'My Dialog Title',
},
})
await openDialog(wrapper)
const card = wrapper.findComponent({ name: 'VCard' })
expect(card.props('title')).toBe('My Dialog Title')
})
it('applies subtitle prop to card', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Title',
subtitle: 'Subtitle text',
},
})
await openDialog(wrapper)
const card = wrapper.findComponent({ name: 'VCard' })
expect(card.props('subtitle')).toBe('Subtitle text')
})
it('applies icon prop to card', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
icon: '$mdiAlert',
},
})
await openDialog(wrapper)
const card = wrapper.findComponent({ name: 'VCard' })
expect(card.props('prependIcon')).toBe('$mdiAlert')
})
it('applies color prop to card', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
color: 'primary',
},
})
await openDialog(wrapper)
const card = wrapper.findComponent({ name: 'VCard' })
expect(card.props('color')).toBe('primary')
})
it('applies persistent prop to dialog', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
persistent: true,
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('persistent')).toBe(true)
})
it('applies fullscreen prop to dialog', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
fullscreen: true,
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('fullscreen')).toBe(true)
})
it('applies scrollable prop to dialog', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
scrollable: true,
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('scrollable')).toBe(true)
})
it('defaults persistent to false', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('persistent')).toBe(false)
})
it('defaults fullscreen to false', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('fullscreen')).toBe(false)
})
it('defaults scrollable to false', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('scrollable')).toBe(false)
})
})
describe('Content Props', () => {
it('renders content as text when no format is provided', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Simple text content',
},
})
await openDialog(wrapper)
const cardText = wrapper.findComponent({ name: 'VCardText' })
expect(cardText.text()).toContain('Simple text content')
})
it('renders content without chip when hasContentProps is false', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Content',
},
})
const chips = wrapper.findAllComponents({ name: 'VChip' })
expect(chips.length).toBe(0)
})
it('renders content as chip when format is provided', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Formatted content',
contentFormat: { color: 'primary' },
},
})
await openDialog(wrapper)
const chip = wrapper.findComponent({ name: 'VChip' })
expect(chip.exists()).toBe(true)
})
it('handles empty content prop', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles undefined content prop', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: undefined,
},
})
expect(wrapper.vm).toBeDefined()
})
it('accepts multiple content formats as array', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Multi-format content',
contentFormat: [{ color: 'primary' }, { variant: 'outlined' }],
},
})
expect(wrapper.vm).toBeDefined()
})
it('accepts single content format object', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Single format content',
contentFormat: { color: 'error', variant: 'elevated' },
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Actions Props', () => {
it('renders action buttons when actions prop is provided as array', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['ok', 'cancel'],
},
})
await openDialog(wrapper)
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThanOrEqual(2)
})
it('renders single action button when actions prop is string', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: 'ok',
},
})
await openDialog(wrapper)
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThanOrEqual(1)
})
it('renders no action buttons when actions prop is empty array', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: [],
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBe(0)
})
it('renders no action buttons when actions prop is undefined', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBe(0)
})
it('renders multiple action buttons with different names', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['save', 'cancel', 'delete'],
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles action objects with format properties', () => {
const actions: OvAction[] = [
{
name: 'confirm',
format: { color: 'success', icon: '$mdiCheck' },
},
{
name: 'cancel',
format: { color: 'error', icon: '$mdiClose' },
},
]
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: actions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('accepts multiple action formats as array', () => {
const actions: OvAction[] = [
{
name: 'action1',
format: [{ color: 'primary' }, { variant: 'elevated' }],
},
]
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: actions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('accepts single action format as object', () => {
const actions: OvAction[] = [
{
name: 'action1',
format: { color: 'primary', variant: 'outlined' },
},
]
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: actions,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Exposed Dialog State', () => {
it('exposes dialog ref that can be accessed', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
modelValue: false,
},
})
expect(wrapper.vm).toBeDefined()
})
it('dialog ref is initially false', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
modelValue: false,
},
})
expect(wrapper.props('modelValue')).toBe(false)
})
it('dialog ref can be toggled', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
modelValue: false,
},
})
await wrapper.setProps({ modelValue: true })
await wrapper.vm.$nextTick()
expect(wrapper.props('modelValue')).toBe(true)
})
it('dialog ref can be opened from false to true', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
modelValue: false,
},
})
expect(wrapper.props('modelValue')).toBe(false)
await wrapper.setProps({ modelValue: true })
await wrapper.vm.$nextTick()
expect(wrapper.props('modelValue')).toBe(true)
})
it('dialog ref can be closed from true to false', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
modelValue: true,
},
})
await wrapper.vm.$nextTick()
expect(wrapper.props('modelValue')).toBe(true)
await wrapper.setProps({ modelValue: false })
await wrapper.vm.$nextTick()
expect(wrapper.props('modelValue')).toBe(false)
})
})
describe('Event Emissions', () => {
it('emits action event when custom action is clicked', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['custom'],
},
})
await wrapper.vm.$nextTick()
const customAction: OvAction = 'custom'
wrapper.vm.$emit('action', customAction)
expect(wrapper.emitted('action')).toBeTruthy()
})
it('emits submit event when submit action is triggered', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['submit'],
actionSubmit: 'submit',
},
})
await wrapper.vm.$nextTick()
const submitAction: OvAction = 'submit'
wrapper.vm.$emit('submit', submitAction)
expect(wrapper.emitted('submit')).toBeTruthy()
})
it('emits cancel event when cancel action is triggered', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['cancel'],
actionCancel: 'cancel',
},
})
await wrapper.vm.$nextTick()
wrapper.vm.$emit('cancel')
expect(wrapper.emitted('cancel')).toBeTruthy()
})
it('emits multiple events for different actions', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['action1', 'action2'],
},
})
await wrapper.vm.$nextTick()
wrapper.vm.$emit('action', 'action1')
wrapper.vm.$emit('action', 'action2')
expect(wrapper.emitted('action')).toBeTruthy()
expect(wrapper.emitted('action')?.length).toBe(2)
})
})
describe('Slots', () => {
it('renders content slot when provided', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
slots: {
content: { template: '<div class="custom-content">Custom Content</div>' },
},
})
await openDialog(wrapper)
// Verify component mounts with slot defined
expect(wrapper.vm).toBeDefined()
})
it('renders actions slot when provided', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
slots: {
actions: { template: '<button class="custom-button">Custom Button</button>' },
},
})
await openDialog(wrapper)
// Verify component mounts with slot defined
expect(wrapper.vm).toBeDefined()
})
it('content slot receives onClose callback', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
slots: {
content: '<div class="content"></div>',
},
})
expect(wrapper.vm).toBeDefined()
})
it('both slots can be used together', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
slots: {
content: { template: '<div class="content">Content Slot</div>' },
actions: { template: '<div class="actions">Actions Slot</div>' },
},
})
await openDialog(wrapper)
// Verify component mounts with both slots defined
expect(wrapper.vm).toBeDefined()
})
it('content slot overrides content prop', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Prop content',
},
slots: {
content: { template: '<div class="slot-override">Slot content</div>' },
},
})
await openDialog(wrapper)
// Verify component mounts with slot defined (slot takes precedence over prop)
expect(wrapper.vm).toBeDefined()
})
it('actions slot works independently of actions prop', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['ok'],
},
slots: {
actions: '<button>Extra Action</button>',
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Complex Scenarios', () => {
it('renders complete dialog with title, content, icon, and actions', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Confirmation',
subtitle: 'Are you sure?',
icon: '$mdiAlert',
color: 'warning',
content: 'This action cannot be undone.',
actions: ['cancel', 'confirm'],
actionCancel: 'cancel',
actionSubmit: 'confirm',
},
})
await openDialog(wrapper)
const card = wrapper.findComponent({ name: 'VCard' })
expect(card.props('title')).toBe('Confirmation')
expect(card.props('subtitle')).toBe('Are you sure?')
expect(card.props('prependIcon')).toBe('$mdiAlert')
expect(card.props('color')).toBe('warning')
})
it('renders dialog with formatted content and actions', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Styled Dialog',
content: 'Important message',
contentFormat: { color: 'error', variant: 'elevated' },
actions: [
{ name: 'accept', format: { color: 'success', icon: '$mdiCheck' } },
{ name: 'reject', format: { color: 'error', icon: '$mdiClose' } },
],
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders fullscreen persistent dialog', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Fullscreen Dialog',
fullscreen: true,
persistent: true,
content: 'Important content',
actions: ['close'],
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('fullscreen')).toBe(true)
expect(dialog.props('persistent')).toBe(true)
})
it('renders scrollable dialog with long content', () => {
const longContent = 'Lorem ipsum dolor sit amet. '.repeat(50) + 'Lorem ipsum dolor sit amet.'
const wrapper = mount(VOvDialog, {
props: {
title: 'Long Content Dialog',
scrollable: true,
content: longContent,
actions: ['close'],
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('scrollable')).toBe(true)
})
it('renders dialog with mixed action types (strings and objects)', () => {
const actions: OvAction[] = [
'cancel',
{ name: 'save', format: { color: 'primary' } },
'delete',
{ name: 'archive', format: { color: 'info' } },
]
const wrapper = mount(VOvDialog, {
props: {
title: 'Mixed Actions Dialog',
actions: actions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders dialog with multiple content formats', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Content',
contentFormat: [
{ color: 'primary', rules: { type: 'required', params: true } },
{ variant: 'outlined' },
],
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders dialog with multiple action formats', () => {
const actions: OvAction[] = [
{
name: 'action1',
format: [{ color: 'primary' }, { variant: 'elevated', size: 'large' }],
},
]
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: actions,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Props Updates', () => {
it('updates dialog title when title prop changes', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Initial Title',
},
})
await openDialog(wrapper)
await wrapper.setProps({ title: 'Updated Title' })
const card = wrapper.findComponent({ name: 'VCard' })
expect(card.props('title')).toBe('Updated Title')
})
it('updates dialog content when content prop changes', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Initial content',
},
})
await wrapper.setProps({ content: 'Updated content' })
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('updates actions when actions prop changes', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['action1'],
},
})
await wrapper.setProps({ actions: ['action1', 'action2', 'action3'] })
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('updates dialog state when persistent prop changes', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
persistent: false,
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('persistent')).toBe(false)
await wrapper.setProps({ persistent: true })
expect(dialog.props('persistent')).toBe(true)
})
it('updates multiple props simultaneously', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Initial',
content: 'Initial content',
icon: '$mdiInformation',
color: 'info',
},
})
await openDialog(wrapper)
await wrapper.setProps({
title: 'Updated',
content: 'Updated content',
icon: '$mdiAlert',
color: 'warning',
})
const card = wrapper.findComponent({ name: 'VCard' })
expect(card.props('title')).toBe('Updated')
expect(card.props('color')).toBe('warning')
})
})
describe('Edge Cases', () => {
it('handles empty title prop', () => {
const wrapper = mount(VOvDialog, {
props: {
title: '',
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles undefined title prop', () => {
const wrapper = mount(VOvDialog, {
props: {},
})
expect(wrapper.vm).toBeDefined()
})
it('handles very long title', () => {
const longTitle = 'A'.repeat(200)
const wrapper = mount(VOvDialog, {
props: {
title: longTitle,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles special characters in title', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog with <special> & "characters"',
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles all undefined optional props', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
persistent: undefined,
fullscreen: undefined,
scrollable: undefined,
subtitle: undefined,
icon: undefined,
color: undefined,
content: undefined,
contentFormat: undefined,
actions: undefined,
actionFormat: undefined,
actionSubmit: undefined,
actionCancel: undefined,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles dialog mounting and unmounting', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
})
expect(wrapper.exists()).toBe(true)
await wrapper.unmount()
expect(wrapper.vm).toBeDefined()
})
it('handles rapid prop changes', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog 1',
},
})
for (let i = 2; i <= 10; i++) {
await wrapper.setProps({ title: `Dialog ${i}` })
}
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('handles empty actions array and then adding actions', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: [],
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBe(0)
await wrapper.setProps({ actions: ['ok', 'cancel'] })
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Breakpoint and Width Handling', () => {
it('calculates width based on display breakpoint', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
})
const dialog = wrapper.findComponent({ name: 'VDialog' })
expect(dialog.props('width')).toBeDefined()
})
it('width prop is a computed value based on breakpoint', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Content and Action Formatting', () => {
it('applies OvFormat to content correctly', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
content: 'Formatted content',
contentFormat: { color: 'primary', icon: '$mdiInformation' },
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies OvActionFormat to actions correctly', () => {
const actions: OvAction[] = [
{
name: 'save',
format: { color: 'success', text: 'Save Changes' },
},
]
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: actions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('uses action name as button text when format text is not provided', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['save'],
},
})
expect(wrapper.vm).toBeDefined()
})
it('uses format text when provided instead of action name', () => {
const actions: OvAction[] = [
{
name: 'save',
format: { text: 'Save Changes' },
},
]
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: actions,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Action Button Rendering', () => {
it('renders all action buttons', async () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['action1', 'action2', 'action3'],
},
})
await openDialog(wrapper)
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThanOrEqual(3)
})
it('renders buttons with correct props from actionFormat', () => {
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: ['ok'],
actionFormat: { color: 'primary' },
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders buttons with individual format properties', () => {
const actions: OvAction[] = [
{ name: 'ok', format: { color: 'success' } },
{ name: 'cancel', format: { color: 'error' } },
]
const wrapper = mount(VOvDialog, {
props: {
title: 'Dialog',
actions: actions,
},
})
expect(wrapper.vm).toBeDefined()
})
})
})VOvEditor.test.ts
ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
// very light mocks so we don't pull in real TipTap/Vuetify internals
vi.mock('@tiptap/vue-3', () => ({
useEditor: vi.fn(() => ({
value: {
getHTML: vi.fn(() => '<p>from editor</p>'),
destroy: vi.fn(),
isActive: vi.fn(() => false),
chain: vi.fn(() => ({
focus: vi.fn(() => ({
toggleBold: vi.fn(() => ({ run: vi.fn() })),
toggleItalic: vi.fn(() => ({ run: vi.fn() })),
toggleUnderline: vi.fn(() => ({ run: vi.fn() })),
toggleStrike: vi.fn(() => ({ run: vi.fn() })),
toggleBulletList: vi.fn(() => ({ run: vi.fn() })),
toggleOrderedList: vi.fn(() => ({ run: vi.fn() })),
toggleHeading: vi.fn(() => ({ run: vi.fn() })),
})),
})),
},
})),
EditorContent: { name: 'EditorContent', template: '<div class="editor-content" />' },
}))
vi.mock('@tiptap/starter-kit', () => ({ default: {} }))
import VOvEditor from '../VOvEditor.vue'
describe('VOvEditor', () => {
it('mounts with v-model and toolbar like sandbox example', () => {
const wrapper = mount(VOvEditor, {
props: {
modelValue: '<p>Initial</p>',
toolbar: [
'bold',
'italic',
'underline',
'strike',
'bulletList',
'orderedList',
'heading1',
'heading2',
'heading3',
],
toolbarClass: 'mb-4',
},
})
expect(wrapper.exists()).toBe(true)
expect(wrapper.props('modelValue')).toBe('<p>Initial</p>')
expect(wrapper.props('toolbar')).toHaveLength(9)
expect(wrapper.props('toolbarClass')).toBe('mb-4')
})
it('emits updated when content changes', async () => {
const wrapper = mount(VOvEditor, {
props: {
modelValue: '<p>Initial</p>',
},
})
// The 'updated' event should be emitted when model value changes
await wrapper.setProps({ modelValue: '<p>Updated</p>' })
await wrapper.vm.$nextTick()
// The component should be mounted and functional
expect(wrapper.exists()).toBe(true)
})
it('can be unmounted without errors', async () => {
const wrapper = mount(VOvEditor, {
props: {
modelValue: '',
},
})
await wrapper.unmount()
expect(wrapper).toBeDefined()
})
it('toggles bold formatting when bold button is clicked', async () => {
const wrapper = mount(VOvEditor, {
props: {
modelValue: '<p>Test</p>',
toolbar: ['bold'],
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThan(0)
// Click the bold button
await buttons[0]!.trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.exists()).toBe(true)
})
})VOvForm.test.ts
ts
import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import type { OvFormOptions, OvFormData } from '../index'
import VOvForm from '../VOvForm.vue'
// Mock visualViewport for Vuetify VOverlay with proper event handling
if (typeof global.visualViewport === 'undefined') {
const mockViewport: Partial<VisualViewport> & {
addEventListener: ReturnType<typeof vi.fn>
removeEventListener: ReturnType<typeof vi.fn>
} = {
width: 1024,
height: 768,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
global.visualViewport = mockViewport as VisualViewport
}
// Mock Vuetify components and composables
vi.mock('vuetify/components', () => ({
VTextField: { name: 'VTextField', template: '<input />' },
VSelect: { name: 'VSelect', template: '<select></select>' },
VCombobox: { name: 'VCombobox', template: '<input />' },
VAutocomplete: { name: 'VAutocomplete', template: '<input />' },
VFileInput: { name: 'VFileInput', template: '<input type="file" />' },
VSwitch: { name: 'VSwitch', template: '<input type="checkbox" />' },
VCheckbox: { name: 'VCheckbox', template: '<input type="checkbox" />' },
VRating: { name: 'VRating', template: '<div></div>' },
VTextarea: { name: 'VTextarea', template: '<textarea></textarea>' },
}))
describe('VOvForm', () => {
const basicFormOptions: OvFormOptions = {
fields: [
{
type: 'text',
name: 'username',
label: 'Username',
placeholder: 'Enter username',
},
{
type: 'email',
name: 'email',
label: 'Email',
placeholder: 'Enter email',
},
],
actions: ['submit', 'cancel'],
actionSubmit: 'submit',
actionCancel: 'cancel',
}
describe('Rendering', () => {
it('mounts without error', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders VContainer component', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
const container = wrapper.findComponent({ name: 'VContainer' })
expect(container.exists()).toBe(true)
})
it('renders VForm component', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
const form = wrapper.findComponent({ name: 'VForm' })
expect(form.exists()).toBe(true)
})
it('renders form fields based on options', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
const fields = wrapper.findAllComponents({ name: 'VTextField' })
expect(fields.length).toBeGreaterThan(0)
})
it('renders action buttons when actions are provided', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThan(0)
})
it('renders form with loading state false by default', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
loading: false,
},
})
expect(wrapper.props('loading')).toBe(false)
})
})
describe('Props - Form Options', () => {
it('accepts options prop with fields and actions', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
expect(wrapper.props('options')).toEqual(basicFormOptions)
})
it('renders all field types from options', () => {
const allFieldsOptions: OvFormOptions = {
fields: [
{ type: 'text', name: 'text', label: 'Text' },
{ type: 'email', name: 'email', label: 'Email' },
{ type: 'password', name: 'password', label: 'Password' },
{ type: 'number', name: 'number', label: 'Number' },
{ type: 'date', name: 'date', label: 'Date' },
{ type: 'time', name: 'time', label: 'Time' },
{ type: 'datetime', name: 'datetime', label: 'DateTime' },
{ type: 'textarea', name: 'textarea', label: 'Textarea' },
{ type: 'checkbox', name: 'checkbox', label: 'Checkbox' },
{ type: 'switch', name: 'switch', label: 'Switch' },
{ type: 'rating', name: 'rating', label: 'Rating' },
{ type: 'select', name: 'select', label: 'Select', items: ['A', 'B'] },
{ type: 'combobox', name: 'combobox', label: 'Combobox', items: ['A', 'B'] },
{ type: 'autocomplete', name: 'autocomplete', label: 'Autocomplete', items: ['A', 'B'] },
{ type: 'file', name: 'file', label: 'File' },
],
}
const wrapper = mount(VOvForm, {
props: {
options: allFieldsOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('hides fields marked with hidden property', () => {
const optionsWithHidden: OvFormOptions = {
fields: [
{ type: 'text', name: 'visible', label: 'Visible' },
{ type: 'text', name: 'hidden', label: 'Hidden', hidden: true },
],
}
const wrapper = mount(VOvForm, {
props: {
options: optionsWithHidden,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Props - Data', () => {
it('accepts data prop and initializes form values', async () => {
const initialData: OvFormData = {
username: 'testuser',
email: 'test@example.com',
}
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
data: initialData,
},
})
await wrapper.vm.$nextTick()
expect(wrapper.props('data')).toEqual(initialData)
})
it('handles undefined data prop', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
expect(wrapper.props('data')).toBeUndefined()
})
it('updates form when data prop changes', async () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
data: { username: 'user1', email: 'user1@example.com' },
},
})
await wrapper.vm.$nextTick()
await wrapper.setProps({
data: { username: 'user2', email: 'user2@example.com' },
})
await flushPromises()
expect(wrapper.props('data')?.username).toBe('user2')
})
})
describe('Props - Translation Function', () => {
it('accepts translation function prop', () => {
const mockT = (text?: string) => `translated_${text}`
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
t: mockT,
},
})
expect(wrapper.props('t')).toBe(mockT)
})
it('uses translation function for labels', () => {
const mockT = (text?: string) => `translated_${text}`
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
t: mockT,
},
})
expect(wrapper.vm).toBeDefined()
})
it('defaults translation function if not provided', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
const defaultT = wrapper.props('t')
expect(typeof defaultT).toBe('function')
})
})
describe('Props - Loading', () => {
it('accepts loading prop as false', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
loading: false,
},
})
expect(wrapper.props('loading')).toBe(false)
})
it('defaults loading to false', () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
expect(wrapper.props('loading')).toBe(false)
})
})
describe('Form Field Rendering', () => {
it('renders text field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'text', label: 'Text' }],
},
},
})
const textFields = wrapper.findAllComponents({ name: 'VTextField' })
expect(textFields.length).toBeGreaterThan(0)
})
it('renders email field as VTextField with email type', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'email', name: 'email', label: 'Email' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders password field with visibility toggle', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'password', name: 'password', label: 'Password' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders textarea field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'textarea', name: 'textarea', label: 'Textarea', rows: 5 }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders select field with items', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'select',
name: 'select',
label: 'Select',
items: ['Option A', 'Option B'],
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders checkbox field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'checkbox', name: 'checkbox', label: 'Checkbox' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders switch field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'switch', name: 'switch', label: 'Switch' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders rating field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'rating', name: 'rating', label: 'Rating', length: 5 }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Form Actions', () => {
it('renders action buttons', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: ['submit', 'cancel'],
},
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThan(0)
})
it('renders no action buttons when actions array is empty', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: [],
},
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBe(0)
})
it('renders no action buttons when actions is undefined', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
},
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBe(0)
})
it('renders custom action buttons', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: ['submit', 'reset', { name: 'custom', format: { color: 'red' } }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Form Submission', () => {
it('emits submit event with form data on submit action', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'text',
name: 'username',
label: 'Username',
rules: [],
},
],
actions: ['submit'],
actionSubmit: 'submit',
},
data: { username: 'testuser' },
},
})
await wrapper.vm.$nextTick()
// Simulate form validation and submit
// In real scenario, this would be triggered by button click
expect(wrapper.vm).toBeDefined()
})
it('emits cancel event on cancel action', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: ['cancel'],
actionCancel: 'cancel',
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('emits reset event on reset action', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'username', label: 'Username', value: 'default' }],
actions: ['reset'],
actionReset: 'reset',
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('emits validate event on validate action', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: ['validate'],
actionValidate: 'validate',
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('emits action event on custom action', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: [{ name: 'customAction', format: {} }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Form Validation', () => {
it('applies validation rules to fields', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'email',
name: 'email',
label: 'Email',
rules: [{ type: 'email', params: true, message: 'Invalid email' }],
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('displays error messages from options.errors', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'username', label: 'Username' }],
errors: [{ name: 'username', message: 'Username is required' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('updates error display when errors prop changes', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'username', label: 'Username' }],
errors: [],
},
},
})
await wrapper.setProps({
options: {
...basicFormOptions,
errors: [{ name: 'username', message: 'Error' }],
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Form State Management', () => {
it('maintains form state with field values', async () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
data: { username: 'user1', email: 'user1@example.com' },
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
})
it('applies default values from field definitions', () => {
const optionsWithDefaults: OvFormOptions = {
fields: [
{ type: 'text', name: 'field1', label: 'Field 1', value: 'default1' },
{ type: 'text', name: 'field2', label: 'Field 2', value: 'default2' },
],
}
const wrapper = mount(VOvForm, {
props: {
options: optionsWithDefaults,
},
})
expect(wrapper.vm).toBeDefined()
})
it('merges provided data with default field values', async () => {
const optionsWithDefaults: OvFormOptions = {
fields: [
{ type: 'text', name: 'field1', label: 'Field 1', value: 'default1' },
{ type: 'text', name: 'field2', label: 'Field 2', value: 'default2' },
],
}
const wrapper = mount(VOvForm, {
props: {
options: optionsWithDefaults,
data: { field1: 'custom1' },
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
})
})
describe('Form Layout', () => {
it('renders columns based on cols option', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{ type: 'text', name: 'field1', label: 'Field 1' },
{ type: 'text', name: 'field2', label: 'Field 2' },
{ type: 'text', name: 'field3', label: 'Field 3' },
],
cols: 3,
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('defaults to 1 column layout when cols is not specified', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{ type: 'text', name: 'field1', label: 'Field 1' },
{ type: 'text', name: 'field2', label: 'Field 2' },
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('aligns action buttons according to actionAlign option', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: ['submit', 'cancel'],
actionAlign: 'center',
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('supports left action alignment', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: ['submit'],
actionAlign: 'left',
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('supports right action alignment', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
actions: ['submit'],
actionAlign: 'right',
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Form Properties', () => {
it('applies disabled state to form when disabled option is true', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field' }],
disabled: true,
},
},
})
const form = wrapper.findComponent({ name: 'VForm' })
expect(form.props('disabled')).toBe(true)
})
it('applies readonly state to form when readonly option is true', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field' }],
readonly: true,
},
},
})
const form = wrapper.findComponent({ name: 'VForm' })
expect(form.props('readonly')).toBe(true)
})
it('applies fastFail option to form validation', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field' }],
fastFail: true,
},
},
})
const form = wrapper.findComponent({ name: 'VForm' })
expect(form.props('fastFail')).toBe(true)
})
it('applies autocomplete option to form', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field' }],
autocomplete: 'off',
},
},
})
expect(wrapper.props('options').autocomplete).toBe('off')
})
it('focuses first field when focusFirst option is true', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{ type: 'text', name: 'field1', label: 'Field 1' },
{ type: 'text', name: 'field2', label: 'Field 2' },
],
focusFirst: true,
},
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
})
it('does not focus first field when focusFirst is false or undefined', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field' }],
focusFirst: false,
},
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
})
})
describe('Field Properties', () => {
it('applies label to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field Label' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies placeholder to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field', placeholder: 'Enter value' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies hint to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'text',
name: 'field',
label: 'Field',
hint: 'This is a hint',
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies required property to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field', required: true }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies readonly property to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field', readonly: true }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies disabled property to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field', disabled: true }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies clearable property to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field', clearable: true }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies icon properties to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'text',
name: 'field',
label: 'Field',
prependIcon: '$mdiAccount',
appendIcon: '$mdiClose',
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies variant property to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'text',
name: 'field',
label: 'Field',
variant: 'underlined',
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies density property to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field', density: 'compact' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies color property to field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field', color: 'primary' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies counter property to textarea', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'textarea',
name: 'field',
label: 'Field',
counter: 200,
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies prefix and suffix to text fields', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'text',
name: 'price',
label: 'Price',
prefix: '$',
suffix: 'USD',
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Textarea Field Properties', () => {
it('applies rows property to textarea', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'textarea',
name: 'textarea',
label: 'Textarea',
rows: 8,
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies noResize property to textarea', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'textarea',
name: 'textarea',
label: 'Textarea',
noResize: true,
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies autoGrow property to textarea', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'textarea',
name: 'textarea',
label: 'Textarea',
autoGrow: true,
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Rating Field Properties', () => {
it('applies length property to rating', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'rating',
name: 'rating',
label: 'Rating',
length: 10,
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies size property to rating', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'rating',
name: 'rating',
label: 'Rating',
size: 32,
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies itemLabels property to rating', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'rating',
name: 'rating',
label: 'Rating',
itemLabels: ['Poor', 'Fair', 'Good', 'Very Good', 'Excellent'],
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('disables rating when form is disabled', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'rating', name: 'rating', label: 'Rating' }],
disabled: true,
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Selection Field Properties', () => {
it('applies items property to select field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'select',
name: 'select',
label: 'Select',
items: ['Option A', 'Option B', 'Option C'],
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies chips property to select field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'select',
name: 'select',
label: 'Select',
items: ['A', 'B'],
chips: true,
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies multiple property to select field', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'select',
name: 'select',
label: 'Select',
items: ['A', 'B'],
multiple: true,
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles combobox field type', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'combobox',
name: 'combobox',
label: 'Combobox',
items: ['A', 'B'],
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles autocomplete field type', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{
type: 'autocomplete',
name: 'autocomplete',
label: 'Autocomplete',
items: ['A', 'B'],
},
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Password Field', () => {
it('renders password field with password type', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'password', name: 'password', label: 'Password' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('toggles password visibility with eye icon', async () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'password', name: 'password', label: 'Password' }],
},
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
})
})
describe('Responsive Layout', () => {
it('adapts layout for mobile devices', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{ type: 'text', name: 'field1', label: 'Field 1' },
{ type: 'text', name: 'field2', label: 'Field 2' },
],
cols: 2,
},
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Mounting and Unmounting', () => {
it('mounts and initializes correctly', async () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('can be unmounted without errors', async () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
},
})
await wrapper.unmount()
expect(wrapper).toBeDefined()
})
})
describe('Props Updates', () => {
it('updates form when options prop changes', async () => {
const initialOptions: OvFormOptions = {
fields: [{ type: 'text', name: 'field1', label: 'Field 1' }],
}
const wrapper = mount(VOvForm, {
props: {
options: initialOptions,
},
})
const newOptions: OvFormOptions = {
fields: [
{ type: 'text', name: 'field1', label: 'Field 1' },
{ type: 'text', name: 'field2', label: 'Field 2' },
],
}
await wrapper.setProps({ options: newOptions })
await flushPromises()
expect(wrapper.props('options').fields.length).toBe(2)
})
it('updates loading state', async () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
loading: false,
},
})
expect(wrapper.props('loading')).toBe(false)
})
})
describe('Complex Form Scenarios', () => {
it('handles form with all field types like sandbox-form example', async () => {
const complexFormOptions: OvFormOptions = {
fields: [
{ type: 'text', name: 'text1', label: 'Text 1' },
{ type: 'email', name: 'email', label: 'Email' },
{ type: 'number', name: 'number', label: 'Number' },
{ type: 'password', name: 'password', label: 'Password' },
{ type: 'textarea', name: 'textarea', label: 'Textarea' },
{ type: 'switch', name: 'acceptTerms', label: 'Accept Terms' },
{ type: 'rating', name: 'rating', label: 'Rating' },
{ type: 'checkbox', name: 'checkbox', label: 'Checkbox' },
{
type: 'select',
name: 'select',
label: 'Select',
items: ['A', 'B', 'C'],
multiple: true,
},
{
type: 'combobox',
name: 'combobox',
label: 'Combobox',
items: ['A', 'B', 'C'],
},
{
type: 'autocomplete',
name: 'autocomplete',
label: 'Autocomplete',
items: ['A', 'B', 'C'],
},
{ type: 'file', name: 'file', label: 'File' },
{ type: 'date', name: 'date', label: 'Date' },
{ type: 'time', name: 'time', label: 'Time' },
{ type: 'datetime', name: 'datetime', label: 'DateTime' },
],
cols: 3,
actions: ['validate', 'cancel', 'submit', 'reset'],
actionFormat: { color: 'primary' },
actionAlign: 'center',
actionSubmit: 'submit',
actionReset: 'reset',
actionValidate: 'validate',
actionCancel: 'cancel',
}
const wrapper = mount(VOvForm, {
props: {
options: complexFormOptions,
data: {
text1: 'Test',
email: 'test@example.com',
number: 42,
},
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('handles form with validation rules', () => {
const formWithValidation: OvFormOptions = {
fields: [
{
type: 'text',
name: 'username',
label: 'Username',
rules: [
{ type: 'required', params: true, message: 'Required' },
{
type: 'min-length',
params: 3,
message: 'Min 3 chars',
},
],
},
{
type: 'email',
name: 'email',
label: 'Email',
rules: [
{ type: 'required', params: true, message: 'Required' },
{ type: 'email', params: true, message: 'Invalid email' },
],
},
],
actions: ['submit'],
actionSubmit: 'submit',
}
const wrapper = mount(VOvForm, {
props: {
options: formWithValidation,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles form with field errors from server', () => {
const formWithErrors: OvFormOptions = {
fields: [
{ type: 'text', name: 'username', label: 'Username' },
{ type: 'email', name: 'email', label: 'Email' },
],
errors: [
{ name: 'username', message: 'Username already taken' },
{ name: 'email', message: 'Email already registered' },
],
}
const wrapper = mount(VOvForm, {
props: {
options: formWithErrors,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles form with custom actions', () => {
const formWithCustomActions: OvFormOptions = {
fields: [{ type: 'text', name: 'field', label: 'Field' }],
actions: [
'submit',
'reset',
{
name: 'customAction',
format: { color: 'red', text: 'Custom' },
},
{
name: 'anotherAction',
format: { color: 'blue', icon: '$mdiCheck' },
},
],
}
const wrapper = mount(VOvForm, {
props: {
options: formWithCustomActions,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Edge Cases', () => {
it('handles empty form with no fields', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles form with only hidden fields', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [
{ type: 'text', name: 'field1', label: 'Field 1', hidden: true },
{ type: 'text', name: 'field2', label: 'Field 2', hidden: true },
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles form with special characters in labels', () => {
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: 'field', label: 'Field with <special> & chars' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles form with very long field names', () => {
const longName = 'field_' + 'x'.repeat(100)
const wrapper = mount(VOvForm, {
props: {
options: {
fields: [{ type: 'text', name: longName, label: 'Long Name' }],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles prop updates without errors', async () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
loading: false,
},
})
expect(wrapper.props('loading')).toBe(false)
})
it('handles undefined or null values in data', async () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
data: {
username: undefined,
email: null,
} as unknown as OvFormData,
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
})
})
describe('Form with Translation', () => {
it('applies translations to all field labels', () => {
const mockT = (text?: string): string => {
const translations: Record<string, string> = {
Username: 'Nombre de usuario',
Email: 'Correo electrónico',
}
return translations[text || ''] || text || ''
}
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
t: mockT,
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies translations to action button text', () => {
const mockT = (text?: string): string => {
const translations: Record<string, string> = {
submit: 'Enviar',
cancel: 'Cancelar',
}
return translations[text || ''] || text || ''
}
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
t: mockT,
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies translations to validation error messages', () => {
const mockT = (text?: string): string => {
const translations: Record<string, string> = {
'Field is required': 'El campo es obligatorio',
'Invalid format': 'Formato inválido',
}
return translations[text || ''] || text || ''
}
const formWithErrors: OvFormOptions = {
fields: [{ type: 'text', name: 'field', label: 'Field' }],
errors: [{ name: 'field', message: 'Field is required' }],
}
const wrapper = mount(VOvForm, {
props: {
options: formWithErrors,
t: mockT,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Integration with Sandbox Example', () => {
it('can be used with sandbox-form.vue style configuration', async () => {
const sandboxOptions: OvFormOptions = {
fields: [
{
type: 'text',
name: 'text1',
label: 'Text 1',
placeholder: 'Enter some text',
value: 'Hello World - default!',
},
{
type: 'text',
name: 'text2',
label: 'Text 2',
placeholder: 'Enter some text',
rules: [{ type: 'required', params: true, message: 'Text 2 is required' }],
},
{
type: 'email',
name: 'email',
label: 'Email',
placeholder: 'Enter valid e-mail address',
},
{
type: 'password',
name: 'password',
label: 'Password',
placeholder: 'Enter password',
required: true,
},
{
type: 'textarea',
name: 'textarea',
label: 'Textarea',
placeholder: 'Enter some text',
rows: 3,
counter: 200,
},
{
type: 'select',
name: 'select',
label: 'Select',
items: ['Alfa', 'Bravo', 'Charlie'],
multiple: true,
},
{
type: 'rating',
name: 'rating',
label: 'Rating',
length: 5,
},
],
cols: 3,
actions: ['validate', 'cancel', 'submit', 'reset'],
actionAlign: 'center',
actionSubmit: 'submit',
actionReset: 'reset',
actionCancel: 'cancel',
focusFirst: true,
}
const wrapper = mount(VOvForm, {
props: {
options: sandboxOptions,
data: {
text1: 'Hello World!',
email: '',
password: '',
},
loading: false,
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
expect(wrapper.props('options').fields.length).toBe(7)
})
it('supports loading state management like sandbox-form', async () => {
const wrapper = mount(VOvForm, {
props: {
options: basicFormOptions,
loading: false,
},
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.props('loading')).toBe(false)
})
})
})VOvMap.test.ts
ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import VOvMap from '../VOvMap.vue'
import type { TOvMapMarker } from '../VOvMap.vue'
// Mock google maps library
vi.mock('vue3-google-map', () => ({
GoogleMap: {
name: 'GoogleMap',
template: '<div id="google-map-mock"><slot /></div>',
props: ['id', 'mapId', 'apiKey', 'center', 'zoom', 'autoCenter', 'autoLocation'],
emits: ['zoom_changed', 'click'],
},
AdvancedMarker: {
name: 'AdvancedMarker',
template: '<div class="marker-mock"><slot /></div>',
props: ['options', 'pinOptions'],
},
InfoWindow: {
name: 'InfoWindow',
template: '<div class="infowindow-mock"><slot /></div>',
props: ['position'],
},
}))
// Mock import.meta.env
vi.stubGlobal('import', {
meta: {
env: {
VITE_GOOGLE_MAP_API_KEY: 'test-api-key',
},
},
})
describe('VOvMap', () => {
beforeEach(() => {
// Mock geolocation API
const mockGeolocation = {
getCurrentPosition: vi.fn((success) =>
success({
coords: {
latitude: 40.7128,
longitude: -74.006,
},
}),
),
}
Object.defineProperty(navigator, 'geolocation', {
value: mockGeolocation,
configurable: true,
})
})
afterEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('mounts without error', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders google map component', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const googleMap = wrapper.find('#google-map-mock')
expect(googleMap.exists()).toBe(true)
})
it('renders v-overlay component', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
},
global: {
stubs: {
'v-overlay': { template: '<div class="overlay-stub"></div>' },
},
},
})
const overlay = wrapper.find('.overlay-stub')
expect(overlay.exists()).toBe(true)
})
})
describe('Props - Center', () => {
it('accepts center prop', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 51.5074, lng: -0.1278 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('center')).toEqual({ lat: 51.5074, lng: -0.1278 })
})
it('defaults center to 0,0', () => {
const wrapper = mount(VOvMap, {
props: {
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('center')).toEqual({ lat: 0, lng: 0 })
})
it('updates localCenter when center prop changes', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await wrapper.setProps({ center: { lat: 40.7128, lng: -74.006 } })
// Verify the event was emitted with the new center
expect(wrapper.emitted('centered')).toBeTruthy()
})
})
describe('Props - Zoom', () => {
it('accepts zoom prop', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 15,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('zoom')).toBe(15)
})
it('defaults zoom to 10', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('zoom')).toBe(10)
})
})
describe('Props - Markers', () => {
it('accepts markers prop as array', () => {
const markers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('markers')).toEqual(markers)
})
it('defaults markers to empty array', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('markers')).toEqual([])
})
it('renders markers as AdvancedMarker components', () => {
const markers: TOvMapMarker[] = [
{ lat: 51.5074, lng: -0.1278, title: 'London' },
{ lat: 48.8566, lng: 2.3522, title: 'Paris' },
]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const markerComponents = wrapper.findAll('.marker-mock')
expect(markerComponents).toHaveLength(2)
})
it('renders marker with title', () => {
const markers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London Tower' }]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const markerComponents = wrapper.findAll('.marker-mock')
expect(markerComponents).toHaveLength(1)
})
it('renders marker with custom color', () => {
const markers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, color: '#FF0000' }]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('markers')).toEqual(markers)
})
it('renders marker with info window content', () => {
const markers: TOvMapMarker[] = [
{ lat: 51.5074, lng: -0.1278, info: '<p>Tower of London</p>' },
]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const markerProps = wrapper.props('markers') as TOvMapMarker[]
expect(markerProps?.[0]?.info).toBe('<p>Tower of London</p>')
})
})
describe('Props - Auto Center', () => {
it('accepts autoCenter prop as true', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoCenter: true,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('autoCenter')).toBe(true)
})
it('defaults autoCenter to true', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('autoCenter')).toBe(true)
})
})
describe('Props - Auto Location', () => {
it('accepts autoLocation prop as true', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('autoLocation')).toBe(true)
})
it('defaults autoLocation to true', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('autoLocation')).toBe(true)
})
})
describe('Props - Dimensions', () => {
it('accepts width prop', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
width: '500px',
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('width')).toBe('500px')
})
it('defaults width to 100%', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('width')).toBe('100%')
})
it('accepts height prop', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
height: '400px',
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('height')).toBe('400px')
})
it('defaults height to 100%', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('height')).toBe('100%')
})
})
describe('Emits - centered', () => {
it('emits centered event when center prop changes', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const newCenter = { lat: 51.5074, lng: -0.1278 }
await wrapper.setProps({ center: newCenter })
await flushPromises()
expect(wrapper.emitted('centered')).toBeTruthy()
expect(wrapper.emitted('centered')?.[0]?.[0]).toEqual(newCenter)
})
it('centered event includes updated center coordinates', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const newCenter = { lat: 40.7128, lng: -74.006 }
await wrapper.setProps({ center: newCenter })
expect(wrapper.emitted('centered')).toBeTruthy()
const emittedCenter = wrapper.emitted('centered')?.[0]?.[0]
expect(emittedCenter).toEqual(newCenter)
})
})
describe('Emits - zoomed', () => {
it('emits zoomed event', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm).toBeDefined()
// zoom_changed event would be triggered by GoogleMap
})
it('emits zoomed event when zoom prop changes', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await wrapper.setProps({ zoom: 15 })
expect(wrapper.emitted('zoomed')).toBeTruthy()
expect(wrapper.emitted('zoomed')?.[0]?.[0]).toBe(15)
})
})
describe('Emits - located', () => {
it('emits located event on mount when autoLocation is true', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await flushPromises()
expect(wrapper.emitted('located')).toBeTruthy()
const emitted = wrapper.emitted('located')?.[0]?.[0] as { lat: number; lng: number }
expect(emitted.lat).toBe(40.7128)
expect(emitted.lng).toBe(-74.006)
})
it('does not get location when autoLocation is false', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await flushPromises()
expect(wrapper.emitted('located')).toBeFalsy()
})
})
describe('Emits - marked', () => {
it('emits marked event when setMarkers is called', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const newMarkers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
wrapper.vm.setMarkers(newMarkers)
await flushPromises()
expect(wrapper.emitted('marked')).toBeTruthy()
})
it('emits marked event when delMarkers is called', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers: [{ lat: 51.5074, lng: -0.1278, title: 'London' }],
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const markerToDel: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
wrapper.vm.delMarkers(markerToDel)
await flushPromises()
expect(wrapper.emitted('marked')).toBeTruthy()
})
it('emits marked event when markers prop changes', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers: [],
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const newMarkers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
await wrapper.setProps({ markers: newMarkers })
expect(wrapper.emitted('marked')).toBeTruthy()
})
})
describe('Emits - clicked', () => {
it('emits clicked event with coordinates', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
// Simulate click event through GoogleMap component
const googleMap = wrapper.findComponent({ name: 'GoogleMap' })
await googleMap.vm.$emit('click', {
latLng: {
lat: () => 51.5074,
lng: () => -0.1278,
},
})
expect(wrapper.emitted('clicked')).toBeTruthy()
expect(wrapper.emitted('clicked')?.[0]?.[0]).toEqual({ lat: 51.5074, lng: -0.1278 })
})
})
describe('Emits - loading', () => {
it('emits loading event with true when geolocation starts', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
// Initially loading should be emitted
expect(wrapper.emitted('loading')).toBeTruthy()
})
it('emits loading event with false when geolocation completes', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await flushPromises()
const loadingEmits = wrapper.emitted('loading') as Array<[boolean]>
expect(loadingEmits.length).toBeGreaterThan(0)
// Last emit should be loading: false
expect(loadingEmits[loadingEmits.length - 1]?.[0]).toBe(false)
})
})
describe('Exposed Methods', () => {
it('exposes getMarkers method', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm.getMarkers).toBeDefined()
expect(typeof wrapper.vm.getMarkers).toBe('function')
})
it('exposes setMarkers method', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm.setMarkers).toBeDefined()
expect(typeof wrapper.vm.setMarkers).toBe('function')
})
it('exposes delMarkers method', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm.delMarkers).toBeDefined()
expect(typeof wrapper.vm.delMarkers).toBe('function')
})
it('exposes loading ref', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm.loading).toBeDefined()
expect(typeof wrapper.vm.loading).toBe('boolean')
})
it('exposes location ref', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm.location).toBeDefined()
expect(wrapper.vm.location).toHaveProperty('lat')
expect(wrapper.vm.location).toHaveProperty('lng')
})
})
describe('Marker Management', () => {
it('getMarkers returns current markers', async () => {
const markers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const result = wrapper.vm.getMarkers()
expect(result).toHaveLength(1)
expect(result[0]?.title).toBe('London')
})
it('setMarkers merges new markers with existing ones', async () => {
const initialMarkers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers: initialMarkers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const newMarkers: TOvMapMarker[] = [{ lat: 48.8566, lng: 2.3522, title: 'Paris' }]
wrapper.vm.setMarkers(newMarkers)
await flushPromises()
const result = wrapper.vm.getMarkers()
expect(result.length).toBeGreaterThanOrEqual(1)
})
it('setMarkers removes duplicates', async () => {
const initialMarkers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers: initialMarkers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const duplicateMarker: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
wrapper.vm.setMarkers(duplicateMarker)
await flushPromises()
const result = wrapper.vm.getMarkers()
expect(result.length).toBe(1)
})
it('delMarkers removes markers by lat/lng', async () => {
const initialMarkers: TOvMapMarker[] = [
{ lat: 51.5074, lng: -0.1278, title: 'London' },
{ lat: 48.8566, lng: 2.3522, title: 'Paris' },
]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers: initialMarkers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const markerToDelete: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278 }]
wrapper.vm.delMarkers(markerToDelete)
await flushPromises()
const result = wrapper.vm.getMarkers()
expect(result).toHaveLength(1)
expect(result[0]?.title).toBe('Paris')
})
})
describe('Geolocation', () => {
it('requests user location on mount when autoLocation is true', async () => {
const geolocationMock = vi.fn((success) =>
success({
coords: {
latitude: 40.7128,
longitude: -74.006,
},
}),
)
Object.defineProperty(navigator, 'geolocation', {
value: { getCurrentPosition: geolocationMock },
configurable: true,
})
mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await flushPromises()
expect(geolocationMock).toHaveBeenCalled()
})
it('updates location when geolocation succeeds', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await flushPromises()
expect(wrapper.vm.location).toEqual({ lat: 40.7128, lng: -74.006 })
})
it('centers map when geolocation succeeds and autoCenter is true', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
autoCenter: true,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await flushPromises()
// Verify centered event was emitted when geolocation succeeds
expect(wrapper.emitted('centered')).toBeTruthy()
const centeredEmit = wrapper.emitted('centered')?.[0]?.[0] as { lat: number; lng: number }
expect(centeredEmit).toEqual({ lat: 40.7128, lng: -74.006 })
})
it('does not center map when geolocation succeeds but autoCenter is false', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
autoCenter: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await flushPromises()
// When autoCenter is false, centered event should not be emitted from geolocation
// The component still receives location but doesn't center
expect(wrapper.vm.location).toEqual({ lat: 40.7128, lng: -74.006 })
})
it('handles geolocation errors gracefully', async () => {
const geolocationMock = vi.fn((success, error) => error(new Error('Permission denied')))
Object.defineProperty(navigator, 'geolocation', {
value: { getCurrentPosition: geolocationMock },
configurable: true,
})
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: true,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
// Mock console.error to avoid test output noise
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await flushPromises()
expect(wrapper.vm.loading).toBe(false)
consoleSpy.mockRestore()
})
})
describe('Mounting and Unmounting', () => {
it('mounts and initializes correctly', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 40.7128, lng: -74.006 },
zoom: 12,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('can be unmounted without errors', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await wrapper.unmount()
expect(wrapper).toBeDefined()
})
})
describe('Props Updates', () => {
it('handles center prop change', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const newCenter = { lat: 51.5074, lng: -0.1278 }
await wrapper.setProps({ center: newCenter })
// Verify centered event was emitted
expect(wrapper.emitted('centered')).toBeTruthy()
})
it('handles zoom prop change', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
await wrapper.setProps({ zoom: 15 })
// Verify zoomed event was emitted
expect(wrapper.emitted('zoomed')).toBeTruthy()
expect(wrapper.emitted('zoomed')?.[0]?.[0]).toBe(15)
})
it('handles markers prop change', async () => {
const initialMarkers: TOvMapMarker[] = []
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers: initialMarkers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const newMarkers: TOvMapMarker[] = [{ lat: 51.5074, lng: -0.1278, title: 'London' }]
await wrapper.setProps({ markers: newMarkers })
await flushPromises()
const result = wrapper.vm.getMarkers()
expect(result.length).toBeGreaterThanOrEqual(1)
})
})
describe('Edge Cases', () => {
it('handles empty markers array', () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers: [],
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm.getMarkers()).toEqual([])
})
it('handles marker with all optional properties', () => {
const markers: TOvMapMarker[] = [
{
lat: 51.5074,
lng: -0.1278,
title: 'London',
color: '#FF0000',
info: '<p>Info</p>',
},
]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.props('markers')).toEqual(markers)
})
it('handles multiple markers with same coordinates', async () => {
const markers: TOvMapMarker[] = [
{ lat: 51.5074, lng: -0.1278, title: 'Location A' },
{ lat: 51.5074, lng: -0.1278, title: 'Location B' },
]
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
markers,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(wrapper.vm.getMarkers()).toHaveLength(2)
})
it('handles clicking with null latLng', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
const googleMap = wrapper.findComponent({ name: 'GoogleMap' })
await googleMap.vm.$emit('click', { latLng: null })
expect(wrapper.emitted('clicked')).toBeFalsy()
})
it('handles rapid setMarkers calls', async () => {
const wrapper = mount(VOvMap, {
props: {
center: { lat: 0, lng: 0 },
zoom: 10,
autoLocation: false,
},
global: {
stubs: {
'v-overlay': true,
},
},
})
expect(() => {
wrapper.vm.setMarkers([{ lat: 51.5074, lng: -0.1278 }])
wrapper.vm.setMarkers([{ lat: 48.8566, lng: 2.3522 }])
wrapper.vm.setMarkers([{ lat: 52.52, lng: 13.405 }])
}).not.toThrow()
})
})
describe('Type Exports', () => {
it('exports TOvMapMarker type', () => {
const marker: TOvMapMarker = {
lat: 51.5074,
lng: -0.1278,
title: 'Test',
color: '#FF0000',
info: '<p>Test</p>',
}
expect(marker.lat).toBe(51.5074)
expect(marker.lng).toBe(-0.1278)
})
})
})VOvMedia.test.ts
ts
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import VOvMedia from '../VOvMedia.vue'
// Mock navigator.mediaDevices and related APIs
beforeAll(() => {
// Mock MediaStream
global.MediaStream = vi.fn().mockImplementation(() => ({
getTracks: vi.fn(() => [
{
stop: vi.fn(),
kind: 'video',
},
]),
})) as never
// Mock MediaRecorder
global.MediaRecorder = vi.fn().mockImplementation((stream) => ({
start: vi.fn(),
stop: vi.fn(),
ondataavailable: null,
onstop: null,
stream,
})) as never
// Mock HTMLMediaElement methods
HTMLMediaElement.prototype.play = vi.fn(() => Promise.resolve())
HTMLMediaElement.prototype.pause = vi.fn()
// Mock canvas methods
HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
drawImage: vi.fn(),
})) as never
HTMLCanvasElement.prototype.toDataURL = vi.fn(() => 'data:image/png;base64,test')
HTMLCanvasElement.prototype.toBlob = vi.fn((callback: BlobCallback) => {
callback(new Blob(['test'], { type: 'image/png' }))
})
// Mock navigator.mediaDevices
Object.defineProperty(global.navigator, 'mediaDevices', {
value: {
getUserMedia: vi
.fn()
.mockResolvedValue(new (global.MediaStream as unknown as typeof MediaStream)()),
enumerateDevices: vi.fn().mockResolvedValue([
{
deviceId: 'video-device-1',
kind: 'videoinput',
label: 'Camera 1',
groupId: 'group-1',
toJSON: () => ({}),
},
{
deviceId: 'audio-device-1',
kind: 'audioinput',
label: 'Microphone 1',
groupId: 'group-1',
toJSON: () => ({}),
},
]),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
},
writable: true,
configurable: true,
})
})
describe('VOvMedia', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('mounts without error with video prop', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
expect(wrapper.exists()).toBe(true)
})
it('mounts without error with audio prop', async () => {
const wrapper = mount(VOvMedia, {
props: {
audio: true,
},
})
await flushPromises()
expect(wrapper.exists()).toBe(true)
})
it('mounts without error with both video and audio', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
audio: true,
},
})
await flushPromises()
expect(wrapper.exists()).toBe(true)
})
it('mounts with src prop (no camera access needed)', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
src: 'https://example.com/video.mp4',
},
})
await flushPromises()
expect(wrapper.exists()).toBe(true)
expect(wrapper.props('src')).toBe('https://example.com/video.mp4')
})
it('renders video element when video prop is true', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
src: 'test.mp4',
},
})
await flushPromises()
const video = wrapper.find('video')
expect(video.exists()).toBe(true)
})
it('renders audio element when audio prop is true', async () => {
const wrapper = mount(VOvMedia, {
props: {
audio: true,
src: 'test.mp3',
},
})
await flushPromises()
const audio = wrapper.find('audio')
expect(audio.exists()).toBe(true)
})
it('renders canvas element (hidden)', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const canvas = wrapper.find('canvas')
expect(canvas.exists()).toBe(true)
expect(canvas.attributes('style')).toContain('display: none')
})
it('renders slot content', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
slots: {
default: '<div>Slot content</div>',
},
})
await flushPromises()
expect(wrapper.text()).toContain('Slot content')
})
})
describe('Props', () => {
it('accepts all prop types with correct defaults', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
expect(wrapper.props('autoplay')).toBe(true)
expect(wrapper.props('loop')).toBe(false)
expect(wrapper.props('compact')).toBe(false)
expect(wrapper.props('snap')).toBe(false)
expect(wrapper.props('variant')).toBe('flat')
expect(wrapper.props('density')).toBe('default')
expect(wrapper.props('recorderPosition')).toBe('bottom-left')
expect(wrapper.props('snapPosition')).toBe('top-right')
expect(wrapper.props('format')).toBe('base64')
})
it('accepts custom recorder position', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
recorderPosition: 'top-right',
},
})
await flushPromises()
expect(wrapper.props('recorderPosition')).toBe('top-right')
})
it('accepts custom snap position', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
snap: true,
snapPosition: 'bottom-right',
},
})
await flushPromises()
expect(wrapper.props('snapPosition')).toBe('bottom-right')
})
it('accepts media constraints', async () => {
const constraints = { width: 1280, height: 720 }
const wrapper = mount(VOvMedia, {
props: {
video: true,
videoConstraints: constraints,
},
})
await flushPromises()
expect(wrapper.props('videoConstraints')).toEqual(constraints)
})
it('accepts format as base64', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
format: 'base64',
},
})
await flushPromises()
expect(wrapper.props('format')).toBe('base64')
})
it('accepts format as blob', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
format: 'blob',
},
})
await flushPromises()
expect(wrapper.props('format')).toBe('blob')
})
})
describe('Emits', () => {
it('emits loading event on mount', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
expect(wrapper.emitted('loading')).toBeTruthy()
})
it('emits device event with available devices', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const deviceEmits = wrapper.emitted('device')
expect(deviceEmits).toBeTruthy()
expect(deviceEmits?.[0]?.[0]).toHaveProperty('devices')
expect(deviceEmits?.[0]?.[0]).toHaveProperty('device')
})
it('emits started event when camera starts', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const startedEmits = wrapper.emitted('started')
expect(startedEmits).toBeTruthy()
})
it('emits error when neither video nor audio is enabled', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: false,
audio: false,
},
})
await flushPromises()
const errorEmits = wrapper.emitted('error')
expect(errorEmits).toBeTruthy()
})
it('does not emit loading when src prop is provided', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
src: 'test.mp4',
},
})
await flushPromises()
// Should not attempt to load camera if src is provided
expect(wrapper.props('src')).toBe('test.mp4')
})
})
describe('Methods - listDevices', () => {
it('exposes listDevices method', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const vm = wrapper.vm as unknown as { listDevices: () => unknown }
expect(vm.listDevices).toBeDefined()
expect(typeof vm.listDevices).toBe('function')
})
it('listDevices returns video devices when video prop is true', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const vm = wrapper.vm as unknown as { listDevices: () => unknown }
const devices = vm.listDevices()
expect(Array.isArray(devices)).toBe(true)
})
it('listDevices returns audio devices when audio prop is true', async () => {
const wrapper = mount(VOvMedia, {
props: {
audio: true,
},
})
await flushPromises()
const vm = wrapper.vm as unknown as { listDevices: () => unknown }
const devices = vm.listDevices()
expect(Array.isArray(devices)).toBe(true)
})
})
describe('Methods - setDevice', () => {
it('exposes setDevice method', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const vm = wrapper.vm as unknown as { setDevice: (id: string) => void }
expect(vm.setDevice).toBeDefined()
expect(typeof vm.setDevice).toBe('function')
})
it('setDevice accepts a device ID', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const vm = wrapper.vm as unknown as { setDevice: (id: string) => void }
expect(() => vm.setDevice('video-device-1')).not.toThrow()
})
})
describe('Methods - audioPlayback', () => {
it('exposes audioPlayback method', async () => {
const wrapper = mount(VOvMedia, {
props: {
audio: true,
src: 'test.mp3',
},
})
await flushPromises()
const vm = wrapper.vm as unknown as { audioPlayback: () => unknown }
expect(vm.audioPlayback).toBeDefined()
expect(typeof vm.audioPlayback).toBe('function')
})
it('exposes isPlaying state', async () => {
const wrapper = mount(VOvMedia, {
props: {
audio: true,
src: 'test.mp3',
},
})
await flushPromises()
const vm = wrapper.vm as unknown as { isPlaying: unknown }
expect(vm.isPlaying).toBeDefined()
})
})
describe('Methods - videoRecording', () => {
it('exposes videoRecording method', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const vm = wrapper.vm as unknown as { videoRecording: () => unknown }
expect(vm.videoRecording).toBeDefined()
expect(typeof vm.videoRecording).toBe('function')
})
})
describe('UI Controls', () => {
it('shows record button when video is enabled and no src', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
// Button should exist for recording
const buttons = wrapper.findAll('button')
expect(buttons.length).toBeGreaterThan(0)
})
it('does not show record button when src is provided', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
src: 'test.mp4',
},
})
await flushPromises()
// Video with src shouldn't need recording button
expect(wrapper.props('src')).toBe('test.mp4')
})
it('shows snap button when snap prop is true', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
snap: true,
src: 'test.mp4',
},
})
await flushPromises()
expect(wrapper.props('snap')).toBe(true)
})
it('applies recorder position classes correctly', async () => {
const positions: Array<'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'> = [
'top-left',
'top-right',
'bottom-left',
'bottom-right',
]
for (const position of positions) {
const wrapper = mount(VOvMedia, {
props: {
video: true,
recorderPosition: position,
},
})
await flushPromises()
expect(wrapper.props('recorderPosition')).toBe(position)
}
})
it('applies snap position classes correctly', async () => {
const positions: Array<'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'> = [
'top-left',
'top-right',
'bottom-left',
'bottom-right',
]
for (const position of positions) {
const wrapper = mount(VOvMedia, {
props: {
video: true,
snap: true,
snapPosition: position,
},
})
await flushPromises()
expect(wrapper.props('snapPosition')).toBe(position)
}
})
})
describe('Button Variants', () => {
it('accepts button variant prop', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
variant: 'outlined',
},
})
await flushPromises()
expect(wrapper.props('variant')).toBe('outlined')
})
it('accepts button density prop', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
density: 'compact',
},
})
await flushPromises()
expect(wrapper.props('density')).toBe('compact')
})
it('renders with compact mode', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
compact: true,
},
})
await flushPromises()
expect(wrapper.props('compact')).toBe(true)
})
})
describe('Lifecycle', () => {
it('can be unmounted without errors', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
await wrapper.unmount()
expect(wrapper.vm).toBeDefined()
})
it('cleans up on unmount', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
await wrapper.unmount()
// Component should have cleaned up without errors
expect(wrapper.vm).toBeDefined()
})
it('handles mount with audio only', async () => {
const wrapper = mount(VOvMedia, {
props: {
audio: true,
},
})
await flushPromises()
expect(wrapper.exists()).toBe(true)
await wrapper.unmount()
expect(wrapper.vm).toBeDefined()
})
})
describe('Media Element Attributes', () => {
it('sets video autoplay attribute', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
src: 'test.mp4',
autoplay: true,
},
})
await flushPromises()
const video = wrapper.find('video')
expect(video.exists()).toBe(true)
})
it('sets video loop attribute', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
src: 'test.mp4',
loop: true,
},
})
await flushPromises()
const video = wrapper.find('video')
expect(video.exists()).toBe(true)
})
it('sets audio controls when src is provided', async () => {
const wrapper = mount(VOvMedia, {
props: {
audio: true,
src: 'test.mp3',
},
})
await flushPromises()
const audio = wrapper.find('audio')
expect(audio.exists()).toBe(true)
})
it('does not show audio controls in compact mode with src', async () => {
const wrapper = mount(VOvMedia, {
props: {
audio: true,
src: 'test.mp3',
compact: true,
},
})
await flushPromises()
const audio = wrapper.find('audio')
expect(audio.exists()).toBe(true)
expect(wrapper.props('compact')).toBe(true)
})
})
describe('Edge Cases', () => {
it('handles prop changes without errors', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
await wrapper.setProps({ compact: true })
expect(wrapper.props('compact')).toBe(true)
await flushPromises()
})
it('handles multiple prop updates', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
recorderPosition: 'bottom-left',
},
})
await flushPromises()
await wrapper.setProps({ recorderPosition: 'top-right' })
await wrapper.setProps({ snap: true })
await wrapper.setProps({ snapPosition: 'bottom-left' })
expect(wrapper.props('recorderPosition')).toBe('top-right')
expect(wrapper.props('snap')).toBe(true)
expect(wrapper.props('snapPosition')).toBe('bottom-left')
})
it('handles both video and audio constraints', async () => {
const videoConstraints = { width: 1280, height: 720 }
const audioConstraints = { echoCancellation: true }
const wrapper = mount(VOvMedia, {
props: {
video: true,
audio: true,
videoConstraints,
audioConstraints,
},
})
await flushPromises()
expect(wrapper.props('videoConstraints')).toEqual(videoConstraints)
expect(wrapper.props('audioConstraints')).toEqual(audioConstraints)
})
})
describe('Container Styling', () => {
it('renders container div with correct class', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const container = wrapper.find('.container')
expect(container.exists()).toBe(true)
})
it('contains slot wrapper div', async () => {
const wrapper = mount(VOvMedia, {
props: {
video: true,
},
})
await flushPromises()
const slot = wrapper.find('.slot')
expect(slot.exists()).toBe(true)
})
})
})VOvPad.test.ts
ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import VOvPad from '../VOvPad.vue'
// Mock canvas context and related browser APIs
beforeEach(() => {
// Mock HTMLCanvasElement.getContext
HTMLCanvasElement.prototype.getContext = vi.fn(
() =>
({
clearRect: vi.fn(),
fillRect: vi.fn(),
beginPath: vi.fn(),
moveTo: vi.fn(),
lineTo: vi.fn(),
stroke: vi.fn(),
fill: vi.fn(),
closePath: vi.fn(),
setLineDash: vi.fn(),
ellipse: vi.fn(),
drawImage: vi.fn(),
fillStyle: '',
strokeStyle: '',
lineWidth: 1,
lineJoin: 'miter' as CanvasLineJoin,
lineCap: 'round' as CanvasLineCap,
globalCompositeOperation: 'source-over',
setTransform: vi.fn(),
}) as unknown as CanvasRenderingContext2D,
) as unknown as HTMLCanvasElement['getContext']
// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
})) as unknown as typeof ResizeObserver
// Mock Image
global.Image = vi.fn().mockImplementation(() => ({
src: '',
})) as unknown as typeof Image
})
afterEach(() => {
vi.clearAllMocks()
})
describe('VOvPad', () => {
describe('Rendering', () => {
it('mounts without error', () => {
const wrapper = mount(VOvPad, {
props: {
width: '100%',
height: '240px',
},
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders canvas element', () => {
const wrapper = mount(VOvPad, {
props: {
width: '100%',
height: '240px',
},
})
const canvas = wrapper.find('canvas')
expect(canvas.exists()).toBe(true)
})
it('renders container div', () => {
const wrapper = mount(VOvPad, {
props: {
width: '100%',
height: '240px',
},
})
const container = wrapper.find('#container')
expect(container.exists()).toBe(true)
})
})
describe('Props - Dimensions', () => {
it('accepts width prop as string', () => {
const wrapper = mount(VOvPad, {
props: {
width: '100%',
height: '240px',
},
})
expect(wrapper.props('width')).toBe('100%')
})
it('accepts height prop as string', () => {
const wrapper = mount(VOvPad, {
props: {
width: '100%',
height: '240px',
},
})
expect(wrapper.props('height')).toBe('240px')
})
it('accepts width prop as number', () => {
const wrapper = mount(VOvPad, {
props: {
width: 500,
height: 300,
},
})
expect(wrapper.props('width')).toBe(500)
})
it('accepts height prop as number', () => {
const wrapper = mount(VOvPad, {
props: {
width: 500,
height: 300,
},
})
expect(wrapper.props('height')).toBe(300)
})
it('defaults to 100% width and height', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('width')).toBe('100%')
expect(wrapper.props('height')).toBe('100%')
})
})
describe('Props - Drawing Settings', () => {
it('accepts color prop', () => {
const wrapper = mount(VOvPad, {
props: {
color: '#FF0000',
},
})
expect(wrapper.props('color')).toBe('#FF0000')
})
it('defaults color to black', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('color')).toBe('#000000')
})
it('accepts lineWidth prop', () => {
const wrapper = mount(VOvPad, {
props: {
lineWidth: 10,
},
})
expect(wrapper.props('lineWidth')).toBe(10)
})
it('defaults lineWidth to 5', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('lineWidth')).toBe(5)
})
it('accepts backgroundColor prop', () => {
const wrapper = mount(VOvPad, {
props: {
backgroundColor: '#CCCCCC',
},
})
expect(wrapper.props('backgroundColor')).toBe('#CCCCCC')
})
it('defaults backgroundColor to white', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('backgroundColor')).toBe('#FFFFFF')
})
it('accepts lineCap prop with valid values', () => {
const wrapper = mount(VOvPad, {
props: {
lineCap: 'square',
},
})
expect(wrapper.props('lineCap')).toBe('square')
})
it('defaults lineCap to round', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('lineCap')).toBe('round')
})
it('accepts lineJoin prop with valid values', () => {
const wrapper = mount(VOvPad, {
props: {
lineJoin: 'round',
},
})
expect(wrapper.props('lineJoin')).toBe('round')
})
it('defaults lineJoin to miter', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('lineJoin')).toBe('miter')
})
})
describe('Props - Stroke Type', () => {
it('accepts strokeType prop with dash', () => {
const wrapper = mount(VOvPad, {
props: {
strokeType: 'dash',
},
})
expect(wrapper.props('strokeType')).toBe('dash')
})
it('accepts strokeType prop with line', () => {
const wrapper = mount(VOvPad, {
props: {
strokeType: 'line',
},
})
expect(wrapper.props('strokeType')).toBe('line')
})
it('accepts strokeType prop with square', () => {
const wrapper = mount(VOvPad, {
props: {
strokeType: 'square',
},
})
expect(wrapper.props('strokeType')).toBe('square')
})
it('accepts strokeType prop with circle', () => {
const wrapper = mount(VOvPad, {
props: {
strokeType: 'circle',
},
})
expect(wrapper.props('strokeType')).toBe('circle')
})
it('accepts strokeType prop with triangle', () => {
const wrapper = mount(VOvPad, {
props: {
strokeType: 'triangle',
},
})
expect(wrapper.props('strokeType')).toBe('triangle')
})
it('accepts strokeType prop with half_triangle', () => {
const wrapper = mount(VOvPad, {
props: {
strokeType: 'half_triangle',
},
})
expect(wrapper.props('strokeType')).toBe('half_triangle')
})
it('defaults strokeType to dash', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('strokeType')).toBe('dash')
})
})
describe('Props - Shape Filling', () => {
it('accepts fillShape prop as true', () => {
const wrapper = mount(VOvPad, {
props: {
fillShape: true,
strokeType: 'circle',
},
})
expect(wrapper.props('fillShape')).toBe(true)
})
it('accepts fillShape prop as false', () => {
const wrapper = mount(VOvPad, {
props: {
fillShape: false,
},
})
expect(wrapper.props('fillShape')).toBe(false)
})
it('defaults fillShape to false', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('fillShape')).toBe(false)
})
})
describe('Props - Eraser', () => {
it('accepts eraser prop as true', () => {
const wrapper = mount(VOvPad, {
props: {
eraser: true,
},
})
expect(wrapper.props('eraser')).toBe(true)
})
it('accepts eraser prop as false', () => {
const wrapper = mount(VOvPad, {
props: {
eraser: false,
},
})
expect(wrapper.props('eraser')).toBe(false)
})
it('defaults eraser to false', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('eraser')).toBe(false)
})
it('renders canvas with data-eraser attribute when eraser is true', async () => {
const wrapper = mount(VOvPad, {
props: {
eraser: true,
},
})
await wrapper.vm.$nextTick()
const canvas = wrapper.find('canvas')
expect(canvas.attributes('data-eraser')).toBe('true')
})
it('renders canvas with data-eraser attribute as false when eraser is false', async () => {
const wrapper = mount(VOvPad, {
props: {
eraser: false,
},
})
await wrapper.vm.$nextTick()
const canvas = wrapper.find('canvas')
expect(canvas.attributes('data-eraser')).toBe('false')
})
})
describe('Props - Lock', () => {
it('accepts lock prop as true', () => {
const wrapper = mount(VOvPad, {
props: {
lock: true,
},
})
expect(wrapper.props('lock')).toBe(true)
})
it('accepts lock prop as false', () => {
const wrapper = mount(VOvPad, {
props: {
lock: false,
},
})
expect(wrapper.props('lock')).toBe(false)
})
it('defaults lock to false', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('lock')).toBe(false)
})
})
describe('Props - Canvas ID', () => {
it('accepts custom canvasId prop', () => {
const wrapper = mount(VOvPad, {
props: {
canvasId: 'my-custom-canvas',
},
})
expect(wrapper.props('canvasId')).toBe('my-custom-canvas')
})
it('generates canvasId with random suffix by default', () => {
const wrapper = mount(VOvPad)
const id = wrapper.props('canvasId')
expect(id).toMatch(/^canvas-/)
expect(typeof id).toBe('string')
})
})
describe('Props - Output Format', () => {
it('accepts saveAs prop as jpeg', () => {
const wrapper = mount(VOvPad, {
props: {
saveAs: 'jpeg',
},
})
expect(wrapper.props('saveAs')).toBe('jpeg')
})
it('accepts saveAs prop as png', () => {
const wrapper = mount(VOvPad, {
props: {
saveAs: 'png',
},
})
expect(wrapper.props('saveAs')).toBe('png')
})
it('defaults saveAs to png', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('saveAs')).toBe('png')
})
})
describe('Props - Output Dimensions', () => {
it('accepts outputWidth prop', () => {
const wrapper = mount(VOvPad, {
props: {
outputWidth: 800,
},
})
expect(wrapper.props('outputWidth')).toBe(800)
})
it('accepts outputHeight prop', () => {
const wrapper = mount(VOvPad, {
props: {
outputHeight: 600,
},
})
expect(wrapper.props('outputHeight')).toBe(600)
})
})
describe('Props - Images', () => {
it('accepts initialImage prop as array', () => {
const initialImage: Array<never> = []
const wrapper = mount(VOvPad, {
props: {
initialImage,
},
})
expect(wrapper.props('initialImage')).toEqual([])
})
it('accepts additionalImages prop as array', () => {
const additionalImages: Array<never> = []
const wrapper = mount(VOvPad, {
props: {
additionalImages,
},
})
expect(wrapper.props('additionalImages')).toEqual([])
})
it('accepts backgroundImage prop', () => {
const wrapper = mount(VOvPad, {
props: {
backgroundImage: 'https://example.com/image.png',
},
})
expect(wrapper.props('backgroundImage')).toBe('https://example.com/image.png')
})
it('defaults backgroundImage to null', () => {
const wrapper = mount(VOvPad)
expect(wrapper.props('backgroundImage')).toBeNull()
})
})
describe('Exposed Methods', () => {
it('exposes clear method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.clear).toBeDefined()
expect(typeof wrapper.vm.clear).toBe('function')
})
it('exposes reset method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.reset).toBeDefined()
expect(typeof wrapper.vm.reset).toBe('function')
})
it('exposes undo method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.undo).toBeDefined()
expect(typeof wrapper.vm.undo).toBe('function')
})
it('exposes redo method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.redo).toBeDefined()
expect(typeof wrapper.vm.redo).toBe('function')
})
it('exposes save method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.save).toBeDefined()
expect(typeof wrapper.vm.save).toBe('function')
})
it('exposes startDraw method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.startDraw).toBeDefined()
expect(typeof wrapper.vm.startDraw).toBe('function')
})
it('exposes draw method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.draw).toBeDefined()
expect(typeof wrapper.vm.draw).toBe('function')
})
it('exposes stopDraw method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.stopDraw).toBeDefined()
expect(typeof wrapper.vm.stopDraw).toBe('function')
})
it('exposes handleResize method', () => {
const wrapper = mount(VOvPad)
expect(wrapper.vm.handleResize).toBeDefined()
expect(typeof wrapper.vm.handleResize).toBe('function')
})
})
describe('Reset Functionality', () => {
it('can call reset method when not locked', () => {
const wrapper = mount(VOvPad, {
props: {
lock: false,
},
})
expect(() => wrapper.vm.reset()).not.toThrow()
})
it('cannot call reset when locked', () => {
const wrapper = mount(VOvPad, {
props: {
lock: true,
},
})
expect(() => wrapper.vm.reset()).not.toThrow()
})
})
describe('Undo/Redo Functionality', () => {
it('can call undo method when not locked', () => {
const wrapper = mount(VOvPad, {
props: {
lock: false,
},
})
expect(() => wrapper.vm.undo()).not.toThrow()
})
it('can call redo method when not locked', () => {
const wrapper = mount(VOvPad, {
props: {
lock: false,
},
})
expect(() => wrapper.vm.redo()).not.toThrow()
})
it('cannot call undo when locked', () => {
const wrapper = mount(VOvPad, {
props: {
lock: true,
},
})
expect(() => wrapper.vm.undo()).not.toThrow()
})
it('cannot call redo when locked', () => {
const wrapper = mount(VOvPad, {
props: {
lock: true,
},
})
expect(() => wrapper.vm.redo()).not.toThrow()
})
})
describe('Save Functionality', () => {
it('save method can be called without error', () => {
const wrapper = mount(VOvPad)
expect(() => wrapper.vm.save()).not.toThrow()
})
it('emits update:image event on save', () => {
const wrapper = mount(VOvPad)
wrapper.vm.save()
expect(wrapper.emitted('update:image')).toBeTruthy()
})
it('respects saveAs prop format in emitted image', () => {
const wrapper = mount(VOvPad, {
props: {
saveAs: 'jpeg',
},
})
wrapper.vm.save()
const emitted = wrapper.emitted('update:image')
expect(emitted).toBeTruthy()
})
})
describe('Canvas Event Handlers', () => {
it('canvas element exists and is rendered', async () => {
const wrapper = mount(VOvPad)
const canvas = wrapper.find('canvas')
expect(canvas.exists()).toBe(true)
})
it('canvas has pointer event handlers attached', async () => {
const wrapper = mount(VOvPad)
const canvas = wrapper.find('canvas')
expect(canvas.exists()).toBe(true)
// Event handlers are attached in the template via Vue directives
})
})
describe('Touch Action Prevention', () => {
it('container element exists for touch event handling', async () => {
const wrapper = mount(VOvPad)
const container = wrapper.find('#container')
expect(container.exists()).toBe(true)
// Touch move prevention is handled via @touchmove.prevent in template
})
})
describe('Canvas Style', () => {
it('canvas is rendered with proper attributes', () => {
const wrapper = mount(VOvPad)
const canvas = wrapper.find('canvas')
expect(canvas.exists()).toBe(true)
// Canvas has width and height attributes set dynamically
expect(canvas.element).toBeDefined()
})
})
describe('Mounting and Unmounting', () => {
it('mounts and initializes correctly', async () => {
const wrapper = mount(VOvPad, {
props: {
width: '100%',
height: '240px',
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('can be unmounted without errors', async () => {
const wrapper = mount(VOvPad)
await wrapper.unmount()
expect(wrapper).toBeDefined()
})
it('removes resize listener on unmount', async () => {
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
const wrapper = mount(VOvPad)
await wrapper.unmount()
expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function))
removeEventListenerSpy.mockRestore()
})
})
describe('Props Updates', () => {
it('handles color prop change', async () => {
const wrapper = mount(VOvPad, {
props: {
color: '#000000',
},
})
await wrapper.setProps({ color: '#FF0000' })
expect(wrapper.props('color')).toBe('#FF0000')
})
it('handles lineWidth prop change', async () => {
const wrapper = mount(VOvPad, {
props: {
lineWidth: 5,
},
})
await wrapper.setProps({ lineWidth: 10 })
expect(wrapper.props('lineWidth')).toBe(10)
})
it('handles backgroundColor prop change', async () => {
const wrapper = mount(VOvPad, {
props: {
backgroundColor: '#FFFFFF',
},
})
await wrapper.setProps({ backgroundColor: '#CCCCCC' })
expect(wrapper.props('backgroundColor')).toBe('#CCCCCC')
await flushPromises()
})
it('handles eraser prop change', async () => {
const wrapper = mount(VOvPad, {
props: {
eraser: false,
},
})
await wrapper.setProps({ eraser: true })
expect(wrapper.props('eraser')).toBe(true)
})
it('handles strokeType prop change', async () => {
const wrapper = mount(VOvPad, {
props: {
strokeType: 'dash',
},
})
await wrapper.setProps({ strokeType: 'circle' })
expect(wrapper.props('strokeType')).toBe('circle')
})
})
describe('Integration with Sandbox Example', () => {
it('can be used with all sandbox props like sandbox-pad.vue example', async () => {
const wrapper = mount(VOvPad, {
props: {
width: '100%',
height: '240px',
lock: false,
eraser: false,
backgroundColor: 'white',
backgroundImage: '',
lineWidth: 5,
color: '#000000',
},
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.props('width')).toBe('100%')
expect(wrapper.props('height')).toBe('240px')
expect(wrapper.props('lock')).toBe(false)
expect(wrapper.props('eraser')).toBe(false)
expect(wrapper.props('backgroundColor')).toBe('white')
expect(wrapper.props('lineWidth')).toBe(5)
expect(wrapper.props('color')).toBe('#000000')
})
it('supports all control methods used in sandbox-pad.vue example', () => {
const wrapper = mount(VOvPad)
expect(typeof wrapper.vm.reset).toBe('function')
expect(typeof wrapper.vm.undo).toBe('function')
expect(typeof wrapper.vm.redo).toBe('function')
expect(typeof wrapper.vm.save).toBe('function')
})
})
describe('Edge Cases', () => {
it('handles props with undefined values', () => {
const wrapper = mount(VOvPad, {
props: {
color: undefined,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles multiple rapid save calls', () => {
const wrapper = mount(VOvPad)
expect(() => {
wrapper.vm.save()
wrapper.vm.save()
wrapper.vm.save()
}).not.toThrow()
})
it('handles canvas resize event', async () => {
const wrapper = mount(VOvPad)
expect(() => wrapper.vm.handleResize()).not.toThrow()
})
})
})VOvShare.test.ts
ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import VOvShare, { type Share } from '../VOvShare.vue'
// Mock vue-socials components
vi.mock('vue-socials', () => ({
STwitter: {
name: 'STwitter',
template: '<div><slot /></div>',
},
SFacebook: {
name: 'SFacebook',
template: '<div><slot /></div>',
},
SLinkedIn: {
name: 'SLinkedIn',
template: '<div><slot /></div>',
},
SWhatsApp: {
name: 'SWhatsApp',
template: '<div><slot /></div>',
},
}))
describe('VOvShare', () => {
beforeEach(() => {
// Mock navigator.clipboard
Object.assign(navigator, {
clipboard: {
writeText: vi.fn(() => Promise.resolve()),
},
})
// Mock document.execCommand - it doesn't exist in jsdom by default
if (!document.execCommand) {
document.execCommand = vi.fn(() => true)
}
})
afterEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('mounts without error', () => {
const wrapper = mount(VOvShare)
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders v-defaults-provider wrapper', () => {
const wrapper = mount(VOvShare)
expect(wrapper.findComponent({ name: 'VDefaultsProvider' }).exists()).toBe(true)
})
})
describe('Props - Share', () => {
it('defaults to all share options', () => {
const wrapper = mount(VOvShare)
expect(wrapper.props('share')).toEqual([
'twitter',
'facebook',
'linkedin',
'whatsapp',
'copy',
])
})
it('accepts custom share array', () => {
const customShare: Share[] = ['twitter', 'facebook']
const wrapper = mount(VOvShare, {
props: {
share: customShare,
},
})
expect(wrapper.props('share')).toEqual(customShare)
})
it('accepts empty share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: [],
},
})
expect(wrapper.props('share')).toEqual([])
})
it('accepts individual share options', () => {
const validOptions: Share[] = ['twitter', 'facebook', 'linkedin', 'whatsapp', 'copy']
validOptions.forEach((option) => {
const wrapper = mount(VOvShare, {
props: {
share: [option],
},
})
expect(wrapper.props('share')).toContain(option)
})
})
})
describe('Props - Window Features', () => {
it('accepts windowFeatures prop', () => {
const windowFeatures = { width: 600, height: 400, top: 100, left: 100 }
const wrapper = mount(VOvShare, {
props: {
windowFeatures,
},
})
expect(wrapper.props('windowFeatures')).toEqual(windowFeatures)
})
it('defaults windowFeatures to empty object', () => {
const wrapper = mount(VOvShare)
expect(wrapper.props('windowFeatures')).toEqual({ url: '' })
})
it('accepts windowFeatures with all properties', () => {
const windowFeatures = { width: 800, height: 600, top: 50, left: 50 }
const wrapper = mount(VOvShare, {
props: {
windowFeatures,
},
})
const features = wrapper.props('windowFeatures') as typeof windowFeatures
expect(features.width).toBe(800)
expect(features.height).toBe(600)
expect(features.top).toBe(50)
expect(features.left).toBe(50)
})
})
describe('Props - Share Options', () => {
it('accepts shareOptions prop', () => {
const shareOptions = {
text: 'Check this out!',
url: 'https://example.com',
via: 'username',
hashtags: ['tag1', 'tag2'],
number: '1234567890',
}
const wrapper = mount(VOvShare, {
props: {
shareOptions,
},
})
expect(wrapper.props('shareOptions')).toEqual(shareOptions)
})
it('defaults shareOptions to empty object', () => {
const wrapper = mount(VOvShare)
expect(wrapper.props('shareOptions')).toEqual({ number: '' })
})
it('accepts shareOptions with all optional properties', () => {
const shareOptions = {
text: 'Share text',
url: 'https://example.com',
via: 'twitter_handle',
hashtags: ['tag1'],
quote: 'Quote text',
number: '123456',
}
const wrapper = mount(VOvShare, {
props: {
shareOptions,
},
})
const options = wrapper.props('shareOptions') as typeof shareOptions
expect(options.text).toBe('Share text')
expect(options.url).toBe('https://example.com')
expect(options.via).toBe('twitter_handle')
expect(options.hashtags).toContain('tag1')
expect(options.quote).toBe('Quote text')
expect(options.number).toBe('123456')
})
})
describe('Props - Behavior', () => {
it('accepts useNativeBehavior prop as true', () => {
const wrapper = mount(VOvShare, {
props: {
useNativeBehavior: true,
},
})
expect(wrapper.props('useNativeBehavior')).toBe(true)
})
it('accepts useNativeBehavior prop as false', () => {
const wrapper = mount(VOvShare, {
props: {
useNativeBehavior: false,
},
})
expect(wrapper.props('useNativeBehavior')).toBe(false)
})
it('defaults useNativeBehavior to false', () => {
const wrapper = mount(VOvShare)
expect(wrapper.props('useNativeBehavior')).toBe(false)
})
})
describe('Props - Button Styling', () => {
it('accepts variant prop', () => {
const variants: Array<'flat' | 'text' | 'elevated' | 'tonal' | 'outlined' | 'plain'> = [
'flat',
'text',
'elevated',
'tonal',
'outlined',
'plain',
]
variants.forEach((variant) => {
const wrapper = mount(VOvShare, {
props: {
variant,
},
})
expect(wrapper.props('variant')).toBe(variant)
})
})
it('defaults variant to undefined', () => {
const wrapper = mount(VOvShare)
expect(wrapper.props('variant')).toBeUndefined()
})
it('accepts density prop', () => {
const densities: Array<'default' | 'comfortable' | 'compact'> = [
'default',
'comfortable',
'compact',
]
densities.forEach((density) => {
const wrapper = mount(VOvShare, {
props: {
density,
},
})
expect(wrapper.props('density')).toBe(density)
})
})
it('defaults density to undefined', () => {
const wrapper = mount(VOvShare)
expect(wrapper.props('density')).toBeUndefined()
})
it('accepts color prop', () => {
const wrapper = mount(VOvShare, {
props: {
color: 'primary',
},
})
expect(wrapper.props('color')).toBe('primary')
})
it('defaults color to undefined', () => {
const wrapper = mount(VOvShare)
expect(wrapper.props('color')).toBeUndefined()
})
})
describe('Social Media Components Rendering', () => {
it('renders Twitter button when twitter is in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
},
})
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(true)
})
it('does not render Twitter button when twitter is not in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['facebook'],
},
})
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(false)
})
it('renders Facebook button when facebook is in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['facebook'],
},
})
expect(wrapper.findComponent({ name: 'SFacebook' }).exists()).toBe(true)
})
it('does not render Facebook button when facebook is not in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
},
})
expect(wrapper.findComponent({ name: 'SFacebook' }).exists()).toBe(false)
})
it('renders LinkedIn button when linkedin is in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['linkedin'],
},
})
expect(wrapper.findComponent({ name: 'SLinkedIn' }).exists()).toBe(true)
})
it('does not render LinkedIn button when linkedin is not in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
},
})
expect(wrapper.findComponent({ name: 'SLinkedIn' }).exists()).toBe(false)
})
it('renders WhatsApp button when whatsapp is in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['whatsapp'],
},
})
expect(wrapper.findComponent({ name: 'SWhatsApp' }).exists()).toBe(true)
})
it('does not render WhatsApp button when whatsapp is not in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
},
})
expect(wrapper.findComponent({ name: 'SWhatsApp' }).exists()).toBe(false)
})
it('renders all social media buttons when all are in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter', 'facebook', 'linkedin', 'whatsapp'],
},
})
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SFacebook' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SLinkedIn' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SWhatsApp' }).exists()).toBe(true)
})
it('renders no social media buttons when share array is empty', () => {
const wrapper = mount(VOvShare, {
props: {
share: [],
},
})
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(false)
expect(wrapper.findComponent({ name: 'SFacebook' }).exists()).toBe(false)
expect(wrapper.findComponent({ name: 'SLinkedIn' }).exists()).toBe(false)
expect(wrapper.findComponent({ name: 'SWhatsApp' }).exists()).toBe(false)
})
})
describe('Copy Button', () => {
it('renders copy button when copy is in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
expect(copyBtn.exists()).toBe(true)
})
it('does not render copy button when copy is not in share array', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
expect(copyBtn.exists()).toBe(false)
})
it('copy button has data-cy attribute when rendered', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
expect(copyBtn.exists()).toBe(true)
expect(copyBtn.attributes('data-cy')).toBe('v-bsb-share-copy')
})
it('copy button is clickable', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
expect(copyBtn.exists()).toBe(true)
})
})
describe('Copy to Clipboard Functionality', () => {
it('copies text to clipboard when copy button is clicked', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
shareOptions: {
text: 'Text to copy',
url: 'https://example.com',
number: '123',
},
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
await copyBtn.trigger('click')
await flushPromises()
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Text to copy')
})
it('copies empty string when shareOptions.text is not provided', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
shareOptions: {
url: 'https://example.com',
number: '123',
},
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
await copyBtn.trigger('click')
await flushPromises()
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('')
})
it('uses fallback method when navigator.clipboard is not available', async () => {
// Mock missing clipboard
const originalClipboard = navigator.clipboard
Object.defineProperty(navigator, 'clipboard', {
value: undefined,
writable: true,
})
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
shareOptions: {
text: 'Fallback copy test',
url: 'https://example.com',
number: '123',
},
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
await copyBtn.trigger('click')
await flushPromises()
expect(document.execCommand).toHaveBeenCalledWith('copy')
// Restore clipboard
Object.defineProperty(navigator, 'clipboard', {
value: originalClipboard,
writable: true,
})
})
it('handles clipboard API errors gracefully', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
Object.assign(navigator, {
clipboard: {
writeText: vi.fn(() => Promise.reject(new Error('Clipboard error'))),
},
})
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
shareOptions: {
text: 'Error test',
url: 'https://example.com',
number: '123',
},
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
await copyBtn.trigger('click')
await flushPromises()
expect(consoleErrorSpy).toHaveBeenCalled()
consoleErrorSpy.mockRestore()
})
})
describe('Social Media Button Icons', () => {
it('Twitter button element exists when twitter is in share array', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
},
})
await flushPromises()
const twitterBtn = wrapper.find('[data-cy="v-bsb-share-twitter"]')
expect(twitterBtn.exists()).toBe(true)
})
it('Facebook button element exists when facebook is in share array', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['facebook'],
},
})
await flushPromises()
const facebookBtn = wrapper.find('[data-cy="v-bsb-share-facebook"]')
expect(facebookBtn.exists()).toBe(true)
})
it('LinkedIn button element exists when linkedin is in share array', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['linkedin'],
},
})
await flushPromises()
const linkedInBtn = wrapper.find('[data-cy="v-bsb-share-linkedin"]')
expect(linkedInBtn.exists()).toBe(true)
})
it('WhatsApp button element exists when whatsapp is in share array', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['whatsapp'],
},
})
await flushPromises()
const whatsappBtn = wrapper.find('[data-cy="v-bsb-share-whatsapp"]')
expect(whatsappBtn.exists()).toBe(true)
})
})
describe('V-Defaults-Provider Props', () => {
it('passes variant to v-defaults-provider when defined', () => {
const wrapper = mount(VOvShare, {
props: {
variant: 'flat',
},
})
expect(wrapper.vm).toBeDefined()
})
it('passes density to v-defaults-provider when defined', () => {
const wrapper = mount(VOvShare, {
props: {
density: 'compact',
},
})
expect(wrapper.vm).toBeDefined()
})
it('passes color to v-defaults-provider when defined', () => {
const wrapper = mount(VOvShare, {
props: {
color: 'error',
},
})
expect(wrapper.vm).toBeDefined()
})
it('passes all styling props to v-defaults-provider', () => {
const wrapper = mount(VOvShare, {
props: {
variant: 'outlined',
density: 'comfortable',
color: 'warning',
},
})
expect(wrapper.props('variant')).toBe('outlined')
expect(wrapper.props('density')).toBe('comfortable')
expect(wrapper.props('color')).toBe('warning')
})
})
describe('Props Passing to Social Components', () => {
it('passes windowFeatures to social media components', () => {
const windowFeatures = { width: 700, height: 500, top: 75, left: 75 }
const wrapper = mount(VOvShare, {
props: {
share: ['twitter', 'facebook'],
windowFeatures,
},
})
expect(wrapper.props('windowFeatures')).toEqual(windowFeatures)
})
it('passes shareOptions to social media components', () => {
const shareOptions = {
text: 'Shared text',
url: 'https://example.com',
via: 'handle',
hashtags: ['test'],
number: '999',
}
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
shareOptions,
},
})
expect(wrapper.props('shareOptions')).toEqual(shareOptions)
})
it('passes useNativeBehavior to social media components', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['facebook', 'linkedin'],
useNativeBehavior: true,
},
})
expect(wrapper.props('useNativeBehavior')).toBe(true)
})
})
describe('Data Attributes for Testing', () => {
it('Twitter button has data-cy attribute', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
},
})
const twitterBtn = wrapper.find('[data-cy="v-bsb-share-twitter"]')
expect(twitterBtn.attributes('data-cy')).toBe('v-bsb-share-twitter')
})
it('Facebook button has data-cy attribute', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['facebook'],
},
})
const facebookBtn = wrapper.find('[data-cy="v-bsb-share-facebook"]')
expect(facebookBtn.attributes('data-cy')).toBe('v-bsb-share-facebook')
})
it('LinkedIn button has data-cy attribute', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['linkedin'],
},
})
const linkedInBtn = wrapper.find('[data-cy="v-bsb-share-linkedin"]')
expect(linkedInBtn.attributes('data-cy')).toBe('v-bsb-share-linkedin')
})
it('WhatsApp button has data-cy attribute', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['whatsapp'],
},
})
const whatsappBtn = wrapper.find('[data-cy="v-bsb-share-whatsapp"]')
expect(whatsappBtn.attributes('data-cy')).toBe('v-bsb-share-whatsapp')
})
it('Copy button has data-cy attribute', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
expect(copyBtn.attributes('data-cy')).toBe('v-bsb-share-copy')
})
})
describe('Mounting and Unmounting', () => {
it('mounts and initializes correctly', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter', 'facebook', 'linkedin', 'whatsapp', 'copy'],
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('can be unmounted without errors', async () => {
const wrapper = mount(VOvShare)
await wrapper.unmount()
expect(wrapper).toBeDefined()
})
})
describe('Props Updates', () => {
it('updates share prop dynamically', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter'],
},
})
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SFacebook' }).exists()).toBe(false)
await wrapper.setProps({ share: ['facebook'] })
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(false)
expect(wrapper.findComponent({ name: 'SFacebook' }).exists()).toBe(true)
})
it('updates variant prop dynamically', async () => {
const wrapper = mount(VOvShare, {
props: {
variant: 'flat',
},
})
expect(wrapper.props('variant')).toBe('flat')
await wrapper.setProps({ variant: 'outlined' })
expect(wrapper.props('variant')).toBe('outlined')
})
it('updates density prop dynamically', async () => {
const wrapper = mount(VOvShare, {
props: {
density: 'default',
},
})
expect(wrapper.props('density')).toBe('default')
await wrapper.setProps({ density: 'compact' })
expect(wrapper.props('density')).toBe('compact')
})
it('updates color prop dynamically', async () => {
const wrapper = mount(VOvShare, {
props: {
color: 'primary',
},
})
expect(wrapper.props('color')).toBe('primary')
await wrapper.setProps({ color: 'secondary' })
expect(wrapper.props('color')).toBe('secondary')
})
it('updates shareOptions prop dynamically', async () => {
const initialOptions = {
text: 'Initial text',
url: 'https://initial.com',
number: '111',
}
const wrapper = mount(VOvShare, {
props: {
shareOptions: initialOptions,
},
})
expect(wrapper.props('shareOptions')).toEqual(initialOptions)
const newOptions = {
text: 'New text',
url: 'https://new.com',
number: '222',
}
await wrapper.setProps({ shareOptions: newOptions })
expect(wrapper.props('shareOptions')).toEqual(newOptions)
})
it('updates windowFeatures prop dynamically', async () => {
const initialFeatures = { width: 600, height: 400, top: 100, left: 100 }
const wrapper = mount(VOvShare, {
props: {
windowFeatures: initialFeatures,
},
})
expect(wrapper.props('windowFeatures')).toEqual(initialFeatures)
const newFeatures = { width: 800, height: 600, top: 50, left: 50 }
await wrapper.setProps({ windowFeatures: newFeatures })
expect(wrapper.props('windowFeatures')).toEqual(newFeatures)
})
})
describe('Edge Cases', () => {
it('handles props with undefined values gracefully', () => {
const wrapper = mount(VOvShare, {
props: {
variant: undefined,
density: undefined,
color: undefined,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles empty text in shareOptions when copying', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
shareOptions: {
url: 'https://example.com',
number: '123',
},
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
await copyBtn.trigger('click')
await flushPromises()
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('')
})
it('handles multiple rapid copy clicks', async () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
shareOptions: {
text: 'Test',
url: 'https://example.com',
number: '123',
},
},
})
const copyBtn = wrapper.find('[data-cy="v-bsb-share-copy"]')
await copyBtn.trigger('click')
await copyBtn.trigger('click')
await copyBtn.trigger('click')
await flushPromises()
expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(3)
})
it('handles share array with duplicate values', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter', 'twitter', 'facebook'],
},
})
const twitterCount = wrapper.findAllComponents({ name: 'STwitter' }).length
expect(twitterCount).toBeGreaterThan(0)
})
})
describe('Integration Tests', () => {
it('renders complete share component with all default options', () => {
const wrapper = mount(VOvShare)
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SFacebook' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SLinkedIn' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SWhatsApp' }).exists()).toBe(true)
expect(wrapper.find('[data-cy="v-bsb-share-copy"]').exists()).toBe(true)
})
it('renders custom configuration with specific social media and styling', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['twitter', 'facebook'],
variant: 'outlined',
density: 'compact',
color: 'primary',
shareOptions: {
text: 'Check this out',
url: 'https://example.com',
number: '555',
},
},
})
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SFacebook' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'SLinkedIn' }).exists()).toBe(false)
expect(wrapper.props('variant')).toBe('outlined')
expect(wrapper.props('density')).toBe('compact')
expect(wrapper.props('color')).toBe('primary')
})
it('supports minimal configuration', () => {
const wrapper = mount(VOvShare, {
props: {
share: ['copy'],
},
})
expect(wrapper.find('[data-cy="v-bsb-share-copy"]').exists()).toBe(true)
expect(wrapper.findComponent({ name: 'STwitter' }).exists()).toBe(false)
})
})
})VOvTable.test.ts
ts
import { describe, it, expect, vi, beforeAll } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import type { OvTableOptions, OvTableData } from '../index'
import VOvTable from '../VOvTable.vue'
// Mock ResizeObserver globally for all tests
beforeAll(() => {
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
})) as never
})
// Mock visualViewport for Vuetify VOverlay with proper event handling
if (typeof global.visualViewport === 'undefined') {
const mockViewport: Partial<VisualViewport> & {
addEventListener: ReturnType<typeof vi.fn>
removeEventListener: ReturnType<typeof vi.fn>
} = {
width: 1024,
height: 768,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
global.visualViewport = mockViewport as VisualViewport
}
describe('VOvTable', () => {
const basicTableOptions: OvTableOptions = {
key: 'id',
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
],
}
const basicTableData: OvTableData[] = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' },
{ id: 3, name: 'Bob', email: 'bob@example.com' },
]
describe('Rendering', () => {
it('mounts without error', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders VContainer component', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
const container = wrapper.findComponent({ name: 'VContainer' })
expect(container.exists()).toBe(true)
})
it('renders VTable component', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
const table = wrapper.findComponent({ name: 'VTable' })
expect(table.exists()).toBe(true)
})
it('renders table with correct columns', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('renders table rows with data', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('renders pagination buttons', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThan(0)
})
it('renders loading overlay when loading is true', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
loading: true,
},
})
await flushPromises()
const overlay = wrapper.findComponent({ name: 'VOverlay' })
expect(overlay.exists()).toBe(true)
})
})
describe('Props - Table Options', () => {
it('accepts options prop with key and columns', () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
expect(wrapper.props('options')).toEqual(basicTableOptions)
})
it('accepts options with search configuration', async () => {
const optionsWithSearch: OvTableOptions = {
...basicTableOptions,
search: {
label: 'Search',
placeholder: 'Enter search term',
value: '',
},
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSearch,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.props('options').search).toBeDefined()
})
it('accepts options with filter configuration', async () => {
const optionsWithFilter: OvTableOptions = {
...basicTableOptions,
filter: {
fields: [{ type: 'text', name: 'name', label: 'Name' }],
},
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithFilter,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.props('options').filter).toBeDefined()
})
it('accepts options with sort configuration', async () => {
const optionsWithSort: OvTableOptions = {
...basicTableOptions,
sort: [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSort,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.props('options').sort).toBeDefined()
})
it('accepts options with itemsPerPage', async () => {
const optionsWithPagination: OvTableOptions = {
...basicTableOptions,
itemsPerPage: 5,
currentPage: 1,
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithPagination,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.props('options').itemsPerPage).toBe(5)
})
it('accepts options with column actions', async () => {
const optionsWithActions: OvTableOptions = {
...basicTableOptions,
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
{
name: 'actions',
label: 'Actions',
actions: ['edit', 'delete'],
},
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithActions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('accepts options with table-level actions', async () => {
const optionsWithTableActions: OvTableOptions = {
...basicTableOptions,
actions: ['add', 'export'],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithTableActions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.props('options').actions).toBeDefined()
})
it('defaults canRefresh to true', () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
expect(wrapper.props('options').canRefresh).toBeUndefined()
})
})
describe('Props - Data', () => {
it('accepts data prop as array of table data', () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
expect(wrapper.props('data')).toEqual(basicTableData)
})
it('handles empty data array', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: [],
},
})
await flushPromises()
expect(wrapper.props('data')).toEqual([])
})
it('defaults data to empty array if not provided', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
},
})
await flushPromises()
// Component defaults to empty array when data is not provided
const data = wrapper.props('data')
expect(Array.isArray(data) || data === undefined).toBe(true)
})
it('updates when data prop changes', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
const newData = [
{ id: 4, name: 'Alice', email: 'alice@example.com' },
{ id: 5, name: 'Charlie', email: 'charlie@example.com' },
]
await wrapper.setProps({ data: newData })
await flushPromises()
expect(wrapper.props('data')).toEqual(newData)
})
it('handles data with multiple rows', async () => {
const manyRows = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
}))
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: manyRows,
},
})
await flushPromises()
expect(wrapper.props('data')).toHaveLength(100)
})
})
describe('Props - Loading', () => {
it('accepts loading prop as false', () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
loading: false,
},
})
expect(wrapper.props('loading')).toBe(false)
})
it('accepts loading prop as true', () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
loading: true,
},
})
expect(wrapper.props('loading')).toBe(true)
})
it('defaults loading to false', () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
expect(wrapper.props('loading')).toBe(false)
})
})
describe('Table Columns', () => {
it('renders all columns from options', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('displays column titles from options', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('applies column alignment', async () => {
const optionsWithAlign: OvTableOptions = {
...basicTableOptions,
columns: [
{ name: 'id', label: 'ID', align: 'center' },
{ name: 'name', label: 'Name', align: 'left' },
{ name: 'email', label: 'Email', align: 'right' },
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithAlign,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('applies format to column values', async () => {
const optionsWithFormat: OvTableOptions = {
...basicTableOptions,
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
{
name: 'email',
label: 'Email',
format: { icon: '$mdiEmail', color: 'primary' },
},
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithFormat,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('respects maxLength on columns', async () => {
const optionsWithMaxLength: OvTableOptions = {
...basicTableOptions,
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name', maxLength: 5 },
{ name: 'email', label: 'Email' },
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithMaxLength,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Table Actions', () => {
it('renders table-level action buttons', async () => {
const optionsWithActions: OvTableOptions = {
...basicTableOptions,
actions: ['add', 'delete'],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithActions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('emits action event when table action is clicked', async () => {
const optionsWithActions: OvTableOptions = {
...basicTableOptions,
actions: ['add'],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithActions,
data: basicTableData,
},
})
await flushPromises()
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThan(0)
await buttons[0]!.trigger('click')
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('renders column-level action buttons', async () => {
const optionsWithColumnActions: OvTableOptions = {
...basicTableOptions,
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
{
name: 'actions',
label: 'Actions',
actions: ['edit', 'delete'],
},
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithColumnActions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Table Search', () => {
it('renders search input when search option is provided', async () => {
const optionsWithSearch: OvTableOptions = {
...basicTableOptions,
search: { label: 'Search', placeholder: 'Type here...' },
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSearch,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('does not render search input when search option is not provided', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('emits fetch event on search submission', async () => {
const optionsWithSearch: OvTableOptions = {
...basicTableOptions,
search: { label: 'Search' },
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSearch,
data: basicTableData,
},
})
await flushPromises()
// Verify fetch event is emitted
const emitted = wrapper.emitted('fetch')
expect(emitted).toBeDefined()
})
})
describe('Table Filter', () => {
it('renders filter button when filter option is provided', async () => {
const optionsWithFilter: OvTableOptions = {
...basicTableOptions,
filter: {
fields: [{ type: 'text', name: 'name', label: 'Name' }],
},
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithFilter,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('renders filter chips when filters are active', async () => {
const optionsWithFilter: OvTableOptions = {
...basicTableOptions,
filter: {
fields: [
{
type: 'text',
name: 'name',
label: 'Name',
value: 'John',
},
],
},
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithFilter,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('emits fetch event when filter changes', async () => {
const optionsWithFilter: OvTableOptions = {
...basicTableOptions,
filter: {
fields: [{ type: 'text', name: 'name', label: 'Name' }],
},
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithFilter,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Table Sort', () => {
it('renders sort button when sort option is provided', async () => {
const optionsWithSort: OvTableOptions = {
...basicTableOptions,
sort: [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSort,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('renders sort chips when sorts are active', async () => {
const optionsWithSort: OvTableOptions = {
...basicTableOptions,
sort: [{ name: 'name', label: 'Name', value: 'asc' }],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSort,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('emits fetch event when sort changes', async () => {
const optionsWithSort: OvTableOptions = {
...basicTableOptions,
sort: [{ name: 'name', label: 'Name' }],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSort,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Pagination', () => {
it('renders next/prev pagination buttons', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThan(0)
})
it('disables prev button on first page', async () => {
const wrapper = mount(VOvTable, {
props: {
options: {
...basicTableOptions,
itemsPerPage: 2,
currentPage: 1,
},
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('disables next button on last page', async () => {
const wrapper = mount(VOvTable, {
props: {
options: {
...basicTableOptions,
itemsPerPage: 10,
currentPage: 1,
},
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('respects itemsPerPage option', async () => {
const wrapper = mount(VOvTable, {
props: {
options: {
...basicTableOptions,
itemsPerPage: 5,
},
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.props('options').itemsPerPage).toBe(5)
})
it('emits fetch event on page change', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
})),
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Refresh Button', () => {
it('renders refresh button when canRefresh is true', async () => {
const wrapper = mount(VOvTable, {
props: {
options: {
...basicTableOptions,
canRefresh: true,
},
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('hides refresh button when canRefresh is false', async () => {
const wrapper = mount(VOvTable, {
props: {
options: {
...basicTableOptions,
canRefresh: false,
},
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('emits fetch event on refresh', async () => {
const wrapper = mount(VOvTable, {
props: {
options: {
...basicTableOptions,
canRefresh: true,
},
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Form Dialog', () => {
it('shows form dialog when row action with form is triggered', async () => {
const optionsWithForm: OvTableOptions = {
...basicTableOptions,
columns: [
{ name: 'id', label: 'ID' },
{
name: 'name',
label: 'Name',
actions: [
{
name: 'edit',
form: {
fields: [{ type: 'text', name: 'name', label: 'Name' }],
},
},
],
},
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithForm,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('shows form dialog for table action with form', async () => {
const optionsWithTableForm: OvTableOptions = {
...basicTableOptions,
actions: [
{
name: 'add',
form: {
fields: [{ type: 'text', name: 'name', label: 'Name' }],
},
},
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithTableForm,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('emits action event on form submission', async () => {
const optionsWithForm: OvTableOptions = {
...basicTableOptions,
actions: [
{
name: 'add',
form: {
fields: [{ type: 'text', name: 'name', label: 'Name' }],
actions: ['submit', 'cancel'],
actionSubmit: 'submit',
actionCancel: 'cancel',
},
},
],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithForm,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Emitted Events', () => {
it('emits fetch event on mount', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
const emitted = wrapper.emitted('fetch')
expect(emitted).toBeDefined()
expect(emitted?.length).toBeGreaterThan(0)
})
it('emits action event with correct parameters', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('fetch event includes pagination parameters', async () => {
const wrapper = mount(VOvTable, {
props: {
options: {
...basicTableOptions,
itemsPerPage: 10,
},
data: basicTableData,
},
})
await flushPromises()
const emitted = wrapper.emitted('fetch')
expect(emitted).toBeDefined()
})
it('fetch event includes search parameters when search is enabled', async () => {
const optionsWithSearch: OvTableOptions = {
...basicTableOptions,
search: { label: 'Search' },
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSearch,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('fetch event includes filter parameters when filter is enabled', async () => {
const optionsWithFilter: OvTableOptions = {
...basicTableOptions,
filter: {
fields: [{ type: 'text', name: 'name', label: 'Name' }],
},
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithFilter,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('fetch event includes sort parameters when sort is enabled', async () => {
const optionsWithSort: OvTableOptions = {
...basicTableOptions,
sort: [{ name: 'name', label: 'Name' }],
}
const wrapper = mount(VOvTable, {
props: {
options: optionsWithSort,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Exposed Methods', () => {
it('exposes fetch method', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(typeof wrapper.vm.fetch).toBe('function')
})
it('fetch method can be called without parameters', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
await wrapper.vm.fetch()
expect(wrapper.vm).toBeDefined()
})
it('fetch method can be called with page number', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
await wrapper.vm.fetch(2)
expect(wrapper.vm).toBeDefined()
})
})
describe('Responsive Layout', () => {
it('renders table with default desktop layout', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('handles data with different object structures', async () => {
const complexData: OvTableData[] = [
{ id: 1, name: 'John', email: 'john@example.com', nested: { value: 'test' } },
{ id: 2, name: 'Jane', email: 'jane@example.com', nested: { value: 'test2' } },
]
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: complexData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Edge Cases', () => {
it('handles empty options columns array', async () => {
const emptyColumnsOptions: OvTableOptions = {
key: 'id',
columns: [],
}
const wrapper = mount(VOvTable, {
props: {
options: emptyColumnsOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('handles data without key field', async () => {
const dataWithoutKey = [
{ name: 'John', email: 'john@example.com' },
{ name: 'Jane', email: 'jane@example.com' },
]
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: dataWithoutKey,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('handles very long text values', async () => {
const longTextData: OvTableData[] = [
{
id: 1,
name: 'John ' + 'x'.repeat(1000),
email: 'john@example.com',
},
]
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: longTextData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('handles special characters in data', async () => {
const specialCharData: OvTableData[] = [
{ id: 1, name: 'John <script>', email: 'john&jane@example.com' },
{ id: 2, name: 'Jane "quotes"', email: 'jane@example.com' },
]
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: specialCharData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('handles null and undefined values in data', async () => {
const nullData: OvTableData[] = [
{ id: 1, name: null, email: 'john@example.com' },
{ id: 2, name: undefined, email: 'jane@example.com' },
]
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: nullData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('handles numeric data in cells', async () => {
const numericData: OvTableData[] = [
{ id: 1, name: 123, email: 'john@example.com' },
{ id: 2, name: 45.67, email: 'jane@example.com' },
]
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: numericData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Complex Scenarios', () => {
it('handles table with all features enabled', async () => {
const fullFeaturedOptions: OvTableOptions = {
key: 'id',
columns: [
{ name: 'id', label: 'ID', align: 'center' },
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email', format: { icon: '$mdiEmail' } },
{
name: 'actions',
label: 'Actions',
actions: ['edit', 'delete'],
},
],
search: { label: 'Search', placeholder: 'Search...' },
filter: {
fields: [{ type: 'text', name: 'name', label: 'Name' }],
},
sort: [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
],
actions: ['add', 'export'],
itemsPerPage: 10,
currentPage: 1,
canRefresh: true,
}
const wrapper = mount(VOvTable, {
props: {
options: fullFeaturedOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('handles table with multiple column formats', async () => {
const multiFormatOptions: OvTableOptions = {
key: 'id',
columns: [
{ name: 'id', label: 'ID' },
{
name: 'status',
label: 'Status',
format: [
{ color: 'green', text: 'Active', rules: { type: 'equals', params: 'active' } },
{ color: 'red', text: 'Inactive', rules: { type: 'equals', params: 'inactive' } },
],
},
{ name: 'email', label: 'Email' },
],
}
const wrapper = mount(VOvTable, {
props: {
options: multiFormatOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
it('handles complex form submission in table context', async () => {
const formOptions: OvTableOptions = {
...basicTableOptions,
actions: [
{
name: 'add',
form: {
fields: [
{ type: 'text', name: 'name', label: 'Name', required: true },
{ type: 'email', name: 'email', label: 'Email', required: true },
],
actions: ['submit', 'cancel'],
actionSubmit: 'submit',
actionCancel: 'cancel',
},
},
],
}
const wrapper = mount(VOvTable, {
props: {
options: formOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
})
})
describe('Mounting and Unmounting', () => {
it('mounts and initializes correctly', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('can be unmounted without errors', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await wrapper.unmount()
expect(wrapper).toBeDefined()
})
})
describe('Props Updates', () => {
it('updates when options prop changes', async () => {
const initialOptions = basicTableOptions
const wrapper = mount(VOvTable, {
props: {
options: initialOptions,
data: basicTableData,
},
})
await flushPromises()
const newOptions: OvTableOptions = {
key: 'id',
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ name: 'phone', label: 'Phone' },
],
}
await wrapper.setProps({ options: newOptions })
await flushPromises()
expect(wrapper.props('options').columns.length).toBe(4)
})
it('updates when data prop changes', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
},
})
await flushPromises()
const newData: OvTableData[] = [{ id: 10, name: 'Alice', email: 'alice@example.com' }]
await wrapper.setProps({ data: newData })
await flushPromises()
expect(wrapper.props('data')).toEqual(newData)
})
it('updates loading state', async () => {
const wrapper = mount(VOvTable, {
props: {
options: basicTableOptions,
data: basicTableData,
loading: false,
},
})
expect(wrapper.props('loading')).toBe(false)
await wrapper.setProps({ loading: true })
expect(wrapper.props('loading')).toBe(true)
})
})
})VOvView.test.ts
ts
import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import type { OvViewOptions, OvViewData } from '../index'
import VOvView from '../VOvView.vue'
// Mock visualViewport for Vuetify VOverlay with proper event handling
if (typeof global.visualViewport === 'undefined') {
const mockViewport: Partial<VisualViewport> & {
addEventListener: ReturnType<typeof vi.fn>
removeEventListener: ReturnType<typeof vi.fn>
} = {
width: 1024,
height: 768,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
global.visualViewport = mockViewport as VisualViewport
}
// Mock ResizeObserver for VProgressCircular
if (typeof global.ResizeObserver === 'undefined') {
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}))
}
describe('VOvView', () => {
const basicViewOptions: OvViewOptions = {
items: [
{ name: 'field1', label: 'Field 1' },
{ name: 'field2', label: 'Field 2' },
],
}
const basicViewData: OvViewData = {
field1: 'Value 1',
field2: 'Value 2',
}
describe('Rendering', () => {
it('mounts without error', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders VContainer when multiple items exist', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
const container = wrapper.findComponent({ name: 'VContainer' })
expect(container.exists()).toBe(true)
})
it('renders single item directly without VContainer', () => {
const singleItemOptions: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1' }],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: singleItemOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders VRow for items', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
const rows = wrapper.findAllComponents({ name: 'VRow' })
expect(rows.length).toBeGreaterThan(0)
})
it('renders VCol for each item', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
const cols = wrapper.findAllComponents({ name: 'VCol' })
expect(cols.length).toBeGreaterThanOrEqual(2)
})
it('renders action buttons when actions are provided', () => {
const optionsWithActions: OvViewOptions = {
items: [
{ name: 'field1', label: 'Field 1' },
{ name: 'field2', label: 'Field 2' },
],
actions: ['edit', 'delete'],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1', field2: 'Value 2' },
options: optionsWithActions,
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBeGreaterThan(0)
})
it('does not render action buttons when actions are empty', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: { ...basicViewOptions, actions: [] },
},
})
const buttons = wrapper.findAllComponents({ name: 'VBtn' })
expect(buttons.length).toBe(0)
})
})
describe('Props - Data', () => {
it('accepts data prop', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
expect(wrapper.props('data')).toEqual(basicViewData)
})
it('handles empty data object', () => {
const wrapper = mount(VOvView, {
props: {
data: {},
options: basicViewOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles undefined values in data', () => {
const wrapper = mount(VOvView, {
props: {
data: {
field1: undefined,
field2: 'Value 2',
},
options: basicViewOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('updates view when data prop changes', async () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
const newData = { field1: 'Updated 1', field2: 'Updated 2' }
await wrapper.setProps({ data: newData })
await flushPromises()
expect(wrapper.props('data')).toEqual(newData)
})
})
describe('Props - Options', () => {
it('accepts options prop', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
expect(wrapper.props('options')).toEqual(basicViewOptions)
})
it('renders items from options', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies cols option for grid layout', () => {
const optionsWithCols: OvViewOptions = {
items: basicViewOptions.items,
cols: 2,
}
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: optionsWithCols,
},
})
expect(wrapper.vm).toBeDefined()
})
it('defaults to 1 column when cols is not specified', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Props - Translation Function', () => {
it('accepts translation function prop', () => {
const mockT = (text?: string) => `translated_${text}`
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
t: mockT,
},
})
expect(wrapper.props('t')).toBe(mockT)
})
it('defaults translation function if not provided', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
const defaultT = wrapper.props('t')
expect(typeof defaultT).toBe('function')
})
it('uses translation function for button text', () => {
const mockT = (text?: string) => {
const translations: Record<string, string> = {
edit: 'Editar',
delete: 'Eliminar',
}
return translations[text || ''] || text || ''
}
const optionsWithActions: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1' }],
actions: ['edit'],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value' },
options: optionsWithActions,
t: mockT,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Props - Loading', () => {
it('accepts loading prop', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
loading: false,
},
})
expect(wrapper.props('loading')).toBe(false)
})
it('defaults loading to false', () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
expect(wrapper.props('loading')).toBe(false)
})
it('renders progress circular when loading is true', () => {
const optionsWithItems: OvViewOptions = {
items: [
{ name: 'field1', label: 'Field 1' },
{ name: 'field2', label: 'Field 2' },
],
}
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: optionsWithItems,
loading: true,
},
})
const progress = wrapper.findComponent({ name: 'VProgressCircular' })
expect(progress.exists()).toBe(true)
})
it('shows overlay when loading is true', () => {
const optionsWithItems: OvViewOptions = {
items: [
{ name: 'field1', label: 'Field 1' },
{ name: 'field2', label: 'Field 2' },
],
}
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: optionsWithItems,
loading: true,
},
})
const overlay = wrapper.findComponent({ name: 'VOverlay' })
expect(overlay.exists()).toBe(true)
// Verify the overlay is visible by checking if its modelValue is true
expect(wrapper.vm.loading).toBe(true)
})
})
describe('View Items', () => {
it('renders view items with labels', () => {
const optionsWithLabels: OvViewOptions = {
items: [
{ name: 'field1', label: 'User Name' },
{ name: 'field2', label: 'Email Address' },
],
}
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: optionsWithLabels,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles view items without labels', () => {
const optionsNoLabels: OvViewOptions = {
items: [{ name: 'field1' }, { name: 'field2' }],
}
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: optionsNoLabels,
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies format to view items', () => {
const optionsWithFormat: OvViewOptions = {
items: [
{ name: 'field1', label: 'Field 1', format: { color: 'primary', icon: '$mdiAccount' } },
],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: optionsWithFormat,
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders item actions', () => {
const optionsWithItemActions: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1', actions: ['view', 'edit'] }],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: optionsWithItemActions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies maxLength to item values', () => {
const optionsWithMaxLength: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1', maxLength: 10 }],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'This is a very long value that should be truncated' },
options: optionsWithMaxLength,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Actions', () => {
it('renders action buttons with action names', () => {
const optionsWithActions: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1' }],
actions: ['edit', 'delete'],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: optionsWithActions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('renders action buttons with format', () => {
const optionsWithFormattedActions: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1' }],
actions: [
{ name: 'edit', format: { color: 'primary', icon: '$mdiPencil' } },
{ name: 'delete', format: { color: 'error', icon: '$mdiDelete' } },
],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: optionsWithFormattedActions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies actionAlign to action buttons', () => {
const optionsWithActionAlign: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1' }],
actions: ['edit'],
actionAlign: 'center',
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: optionsWithActionAlign,
},
})
expect(wrapper.vm).toBeDefined()
})
it('applies actionFormat to all actions', () => {
const optionsWithActionFormat: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1' }],
actions: ['edit', 'delete'],
actionFormat: { variant: 'outlined', density: 'compact' },
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: optionsWithActionFormat,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Events', () => {
it('emits action event on button click', async () => {
const optionsWithActions: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1' }],
actions: ['edit'],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: optionsWithActions,
},
})
await wrapper.vm.$nextTick()
wrapper.vm.$emit('action', 'edit', 'Value 1')
expect(wrapper.emitted('action')).toBeTruthy()
})
it('emits details event for truncated content', async () => {
const optionsWithMaxLength: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1', maxLength: 5 }],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'This is a long value' },
options: optionsWithMaxLength,
},
})
await wrapper.vm.$nextTick()
wrapper.vm.$emit('details', 'Field 1', 'This is a long value')
expect(wrapper.emitted('details')).toBeTruthy()
})
})
describe('Responsive Layout', () => {
it('adapts columns for mobile display', () => {
const optionsWithCols: OvViewOptions = {
items: [
{ name: 'field1', label: 'Field 1' },
{ name: 'field2', label: 'Field 2' },
],
cols: 2,
}
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: optionsWithCols,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Props Updates', () => {
it('updates view when options prop changes', async () => {
const initialOptions: OvViewOptions = {
items: [{ name: 'field1', label: 'Field 1' }],
}
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: initialOptions,
},
})
const newOptions: OvViewOptions = {
items: [
{ name: 'field1', label: 'Field 1' },
{ name: 'field2', label: 'Field 2' },
],
}
await wrapper.setProps({
options: newOptions,
data: { field1: 'Value 1', field2: 'Value 2' },
})
await flushPromises()
expect(wrapper.props('options')!.items.length).toBe(2)
})
it('updates loading state', async () => {
const optionsWithItems: OvViewOptions = {
items: [
{ name: 'field1', label: 'Field 1' },
{ name: 'field2', label: 'Field 2' },
],
}
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: optionsWithItems,
loading: false,
},
})
expect(wrapper.props('loading')).toBe(false)
await wrapper.setProps({ loading: true })
await wrapper.vm.$nextTick()
expect(wrapper.props('loading')).toBe(true)
})
})
describe('Complex Scenarios', () => {
it('renders view with all features combined', async () => {
const complexOptions: OvViewOptions = {
items: [
{ name: 'username', label: 'Username', format: { color: 'primary' } },
{ name: 'email', label: 'Email', format: { icon: '$mdiEmail' } },
{ name: 'role', label: 'Role', maxLength: 20 },
],
actions: [
{ name: 'edit', format: { color: 'primary', icon: '$mdiPencil' } },
{ name: 'delete', format: { color: 'error', icon: '$mdiDelete' } },
],
actionAlign: 'center',
actionFormat: { variant: 'outlined' },
cols: 2,
}
const complexData: OvViewData = {
username: 'john_doe',
email: 'john@example.com',
role: 'Administrator',
}
const wrapper = mount(VOvView, {
props: {
data: complexData,
options: complexOptions,
loading: false,
},
})
await wrapper.vm.$nextTick()
expect(wrapper.vm).toBeDefined()
expect(wrapper.exists()).toBe(true)
})
it('renders single item view without container', () => {
const singleItemOptions: OvViewOptions = {
items: [{ name: 'status', label: 'Status', format: { color: 'success' } }],
}
const wrapper = mount(VOvView, {
props: {
data: { status: 'Active' },
options: singleItemOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
})
describe('Edge Cases', () => {
it('handles view with no items', () => {
const emptyOptions: OvViewOptions = {
items: [],
}
const wrapper = mount(VOvView, {
props: {
data: {},
options: emptyOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles view with missing data values', () => {
const wrapper = mount(VOvView, {
props: {
data: { field1: 'Value 1' },
options: {
items: [
{ name: 'field1', label: 'Field 1' },
{ name: 'field2', label: 'Field 2' },
],
},
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles view with null values in data', () => {
const wrapper = mount(VOvView, {
props: {
data: { field1: null, field2: 'Value 2' },
options: basicViewOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles view with special characters in values', () => {
const wrapper = mount(VOvView, {
props: {
data: {
field1: 'Value with <special> & "characters"',
field2: 'Value with "quotes" and \'apostrophes\'',
},
options: basicViewOptions,
},
})
expect(wrapper.vm).toBeDefined()
})
it('handles mounting and unmounting', async () => {
const wrapper = mount(VOvView, {
props: {
data: basicViewData,
options: basicViewOptions,
},
})
expect(wrapper.exists()).toBe(true)
await wrapper.unmount()
expect(wrapper.vm).toBeDefined()
})
})
})Composables
http.test.ts
ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
vi.mock('ofetch', () => {
const mockCreate = vi.fn((options) => ({
...options,
__isMocked: true,
}))
return {
$fetch: {
create: mockCreate,
},
}
})
import { useHttp } from '../http'
import { $fetch } from 'ofetch'
describe('useHttp', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns a fetch instance', () => {
const http = useHttp()
expect(http).toBeDefined()
expect(typeof http).toBe('function')
expect(http.get).toBeDefined()
expect(http.post).toBeDefined()
expect(http.put).toBeDefined()
expect(http.delete).toBeDefined()
expect(http.patch).toBeDefined()
})
it('creates instance with baseURL configuration', () => {
vi.mocked($fetch.create).mockClear()
useHttp()
expect(vi.mocked($fetch.create)).toHaveBeenCalledOnce()
const callArgs = vi.mocked($fetch.create).mock.calls[0]?.[0]
expect(callArgs).toHaveProperty('baseURL')
})
it('returns same type instance on multiple calls', () => {
const http1 = useHttp()
const http2 = useHttp()
expect(typeof http1).toBe(typeof http2)
})
})ui.test.ts
ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useCardBackground } from '../ui'
// Mock vuetify useTheme
const mockTheme = {
current: {
value: {
dark: false,
},
},
}
vi.mock('vuetify', () => ({
useTheme: vi.fn(() => mockTheme),
}))
describe('useCardBackground', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns a computed object with background property', () => {
const result = useCardBackground('#FF0000')
expect(result).toBeDefined()
expect(result.value).toBeDefined()
expect(result.value.background).toBeDefined()
})
it('generates gradient background for light theme', () => {
mockTheme.current.value.dark = false
const result = useCardBackground('#FF0000')
const background = result.value.background
expect(background).toContain('linear-gradient')
expect(background).toContain('#FF000066')
expect(background).toContain('#FF000033')
})
it('generates gradient background for dark theme', () => {
mockTheme.current.value.dark = true
const result = useCardBackground('#FF0000')
const background = result.value.background
expect(background).toContain('linear-gradient')
expect(background).toContain('#FF000033')
expect(background).toContain('#FF000066')
})
it('accepts different color values', () => {
mockTheme.current.value.dark = false
const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFA500']
colors.forEach((color) => {
const result = useCardBackground(color)
expect(result.value.background).toContain(color)
})
})
it('responds to theme changes reactively', () => {
mockTheme.current.value.dark = false
const result = useCardBackground('#FF0000')
const lightBackground = result.value.background
mockTheme.current.value.dark = true
// Computed is reactive - accessing .value after theme change should reflect new theme
const darkBackground = result.value.background
expect(lightBackground).toContain('#FF000066')
expect(darkBackground).toContain('#FF000033')
})
it('includes 135deg angle in gradient', () => {
const result = useCardBackground('#FF0000')
expect(result.value.background).toContain('135deg')
})
it('returns consistent output for same input', () => {
const result1 = useCardBackground('#FF0000')
const result2 = useCardBackground('#FF0000')
expect(result1.value.background).toBe(result2.value.background)
})
})