import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IPaymentDetails } from '@business-logic/types/CustomerDetails'; import { PaymentMethod, PaymentType, ServicePackage, VehicleDamage } from '@business-logic/types/Enums'; import { PaymentPage } from './PaymentPage'; import { AfterpayPage } from './AfterpayPage'; import { PaypalPage } from './PaypalPage'; import { ITestData } from '@business-logic/types/ITestData'; export class PaymentMethodPage extends BasePage { readonly page: Page; readonly payAtServiceButton: Locator; // readonly paypalButton: Locator; // readonly creditCardButton: Locator; readonly payNowButton: Locator; readonly payWithInsuranceButton: Locator; readonly payInFourButton: Locator; readonly amountDueTextField: Locator; readonly amountDueDropDown: Locator; readonly appointmentDetailsDropdown: Locator; readonly subtotalAmountTextField: Locator; readonly submitButton: Locator; readonly recalibrationCheckbox: Locator; readonly paymentPage: PaymentPage; readonly paypalPage: PaypalPage; // Payment detail page validation locators readonly reviewTable: Locator; readonly vehicleSection: Locator; readonly damageSection: Locator; readonly serviceDetailsSection: Locator; readonly serviceLocationSection: Locator; readonly appointmentDateSection: Locator; readonly contactDetailsSection: Locator; readonly cartPanelDetails: Locator; readonly subtotalText: Locator; readonly finalAmountDueText: Locator; readonly glassServiceText: Locator; readonly wiperBladesText: Locator; readonly rainDefenseText: Locator; readonly deductibleText: Locator; url = process.env['BASE_URL']! + '/fmg/?fmgPage=payment-method'; constructor(page: Page) { super(page); this.page = page; this.payAtServiceButton = this.page.locator('[buttonlabel="Pay at my appointment"]'); //this.page.getByText('Pay at time of service'); this.amountDueTextField = this.page.getByLabel('expand cart').locator('.amount-due'); this.amountDueDropDown = this.page.getByLabel('expand cart'); this.appointmentDetailsDropdown = this.page.getByLabel('expand appointment details'); this.payWithInsuranceButton = this.page.locator('div').filter({ hasText: /^Pay with insurance$/ }).nth(1); this.subtotalAmountTextField = this.page.locator('.sub-total span').nth(1); this.payNowButton = this.page.locator('[buttonlabel="Pay now"]'); this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]'); this.submitButton = this.page.locator('[data-test-id="nav-bar-main-button"]'); this.recalibrationCheckbox = this.page.getByLabel('I understand after windshield'); // this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1); this.paymentPage = new PaymentPage(page); this.paypalPage = new PaypalPage(page); // Payment details validation locators this.reviewTable = this.page.locator('div.review-table'); // Section locators - find by heading text this.vehicleSection = this.page.locator('.review-table').locator('div', { hasText: 'Vehicle' }).first(); this.damageSection = this.page.locator('.review-table').locator('div', { hasText: 'Damage' }).first(); this.serviceDetailsSection = this.page.locator('.review-table').locator('div', { hasText: 'Glass service only' }).first(); this.serviceLocationSection = this.page.locator('.review-table').locator('div', { hasText: "We're coming to you" }).first(); this.appointmentDateSection = this.page.locator('.review-table').locator('div', { hasText: 'Appointment date + time' }).first(); this.contactDetailsSection = this.page.locator('.review-table').locator('div', { hasText: 'Contact details' }).first(); // Cart panel elements this.cartPanelDetails = this.page.locator('.cart-panel'); this.subtotalText = this.page.locator('.sub-total'); this.finalAmountDueText = this.page.locator('div.amount-due'); this.glassServiceText = this.page.locator('div', { hasText: /^Glass service only$/ }); this.wiperBladesText = this.page.locator('div', { hasText: /^New wiper blades$/ }); this.rainDefenseText = this.page.locator('div', { hasText: /^Rain Defenseā„¢$/ }); this.deductibleText = this.page.locator('#deductible-value'); } async validatePaymentDetailsPage(testData: Partial) { // Destructure data we use const { vehicleDetails, customerDetails, servicePackage, promoCode, isPolicyFound, claimDetails, paymentDetails, appointmentDetails, isUseVehicleOnPolicy, paymentMethod, vehicleDamage } = testData; // Wait for review table to be visible to ensure page is loaded await this.appointmentDetailsDropdown.waitFor({state: "visible"}); await this.appointmentDetailsDropdown.click(); // Expand cart to see all details await this.amountDueDropDown.click(); await this.reviewTable.waitFor({ state: "visible" }); // Helper method to get content lines from a section const getSectionContent = async (section: Locator) => { // First make sure section exists if (await section.count() === 0) return []; // Find all content lines within this section const contentLines = await section.locator('div.small.review-block-content').allInnerTexts(); return contentLines; }; // Validate vehicle information const vehicleContent = await getSectionContent(this.vehicleSection); if (vehicleContent.length > 0) { const vehicleText = vehicleContent[0]; expect.soft(vehicleText).toContain(`${vehicleDetails?.year} ${vehicleDetails?.make} ${vehicleDetails?.model}`); } // Validate damage type // TODO: Work out logic on how to verify vehicle Damage in payment details with vehicleDamage // const damageContent = await getSectionContent(this.damageSection); // if (vehicleDamage && damageContent.length > 0) { // const damageText = damageContent[0]; // // For each damage type in the array, check if its display text is in the damage content // for (const damage of vehicleDamage) { // const expectedDamageText = this.getDamageDisplayText(damage); // // If this is a single damage item, it should match exactly // if (vehicleDamage.length === 1) { // expect.soft(damageText).toContain(expectedDamageText); // } else { // // For multiple damages, check if any of the damage content lines contain this damage type // const damageFound = damageContent.some(content => // content.includes(expectedDamageText) // ); // expect.soft(damageFound).toBeTruthy(); // } // } // } // Validate service details based on package const serviceContent = await getSectionContent(this.serviceDetailsSection); if (servicePackage && serviceContent.length > 0) { //TODO: Add service package validation // move over logic from Kishan's code // Additional validations for Standard and Premium packages if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) { await expect.soft(this.wiperBladesText).toBeVisible(); } if (servicePackage === ServicePackage.Premium) { await expect.soft(this.rainDefenseText).toBeVisible(); } } // Validate service location const locationContent = await getSectionContent(this.serviceLocationSection); if (appointmentDetails?.serviceLocation && locationContent.length > 0) { // Find the title element of the service location section const serviceLocationTitle = this.serviceLocationSection.locator('span').first(); const serviceLocationValue = await serviceLocationTitle.innerText(); // Check for mobile/inshop service wording if (appointmentDetails.serviceLocation.toString().includes('Mobile')) { expect.soft(serviceLocationValue).toContain("We're coming to you"); } else if (appointmentDetails.serviceLocation.toString().includes('InShop')) { expect.soft(serviceLocationValue).toContain("Bring to shop"); } // Validate address if available if (appointmentDetails.serviceAddress && locationContent.length > 0) { const addressText = locationContent[0].toLowerCase(); expect.soft(addressText).toContain(appointmentDetails.serviceAddress.street.toLowerCase()); } } // Validate appointment date/time const appointmentContent = await getSectionContent(this.appointmentDateSection); if (customerDetails?.apptDate && appointmentContent.length > 0) { const appointmentText = appointmentContent[0]; expect.soft(appointmentText).toContain(customerDetails.apptDate); } // Validate contact details const contactContent = await getSectionContent(this.contactDetailsSection); if (customerDetails && contactContent.length > 0) { const fullName = `${customerDetails.firstName} ${customerDetails.lastName}`; const email = customerDetails.email; const phone = customerDetails.phoneNumber; // Check if contact details are present const contactTextJoined = contactContent.join(' '); expect.soft(contactTextJoined).toContain(fullName); expect.soft(contactTextJoined).toContain(email); expect.soft(contactTextJoined).toContain(phone); } // Cart Validation // Get pricing information from cart panel if (await this.subtotalText.isVisible()) { const subtotalValue = await this.subtotalText.innerText(); const finalAmountDueValue = await this.finalAmountDueText.innerText(); // Pricing validations differ by payment method if (paymentMethod === PaymentMethod.SelfPay && paymentDetails?.paymentType !== PaymentType.PayWithInsurance) { // Extract amounts for self-pay customers const subtotalAmount = this.extractAmount(subtotalValue); const finalAmountDueAmount = this.extractAmount(finalAmountDueValue); // Subtotal should be greater than 0 expect.soft(subtotalAmount).toBeGreaterThan(0); // Final amount differs based on payment type if (paymentDetails?.paymentType === PaymentType.PayAtService) { expect.soft(finalAmountDueAmount).toBeGreaterThan(0); } else if (paymentDetails?.paymentType === PaymentType.Credit || paymentDetails?.paymentType === PaymentType.Paypal || paymentDetails?.paymentType === PaymentType.AfterPay) { // For payment types that charge immediately, amount due could be 0 // This logic might need adjusting based on actual business rules } } else { // For insurance payments, check for proper indicators // TODO: Apply logic for deductible and insurance payment logic } // Service package validations const servicePackageValue = await this.cartPanelDetails.textContent(); await expect.soft(this.cartPanelDetails).toContainText(`${servicePackage}`) if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) { expect.soft(servicePackageValue).toContain('New wiper blades'); } if (servicePackage === ServicePackage.Premium) { expect.soft(servicePackageValue).toContain('Rain Defenseā„¢'); } // Promo Code Validation if (promoCode) { expect.soft(servicePackageValue).toContain(`Promo code ${promoCode} applied`); }; } } /** * Helper method to extract numeric amount from price strings like "$123.45" */ private extractAmount(valueString: string): number { const matches = valueString.match(/\$([0-9,]+(\.[0-9]{2})?)/); if (matches && matches[1]) { return parseFloat(matches[1].replace(/,/g, '')); } return 0; } async getAmountDue(): Promise { await expect(this.amountDueTextField).toBeVisible(); const text = await this.amountDueTextField.textContent(); if (!text) throw new Error("Amount due text is empty"); return text; } async getSubtotal(): Promise { await this.amountDueDropDown.click(); await expect(this.subtotalAmountTextField).toBeVisible(); const text = await this.subtotalAmountTextField.textContent(); if (!text) throw new Error("Subtotal text is empty"); return text; } async executePayment(paymentDetails: IPaymentDetails, isRecalVehicle: boolean) { const browserContext = this.page.context(); switch(paymentDetails.paymentType) { case PaymentType.Credit: await this.selectCreditCard(); await this.nextPage(); await this.paymentPage.populateCreditCardDetails(paymentDetails); await this.nextPage(); break; case PaymentType.AfterPay: await this.payInFourButton.click(); await this.nextPage(); // Capture popup const afterpayPopup = await browserContext.waitForEvent('page'); const afterpayPage = new AfterpayPage(afterpayPopup); // Execute payment await afterpayPage.executeAfterpayPayment(paymentDetails); break; case PaymentType.Paypal: await this.selectPaypal(); // TODO: Click paypal button await this.nextPage(); await this.paymentPage.navigateToPaypalCheckout(); await this.paypalPage.completePaypalPurchase(paymentDetails); break; case PaymentType.PayAtService: await this.selectPayAtService(isRecalVehicle); await this.nextPage(); break; case PaymentType.PayWithInsurance: await this.payWithInsuranceButton.click(); await this.nextPage(); break; default: console.error('PaymentMethodPage >> Logic for this payment method unimplemented'); break; } } async selectPaypal(){ await this.payNowButton.click(); } l async selectCreditCard(){ await this.payNowButton.click(); } async selectPayAtService(isRecalVehicle: boolean){ if (await this.payAtServiceButton.isVisible()) { await this.payAtServiceButton.click(); } else if (isRecalVehicle) { await this.recalibrationCheckbox.click(); } } async verifyVAPS(): Promise { // Get Vuex state from localStorage const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); // Validate the count of FRONT WIPER parts if (vuexState.order?.lineItems?.vaps?.length > 0) { const frontWiperPartsCount = vuexState.order.lineItems.vaps.filter(vap => vap.partType === "FRONT WIPER" ).length; await expect(frontWiperPartsCount).toBe(2); } else { throw new Error("No VAPS found in the order"); } } }