From 91b4cd7db78d82a65b9f31be3fb2d859c27578e9 Mon Sep 17 00:00:00 2001 From: gnezim Date: Sat, 4 Apr 2026 12:17:25 +0300 Subject: [PATCH] feat: add error states and recovery e2e tests (30 tests for network, validation, empty states, retry) --- .../integration/features/error-states.cy.ts | 475 ++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100644 ClientApp/cypress/integration/features/error-states.cy.ts diff --git a/ClientApp/cypress/integration/features/error-states.cy.ts b/ClientApp/cypress/integration/features/error-states.cy.ts new file mode 100644 index 00000000..f21da6cd --- /dev/null +++ b/ClientApp/cypress/integration/features/error-states.cy.ts @@ -0,0 +1,475 @@ +import { CITIES, MOCK_FLIGHTS_ARRIVAL } from '../../support/fixtures'; + +describe('Error States & Recovery Tests', () => { + beforeEach(() => { + cy.forbidGeolocation(); + cy.visit('/'); + }); + + describe('Network Errors (10 tests)', () => { + it('Should handle 404 Not Found error - error message displays', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 404, + body: { error: 'Resource not found' }, + }).as('notFound'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@notFound'); + cy.getByTestId('error-message').should('be.visible'); + cy.getByTestId('error-message').should('contain.text', '404'); + }); + + it('Should handle 404 Not Found error - retry button appears', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 404, + body: { error: 'Resource not found' }, + }).as('notFound'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@notFound'); + cy.getByTestId('retry-button').should('be.visible'); + cy.getByTestId('retry-button').should('be.enabled'); + }); + + it('Should handle 500 Server Error - error message displays', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 500, + body: { error: 'Internal server error' }, + }).as('serverError'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@serverError'); + cy.getByTestId('error-message').should('be.visible'); + cy.getByTestId('error-message').should('contain.text', '500'); + }); + + it('Should handle 500 Server Error - retry button appears', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 500, + body: { error: 'Internal server error' }, + }).as('serverError'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@serverError'); + cy.getByTestId('retry-button').should('be.visible'); + }); + + it('Should handle 503 Service Unavailable - error message displays', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 503, + body: { error: 'Service unavailable' }, + }).as('unavailable'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@unavailable'); + cy.getByTestId('error-message').should('be.visible'); + cy.getByTestId('error-message').should('contain.text', '503'); + }); + + it('Should handle 503 Service Unavailable - retry button appears', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 503, + body: { error: 'Service unavailable' }, + }).as('unavailable'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@unavailable'); + cy.getByTestId('retry-button').should('be.visible'); + }); + + it('Should handle request timeout - timeout message shows', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', (req) => { + req.reply((res) => { + res.delay(15000); + }); + }).as('timeout'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.getByTestId('timeout-message', { timeout: 15000 }).should('be.visible'); + cy.getByTestId('timeout-message').should('contain.text', 'timeout'); + }); + + it('Should handle connection refused - error message displays gracefully', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + forceNetworkError: true, + }).as('connectionRefused'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@connectionRefused'); + cy.getByTestId('error-message').should('be.visible'); + cy.getByTestId('error-message').should('contain.text', 'network'); + }); + + it('Should handle multiple consecutive errors - error counter increments', () => { + let callCount = 0; + cy.intercept('GET', '**/api/flights/v1.1/**/board**', (req) => { + callCount++; + req.reply({ + statusCode: 500, + body: { error: 'Server error' }, + }); + }).as('consecutiveErrors'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@consecutiveErrors'); + cy.getByTestId('error-message').should('be.visible'); + + cy.getByTestId('retry-button').click(); + cy.wait('@consecutiveErrors'); + cy.getByTestId('error-message').should('be.visible'); + cy.getByTestId('error-count').should('contain.text', '2'); + }); + + it('Should handle multiple consecutive errors - escalation message appears', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 500, + body: { error: 'Server error' }, + }).as('errors'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.wait('@errors'); + cy.getByTestId('retry-button').click(); + cy.wait('@errors'); + cy.getByTestId('retry-button').click(); + cy.wait('@errors'); + + cy.getByTestId('error-escalation-message').should('be.visible'); + cy.getByTestId('error-escalation-message').should('contain.text', 'contact'); + }); + }); + + describe('Validation Errors (8 tests)', () => { + it('Should show error when required city field is missing', () => { + cy.getByTestId('search-button').click(); + + cy.getByTestId('validation-error').should('be.visible'); + cy.getByTestId('validation-error').should('contain.text', 'required'); + }); + + it('Should highlight required city field when missing', () => { + cy.getByTestId('search-button').click(); + + cy.getByTestId('city-autocomplete-input').parent().should('have.class', 'error'); + }); + + it('Should show error when required date field is missing', () => { + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('search-button').click(); + + cy.getByTestId('validation-error').should('be.visible'); + cy.getByTestId('validation-error').should('contain.text', 'date'); + }); + + it('Should show error for invalid date format', () => { + cy.getByTestId('calendar-input').type('invalid-date'); + cy.getByTestId('search-button').click(); + + cy.getByTestId('validation-error').should('be.visible'); + cy.getByTestId('validation-error').should('contain.text', 'format'); + }); + + it('Should show error when past date is selected', () => { + cy.getByTestId('calendar-input').type('01.01.2020'); + cy.getByTestId('search-button').click(); + + cy.getByTestId('validation-error').should('be.visible'); + cy.getByTestId('validation-error').should('contain.text', 'past'); + }); + + it('Should handle special characters in text fields gracefully', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 200, + body: { flights: [] }, + }).as('searchWithSpecial'); + + cy.getByTestId('city-autocomplete-input').type('Москва '); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@searchWithSpecial'); + cy.getByTestId('board-search-result').should('be.visible'); + }); + + it('Should prevent or show error when max length exceeded in city input', () => { + const longString = 'A'.repeat(200); + cy.getByTestId('city-autocomplete-input').type(longString); + + cy.getByTestId('city-autocomplete-input').invoke('val').then((value) => { + expect((value as string).length).to.be.lessThan(200); + }); + }); + + it('Should show validation error for invalid email format (if applicable)', () => { + cy.getByTestId('email-input', { timeout: 3000 }).then(($el) => { + if ($el.length > 0) { + cy.wrap($el).type('invalid-email'); + cy.getByTestId('search-button').click(); + cy.getByTestId('validation-error').should('contain.text', 'email'); + } + }); + }); + }); + + describe('Empty State Tests (5 tests)', () => { + it('Should display empty state message when no flights found', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 200, + body: { flights: [] }, + }).as('noFlights'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@noFlights'); + cy.getByTestId('empty-results').should('be.visible'); + cy.getByTestId('empty-state-message').should('contain.text', 'no flights'); + }); + + it('Should display empty autocomplete state when no matching cities', () => { + cy.getByTestId('city-autocomplete-input').type('XYZCityNotExist'); + + cy.getByTestId('empty-autocomplete-message').should('be.visible'); + cy.getByTestId('empty-autocomplete-message').should('contain.text', 'not found'); + }); + + it('Should display empty search results with proper messaging', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 200, + body: { flights: [] }, + }).as('emptySearch'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@emptySearch'); + cy.getByTestId('empty-results').should('be.visible'); + cy.getByTestId('empty-results').should('have.text', 'Flights not found for the selected criteria'); + }); + + it('Should display correct empty state styling', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 200, + body: { flights: [] }, + }).as('emptySearchStyle'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@emptySearchStyle'); + cy.getByTestId('empty-state-container').should('be.visible'); + cy.getByTestId('empty-state-container').should('have.css', 'display', 'flex'); + }); + + it('Should display proper messaging for each empty state type', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 200, + body: { flights: [] }, + }).as('emptyMessaging'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@emptyMessaging'); + cy.getByTestId('empty-state-message').should('contain.text', 'Flights'); + }); + }); + + describe('Recovery & Retry Tests (7 tests)', () => { + it('Should clear error after successful retry', () => { + let callCount = 0; + cy.intercept('GET', '**/api/flights/v1.1/**/board**', (req) => { + callCount++; + if (callCount === 1) { + req.reply({ + statusCode: 500, + body: { error: 'Server error' }, + }); + } else { + req.reply({ + statusCode: 200, + body: { flights: MOCK_FLIGHTS_ARRIVAL }, + }); + } + }).as('flakyApi'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@flakyApi'); + cy.getByTestId('error-message').should('be.visible'); + + cy.getByTestId('retry-button').click(); + cy.wait('@flakyApi'); + + cy.getByTestId('error-message').should('not.exist'); + cy.getByTestId('board-search-result').should('be.visible'); + }); + + it('Should work with retry button after API error', () => { + let callCount = 0; + cy.intercept('GET', '**/api/flights/v1.1/**/board**', (req) => { + callCount++; + if (callCount === 1) { + req.reply({ + statusCode: 500, + body: { error: 'Server error' }, + }); + } else { + req.reply({ + statusCode: 200, + body: { flights: MOCK_FLIGHTS_ARRIVAL }, + }); + } + }).as('retryableApi'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@retryableApi'); + cy.getByTestId('retry-button').click(); + + cy.wait('@retryableApi'); + cy.getByTestId('flight-result').should('have.length.at.least', 1); + }); + + it('Should detect SignalR connection loss', () => { + cy.on('window:before:load', (window) => { + const signalr = window.HubConnection || {}; + signalr.state = 'Disconnected'; + }); + + cy.visit('/'); + cy.getByTestId('connection-lost-banner', { timeout: 3000 }).then(($el) => { + if ($el.length > 0) { + cy.wrap($el).should('be.visible'); + } + }); + }); + + it('Should provide SignalR reconnect button when connection lost', () => { + cy.on('window:before:load', (window) => { + const signalr = window.HubConnection || {}; + signalr.state = 'Disconnected'; + }); + + cy.visit('/'); + cy.getByTestId('reconnect-button', { timeout: 3000 }).then(($el) => { + if ($el.length > 0) { + cy.wrap($el).should('be.visible'); + cy.wrap($el).should('be.enabled'); + } + }); + }); + + it('Should work with manual refresh button', () => { + cy.intercept('GET', '**/api/flights/v1.1/**/board**', { + statusCode: 200, + body: { flights: MOCK_FLIGHTS_ARRIVAL }, + }).as('refresh'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@refresh'); + cy.getByTestId('refresh-button').click(); + + cy.wait('@refresh'); + cy.getByTestId('board-search-result').should('be.visible'); + }); + + it('Should auto-retry after delay when enabled', () => { + let callCount = 0; + cy.intercept('GET', '**/api/flights/v1.1/**/board**', (req) => { + callCount++; + if (callCount === 1) { + req.reply({ + statusCode: 500, + body: { error: 'Server error' }, + }); + } else { + req.reply({ + statusCode: 200, + body: { flights: MOCK_FLIGHTS_ARRIVAL }, + }); + } + }).as('autoRetry'); + + cy.getByTestId('city-autocomplete-input').type('Москва'); + cy.getByTestId('calendar-input').type('04.04.2026'); + cy.getByTestId('search-button').click(); + + cy.wait('@autoRetry'); + cy.getByTestId('error-message').should('be.visible'); + + cy.getByTestId('auto-retry-enabled', { timeout: 3000 }).then(($el) => { + if ($el.length > 0) { + cy.wait('@autoRetry', { timeout: 10000 }); + cy.getByTestId('board-search-result').should('be.visible'); + } + }); + }); + + it('Should preserve state during retry', () => { + let callCount = 0; + cy.intercept('GET', '**/api/flights/v1.1/**/board**', (req) => { + callCount++; + if (callCount === 1) { + req.reply({ + statusCode: 500, + body: { error: 'Server error' }, + }); + } else { + req.reply({ + statusCode: 200, + body: { flights: MOCK_FLIGHTS_ARRIVAL }, + }); + } + }).as('statePreserve'); + + const testCity = 'Москва'; + const testDate = '04.04.2026'; + + cy.getByTestId('city-autocomplete-input').type(testCity); + cy.getByTestId('calendar-input').type(testDate); + cy.getByTestId('search-button').click(); + + cy.wait('@statePreserve'); + + cy.getByTestId('city-autocomplete-input').invoke('val').should('contain', testCity); + cy.getByTestId('calendar-input').invoke('val').should('contain', testDate); + + cy.getByTestId('retry-button').click(); + cy.wait('@statePreserve'); + + cy.getByTestId('city-autocomplete-input').invoke('val').should('contain', testCity); + cy.getByTestId('calendar-input').invoke('val').should('contain', testDate); + }); + }); +});