From ce414af136eeb4c19e7e595c60e5d7e5e269453f Mon Sep 17 00:00:00 2001 From: Taisia Pitko Date: Thu, 27 Jun 2024 23:43:58 +0300 Subject: [PATCH] [#120] [patch] Fix screenshots for tests with failed hooks (#150) --- src/plugins/allure-global-hook.ts | 3 +- src/plugins/allure-reporter-plugin.ts | 95 ++++++++++++++----- src/plugins/allure-types.ts | 12 ++- src/setup/screenshots.ts | 2 +- .../screenshot-global-hook-fail.test.ts | 10 +- .../hooks/regression-steps.test.ts | 23 ++++- 6 files changed, 108 insertions(+), 37 deletions(-) diff --git a/src/plugins/allure-global-hook.ts b/src/plugins/allure-global-hook.ts index 01e7c762..4a855caa 100644 --- a/src/plugins/allure-global-hook.ts +++ b/src/plugins/allure-global-hook.ts @@ -122,7 +122,8 @@ export class GlobalHooks { } hook.attachments?.forEach(attach => { log('process attach'); - this.reporter.testFileAttachment({ name: attach.name, file: attach?.file, type: attach.type }); + this.reporter.testFileAttachment({ name: attach.name, file: attach.file, type: attach.type }); + this.reporter.setAttached(attach.file); }); }); } diff --git a/src/plugins/allure-reporter-plugin.ts b/src/plugins/allure-reporter-plugin.ts index 7490d203..e291cf8d 100644 --- a/src/plugins/allure-reporter-plugin.ts +++ b/src/plugins/allure-reporter-plugin.ts @@ -15,12 +15,20 @@ import getUuid from 'uuid-by-string'; import { parseAllure } from 'allure-js-parser'; import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { readFile } from 'fs/promises'; -import path, { basename } from 'path'; +import path, { basename, dirname } from 'path'; import glob from 'fast-glob'; import { ReporterOptions } from './allure'; import Debug from 'debug'; import { GlobalHooks } from './allure-global-hook'; -import { AfterSpecScreenshots, AllureTaskArgs, LabelName, Stage, StatusType, UNKNOWN } from './allure-types'; +import { + AfterSpecScreenshots, + AllureTaskArgs, + AutoScreen, + LabelName, + Stage, + StatusType, + UNKNOWN, +} from './allure-types'; import { extname, packageLog } from '../common'; import type { ContentType } from '../common/types'; import { randomUUID } from 'crypto'; @@ -162,7 +170,7 @@ export class AllureReporter { allureRuntime: AllureRuntime; descriptionHtml: string[] = []; - private screenshotsTest: { [testId: string]: { [testAttemptIndex: string]: string[] } } = {}; + private screenshotsTest: (AutoScreen & { attached?: boolean })[] = []; testStatusStored: AllureTaskArgs<'testStatus'> | undefined; testDetailsStored: AllureTaskArgs<'testDetails'> | undefined; @@ -452,6 +460,7 @@ export class AllureReporter { }); } + // after spec attach attachScreenshots(arg: AfterSpecScreenshots) { // attach auto screenshots for fails const { screenshots } = arg; @@ -463,13 +472,44 @@ export class AllureReporter { log('screenshotsTest:'); log(JSON.stringify(this.screenshotsTest)); + log('screenshots arg:'); + log(JSON.stringify(screenshots)); + + const arr = [...screenshots, ...this.screenshotsTest.filter(x => !x.attached)]; + + const uniqueScreenshotsArr = arr.reduce( + (acc: { map: Map; list: AutoScreen[] }, current) => { + const key = `${current.path}`; + + if (!acc.map.has(key)) { + acc.map.set(key, true); + current.specName = basename(dirname(current.path)); + acc.list.push(current); + } else { + const existing = acc.list.find(t => t.path === current.path); + const merged = { ...existing, ...current }; + acc.list = acc.list.map(item => (item.path === current.path ? merged : item)); + } + + return acc; + }, + { map: new Map(), list: [] }, + ).list; - screenshots.forEach(x => { - log(`attachScreenshots:${x.path}`); + uniqueScreenshotsArr.forEach(afterSpecRes => { + log(`attachScreenshots: ${afterSpecRes.path}`); - const uuids = allTests - .filter(t => t.retryIndex === x.testAttemptIndex && t.mochaId === x.testId && t.status !== Status.PASSED) - .map(t => t.uuid); + const getUuiToAdd = () => { + return allTests.filter( + t => + t.status !== Status.PASSED && + t.retryIndex === afterSpecRes.testAttemptIndex && + basename(t.specRelative ?? '') === afterSpecRes.specName && + (afterSpecRes.testId ? t.mochaId === afterSpecRes.testId : true), + ); + }; + + const uuids = getUuiToAdd().map(t => t.uuid); if (uuids.length === 0) { log('no attach auto screens, only for non-success tests tests'); @@ -477,8 +517,8 @@ export class AllureReporter { return; } - if (!uuids[x.testAttemptIndex ?? 0]) { - log(`no attach, current attempt ${x.testAttemptIndex}`); + if (afterSpecRes.testAttemptIndex && afterSpecRes.testId && !uuids[afterSpecRes.testAttemptIndex ?? 0]) { + log(`no attach, current attempt ${afterSpecRes.testAttemptIndex}`); // test passed or no return; @@ -489,8 +529,9 @@ export class AllureReporter { try { const contents = readFileSync(testFile); - const ext = path.extname(x.path); - const name = path.basename(x.path); + const ext = path.extname(afterSpecRes.path); + const name = path.basename(afterSpecRes.path); + type ParsedAttachment = { name: string; type: ContentType; source: string }; const testCon: { attachments: ParsedAttachment[] } = JSON.parse(contents.toString()); const uuidNew = randomUUID(); @@ -498,7 +539,7 @@ export class AllureReporter { const newPath = path.join(this.allureResults, nameAttach); if (!existsSync(newPath)) { - copyFileSync(x.path, path.join(this.allureResults, nameAttach)); + copyFileSync(afterSpecRes.path, path.join(this.allureResults, nameAttach)); } if (!testCon.attachments) { @@ -513,7 +554,7 @@ export class AllureReporter { writeFileSync(testFile, JSON.stringify(testCon)); } catch (e) { - console.log(`${packageLog} Could not attach screenshot ${x.screenshotId}`); + console.log(`${packageLog} Could not attach screenshot ${afterSpecRes.screenshotId ?? afterSpecRes.path}`); } }); }); @@ -524,16 +565,9 @@ export class AllureReporter { } screenshotAttachment(arg: AllureTaskArgs<'screenshotAttachment'>) { - const { testId, path, testAttemptIndex } = arg; + const { testId, path, testAttemptIndex, specName, testFailure } = arg; - if (!this.screenshotsTest[this.keyWhenNoTest(testId)]) { - this.screenshotsTest[this.keyWhenNoTest(testId)] = {}; - } - - if (!this.screenshotsTest[this.keyWhenNoTest(testId)][testAttemptIndex ?? 0]) { - this.screenshotsTest[this.keyWhenNoTest(testId)][testAttemptIndex ?? 0] = []; - } - this.screenshotsTest[this.keyWhenNoTest(testId)][testAttemptIndex ?? 0].push(path); + this.screenshotsTest.push({ testId, path, testAttemptIndex, specName, testFailure }); } screenshotOne(arg: AllureTaskArgs<'screenshotOne'>) { @@ -1050,6 +1084,14 @@ export class AllureReporter { exec.addAttachment(arg.name, arg.type, file); } + public setAttached(file: string) { + const screen = this.screenshotsTest.find(t => t.path === file); + + if (screen) { + screen.attached = true; + } + } + private executableFileAttachment(exec: ExecutableItemWrapper | undefined, arg: AllureTaskArgs<'fileAttachment'>) { if (!this.currentExecutable && this.globalHooks.currentHook) { log('No current executable, test or hook - add to global hook'); @@ -1078,12 +1120,13 @@ export class AllureReporter { mkdirSync(this.allureResults, { recursive: true }); } - // how to understand where to attach + const currExec = exec ?? this.currentExecutable; - if (exec ?? this.currentExecutable) { + if (currExec) { copyFileSync(arg.file, `${this.allureResults}/${fileNew}`); - (exec ?? this.currentExecutable)?.addAttachment(arg.name, arg.type, fileNew); + currExec.addAttachment(arg.name, arg.type, fileNew); log(`added attachment: ${fileNew} ${arg.file}`); + this.setAttached(arg.file); } } catch (err) { console.error(`${packageLog} Could not attach ${arg.file}`); diff --git a/src/plugins/allure-types.ts b/src/plugins/allure-types.ts index e8c9a7bc..41dc3964 100644 --- a/src/plugins/allure-types.ts +++ b/src/plugins/allure-types.ts @@ -2,15 +2,17 @@ import type { StatusDetails } from 'allure-js-commons'; import type { ContentType } from '../common/types'; export interface AutoScreen { - screenshotId: string; + screenshotId?: string; specName?: string; testId: string | undefined; - testAttemptIndex: number; - takenAt: string; // date + testAttemptIndex?: number; + takenAt?: string; // date path: string; // abs path - height: number; - width: number; + height?: number; + width?: number; + testFailure?: boolean; } + export type AfterSpecScreenshots = { screenshots: AutoScreen[]; }; diff --git a/src/setup/screenshots.ts b/src/setup/screenshots.ts index f6899f04..8ed6f20d 100644 --- a/src/setup/screenshots.ts +++ b/src/setup/screenshots.ts @@ -13,7 +13,7 @@ export const registerScreenshotHandler = (message: MessageManager, testMsg: (msg // eslint-disable-next-line @typescript-eslint/no-explicit-any (Cypress.Screenshot as any).onAfterScreenshot = (_$el: unknown, ...args: AutoScreen[]) => { debug('Screenshot handler'); - // testAttemptIndex, takenAt, name + // testAttemptIndex, takenAt, name, specName, testFailure const [screensArgs] = args; const [{ path }] = args; diff --git a/tests/test-folder/mocha-events/attachments/screenshot-global-hook-fail.test.ts b/tests/test-folder/mocha-events/attachments/screenshot-global-hook-fail.test.ts index 049d24c8..5a43bdf1 100644 --- a/tests/test-folder/mocha-events/attachments/screenshot-global-hook-fail.test.ts +++ b/tests/test-folder/mocha-events/attachments/screenshot-global-hook-fail.test.ts @@ -6,7 +6,6 @@ import { } from '../../../cy-helper/utils'; import { AllureTest, parseAllure } from 'allure-js-parser'; -// this doesn't work describe('test screenshot when global before hook fails', () => { const res = createResTest2( [ @@ -53,10 +52,15 @@ describe('screenshot when global before hook fails @screen', () => { t => !t.name.includes('after each') && !t.name.includes('before each'), ); - // todo: expected to have screenshots expect(obj).toEqual([ { - attachments: [], + attachments: [ + { + name: '01 test -- before all hook (failed).png', + source: 'source.png', + type: 'image/png', + }, + ], name: '01 test', parents: [ { diff --git a/tests/test-folder/mocha-events/hooks/regression-steps.test.ts b/tests/test-folder/mocha-events/hooks/regression-steps.test.ts index fe323321..be8e6fad 100644 --- a/tests/test-folder/mocha-events/hooks/regression-steps.test.ts +++ b/tests/test-folder/mocha-events/hooks/regression-steps.test.ts @@ -134,7 +134,28 @@ describe('hooks test - failed global hook step', () => { }); it('check attachments', async () => { - expect(resFixed.flatMap(t => t.attachments).sort()).toEqual([]); + expect(resFixed.flatMap(t => t.attachments).sort()).toEqual([ + { + name: 'test 1 -- before all hook Global Setup (failed).png', + source: 'source.png', + type: 'image/png', + }, + { + name: 'test 1 -- before all hook Global Setup (failed).png', + source: 'source.png', + type: 'image/png', + }, + { + name: 'test 1 -- before all hook Global Setup (failed).png', + source: 'source.png', + type: 'image/png', + }, + ]); + expect( + resFixed + .map(t => t.parent?.befores?.flatMap(x => x.attachments)) + .sort(), + ).toEqual([[], [], []]); expect( resFixed.map(t => t.parent?.afters?.flatMap(x => x.attachments)).sort(), ).toEqual([