feat: add error states and recovery e2e tests (30 tests for network, validation, empty states, retry)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user