diff --git a/src/test/ShowAllCard.test.ts b/src/test/ShowAllCard.test.ts new file mode 100644 index 00000000..757ba614 --- /dev/null +++ b/src/test/ShowAllCard.test.ts @@ -0,0 +1,494 @@ +/** + * ShowAllCard Component Tests + * + * Comprehensive unit tests covering all required test categories: + * - Component Rendering + * - Component Styling + * - Component Props + * - User Interactions + * - Component Methods + * - Edge Cases + * - Error Handling + * - Accessibility + * - Performance + * - Integration + * + * @author Matthew Raymer + */ + +import { mount, VueWrapper } from '@vue/test-utils' +import ShowAllCard from '@/components/ShowAllCard.vue' +import { + ShowAllCardSimpleMock, + ShowAllCardStandardMock, + ShowAllCardComplexMock, + createPeopleShowAllCardMock, + createProjectsShowAllCardMock, + createShowAllCardMockWithComplexQuery +} from './__mocks__/ShowAllCard.mock' + +describe('ShowAllCard', () => { + let wrapper: VueWrapper + + // Default props for testing + const defaultProps = { + entityType: 'people' as const, + routeName: 'contacts', + queryParams: {} + } + + // Component wrapper factory + const mountComponent = (props = {}) => { + return mount(ShowAllCard, { + props: { ...defaultProps, ...props } + }) + } + + beforeEach(() => { + wrapper = mountComponent() + }) + + afterEach(() => { + wrapper?.unmount() + }) + + describe('Component Rendering', () => { + it('should render correctly', () => { + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('li').exists()).toBe(true) + expect(wrapper.find('router-link').exists()).toBe(true) + }) + + it('should render with correct structure', () => { + const listItem = wrapper.find('li') + const routerLink = wrapper.find('router-link') + const icon = wrapper.find('font-awesome') + const title = wrapper.find('h3') + + expect(listItem.exists()).toBe(true) + expect(routerLink.exists()).toBe(true) + expect(icon.exists()).toBe(true) + expect(title.exists()).toBe(true) + expect(title.text()).toBe('Show All') + }) + + it('should render conditionally based on props', () => { + wrapper = mountComponent({ entityType: 'projects' }) + expect(wrapper.exists()).toBe(true) + + wrapper = mountComponent({ entityType: 'people' }) + expect(wrapper.exists()).toBe(true) + }) + + it('should render with different entity types', () => { + const peopleWrapper = mountComponent({ entityType: 'people' }) + const projectsWrapper = mountComponent({ entityType: 'projects' }) + + expect(peopleWrapper.exists()).toBe(true) + expect(projectsWrapper.exists()).toBe(true) + + peopleWrapper.unmount() + projectsWrapper.unmount() + }) + }) + + describe('Component Styling', () => { + it('should have correct CSS classes on list item', () => { + const listItem = wrapper.find('li') + expect(listItem.classes()).toContain('cursor-pointer') + }) + + it('should have correct CSS classes on icon', () => { + const icon = wrapper.find('font-awesome') + expect(icon.exists()).toBe(true) + expect(icon.attributes('icon')).toBe('circle-right') + expect(icon.classes()).toContain('text-blue-500') + expect(icon.classes()).toContain('text-5xl') + expect(icon.classes()).toContain('mb-1') + }) + + it('should have correct CSS classes on title', () => { + const title = wrapper.find('h3') + expect(title.classes()).toContain('text-xs') + expect(title.classes()).toContain('text-slate-500') + expect(title.classes()).toContain('font-medium') + expect(title.classes()).toContain('italic') + expect(title.classes()).toContain('text-ellipsis') + expect(title.classes()).toContain('whitespace-nowrap') + expect(title.classes()).toContain('overflow-hidden') + }) + + it('should have responsive design classes', () => { + const title = wrapper.find('h3') + expect(title.classes()).toContain('text-ellipsis') + expect(title.classes()).toContain('whitespace-nowrap') + expect(title.classes()).toContain('overflow-hidden') + }) + + it('should have Tailwind CSS integration', () => { + const icon = wrapper.find('font-awesome') + const title = wrapper.find('h3') + + expect(icon.classes()).toContain('text-blue-500') + expect(icon.classes()).toContain('text-5xl') + expect(title.classes()).toContain('text-slate-500') + }) + }) + + describe('Component Props', () => { + it('should accept all required props', () => { + expect(wrapper.vm.entityType).toBe('people') + expect(wrapper.vm.routeName).toBe('contacts') + expect(wrapper.vm.queryParams).toEqual({}) + }) + + it('should handle required entityType prop', () => { + wrapper = mountComponent({ entityType: 'projects' }) + expect(wrapper.vm.entityType).toBe('projects') + + wrapper = mountComponent({ entityType: 'people' }) + expect(wrapper.vm.entityType).toBe('people') + }) + + it('should handle required routeName prop', () => { + wrapper = mountComponent({ routeName: 'projects' }) + expect(wrapper.vm.routeName).toBe('projects') + + wrapper = mountComponent({ routeName: 'contacts' }) + expect(wrapper.vm.routeName).toBe('contacts') + }) + + it('should handle optional queryParams prop', () => { + const queryParams = { filter: 'active', sort: 'name' } + wrapper = mountComponent({ queryParams }) + expect(wrapper.vm.queryParams).toEqual(queryParams) + }) + + it('should handle empty queryParams prop', () => { + wrapper = mountComponent({ queryParams: {} }) + expect(wrapper.vm.queryParams).toEqual({}) + }) + + it('should handle undefined queryParams prop', () => { + wrapper = mountComponent({ queryParams: undefined }) + expect(wrapper.vm.queryParams).toEqual({}) + }) + + it('should validate prop types correctly', () => { + expect(typeof wrapper.vm.entityType).toBe('string') + expect(typeof wrapper.vm.routeName).toBe('string') + expect(typeof wrapper.vm.queryParams).toBe('object') + }) + }) + + describe('User Interactions', () => { + it('should have clickable router link', () => { + const routerLink = wrapper.find('router-link') + expect(routerLink.exists()).toBe(true) + expect(routerLink.attributes('to')).toBeDefined() + }) + + it('should have accessible cursor pointer', () => { + const listItem = wrapper.find('li') + expect(listItem.classes()).toContain('cursor-pointer') + }) + + it('should support keyboard navigation', () => { + const routerLink = wrapper.find('router-link') + expect(routerLink.exists()).toBe(true) + // Router link should be keyboard accessible by default + }) + + it('should have hover effects defined in CSS', () => { + // Check that hover effects are defined in the component's style section + const component = wrapper.vm + expect(component).toBeDefined() + }) + }) + + describe('Component Methods', () => { + it('should have navigationRoute computed property', () => { + expect(wrapper.vm.navigationRoute).toBeDefined() + expect(typeof wrapper.vm.navigationRoute).toBe('object') + }) + + it('should compute navigationRoute correctly', () => { + const expectedRoute = { + name: 'contacts', + query: {} + } + expect(wrapper.vm.navigationRoute).toEqual(expectedRoute) + }) + + it('should compute navigationRoute with custom props', () => { + wrapper = mountComponent({ + routeName: 'projects', + queryParams: { filter: 'active' } + }) + + const expectedRoute = { + name: 'projects', + query: { filter: 'active' } + } + expect(wrapper.vm.navigationRoute).toEqual(expectedRoute) + }) + + it('should handle complex query parameters', () => { + const complexQuery = { + filter: 'active', + sort: 'name', + page: '1', + limit: '20' + } + + wrapper = mountComponent({ queryParams: complexQuery }) + + const expectedRoute = { + name: 'contacts', + query: complexQuery + } + expect(wrapper.vm.navigationRoute).toEqual(expectedRoute) + }) + }) + + describe('Edge Cases', () => { + it('should handle empty string routeName', () => { + wrapper = mountComponent({ routeName: '' }) + expect(wrapper.vm.navigationRoute).toEqual({ + name: '', + query: {} + }) + }) + + it('should handle null queryParams', () => { + wrapper = mountComponent({ queryParams: null as any }) + expect(wrapper.vm.navigationRoute).toEqual({ + name: 'contacts', + query: null + }) + }) + + it('should handle undefined queryParams', () => { + wrapper = mountComponent({ queryParams: undefined }) + expect(wrapper.vm.navigationRoute).toEqual({ + name: 'contacts', + query: {} + }) + }) + + it('should handle empty object queryParams', () => { + wrapper = mountComponent({ queryParams: {} }) + expect(wrapper.vm.navigationRoute).toEqual({ + name: 'contacts', + query: {} + }) + }) + + it('should handle rapid prop changes', async () => { + for (let i = 0; i < 10; i++) { + await wrapper.setProps({ + entityType: i % 2 === 0 ? 'people' : 'projects', + routeName: `route-${i}`, + queryParams: { index: i.toString() } + }) + + expect(wrapper.vm.entityType).toBe(i % 2 === 0 ? 'people' : 'projects') + expect(wrapper.vm.routeName).toBe(`route-${i}`) + expect(wrapper.vm.queryParams).toEqual({ index: i.toString() }) + } + }) + }) + + describe('Error Handling', () => { + it('should handle invalid entityType gracefully', () => { + wrapper = mountComponent({ entityType: 'invalid' as any }) + expect(wrapper.exists()).toBe(true) + expect(wrapper.vm.entityType).toBe('invalid') + }) + + it('should handle malformed queryParams gracefully', () => { + wrapper = mountComponent({ queryParams: 'invalid' as any }) + expect(wrapper.exists()).toBe(true) + // Should handle gracefully even with invalid queryParams + }) + + it('should handle missing props gracefully', () => { + // Component should not crash with missing props + expect(() => mountComponent({})).not.toThrow() + }) + + it('should handle extreme prop values', () => { + const extremeProps = { + entityType: 'people', + routeName: 'a'.repeat(1000), + queryParams: { key: 'value'.repeat(1000) } + } + + wrapper = mountComponent(extremeProps) + expect(wrapper.exists()).toBe(true) + expect(wrapper.vm.routeName).toBe(extremeProps.routeName) + }) + }) + + describe('Accessibility', () => { + it('should have semantic HTML structure', () => { + expect(wrapper.find('li').exists()).toBe(true) + expect(wrapper.find('h3').exists()).toBe(true) + }) + + it('should have proper heading hierarchy', () => { + const heading = wrapper.find('h3') + expect(heading.exists()).toBe(true) + expect(heading.text()).toBe('Show All') + }) + + it('should have accessible icon', () => { + const icon = wrapper.find('font-awesome') + expect(icon.exists()).toBe(true) + expect(icon.attributes('icon')).toBe('circle-right') + }) + + it('should have proper text content', () => { + const title = wrapper.find('h3') + expect(title.text()).toBe('Show All') + expect(title.text().trim()).toBe('Show All') + }) + }) + + describe('Performance', () => { + it('should render within acceptable time', () => { + const start = performance.now() + wrapper = mountComponent() + const end = performance.now() + + expect(end - start).toBeLessThan(100) // 100ms threshold + }) + + it('should handle rapid re-renders efficiently', async () => { + const start = performance.now() + + for (let i = 0; i < 50; i++) { + await wrapper.setProps({ + entityType: i % 2 === 0 ? 'people' : 'projects', + queryParams: { index: i.toString() } + }) + } + + const end = performance.now() + expect(end - start).toBeLessThan(500) // 500ms threshold for 50 updates + }) + + it('should not cause memory leaks during prop changes', async () => { + const initialMemory = (performance as any).memory?.usedJSHeapSize || 0 + + for (let i = 0; i < 100; i++) { + await wrapper.setProps({ + queryParams: { iteration: i.toString() } + }) + } + + const finalMemory = (performance as any).memory?.usedJSHeapSize || 0 + const memoryIncrease = finalMemory - initialMemory + + // Memory increase should be reasonable (less than 10MB) + expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024) + }) + }) + + describe('Integration', () => { + it('should work with router-link integration', () => { + const routerLink = wrapper.find('router-link') + expect(routerLink.exists()).toBe(true) + expect(routerLink.attributes('to')).toBeDefined() + }) + + it('should work with FontAwesome icon integration', () => { + const icon = wrapper.find('font-awesome') + expect(icon.exists()).toBe(true) + expect(icon.attributes('icon')).toBe('circle-right') + }) + + it('should work with Vue Router navigation', () => { + const navigationRoute = wrapper.vm.navigationRoute + expect(navigationRoute).toHaveProperty('name') + expect(navigationRoute).toHaveProperty('query') + }) + + it('should integrate with parent component props', () => { + const parentProps = { + entityType: 'projects' as const, + routeName: 'project-list', + queryParams: { category: 'featured' } + } + + wrapper = mountComponent(parentProps) + + expect(wrapper.vm.entityType).toBe(parentProps.entityType) + expect(wrapper.vm.routeName).toBe(parentProps.routeName) + expect(wrapper.vm.queryParams).toEqual(parentProps.queryParams) + }) + }) + + describe('Mock Integration Testing', () => { + it('should work with simple mock', () => { + const mock = new ShowAllCardSimpleMock() + expect(mock.navigationRoute).toEqual({ + name: 'contacts', + query: {} + }) + }) + + it('should work with standard mock', () => { + const mock = new ShowAllCardStandardMock({ + entityType: 'projects', + routeName: 'projects' + }) + expect(mock.getEntityType()).toBe('projects') + expect(mock.getRouteName()).toBe('projects') + }) + + it('should work with complex mock', () => { + const mock = new ShowAllCardComplexMock({ + entityType: 'people', + routeName: 'contacts', + queryParams: { filter: 'active' } + }) + + expect(mock.isValidState()).toBe(true) + expect(mock.getValidationErrors()).toEqual([]) + }) + + it('should work with factory functions', () => { + const peopleMock = createPeopleShowAllCardMock() + const projectsMock = createProjectsShowAllCardMock() + + expect(peopleMock.getEntityType()).toBe('people') + expect(projectsMock.getEntityType()).toBe('projects') + }) + + it('should work with complex query mock', () => { + const mock = createShowAllCardMockWithComplexQuery() + expect(mock.getQueryParams()).toHaveProperty('filter') + expect(mock.getQueryParams()).toHaveProperty('sort') + expect(mock.getQueryParams()).toHaveProperty('page') + }) + }) + + describe('Snapshot Testing', () => { + it('should maintain consistent DOM structure', () => { + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should maintain consistent structure with different props', () => { + wrapper = mountComponent({ entityType: 'projects' }) + expect(wrapper.html()).toMatchSnapshot() + }) + + it('should maintain consistent structure with query params', () => { + wrapper = mountComponent({ + queryParams: { filter: 'active', sort: 'name' } + }) + expect(wrapper.html()).toMatchSnapshot() + }) + }) +}) diff --git a/src/test/__mocks__/ShowAllCard.mock.ts b/src/test/__mocks__/ShowAllCard.mock.ts new file mode 100644 index 00000000..e2a313ca --- /dev/null +++ b/src/test/__mocks__/ShowAllCard.mock.ts @@ -0,0 +1,298 @@ +/** + * ShowAllCard Mock Component + * + * Provides three-tier mock architecture for testing: + * - Simple: Basic interface compliance + * - Standard: Full interface with realistic behavior + * - Complex: Enhanced testing capabilities + * + * @author Matthew Raymer + */ + +import { RouteLocationRaw } from "vue-router"; + +export interface ShowAllCardProps { + entityType: "people" | "projects"; + routeName: string; + queryParams?: Record; +} + +export interface ShowAllCardMock { + props: ShowAllCardProps; + navigationRoute: RouteLocationRaw; + getCssClasses(): string[]; + getIconClasses(): string[]; + getTitleClasses(): string[]; + simulateClick(): void; + simulateHover(): void; + getComputedNavigationRoute(): RouteLocationRaw; +} + +/** + * Simple Mock - Basic interface compliance + */ +export class ShowAllCardSimpleMock implements ShowAllCardMock { + props: ShowAllCardProps = { + entityType: "people", + routeName: "contacts", + queryParams: {} + }; + + get navigationRoute(): RouteLocationRaw { + return { + name: this.props.routeName, + query: this.props.queryParams || {} + }; + } + + getCssClasses(): string[] { + return ["cursor-pointer"]; + } + + getIconClasses(): string[] { + return ["text-blue-500", "text-5xl", "mb-1"]; + } + + getTitleClasses(): string[] { + return ["text-xs", "text-slate-500", "font-medium", "italic", "text-ellipsis", "whitespace-nowrap", "overflow-hidden"]; + } + + simulateClick(): void { + // Basic click simulation + } + + simulateHover(): void { + // Basic hover simulation + } + + getComputedNavigationRoute(): RouteLocationRaw { + return this.navigationRoute; + } +} + +/** + * Standard Mock - Full interface compliance with realistic behavior + */ +export class ShowAllCardStandardMock extends ShowAllCardSimpleMock { + constructor(props?: Partial) { + super(); + if (props) { + this.props = { ...this.props, ...props }; + } + } + + getCssClasses(): string[] { + return [ + "cursor-pointer", + "show-all-card", + `entity-type-${this.props.entityType}` + ]; + } + + getIconClasses(): string[] { + return [ + "text-blue-500", + "text-5xl", + "mb-1", + "fa-circle-right", + "transition-transform" + ]; + } + + getTitleClasses(): string[] { + return [ + "text-xs", + "text-slate-500", + "font-medium", + "italic", + "text-ellipsis", + "whitespace-nowrap", + "overflow-hidden", + "show-all-title" + ]; + } + + simulateClick(): void { + // Simulate router navigation + this.getComputedNavigationRoute(); + } + + simulateHover(): void { + // Simulate hover effects + this.getIconClasses().push("hover:scale-110"); + } + + getComputedNavigationRoute(): RouteLocationRaw { + return { + name: this.props.routeName, + query: this.props.queryParams || {} + }; + } + + // Helper methods for test scenarios + setEntityType(entityType: "people" | "projects"): void { + this.props.entityType = entityType; + } + + setRouteName(routeName: string): void { + this.props.routeName = routeName; + } + + setQueryParams(queryParams: Record): void { + this.props.queryParams = queryParams; + } + + getEntityType(): string { + return this.props.entityType; + } + + getRouteName(): string { + return this.props.routeName; + } + + getQueryParams(): Record { + return this.props.queryParams || {}; + } +} + +/** + * Complex Mock - Enhanced testing capabilities + */ +export class ShowAllCardComplexMock extends ShowAllCardStandardMock { + private clickCount: number = 0; + private hoverCount: number = 0; + private navigationHistory: RouteLocationRaw[] = []; + + constructor(props?: Partial) { + super(props); + } + + simulateClick(): void { + this.clickCount++; + const route = this.getComputedNavigationRoute(); + this.navigationHistory.push(route); + + // Simulate click event with additional context + this.getIconClasses().push("clicked"); + } + + simulateHover(): void { + this.hoverCount++; + this.getIconClasses().push("hovered", "scale-110"); + } + + // Performance testing hooks + getClickCount(): number { + return this.clickCount; + } + + getHoverCount(): number { + return this.hoverCount; + } + + getNavigationHistory(): RouteLocationRaw[] { + return [...this.navigationHistory]; + } + + // Error scenario simulation + simulateInvalidRoute(): void { + this.props.routeName = "invalid-route"; + } + + simulateEmptyQueryParams(): void { + this.props.queryParams = {}; + } + + simulateComplexQueryParams(): void { + this.props.queryParams = { + filter: "active", + sort: "name", + page: "1", + limit: "20" + }; + } + + // Accessibility testing support + getAccessibilityAttributes(): Record { + return { + role: "listitem", + "aria-label": `Show all ${this.props.entityType}`, + tabindex: "0" + }; + } + + // State validation helpers + isValidState(): boolean { + return !!this.props.entityType && + !!this.props.routeName && + typeof this.props.queryParams === "object"; + } + + getValidationErrors(): string[] { + const errors: string[] = []; + + if (!this.props.entityType) { + errors.push("entityType is required"); + } + + if (!this.props.routeName) { + errors.push("routeName is required"); + } + + if (this.props.queryParams && typeof this.props.queryParams !== "object") { + errors.push("queryParams must be an object"); + } + + return errors; + } + + // Reset functionality for test isolation + reset(): void { + this.clickCount = 0; + this.hoverCount = 0; + this.navigationHistory = []; + this.props = { + entityType: "people", + routeName: "contacts", + queryParams: {} + }; + } +} + +// Default export for convenience +export default ShowAllCardComplexMock; + +// Factory functions for common test scenarios +export const createShowAllCardMock = (props?: Partial): ShowAllCardComplexMock => { + return new ShowAllCardComplexMock(props); +}; + +export const createPeopleShowAllCardMock = (): ShowAllCardComplexMock => { + return new ShowAllCardComplexMock({ + entityType: "people", + routeName: "contacts", + queryParams: { filter: "all" } + }); +}; + +export const createProjectsShowAllCardMock = (): ShowAllCardComplexMock => { + return new ShowAllCardComplexMock({ + entityType: "projects", + routeName: "projects", + queryParams: { sort: "name" } + }); +}; + +export const createShowAllCardMockWithComplexQuery = (): ShowAllCardComplexMock => { + return new ShowAllCardComplexMock({ + entityType: "people", + routeName: "contacts", + queryParams: { + filter: "active", + sort: "name", + page: "1", + limit: "20", + search: "test" + } + }); +}; diff --git a/src/test/__snapshots__/ShowAllCard.test.ts.snap b/src/test/__snapshots__/ShowAllCard.test.ts.snap new file mode 100644 index 00000000..652f6ec2 --- /dev/null +++ b/src/test/__snapshots__/ShowAllCard.test.ts.snap @@ -0,0 +1,28 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ShowAllCard > Snapshot Testing > should maintain consistent DOM structure 1`] = ` +"
  • + + +

    Show All

    +
    +
  • " +`; + +exports[`ShowAllCard > Snapshot Testing > should maintain consistent structure with different props 1`] = ` +"
  • + + +

    Show All

    +
    +
  • " +`; + +exports[`ShowAllCard > Snapshot Testing > should maintain consistent structure with query params 1`] = ` +"
  • + + +

    Show All

    +
    +
  • " +`;