DigitalConsumer.FixMyGlass/playwright-tests/pages/BasePage.ts
2026-02-19 12:23:49 -05:00

269 lines
No EOL
12 KiB
TypeScript

import { ICustomerDetails } from 'safelite-playwright-core';
import { Soft } from 'safelite-playwright-core';
import { waitUntil } from 'safelite-playwright-core';
import test, { expect, type Locator, type Page } from '@playwright/test';
import { error } from 'console';
import { ITestData } from 'framework/TestData';
/**
* Base class for all page objects.
* Provides common functionality for interacting with pages.
*/
export class BasePage {
// Common locators
readonly page: Page;
readonly continueButton: Locator;
readonly backButton: Locator;
readonly pageSpinner: Locator;
readonly buttonLoadSpin: Locator;
readonly hamburgerMenu: Locator;
readonly progressBar: Locator;
readonly loaders: Locator;
constructor(page: Page) {
this.page = page;
this.continueButton = page.locator('[id="infoBox"]').getByRole('button');
this.backButton = page.locator('[id="infoBox"]').getByRole('link');
this.pageSpinner = page.getByRole('status');
this.buttonLoadSpin = page.getByRole('alert');
this.hamburgerMenu = this.page.getByRole('button', { name: 'Hamburger Menu (modal window)' });
this.progressBar = this.page.locator('.progress-bar-outer .progress-bar-inner');
this.loaders = this.page.locator('.spinner-border, .loader');
}
async nextPage() {
await waitUntil(async () => {
return (await this.continueButton.getAttribute('aria-disabled')) !== 'true'
});
await this.continueButton.click();
await waitUntil(async () => {
let loaderElements = await this.page.locator('button .loader, .buy-loader, timeout, .modal-loader').all();
return !(await Promise.any(loaderElements.map(el => el.isVisible())).catch(() => false));
});
}
async previousPage() {
const startingUrl = this.page.url();
await expect(async () => {
const currentUrl = this.page.url();
if (currentUrl === startingUrl) {
await this.backButton.click({ timeout: 1000 });
}
expect(currentUrl).not.toEqual(startingUrl);
}).toPass({ timeout: 240_000 });
}
async fillAndValidate(element: Locator, value: string) {
await expect(async () => {
var text = await element.textContent();
if (text !== value) {
await element.clear();
await element.fill(value);
}
await expect(element).toHaveValue(value);
}).toPass();
}
async clickWithRetry(element, page) {
const timeout = 5000; // milli seconds
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
if (await element.isEnabled()) {
await element.click();
await element.keyboard.press('Tab');
return; // Exit loop if click succeeds
}
} catch (error) {
// Ignore error and retry
}
await page.waitForTimeout(100); // Small delay before retrying
}
console.log(`Failed to click the the element within ${timeout / 1000} seconds` + error);
}
async logReferralNumber() {
let mainLocalStorage = JSON.parse(await this.page.evaluate('localStorage.getItem(\'main\')'));
let referralNumber = mainLocalStorage.order.referralNumber as number;
let referralSequenceNumber = mainLocalStorage.order.referralSequenceNumber as number;
if (referralNumber == null) {
for (let i = 1; i <= 20; i++) {
if (!referralNumber == null) break;
await this.page.waitForTimeout(500);
mainLocalStorage = JSON.parse(await this.page.evaluate('localStorage.getItem(\'main\')'));
referralNumber = mainLocalStorage.order.referralNumber as number;
referralSequenceNumber = mainLocalStorage.order.referralSequenceNumber as number;
}
}
await test.step(`Referral Number:${referralNumber} Referral Sequence Number:${referralSequenceNumber}`, async () => {
console.log(`Referral Number:${referralNumber}`);
console.log(`Referral Sequence Number:${referralSequenceNumber}`);
});
}
async mockScheduleResponseForEarlyBird(customerDetails: ICustomerDetails) {
const apiUrl = `https://digitalapi.${process.env['PLAYWRIGHT_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/schedule/api/v1/schedule/mobile-time-slots`;
await this.page.route(apiUrl, async (route) => {
const currentDate = new Date().toISOString().split('T')[0]; // e.g., "2025-07-23"
if (route.request().postDataJSON().startDate === currentDate) {
const response = await route.fetch();
const responseBody = await response.json();
responseBody.days.forEach((day: any) => {
day.timeSlots.forEach((slot: any) => {
if (slot.id.includes("AM")) {
slot.offerPremium = true;
}
});
});
customerDetails.apptDate = responseBody.days.find((day: any) => day.timeSlots.some((slot: any) => slot.offerPremium === true)).date || undefined;
// Mock the response
await route.fulfill({
response,
body: JSON.stringify(responseBody),
});
}
});
}
async mockScheduleResponseForFirstInshopCallNoSchedule(customerDetails: ICustomerDetails) {
let fistInshopScheduleCall = true;
const apiUrl = `https://digitalapi.${process.env['PLAYWRIGHT_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/schedule/api/v1/schedule/shop-time-slots`;
await this.page.route(apiUrl, async (route) => {
if (!fistInshopScheduleCall) {
return route.continue();
}
// const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
// const startDate = new Date();
//const beginningOfWeek = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() - startDate.getDay());
// const endDate = new Date(beginningOfWeek.getFullYear(), beginningOfWeek.getMonth(), beginningOfWeek.getDate() + 13);
// e.g., "2025-07-23"
/*if (route.request().postDataJSON().startDate === fmt(startDate) && route.request().postDataJSON().endDate === fmt(endDate)) {
const response = await route.fetch();
const responseBody = await response.json();
responseBody.days = [];
// Mock the response
await route.fulfill({
response,
body: JSON.stringify(responseBody),
});
}*/
const response = await route.fetch();
const responseBody = await response.json();
responseBody.days = [];
// Mock the response
await route.fulfill({
response,
body: JSON.stringify(responseBody),
});
fistInshopScheduleCall = false;
});
}
async getRepairPartsTotal(testData: Partial<ITestData>) {
const apiUrl = `https://digitalapi.${process.env['PLAYWRIGHT_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/price/api/v1/price/order-items`;
await this.page.on('response', async (response) => {
const orderItemRequest = response.request();
if (response.request().url() === apiUrl && orderItemRequest.postDataJSON().lineItems.some(item => item.partNumber === "WSREPAIR")) {
const responseBody = await response.json();
const targetParts = ['WSREPAIR']; // ["SUPPLIES-REPAIR", "WSREPAIR"];
testData.totalAmount = responseBody.lineItems
.filter(item => targetParts.includes(item.partNumber))
.reduce((sum: number, item: any) => {
return sum + item.laborAmount + item.sellingPrice + item.kitPrice;
}, 0).toFixed(2);
}
});
testData.totalAmount! > 0 ? console.log(`Total Amount: ${testData.totalAmount}`) : console.error('No repair parts found in the order items.');
}
async validateProgressBar(progressPercentage: string, timeout: number = 60000) {
// This section has been commented out until progress bar work is completed for parity.
await waitUntil(async () => {
let loaderElements = await this.page.locator('button .loader, .buy-loader, timeout, .modal-loader').all();
return !(await Promise.any(loaderElements.map(el => el.isVisible())).catch(() => false));
});
// Take a screenshot of the page before validating the progress bar
await this.page.screenshot({ path: `test-results\\ortoni-data\\progress-bar-${Date.now()}.png`, fullPage: true });
// Wait until the progress bar element is attached and visible
const actualProgressPercentage = await this.progressBar.evaluate(
async (element) => {
await new Promise(resolve => setTimeout(resolve, 200));
return element.style.width || "Not Found";
}
);
Soft.expect(actualProgressPercentage).toBe(progressPercentage);
console.log(`Progress Bar Percentage: Actual - ${actualProgressPercentage} vs Expected - ${progressPercentage}`);
}
async waitForPageOrComponentload(): Promise<void> {
await Promise.all([
(async () => {
const loaderCount = await this.loaders.count();
if (loaderCount > 0) {
for (let i = 0; i < loaderCount; i++) {
await this.loaders.nth(i).waitFor({ state: 'hidden' });
}
}
})(),
this.page.waitForLoadState('load'),
this.page.waitForLoadState('networkidle')
]);
}
async validateOEMPart(isOEM: boolean): Promise<void> {
if (isOEM!) {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate the presence of an OEM part
if (vuexState.order?.lineItems?.glassParts?.length > 0) {
const firstGlassPartNumber = vuexState.order.lineItems.glassParts[0].partNumber;
await expect(firstGlassPartNumber.includes("OEM")).toBe(true);
} else {
throw new Error("No glass parts found in the order");
}
}
}
async buildExperimentUrl(testData: Partial<ITestData>): Promise<string> {
let experimentsURLExtension = "";
const { experiments } = testData;
if (experiments !== undefined) {
experimentsURLExtension += "?cns=all&experiments="
experimentsURLExtension += experiments?.isMobileFirst
? "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_TEST=true"
: "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_CONTROL=true";
experimentsURLExtension += experiments?.isAdyenPayments
? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)"
: ",Adyen%20Payments=Adyen%20Payment%20Test=CyberSource%20(Control)";
} else {
console.log("Url extension without query string");
}
// Convert to HTML encoding before returning
return experimentsURLExtension;
}
}