diff --git a/@bellatrix/appium/src/android/AndroidDriver.ts b/@bellatrix/appium/src/android/AndroidDriver.ts index e37ed8b..2391558 100644 --- a/@bellatrix/appium/src/android/AndroidDriver.ts +++ b/@bellatrix/appium/src/android/AndroidDriver.ts @@ -135,7 +135,7 @@ export class AndroidDriver extends AppiumDriver { }); } - async getScreenshot(): Promise { + async takeScreenshot(): Promise { const base64image = await this.commandExecutor.execute>(MobileCommands.SCREENSHOT); return Image.fromBase64(base64image); } diff --git a/@bellatrix/core/src/image/Image.ts b/@bellatrix/core/src/image/Image.ts index c1a2558..b842f3d 100644 --- a/@bellatrix/core/src/image/Image.ts +++ b/@bellatrix/core/src/image/Image.ts @@ -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': @@ -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)) { diff --git a/@bellatrix/extras/src/plugins/ScreenshotOnFailPlugin.ts b/@bellatrix/extras/src/plugins/ScreenshotOnFailPlugin.ts new file mode 100644 index 0000000..811780f --- /dev/null +++ b/@bellatrix/extras/src/plugins/ScreenshotOnFailPlugin.ts @@ -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 { + 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, +} diff --git a/@bellatrix/extras/src/plugins/index.ts b/@bellatrix/extras/src/plugins/index.ts index 5ad46e3..ffdd303 100644 --- a/@bellatrix/extras/src/plugins/index.ts +++ b/@bellatrix/extras/src/plugins/index.ts @@ -1 +1,2 @@ export * from './LogLifecyclePlugin'; +export * from './ScreenshotOnFailPlugin'; diff --git a/@bellatrix/runner/bellatrix.js b/@bellatrix/runner/bellatrix.js index 9addb1c..600a0b0 100755 --- a/@bellatrix/runner/bellatrix.js +++ b/@bellatrix/runner/bellatrix.js @@ -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')) { diff --git a/@bellatrix/web/src/infrastructure/browsercontroller/core/BrowserController.ts b/@bellatrix/web/src/infrastructure/browsercontroller/core/BrowserController.ts index ca32f4a..fb861db 100644 --- a/@bellatrix/web/src/infrastructure/browsercontroller/core/BrowserController.ts +++ b/@bellatrix/web/src/infrastructure/browsercontroller/core/BrowserController.ts @@ -1,4 +1,5 @@ import { Locator, SearchContext, WebElement } from '.'; +import { Image } from '@bellatrix/core/image'; export type Cookie = { name: string; @@ -30,6 +31,7 @@ export abstract class BrowserController implements SearchContext { abstract getCookie(name: string): Promise; abstract getAllCookies(): Promise; abstract clearCookies(): Promise; + abstract takeScreenshot(): Promise; abstract executeJavascript(script: string | ((...args: VarArgs) => T), ...args: VarArgs): Promise; abstract waitUntil(condition: (browserController: Omit) => boolean | Promise, timeout: number, pollingInterval: number): Promise diff --git a/@bellatrix/web/src/infrastructure/browsercontroller/playwright/PlaywrightBrowserController.ts b/@bellatrix/web/src/infrastructure/browsercontroller/playwright/PlaywrightBrowserController.ts index a827857..60b257b 100644 --- a/@bellatrix/web/src/infrastructure/browsercontroller/playwright/PlaywrightBrowserController.ts +++ b/@bellatrix/web/src/infrastructure/browsercontroller/playwright/PlaywrightBrowserController.ts @@ -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; @@ -48,6 +49,10 @@ export class PlaywrightBrowserController extends BrowserController { return await this._page.content(); } + override async takeScreenshot(): Promise { + return await Image.fromBase64((await this._page.screenshot()).toString('base64')); + } + override async back(): Promise { await this._page.goBack(); } diff --git a/@bellatrix/web/src/infrastructure/browsercontroller/selenium/SeleniumBrowserController.ts b/@bellatrix/web/src/infrastructure/browsercontroller/selenium/SeleniumBrowserController.ts index c59c263..bf5e55a 100644 --- a/@bellatrix/web/src/infrastructure/browsercontroller/selenium/SeleniumBrowserController.ts +++ b/@bellatrix/web/src/infrastructure/browsercontroller/selenium/SeleniumBrowserController.ts @@ -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; @@ -32,6 +33,11 @@ export class SeleniumBrowserController extends BrowserController { return await this.wrappedDriver.getPageSource(); } + override async takeScreenshot(): Promise { + const base64image = (await this.wrappedDriver.takeScreenshot()); + return Image.fromBase64(base64image); + } + override async back(): Promise { await this.wrappedDriver.navigate().back(); } diff --git a/@bellatrix/web/src/services/BrowserService.ts b/@bellatrix/web/src/services/BrowserService.ts index 4fd1623..e26a4e2 100644 --- a/@bellatrix/web/src/services/BrowserService.ts +++ b/@bellatrix/web/src/services/BrowserService.ts @@ -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 @@ -21,6 +22,11 @@ export class BrowserService extends WebService { return await this.driver.getPageSource(); } + async takeScreenshot(): Promise { + const base64image = (await this.driver.takeScreenshot()).base64; + return Image.fromBase64(base64image); + } + async back(): Promise { return await this.driver.back(); } diff --git a/example/bellatrix.config.ts b/example/bellatrix.config.ts index 9f0d7bb..ca7dfaf 100644 --- a/example/bellatrix.config.ts +++ b/example/bellatrix.config.ts @@ -43,6 +43,11 @@ const config: BellatrixConfigurationOverride = { browserName: 'chrome' } } + }, + screenshotOnFailPluginSettings: { + isPluginEnabled: true, + outputPath: `./reports/screenshots${Date.now()}`, + shouldCreateFolderPerSuite: false, } }; diff --git a/example/tests/ProductPurchaseTests.test.ts b/example/tests/ProductPurchaseTests.test.ts index 5112ed1..5334735 100644 --- a/example/tests/ProductPurchaseTests.test.ts +++ b/example/tests/ProductPurchaseTests.test.ts @@ -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'; @@ -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}`)); }