diff --git a/e2e/cypress/integration/schedule/download.cy.ts b/e2e/cypress/integration/schedule/download.cy.ts new file mode 100644 index 000000000..31bdea309 --- /dev/null +++ b/e2e/cypress/integration/schedule/download.cy.ts @@ -0,0 +1,266 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' + +describe('Schedule - Download Functionality', () => { + beforeEach(() => { + cy.visit('http://localhost:3001/schedule') + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 200, + body: { schedules: [{ id: '1', flight: 'SU123' }] }, + }).as('scheduleSearch') + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + + describe('Download Options', () => { + it('should display download button', () => { + cy.getByTestId('download-schedule-button').should('be.visible') + }) + + it('should show download format options', () => { + cy.getByTestId('download-schedule-button').click() + cy.getByTestId('download-format-menu').should('be.visible') + }) + + it('should allow CSV export', () => { + cy.getByTestId('download-csv-option').should('be.visible') + }) + + it('should allow PDF export', () => { + cy.getByTestId('download-pdf-option').should('be.visible') + }) + + it('should allow Excel export', () => { + cy.getByTestId('download-excel-option').should('be.visible') + }) + + it('should allow JSON export', () => { + cy.getByTestId('download-json-option').should('be.visible') + }) + }) + + describe('CSV Download', () => { + it('should download schedule as CSV', () => { + cy.getByTestId('download-csv-option').click() + cy.readFile('cypress/downloads/schedule.csv').should('exist') + }) + + it('should include headers in CSV', () => { + cy.getByTestId('download-csv-option').click() + cy.readFile('cypress/downloads/schedule.csv').then(content => { + expect(content).to.contain('Flight') + expect(content).to.contain('Departure') + expect(content).to.contain('Arrival') + }) + }) + + it('should include all schedule data in CSV', () => { + cy.getByTestId('download-csv-option').click() + cy.readFile('cypress/downloads/schedule.csv').then(content => { + expect(content).to.contain('SU') + }) + }) + + it('should format dates correctly in CSV', () => { + cy.getByTestId('download-csv-option').click() + cy.readFile('cypress/downloads/schedule.csv').then(content => { + expect(content).to.match(/\d{4}-\d{2}-\d{2}/) + }) + }) + }) + + describe('PDF Download', () => { + it('should download schedule as PDF', () => { + cy.getByTestId('download-pdf-option').click() + cy.readFile('cypress/downloads/schedule.pdf').should('exist') + }) + + it('should include header in PDF', () => { + cy.getByTestId('download-pdf-option').click() + // PDF validation would be more complex + cy.readFile('cypress/downloads/schedule.pdf', 'binary') + .should('include', 'PDF') + }) + + it('should include schedule table in PDF', () => { + cy.getByTestId('download-pdf-option').click() + cy.readFile('cypress/downloads/schedule.pdf').should('exist') + }) + + it('should format PDF for printing', () => { + cy.getByTestId('download-pdf-option').click() + cy.readFile('cypress/downloads/schedule.pdf').should('exist') + }) + }) + + describe('Excel Download', () => { + it('should download schedule as Excel', () => { + cy.getByTestId('download-excel-option').click() + cy.readFile('cypress/downloads/schedule.xlsx').should('exist') + }) + + it('should create properly formatted Excel file', () => { + cy.getByTestId('download-excel-option').click() + cy.readFile('cypress/downloads/schedule.xlsx', 'binary').should('exist') + }) + }) + + describe('Download Settings', () => { + it('should allow selecting columns to export', () => { + cy.getByTestId('download-settings-button').click() + cy.getByTestId('select-columns-option').should('be.visible') + }) + + it('should allow date range selection', () => { + cy.getByTestId('download-settings-button').click() + cy.getByTestId('date-range-picker').should('be.visible') + }) + + it('should allow filtering routes before download', () => { + cy.getByTestId('download-settings-button').click() + cy.getByTestId('filter-routes-option').should('be.visible') + }) + + it('should apply custom filename', () => { + cy.getByTestId('download-settings-button').click() + cy.getByTestId('filename-input').clear().type('my-schedule') + cy.getByTestId('download-csv-option').click() + cy.readFile('cypress/downloads/my-schedule.csv').should('exist') + }) + }) + + describe('Batch Download', () => { + it('should select multiple schedules', () => { + cy.getByTestId('select-all-checkbox').click() + cy.getByTestId('schedule-item').each($item => { + cy.wrap($item).should('have.class', 'selected') + }) + }) + + it('should download selected schedules', () => { + cy.getByTestId('schedule-item').first().find('input[type="checkbox"]').click() + cy.getByTestId('download-selected-button').click() + cy.readFile('cypress/downloads/schedule.csv').should('exist') + }) + + it('should show selection count', () => { + cy.getByTestId('schedule-item').first().find('input[type="checkbox"]').click() + cy.getByTestId('selected-count-badge').should('contain', '1') + }) + }) + + describe('Download Progress', () => { + it('should show download progress', () => { + cy.intercept('GET', '**/api/schedule/export**', { + statusCode: 200, + body: {}, + delay: 1000, + }).as('export') + + cy.getByTestId('download-csv-option').click() + cy.getByTestId('download-progress-bar').should('be.visible') + cy.wait('@export') + cy.getByTestId('download-complete-message').should('be.visible') + }) + + it('should show file size before download', () => { + cy.getByTestId('download-settings-button').click() + cy.getByTestId('estimated-file-size').should('be.visible') + }) + + it('should allow cancel during download', () => { + cy.getByTestId('download-csv-option').click() + cy.getByTestId('cancel-download-button').click() + cy.getByTestId('download-cancelled-message').should('be.visible') + }) + }) + + describe('Download Validation', () => { + it('should validate data before download', () => { + cy.getByTestId('download-csv-option').click() + cy.readFile('cypress/downloads/schedule.csv').then(content => { + const lines = content.split('\n') + expect(lines.length).to.be.greaterThan(1) + }) + }) + + it('should handle empty schedule download', () => { + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 200, + body: { schedules: [] }, + }).as('emptySearch') + + cy.reload() + cy.getByTestId('download-csv-option').should('be.disabled') + }) + + it('should show file format info', () => { + cy.getByTestId('download-csv-option').trigger('mouseenter') + cy.getByTestId('format-info-tooltip').should('be.visible') + }) + }) + + describe('Download History', () => { + it('should show download history', () => { + cy.getByTestId('download-history-button').click() + cy.getByTestId('download-history-list').should('be.visible') + }) + + it('should allow re-downloading from history', () => { + cy.getByTestId('download-csv-option').click() + cy.getByTestId('download-history-button').click() + cy.getByTestId('download-history-item').first().click() + cy.readFile('cypress/downloads/schedule.csv').should('exist') + }) + + it('should clear download history', () => { + cy.getByTestId('clear-history-button').click() + cy.getByTestId('download-history-list').should('contain', 'No downloads') + }) + }) + + describe('Error Handling', () => { + it('should handle download failure', () => { + cy.intercept('GET', '**/api/schedule/export**', { + statusCode: 500, + body: { error: 'Export failed' }, + }).as('exportError') + + cy.getByTestId('download-csv-option').click() + cy.wait('@exportError') + cy.getByTestId('download-error-message').should('be.visible') + }) + + it('should handle network error', () => { + cy.intercept('GET', '**/api/schedule/export**', req => { + req.destroy() + }).as('networkError') + + cy.getByTestId('download-csv-option').click() + cy.getByTestId('network-error-message').should('be.visible') + }) + + it('should handle timeout', () => { + cy.intercept('GET', '**/api/schedule/export**', { + statusCode: 200, + body: {}, + delay: 30000, + }).as('timeout') + + cy.getByTestId('download-csv-option').click() + cy.getByTestId('timeout-error-message').should('be.visible') + }) + }) + + describe('Accessibility', () => { + it('should have accessible download button', () => { + cy.getByTestId('download-schedule-button').should('have.attr', 'aria-label') + }) + + it('should announce download status', () => { + cy.getByTestId('download-csv-option').click() + cy.getByTestId('download-status-announcement').should('have.attr', 'role', 'status') + }) + }) +}) diff --git a/e2e/cypress/integration/schedule/filtering.cy.ts b/e2e/cypress/integration/schedule/filtering.cy.ts new file mode 100644 index 000000000..94e76c6d1 --- /dev/null +++ b/e2e/cypress/integration/schedule/filtering.cy.ts @@ -0,0 +1,286 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' + +describe('Schedule - Filtering', () => { + beforeEach(() => { + cy.visit('http://localhost:3001/schedule') + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 200, + body: { + schedules: Array.from({ length: 20 }, (_, i) => ({ + id: String(i), + flight: `SU${1000 + i}`, + days: i % 2 === 0 ? ['Mon', 'Wed', 'Fri'] : ['Tue', 'Thu', 'Sat', 'Sun'], + time: `${(10 + (i % 8))}:00`, + })), + }, + }).as('scheduleSearch') + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + + describe('Operating Days Filter', () => { + it('should filter by Monday', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should filter by weekdays', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('weekdays-preset-button').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should filter by weekends', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('weekends-preset-button').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should select daily flights', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('daily-preset-button').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should show selected days', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('tuesday-checkbox').click() + cy.getByTestId('selected-days-display').should('contain', 'Mon') + cy.getByTestId('selected-days-display').should('contain', 'Tue') + }) + }) + + describe('Time Range Filter', () => { + it('should filter by departure time', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('time-range-tab').click() + cy.getByTestId('time-from-input').clear().type('08:00') + cy.getByTestId('time-to-input').clear().type('12:00') + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should use time slider', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('time-range-tab').click() + cy.getByTestId('time-slider-from').invoke('val', 8).trigger('change') + cy.getByTestId('time-slider-to').invoke('val', 12).trigger('change') + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should show morning flights preset', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('time-range-tab').click() + cy.getByTestId('morning-preset-button').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should show evening flights preset', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('time-range-tab').click() + cy.getByTestId('evening-preset-button').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + }) + + describe('Airline Filter', () => { + it('should filter by single airline', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('airline-tab').click() + cy.getByTestId('airline-checkbox').first().click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should filter by multiple airlines', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('airline-tab').click() + cy.getByTestId('airline-checkbox').first().click() + cy.getByTestId('airline-checkbox').eq(1).click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should show airline list with flight count', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('airline-tab').click() + cy.getByTestId('airline-item').first().should('contain', '(') + }) + + it('should search airlines', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('airline-tab').click() + cy.getByTestId('airline-search-input').clear().type('Aero') + cy.getByTestId('airline-item').should('have.length.lessThan', 10) + }) + }) + + describe('Aircraft Type Filter', () => { + it('should filter by aircraft type', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('aircraft-tab').click() + cy.getByTestId('aircraft-checkbox').first().click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should show aircraft list', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('aircraft-tab').click() + cy.getByTestId('aircraft-item').should('have.length.greaterThan', 0) + }) + }) + + describe('Multiple Filters', () => { + it('should apply multiple filters simultaneously', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('time-range-tab').click() + cy.getByTestId('time-from-input').clear().type('10:00') + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should show active filter badges', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('active-filter-badge').should('be.visible') + }) + + it('should count active filters', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('tuesday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('filter-count-badge').should('contain', '2') + }) + + it('should reset all filters', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('reset-all-filters-button').click() + cy.getByTestId('active-filter-badge').should('not.exist') + }) + }) + + describe('Filter Persistence', () => { + it('should preserve filters on sort', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('sort-by-departure').click() + cy.getByTestId('filter-count-badge').should('contain', '1') + }) + + it('should preserve filters on page change', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('next-page-button').click() + cy.getByTestId('filter-count-badge').should('contain', '1') + }) + + it('should save filters to URL', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.url().should('contain', 'monday') + }) + + it('should restore filters on page reload', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.reload() + cy.getByTestId('filter-count-badge').should('contain', '1') + }) + }) + + describe('Filter UI', () => { + it('should display filter panel', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('filter-panel').should('be.visible') + }) + + it('should have tabs for different filters', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('filter-tabs').should('be.visible') + cy.getByTestId('days-tab').should('be.visible') + cy.getByTestId('time-tab').should('be.visible') + }) + + it('should collapse filter panel', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('close-filter-panel-button').click() + cy.getByTestId('filter-panel').should('not.be.visible') + }) + + it('should show selected filters summary', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('filters-summary').should('contain', 'Mon') + }) + }) + + describe('Filter Search', () => { + it('should search in airline filter', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('airline-tab').click() + cy.getByTestId('airline-search').type('Aero') + cy.getByTestId('airline-item').should('have.length.lessThan', 20) + }) + + it('should clear search', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('airline-tab').click() + cy.getByTestId('airline-search').type('Aero') + cy.getByTestId('clear-search-button').click() + cy.getByTestId('airline-search').should('have.value', '') + }) + }) + + describe('Accessibility', () => { + it('should have accessible filter panel', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('filter-panel').should('have.attr', 'role', 'dialog') + }) + + it('should support keyboard navigation', () => { + cy.getByTestId('filter-panel-button').focus() + cy.getByTestId('filter-panel-button').type('{enter}') + cy.getByTestId('filter-panel').should('be.visible') + }) + + it('should have clear filter labels', () => { + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').should('have.attr', 'aria-label') + }) + }) + + describe('Error Handling', () => { + it('should handle filter error', () => { + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 500, + body: { error: 'Filter failed' }, + }).as('filterError') + + cy.getByTestId('filter-panel-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('error-message').should('be.visible') + }) + }) +}) diff --git a/e2e/cypress/integration/schedule/results.cy.ts b/e2e/cypress/integration/schedule/results.cy.ts new file mode 100644 index 000000000..bf724c504 --- /dev/null +++ b/e2e/cypress/integration/schedule/results.cy.ts @@ -0,0 +1,209 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' +import { apiHelpers } from '../../support/helpers/api-helpers' + +describe('Schedule - Results Display', () => { + beforeEach(() => { + cy.visit('http://localhost:3001/schedule') + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 200, + body: { + schedules: Array.from({ length: 10 }, (_, i) => ({ + id: String(i), + flight: `SU${1000 + i}`, + departure: '10:00', + arrival: '12:30', + days: ['Mon', 'Tue', 'Wed'], + airline: 'Aeroflot', + })), + }, + }).as('scheduleSearch') + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + + describe('Results Display', () => { + it('should display schedule results container', () => { + cy.getByTestId('schedule-results').should('be.visible') + }) + + it('should display multiple schedules', () => { + cy.getByTestId('schedule-item').should('have.length.greaterThan', 1) + }) + + it('should show flight number', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('flight-number').should('be.visible') + }) + }) + + it('should show departure and arrival times', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('departure-time').should('be.visible') + cy.getByTestId('arrival-time').should('be.visible') + }) + }) + + it('should display operating days', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('operating-days').should('be.visible') + }) + }) + + it('should show airline info', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('airline-name').should('be.visible') + }) + }) + }) + + describe('Results Sorting', () => { + it('should sort by departure time', () => { + cy.getByTestId('sort-by-departure').click() + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('departure-time').should('be.visible') + }) + }) + + it('should sort by arrival time', () => { + cy.getByTestId('sort-by-arrival').click() + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('arrival-time').should('be.visible') + }) + }) + + it('should sort by duration', () => { + cy.getByTestId('sort-by-duration').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should sort by airline', () => { + cy.getByTestId('sort-by-airline').click() + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('airline-name').should('be.visible') + }) + }) + }) + + describe('Results Filtering', () => { + it('should filter by operating day', () => { + cy.getByTestId('filter-by-day').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should filter by airline', () => { + cy.getByTestId('filter-by-airline').click() + cy.getByTestId('airline-option').first().click() + cy.getByTestId('apply-filter').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should filter by time range', () => { + cy.getByTestId('filter-by-time').click() + cy.getByTestId('time-from').clear().type('08:00') + cy.getByTestId('time-to').clear().type('14:00') + cy.getByTestId('apply-filter').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should show active filter count', () => { + cy.getByTestId('filter-by-day').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter').click() + cy.getByTestId('active-filter-badge').should('contain', '1') + }) + + it('should reset filters', () => { + cy.getByTestId('filter-by-day').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter').click() + cy.getByTestId('reset-filters').click() + cy.getByTestId('active-filter-badge').should('not.exist') + }) + }) + + describe('Results Pagination', () => { + it('should display pagination controls', () => { + cy.getByTestId('pagination-controls').should('be.visible') + }) + + it('should navigate to next page', () => { + cy.getByTestId('next-page-button').click() + cy.getByTestId('current-page').should('contain', '2') + }) + + it('should navigate to previous page', () => { + cy.getByTestId('next-page-button').click() + cy.getByTestId('prev-page-button').click() + cy.getByTestId('current-page').should('contain', '1') + }) + + it('should jump to specific page', () => { + cy.getByTestId('page-input').clear().type('3') + cy.getByTestId('go-button').click() + cy.getByTestId('current-page').should('contain', '3') + }) + }) + + describe('Results Export', () => { + it('should export results to CSV', () => { + cy.getByTestId('export-csv-button').click() + cy.readFile('cypress/downloads/schedule.csv').should('exist') + }) + + it('should export results to PDF', () => { + cy.getByTestId('export-pdf-button').click() + cy.readFile('cypress/downloads/schedule.pdf').should('exist') + }) + + it('should print schedule results', () => { + cy.getByTestId('print-button').click() + cy.window().its('print').should('be.called') + }) + }) + + describe('Results Actions', () => { + it('should expand schedule details', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('schedule-details-expanded').should('be.visible') + }) + + it('should show book flight button', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('book-flight-button').should('be.visible') + }) + }) + + it('should show add to favorites button', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('favorite-button').should('be.visible') + }) + }) + + it('should add schedule to favorites', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('favorite-button').click() + cy.getByTestId('favorite-button').should('have.class', 'active') + }) + }) + }) + + describe('Results Accessibility', () => { + it('should have accessible results list', () => { + cy.getByTestId('schedule-list').should('have.attr', 'role', 'list') + }) + + it('should support keyboard navigation', () => { + cy.getByTestId('schedule-item').first().focus() + cy.getByTestId('schedule-item').first().type('{enter}') + cy.getByTestId('schedule-details-expanded').should('be.visible') + }) + + it('should announce result count', () => { + cy.getByTestId('results-count-announcement').should('have.attr', 'role', 'status') + }) + }) +}) diff --git a/e2e/cypress/integration/schedule/route-display.cy.ts b/e2e/cypress/integration/schedule/route-display.cy.ts new file mode 100644 index 000000000..2ca0f1a15 --- /dev/null +++ b/e2e/cypress/integration/schedule/route-display.cy.ts @@ -0,0 +1,295 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' + +describe('Schedule - Route Display', () => { + beforeEach(() => { + cy.visit('http://localhost:3001/schedule') + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 200, + body: { + schedules: [ + { + id: '1', + flight: 'SU123', + departure: { code: 'SVO', city: 'Moscow', time: '10:00' }, + arrival: { code: 'LED', city: 'Saint Petersburg', time: '12:30' }, + duration: '2h 30m', + stops: 0, + }, + ], + }, + }).as('scheduleSearch') + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + + describe('Route Information Display', () => { + it('should display departure airport code', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('departure-code').should('contain', 'SVO') + }) + }) + + it('should display arrival airport code', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('arrival-code').should('contain', 'LED') + }) + }) + + it('should display departure city', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('departure-city').should('contain', 'Moscow') + }) + }) + + it('should display arrival city', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('arrival-city').should('contain', 'Saint Petersburg') + }) + }) + + it('should display flight duration', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('flight-duration').should('contain', '2h 30m') + }) + }) + + it('should display stops count', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('stops-count').should('contain', '0') + }) + }) + }) + + describe('Route Map Visualization', () => { + it('should display route map', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-map-container').should('be.visible') + }) + + it('should show departure marker on map', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-map').within(() => { + cy.getByTestId('departure-marker').should('be.visible') + }) + }) + + it('should show arrival marker on map', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-map').within(() => { + cy.getByTestId('arrival-marker').should('be.visible') + }) + }) + + it('should draw flight path', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('flight-path').should('be.visible') + }) + + it('should show route distance', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-distance').should('be.visible') + }) + + it('should be interactive', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-map').trigger('mouseenter') + cy.getByTestId('map-controls').should('be.visible') + }) + }) + + describe('Route Details', () => { + it('should show detailed departure info', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('departure-details').within(() => { + cy.getByTestId('airport-name').should('be.visible') + cy.getByTestId('airport-code').should('be.visible') + cy.getByTestId('city-name').should('be.visible') + }) + }) + + it('should show detailed arrival info', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('arrival-details').within(() => { + cy.getByTestId('airport-name').should('be.visible') + cy.getByTestId('airport-code').should('be.visible') + cy.getByTestId('city-name').should('be.visible') + }) + }) + + it('should display route summary', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-summary').should('be.visible') + }) + + it('should show flight number on route', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-flight-number').should('contain', 'SU123') + }) + }) + + describe('Multi-leg Routes', () => { + beforeEach(() => { + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 200, + body: { + schedules: [ + { + id: '1', + flight: 'SU123 -> SU456', + stops: 1, + legs: [ + { from: 'SVO', to: 'DME', duration: '1h' }, + { from: 'DME', to: 'LED', duration: '1.5h' }, + ], + }, + ], + }, + }).as('connectingFlights') + cy.reload() + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@connectingFlights') + }) + + it('should display connecting flights', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('legs-list').should('be.visible') + }) + + it('should show stop information', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('stop-item').should('be.visible') + }) + + it('should display stop duration', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('stop-item').first().within(() => { + cy.getByTestId('stop-duration').should('be.visible') + }) + }) + + it('should show multi-leg route map', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-map').should('be.visible') + cy.getByTestId('route-waypoint').should('have.length.greaterThan', 2) + }) + }) + + describe('Route Comparison', () => { + it('should compare two routes', () => { + cy.getByTestId('schedule-item').first().find('input[type="checkbox"]').click() + cy.getByTestId('schedule-item').eq(1).find('input[type="checkbox"]').click() + cy.getByTestId('compare-routes-button').click() + cy.getByTestId('comparison-modal').should('be.visible') + }) + + it('should show side-by-side comparison', () => { + cy.getByTestId('schedule-item').first().find('input[type="checkbox"]').click() + cy.getByTestId('schedule-item').eq(1).find('input[type="checkbox"]').click() + cy.getByTestId('compare-routes-button').click() + cy.getByTestId('route-comparison-table').should('be.visible') + }) + + it('should highlight differences', () => { + cy.getByTestId('schedule-item').first().find('input[type="checkbox"]').click() + cy.getByTestId('schedule-item').eq(1).find('input[type="checkbox"]').click() + cy.getByTestId('compare-routes-button').click() + cy.getByTestId('difference-highlight').should('be.visible') + }) + }) + + describe('Route Preferences', () => { + it('should allow saving favorite routes', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('favorite-route-button').click() + cy.getByTestId('favorite-route-button').should('have.class', 'active') + }) + }) + + it('should show saved routes list', () => { + cy.getByTestId('saved-routes-button').click() + cy.getByTestId('saved-routes-list').should('be.visible') + }) + + it('should quick-search from saved routes', () => { + cy.getByTestId('saved-routes-button').click() + cy.getByTestId('saved-route-item').first().click() + cy.getByTestId('schedule-results').should('be.visible') + }) + + it('should remove saved route', () => { + cy.getByTestId('saved-routes-button').click() + cy.getByTestId('saved-route-item').first().within(() => { + cy.getByTestId('remove-saved-route-button').click() + }) + cy.getByTestId('route-removed-message').should('be.visible') + }) + }) + + describe('Route Statistics', () => { + it('should display average flight time', () => { + cy.getByTestId('route-statistics-button').click() + cy.getByTestId('average-duration').should('be.visible') + }) + + it('should show frequency of flights', () => { + cy.getByTestId('route-statistics-button').click() + cy.getByTestId('flight-frequency').should('be.visible') + }) + + it('should display on-time performance', () => { + cy.getByTestId('route-statistics-button').click() + cy.getByTestId('ontime-performance').should('be.visible') + }) + + it('should show busiest times', () => { + cy.getByTestId('route-statistics-button').click() + cy.getByTestId('busiest-times').should('be.visible') + }) + }) + + describe('Route Accessibility', () => { + it('should have descriptive route labels', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('route-description').should('have.attr', 'aria-label') + }) + }) + + it('should announce route changes', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-update-announcement').should('have.attr', 'role', 'status') + }) + + it('should support keyboard navigation in map', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('route-map').focus() + cy.getByTestId('route-map').type('{arrowup}') + cy.getByTestId('route-map').should('have.focus') + }) + }) + + describe('Route Export', () => { + it('should export route details', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('export-route-button').click() + cy.getByTestId('export-options').should('be.visible') + }) + + it('should export as PDF', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('export-route-button').click() + cy.getByTestId('export-pdf-option').click() + cy.readFile('cypress/downloads/route.pdf').should('exist') + }) + + it('should print route', () => { + cy.getByTestId('schedule-item').first().click() + cy.getByTestId('export-route-button').click() + cy.getByTestId('print-route-option').click() + cy.window().its('print').should('be.called') + }) + }) +}) diff --git a/e2e/cypress/integration/schedule/search.cy.ts b/e2e/cypress/integration/schedule/search.cy.ts new file mode 100644 index 000000000..795603802 --- /dev/null +++ b/e2e/cypress/integration/schedule/search.cy.ts @@ -0,0 +1,213 @@ +import { uiHelpers } from '../../support/helpers/ui-helpers' +import { apiHelpers } from '../../support/helpers/api-helpers' +import { dataHelpers } from '../../support/helpers/data-helpers' + +describe('Schedule - Search', () => { + beforeEach(() => { + cy.visit('http://localhost:3001/schedule') + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 200, + body: { + schedules: [ + { id: '1', route: 'SVO-LED', days: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, + ], + }, + }).as('scheduleSearch') + }) + + describe('Schedule Search Interface', () => { + it('should display schedule search form', () => { + cy.getByTestId('schedule-search-form').should('be.visible') + }) + + it('should have departure city input', () => { + cy.getByTestId('schedule-departure-input').should('be.visible') + }) + + it('should have arrival city input', () => { + cy.getByTestId('schedule-arrival-input').should('be.visible') + }) + + it('should have search button', () => { + cy.getByTestId('schedule-search-button').should('be.visible') + }) + + it('should search schedule by route', () => { + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + cy.getByTestId('schedule-results').should('be.visible') + }) + + it('should validate required fields', () => { + cy.getByTestId('schedule-search-button').click() + cy.getByTestId('validation-error').should('be.visible') + }) + + it('should clear search filters', () => { + uiHelpers.fillInput('schedule-departure-input', 'SVO') + cy.getByTestId('schedule-clear-button').click() + cy.getByTestId('schedule-departure-input').should('have.value', '') + }) + }) + + describe('Schedule Search Results', () => { + beforeEach(() => { + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + + it('should display schedule list', () => { + cy.getByTestId('schedule-list').should('be.visible') + }) + + it('should show schedule items', () => { + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should display flight number', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('schedule-flight-number').should('be.visible') + }) + }) + + it('should show departure time', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('schedule-departure-time').should('be.visible') + }) + }) + + it('should show arrival time', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('schedule-arrival-time').should('be.visible') + }) + }) + + it('should display operating days', () => { + cy.getByTestId('schedule-item').first().within(() => { + cy.getByTestId('operating-days').should('be.visible') + }) + }) + }) + + describe('Schedule Filters', () => { + beforeEach(() => { + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + + it('should filter by airline', () => { + cy.getByTestId('filter-by-airline-button').click() + cy.getByTestId('airline-checkbox').first().click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should filter by departure time', () => { + cy.getByTestId('filter-by-time-button').click() + cy.getByTestId('time-range-start').clear().type('10:00') + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + + it('should filter by operating days', () => { + cy.getByTestId('filter-by-days-button').click() + cy.getByTestId('monday-checkbox').click() + cy.getByTestId('apply-filter-button').click() + cy.getByTestId('schedule-item').should('have.length.greaterThan', 0) + }) + }) + + describe('Advanced Search Options', () => { + it('should search by airline', () => { + cy.getByTestId('search-by-airline-option').click() + cy.getByTestId('airline-select').should('be.visible') + cy.getByTestId('airline-select').select('Aeroflot') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + + it('should search by aircraft type', () => { + cy.getByTestId('search-by-aircraft-option').click() + cy.getByTestId('aircraft-select').should('be.visible') + cy.getByTestId('aircraft-select').select('A320') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + + it('should search by service class', () => { + cy.getByTestId('search-by-class-option').click() + cy.getByTestId('service-class-select').should('be.visible') + cy.getByTestId('service-class-select').select('Business') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + }) + }) + + describe('Search History', () => { + it('should display search history', () => { + cy.getByTestId('search-history-section').should('be.visible') + }) + + it('should allow quick search from history', () => { + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@scheduleSearch') + + cy.getByTestId('search-history-item').first().click() + cy.getByTestId('schedule-results').should('be.visible') + }) + + it('should clear search history', () => { + cy.getByTestId('clear-history-button').click() + cy.getByTestId('search-history-empty').should('be.visible') + }) + }) + + describe('Error Handling', () => { + it('should handle search error', () => { + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 500, + body: { error: 'Search failed' }, + }).as('searchError') + + uiHelpers.fillInput('schedule-departure-input', 'SVO') + uiHelpers.fillInput('schedule-arrival-input', 'LED') + cy.getByTestId('schedule-search-button').click() + cy.wait('@searchError') + cy.getByTestId('error-message').should('be.visible') + }) + + it('should handle no results', () => { + cy.intercept('GET', '**/api/schedule/search**', { + statusCode: 200, + body: { schedules: [] }, + }).as('emptySearch') + + uiHelpers.fillInput('schedule-departure-input', 'XXX') + uiHelpers.fillInput('schedule-arrival-input', 'YYY') + cy.getByTestId('schedule-search-button').click() + cy.wait('@emptySearch') + cy.getByTestId('no-results-message').should('be.visible') + }) + }) + + describe('Accessibility', () => { + it('should have accessible search form', () => { + cy.getByTestId('schedule-search-form').should('have.attr', 'role', 'search') + }) + + it('should support keyboard navigation', () => { + cy.getByTestId('schedule-departure-input').focus() + cy.getByTestId('schedule-departure-input').type('SVO') + cy.getByTestId('schedule-departure-input').type('{tab}') + cy.getByTestId('schedule-arrival-input').should('have.focus') + }) + }) +})