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.
342 lines
No EOL
16 KiB
TypeScript
342 lines
No EOL
16 KiB
TypeScript
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<ITestData>) {
|
|
// 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<string> {
|
|
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<string> {
|
|
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<void> {
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
} |