import LoggingUtils from '@impl/utils/LoggingUtils'; import { Page } from '@playwright/test'; import { TestInfo } from '@business-logic/types/Test'; import { TestInfoError } from "@playwright/test"; import { DateTime } from 'luxon'; import { expect as pw_expect } from '@playwright/test'; export default class Soft { private static _instance: Soft | null = null; private testInfo: TestInfo; private page: Page; private failedAssertions: string[] = []; private errorCounter: number = 0; private errorsOnly: boolean = false; private constructor(testInfo: TestInfo, page: Page) { this.testInfo = testInfo; this.page = page; } public static initialize(testInfo: TestInfo, page: Page): void { Soft._instance = new Soft(testInfo, page); } public static setOptions(options: { errorsOnly: boolean }): void { Soft.getInstance().errorsOnly = options.errorsOnly; } public static getOptions(): { errorsOnly: boolean } { return { errorsOnly: Soft.getInstance().errorsOnly }; } private static getInstance(): Soft { if (!Soft._instance) throw new Error("Soft is not initialized. Call Soft.initialize(testInfo, page) first!"); return Soft._instance; } public async handleAssertion( matcherFull: string, matcherDisplay: string, matcherFunction: () => Promise, reason?: string): Promise { reason = reason ? reason : '' const reasonText = reason ? `'${reason}' ` : ''; try { await matcherFunction(); if (!this.errorsOnly) console.log(LoggingUtils.logValidate(`Validation ${reasonText}passed: ${matcherDisplay}!`, true)); } catch (error) { console.log(LoggingUtils.logValidate(`Validation ${reasonText}failed: ${matcherDisplay}!`, false)); //const errorMessage = error instanceof Error ? error.message : String(error); this.failedAssertions.push(`\n${++this.errorCounter}_Validation ${reasonText}failed:\n${LoggingUtils.replaceEmptyLinesWithMiddleDot(matcherFull)}!\n${error.stack}\n`); // NOTE: I find it more helpful for the stack trace to be included here, so we know which line the validation is failing try { const screenshot: Buffer = await this.page.screenshot({ fullPage: true }); const name: string = LoggingUtils.sanitizeFileName(`${this.errorCounter}_Validation_${reason}${DateTime.now().valueOf()}`); await this.testInfo.attach(name, { body: screenshot, contentType: 'image/png' }); } catch(err){ // ohwell console.error(error) } } } public static expect(value: any, reason?: string): ExpectationChain { return new ExpectationChain(value, Soft.getInstance(), reason); } public static getFailedAssertions(): string[] { return Soft.getInstance().failedAssertions; } public static hasFailedAssertions(): boolean { return Soft.getInstance().failedAssertions.length > 0; } public static getFailureCount(): number { return Soft.getInstance().failedAssertions.length; } public static clearFailedAssertions(): void { Soft.getInstance().failedAssertions = []; } } export class SoftError implements TestInfoError { public readonly message?: string | undefined; constructor(msg: string) { this.message = msg; } } export class ExpectationChain { constructor(private value: any, private soft: Soft, private reason?: string) { } private formatValue(value: any): string { return LoggingUtils.truncateString(value); } public async toBe(expected: any): Promise { await this.soft.handleAssertion( `expect(${this.value}).toBe(${expected})`, `expect(${this.formatValue(this.value)}).toBe(${this.formatValue(expected)})`, async () => await pw_expect(this.value).toBe(expected), this.reason ); return this; } public async toEqual(expected: any): Promise { await this.soft.handleAssertion( `expect(${JSON.stringify(this.value)}).toEqual(${expected})`, `expect(${this.formatValue(JSON.stringify(this.value))}).toEqual(${this.formatValue(expected)})`, async () => await pw_expect(this.value).toEqual(expected), this.reason ); return this; } public async toContain(expected: any): Promise { await this.soft.handleAssertion( `expect(${this.value}).toContain(${expected})`, `expect(${this.formatValue(this.value)}).toContain(${this.formatValue(expected)})`, async () => await pw_expect(this.value).toContain(expected), this.reason ); return this; } public async toHaveText(expected: string): Promise { await this.soft.handleAssertion( `expect(${this.value}).toHaveText(${expected})`, `expect(${this.formatValue(this.value)}).toHaveText(${this.formatValue(expected)})`, async () => { if (typeof this.value.textContent !== 'function') { throw new Error('value does not have a textContent method'); } const text = await this.value.textContent(); await pw_expect(text).toHaveText(expected); }, this.reason ); return this; } public async toBeGreaterThan(expected: number): Promise { await this.soft.handleAssertion( `expect(${this.value}).toBeGreaterThan(${expected})`, `expect(${this.formatValue(this.value)}).toBeGreaterThan(${this.formatValue(expected)})`, async () => await pw_expect(this.value).toBeGreaterThan(expected), this.reason ); return this; } public async toBeTruthy(): Promise { await this.soft.handleAssertion( `expect(${this.value}).toBeTruthy`, `expect(${this.formatValue(this.value)}).toBeTruthy`, async () => await pw_expect(this.value).toBeTruthy(), this.reason ); return this; } public async toBeFalsy(): Promise { await this.soft.handleAssertion( `expect(${this.value}).toBeFalsy`, `expect(${this.formatValue(this.value)}).toBeFalsy`, async () => await pw_expect(this.value).toBeFalsy(), this.reason ); return this; } }