Skip to content

Commit

Permalink
Cypress evidence improvements (#1065)
Browse files Browse the repository at this point in the history
  • Loading branch information
matzehecht authored Oct 10, 2024
1 parent a2ab9a7 commit 31f4453
Show file tree
Hide file tree
Showing 14 changed files with 205 additions and 40 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- Introduce Rust Quickstarter dependency graph linting (cargo-deny) and upgrade maintenance ([#1061](https://github.com/opendevstack/ods-quickstarters/issues/1061))
- Add microsoft-edge to nodejs agents for using with cypress ([#1063](https://github.com/opendevstack/ods-quickstarters/pull/1063))
- Replaced centos8 repository for AlmaLinux 8 due to deprecation ([#1063](https://github.com/opendevstack/ods-quickstarters/pull/1063))
- Improvements in the reporter for cypress ([#1042](https://github.com/opendevstack/ods-quickstarters/issues/1042))

### Added

Expand Down Expand Up @@ -466,4 +467,4 @@

## [0.1.0 ods-project-quickstarters] - 2018-07-27

Initial release.
Initial release.
4 changes: 3 additions & 1 deletion docs/modules/quickstarters/pages/e2e-cypress.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ This is a Cypress end-to-end testing project quickstarter with basic setup for h
├── fixtures
│ └── example.json
├── plugins
│ └── index.js
│ ├── index.ts
│ ├── screenshot.ts
│ └── screenshot.types.ts
├── reporters
│ └── custom-reporter.js
├── support
Expand Down
31 changes: 25 additions & 6 deletions e2e-cypress/Jenkinsfile.template
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,24 @@ odsComponentPipeline(
// 'release/': 'test'
]
) { context ->

def targetDirectory = "${context.projectId}/${context.componentId}/${context.gitBranch.replaceAll('/', '-')}/${context.buildNumber}"

stageTest(context)
odsComponentStageScanWithSonar(context)


if (fileExists('cypress/screenshots.zip')) {
odsComponentStageUploadToNexus(context,
[
distributionFile: 'cypress/screenshots.zip',
repository: 'leva-documentation',
repositoryType: 'raw',
targetDirectory: "${targetDirectory}"
]
)
}

}

def stageTest(def context) {
stage('Integration Test') {
// OPTIONAL: load environment variables for Azure SSO with MSALv2; please adapt variable names to your OpenShift config
Expand All @@ -54,6 +66,8 @@ def stageTest(def context) {
// "CYPRESS_CLIENT_SECRET=${azureClientSecret}",
// "CYPRESS_USERNAME=${cypressUser}",
// "CYPRESS_PASSWORD=${cypressPassword}"
"COMMIT_INFO_SHA=${context.gitCommit}",
"BUILD_NUMBER=${context.buildNumber}",
]) {
sh 'npm install'
def status = sh(script: 'npm run e2e', returnStatus: true)
Expand All @@ -62,14 +76,19 @@ def stageTest(def context) {
stash(name: "installation-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/installation-junit.xml', allowEmpty: true)
stash(name: "integration-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/integration-junit.xml', allowEmpty: true)
stash(name: "acceptance-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/acceptance-junit.xml', allowEmpty: true)
zip zipFile: 'cypress/videos.zip', archive: false, dir: 'cypress/videos'
stash(name: "acceptance-test-videos-${context.componentId}-${context.buildNumber}", includes: 'cypress/videos.zip', allowEmpty: true)
archiveArtifacts artifacts: 'cypress/videos.zip', fingerprint: true, daysToKeep: 2, numToKeep: 3
if (status != 0) {

if (fileExists('cypress/videos')) {
zip zipFile: 'cypress/videos.zip', archive: false, dir: 'cypress/videos'
stash(name: "acceptance-test-videos-${context.componentId}-${context.buildNumber}", includes: 'cypress/videos.zip', allowEmpty: true)
archiveArtifacts artifacts: 'cypress/videos.zip', fingerprint: true, daysToKeep: 2, numToKeep: 3
}

if (fileExists('cypress/screenshots')) {
zip zipFile: 'cypress/screenshots.zip', archive: false, dir: 'cypress/screenshots'
stash(name: "acceptance-test-screenshots-${context.componentId}-${context.buildNumber}", includes: 'cypress/screenshots.zip', allowEmpty: true)
archiveArtifacts artifacts: 'cypress/screenshots.zip', fingerprint: true, daysToKeep: 2, numToKeep: 3
}

return status
}
}
Expand Down
4 changes: 2 additions & 2 deletions e2e-cypress/files/cypress-acceptance.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export default defineConfig({
viewportHeight: 660,
experimentalModifyObstructiveThirdPartyCode:true,
video: true,
setupNodeEvents(on, config) {
return require('./plugins/index.js')(on, config)
async setupNodeEvents(on, config) {
return (await import('./plugins/index')).default(on, config);
},
},
})
4 changes: 2 additions & 2 deletions e2e-cypress/files/cypress-installation.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export default defineConfig({
viewportHeight: 660,
experimentalModifyObstructiveThirdPartyCode:true,
video: true,
setupNodeEvents(on, config) {
return require('./plugins/index.js')(on, config)
async setupNodeEvents(on, config) {
return (await import('./plugins/index')).default(on, config);
},
},
})
4 changes: 2 additions & 2 deletions e2e-cypress/files/cypress-integration.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export default defineConfig({
viewportHeight: 660,
experimentalModifyObstructiveThirdPartyCode:true,
video: true,
setupNodeEvents(on, config) {
return require('./plugins/index.js')(on, config)
async setupNodeEvents(on, config) {
return (await import('./plugins/index')).default(on, config);
},
},
})
4 changes: 2 additions & 2 deletions e2e-cypress/files/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export default defineConfig({
viewportHeight: 660,
experimentalModifyObstructiveThirdPartyCode: true,
video: true,
setupNodeEvents(on, config) {
return require('./plugins/index.js')(on, config)
async setupNodeEvents(on, config) {
return (await import('./plugins/index')).default(on, config);
},
},
})
1 change: 1 addition & 0 deletions e2e-cypress/files/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"mocha-junit-reporter": "^2.2.1",
"npm-run-all": "^4.1.5",
"rimraf": "^6.0.1",
"sharp": "^0.33.5",
"typescript": "^5.5.4"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,24 @@
// https://on.cypress.io/plugins-guide
// ***********************************************************

import type { ScreenshotEvidenceData } from './screenshot.types';
import { addEvidenceMetaToScreenshot } from './screenshot';

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

module.exports = (on, config) => {
const setupNodeEvents: NonNullable<Cypress.ConfigOptions['setupNodeEvents']> = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
on('task', {
log(message) {
console.log(message);
return null;
},
async takeScreenshotEvidence(data: ScreenshotEvidenceData) {
return await addEvidenceMetaToScreenshot(data);
}
});
};

export default setupNodeEvents;
61 changes: 61 additions & 0 deletions e2e-cypress/files/plugins/screenshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createHash } from 'crypto';
import { writeFile } from 'fs/promises';
import * as sharp from 'sharp';
import { ScreenshotEvidenceData, ScreenshotEvidenceResult } from './screenshot.types';

const SCREENSHOT_METADATA = {
height: 50,
margin: 10,
textAlign: 'left',
textColor: '#000',
textSize: 'large',
backgroundColor: '#fff',
maxNameLength: 100,
} as const;
const EVIDENCE_HASH_ALGORITHM = 'sha256' as const;

export const addEvidenceMetaToScreenshot = async (data: ScreenshotEvidenceData): Promise<ScreenshotEvidenceResult> => {
const metadata: [string, string][] = [
['Timestamp', data.takenAt],
['Testname', data.name.substring(0, SCREENSHOT_METADATA.maxNameLength)],
['Step', data.step.toString()],
['Screenshot', data.subStep.toString()],
['Build number', process.env.BUILD_NUMBER ?? '-'],
['Git commit', process.env.COMMIT_INFO_SHA ?? '-'],
];

const image = sharp(data.path);
const imageMetadata = await image.metadata();
const imageBuffer = await image
.resize({
background: SCREENSHOT_METADATA.backgroundColor,
fit: 'contain',
height: (imageMetadata.height ?? 0) + SCREENSHOT_METADATA.height,
position: 'bottom',
width: imageMetadata.width ?? 0,
})
.composite([
{
input: {
text: {
align: SCREENSHOT_METADATA.textAlign,
rgba: true,
text: metadata
.map(([key, value]) => `<span foreground="${SCREENSHOT_METADATA.textColor}" size="${SCREENSHOT_METADATA.textSize}"><b>${key}</b>: ${value}</span>`)
.join('\t'),
width: (imageMetadata.width ?? 0) - SCREENSHOT_METADATA.margin * 2,
},
},
left: SCREENSHOT_METADATA.margin,
top: SCREENSHOT_METADATA.margin,
},
])
.toBuffer();
await writeFile(data.path, imageBuffer);

const hash = createHash(EVIDENCE_HASH_ALGORITHM).update(imageBuffer).digest('hex');
return {
hash,
path: data.path,
}
};
21 changes: 21 additions & 0 deletions e2e-cypress/files/plugins/screenshot.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type ScreenshotEvidenceData = {
name: string;
path: string;
step: number;
subStep: number;
takenAt: string;
};

export type ScreenshotEvidenceResult = {
hash: string;
path: string;
};
export const isScreenshotEvidenceResult = (candidate: unknown): candidate is ScreenshotEvidenceResult =>
Boolean(
typeof candidate === 'object' &&
candidate &&
'hash' in candidate &&
'path' in candidate &&
typeof candidate.hash === 'string' &&
typeof candidate.path === 'string'
);
13 changes: 3 additions & 10 deletions e2e-cypress/files/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,10 @@ Cypress.Commands.add('loginToAAD', (username: string, password: string) => {
log.end()
})

let consoleLogs: string[] = []

Cypress.on('log:added', (options) => {
const message = options.message;
if(message) {
consoleLogs.push(message);
}
});
export const consoleLogs: string[] = [];

beforeEach(function() {
consoleLogs = [];
consoleLogs.splice(0);
})

afterEach(function() {
Expand All @@ -88,5 +81,5 @@ afterEach(function() {

cy.writeFile(filePath, consoleLogs.join('\n'));

consoleLogs = [];
consoleLogs.splice(0);
})
74 changes: 64 additions & 10 deletions e2e-cypress/files/support/test-evidence.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,70 @@
export const printTestEvidence = (testName: string, testStep: number, selector: string, description: string) => {
import * as path from 'path';
import { isScreenshotEvidenceResult, ScreenshotEvidenceData } from "../plugins/screenshot.types";
import { consoleLogs } from "./e2e";

const logEvidence = (name: string, step: number, description: string, evidenceLogs: string[]) => {
cy.url().then(url => {
const logs: string[] = [];
logs.push('=====================================');
logs.push(`Testname: ${name} // step: ${step}`);
logs.push(`URL: ${url}`);
logs.push(`Description: ${description}`);
logs.push('----- Test Evidence starts here ----');
logs.push(...evidenceLogs);
logs.push('----- Test Evidence ends here ----');
consoleLogs.push(...logs);
cy.task('log', logs.join('\n'));
});
}

export const printTestDOMEvidence = (testName: string, testStep: number, selector: string, description: string) => {
if (!selector) {
throw new Error('selector must not NOT be undefined');
}
cy.task('log', '=====================================');
cy.task('log', 'Testname: ' + testName + ' // step: ' + testStep);
cy.url().then(urlString => {
cy.task('log', 'URL: ' + urlString);
});
cy.task('log', 'Description: ' + description);
cy.task('log', '----- Test Evidence starts here ----');
cy.get(selector).then($selectedElement => {
cy.task('log', 'Selector: ' + selector + '\r ' + $selectedElement.get(0).outerHTML);
logEvidence(testName, testStep, description, [`Selector: ${selector}\n ${$selectedElement.get(0).outerHTML}`]);
});
};

export const printTestPlainEvidence = (testName: string, testStep: number, expectedValue: string, actualValue: string, description: string) => {
if (!expectedValue || !actualValue) {
throw new Error('expectedValue and actualValue must not NOT be undefined');
}
logEvidence(testName, testStep, description, [
`Expected Result:\n ${String(expectedValue)}`,
`Actual Result:\n ${String(actualValue)}`
]);
};

export const takeScreenshotEvidence = (testName: string, testStep: number, testSubStep: number = 1, description: string, skipMeta = false) => {
cy.wrap(null).then(() => {
const data: Omit<ScreenshotEvidenceData, 'path' | 'takenAt'> &
Partial<Pick<ScreenshotEvidenceData, 'path' | 'takenAt'>> = {
name: testName,
step: testStep,
subStep: testSubStep,
};

cy.screenshot(`testevidence_${testName}_${testStep}_${testSubStep}`, {
onAfterScreenshot(_, { path, takenAt }) {
data.path = path;
data.takenAt = new Date(takenAt).toISOString();
},
})
.then<unknown>(() => {
if (!data.path || !data.takenAt || skipMeta) {
return null;
}
cy.task('takeScreenshotEvidence', data);
})
.then((result) => {
if (!isScreenshotEvidenceResult(result)) {
return null;
}

logEvidence(testName, testStep, description, [
`Stored screenshot "${path.basename(result.path)}" with hash (sha256) ${result.hash} taken at ${String(data.takenAt)} as evidence.`
]);
});
});
cy.task('log', '----- Test Evidence ends here ----');
};
11 changes: 8 additions & 3 deletions e2e-cypress/files/tests/acceptance/acceptance.spec.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { printTestEvidence } from '../../support/test-evidence';
import { printTestDOMEvidence, printTestPlainEvidence, takeScreenshotEvidence } from '../../support/test-evidence';

/* tslint:disable:no-unused-expression */
// describe('ADD login example test', () => {
Expand All @@ -19,7 +19,12 @@ describe('W3 application test', () => {
it('Application is reachable', function () {
cy.visit('/html/tryit.asp?filename=tryhtml_basic_paragraphs');
cy.title().should('include', 'Tryit Editor');
printTestEvidence(this.test.fullTitle(), 1, '#textareaCode', 'code area');
printTestEvidence(this.test.fullTitle(), 2, '#iframecontainer', 'rendered code area');
printTestDOMEvidence(this.test.fullTitle(), 1, '#textareaCode', 'code area');
printTestDOMEvidence(this.test.fullTitle(), 2, '#iframecontainer', 'rendered code area');
takeScreenshotEvidence(this.test.fullTitle(), 3, 1, 'screenshot');
takeScreenshotEvidence(this.test.fullTitle(), 3, 2, 'screenshot substep 2');
cy.title().then(title => {
printTestPlainEvidence(this.test.fullTitle(), 4, title, 'Tryit Editor', 'Title should include Tryit Editor');
});
});
});

0 comments on commit 31f4453

Please sign in to comment.