Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions src/components/atoms/ActionButton.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import ActionButton from '@/components/atoms/ActionButton.vue'

// Mock Vue Router
vi.mock('vue-router', () => ({
useRoute: () => ({
path: '/test',
}),
RouterLink: {
template: '<a :to="to"><slot /></a>',
props: ['to'],
},
}))

// Mock analytics
vi.mock('@/utils/analytics', () => ({
trackButtonClick: vi.fn(),
}))

describe('ActionButton', () => {
it('renders as button by default', () => {
const wrapper = mount(ActionButton, {
slots: {
default: 'Click me',
},
})

const button = wrapper.find('button')
expect(button.exists()).toBe(true)
expect(button.text()).toBe('Click me')
})

it('renders as link when href is provided', () => {
const wrapper = mount(ActionButton, {
props: {
href: 'https://example.com',
},
slots: {
default: 'Link text',
},
})

const link = wrapper.find('a')
expect(link.exists()).toBe(true)
expect(link.attributes('href')).toBe('https://example.com')
})

it('renders as RouterLink when to is provided', () => {
const wrapper = mount(ActionButton, {
props: {
to: '/home',
},
slots: {
default: 'Router Link',
},
global: {
stubs: {
RouterLink: {
template: '<a :to="to"><slot /></a>',
props: ['to'],
},
},
},
})

expect(wrapper.html()).toContain('Router Link')
})

it('applies primary variant classes by default', () => {
const wrapper = mount(ActionButton, {
slots: {
default: 'Button',
},
})

const button = wrapper.find('button')
expect(button.classes()).toContain('bg-primary')
})

it('applies secondary variant classes when specified', () => {
const wrapper = mount(ActionButton, {
props: {
variant: 'secondary',
},
slots: {
default: 'Button',
},
})

const button = wrapper.find('button')
expect(button.classes()).toContain('border-primary')
})

it('disables button when disabled prop is true', () => {
const wrapper = mount(ActionButton, {
props: {
disabled: true,
},
slots: {
default: 'Disabled',
},
})

const button = wrapper.find('button')
expect(button.attributes('disabled')).toBeDefined()
})

it('calls trackButtonClick on click', async () => {
const { trackButtonClick } = await import('@/utils/analytics')

const wrapper = mount(ActionButton, {
props: {
contentSection: 'test-section',
},
slots: {
default: 'Click me',
},
})

await wrapper.find('button').trigger('click')

expect(trackButtonClick).toHaveBeenCalledWith({
button_name: 'Click me',
content_section: 'test-section',
link_category: '/test',
})
})

it('applies size classes correctly', () => {
const wrapper = mount(ActionButton, {
props: {
size: 'lg',
},
slots: {
default: 'Large Button',
},
})

const button = wrapper.find('button')
expect(button.classes()).toContain('px-4')
expect(button.classes()).toContain('py-2')
})

it('supports download attribute for links', () => {
const wrapper = mount(ActionButton, {
props: {
href: 'https://example.com/file.pdf',
download: true,
},
slots: {
default: 'Download',
},
})

const link = wrapper.find('a')
expect(link.attributes('download')).toBeDefined()
})

it('supports target and rel attributes for links', () => {
const wrapper = mount(ActionButton, {
props: {
href: 'https://example.com',
target: '_blank',
rel: 'noopener noreferrer',
},
slots: {
default: 'External Link',
},
})

const link = wrapper.find('a')
expect(link.attributes('target')).toBe('_blank')
expect(link.attributes('rel')).toBe('noopener noreferrer')
})
})
88 changes: 88 additions & 0 deletions src/components/atoms/BackButton.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import BackButton from '@/components/atoms/BackButton.vue'

// Mock Vue Router
vi.mock('vue-router', () => ({
useRoute: () => ({
path: '/test',
}),
}))

// Mock analytics
vi.mock('@/utils/analytics', () => ({
trackButtonClick: vi.fn(),
}))

describe('BackButton', () => {
it('renders with default label', () => {
const wrapper = mount(BackButton, {
global: {
stubs: {
ActionButton: {
template: '<button><slot /></button>',
},
},
},
})

expect(wrapper.text()).toContain('Back')
})

it('renders with custom label', () => {
const wrapper = mount(BackButton, {
props: {
label: 'Go Back',
},
global: {
stubs: {
ActionButton: {
template: '<button><slot /></button>',
},
},
},
})

expect(wrapper.text()).toContain('Go Back')
})

it('calls onClick callback when clicked', async () => {
const onClickMock = vi.fn()
const wrapper = mount(BackButton, {
props: {
onClick: onClickMock,
},
global: {
stubs: {
ActionButton: {
template: '<button @click="$emit(\'click\')"><slot /></button>',
},
},
},
})

await wrapper.find('button').trigger('click')

expect(onClickMock).toHaveBeenCalled()
})

it('passes contentSection prop to ActionButton', () => {
const wrapper = mount(BackButton, {
props: {
contentSection: 'test-section',
},
global: {
stubs: {
ActionButton: {
name: 'ActionButton',
template: '<button :content-section="contentSection"><slot /></button>',
props: ['contentSection'],
},
},
},
})

const button = wrapper.findComponent({ name: 'ActionButton' })
expect(button.props('contentSection')).toBe('test-section')
})
})
101 changes: 101 additions & 0 deletions src/components/atoms/BackToTop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import BackToTop from '@/components/atoms/BackToTop.vue'

describe('BackToTop', () => {
beforeEach(() => {
// Reset window scroll position
window.scrollY = 0
vi.clearAllMocks()
})

it('does not render button when not visible initially', () => {
const wrapper = mount(BackToTop, {
global: {
stubs: {
ArrowUpIcon: true,
},
},
})

const button = wrapper.find('button')
expect(button.exists()).toBe(false)
})

it('shows button when scrolled down more than 300px', async () => {
const wrapper = mount(BackToTop, {
global: {
stubs: {
ArrowUpIcon: true,
},
},
})

// Simulate scroll
Object.defineProperty(window, 'scrollY', { value: 400, writable: true })
window.dispatchEvent(new Event('scroll'))
await wrapper.vm.$nextTick()

expect((wrapper.vm as any).isVisible).toBe(true)
})

it('hides button when scrolled less than 300px', async () => {
const wrapper = mount(BackToTop, {
global: {
stubs: {
ArrowUpIcon: true,
},
},
})

// Simulate scroll
Object.defineProperty(window, 'scrollY', { value: 200, writable: true })
window.dispatchEvent(new Event('scroll'))
await wrapper.vm.$nextTick()

expect((wrapper.vm as any).isVisible).toBe(false)
})

it('scrolls to top when button is clicked', async () => {
const scrollToMock = vi.fn()
window.scrollTo = scrollToMock

const wrapper = mount(BackToTop, {
global: {
stubs: {
ArrowUpIcon: true,
},
},
})

// Make button visible
Object.defineProperty(window, 'scrollY', { value: 400, writable: true })
window.dispatchEvent(new Event('scroll'))
await wrapper.vm.$nextTick()

await wrapper.find('button').trigger('click')

expect(scrollToMock).toHaveBeenCalledWith({
top: 0,
behavior: 'smooth',
})
})

it('has proper aria-label', async () => {
const wrapper = mount(BackToTop, {
global: {
stubs: {
ArrowUpIcon: true,
},
},
})

// Make button visible
Object.defineProperty(window, 'scrollY', { value: 400, writable: true })
window.dispatchEvent(new Event('scroll'))
await wrapper.vm.$nextTick()

const button = wrapper.find('button')
expect(button.attributes('aria-label')).toBe('Back to top')
})
})
Loading
Loading