chore(ws): add cypress structure and initial tests for frontend (#73)
Signed-off-by: Griffin-Sullivan <gsulliva@redhat.com>
This commit is contained in:
parent
4068e9e4e3
commit
9d02acfdd7
|
@ -0,0 +1,2 @@
|
|||
# Test against prod build hosted by lightweight http server
|
||||
BASE_URL=http://localhost:9001
|
|
@ -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
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
coverage
|
||||
results
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
class Home {
|
||||
visit() {
|
||||
cy.visit(`/`);
|
||||
}
|
||||
|
||||
findButton() {
|
||||
return cy.get('button:contains("Primary Action")');
|
||||
}
|
||||
}
|
||||
|
||||
export const home = new Home();
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
import '@testing-library/cypress/add-commands';
|
||||
import './axe';
|
|
@ -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 });
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
|
@ -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 };
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": ["../../**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"types": ["node", "cypress", "@testing-library/cypress", "cypress-axe"]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue