diff --git a/e2e/cypress/integration/components/datepicker.cy.ts b/e2e/cypress/integration/components/datepicker.cy.ts new file mode 100644 index 000000000..f51c24e6e --- /dev/null +++ b/e2e/cypress/integration/components/datepicker.cy.ts @@ -0,0 +1,252 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' +import { dataHelpers } from '../../support/helpers/data-helpers' + +describe('DatePicker Component Tests', () => { + beforeEach(() => { + cy.visit('http://localhost:3001') + }) + + describe('DatePicker Display', () => { + it('should display date input', () => { + cy.getByTestId('date-input-field').should('be.visible') + }) + + it('should show placeholder', () => { + cy.getByTestId('date-input-field').should('have.attr', 'placeholder') + }) + + it('should open calendar on click', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-popup').should('be.visible') + }) + + it('should display calendar header', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-header').should('be.visible') + }) + + it('should display calendar grid', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-grid').should('be.visible') + }) + }) + + describe('Date Selection', () => { + it('should select date', () => { + const dates = dataHelpers.getTestDates() + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day').contains(dates.tomorrow.split('-')[2]).click() + cy.getByTestId('date-input-field').should('have.value', dates.tomorrow) + }) + + it('should close calendar after selection', () => { + const dates = dataHelpers.getTestDates() + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day').contains(dates.tomorrow.split('-')[2]).click() + cy.getByTestId('calendar-popup').should('not.be.visible') + }) + + it('should highlight selected date', () => { + const dates = dataHelpers.getTestDates() + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day').contains(dates.tomorrow.split('-')[2]).click() + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day-selected').should('be.visible') + }) + }) + + describe('Month Navigation', () => { + it('should navigate to next month', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('next-month-button').click() + cy.getByTestId('calendar-month-display').should('contain', new Date(new Date().setMonth(new Date().getMonth() + 1)).toLocaleDateString()) + }) + + it('should navigate to previous month', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('next-month-button').click() + cy.getByTestId('prev-month-button').click() + cy.getByTestId('calendar-month-display').should('be.visible') + }) + + it('should disable prev button on minimum month', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('prev-month-button').should('have.attr', 'disabled') + }) + }) + + describe('Year Navigation', () => { + it('should navigate to next year', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('next-year-button').click() + cy.getByTestId('calendar-year-display').should('contain', new Date().getFullYear() + 1) + }) + + it('should navigate to previous year', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('prev-year-button').click() + cy.getByTestId('calendar-year-display').should('be.visible') + }) + }) + + describe('Quick Select', () => { + it('should select today', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('today-button').click() + const today = new Date().toISOString().split('T')[0] + cy.getByTestId('date-input-field').should('have.value', today) + }) + + it('should select tomorrow', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('tomorrow-button').click() + const tomorrow = new Date(new Date().getTime() + 86400000).toISOString().split('T')[0] + cy.getByTestId('date-input-field').should('have.value', tomorrow) + }) + + it('should select next week', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('next-week-button').click() + cy.getByTestId('date-input-field').should('have.value') + }) + + it('should select next month', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('next-month-preset-button').click() + cy.getByTestId('date-input-field').should('have.value') + }) + }) + + describe('Date Range', () => { + it('should select date range', () => { + const dates = dataHelpers.getTestDates() + cy.getByTestId('range-start-input').click() + cy.getByTestId('calendar-day').contains(dates.tomorrow.split('-')[2]).click() + cy.getByTestId('range-end-input').click() + cy.getByTestId('calendar-day').contains(dates.weekLater.split('-')[2]).click() + cy.getByTestId('range-start-input').should('have.value', dates.tomorrow) + }) + + it('should highlight date range', () => { + const dates = dataHelpers.getTestDates() + cy.getByTestId('range-start-input').click() + cy.getByTestId('calendar-day').contains(dates.tomorrow.split('-')[2]).click() + cy.getByTestId('range-end-input').click() + cy.getByTestId('calendar-day').contains(dates.weekLater.split('-')[2]).click() + cy.getByTestId('calendar-range-highlight').should('be.visible') + }) + + it('should prevent invalid range', () => { + const dates = dataHelpers.getTestDates() + cy.getByTestId('range-start-input').click() + cy.getByTestId('calendar-day').contains(dates.weekLater.split('-')[2]).click() + cy.getByTestId('range-end-input').click() + cy.getByTestId('calendar-day').contains(dates.tomorrow.split('-')[2]).click({ force: true }) + // Should not allow end date before start + cy.getByTestId('invalid-range-error').should('be.visible') + }) + }) + + describe('Disabled Dates', () => { + it('should disable past dates', () => { + cy.getByTestId('no-past-dates-input').click() + cy.getByTestId('calendar-day-disabled').should('have.class', 'disabled') + }) + + it('should disable specific dates', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day-disabled').should('have.class', 'disabled') + }) + + it('should prevent selecting disabled date', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day-disabled').first().click({ force: true }) + cy.getByTestId('date-input-field').should('have.value', '') + }) + }) + + describe('Input Validation', () => { + it('should validate date format', () => { + cy.getByTestId('date-input-field').type('invalid') + cy.getByTestId('date-format-error').should('be.visible') + }) + + it('should accept valid date format', () => { + cy.getByTestId('date-input-field').type('2025-05-15') + cy.getByTestId('date-format-error').should('not.exist') + }) + + it('should clear error on valid input', () => { + cy.getByTestId('date-input-field').type('invalid') + cy.getByTestId('date-format-error').should('be.visible') + cy.getByTestId('date-input-field').clear().type('2025-05-15') + cy.getByTestId('date-format-error').should('not.exist') + }) + }) + + describe('Keyboard Navigation', () => { + it('should navigate with arrow keys', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day').first().focus() + cy.getByTestId('calendar-day').first().type('{rightarrow}') + cy.getByTestId('calendar-day').eq(1).should('have.focus') + }) + + it('should select with enter key', () => { + const dates = dataHelpers.getTestDates() + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day').contains(dates.tomorrow.split('-')[2]).focus() + cy.getByTestId('calendar-day').contains(dates.tomorrow.split('-')[2]).type('{enter}') + cy.getByTestId('calendar-popup').should('not.be.visible') + }) + + it('should close with escape', () => { + cy.getByTestId('date-input-field').click() + cy.get('body').type('{esc}') + cy.getByTestId('calendar-popup').should('not.be.visible') + }) + }) + + describe('Accessibility', () => { + it('should have label', () => { + cy.getByTestId('date-input-label').should('be.visible') + }) + + it('should have aria-label', () => { + cy.getByTestId('date-input-field').should('have.attr', 'aria-label') + }) + + it('should announce calendar days', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-day').first().should('have.attr', 'aria-label') + }) + + it('should have calendar dialog role', () => { + cy.getByTestId('date-input-field').click() + cy.getByTestId('calendar-popup').should('have.attr', 'role', 'dialog') + }) + }) + + describe('Disabled State', () => { + it('should disable date input', () => { + cy.getByTestId('disabled-date-input').should('be.disabled') + }) + + it('should not open calendar when disabled', () => { + cy.getByTestId('disabled-date-input').click({ force: true }) + cy.getByTestId('calendar-popup').should('not.exist') + }) + }) + + describe('Error Handling', () => { + it('should handle invalid date gracefully', () => { + cy.getByTestId('date-input-field').type('2025-13-45') + cy.getByTestId('date-input-field').should('be.visible') + }) + + it('should show error message', () => { + cy.getByTestId('date-input-field').type('invalid') + cy.getByTestId('date-error-message').should('be.visible') + }) + }) +}) diff --git a/e2e/cypress/integration/components/input.cy.ts b/e2e/cypress/integration/components/input.cy.ts new file mode 100644 index 000000000..67fbd5d90 --- /dev/null +++ b/e2e/cypress/integration/components/input.cy.ts @@ -0,0 +1,271 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' + +describe('Input Component Tests', () => { + beforeEach(() => { + cy.visit('http://localhost:3001') + }) + + describe('Input Display', () => { + it('should render input element', () => { + cy.getByTestId('text-input').should('be.visible') + }) + + it('should have placeholder text', () => { + cy.getByTestId('text-input').should('have.attr', 'placeholder') + }) + + it('should have correct input type', () => { + cy.getByTestId('text-input').should('have.attr', 'type', 'text') + }) + + it('should be enabled by default', () => { + cy.getByTestId('text-input').should('not.be.disabled') + }) + }) + + describe('Input Interaction', () => { + it('should accept text input', () => { + cy.getByTestId('text-input').type('test value') + cy.getByTestId('text-input').should('have.value', 'test value') + }) + + it('should clear input value', () => { + cy.getByTestId('text-input').type('test') + cy.getByTestId('text-input').clear() + cy.getByTestId('text-input').should('have.value', '') + }) + + it('should support focus', () => { + cy.getByTestId('text-input').focus() + cy.getByTestId('text-input').should('have.focus') + }) + + it('should trigger change event', () => { + cy.getByTestId('text-input').type('test') + cy.getByTestId('input-change-count').should('contain', '1') + }) + + it('should trigger blur event', () => { + cy.getByTestId('text-input').focus().blur() + cy.getByTestId('input-blur-count').should('contain', '1') + }) + }) + + describe('Input Validation', () => { + it('should show required error', () => { + cy.getByTestId('required-input').focus().blur() + cy.getByTestId('required-error').should('be.visible') + }) + + it('should show min length error', () => { + cy.getByTestId('min-length-input').type('a') + cy.getByTestId('min-length-error').should('be.visible') + }) + + it('should show email validation error', () => { + cy.getByTestId('email-input').type('invalid') + cy.getByTestId('email-error').should('be.visible') + }) + + it('should clear error on valid input', () => { + cy.getByTestId('email-input').type('invalid') + cy.getByTestId('email-error').should('be.visible') + cy.getByTestId('email-input').clear().type('test@example.com') + cy.getByTestId('email-error').should('not.be.visible') + }) + + it('should validate pattern', () => { + cy.getByTestId('phone-input').type('abc') + cy.getByTestId('phone-error').should('be.visible') + }) + }) + + describe('Input Disabled State', () => { + it('should disable input', () => { + cy.getByTestId('disabled-input').should('be.disabled') + }) + + it('should prevent typing in disabled input', () => { + cy.getByTestId('disabled-input').type('test', { force: true }) + cy.getByTestId('disabled-input').should('have.value', '') + }) + + it('should show disabled styling', () => { + cy.getByTestId('disabled-input').should('have.css', 'opacity') + }) + }) + + describe('Input Read-only State', () => { + it('should be read-only', () => { + cy.getByTestId('readonly-input').should('have.attr', 'readonly') + }) + + it('should prevent modification', () => { + cy.getByTestId('readonly-input').type('test', { force: true }) + cy.getByTestId('readonly-input').should('not.have.value', 'test') + }) + }) + + describe('Input Types', () => { + it('should support text input', () => { + cy.getByTestId('text-input').type('test') + cy.getByTestId('text-input').should('have.value', 'test') + }) + + it('should support email input', () => { + cy.getByTestId('email-input').type('test@example.com') + cy.getByTestId('email-input').should('have.value', 'test@example.com') + }) + + it('should support password input', () => { + cy.getByTestId('password-input').type('secret123') + cy.getByTestId('password-input').should('have.attr', 'type', 'password') + }) + + it('should support number input', () => { + cy.getByTestId('number-input').type('123') + cy.getByTestId('number-input').should('have.value', '123') + }) + + it('should support date input', () => { + cy.getByTestId('date-input').type('2025-05-15') + cy.getByTestId('date-input').should('have.value', '2025-05-15') + }) + }) + + describe('Input Variants', () => { + it('should render standard input', () => { + cy.getByTestId('standard-input').should('be.visible') + }) + + it('should render outlined input', () => { + cy.getByTestId('outlined-input').should('have.class', 'outlined') + }) + + it('should render filled input', () => { + cy.getByTestId('filled-input').should('have.class', 'filled') + }) + + it('should have different sizes', () => { + cy.getByTestId('small-input').should('have.class', 'small') + cy.getByTestId('medium-input').should('have.class', 'medium') + cy.getByTestId('large-input').should('have.class', 'large') + }) + }) + + describe('Input Prefix/Suffix', () => { + it('should display prefix icon', () => { + cy.getByTestId('input-with-prefix').within(() => { + cy.getByTestId('input-prefix').should('be.visible') + }) + }) + + it('should display suffix icon', () => { + cy.getByTestId('input-with-suffix').within(() => { + cy.getByTestId('input-suffix').should('be.visible') + }) + }) + + it('should allow suffix action', () => { + cy.getByTestId('input-with-clear-button').within(() => { + cy.getByTestId('clear-button').should('be.visible') + }) + }) + + it('should toggle visibility with suffix', () => { + cy.getByTestId('password-input-with-toggle').within(() => { + cy.getByTestId('toggle-visibility-button').click() + cy.getByTestId('password-input').should('have.attr', 'type', 'text') + }) + }) + }) + + describe('Input Placeholder Behavior', () => { + it('should show placeholder when empty', () => { + cy.getByTestId('text-input').should('have.attr', 'placeholder', 'Enter text') + }) + + it('should hide placeholder when focused', () => { + cy.getByTestId('text-input').focus() + cy.getByTestId('text-input').should('have.attr', 'placeholder') + }) + + it('should hide placeholder when value present', () => { + cy.getByTestId('text-input').type('value') + // Placeholder attr still exists but visual should be hidden + cy.getByTestId('text-input').should('have.value', 'value') + }) + }) + + describe('Input Label', () => { + it('should display input label', () => { + cy.getByTestId('labeled-input-label').should('be.visible') + }) + + it('should associate label with input', () => { + cy.getByTestId('labeled-input-label').click() + cy.getByTestId('labeled-input').should('have.focus') + }) + + it('should show required indicator', () => { + cy.getByTestId('required-input-label').should('contain', '*') + }) + + it('should show error state in label', () => { + cy.getByTestId('error-input').focus().blur() + cy.getByTestId('error-input-label').should('have.class', 'error') + }) + }) + + describe('Input Helper Text', () => { + it('should display helper text', () => { + cy.getByTestId('input-helper-text').should('be.visible') + }) + + it('should show character count', () => { + cy.getByTestId('input-with-counter').type('test') + cy.getByTestId('char-count').should('contain', '4') + }) + + it('should show error text', () => { + cy.getByTestId('error-input').focus().blur() + cy.getByTestId('error-message').should('be.visible') + }) + }) + + describe('Input Performance', () => { + it('should handle rapid typing', () => { + cy.getByTestId('text-input').type('abcdefghijklmnop{backspace}{backspace}') + cy.getByTestId('text-input').should('have.value', 'abcdefghijklmno') + }) + + it('should handle long text', () => { + const longText = 'a'.repeat(500) + cy.getByTestId('text-input').type(longText) + cy.getByTestId('text-input').should('have.value', longText) + }) + + it('should handle paste events', () => { + cy.getByTestId('text-input').invoke('val', 'pasted text') + cy.getByTestId('text-input').trigger('input') + cy.getByTestId('text-input').should('have.value', 'pasted text') + }) + }) + + describe('Accessibility', () => { + it('should have accessible name', () => { + cy.getByTestId('labeled-input').should('have.attr', 'aria-label') + }) + + it('should announce errors', () => { + cy.getByTestId('error-input').focus().blur() + cy.getByTestId('error-input').should('have.attr', 'aria-invalid', 'true') + }) + + it('should support keyboard shortcuts', () => { + cy.getByTestId('text-input').focus() + cy.getByTestId('text-input').type('{ctrl}a') + cy.getByTestId('text-input').type('{ctrl}c') + }) + }) +}) diff --git a/e2e/cypress/integration/components/modal.cy.ts b/e2e/cypress/integration/components/modal.cy.ts new file mode 100644 index 000000000..cefb1e5e9 --- /dev/null +++ b/e2e/cypress/integration/components/modal.cy.ts @@ -0,0 +1,246 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' + +describe('Modal Component Tests', () => { + beforeEach(() => { + cy.visit('http://localhost:3001') + }) + + describe('Modal Display', () => { + it('should display modal when opened', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-container').should('be.visible') + }) + + it('should have modal header', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-header').should('be.visible') + }) + + it('should have modal body', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-body').should('be.visible') + }) + + it('should have modal footer', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-footer').should('be.visible') + }) + + it('should have close button', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-close-button').should('be.visible') + }) + + it('should display modal title', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-title').should('be.visible') + }) + }) + + describe('Modal Closing', () => { + it('should close on close button click', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-close-button').click() + cy.getByTestId('modal-container').should('not.be.visible') + }) + + it('should close on escape key', () => { + cy.getByTestId('modal-trigger-button').click() + cy.get('body').type('{esc}') + cy.getByTestId('modal-container').should('not.be.visible') + }) + + it('should close on backdrop click', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-backdrop').click({ force: true }) + cy.getByTestId('modal-container').should('not.be.visible') + }) + + it('should close on confirm button', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-confirm-button').click() + cy.getByTestId('modal-container').should('not.be.visible') + }) + }) + + describe('Modal Sizes', () => { + it('should render small modal', () => { + cy.getByTestId('small-modal-trigger').click() + cy.getByTestId('modal-container').should('have.class', 'small') + }) + + it('should render medium modal', () => { + cy.getByTestId('medium-modal-trigger').click() + cy.getByTestId('modal-container').should('have.class', 'medium') + }) + + it('should render large modal', () => { + cy.getByTestId('large-modal-trigger').click() + cy.getByTestId('modal-container').should('have.class', 'large') + }) + + it('should render full width modal', () => { + cy.getByTestId('fullwidth-modal-trigger').click() + cy.getByTestId('modal-container').should('have.class', 'fullwidth') + }) + }) + + describe('Modal Content', () => { + it('should accept custom content', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-body').should('contain', 'Modal content') + }) + + it('should support scrollable content', () => { + cy.getByTestId('scrollable-modal-trigger').click() + cy.getByTestId('modal-body').scrollTo('bottom') + cy.getByTestId('modal-body').should('be.visible') + }) + + it('should display custom actions', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-confirm-button').should('be.visible') + cy.getByTestId('modal-cancel-button').should('be.visible') + }) + }) + + describe('Modal Buttons', () => { + it('should have confirm button', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-confirm-button').should('be.visible') + }) + + it('should have cancel button', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-cancel-button').should('be.visible') + }) + + it('should handle button click', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-confirm-button').click() + cy.getByTestId('modal-action-result').should('contain', 'confirmed') + }) + + it('should disable button when loading', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-confirm-button').click() + cy.getByTestId('modal-loading-state').should('be.visible') + }) + }) + + describe('Modal Animations', () => { + it('should animate modal open', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-container').should('have.css', 'animation') + }) + + it('should animate modal close', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-close-button').click() + cy.getByTestId('modal-container').should('not.be.visible') + }) + }) + + describe('Modal Stacking', () => { + it('should stack multiple modals', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('nested-modal-trigger').click() + cy.getByTestId('nested-modal-container').should('be.visible') + }) + + it('should close top modal first', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('nested-modal-trigger').click() + cy.getByTestId('nested-modal-close-button').click() + cy.getByTestId('nested-modal-container').should('not.be.visible') + cy.getByTestId('modal-container').should('be.visible') + }) + }) + + describe('Modal Backdrop', () => { + it('should show backdrop', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-backdrop').should('be.visible') + }) + + it('should prevent background scroll', () => { + cy.getByTestId('modal-trigger-button').click() + cy.get('body').should('have.css', 'overflow', 'hidden') + }) + + it('should restore scroll on close', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-close-button').click() + cy.get('body').should('not.have.css', 'overflow', 'hidden') + }) + }) + + describe('Modal Focus Management', () => { + it('should focus close button on open', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-close-button').should('have.focus') + }) + + it('should trap focus inside modal', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-confirm-button').focus() + cy.getByTestId('modal-confirm-button').type('{tab}') + cy.getByTestId('modal-close-button').should('have.focus') + }) + + it('should return focus to trigger on close', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-close-button').click() + cy.getByTestId('modal-trigger-button').should('have.focus') + }) + }) + + describe('Accessibility', () => { + it('should have role dialog', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-container').should('have.attr', 'role', 'dialog') + }) + + it('should have aria-labelledby', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-container').should('have.attr', 'aria-labelledby') + }) + + it('should announce title to screen readers', () => { + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-title').should('have.attr', 'id') + }) + + it('should support keyboard commands', () => { + cy.getByTestId('modal-trigger-button').click() + cy.get('body').type('{esc}') + cy.getByTestId('modal-container').should('not.be.visible') + }) + }) + + describe('Modal Responsive', () => { + it('should be responsive on mobile', () => { + cy.viewport('iphone-x') + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-container').should('be.visible') + }) + + it('should adjust size on tablet', () => { + cy.viewport('ipad-2') + cy.getByTestId('modal-trigger-button').click() + cy.getByTestId('modal-container').should('be.visible') + }) + }) + + describe('Error Handling', () => { + it('should handle missing content gracefully', () => { + cy.getByTestId('empty-modal-trigger').click() + cy.getByTestId('modal-container').should('be.visible') + }) + + it('should display error message', () => { + cy.getByTestId('error-modal-trigger').click() + cy.getByTestId('modal-error-message').should('be.visible') + }) + }) +}) diff --git a/e2e/cypress/integration/components/tabs.cy.ts b/e2e/cypress/integration/components/tabs.cy.ts new file mode 100644 index 000000000..be9313021 --- /dev/null +++ b/e2e/cypress/integration/components/tabs.cy.ts @@ -0,0 +1,189 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' + +describe('Tabs Component Tests', () => { + beforeEach(() => { + cy.visit('http://localhost:3001') + }) + + describe('Tabs Display', () => { + it('should display tab list', () => { + cy.getByTestId('tabs-container').should('be.visible') + }) + + it('should display tab buttons', () => { + cy.getByTestId('tab-button').should('have.length.greaterThan', 1) + }) + + it('should display active tab indicator', () => { + cy.getByTestId('tab-indicator').should('be.visible') + }) + + it('should show first tab as active by default', () => { + cy.getByTestId('tab-button').first().should('have.class', 'active') + }) + }) + + describe('Tab Navigation', () => { + it('should switch to second tab', () => { + cy.getByTestId('tab-button').eq(1).click() + cy.getByTestId('tab-button').eq(1).should('have.class', 'active') + }) + + it('should deactivate previous tab', () => { + cy.getByTestId('tab-button').eq(1).click() + cy.getByTestId('tab-button').first().should('not.have.class', 'active') + }) + + it('should display correct content for tab', () => { + cy.getByTestId('tab-button').eq(1).click() + cy.getByTestId('tab-content').eq(1).should('be.visible') + }) + + it('should hide content of inactive tab', () => { + cy.getByTestId('tab-button').first().click() + cy.getByTestId('tab-content').eq(1).should('not.be.visible') + }) + }) + + describe('Tab Content', () => { + it('should display tab label', () => { + cy.getByTestId('tab-label').first().should('be.visible') + }) + + it('should support icon in tab', () => { + cy.getByTestId('tab-icon').should('be.visible') + }) + + it('should display badge on tab', () => { + cy.getByTestId('tab-badge').should('be.visible') + }) + + it('should show content panel', () => { + cy.getByTestId('tab-panel').should('have.length.greaterThan', 0) + }) + }) + + describe('Tab Variants', () => { + it('should render standard tabs', () => { + cy.getByTestId('standard-tabs').should('be.visible') + }) + + it('should render filled tabs', () => { + cy.getByTestId('filled-tabs').should('have.class', 'filled') + }) + + it('should render scrollable tabs', () => { + cy.getByTestId('scrollable-tabs').should('have.class', 'scrollable') + }) + + it('should render vertical tabs', () => { + cy.getByTestId('vertical-tabs').should('have.class', 'vertical') + }) + }) + + describe('Disabled Tabs', () => { + it('should disable tab', () => { + cy.getByTestId('disabled-tab-button').should('have.attr', 'aria-disabled', 'true') + }) + + it('should prevent clicking disabled tab', () => { + cy.getByTestId('disabled-tab-button').click({ force: true }) + cy.getByTestId('disabled-tab-button').should('not.have.class', 'active') + }) + }) + + describe('Tab Keyboard Navigation', () => { + it('should navigate with arrow keys', () => { + cy.getByTestId('tab-button').first().focus() + cy.getByTestId('tab-button').first().type('{rightarrow}') + cy.getByTestId('tab-button').eq(1).should('have.focus') + }) + + it('should navigate backward', () => { + cy.getByTestId('tab-button').eq(1).focus() + cy.getByTestId('tab-button').eq(1).type('{leftarrow}') + cy.getByTestId('tab-button').first().should('have.focus') + }) + + it('should activate with enter', () => { + cy.getByTestId('tab-button').eq(1).focus() + cy.getByTestId('tab-button').eq(1).type('{enter}') + cy.getByTestId('tab-button').eq(1).should('have.class', 'active') + }) + }) + + describe('Tab Scrolling', () => { + it('should show scroll buttons when needed', () => { + cy.getByTestId('scrollable-tabs-button-prev').should('be.visible') + cy.getByTestId('scrollable-tabs-button-next').should('be.visible') + }) + + it('should scroll tabs left', () => { + cy.getByTestId('scrollable-tabs-button-prev').click() + cy.getByTestId('scrollable-tabs-container').should('be.visible') + }) + + it('should scroll tabs right', () => { + cy.getByTestId('scrollable-tabs-button-next').click() + cy.getByTestId('scrollable-tabs-container').should('be.visible') + }) + }) + + describe('Accessibility', () => { + it('should have tablist role', () => { + cy.getByTestId('tabs-container').should('have.attr', 'role', 'tablist') + }) + + it('should have tab role for buttons', () => { + cy.getByTestId('tab-button').first().should('have.attr', 'role', 'tab') + }) + + it('should have tabpanel role for content', () => { + cy.getByTestId('tab-panel').first().should('have.attr', 'role', 'tabpanel') + }) + + it('should set aria-selected correctly', () => { + cy.getByTestId('tab-button').first().should('have.attr', 'aria-selected', 'true') + cy.getByTestId('tab-button').eq(1).should('have.attr', 'aria-selected', 'false') + }) + + it('should manage focus properly', () => { + cy.getByTestId('tab-button').first().focus() + cy.getByTestId('tab-button').first().should('have.focus') + }) + }) + + describe('Tab Animations', () => { + it('should animate tab change', () => { + cy.getByTestId('tab-button').eq(1).click() + cy.getByTestId('tab-panel').eq(1).should('have.css', 'animation') + }) + + it('should animate indicator movement', () => { + cy.getByTestId('tab-button').eq(1).click() + cy.getByTestId('tab-indicator').should('have.css', 'transition') + }) + }) + + describe('Tab Events', () => { + it('should trigger change event', () => { + cy.getByTestId('tab-button').eq(1).click() + cy.getByTestId('tab-change-count').should('contain', '1') + }) + + it('should provide selected tab info', () => { + cy.getByTestId('tab-button').eq(1).click() + cy.getByTestId('selected-tab-index').should('contain', '1') + }) + }) + + describe('Error Handling', () => { + it('should handle empty tabs', () => { + cy.getByTestId('empty-tabs').should('be.visible') + }) + + it('should handle missing content', () => { + cy.getByTestId('incomplete-tabs').should('be.visible') + }) + }) +})