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) { 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 { 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 { 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"); } } } }