Skip to content

Commit

Permalink
Merge pull request #10 from AutomateThePlanet/add-web-tests
Browse files Browse the repository at this point in the history
Add Screenshot on fail plugin
  • Loading branch information
n1xan authored Jan 30, 2025
2 parents c6641f7 + f201943 commit 999188f
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 7 deletions.
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

0 comments on commit 999188f

Please sign in to comment.