✅ Add unit tests for BaseButton component
This commit is contained in:
@@ -0,0 +1,300 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import BaseButton from '../../../src/components/shared/BaseButton.vue';
|
||||||
|
|
||||||
|
describe('BaseButton', () => {
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('renders with default props', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
slots: {
|
||||||
|
default: 'Click me'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('button').exists()).toBe(true);
|
||||||
|
expect(wrapper.text()).toBe('Click me');
|
||||||
|
expect(wrapper.classes()).toContain('base-button--primary');
|
||||||
|
expect(wrapper.classes()).toContain('base-button--medium');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all button variants', () => {
|
||||||
|
const variants = ['primary', 'secondary', 'danger', 'ghost', 'icon-only'];
|
||||||
|
|
||||||
|
variants.forEach(variant => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { variant },
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.classes()).toContain(`base-button--${variant}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all button sizes', () => {
|
||||||
|
const sizes = ['small', 'medium', 'large'];
|
||||||
|
|
||||||
|
sizes.forEach(size => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { size },
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.classes()).toContain(`base-button--${size}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with icon on left', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: {
|
||||||
|
icon: '🚀',
|
||||||
|
iconPosition: 'left'
|
||||||
|
},
|
||||||
|
slots: { default: 'Launch' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const icons = wrapper.findAll('.icon');
|
||||||
|
expect(icons.length).toBe(1);
|
||||||
|
expect(icons[0].classes()).toContain('icon-left');
|
||||||
|
expect(icons[0].text()).toBe('🚀');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with icon on right', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: {
|
||||||
|
icon: '→',
|
||||||
|
iconPosition: 'right'
|
||||||
|
},
|
||||||
|
slots: { default: 'Next' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const icons = wrapper.findAll('.icon');
|
||||||
|
expect(icons.length).toBe(1);
|
||||||
|
expect(icons[0].classes()).toContain('icon-right');
|
||||||
|
expect(icons[0].text()).toBe('→');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders as icon-only button when no slot content', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: {
|
||||||
|
icon: '×'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.classes()).toContain('base-button--icon-only');
|
||||||
|
expect(wrapper.text()).toBe('×');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders as full width', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { fullWidth: true },
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.classes()).toContain('base-button--full-width');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Button Types', () => {
|
||||||
|
it('renders with button type by default', () => {
|
||||||
|
const wrapper = mount(BaseButton);
|
||||||
|
expect(wrapper.attributes('type')).toBe('button');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with submit type', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { type: 'submit' }
|
||||||
|
});
|
||||||
|
expect(wrapper.attributes('type')).toBe('submit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with reset type', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { type: 'reset' }
|
||||||
|
});
|
||||||
|
expect(wrapper.attributes('type')).toBe('reset');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Loading State', () => {
|
||||||
|
it('shows spinner when loading', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Loading...' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('.spinner').exists()).toBe(true);
|
||||||
|
expect(wrapper.classes()).toContain('base-button--loading');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides icon when loading', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: {
|
||||||
|
loading: true,
|
||||||
|
icon: '🚀'
|
||||||
|
},
|
||||||
|
slots: { default: 'Launch' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('.icon').exists()).toBe(false);
|
||||||
|
expect(wrapper.find('.spinner').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makes button text screen-reader only when loading', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Processing' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const textSpan = wrapper.find('span:not(.spinner)');
|
||||||
|
expect(textSpan.classes()).toContain('sr-only');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not emit click when loading', async () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
expect(wrapper.emitted('click')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Disabled State', () => {
|
||||||
|
it('applies disabled class', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { disabled: true },
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.classes()).toContain('base-button--disabled');
|
||||||
|
expect(wrapper.attributes('disabled')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not emit click when disabled', async () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { disabled: true },
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
expect(wrapper.emitted('click')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Click Events', () => {
|
||||||
|
it('emits click event when clicked', async () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
expect(wrapper.emitted('click')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('click')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes event to click handler', async () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
const clickEvents = wrapper.emitted('click');
|
||||||
|
expect(clickEvents[0][0]).toBeInstanceOf(Event);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be clicked multiple times', async () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
|
||||||
|
expect(wrapper.emitted('click')).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('has proper focus-visible styling', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that focus-visible class exists in CSS (component has focus styling)
|
||||||
|
expect(wrapper.html()).toContain('button');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('spinner has aria-hidden', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Loading' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const spinner = wrapper.find('.spinner');
|
||||||
|
expect(spinner.attributes('aria-hidden')).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintains text for screen readers when loading', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Submit Form' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Submit Form');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('handles empty slot gracefully', () => {
|
||||||
|
const wrapper = mount(BaseButton);
|
||||||
|
expect(wrapper.find('button').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles both loading and disabled', () => {
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: {
|
||||||
|
loading: true,
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
slots: { default: 'Button' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.classes()).toContain('base-button--loading');
|
||||||
|
expect(wrapper.classes()).toContain('base-button--disabled');
|
||||||
|
expect(wrapper.attributes('disabled')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates variant prop', () => {
|
||||||
|
// Valid variants should not throw
|
||||||
|
expect(() => {
|
||||||
|
mount(BaseButton, {
|
||||||
|
props: { variant: 'primary' }
|
||||||
|
});
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
// Invalid variant should fail validation (but mount won't throw, just warn)
|
||||||
|
const wrapper = mount(BaseButton, {
|
||||||
|
props: { variant: 'invalid' }
|
||||||
|
});
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates size prop', () => {
|
||||||
|
expect(() => {
|
||||||
|
mount(BaseButton, {
|
||||||
|
props: { size: 'medium' }
|
||||||
|
});
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates type prop', () => {
|
||||||
|
expect(() => {
|
||||||
|
mount(BaseButton, {
|
||||||
|
props: { type: 'submit' }
|
||||||
|
});
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user