chore(ws): add cypress structure and initial tests for frontend (#73)

Signed-off-by: Griffin-Sullivan <gsulliva@redhat.com>
This commit is contained in:
Griffin Sullivan 2024-11-08 14:59:13 -05:00 committed by GitHub
parent 4068e9e4e3
commit 9d02acfdd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 6481 additions and 316 deletions

View File

@ -0,0 +1,2 @@
# Test against prod build hosted by lightweight http server
BASE_URL=http://localhost:9001

View File

@ -23,8 +23,8 @@ npm run start:dev
# Run a production build (outputs to "dist" dir)
npm run build
# Run the unit test suite
npm run test:jest
# Run the mocked test suite
npm run test:cypress-ci
# Start the express server (run a production build first)
npm run start

File diff suppressed because it is too large Load Diff

View File

@ -17,20 +17,39 @@
"build:clean": "rimraf ./dist",
"build:prod": "webpack --config ./config/webpack.prod.js",
"start:dev": "webpack serve --hot --color --config ./config/webpack.dev.js",
"test:jest": "jest"
"test:cypress-ci": "npx concurrently -P -k -s first \"npm run cypress:server:build && npm run cypress:server\" \"npx wait-on tcp:127.0.0.1:9001 && npm run cypress:run:mock -- {@}\" -- ",
"test:jest": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"cypress:open": "cypress open --project src/__tests__/cypress",
"cypress:open:mock": "CY_MOCK=1 CY_WS_PORT=9002 npm run cypress:open -- ",
"cypress:run": "cypress run -b chrome --project src/__tests__/cypress",
"cypress:run:mock": "CY_MOCK=1 npm run cypress:run -- ",
"cypress:server:build": "POLL_INTERVAL=9999999 FAST_POLL_INTERVAL=9999999 npm run build",
"cypress:server": "serve ./dist -p 9001 -s -L"
},
"devDependencies": {
"@cypress/code-coverage": "^3.12.34",
"@testing-library/cypress": "^10.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/chai-subset": "^1.3.5",
"@types/jest": "^29.5.3",
"@types/react-router-dom": "^5.3.3",
"@types/victory": "^33.1.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"chai-subset": "^1.6.0",
"concurrently": "^9.1.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.11.0",
"css-minimizer-webpack-plugin": "^5.0.1",
"cypress": "^13.10.0",
"cypress-axe": "^1.5.0",
"cypress-high-resolution": "^1.0.0",
"cypress-mochawesome-reporter": "^3.8.2",
"cypress-multi-reporters": "^1.6.4",
"dotenv-webpack": "^8.0.1",
"eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3",
@ -40,6 +59,7 @@
"imagemin": "^8.0.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"junit-report-merger": "^7.0.0",
"mini-css-extract-plugin": "^2.9.0",
"postcss": "^8.4.38",
"prettier": "^3.3.0",
@ -47,7 +67,8 @@
"raw-loader": "^4.0.2",
"react-router-dom": "^6.26.1",
"regenerator-runtime": "^0.13.11",
"rimraf": "^5.0.7",
"rimraf": "^6.0.1",
"serve": "^14.2.1",
"style-loader": "^3.3.4",
"svg-url-loader": "^8.0.0",
"terser-webpack-plugin": "^5.3.10",

View File

@ -0,0 +1,2 @@
coverage
results

View File

@ -0,0 +1,122 @@
import path from 'path';
import fs from 'fs';
import { defineConfig } from 'cypress';
import coverage from '@cypress/code-coverage/task';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types available
import cypressHighResolution from 'cypress-high-resolution';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types available
import { beforeRunHook, afterRunHook } from 'cypress-mochawesome-reporter/lib';
import { mergeFiles } from 'junit-report-merger';
import { env, BASE_URL } from '~/__tests__/cypress/cypress/utils/testConfig';
const resultsDir = `${env.CY_RESULTS_DIR || 'results'}/${env.CY_MOCK ? 'mocked' : 'e2e'}`;
export default defineConfig({
experimentalMemoryManagement: true,
// Use relative path as a workaround to https://github.com/cypress-io/cypress/issues/6406
reporter: '../../../node_modules/cypress-multi-reporters',
reporterOptions: {
reporterEnabled: 'cypress-mochawesome-reporter, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `${resultsDir}/junit/junit-[hash].xml`,
},
cypressMochawesomeReporterReporterOptions: {
charts: true,
embeddedScreenshots: false,
ignoreVideos: false,
inlineAssets: true,
reportDir: resultsDir,
videoOnFailOnly: true,
},
},
chromeWebSecurity: false,
viewportWidth: 1920,
viewportHeight: 1080,
numTestsKeptInMemory: 1,
video: true,
screenshotsFolder: `${resultsDir}/screenshots`,
videosFolder: `${resultsDir}/videos`,
env: {
MOCK: !!env.CY_MOCK,
coverage: !!env.CY_COVERAGE,
codeCoverage: {
exclude: [path.resolve(__dirname, '../../third_party/**')],
},
resolution: 'high',
},
defaultCommandTimeout: 10000,
e2e: {
baseUrl: BASE_URL,
specPattern: env.CY_MOCK
? `cypress/tests/mocked/**/*.cy.ts`
: `cypress/tests/e2e/**/*.cy.ts`,
experimentalInteractiveRunEvents: true,
setupNodeEvents(on, config) {
cypressHighResolution(on, config);
coverage(on, config);
on('task', {
readJSON(filePath: string) {
const absPath = path.resolve(__dirname, filePath);
if (fs.existsSync(absPath)) {
try {
return Promise.resolve(JSON.parse(fs.readFileSync(absPath, 'utf8')));
} catch {
// return default value
}
}
return Promise.resolve({});
},
log(message) {
// eslint-disable-next-line no-console
console.log(message);
return null;
},
error(message) {
// eslint-disable-next-line no-console
console.error(message);
return null;
},
table(message) {
// eslint-disable-next-line no-console
console.table(message);
return null;
},
});
// Delete videos for specs without failing or retried tests
on('after:spec', (_, results) => {
if (results.video) {
// Do we have failures for any retry attempts?
const failures = results.tests.some((test) =>
test.attempts.some((attempt) => attempt.state === 'failed'),
);
if (!failures) {
// delete the video if the spec passed and no tests retried
fs.unlinkSync(results.video);
}
}
});
on('before:run', async (details) => {
// cypress-mochawesome-reporter
await beforeRunHook(details);
});
on('after:run', async () => {
// cypress-mochawesome-reporter
await afterRunHook();
// merge junit reports into a single report
const outputFile = path.join(__dirname, resultsDir, 'junit-report.xml');
const inputFiles = [`./${resultsDir}/junit/*.xml`];
await mergeFiles(outputFile, inputFiles);
});
return config;
},
},
});

View File

@ -0,0 +1,11 @@
class Home {
visit() {
cy.visit(`/`);
}
findButton() {
return cy.get('button:contains("Primary Action")');
}
}
export const home = new Home();

View File

@ -0,0 +1,17 @@
class PageNotFound {
visit() {
cy.visit(`/force-not-found-page`, {'failOnStatusCode': false});
this.wait();
}
private wait() {
this.findPage();
cy.testA11y();
}
findPage() {
return cy.get('h1:contains("404 Page not found")');
}
}
export const pageNotfound = new PageNotFound();

View File

@ -0,0 +1,68 @@
import 'cypress-axe';
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace Cypress {
interface Chainable {
testA11y: (context?: Parameters<cy['checkA11y']>[0]) => void;
}
}
}
Cypress.Commands.add('testA11y', { prevSubject: 'optional' }, (subject, context) => {
const test = (c: Parameters<typeof cy.checkA11y>[0]) => {
cy.window({ log: false }).then((win) => {
// inject on demand
if (!(win as { axe: unknown }).axe) {
cy.injectAxe();
}
cy.checkA11y(
c,
{
includedImpacts: ['serious', 'critical'],
},
(violations) => {
cy.task(
'error',
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${
violations.length === 1 ? 'was' : 'were'
} detected`,
);
// pluck specific keys to keep the table readable
const violationData = violations.map(({ id, impact, description, nodes }) => ({
id,
impact,
description,
nodes: nodes.length,
}));
cy.task('table', violationData);
cy.task(
'log',
violations
.map(
({ nodes }, i) =>
`${i}. Affected elements:\n${nodes.map(
({ target, failureSummary, ancestry }) =>
`\t${failureSummary} - ${target
.map((node) => `"${node}"\n${ancestry}`)
.join(', ')}`,
)}`,
)
.join('\n'),
);
},
);
});
};
if (!context && subject) {
cy.wrap(subject).each(($el) => {
Cypress.log({ displayName: 'testA11y', $el });
test($el[0]);
});
} else {
Cypress.log({ displayName: 'testA11y' });
test(context);
}
});

View File

@ -0,0 +1,2 @@
import '@testing-library/cypress/add-commands';
import './axe';

View File

@ -0,0 +1,32 @@
// ***********************************************************
// This example support/e2e.ts 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
// ***********************************************************
import chaiSubset from 'chai-subset';
import '@cypress/code-coverage/support';
import 'cypress-mochawesome-reporter/register';
import './commands';
chai.use(chaiSubset);
Cypress.Keyboard.defaults({
keystrokeDelay: 0,
});
beforeEach(() => {
if (Cypress.env('MOCK')) {
// fallback: return 404 for all api requests
cy.intercept({ pathname: '/api/**' }, { statusCode: 404 });
}
});

View File

@ -0,0 +1,15 @@
import { pageNotfound } from "~/__tests__/cypress/cypress/pages/pageNotFound";
import { home } from "~/__tests__/cypress/cypress/pages/home";
describe('Application', () => {
it('Page not found should render', () => {
pageNotfound.visit()
});
it('Home page should have primary button', () => {
home.visit()
home.findButton();
});
});

View File

@ -0,0 +1,19 @@
import path from 'path';
import { env } from 'process';
import dotenv from 'dotenv';
[
`.env.cypress${env.CY_MOCK ? '.mock' : ''}.local`,
`.env.cypress${env.CY_MOCK ? '.mock' : ''}`,
'.env.local',
'.env',
].forEach((file) =>
dotenv.config({
path: path.resolve(__dirname, '../../../../../', file),
}),
);
export const BASE_URL = env.BASE_URL || '';
// re-export the updated process env
export { env };

View File

@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"include": ["../../**/*.ts"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"types": ["node", "cypress", "@testing-library/cypress", "cypress-axe"]
}
}