Skip to content

Commit de2909d

Browse files
author
NikolayAvramov
committed
Add Screenshot on fail plugin
1 parent c6641f7 commit de2909d

File tree

7 files changed

+76
-1
lines changed

7 files changed

+76
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Plugin } from '@bellatrix/core/infrastructure';
2+
import { TestMetadata } from '@bellatrix/core/test/props';
3+
import { ServiceLocator } from '@bellatrix/core/utilities';
4+
import { App } from '@bellatrix/web/infrastructure';
5+
import * as fs from 'fs';
6+
import * as path from 'path';
7+
8+
export class ScreenshotOnFailPlugin extends Plugin {
9+
override async preAfterTest(metadata: TestMetadata): Promise<void> {
10+
if(metadata.error !== undefined) {
11+
const app = ServiceLocator.resolve(App);
12+
const screenshotImage = await app.browser.getScreenshot();
13+
// Save the screenshot as an image file
14+
try {
15+
const savePath = this.saveImageFromBase64(screenshotImage.base64, '../reports/screenshots/' + metadata.testName); // TODO: take from config
16+
console.info('\n Screenshot for failed test ' + metadata.testName + ': ' + savePath + '\n');
17+
} catch (error) {
18+
console.error('Error saving screenshot:', (error as Error).message);
19+
}
20+
}
21+
}
22+
23+
/**
24+
* Save a Base64 string as an image file
25+
* @param base64String - The Base64 string of the image
26+
* @param outputPath - The path to save the image file
27+
*/
28+
private saveImageFromBase64(base64String: string, outputPath: string): string {
29+
// Check if the Base64 string contains the data prefix (e.g., "data:image/png;base64,")
30+
let matches = base64String.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/);
31+
if (!matches || matches.length !== 3) {
32+
base64String = 'data:image/png;base64,' + base64String;
33+
matches = base64String.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/);
34+
}
35+
// Extract the file extension and the Base64-encoded data
36+
const fileExtension = matches[1]; // e.g., 'png', 'jpeg', etc.
37+
const base64Data = matches[2];
38+
// Decode the Base64 string into binary data
39+
const binaryData = Buffer.from(base64Data, 'base64');
40+
const arrayBufferView: Uint8Array = new Uint8Array(binaryData.buffer, binaryData.byteOffset, binaryData.length);
41+
// Ensure the output directory exists
42+
const outputDir = path.dirname(outputPath);
43+
if (!fs.existsSync(outputDir)) {
44+
fs.mkdirSync(outputDir, { recursive: true });
45+
}
46+
// Determine the output file path (with the correct file extension)
47+
const outputFilePath = path.extname(outputPath)
48+
? outputPath
49+
: `${outputPath}.${fileExtension}`;
50+
// Write the binary data to a file
51+
fs.writeFileSync(outputFilePath, arrayBufferView);
52+
return outputFilePath;
53+
}
54+
}
+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './LogLifecyclePlugin';
2+
export * from './ScreenshotOnFailPlugin';

@bellatrix/web/src/infrastructure/browsercontroller/core/BrowserController.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Locator, SearchContext, WebElement } from '.';
2+
import { Image } from '@bellatrix/core/image';
23

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

@bellatrix/web/src/infrastructure/browsercontroller/playwright/PlaywrightBrowserController.ts

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Cookie, BrowserController, WebElement, Locator } from '@bellatrix/web/i
44
import { PlaywrightWebElement } from '@bellatrix/web/infrastructure/browsercontroller/playwright';
55
import { BellatrixSettings } from '@bellatrix/core/settings';
66
import { HttpClient } from '@bellatrix/core/http';
7+
import { Image } from '@bellatrix/core/image';
78

89
export class PlaywrightBrowserController extends BrowserController {
910
private _browser: Browser;
@@ -48,6 +49,10 @@ export class PlaywrightBrowserController extends BrowserController {
4849
return await this._page.content();
4950
}
5051

52+
override async getScreenshot(): Promise<Image> {
53+
return await Image.fromBase64((await this._page.screenshot()).toString('base64'));
54+
}
55+
5156
override async back(): Promise<void> {
5257
await this._page.goBack();
5358
}

@bellatrix/web/src/infrastructure/browsercontroller/selenium/SeleniumBrowserController.ts

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { By, WebDriver as NativeWebDriver, until } from 'selenium-webdriver';
33
import { Cookie, BrowserController, WebElement, Locator } from '@bellatrix/web/infrastructure/browsercontroller/core';
44
import { SeleniumShadowRootWebElement, SeleniumWebElement } from '@bellatrix/web/infrastructure/browsercontroller/selenium';
55
import { BellatrixSettings } from '@bellatrix/core/settings';
6+
import { Image } from '@bellatrix/core/image';
67

78
export class SeleniumBrowserController extends BrowserController {
89
private _driver: NativeWebDriver;
@@ -32,6 +33,11 @@ export class SeleniumBrowserController extends BrowserController {
3233
return await this.wrappedDriver.getPageSource();
3334
}
3435

36+
override async getScreenshot(): Promise<Image> {
37+
const base64image = (await this.wrappedDriver.takeScreenshot());
38+
return Image.fromBase64(base64image);
39+
}
40+
3541
override async back(): Promise<void> {
3642
await this.wrappedDriver.navigate().back();
3743
}

@bellatrix/web/src/services/BrowserService.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BrowserController } from '@bellatrix/web/infrastructure/browsercontroller/core';
22
import { BellatrixSettings } from '@bellatrix/core/settings';
33
import { BellatrixWebService } from '@bellatrix/web/services/decorators';
4+
import { Image } from '@bellatrix/core/image';
45
import { WebService } from '.';
56

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

25+
async getScreenshot(): Promise<Image> {
26+
const base64image = (await this.driver.getScreenshot()).base64;
27+
return Image.fromBase64(base64image);
28+
}
29+
2430
async back(): Promise<void> {
2531
return await this.driver.back();
2632
}

example/tests/ProductPurchaseTests.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Test, TestClass } from '@bellatrix/web/test';
22
import { WebTest } from '@bellatrix/web/infrastructure';
33
import { Button } from '@bellatrix/web/components';
44
import { ExtraWebHooks } from '@bellatrix/extras/hooks';
5-
import { LogLifecyclePlugin } from '@bellatrix/extras/plugins';
5+
import { LogLifecyclePlugin, ScreenshotOnFailPlugin } from '@bellatrix/extras/plugins';
66
import { MainPage, CartPage, CheckoutPage, PurchaseInfo } from '../src/pages';
77
import { PluginExecutionEngine } from '@bellatrix/core/infrastructure';
88
import { WebServiceHooks } from '@bellatrix/web/services/utilities';
@@ -14,6 +14,7 @@ export class ProductPurchaseTests extends WebTest {
1414
await super.configure();
1515
ExtraWebHooks.addComponentBDDLogging();
1616
PluginExecutionEngine.addPlugin(LogLifecyclePlugin);
17+
PluginExecutionEngine.addPlugin(ScreenshotOnFailPlugin);
1718
WebServiceHooks.addListenerTo(NavigationService).before('navigate', (_, url) => console.log(`navigating to ${url}`));
1819
}
1920

0 commit comments

Comments
 (0)