DigitalConsumer.FixMyGlass/playwright-tests/business-logic/validations/Soft.ts
maguire-arman 404dda6556 Adds Playwright test framework for FMG
Initial commit of the Playwright test framework, including:
- Configuration files for Playwright, Docker, and Sauce Labs
- Test case structure and page object models
- CI/CD pipeline configuration
- Utilities and business logic implementations

This framework will be used for end-to-end testing of the FMG application.
2025-04-07 15:40:31 -04:00

179 lines
No EOL
6.7 KiB
TypeScript

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<void>,
reason?: string): Promise<void> {
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<ExpectationChain> {
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<ExpectationChain> {
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<ExpectationChain> {
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<ExpectationChain> {
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<ExpectationChain> {
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<ExpectationChain> {
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<ExpectationChain> {
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;
}
}