feat: add cypress e2e test infrastructure and support files

This commit is contained in:
2026-04-04 12:14:20 +03:00
parent a9b2f4ac5c
commit 0e973d1317
8 changed files with 26306 additions and 15978 deletions
+32
View File
@@ -0,0 +1,32 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:4200',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 5000,
requestTimeout: 10000,
responseTimeout: 10000,
pageLoadTimeout: 30000,
chromeWebSecurity: false,
video: true,
screenshotOnRunFailure: true,
specPattern: 'cypress/integration/**/*.ts',
supportFile: 'cypress/support/index.ts',
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
fixturesFolder: 'cypress/fixtures',
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
component: {
specPattern: 'cypress/component/**/*.ts',
supportFile: 'cypress/support/index.ts',
devServer: {
framework: 'angular',
bundler: 'webpack',
},
},
});
-27
View File
@@ -1,27 +0,0 @@
{
"integrationFolder": "cypress/integration",
"supportFile": "cypress/support/index.ts",
"videosFolder": "cypress/videos",
"screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": "cypress/fixtures",
"baseUrl": "http://localhost:4200",
"screenshotOnRunFailure": false,
"video": false,
"viewportHeight": 768,
"viewportWidth": 1366,
"chromeWebSecurity": false,
"env": {
"browserPermissions": {
"notifications": "allow",
"geolocation": "block",
"camera": "block",
"microphone": "block",
"images": "allow",
"javascript": "allow",
"popups": "ask",
"plugins": "ask",
"cookies": "allow"
}
}
}
+74 -43
View File
@@ -1,48 +1,8 @@
/// <reference types="." />
// ***********************************************
// This example namespace declaration will help
// with Intellisense and code completion in your
// IDE or Text Editor.
// ***********************************************
// declare namespace Cypress {
// interface Chainable<Subject = any> {
// customCommand(param: any): typeof customCommand;
// }
// }
//
// function customCommand(param: any): void {
// console.warn(param);
// }
//
// NOTE: You can use it like so:
// Cypress.Commands.add('customCommand', customCommand);
//
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
/**
* Custom Cypress commands for Aeroflot Flights Web testing
*/
Cypress.Commands.add('getByTestId', (id: string, timeout = 8000) => {
return cy.get(`[data-testid="${id}"]`, { timeout });
@@ -71,3 +31,74 @@ Cypress.Commands.add('forbidGeolocation', () => {
cy.stub(window.navigator.geolocation, 'watchPosition').callsFake(callback);
});
});
// Select arrival city by name
Cypress.Commands.add('selectArrivalCity', (cityName: string) => {
cy.getByTestId('city-autocomplete-input-arrival').clear().type(cityName);
cy.getByTestId('city-dropdown-option').contains(cityName).click();
});
// Select departure city by name
Cypress.Commands.add('selectDepartureCity', (cityName: string) => {
cy.getByTestId('city-autocomplete-input-departure').clear().type(cityName);
cy.getByTestId('city-dropdown-option').contains(cityName).click();
});
// Set arrival date using date picker
Cypress.Commands.add('setArrivalDate', (date: string) => {
cy.getByTestId('arrival-date-input').clear().type(date).type('{enter}');
});
// Set departure date using date picker
Cypress.Commands.add('setDepartureDate', (date: string) => {
cy.getByTestId('departure-date-input').clear().type(date).type('{enter}');
});
// Click search button
Cypress.Commands.add('clickSearchButton', () => {
cy.getByTestId('search-button').click();
});
// Get all flight results
Cypress.Commands.add('getFlightResults', () => {
return cy.getByTestId('flight-result');
});
// Get first flight result
Cypress.Commands.add('getFirstFlightResult', () => {
return cy.getByTestId('flight-result').first();
});
// Assert validation error is displayed
Cypress.Commands.add('shouldShowValidationError', (message: string) => {
cy.getByTestId('validation-error').should('contain', message);
});
// Select language by code
Cypress.Commands.add('selectLanguage', (langCode: string) => {
cy.getByTestId('language-selector').click();
cy.getByTestId(`language-option-${langCode}`).click();
});
// Get current language
Cypress.Commands.add('getCurrentLanguage', () => {
return cy.getByTestId('language-selector').invoke('text');
});
// Swipe right (for mobile navigation)
Cypress.Commands.add('swipeRight', { prevSubject: 'element' }, (subject) => {
cy.wrap(subject)
.trigger('touchstart', { which: 1, pageX: 0, pageY: 0 })
.trigger('touchmove', { which: 1, pageX: 100, pageY: 0 })
.trigger('touchend', { which: 1, pageX: 100, pageY: 0 });
return cy.wrap(subject);
});
// Swipe left (for mobile navigation)
Cypress.Commands.add('swipeLeft', { prevSubject: 'element' }, (subject) => {
cy.wrap(subject)
.trigger('touchstart', { which: 1, pageX: 100, pageY: 0 })
.trigger('touchmove', { which: 1, pageX: 0, pageY: 0 })
.trigger('touchend', { which: 1, pageX: 0, pageY: 0 });
return cy.wrap(subject);
});
+202
View File
@@ -0,0 +1,202 @@
/**
* Cypress test fixtures for Aeroflot Flights Web application
*/
export const CITIES = {
arrival: [
{
name: 'Москва',
code: 'MOW',
latitude: 55.7558,
longitude: 37.6173,
},
{
name: 'Санкт-Петербург',
code: 'LED',
latitude: 59.8011,
longitude: 30.2642,
},
{
name: 'Анапа',
code: 'AAQ',
latitude: 44.8972,
longitude: 37.3426,
},
{
name: 'Екатеринбург',
code: 'SVX',
latitude: 56.7365,
longitude: 60.8025,
},
{
name: 'Новосибирск',
code: 'OVB',
latitude: 55.0077,
longitude: 82.9484,
},
],
departure: [
{
name: 'Москва',
code: 'MOW',
latitude: 55.7558,
longitude: 37.6173,
},
{
name: 'Сочи',
code: 'AER',
latitude: 43.4391,
longitude: 39.9566,
},
{
name: 'Казань',
code: 'KZN',
latitude: 55.6084,
longitude: 49.2808,
},
],
};
export const MOCK_FLIGHTS_ARRIVAL = [
{
carrier: 'SU',
number: '001',
aircraft: 'A320',
estimatedTime: '10:15',
actualTime: '10:20',
status: 'Landed',
terminal: 'A',
gate: '12',
checkIn: '09:15-10:15',
},
{
carrier: 'SU',
number: '002',
aircraft: 'A330',
estimatedTime: '14:30',
actualTime: '14:28',
status: 'Landed',
terminal: 'B',
gate: '24',
checkIn: '13:30-14:30',
},
{
carrier: 'SU',
number: '003',
aircraft: 'B737',
estimatedTime: '22:45',
actualTime: null,
status: 'On Schedule',
terminal: 'A',
gate: '15',
checkIn: '21:45-22:45',
},
];
export const MOCK_FLIGHTS_DEPARTURE = [
{
carrier: 'SU',
number: '101',
aircraft: 'A320',
estimatedTime: '08:00',
actualTime: '08:05',
status: 'Departed',
terminal: 'A',
gate: '5',
checkIn: '06:00-07:45',
},
{
carrier: 'SU',
number: '102',
aircraft: 'A330',
estimatedTime: '12:30',
actualTime: null,
status: 'Boarding',
terminal: 'B',
gate: '18',
checkIn: '10:30-12:15',
},
];
export const POPULAR_REQUESTS = [
{
departure: 'Москва',
departureCode: 'MOW',
arrival: 'Анапа',
arrivalCode: 'AAQ',
frequency: 'High',
},
{
departure: 'Москва',
departureCode: 'MOW',
arrival: 'Сочи',
arrivalCode: 'AER',
frequency: 'High',
},
{
departure: 'Санкт-Петербург',
departureCode: 'LED',
arrival: 'Москва',
arrivalCode: 'MOW',
frequency: 'Medium',
},
];
export const LANGUAGES = [
{
code: 'ru',
name: 'Русский',
nativeName: 'Русский',
},
{
code: 'en',
name: 'English',
nativeName: 'English',
},
{
code: 'es',
name: 'Spanish',
nativeName: 'Español',
},
{
code: 'fr',
name: 'French',
nativeName: 'Français',
},
{
code: 'it',
name: 'Italian',
nativeName: 'Italiano',
},
{
code: 'ja',
name: 'Japanese',
nativeName: '日本語',
},
{
code: 'ko',
name: 'Korean',
nativeName: '한국어',
},
{
code: 'zh',
name: 'Chinese',
nativeName: '中文',
},
{
code: 'de',
name: 'German',
nativeName: 'Deutsch',
},
];
export const TEST_USERS = {
guest: {
username: null,
displayName: 'Guest',
},
authenticated: {
username: 'testuser@example.com',
displayName: 'Test User',
},
};
+13 -1
View File
@@ -3,6 +3,18 @@ declare namespace Cypress {
interface Chainable {
getByTestId(id: string, timeout?: number): Chainable;
mockGeolocation({ latitude, longitude }): void;
forbidGeolocation();
forbidGeolocation(): void;
selectArrivalCity(cityName: string): Chainable;
selectDepartureCity(cityName: string): Chainable;
setArrivalDate(date: string): Chainable;
setDepartureDate(date: string): Chainable;
clickSearchButton(): Chainable;
getFlightResults(): Chainable;
getFirstFlightResult(): Chainable;
shouldShowValidationError(message: string): Chainable;
selectLanguage(langCode: string): Chainable;
getCurrentLanguage(): Chainable;
swipeRight(): Chainable;
swipeLeft(): Chainable;
}
}
+20 -10
View File
@@ -1,17 +1,27 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
// This support file is processed and loaded automatically
// before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// When a command from ./commands is ready to use, import with `import './commands'` syntax
import './commands';
// Clear application state before each test
beforeEach(() => {
cy.window().then((win) => {
// Clear localStorage
win.localStorage.clear();
// Clear sessionStorage
win.sessionStorage.clear();
// Clear IndexedDB if available
if (win.indexedDB && typeof win.indexedDB.databases === 'function') {
win.indexedDB.databases().then((dbs: any[]) => {
dbs.forEach(db => {
win.indexedDB.deleteDatabase(db.name);
});
});
}
});
});
+25956 -15895
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -15,11 +15,16 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"test": "ng test --code-coverage",
"test:ci": "ng test --watch=false --reporters=teamcity",
"test:e2e": "cypress run",
"pretty": "prettier --write \"./**/*.{ts,html}\"",
"analyze": "webpack-bundle-analyzer dist/stats.json",
"docs:json": "compodoc -p ./tsconfig.json -e json -d .",
"storybook": "npm run docs:json && start-storybook -p 6006",
"build-storybook": "npm run docs:json && build-storybook"
"build-storybook": "npm run docs:json && build-storybook",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:all": "cypress run --spec 'cypress/integration/**/*.ts'",
"cypress:run:feature": "cypress run --spec 'cypress/integration/**/*.ts' --headed"
},
"dependencies": {
"@angular/animations": "~12.2.13",
@@ -64,10 +69,11 @@
"@storybook/testing-library": "0.0.9",
"@types/jasmine": "^3.10.2",
"@types/leaflet": "^1.7.1",
"@types/node": "^12.11.1",
"@types/node": "^12.20.55",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"babel-loader": "^8.2.4",
"cypress": "^13.17.0",
"eslint": "^8.2.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-storybook": "^0.5.7",
@@ -82,6 +88,7 @@
"prettier": "2.4.1",
"start-server-and-test": "~1.14.0",
"timezone-mock": "^1.3.2",
"ts-loader": "^9.5.7",
"typescript": "~4.3.5",
"webpack-bundle-analyzer": "^4.5.0"
},