From 14c3e9ffd840e5e13d18ee559afbb05eaac715d7 Mon Sep 17 00:00:00 2001 From: bartoval Date: Tue, 21 Jan 2025 15:48:18 +0100 Subject: [PATCH] ci: :ferris_wheel: Add e2e env var e tests --- cypress.config.ts | 15 +++- cypress/e2e/api.cy.ts | 82 +++++++++++++++++++ cypress/e2e/navigation-consistency.cy.ts | 56 +++++++++++++ .../skupper-console/shared/errors-pages.cy.ts | 10 --- .../shared => local/sites}/navigation.cy.ts | 3 +- .../sites/site.cy.ts | 6 +- .../sites/sites.cy.ts | 2 +- package.json | 6 +- src/config/testIds.ts | 1 + src/pages/shared/Errors/Http/index.tsx | 7 +- src/pages/shared/Errors/NotFound/index.tsx | 5 +- 11 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 cypress/e2e/api.cy.ts create mode 100644 cypress/e2e/navigation-consistency.cy.ts delete mode 100644 cypress/e2e/skupper-console/shared/errors-pages.cy.ts rename cypress/{e2e/skupper-console/shared => local/sites}/navigation.cy.ts (97%) rename cypress/{e2e/skupper-console => local}/sites/site.cy.ts (88%) rename cypress/{e2e/skupper-console => local}/sites/sites.cy.ts (90%) diff --git a/cypress.config.ts b/cypress.config.ts index 6a512d050..5b3d1b2bc 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -4,11 +4,20 @@ export default defineConfig({ e2e: { baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000', chromeWebSecurity: false, - defaultCommandTimeout: 8000, + defaultCommandTimeout: 15000, screenshotOnRunFailure: false, - specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', video: false, viewportHeight: 1080, - viewportWidth: 1920 + viewportWidth: 1920, + specPattern: (() => { + switch (process.env.TEST_TYPE) { + case 'e2e': + return 'cypress/e2e/**/*.cy.{ts,tsx}'; + case 'local': + return 'cypress/local/**/*.cy.{ts,tsx}'; + default: + return ['cypress/local/**/*.cy.{ts,tsx}', 'cypress/e2e/**/*.cy.{ts,tsx}']; + } + })() } }); diff --git a/cypress/e2e/api.cy.ts b/cypress/e2e/api.cy.ts new file mode 100644 index 000000000..ad73dcee2 --- /dev/null +++ b/cypress/e2e/api.cy.ts @@ -0,0 +1,82 @@ +import { apiEndpoints } from '../../src/config/api'; + +describe('API Endpoint Tests', () => { + for (const [name, url] of Object.entries(apiEndpoints)) { + it(`Should GET ${name} (${url}) and verify the response`, () => { + cy.request('GET', url).as(`get${name}`); + + cy.get(`@get${name}`).then((response: any) => { + expect(response.status).to.eq(200); + expect(response.body).to.be.an('object'); + expect(response.body).to.have.property('count'); + expect(response.body).to.have.property('timeRangeCount'); + expect(response.body).to.have.property('results'); + + expect(response).to.have.property('headers'); + expect(response.headers).to.have.property('content-type'); + expect(response.headers['content-type']).to.contain('application/json'); + }); + }); + } + + it(`Should handle the Network error error for ${apiEndpoints.SITES}`, () => { + cy.intercept('GET', apiEndpoints.SITES, { forceNetworkError: true }).as(apiEndpoints.SITES); + + cy.visit('/'); + + cy.wait(`@${apiEndpoints.SITES}`).then(() => { + cy.get(`body`).contains('ERR_NETWORK').should('be.visible'); + }); + }); + + it(`Should handle 500 error for ${apiEndpoints.SITES}`, () => { + cy.intercept('GET', apiEndpoints.SITES, { statusCode: 500 }).as(apiEndpoints.SITES); + + cy.visit('/'); + + cy.wait(`@${apiEndpoints.SITES}`).its('response.statusCode').should('eq', 500); + cy.get(`body`).contains('500: Internal Server Error').should('be.visible'); + }); + + it(`Should handle 404 error for ${apiEndpoints.SITES}`, () => { + cy.intercept('GET', apiEndpoints.SITES, { statusCode: 404 }).as(apiEndpoints.SITES); + + cy.visit('/'); + + cy.wait(`@${apiEndpoints.SITES}`).its('response.statusCode').should('eq', 404); + cy.get(`body`).contains('404: Not Found').should('be.visible'); + }); +}); + +describe('API Resilience Tests', () => { + it('Should recover after temporary service unavailability, clicking on Try again button', () => { + cy.intercept('GET', apiEndpoints.SITES, { forceNetworkError: true }).as(apiEndpoints.SITES); + cy.visit('/'); + cy.get(`body`).contains('ERR_NETWORK').should('be.visible'); + + cy.intercept('GET', apiEndpoints.SITES, { forceNetworkError: false }).as('secondTry'); + + cy.get(`body`).contains('Try again').should('be.visible').click(); + + cy.wait('@secondTry').its('response.statusCode').should('eq', 200); + }); +}); + +describe('API Concurrency Tests', () => { + it('Should handle multiple concurrent requests', () => { + const endpoints = Object.values(apiEndpoints); + + const requests = endpoints.map( + (url) => + new Cypress.Promise((resolve) => { + cy.request('GET', url).then(resolve); + }) + ); + + cy.wrap(Promise.all(requests)).then((responses: any) => { + responses.forEach((response) => { + expect(response.status).to.eq(200); + }); + }); + }); +}); diff --git a/cypress/e2e/navigation-consistency.cy.ts b/cypress/e2e/navigation-consistency.cy.ts new file mode 100644 index 000000000..39026f235 --- /dev/null +++ b/cypress/e2e/navigation-consistency.cy.ts @@ -0,0 +1,56 @@ +import { apiEndpoints } from '../../src/config/api'; +import { getTestsIds } from '../../src/config/testIds'; +import { ComponentRoutesPaths } from '../../src/pages/Components/Components.enum'; +import { ProcessesRoutesPaths } from '../../src/pages/Processes/Processes.enum'; +import { SitesRoutesPaths } from '../../src/pages/Sites/Sites.enum'; + +const sitesPath = `/#${SitesRoutesPaths.Sites}`; +const componentsPath = `/#${ComponentRoutesPaths.Components}`; +const processesPath = `/#${ProcessesRoutesPaths.Processes}`; + +describe('Data Consistency Tests', () => { + it('Should maintain data consistency across sites and site detail', () => { + let data; + + cy.intercept('GET', apiEndpoints.SITES).as('section'); + + cy.visit(sitesPath); + cy.wait('@section').then((interception) => { + const response = interception.response.body; + data = response.results[0]; + + cy.visit(`${sitesPath}/${data.name}@${data.identity}`); + cy.get(`[data-testid=${getTestsIds.siteView(data.identity)}]`).should('be.visible'); + }); + }); + + it('Should maintain data consistency across components and component detail', () => { + let data; + + cy.intercept('GET', `${apiEndpoints.COMPONENTS}*`).as('section'); + + cy.visit(componentsPath); + cy.wait('@section').then((interception) => { + const response = interception.response.body; + data = response.results[0]; + + cy.visit(`${componentsPath}/${data.name}@${data.identity}`); + cy.get(`[data-testid=${getTestsIds.componentView(data.identity)}]`).should('be.visible'); + }); + }); + + it('Should maintain data consistency across processes and process detail', () => { + let data; + + cy.intercept('GET', `${apiEndpoints.PROCESSES}*`).as('section'); + + cy.visit(processesPath); + cy.wait('@section').then((interception) => { + const response = interception.response.body; + data = response.results[0]; + + cy.visit(`${processesPath}/${data.name}@${data.identity}`); + cy.get(`[data-testid=${getTestsIds.processView(data.identity)}]`).should('be.visible'); + }); + }); +}); diff --git a/cypress/e2e/skupper-console/shared/errors-pages.cy.ts b/cypress/e2e/skupper-console/shared/errors-pages.cy.ts deleted file mode 100644 index 727305796..000000000 --- a/cypress/e2e/skupper-console/shared/errors-pages.cy.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getTestsIds } from '../../../../src/config/testIds'; - -context('Error pages', () => { - beforeEach(() => {}); - - it('should redirect to the Not found page when url not exists', () => { - cy.visit('/#/wrong-page'); - cy.get(`[data-testid=${getTestsIds.notFoundView()}]`).contains('Route not found').should('be.visible'); - }); -}); diff --git a/cypress/e2e/skupper-console/shared/navigation.cy.ts b/cypress/local/sites/navigation.cy.ts similarity index 97% rename from cypress/e2e/skupper-console/shared/navigation.cy.ts rename to cypress/local/sites/navigation.cy.ts index b65be982f..f77e8f5b6 100644 --- a/cypress/e2e/skupper-console/shared/navigation.cy.ts +++ b/cypress/local/sites/navigation.cy.ts @@ -1,4 +1,4 @@ -import { getTestsIds } from '../../../../src/config/testIds'; +import { getTestsIds } from '../../../src/config/testIds'; context('Navigation', () => { beforeEach(() => { @@ -56,6 +56,7 @@ context('Navigation', () => { it('should reload the page and stay on the last page selected', () => { cy.get(`[data-testid=${getTestsIds.navbarComponent()}]`).contains('Sites').click({ force: true }); + cy.wait(500); cy.reload(); cy.location('hash').should('include', 'sites'); diff --git a/cypress/e2e/skupper-console/sites/site.cy.ts b/cypress/local/sites/site.cy.ts similarity index 88% rename from cypress/e2e/skupper-console/sites/site.cy.ts rename to cypress/local/sites/site.cy.ts index 51dfd29c8..3d605047a 100644 --- a/cypress/e2e/skupper-console/sites/site.cy.ts +++ b/cypress/local/sites/site.cy.ts @@ -1,6 +1,6 @@ -import processesData from '../../../../mocks/data/PROCESSES.json'; -import sitesData from '../../../../mocks/data/SITES.json'; -import { getTestsIds } from '../../../../src/config/testIds'; +import processesData from '../../../mocks/data/PROCESSES.json'; +import sitesData from '../../../mocks/data/SITES.json'; +import { getTestsIds } from '../../../src/config/testIds'; const site = sitesData.results[0]; const sitePath = `#/sites/${encodeURIComponent(site.name)}@${encodeURIComponent(site.identity)}`; diff --git a/cypress/e2e/skupper-console/sites/sites.cy.ts b/cypress/local/sites/sites.cy.ts similarity index 90% rename from cypress/e2e/skupper-console/sites/sites.cy.ts rename to cypress/local/sites/sites.cy.ts index f81afab3c..90b0e9eb3 100644 --- a/cypress/e2e/skupper-console/sites/sites.cy.ts +++ b/cypress/local/sites/sites.cy.ts @@ -1,4 +1,4 @@ -import sitesData from '../../../../mocks/data/SITES.json'; +import sitesData from '../../../mocks/data/SITES.json'; context('Sites', () => { beforeEach(() => { diff --git a/package.json b/package.json index 9c784f590..7fe5d954b 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "test": "yarn test:watch --run", "test:watch": "USE_MOCK_SERVER=true vitest", "coverage": "yarn test --coverage", - "cy:open": "cypress open", - "cy:run": "cypress run", - "ci": "start-test 3000 cy:run", "cy:dev": "cypress open", + "cy:run": "cypress run", + "ci": "TEST_TYPE=local start-test 3000 cy:run", + "ci:e2e": "TEST_TYPE=e2e cypress run", "lint": "TIMING=1 eslint --cache", "lint-fix": "yarn lint --fix", "format": "prettier --write '*/**/*.{ts,tsx,json,css}'", diff --git a/src/config/testIds.ts b/src/config/testIds.ts index cb884a25e..8afc003d6 100644 --- a/src/config/testIds.ts +++ b/src/config/testIds.ts @@ -11,6 +11,7 @@ export const getTestsIds = { serviceView: (id: string) => `sk-service-view-${id}`, topologyView: () => 'sk-topology-view', notFoundView: () => `sk-not-found-view`, + networkErrorView: () => `sk-network-error-view`, siteView: (id: string) => `sk-site-view-${id}`, componentView: (id: string) => `sk-component-view-${id}`, navbarComponent: () => 'sk-nav-bar-component', diff --git a/src/pages/shared/Errors/Http/index.tsx b/src/pages/shared/Errors/Http/index.tsx index e9ad2a56c..fb8ff06fc 100644 --- a/src/pages/shared/Errors/Http/index.tsx +++ b/src/pages/shared/Errors/Http/index.tsx @@ -3,6 +3,7 @@ import { FC, MouseEventHandler } from 'react'; import { Button, Divider, List, ListItem, PageSection, Content, ContentVariants, Title } from '@patternfly/react-core'; import { Labels } from '../../../../config/labels'; +import { getTestsIds } from '../../../../config/testIds'; const ErrorHttp: FC<{ code?: string; message?: string; onReset?: MouseEventHandler }> = function ({ code, @@ -10,8 +11,8 @@ const ErrorHttp: FC<{ code?: string; message?: string; onReset?: MouseEventHandl onReset }) { return ( - - + +
{message || Labels.HttpError} {code || ''} @@ -39,7 +40,7 @@ const ErrorHttp: FC<{ code?: string; message?: string; onReset?: MouseEventHandl - +
); }; diff --git a/src/pages/shared/Errors/NotFound/index.tsx b/src/pages/shared/Errors/NotFound/index.tsx index 22ed802bc..a2d90a083 100644 --- a/src/pages/shared/Errors/NotFound/index.tsx +++ b/src/pages/shared/Errors/NotFound/index.tsx @@ -1,11 +1,12 @@ import { Bullseye, PageSection, Content, ContentVariants } from '@patternfly/react-core'; import { Labels } from '../../../../config/labels'; +import { getTestsIds } from '../../../../config/testIds'; const NotFound = function () { return ( - - + + {Labels.NoFoundError}