feat: add error states and recovery e2e tests (30 tests for network, validation, empty states, retry)

This commit is contained in:
2026-04-04 12:17:25 +03:00
parent 0e973d1317
commit 91b4cd7db7
@@ -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('Москва <script>alert("xss")</script>');
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);
});
});
});