Skip to content

Add Screenshot on fail plugin #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion @bellatrix/appium/src/android/AndroidDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class AndroidDriver extends AppiumDriver {
});
}

async getScreenshot(): Promise<Image> {
async takeScreenshot(): Promise<Image> {
const base64image = await this.commandExecutor.execute<Promise<string>>(MobileCommands.SCREENSHOT);
return Image.fromBase64(base64image);
}
Expand Down
8 changes: 4 additions & 4 deletions @bellatrix/core/src/image/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export class Image {
return `data:image/${this.type};base64,${this.base64}`;
}

get buffer() {
return this._buffer;
}

get width(): number {
switch (this._type) {
case 'png':
Expand Down Expand Up @@ -172,10 +176,6 @@ export class Image {
}
}

protected get buffer() {
return this._buffer;
}

protected determineType(buffer: Buffer): keyof typeof this.SIGNATURES {
for (const [format, signature] of Object.entries(this.SIGNATURES)) {
if (buffer.subarray(0, signature.length).equals(signature)) {
Expand Down
81 changes: 81 additions & 0 deletions @bellatrix/extras/src/plugins/ScreenshotOnFailPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Plugin } from '@bellatrix/core/infrastructure';
import { TestMetadata } from '@bellatrix/core/test/props';
import { ServiceLocator } from '@bellatrix/core/utilities';
import { Image } from '@bellatrix/core/image';
import { App } from '@bellatrix/web/infrastructure';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { dirname, extname, join } from 'path';
import { BellatrixSettings } from '@bellatrix/core/settings';

export class ScreenshotOnFailPlugin extends Plugin {
override async preAfterTest(metadata: TestMetadata): Promise<void> {
const pluginSettings = BellatrixSettings.get().screenshotOnFailPluginSettings;

if (!pluginSettings?.isPluginEnabled) {
return;
}

if (!metadata.error) {
return;
}

const app = ServiceLocator.resolve(App);
const screenshotImage = await app.browser.takeScreenshot();

const outputPath = pluginSettings?.outputPath;

if (!outputPath) {
console.error('Output path for screenshots is not defined in the configuration.');
return;
}

try {
const projectRoot = process.env['BELLATRIX_CONFIGURAITON_ROOT']!; // TODO: find a better way to get the project root
const pathArray = [projectRoot, outputPath];
if (pluginSettings?.shouldCreateFolderPerSuite) {
pathArray.push(metadata.suiteName);
}
pathArray.push(metadata.testName);
const savePath = this.saveImageToFile(screenshotImage, join(...pathArray));
console.info('\n Screenshot for failed test ' + metadata.testName + ': ' + savePath + '\n');
} catch (error) {
if (error instanceof Error) {
console.error('Error saving screenshot:', error.message);
} else {
console.error('Error saving screenshot');
}
}
}

/**
* Save an Image class instance as a file
* @param image - The Image instance to be saved
* @param outputPath - The path to save the image file
*/
private saveImageToFile(image: Image, outputPath: string): string {
const outputDir = dirname(outputPath);
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}

const outputFilePath = extname(outputPath) ? outputPath : `${outputPath}.${image.type}`;

const binaryData = image.buffer;
const arrayBufferView = new Uint8Array(binaryData.buffer, binaryData.byteOffset, binaryData.length);
writeFileSync(outputFilePath, arrayBufferView);
return outputFilePath;
}
}

declare module '@bellatrix/core/types' {
interface BellatrixConfiguration {
screenshotOnFailPluginSettings?: ScreenshotOnFailPluginSettings;
}
}

interface ScreenshotOnFailPluginSettings {
isPluginEnabled: boolean;
outputPath: string,
shouldCreateFolderPerSuite?: boolean,
shouldCaptureFullPage?: boolean,
}
1 change: 1 addition & 0 deletions @bellatrix/extras/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './LogLifecyclePlugin';
export * from './ScreenshotOnFailPlugin';
4 changes: 3 additions & 1 deletion @bellatrix/runner/bellatrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ const configs = [
'.bellatrix.json',
];

const configFileURL = pathToFileURL(findFilePath(configs));
const configFilePath = findFilePath(configs);
const configFileURL = pathToFileURL(configFilePath);
process.env.BELLATRIX_CONFIGURAITON_ROOT = dirname(configFilePath);
let config;

if (configFileURL.href.endsWith('.ts') || configFileURL.href.endsWith('.mts')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Locator, SearchContext, WebElement } from '.';
import { Image } from '@bellatrix/core/image';

export type Cookie = {
name: string;
Expand Down Expand Up @@ -30,6 +31,7 @@ export abstract class BrowserController implements SearchContext {
abstract getCookie(name: string): Promise<Cookie | null>;
abstract getAllCookies(): Promise<Cookie[]>;
abstract clearCookies(): Promise<void>;
abstract takeScreenshot(): Promise<Image>;
abstract executeJavascript<T, VarArgs extends unknown[] = []>(script: string | ((...args: VarArgs) => T), ...args: VarArgs): Promise<T>;
abstract waitUntil(condition: (browserController: Omit<BrowserController, 'waitUntil'>) => boolean | Promise<boolean>, timeout: number, pollingInterval: number): Promise<void>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Cookie, BrowserController, WebElement, Locator } from '@bellatrix/web/i
import { PlaywrightWebElement } from '@bellatrix/web/infrastructure/browsercontroller/playwright';
import { BellatrixSettings } from '@bellatrix/core/settings';
import { HttpClient } from '@bellatrix/core/http';
import { Image } from '@bellatrix/core/image';

export class PlaywrightBrowserController extends BrowserController {
private _browser: Browser;
Expand Down Expand Up @@ -48,6 +49,10 @@ export class PlaywrightBrowserController extends BrowserController {
return await this._page.content();
}

override async takeScreenshot(): Promise<Image> {
return await Image.fromBase64((await this._page.screenshot()).toString('base64'));
}

override async back(): Promise<void> {
await this._page.goBack();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { By, WebDriver as NativeWebDriver, until } from 'selenium-webdriver';
import { Cookie, BrowserController, WebElement, Locator } from '@bellatrix/web/infrastructure/browsercontroller/core';
import { SeleniumShadowRootWebElement, SeleniumWebElement } from '@bellatrix/web/infrastructure/browsercontroller/selenium';
import { BellatrixSettings } from '@bellatrix/core/settings';
import { Image } from '@bellatrix/core/image';

export class SeleniumBrowserController extends BrowserController {
private _driver: NativeWebDriver;
Expand Down Expand Up @@ -32,6 +33,11 @@ export class SeleniumBrowserController extends BrowserController {
return await this.wrappedDriver.getPageSource();
}

override async takeScreenshot(): Promise<Image> {
const base64image = (await this.wrappedDriver.takeScreenshot());
return Image.fromBase64(base64image);
}

override async back(): Promise<void> {
await this.wrappedDriver.navigate().back();
}
Expand Down
6 changes: 6 additions & 0 deletions @bellatrix/web/src/services/BrowserService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BrowserController } from '@bellatrix/web/infrastructure/browsercontroller/core';
import { BellatrixSettings } from '@bellatrix/core/settings';
import { BellatrixWebService } from '@bellatrix/web/services/decorators';
import { Image } from '@bellatrix/core/image';
import { WebService } from '.';

@BellatrixWebService
Expand All @@ -21,6 +22,11 @@ export class BrowserService extends WebService {
return await this.driver.getPageSource();
}

async takeScreenshot(): Promise<Image> {
const base64image = (await this.driver.takeScreenshot()).base64;
return Image.fromBase64(base64image);
}

async back(): Promise<void> {
return await this.driver.back();
}
Expand Down
5 changes: 5 additions & 0 deletions example/bellatrix.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ const config: BellatrixConfigurationOverride = {
browserName: 'chrome'
}
}
},
screenshotOnFailPluginSettings: {
isPluginEnabled: true,
outputPath: `./reports/screenshots${Date.now()}`,
shouldCreateFolderPerSuite: false,
}
};

Expand Down
3 changes: 2 additions & 1 deletion example/tests/ProductPurchaseTests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Test, TestClass } from '@bellatrix/web/test';
import { WebTest } from '@bellatrix/web/infrastructure';
import { Button } from '@bellatrix/web/components';
import { ExtraWebHooks } from '@bellatrix/extras/hooks';
import { LogLifecyclePlugin } from '@bellatrix/extras/plugins';
import { LogLifecyclePlugin, ScreenshotOnFailPlugin } from '@bellatrix/extras/plugins';
import { MainPage, CartPage, CheckoutPage, PurchaseInfo } from '../src/pages';
import { PluginExecutionEngine } from '@bellatrix/core/infrastructure';
import { WebServiceHooks } from '@bellatrix/web/services/utilities';
Expand All @@ -14,6 +14,7 @@ export class ProductPurchaseTests extends WebTest {
await super.configure();
ExtraWebHooks.addComponentBDDLogging();
PluginExecutionEngine.addPlugin(LogLifecyclePlugin);
PluginExecutionEngine.addPlugin(ScreenshotOnFailPlugin);
WebServiceHooks.addListenerTo(NavigationService).before('navigate', (_, url) => console.log(`navigating to ${url}`));
}

Expand Down