@tshifhiwa/ohrm-ui-automation-framework
Version:
Playwright and TypeScript–based test automation framework for validating core UI features and workflows of the OrangeHRM demo application.
265 lines (217 loc) • 8.38 kB
text/typescript
import DataSanitizer from "../../sanitization/dataSanitizer.js";
import { ErrorCacheManager } from "./errorCacheManager.js";
import { RegexPatterns } from "./regexPatterns.js";
import type { ErrorDetails, MatcherResult, MatcherError } from "./error-handler.types.js";
export default class ErrorAnalyzer {
private static readonly MESSAGE_PROPS = ["message"];
private static readonly MEANINGLESS_PROPS = new Set([
"name",
"stack",
"constructor",
"toString",
"valueOf",
]);
public static createErrorDetails(error: unknown, source: string, context = ""): ErrorDetails {
if (!error) {
return this.createEmptyErrorDetails(source, context);
}
const details: ErrorDetails = {
source,
context,
message: this.getErrorMessage(error),
timestamp: new Date().toISOString(),
environment: process.env.ENV || "dev",
};
// Add stack trace to main error details
if (error instanceof Error && error.stack) {
details.stack = error.stack;
} else if (this.isErrorObject(error) && "stack" in error) {
const stack = error.stack;
if (typeof stack === "string") {
details.stack = stack;
}
}
// Add error name/type
if (error instanceof Error) {
details.errorType = error.constructor.name;
} else if (this.isErrorObject(error) && "name" in error) {
const name = error.name;
if (typeof name === "string") {
details.errorType = name;
}
}
return details;
}
public static getErrorMessage(error: unknown): string {
if (!error) return "";
if (error instanceof Error) {
return ErrorCacheManager.getSanitizedMessage(error.message);
}
if (typeof error === "string") {
return ErrorCacheManager.getSanitizedMessage(error);
}
if (this.isErrorObject(error)) {
return this.handleObjectError(error);
}
return String(error);
}
public static extractAdditionalErrorDetails(error: unknown): Record<string, unknown> {
if (!this.isErrorObject(error)) return {};
// Start with base details
const details: Record<string, unknown> = {};
// Check for matcher error first
if (this.isMatcherError(error)) {
Object.assign(details, this.extractMatcherDetails(error.matcherResult));
}
// Check for Jest matcher details
const matcherDetails = this.extractJestMatcherDetails(error);
if (Object.keys(matcherDetails).length > 0) {
Object.assign(details, matcherDetails);
}
// Add Playwright-specific details
const playwrightDetails = this.extractPlaywrightDetails(error);
if (Object.keys(playwrightDetails).length > 0) {
Object.assign(details, playwrightDetails);
}
// Get sanitized error object
const sanitizedDetails = DataSanitizer.sanitizeErrorObject(error);
const filtered = this.filterMeaninglessProperties(sanitizedDetails);
// Merge without overwriting existing details
for (const [key, value] of Object.entries(filtered)) {
if (!(key in details)) {
details[key] = value;
}
}
return details;
}
private static extractPlaywrightDetails(error: Record<string, unknown>): Record<string, unknown> {
const details: Record<string, unknown> = {};
// Common Playwright error properties
const playwrightProps = [
"timeout",
"selector",
"locator",
"frame",
"page",
"action",
"retries",
"logs",
] as const;
for (const prop of playwrightProps) {
if (prop in error && error[prop] !== undefined) {
details[prop] = error[prop];
}
}
return details;
}
private static extractJestMatcherDetails(
error: Record<string, unknown>,
): Record<string, unknown> {
const details: Record<string, unknown> = {};
const { expected, actual, received, matcherName, pass, diff, operator } = error;
if (expected !== undefined) details.expected = expected;
if (actual !== undefined) details.received = actual;
else if (received !== undefined) details.received = received;
if (matcherName !== undefined) details.matcher = matcherName;
if (typeof pass === "boolean") details.pass = pass;
if (diff !== undefined) details.diff = diff;
if (operator !== undefined) details.operator = operator;
// Return only if we found meaningful details
return expected !== undefined ||
actual !== undefined ||
received !== undefined ||
operator !== undefined
? details
: {};
}
private static extractMatcherDetails(matcher: MatcherResult): Record<string, unknown> {
const details: Record<string, unknown> = { pass: matcher.pass };
const parsedValues = this.parsePlaywrightMessage(matcher.message);
if (parsedValues.expected !== undefined) details.expected = parsedValues.expected;
if (parsedValues.received !== undefined) details.received = parsedValues.received;
details.message = ErrorCacheManager.getSanitizedMessage(matcher.message);
// Include full original message for debugging
details.fullMessage = matcher.message;
return details;
}
private static parsePlaywrightMessage(message: string): { expected?: string; received?: string } {
const cleanMessage = message.replace(RegexPatterns.ANSI_ESCAPE, "");
const result: { expected?: string; received?: string } = {};
const expectedMatch = cleanMessage.match(RegexPatterns.EXPECTED_MATCH);
if (expectedMatch) {
result.expected = (expectedMatch[1] || expectedMatch[2] || expectedMatch[3])?.trim() || "";
}
const receivedMatch = cleanMessage.match(RegexPatterns.RECEIVED_MATCH);
if (receivedMatch) {
result.received = (receivedMatch[1] || receivedMatch[2] || receivedMatch[3])?.trim() || "";
}
return result;
}
private static isMatcherError(error: unknown): error is MatcherError {
return (
this.hasProperty(error, "matcherResult") && this.isValidMatcherResult(error.matcherResult)
);
}
private static isValidMatcherResult(matcherResult: unknown): matcherResult is MatcherResult {
return (
typeof matcherResult === "object" &&
matcherResult !== null &&
this.hasProperty(matcherResult, "message") &&
this.hasProperty(matcherResult, "pass") &&
typeof matcherResult.message === "string" &&
typeof matcherResult.pass === "boolean"
);
}
private static hasProperty<T extends PropertyKey>(
obj: unknown,
prop: T,
): obj is Record<T, unknown> {
return typeof obj === "object" && obj !== null && prop in obj;
}
private static createEmptyErrorDetails(source: string, context: string): ErrorDetails {
return {
source,
context,
message: "Unknown error",
timestamp: new Date().toISOString(),
environment: process.env.ENV || "dev",
};
}
private static filterMeaninglessProperties(
details: Record<string, unknown>,
): Record<string, unknown> {
const filtered: Record<string, unknown> = {};
for (const [key, value] of Object.entries(details)) {
if (
this.MEANINGLESS_PROPS.has(key) ||
(key === "name" && value === "Error") ||
value == null
) {
continue;
}
filtered[key] = value;
}
return filtered;
}
private static isErrorObject(error: unknown): error is Record<string, unknown> {
return error !== null && typeof error === "object";
}
private static handleObjectError(error: Record<string, unknown>): string {
// Try common message properties first
for (const prop of this.MESSAGE_PROPS) {
const value = error[prop];
if (typeof value === "string" && value.trim()) {
return ErrorCacheManager.getSanitizedMessage(value);
}
}
return this.stringifyErrorObject(error);
}
private static stringifyErrorObject(errorObj: Record<string, unknown>): string {
try {
const stringified = JSON.stringify(errorObj);
return stringified === "{}" ? "Empty object" : stringified;
} catch {
return "Object with circular references";
}
}
}