diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..2b4da7cc --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +help: ## Display this help menu + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +check: + npm run build + +test: ## Build and copy to the dependencies directory for the project + rm dist -rf + npm run build + rm $(filter-out $@,$(MAKECMDGOALS))/tests/UI/node_modules/@prestashop-core/ui-testing/dist/ -r + cp dist $(filter-out $@,$(MAKECMDGOALS))/tests/UI/node_modules/@prestashop-core/ui-testing/ -r + cp package.json $(filter-out $@,$(MAKECMDGOALS))/tests/UI/node_modules/@prestashop-core/ui-testing/ + +.DEFAULT_GOAL := help \ No newline at end of file diff --git a/package.json b/package.json index e07323cc..aa82f4a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@prestashop-core/ui-testing", - "version": "0.0.6", + "version": "0.0.7", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/data/demo/countries.ts b/src/data/demo/countries.ts new file mode 100644 index 00000000..61d722dc --- /dev/null +++ b/src/data/demo/countries.ts @@ -0,0 +1,60 @@ +import CountryData from '@data/faker/country'; + +export default { + france: new CountryData({ + id: 8, + name: 'France', + isoCode: 'FR', + callPrefix: '33', + zone: 'Europe', + active: true, + }), + netherlands: new CountryData({ + id: 13, + name: 'Netherlands', + isoCode: 'NL', + callPrefix: '31', + zone: 'Europe', + active: false, + }), + unitedKingdom: new CountryData({ + id: 17, + name: 'United Kingdom', + isoCode: 'GB', + callPrefix: '44', + zone: 'Europe', + active: false, + }), + germany: new CountryData({ + id: 1, + name: 'Germany', + isoCode: 'DE', + callPrefix: '49', + zone: 'Europe', + active: false, + }), + afghanistan: new CountryData({ + id: 231, + name: 'Afghanistan', + isoCode: 'AF', + callPrefix: '93', + zone: 'Asia', + active: false, + }), + unitedStates: new CountryData({ + id: 21, + name: 'United States', + isoCode: 'US', + callPrefix: '1', + zone: 'North America', + active: false, + }), + canada: new CountryData({ + id: 4, + name: 'Canada', + isoCode: 'CA', + callPrefix: '1', + zone: 'North America', + active: false, + }), +}; diff --git a/src/data/demo/currencies.ts b/src/data/demo/currencies.ts new file mode 100644 index 00000000..a4881ea5 --- /dev/null +++ b/src/data/demo/currencies.ts @@ -0,0 +1,139 @@ +import CurrencyData from '@data/faker/currency'; + +export default { + euro: new CurrencyData({ + name: 'Euro', + frName: 'euro', + symbol: '€', + isoCode: 'EUR', + exchangeRate: 1, + decimals: 2, + enabled: true, + }), + mad: new CurrencyData({ + name: 'Moroccan Dirham', + frName: 'dirham marocain', + symbol: 'MAD', + isoCode: 'MAD', + exchangeRate: 10.560000, + decimals: 2, + enabled: true, + }), + toman: new CurrencyData({ + name: 'Iranian toman', + frName: 'Toman iranien', + symbol: 'TMN', + isoCode: 'TMN', + exchangeRate: 4666.539, + decimals: 2, + enabled: true, + }), + chileanPeso: new CurrencyData({ + name: 'Chilean Peso', + frName: 'peso chilien', + symbol: '$', + isoCode: 'CLP', + exchangeRate: 862.172868, + decimals: 0, + enabled: true, + }), + dzd: new CurrencyData({ + name: 'Algerian Dinar', + frName: 'dinar algérien', + symbol: 'DZD', + isoCode: 'DZD', + exchangeRate: 162.6, + decimals: 2, + enabled: true, + }), + tnd: new CurrencyData({ + name: 'Tunisian Dinar', + frName: 'dinar tunisien', + symbol: 'TND', + isoCode: 'TND', + exchangeRate: 3.29, + decimals: 3, + enabled: true, + }), + try: new CurrencyData({ + name: 'Turkish Lira', + frName: 'livre turque', + symbol: '₺', + isoCode: 'TRY', + exchangeRate: 9.08, + decimals: 2, + enabled: true, + }), + usd: new CurrencyData({ + name: 'US Dollar', + frName: 'dollar des États-Unis', + symbol: '$', + isoCode: 'USD', + exchangeRate: 1.22, + decimals: 2, + enabled: true, + }), + aed: new CurrencyData({ + name: 'United Arab Emirates Dirham', + frName: 'dirham des Émirats arabes unis', + symbol: 'AED', + isoCode: 'AED', + exchangeRate: 4.51, + decimals: 2, + enabled: true, + }), + lyd: new CurrencyData({ + name: 'Libyan Dinar', + frName: 'dinar libyen', + symbol: 'LYD', + isoCode: 'LYD', + exchangeRate: 5.45, + decimals: 3, + enabled: true, + }), + lsl: new CurrencyData({ + name: 'Lesotho Loti', + frName: 'loti lesothan', + symbol: 'LSL', + isoCode: 'LSL', + exchangeRate: 18.06, + decimals: 2, + enabled: true, + }), + all: new CurrencyData({ + name: 'Albanian Lek', + frName: 'lek albanais', + symbol: 'ALL', + isoCode: 'ALL', + exchangeRate: 123.38, + decimals: 0, + enabled: true, + }), + gbp: new CurrencyData({ + name: 'British Pound', + frName: 'livre sterling', + symbol: 'GBP', + isoCode: 'GBP', + exchangeRate: 0.87, + decimals: 2, + enabled: true, + }), + jpy: new CurrencyData({ + name: 'Japanese Yen', + frName: 'yen japonais', + symbol: 'JPY', + isoCode: 'JPY', + exchangeRate: 140.59, + decimals: 0, + enabled: true, + }), + pyg: new CurrencyData({ + name: 'Paraguayan Guarani', + frName: 'guaraní paraguayen', + symbol: 'PYG', + isoCode: 'PYG', + exchangeRate: 7942.22, + decimals: 0, + enabled: true, + }), +}; diff --git a/src/data/demo/orderStatuses.ts b/src/data/demo/orderStatuses.ts new file mode 100644 index 00000000..1c4dbcbc --- /dev/null +++ b/src/data/demo/orderStatuses.ts @@ -0,0 +1,100 @@ +import OrderStatusData from '@data/faker/orderStatus'; + +export default { + awaitingCheckPayment: new OrderStatusData({ + id: 1, + name: 'Awaiting check payment', + sendEmailOn: true, + deliveryOn: false, + invoiceOn: false, + emailTemplate: 'cheque', + }), + paymentAccepted: new OrderStatusData({ + id: 2, + name: 'Payment accepted', + sendEmailOn: true, + deliveryOn: false, + invoiceOn: true, + emailTemplate: 'payment', + }), + processingInProgress: new OrderStatusData({ + id: 3, + name: 'Processing in progress', + sendEmailOn: true, + deliveryOn: true, + invoiceOn: true, + emailTemplate: 'preparation', + }), + shipped: new OrderStatusData({ + id: 4, + name: 'Shipped', + sendEmailOn: true, + deliveryOn: true, + invoiceOn: true, + emailTemplate: 'shipped', + }), + delivered: new OrderStatusData({ + id: 5, + name: 'Delivered', + sendEmailOn: false, + deliveryOn: true, + invoiceOn: true, + emailTemplate: '', + }), + canceled: new OrderStatusData({ + id: 6, + name: 'Canceled', + sendEmailOn: true, + deliveryOn: false, + invoiceOn: false, + emailTemplate: 'order_canceled', + }), + refunded: new OrderStatusData({ + id: 7, + name: 'Refunded', + sendEmailOn: true, + deliveryOn: false, + invoiceOn: true, + emailTemplate: 'refund', + }), + paymentError: new OrderStatusData({ + id: 8, + name: 'Payment error', + sendEmailOn: true, + deliveryOn: false, + invoiceOn: false, + emailTemplate: 'payment_error', + }), + awaitingBankWire: new OrderStatusData({ + id: 10, + name: 'Awaiting bank wire payment', + sendEmailOn: true, + deliveryOn: false, + invoiceOn: false, + emailTemplate: 'bankwire', + }), + awaitingCashOnDelivery: new OrderStatusData({ + id: 13, + name: 'Awaiting Cash On Delivery validation', + sendEmailOn: false, + deliveryOn: false, + invoiceOn: false, + emailTemplate: 'cashondelivery', + }), + onBackorderNotPaid: new OrderStatusData({ + id: 12, + name: 'On backorder (not paid)', + sendEmailOn: true, + deliveryOn: false, + invoiceOn: false, + emailTemplate: 'outofstock', + }), + onBackorderPaid: new OrderStatusData({ + id: 9, + name: 'On backorder (paid)', + sendEmailOn: true, + deliveryOn: false, + invoiceOn: true, + emailTemplate: 'outofstock', + }), +}; diff --git a/src/data/demo/paymentMethods.ts b/src/data/demo/paymentMethods.ts new file mode 100644 index 00000000..df04e020 --- /dev/null +++ b/src/data/demo/paymentMethods.ts @@ -0,0 +1,19 @@ +import PaymentMethodData from '@data/faker/paymentMethod'; + +export default { + cashOnDelivery: new PaymentMethodData({ + moduleName: 'ps_cashondelivery', + name: 'Cash on delivery (COD)', + displayName: 'Cash on delivery (COD)', + }), + checkPayment: new PaymentMethodData({ + moduleName: 'ps_checkpayment', + name: 'Payment by check', + displayName: 'Payments by check', + }), + wirePayment: new PaymentMethodData({ + moduleName: 'ps_wirepayment', + name: 'Bank wire', + displayName: 'Bank transfer', + }), +}; diff --git a/src/data/demo/states.ts b/src/data/demo/states.ts new file mode 100644 index 00000000..b89894e1 --- /dev/null +++ b/src/data/demo/states.ts @@ -0,0 +1,28 @@ +import StateData from '@data/faker/state'; + +export default { + california: new StateData({ + id: 8, + name: 'California', + isoCode: 'CA', + country: 'United States', + zone: 'North America', + status: true, + }), + bari: new StateData({ + id: 134, + name: 'Bari', + isoCode: 'BA', + country: 'Italy', + zone: 'Europe', + status: true, + }), + bihar: new StateData({ + id: 8, + name: 'Bihar', + isoCode: 'BR', + country: 'India', + zone: 'Asia', + status: true, + }), +}; diff --git a/src/data/demo/tax.ts b/src/data/demo/tax.ts new file mode 100644 index 00000000..8ed04be2 --- /dev/null +++ b/src/data/demo/tax.ts @@ -0,0 +1,16 @@ +import TaxData from '@data/faker/tax'; + +export default { + DefaultFrTax: new TaxData({ + id: 1, + name: 'TVA FR 20%', + rate: '20', + enabled: true, + }), + VatUkTax: new TaxData({ + id: 15, + name: 'VAT UK 20%', + rate: '20', + enabled: true, + }), +}; diff --git a/src/data/demo/taxRule.ts b/src/data/demo/taxRule.ts new file mode 100644 index 00000000..1983cd34 --- /dev/null +++ b/src/data/demo/taxRule.ts @@ -0,0 +1,24 @@ +import TaxRuleData from '@data/faker/taxRule'; + +export default [ + new TaxRuleData({ + id: 1, + name: 'FR Taux standard (20%)', + }), + new TaxRuleData({ + id: 2, + name: 'FR Taux réduit (10%)', + }), + new TaxRuleData({ + id: 3, + name: 'FR Taux réduit (5.5%)', + }), + new TaxRuleData({ + id: 4, + name: 'FR Taux super réduit (2.1%)', + }), + new TaxRuleData({ + id: 5, + name: 'EU VAT For Virtual Products', + }), +]; diff --git a/src/data/demo/taxRuleBehaviour.ts b/src/data/demo/taxRuleBehaviour.ts new file mode 100644 index 00000000..4ff8d4b6 --- /dev/null +++ b/src/data/demo/taxRuleBehaviour.ts @@ -0,0 +1 @@ +export default ['This tax only', 'Combine', 'One after another']; diff --git a/src/data/demo/zones.ts b/src/data/demo/zones.ts new file mode 100644 index 00000000..c0cf962e --- /dev/null +++ b/src/data/demo/zones.ts @@ -0,0 +1,44 @@ +import ZoneData from '@data/faker/zone'; + +export default { + europe: new ZoneData({ + id: 1, + name: 'Europe', + status: true, + }), + northAmerica: new ZoneData({ + id: 2, + name: 'North America', + status: true, + }), + asia: new ZoneData({ + id: 3, + name: 'Asia', + status: true, + }), + africa: new ZoneData({ + id: 4, + name: 'Africa', + status: true, + }), + oceania: new ZoneData({ + id: 5, + name: 'Oceania', + status: true, + }), + southAmerica: new ZoneData({ + id: 6, + name: 'South America', + status: true, + }), + europeNonEu: new ZoneData({ + id: 7, + name: 'Europe (non-EU)', + status: true, + }), + centralAmericaAntilla: new ZoneData({ + id: 8, + name: 'Central America/Antilla', + status: true, + }), +}; diff --git a/src/data/faker/address.ts b/src/data/faker/address.ts new file mode 100644 index 00000000..db069d5a --- /dev/null +++ b/src/data/faker/address.ts @@ -0,0 +1,113 @@ +import Countries from '@data/demo/countries'; +import States from '@data/demo/states'; +import type CountryData from '@data/faker/country'; +import type StateData from '@data/faker/state'; +import type AddressCreator from '@data/types/address'; + +import {fakerFR as faker} from '@faker-js/faker'; + +const countriesNames: string[] = Object.values(Countries).map((country: CountryData) => country.name); +const statesNames: string[] = Object.values(States).map((state: StateData) => state.name); + +/** + * Create new address to use in customer address form on BO and FO + * @class + */ +export default class AddressData { + public readonly id: number; + + public readonly name: string; + + public readonly firstName: string; + + public readonly lastName: string; + + public readonly email: string; + + public readonly dni: string; + + public readonly alias: string; + + public readonly company: string; + + public readonly vatNumber: string; + + public readonly address: string; + + public readonly secondAddress: string; + + public readonly postalCode: string; + + public readonly city: string; + + public readonly country: string; + + public readonly state: string; + + public readonly phone: string; + + public readonly other: string; + + /** + * Constructor for class AddressData + * @param addressToCreate {AddressCreator} Could be used to force the value of some members + */ + constructor(addressToCreate: AddressCreator = {}) { + /** @type {string} Tax identification number of the customer */ + this.id = addressToCreate.id || 0; + + /** @type {string} Address Name */ + this.name = addressToCreate.name || faker.word.noun(); + + /** @type {string} Customer firstname */ + this.firstName = addressToCreate.firstName || faker.person.firstName(); + + /** @type {string} Customer lastname */ + this.lastName = addressToCreate.lastName || faker.person.lastName(); + + /** @type {string} Related customer email */ + this.email = addressToCreate.email || faker.internet.email( + { + firstName: this.firstName, + lastName: this.lastName, + provider: 'prestashop.com', + }, + ); + + /** @type {string} Tax identification number of the customer */ + this.dni = addressToCreate.dni || ''; + + /** @type {string} Address alias or name */ + this.alias = addressToCreate.alias || faker.location.streetAddress().substring(0, 30); + + /** @type {string} Company name if it's a company address */ + this.company = (addressToCreate.company || faker.company.name()).substring(0, 63); + + /** @type {string} Tax identification number if it's a company */ + this.vatNumber = addressToCreate.vatNumber || ''; + + /** @type {string} Address first line */ + this.address = addressToCreate.address || faker.location.streetAddress(); + + /** @type {string} Address second line */ + this.secondAddress = addressToCreate.secondAddress || faker.location.secondaryAddress(); + + /** @type {string} Address postal code (default to this format #####) */ + this.postalCode = addressToCreate.postalCode || faker.location.zipCode('#####'); + + /** @type {string} Address city name */ + this.city = addressToCreate.city || faker.location.city(); + + /** @type {string} Address country name */ + this.country = addressToCreate.country || faker.helpers.arrayElement(countriesNames); + + /** @type {string} Address state name */ + this.state = addressToCreate.state || faker.helpers.arrayElement(statesNames); + + /** @type {string} Phone number */ + this.phone = addressToCreate.phone || faker.phone.number(); + + /** @type {string} Other information to add on address */ + this.other = addressToCreate.other || ''; + } +} diff --git a/src/data/faker/carrier.ts b/src/data/faker/carrier.ts new file mode 100644 index 00000000..2af775f7 --- /dev/null +++ b/src/data/faker/carrier.ts @@ -0,0 +1,144 @@ +import TaxRules from '@data/demo/taxRule'; +import Zones from '@data/demo/zones'; +import TaxRuleData from '@data/faker/taxRule'; +import ZoneData from '@data/faker/zone'; +import CarrierCreator from '@data/types/carrier'; + +import {faker} from '@faker-js/faker'; + +const taxes: string[] = Object.values(TaxRules).map((tax: TaxRuleData) => tax.name); +const zonesID: number[] = Object.values(Zones).map((zone: ZoneData) => zone.id); +const outOfRangeBehavior: string[] = ['Apply the cost of the highest defined range', 'Disable carrier']; +const billing: string[] = ['According to total price', 'According to total weight']; + +/** + * Create new carrier to use in carrier form on BO + * @class + */ +export default class CarrierData { + public readonly id: number; + + public readonly position: number; + + public readonly name: string; + + public readonly transitName: string; + + public readonly delay: string; + + public readonly speedGrade: number; + + public readonly trakingURL: string; + + public readonly handlingCosts: boolean; + + public readonly freeShipping: boolean; + + public readonly billing: string; + + public readonly taxRule: string; + + public readonly outOfRangeBehavior: string; + + public readonly rangeSup: number; + + public readonly allZones: boolean; + + public readonly allZonesValue: number; + + public readonly zoneID: number; + + public readonly maxWidth: number; + + public readonly maxHeight: number; + + public readonly maxDepth: number; + + public readonly maxWeight: number; + + public readonly enable: boolean; + + public readonly price: number; + + public readonly priceText: string; + + public readonly priceTTC: number; + + /** + * Constructor for class CarrierData + * @param carrierToCreate {CarrierCreator} Could be used to force the value of some members + */ + constructor(carrierToCreate: CarrierCreator = {}) { + /** @type {number} ID of the carrier */ + this.id = carrierToCreate.id || 0; + + /** @type {number} Position of the carrier */ + this.position = carrierToCreate.position || 0; + + /** @type {string} Name of the carrier */ + this.name = carrierToCreate.name || faker.company.name(); + + /** @type {string} Transit name of the carrier */ + this.transitName = carrierToCreate.transitName || faker.company.name(); + + /** @type {string} Delay of the carrier */ + this.delay = carrierToCreate.delay || ''; + + /** @type {number} Shipping delay, 0 for longest and 9 for shortest */ + this.speedGrade = carrierToCreate.speedGrade || faker.number.int({min: 1, max: 9}); + + /** @type {string} Url of carrier tracking */ + this.trakingURL = carrierToCreate.trakingURL || 'https://example.com/track.php?num=20'; + + /** @type {boolean} True to include handling costs on the price */ + this.handlingCosts = carrierToCreate.handlingCosts === undefined ? true : carrierToCreate.handlingCosts; + + /** @type {boolean} True to make shipping free */ + this.freeShipping = carrierToCreate.freeShipping === undefined ? true : carrierToCreate.freeShipping; + + /** @type {string} Billing method of the carrier */ + this.billing = carrierToCreate.billing || faker.helpers.arrayElement(billing); + + /** @type {string} Tax rule of the carrier */ + this.taxRule = carrierToCreate.taxRule || faker.helpers.arrayElement(taxes); + + /** @type {string} Behavior when no defined range matches the customer carts */ + this.outOfRangeBehavior = carrierToCreate.outOfRangeBehavior || faker.helpers.arrayElement(outOfRangeBehavior); + + /** @type {number} Superior range for the carrier */ + this.rangeSup = carrierToCreate.rangeSup || faker.number.int({min: 1, max: 100}); + + /** @type {boolean} True to apply it to all zones */ + this.allZones = carrierToCreate.allZones === undefined ? true : carrierToCreate.allZones; + + /** @type {number} Value to set when all zones is checked */ + this.allZonesValue = carrierToCreate.allZonesValue || faker.number.int({min: 1, max: 100}); + + /** @type {number} ID of the zone on carrier form */ + this.zoneID = carrierToCreate.zoneID || faker.helpers.arrayElement(zonesID); + + /** @type {number} Max width that the carrier can handle */ + this.maxWidth = carrierToCreate.maxWidth || faker.number.int({min: 1, max: 100}); + + /** @type {number} Max height that the carrier can handle */ + this.maxHeight = carrierToCreate.maxHeight || faker.number.int({min: 1, max: 100}); + + /** @type {number} Max depth that the carrier can handle */ + this.maxDepth = carrierToCreate.maxDepth || faker.number.int({min: 1, max: 100}); + + /** @type {number} Max weight that the carrier can handle */ + this.maxWeight = carrierToCreate.maxWeight || faker.number.int({min: 1, max: 100}); + + /** @type {boolean} Status of the carrier */ + this.enable = carrierToCreate.enable === undefined ? true : carrierToCreate.enable; + + /** @type {number} Price HT */ + this.price = carrierToCreate.price || 0; + + /** @type {string} */ + this.priceText = carrierToCreate.priceText || this.price.toString(); + + /** @type {number} Price TTC */ + this.priceTTC = carrierToCreate.priceTTC || 0; + } +} diff --git a/src/data/faker/country.ts b/src/data/faker/country.ts new file mode 100644 index 00000000..fa364faa --- /dev/null +++ b/src/data/faker/country.ts @@ -0,0 +1,83 @@ +import Currencies from '@data/demo/currencies'; +import Zones from '@data/demo/zones'; +import CurrencyData from '@data/faker/currency'; +import ZoneData from '@data/faker/zone'; +import CountryCreator from '@data/types/country'; + +import {faker} from '@faker-js/faker'; + +const zones: string[] = Object.values(Zones).map((zone: ZoneData) => zone.name); +const currencies: string[] = Object.values(Currencies).map((currency: CurrencyData) => currency.name); + +/** + * Create new country to use on creation form on country page on BO + * @class + */ +export default class CountryData { + public readonly id: number; + + public readonly name: string; + + public readonly isoCode: string; + + public readonly callPrefix: string; + + public readonly currency: string; + + public readonly zone: string; + + public readonly needZipCode: boolean; + + public readonly zipCodeFormat: string; + + public readonly active: boolean; + + public readonly containsStates: boolean; + + public readonly needIdentificationNumber: boolean; + + public readonly displayTaxNumber: boolean; + + /** + * Constructor for class CountryData + * @param countryToCreate {CountryCreator} Could be used to force the value of some members + */ + constructor(countryToCreate: CountryCreator = {}) { + /** @type {number} ID of the country */ + this.id = countryToCreate.id || 0; + + /** @type {string} Name of the country */ + this.name = countryToCreate.name || `test${faker.location.country()}`; + + /** @type {string} Country iso code */ + this.isoCode = countryToCreate.isoCode || faker.location.countryCode(); + + /** @type {string} Country call Prefix */ + this.callPrefix = countryToCreate.callPrefix || '0'; + + /** @type {string} Currency used in the country */ + this.currency = countryToCreate.currency || faker.helpers.arrayElement(currencies); + + /** @type {string} In which zone the country belongs */ + this.zone = countryToCreate.zone || faker.helpers.arrayElement(zones); + + /** @type {boolean} True if the country used zip codes */ + this.needZipCode = countryToCreate.needZipCode === undefined ? false : countryToCreate.needZipCode; + + /** @type {string} Format of the zip code if used */ + this.zipCodeFormat = countryToCreate.zipCodeFormat || ''; + + /** @type {string} Status of the country */ + this.active = countryToCreate.active === undefined ? false : countryToCreate.active; + + /** @type {boolean} True if the country have states */ + this.containsStates = countryToCreate.containsStates === undefined ? false : countryToCreate.containsStates; + + /** @type {boolean} True if need identification number for the country */ + this.needIdentificationNumber = countryToCreate.needIdentificationNumber === undefined + ? false : countryToCreate.needIdentificationNumber; + + /** @type {boolean} True to display tax number when located on the country */ + this.displayTaxNumber = countryToCreate.displayTaxNumber === undefined ? false : countryToCreate.displayTaxNumber; + } +} diff --git a/src/data/faker/currency.ts b/src/data/faker/currency.ts new file mode 100644 index 00000000..1a77b838 --- /dev/null +++ b/src/data/faker/currency.ts @@ -0,0 +1,50 @@ +import type {CurrencyCreator} from '@data/types/currency'; + +import {faker} from '@faker-js/faker'; + +/** + * Create new currency to use in currency form on BO + * @class + */ +export default class CurrencyData { + public readonly name: string; + + public readonly frName: string; + + public readonly symbol: string; + + public readonly isoCode: string; + + public readonly exchangeRate: number; + + public readonly decimals: number; + + public readonly enabled: boolean; + + /** + * Constructor for class CurrencyData + * @param currencyToCreate {CurrencyCreator} Could be used to force the value of some members + */ + constructor(currencyToCreate: CurrencyCreator = {}) { + /** @type {string} Name of the currency */ + this.name = currencyToCreate.name || faker.finance.currencyName(); + + /** @type {string} */ + this.frName = currencyToCreate.frName || this.name; + + /** @type {string} */ + this.symbol = currencyToCreate.symbol || faker.finance.currencySymbol(); + + /** @type {string} */ + this.isoCode = currencyToCreate.isoCode || faker.finance.currencyCode(); + + /** @type {number} */ + this.exchangeRate = currencyToCreate.exchangeRate || 1; + + /** @type {number} */ + this.decimals = currencyToCreate.decimals || 2; + + /** @type {boolean} */ + this.enabled = currencyToCreate.enabled === undefined ? true : currencyToCreate.enabled; + } +} diff --git a/src/data/faker/orderStatus.ts b/src/data/faker/orderStatus.ts new file mode 100644 index 00000000..6ffc6192 --- /dev/null +++ b/src/data/faker/orderStatus.ts @@ -0,0 +1,82 @@ +import OrderStatusCreator from '@data/types/orderStatus'; + +import {faker} from '@faker-js/faker'; + +/** + * Create new order status to use on creation form on order status page on BO + * @class + */ +export default class OrderStatusData { + public readonly id: number; + + public readonly name: string; + + public readonly color: string; + + public readonly logableOn: boolean; + + public readonly invoiceOn: boolean; + + public readonly hiddenOn: boolean; + + public readonly sendEmailOn: boolean; + + public readonly pdfInvoiceOn: boolean; + + public readonly pdfDeliveryOn: boolean; + + public readonly shippedOn: boolean; + + public readonly paidOn: boolean; + + public readonly deliveryOn: boolean; + + public readonly emailTemplate: string; + + /** + * Constructor for class OrderStatusData + * @param orderStatusToCreate {OrderStatusCreator} Could be used to force the value of some members + */ + constructor(orderStatusToCreate: OrderStatusCreator = {}) { + /** @type {number} Name of the status */ + this.id = orderStatusToCreate.id || 0; + + /** @type {string} Name of the status (Max 32 characters) */ + this.name = orderStatusToCreate.name || (`order_status_${faker.lorem.word({ + length: {min: 1, max: 19}, + })}`).substring(0, 32); + + /** @type {string} Hexadecimal value for the status color */ + this.color = orderStatusToCreate.color || faker.internet.color(); + + /** @type {boolean} True to consider order is valid */ + this.logableOn = orderStatusToCreate.logableOn === undefined ? true : orderStatusToCreate.logableOn; + + /** @type {boolean} True to allow a customer to download and view PDF versions of the invoices */ + this.invoiceOn = orderStatusToCreate.invoiceOn === undefined ? true : orderStatusToCreate.invoiceOn; + + /** @type {boolean} True to hide this status in all customer orders. */ + this.hiddenOn = orderStatusToCreate.hiddenOn === undefined ? true : orderStatusToCreate.hiddenOn; + + /** @type {boolean} True to email the customer when his/her order status has changed */ + this.sendEmailOn = orderStatusToCreate.sendEmailOn === undefined ? true : orderStatusToCreate.sendEmailOn; + + /** @type {boolean} True to attach invoice PDF to email */ + this.pdfInvoiceOn = orderStatusToCreate.pdfInvoiceOn === undefined ? true : orderStatusToCreate.pdfInvoiceOn; + + /** @type {boolean} True to attach delivery slip PDF to email */ + this.pdfDeliveryOn = orderStatusToCreate.pdfDeliveryOn === undefined ? true : orderStatusToCreate.pdfDeliveryOn; + + /** @type {boolean} True to set the order as shipped */ + this.shippedOn = orderStatusToCreate.shippedOn === undefined ? true : orderStatusToCreate.shippedOn; + + /** @type {boolean} True to set the order as paid */ + this.paidOn = orderStatusToCreate.paidOn === undefined ? true : orderStatusToCreate.paidOn; + + /** @type {boolean} True to show delivery PDF */ + this.deliveryOn = orderStatusToCreate.deliveryOn === undefined ? true : orderStatusToCreate.deliveryOn; + + /** @type {string} Email Template */ + this.emailTemplate = orderStatusToCreate.emailTemplate === undefined ? '' : orderStatusToCreate.emailTemplate; + } +} diff --git a/src/data/faker/paymentMethod.ts b/src/data/faker/paymentMethod.ts new file mode 100644 index 00000000..99975023 --- /dev/null +++ b/src/data/faker/paymentMethod.ts @@ -0,0 +1,29 @@ +import type PaymentMethodCreator from '@data/types/paymentMethod'; + +import {faker} from '@faker-js/faker'; + +/** + * @class + */ +export default class PaymentMethodData { + public readonly name: string; + + public readonly displayName: string; + + public readonly moduleName: string; + + /** + * Constructor for class PaymentMethodData + * @param valueToCreate {PaymentMethodCreator} Could be used to force the value of some members + */ + constructor(valueToCreate: PaymentMethodCreator = {}) { + /** @type {string} */ + this.name = valueToCreate.name || faker.word.noun(); + + /** @type {string} */ + this.displayName = valueToCreate.displayName || this.name; + + /** @type {string} */ + this.moduleName = valueToCreate.moduleName || this.name; + } +} diff --git a/src/data/faker/state.ts b/src/data/faker/state.ts new file mode 100644 index 00000000..5826dda3 --- /dev/null +++ b/src/data/faker/state.ts @@ -0,0 +1,53 @@ +import Zones from '@data/demo/zones'; +import ZoneData from '@data/faker/zone'; +import type StateCreator from '@data/types/state'; + +import {faker} from '@faker-js/faker'; + +const zones: string[] = Object.values(Zones).map((zone: ZoneData) => zone.name); +const countriesWithState: string[] = [ + 'Argentina', 'Australia', 'Canada', 'India', 'Indonesia', 'Italy', 'Japan', 'Mexico', 'United States', +]; +const statesIsoCodes: string[] = ['IR', 'PK', 'BP', 'BV', 'ZM', 'ZL', 'HM', 'HL', 'BK']; + +/** + * Create new state to use on state creation form on BO + * @class + */ +export default class StateData { + public readonly id: number; + + public readonly name: string; + + public readonly isoCode: string; + + public readonly country: string; + + public readonly zone: string; + + public readonly status: boolean; + + /** + * Constructor for class StateData + * @param stateToCreate {StateCreator} Could be used to force the value of some members + */ + constructor(stateToCreate: StateCreator = {}) { + /** @type {number} ID of the state */ + this.id = stateToCreate.id || 0; + + /** @type {string} Name of the state */ + this.name = stateToCreate.name || `test ${faker.location.state()}`; + + /** @type {string} Iso code of the state */ + this.isoCode = stateToCreate.isoCode || faker.helpers.arrayElement(statesIsoCodes); + + /** @type {string} Country of the state */ + this.country = stateToCreate.country || faker.helpers.arrayElement(countriesWithState); + + /** @type {string} Zone of the state */ + this.zone = stateToCreate.zone || faker.helpers.arrayElement(zones); + + /** @type {boolean} Status of the state */ + this.status = stateToCreate.status === undefined ? false : stateToCreate.status; + } +} diff --git a/src/data/faker/tax.ts b/src/data/faker/tax.ts new file mode 100644 index 00000000..f6b00771 --- /dev/null +++ b/src/data/faker/tax.ts @@ -0,0 +1,41 @@ +// Import data +import TaxCreator from '@data/types/tax'; + +import {faker} from '@faker-js/faker'; + +/** + * Create new tax to use on tax form on BO + * @class + */ +export default class TaxData { + public readonly id: number; + + public readonly rate: string; + + public readonly name: string; + + public readonly frName: string; + + public readonly enabled: boolean; + + /** + * Constructor for class TaxData + * @param taxToCreate {TaxCreator} Could be used to force the value of some members + */ + constructor(taxToCreate: TaxCreator = {}) { + /** @type {number} Name of the tax */ + this.id = taxToCreate.id || 0; + + /** @type {string} Tax of the rate */ + this.rate = taxToCreate.rate || faker.number.int({min: 1, max: 40}).toString(); + + /** @type {string} Name of the tax */ + this.name = taxToCreate.name || `TVA test ${this.rate}%`; + + /** @type {string} French name of the tax */ + this.frName = taxToCreate.frName || this.name; + + /** @type {boolean} Status of the tax */ + this.enabled = taxToCreate.enabled === undefined ? true : taxToCreate.enabled; + } +} diff --git a/src/data/faker/taxRule.ts b/src/data/faker/taxRule.ts new file mode 100644 index 00000000..d05f81d1 --- /dev/null +++ b/src/data/faker/taxRule.ts @@ -0,0 +1,52 @@ +// Import data +import Countries from '@data/demo/countries'; +import tax from '@data/demo/tax'; +import type CountryData from '@data/faker/country'; +import TaxRuleBehaviour from '@data/demo/taxRuleBehaviour'; +import TaxRuleCreator from '@data/types/taxRule'; + +import {faker} from '@faker-js/faker'; + +const countriesNames: string[] = Object.values(Countries).map((country: CountryData) => country.name); + +/** + * Create new tax rule to use on tax rule form on BO + * @class + */ +export default class TaxRuleData { + public readonly id: number; + + public readonly country: string; + + public readonly zipCode: string; + + public readonly behaviour: string; + + public readonly name: string; + + public readonly description: string; + + /** + * Constructor for class TaxRuleData + * @param taxRulesToCreate {TaxRuleCreator} Could be used to force the value of some members + */ + constructor(taxRulesToCreate: TaxRuleCreator = {}) { + /** @type {number} ID */ + this.id = taxRulesToCreate.id || 0; + + /** @type {string} Country to apply the tax */ + this.country = taxRulesToCreate.country || faker.helpers.arrayElement(countriesNames); + + /** @type {string} Postal code of the country */ + this.zipCode = taxRulesToCreate.zipCode || faker.location.zipCode(); + + /** @type {string} Behavior of the tax rule */ + this.behaviour = taxRulesToCreate.behaviour || faker.helpers.arrayElement(TaxRuleBehaviour); + + /** @type {string} Name of the tax to use on the rule */ + this.name = taxRulesToCreate.name || tax.DefaultFrTax.name; + + /** @type {string} Description of the tax rule */ + this.description = taxRulesToCreate.description || faker.lorem.sentence(); + } +} diff --git a/src/data/faker/zone.ts b/src/data/faker/zone.ts new file mode 100644 index 00000000..f9231121 --- /dev/null +++ b/src/data/faker/zone.ts @@ -0,0 +1,31 @@ +// Import data +import ZoneCreator from '@data/types/zone'; + +import {faker} from '@faker-js/faker'; + +/** + * Create new zone to use on zone form on BO + * @class + */ +export default class ZoneData { + public readonly id: number; + + public readonly name: string; + + public readonly status: boolean; + + /** + * Constructor for class ZoneData + * @param zoneToCreate {Object} Could be used to force the value of some members + */ + constructor(zoneToCreate: ZoneCreator = {}) { + /** @type {number} */ + this.id = zoneToCreate.id || 0; + + /** @type {string} Name of the zone */ + this.name = zoneToCreate.name || `test ${faker.lorem.word()}`; + + /** @type {boolean} Status of the zone */ + this.status = zoneToCreate.status === undefined ? true : zoneToCreate.status; + } +} diff --git a/src/data/types/address.d.ts b/src/data/types/address.d.ts new file mode 100644 index 00000000..cd127c19 --- /dev/null +++ b/src/data/types/address.d.ts @@ -0,0 +1,21 @@ +type AddressCreator = { + id?: number + name?: string + firstName?: string + lastName?: string + email?: string + dni?: string + alias?: string + company?: string + vatNumber?: string + address?: string + secondAddress?: string + postalCode?: string + city?: string + country?: string + state?: string + phone?: string + other?: string +}; + +export default AddressCreator; diff --git a/src/data/types/carrier.d.ts b/src/data/types/carrier.d.ts new file mode 100644 index 00000000..906a9a09 --- /dev/null +++ b/src/data/types/carrier.d.ts @@ -0,0 +1,28 @@ +type CarrierCreator = { + id?: number + position?: number + name?: string + transitName?: string + delay?: string + speedGrade?: number + trakingURL?: string + handlingCosts?: boolean + freeShipping?: boolean + billing?: string + taxRule?: string + outOfRangeBehavior?: string + rangeSup?: number + allZones?: boolean + allZonesValue?: number + zoneID?: number + maxWidth?: number + maxHeight?: number + maxDepth?: number + maxWeight?: number + enable?: boolean + price?: number + priceText?: string + priceTTC?: number +}; + +export default CarrierCreator; diff --git a/src/data/types/country.d.ts b/src/data/types/country.d.ts new file mode 100644 index 00000000..84779e64 --- /dev/null +++ b/src/data/types/country.d.ts @@ -0,0 +1,16 @@ +type CountryCreator = { + id?: number + name?: string + isoCode?: string + callPrefix?: string + currency?: string + zone?: string + needZipCode?: boolean + zipCodeFormat?: string + active?: boolean + containsStates?: boolean + needIdentificationNumber?: boolean + displayTaxNumber?: boolean +}; + +export default CountryCreator; diff --git a/src/data/types/currency.d.ts b/src/data/types/currency.d.ts new file mode 100644 index 00000000..0af2e280 --- /dev/null +++ b/src/data/types/currency.d.ts @@ -0,0 +1,16 @@ +type CurrencyCreator = { + name?: string + frName?: string + symbol?: string + isoCode?: string + exchangeRate?: number + decimals?: number + enabled?: boolean +}; + +type CurrencyFormat = 'leftWithSpace' | 'leftWithoutSpace' | 'rightWithSpace' | 'rightWithoutSpace'; + +export { + CurrencyCreator, + CurrencyFormat, +}; diff --git a/src/data/types/orderStatus.d.ts b/src/data/types/orderStatus.d.ts new file mode 100644 index 00000000..b455885f --- /dev/null +++ b/src/data/types/orderStatus.d.ts @@ -0,0 +1,17 @@ +type OrderStatusCreator = { + id?: number + name?: string + color?: string + logableOn?: boolean + invoiceOn?: boolean + hiddenOn?: boolean + sendEmailOn?: boolean + pdfInvoiceOn?: boolean + pdfDeliveryOn?: boolean + shippedOn?: boolean + paidOn?: boolean + deliveryOn?: boolean + emailTemplate?: string +}; + +export default OrderStatusCreator; diff --git a/src/data/types/paymentMethod.d.ts b/src/data/types/paymentMethod.d.ts new file mode 100644 index 00000000..dc6524de --- /dev/null +++ b/src/data/types/paymentMethod.d.ts @@ -0,0 +1,7 @@ +type PaymentMethodCreator = { + name?: string + displayName?: string + moduleName?: string +}; + +export default PaymentMethodCreator; diff --git a/src/data/types/state.d.ts b/src/data/types/state.d.ts new file mode 100644 index 00000000..41f92924 --- /dev/null +++ b/src/data/types/state.d.ts @@ -0,0 +1,10 @@ +type StateCreator = { + id?: number + name?: string + isoCode?: string + country?: string + zone?: string + status?: boolean +}; + +export default StateCreator; diff --git a/src/data/types/tax.d.ts b/src/data/types/tax.d.ts new file mode 100644 index 00000000..6a7aacb6 --- /dev/null +++ b/src/data/types/tax.d.ts @@ -0,0 +1,9 @@ +type TaxCreator = { + id?: number, + rate?: string, + name?: string, + frName?: string, + enabled?: boolean, +}; + +export default TaxCreator; diff --git a/src/data/types/taxRule.d.ts b/src/data/types/taxRule.d.ts new file mode 100644 index 00000000..920b5256 --- /dev/null +++ b/src/data/types/taxRule.d.ts @@ -0,0 +1,10 @@ +type TaxRuleCreator = { + id?: number, + country?: string, + zipCode?: string, + behaviour?: string, + name?: string, + description?: string, +}; + +export default TaxRuleCreator; diff --git a/src/data/types/zone.d.ts b/src/data/types/zone.d.ts new file mode 100644 index 00000000..4d77adaf --- /dev/null +++ b/src/data/types/zone.d.ts @@ -0,0 +1,7 @@ +type ZoneCreator = { + id?: number + name?: string + status?: boolean +}; + +export default ZoneCreator; diff --git a/src/index.ts b/src/index.ts index c9047359..dee94db5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,16 +17,35 @@ export type { WaitForNavigationWaitUntil, } from '@data/types/playwright'; +export {default as dataCountries} from '@data/demo/countries'; +export {default as dataCurrencies} from '@data/demo/currencies'; export {default as dataCustomers} from '@data/demo/customers'; export {default as dataGroups} from '@data/demo/groups'; export {default as dataModules} from '@data/demo/modules'; +export {default as dataOrderStatuses} from '@data/demo/orderStatuses'; +export {default as dataPaymentMethods} from '@data/demo/paymentMethods'; export {default as dataSocialTitles} from '@data/demo/socialTitles'; +export {default as dataStates} from '@data/demo/states'; +export {default as dataTaxes} from '@data/demo/tax'; +export {default as dataTaxRules} from '@data/demo/taxRule'; +export {default as dataTaxRuleBehaviours} from '@data/demo/taxRuleBehaviour'; export {default as dataTitles} from '@data/demo/titles'; +export {default as dataZones} from '@data/demo/zones'; +export {default as fakerAddress} from '@data/faker/address'; +export {default as fakerCarrier} from '@data/faker/carrier'; +export {default as fakerCountry} from '@data/faker/country'; +export {default as fakerCurrency} from '@data/faker/currency'; export {default as fakerCustomer} from '@data/faker/customer'; export {default as fakerGroup} from '@data/faker/group'; export {default as fakerModule} from '@data/faker/module'; +export {default as fakerOrderStatus} from '@data/faker/orderStatus'; +export {default as fakerPaymentMethod} from '@data/faker/paymentMethod'; +export {default as fakerState} from '@data/faker/state'; +export {default as fakerTax} from '@data/faker/tax'; +export {default as fakerTaxRule} from '@data/faker/taxRule'; export {default as fakerTitle} from '@data/faker/title'; +export {default as fakerZone} from '@data/faker/zone'; // Export Pages export * as CommonPage from '@pages/commonPage'; @@ -34,11 +53,15 @@ export * as CommonPage from '@pages/commonPage'; export * as BOBasePage from '@pages/BO/BOBasePage'; export {default as boLoginPage} from '@pages/BO/login'; export {default as boDashboardPage} from '@pages/BO/dashboard'; +export {default as boOrdersPage} from '@pages/BO/orders'; export {default as boModuleManagerPage} from '@pages/BO/modules/moduleManager'; // Export Pages FO export * as FOBasePage from '@pages/FO/FOBasePage'; // Export Pages FO/Classic +export {default as foClassicCartPage} from '@pages/FO/classic/cart'; export {default as foClassicCategoryPage} from '@pages/FO/classic/category'; +export {default as foClassicCheckoutPage} from '@pages/FO/classic/checkout'; +export {default as foClassicCheckoutOrderConfirmationPage} from '@pages/FO/classic/checkout/orderConfirmation'; export {default as foClassicHomePage} from '@pages/FO/classic/home'; export {default as foClassicLoginPage} from '@pages/FO/classic/login'; diff --git a/src/interfaces/BO/modules/moduleManager/index.ts b/src/interfaces/BO/modules/moduleManager/index.ts index d7fdc819..ba19a273 100644 --- a/src/interfaces/BO/modules/moduleManager/index.ts +++ b/src/interfaces/BO/modules/moduleManager/index.ts @@ -5,7 +5,17 @@ import type {Page} from '@playwright/test'; export interface ModuleManagerPageInterface extends BOBasePagePageInterface { pageTitle: string; + readonly resetModuleSuccessMessage: (moduleTag: string) => string; goToConfigurationPage(page: Page, moduleTag: string): Promise; + isModalActionVisible(page: Page, module: ModuleData, action: string): Promise; + isModuleVisible(page: Page, module: ModuleData): Promise; searchModule(page: Page, module: ModuleData): Promise; + setActionInModule( + page: Page, + module: ModuleData, + action: string, + cancel?: boolean, + forceDeletion?: boolean, + ): Promise } diff --git a/src/interfaces/BO/orders/index.ts b/src/interfaces/BO/orders/index.ts new file mode 100644 index 00000000..28c736f0 --- /dev/null +++ b/src/interfaces/BO/orders/index.ts @@ -0,0 +1,10 @@ +import {BOBasePagePageInterface} from '@interfaces/BO'; + +import type {Page} from '@playwright/test'; + +export interface BOOrdersPageInterface extends BOBasePagePageInterface { + readonly pageTitle: string; + + getTextColumn(page: Page, columnName: string, row: number): Promise; + resetAndGetNumberOfLines(page: Page): Promise; +} diff --git a/src/interfaces/FO/cart/index.ts b/src/interfaces/FO/cart/index.ts new file mode 100644 index 00000000..4263c3c6 --- /dev/null +++ b/src/interfaces/FO/cart/index.ts @@ -0,0 +1,8 @@ +import {FOBasePagePageInterface} from '@interfaces/FO'; +import type {Page} from '@playwright/test'; + +export interface FoCartPageInterface extends FOBasePagePageInterface { + readonly pageTitle: string; + + clickOnProceedToCheckout(page: Page): Promise; +} diff --git a/src/interfaces/FO/checkout/index.ts b/src/interfaces/FO/checkout/index.ts new file mode 100644 index 00000000..e65cd5c2 --- /dev/null +++ b/src/interfaces/FO/checkout/index.ts @@ -0,0 +1,13 @@ +import {FOBasePagePageInterface} from '@interfaces/FO'; +import type {Page} from '@playwright/test'; + +export interface FoCheckoutPageInterface extends FOBasePagePageInterface { + personalInformationStepForm: string; + + choosePaymentAndOrder(page: Page, paymentModuleName: string): Promise; + goToDeliveryStep(page: Page): Promise; + goToPaymentStep(page: Page): Promise; + isCheckoutPage(page: Page): Promise; + isPaymentMethodExist(page: Page, paymentModuleName: string): Promise; + isStepCompleted(page: Page, stepSelector: string): Promise; +} diff --git a/src/interfaces/FO/checkout/orderConfirmation.ts b/src/interfaces/FO/checkout/orderConfirmation.ts new file mode 100644 index 00000000..100952a1 --- /dev/null +++ b/src/interfaces/FO/checkout/orderConfirmation.ts @@ -0,0 +1,9 @@ +import {FOBasePagePageInterface} from '@interfaces/FO'; +import type {Page} from '@playwright/test'; + +export interface FoCheckoutOrderConfirmationPageInterface extends FOBasePagePageInterface { + readonly orderConfirmationCardTitle: string; + + getOrderConfirmationCardTitle(page: Page): Promise; + getOrderReferenceValue(page: Page): Promise; +} diff --git a/src/interfaces/FO/home/index.ts b/src/interfaces/FO/home/index.ts index a3b0b0d5..72f08a6f 100644 --- a/src/interfaces/FO/home/index.ts +++ b/src/interfaces/FO/home/index.ts @@ -2,6 +2,8 @@ import {FOBasePagePageInterface} from '@interfaces/FO'; import type {Page} from '@playwright/test'; export interface FoHomePageInterface extends FOBasePagePageInterface { + addProductToCartByQuickView(page: Page, id: number, quantityWanted?: number): Promise; goToAllProductsPage(page: Page): Promise; isHomePage(page: Page): Promise; + proceedToCheckout(page: Page): Promise; } diff --git a/src/interfaces/FO/index.ts b/src/interfaces/FO/index.ts index e35b78be..91269075 100644 --- a/src/interfaces/FO/index.ts +++ b/src/interfaces/FO/index.ts @@ -3,6 +3,7 @@ import type {Page} from '@playwright/test'; export interface FOBasePagePageInterface extends CommonPageInterface { changeLanguage(page: Page, lang: string): Promise; + goToHomePage(page: Page): Promise; goToLoginPage(page: Page): Promise; isCustomerConnected(page: Page): Promise; logout(page: Page): Promise; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index a4bd66f4..3be97be5 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -4,4 +4,5 @@ export interface CommonPageInterface { closePage(browserContext: BrowserContext, page: Page, tabId?: number): Promise; getPageTitle(page: Page): Promise; goTo(page: Page, url: string): Promise; + goToFo(page: Page): Promise; } diff --git a/src/pages/BO/orders/index.ts b/src/pages/BO/orders/index.ts new file mode 100644 index 00000000..18da27a4 --- /dev/null +++ b/src/pages/BO/orders/index.ts @@ -0,0 +1,16 @@ +import type {BOOrdersPageInterface} from '@interfaces/BO/orders'; +import testContext from '@utils/testContext'; +import semver from 'semver'; + +const psVersion = testContext.getPSVersion(); + +/* eslint-disable global-require */ +function requirePage(): BOOrdersPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/BO/orders'); + } + return require('@versions/8.0.0/pages/BO/orders'); +} +/* eslint-enable global-require */ + +export default requirePage(); diff --git a/src/pages/FO/classic/cart/index.ts b/src/pages/FO/classic/cart/index.ts new file mode 100644 index 00000000..312ef93c --- /dev/null +++ b/src/pages/FO/classic/cart/index.ts @@ -0,0 +1,16 @@ +import type {FoCartPageInterface} from '@interfaces/FO/cart'; +import testContext from '@utils/testContext'; +import semver from 'semver'; + +const psVersion = testContext.getPSVersion(); + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): FoCartPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/FO/classic/cart').cartPage; + } + return require('@versions/8.0.0/pages/FO/classic/cart').cartPage; +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/FO/classic/checkout/index.ts b/src/pages/FO/classic/checkout/index.ts new file mode 100644 index 00000000..74bfb4d8 --- /dev/null +++ b/src/pages/FO/classic/checkout/index.ts @@ -0,0 +1,16 @@ +import type {FoCheckoutPageInterface} from '@interfaces/FO/checkout'; +import testContext from '@utils/testContext'; +import semver from 'semver'; + +const psVersion = testContext.getPSVersion(); + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): FoCheckoutPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/FO/classic/checkout').checkoutPage; + } + return require('@versions/8.0.0/pages/FO/classic/checkout').checkoutPage; +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/FO/classic/checkout/orderConfirmation.ts b/src/pages/FO/classic/checkout/orderConfirmation.ts new file mode 100644 index 00000000..1f22ca0a --- /dev/null +++ b/src/pages/FO/classic/checkout/orderConfirmation.ts @@ -0,0 +1,16 @@ +import type {FoCheckoutOrderConfirmationPageInterface} from '@interfaces/FO/checkout/orderConfirmation'; +import testContext from '@utils/testContext'; +import semver from 'semver'; + +const psVersion = testContext.getPSVersion(); + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): FoCheckoutOrderConfirmationPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/FO/classic/checkout/orderConfirmation').orderConfirmationPage; + } + return require('@versions/8.0.0/pages/FO/classic/checkout/orderConfirmation').orderConfirmationPage; +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/versions/8.0.0/pages/BO/orders/index.ts b/src/versions/8.0.0/pages/BO/orders/index.ts new file mode 100644 index 00000000..34d0d638 --- /dev/null +++ b/src/versions/8.0.0/pages/BO/orders/index.ts @@ -0,0 +1,712 @@ +import BOBasePage from '@pages/BO/BOBasePage'; + +import type OrderStatusData from '@data/faker/orderStatus'; +import type {BOOrdersPageInterface} from '@interfaces/BO/orders'; + +import type {Page} from 'playwright'; + +/** + * Orders page, contains functions that can be used on orders page + * @class + * @extends BOBasePage + */ +class Order extends BOBasePage implements BOOrdersPageInterface { + public readonly pageTitle: string; + + private readonly createNewOrderButton: string; + + private readonly gridPanel: string; + + private readonly gridTable: string; + + private readonly gridHeaderTitle: string; + + private readonly tableHead: string; + + private readonly sortColumnDiv: (column: string) => string; + + private readonly sortColumnSpanButton: (column: string) => string; + + private readonly filterColumn: (filterBy: string) => string; + + private readonly filterSearchButton: string; + + private readonly filterResetButton: string; + + private readonly tableBody: string; + + private readonly tableRows: string; + + private readonly tableRow: (row: number) => string; + + private readonly tableEmptyRow: string; + + private readonly tableColumn: (row: number, column: string) => string; + + private readonly tableColumnStatus: (row: number) => string; + + private readonly updateStatusInTableButton: (row: number) => string; + + private readonly updateStatusInTableDropdown: (row: number) => string; + + private readonly updateStatusInTableDropdownChoice: (row: number, statusId: number) => string; + + private readonly expandIcon: (row: number) => string; + + private readonly previewRow: string; + + private readonly shippingDetails: string; + + private readonly customerEmail: string; + + private readonly invoiceDetails: string; + + private readonly productTable: string; + + private readonly productsNumber: string; + + private readonly productRowFromTable: (row: number) => string; + + private readonly previewMoreProductsLink: (row: number) => string; + + private readonly previewOrderButton: string; + + private readonly actionsColumn: (row: number) => string; + + private readonly viewRowLink: (row: number) => string; + + private readonly viewInvoiceRowLink: (row: number) => string; + + private readonly viewDeliverySlipsRowLink: (row: number) => string; + + private readonly gridActionButton: string; + + private readonly gridActionDropDownMenu: string; + + private readonly gridActionExportLink: string; + + private readonly selectAllRowsLabel: string; + + private readonly bulkActionsToggleButton: string; + + private readonly bulkUpdateOrdersStatusButton: string; + + private readonly bulkOpenInNewTabsButton: string; + + private readonly tableColumnOrderBulk: (row: number) => string; + + private readonly tableColumnOrderBulkCheckboxLabel: (row: number) => string; + + private readonly updateOrdersStatusModal: string; + + private readonly updateOrdersStatusModalSelect: string; + + private readonly updateOrdersStatusModalButton: string; + + private readonly paginationBlock: string; + + private readonly paginationLimitSelect: string; + + private readonly paginationLabel: string; + + private readonly paginationNextLink: string; + + private readonly paginationPreviousLink: string; + + /** + * @constructs + * Setting up texts and selectors to use on orders page + */ + constructor() { + super(); + + this.pageTitle = 'Orders •'; + + // Header selectors + this.createNewOrderButton = '#page-header-desc-configuration-add'; + + // Selectors grid panel + this.gridPanel = '#order_grid_panel'; + this.gridTable = '#order_grid_table'; + this.gridHeaderTitle = `${this.gridPanel} h3.card-header-title`; + + // Sort Selectors + this.tableHead = `${this.gridTable} thead`; + this.sortColumnDiv = (column: string) => `${this.tableHead} div.ps-sortable-column[data-sort-col-name='${column}']`; + this.sortColumnSpanButton = (column: string) => `${this.sortColumnDiv(column)} span.ps-sort`; + + // Filters + this.filterColumn = (filterBy: string) => `${this.gridTable} #order_${filterBy}`; + this.filterSearchButton = `${this.gridTable} .grid-search-button`; + this.filterResetButton = `${this.gridTable} .grid-reset-button`; + + // Table rows and columns + this.tableBody = `${this.gridTable} tbody`; + this.tableRows = `${this.tableBody} tr`; + this.tableRow = (row: number) => `${this.tableRows}:nth-child(${row})`; + this.tableEmptyRow = `${this.tableRows}.empty_row`; + this.tableColumn = (row: number, column: string) => `${this.tableRow(row)} td.column-${column}`; + this.tableColumnStatus = (row: number) => `${this.tableRow(row)} td.column-osname`; + this.updateStatusInTableButton = (row: number) => `${this.tableColumnStatus(row)}.choice-type.text-left > div > button`; + this.updateStatusInTableDropdown = (row: number) => `${this.tableColumnStatus(row)} div.js-choice-options`; + this.updateStatusInTableDropdownChoice = (row: number, statusId: number) => `${this.updateStatusInTableDropdown(row)}` + + ` button[data-value='${statusId}']`; + // Preview row + this.expandIcon = (row: number) => `${this.tableRow(row)} span.preview-toggle`; + this.previewRow = `${this.tableRows}.preview-row td div[data-role=preview-row]`; + this.shippingDetails = `${this.previewRow} div[data-role=shipping-details]`; + this.customerEmail = `${this.previewRow} div[data-role=email]`; + this.invoiceDetails = `${this.previewRow} div[data-role=invoice-details]`; + this.productTable = `${this.previewRow} table[data-role=product-table]`; + this.productsNumber = `${this.productTable} thead tr:nth-child(1)`; + this.productRowFromTable = (row: number) => `${this.productTable} tbody tr:nth-child(${row})`; + this.previewMoreProductsLink = (row: number) => `${this.productRowFromTable(row)} td a.js-preview-more-products-btn`; + this.previewOrderButton = `${this.gridTable} tr.preview-row a.btn-primary`; + + // Column actions selectors + this.actionsColumn = (row: number) => `${this.tableRow(row)} td.column-actions`; + this.viewRowLink = (row: number) => `${this.actionsColumn(row)} a.grid-view-row-link`; + this.viewInvoiceRowLink = (row: number) => `${this.actionsColumn(row)} a.grid-view-invoice-row-link`; + this.viewDeliverySlipsRowLink = (row: number) => `${this.actionsColumn(row)} a.grid-view-delivery-slip-row-link`; + + // Grid Actions + this.gridActionButton = '#order-grid-actions-button'; + this.gridActionDropDownMenu = '#order-grid-actions-dropdown-menu'; + this.gridActionExportLink = '#order-grid-action-export'; + + // Bulk actions + this.selectAllRowsLabel = `${this.gridPanel} tr.column-filters .md-checkbox`; + this.bulkActionsToggleButton = `${this.gridPanel} button.js-bulk-actions-btn`; + this.bulkUpdateOrdersStatusButton = '#order_grid_bulk_action_change_order_status'; + this.bulkOpenInNewTabsButton = '#order_grid_bulk_action_open_tabs'; + this.tableColumnOrderBulk = (row: number) => `${this.tableRow(row)} td.column-orders_bulk`; + this.tableColumnOrderBulkCheckboxLabel = (row: number) => `${this.tableColumnOrderBulk(row)} .md-checkbox`; + + // Order status modal + this.updateOrdersStatusModal = '#changeOrdersStatusModal'; + this.updateOrdersStatusModalSelect = '#change_orders_status_new_order_status_id'; + this.updateOrdersStatusModalButton = `${this.updateOrdersStatusModal} .modal-footer .js-submit-modal-form-btn`; + + // Pagination selectors + this.paginationBlock = '.pagination-block'; + this.paginationLimitSelect = '#paginator_select_page_limit'; + this.paginationLabel = `${this.gridPanel} .col-form-label`; + this.paginationNextLink = `${this.gridPanel} [data-role=next-page-link]`; + this.paginationPreviousLink = `${this.gridPanel} [data-role=previous-page-link]`; + } + + /* + Methods + */ + /** + * Go to create new order page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToCreateOrderPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.createNewOrderButton); + } + + /** + * Click on lint to export orders to a csv file + * @param page {Page} Browser tab + * @returns {Promise} + */ + async exportDataToCsv(page: Page): Promise { + await Promise.all([ + page.locator(this.gridActionButton).click(), + this.waitForVisibleSelector(page, `${this.gridActionDropDownMenu}.show`), + ]); + + const [downloadPath] = await Promise.all([ + this.clickAndWaitForDownload(page, this.gridActionExportLink), + this.waitForHiddenSelector(page, `${this.gridActionDropDownMenu}.show`), + ]); + + return downloadPath; + } + + /** + * Filter Orders + * @param page {Page} Browser tab + * @param filterType {string} Type of filter + * @param filterBy {string} Column to filter with + * @param value {string} Value to filter + * @returns {Promise} + */ + async filterOrders(page: Page, filterType: string, filterBy: string, value: string = ''): Promise { + await this.resetFilter(page); + switch (filterType) { + case 'input': + await this.setValue(page, this.filterColumn(filterBy), value); + break; + case 'select': + await this.selectByVisibleText(page, this.filterColumn(filterBy), value); + break; + default: + // Do nothing + } + // click on search + await page.locator(this.filterSearchButton).click(); + await page.waitForURL(`**/?order**filters**${filterBy}**`, {waitUntil: 'domcontentloaded'}); + } + + /** + * Filter orders by date from and date to + * @param page {Page} Browser tab + * @param dateFrom {string} Date from to filter with + * @param dateTo {string} Date to filter with + * @returns {Promise} + */ + async filterOrdersByDate(page: Page, dateFrom: string, dateTo: string): Promise { + await page.locator(this.filterColumn('date_add_from')).fill(dateFrom); + await page.locator(this.filterColumn('date_add_to')).fill(dateTo); + // click on search + await this.clickAndWaitForURL(page, this.filterSearchButton); + } + + /** + * Reset filter in orders + * @param page {Page} Browser tab + * @returns {Promise} + */ + async resetFilter(page: Page): Promise { + if (await this.elementVisible(page, this.filterResetButton, 2000)) { + await this.clickAndWaitForURL(page, this.filterResetButton); + } + } + + /** + * Reset Filter And get number of elements in list + * @param page {Page} Browser tab + * @returns {Promise} + */ + async resetAndGetNumberOfLines(page: Page): Promise { + await this.resetFilter(page); + return this.getNumberOfElementInGrid(page); + } + + /** + * Get number of orders in grid + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getNumberOfElementInGrid(page: Page): Promise { + return this.getNumberFromText(page, this.gridHeaderTitle); + } + + /** + * Go to orders Page + * @param page {Page} Browser tab + * @param orderRow {number} Order row on table + * @returns {Promise} + */ + async goToOrder(page: Page, orderRow: number): Promise { + await this.clickAndWaitForURL(page, this.viewRowLink(orderRow)); + } + + /** + * Get text from Column + * @param page {Page} Browser tab + * @param columnName {string} Column name on table + * @param row {number} Order row in table + * @returns {Promise} + */ + async getTextColumn(page: Page, columnName: string, row: number = 1): Promise { + if (columnName === 'osname') { + return this.getTextContent(page, this.updateStatusInTableButton(row)); + } + if (columnName === 'id_order') { + return (await this.getNumberFromText(page, this.tableColumn(row, 'id_order'))).toString(); + } + + return this.getTextContent(page, this.tableColumn(row, columnName)); + } + + /** + * Get order ID from table + * @param page {Page} Browser tab + * @param row {number} Order row in table + * @returns {Promise} + */ + async getOrderIDNumber(page: Page, row: number = 1): Promise { + return this.getNumberFromText(page, this.tableColumn(row, 'id_order')); + } + + /** + * Get all row information from orders table + * @param page {Page} Browser tab + * @param row {number} Order row on table + * @returns {Promise<{id: number, reference: string, newClient:string, delivery: string, customer: string, + * totalPaid: string, payment: string, status: string}>} + */ + async getOrderFromTable(page: Page, row: number) { + return { + id: parseFloat(await this.getTextColumn(page, 'id_order', row)), + reference: await this.getTextColumn(page, 'reference', row), + newClient: await this.getTextColumn(page, 'new', row), + delivery: await this.getTextColumn(page, 'country_name', row), + customer: await this.getTextColumn(page, 'customer', row), + totalPaid: await this.getTextColumn(page, 'total_paid_tax_incl', row), + payment: await this.getTextColumn(page, 'payment', row), + status: await this.getTextColumn(page, 'osname', row), + }; + } + + /** + * Get number of orders in page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getNumberOfOrdersInPage(page: Page): Promise { + return page.locator(this.tableRows).count(); + } + + /** + * Get column content in all rows + * @param page {Page} Browser tab + * @param column {string} Column name on table + * @returns {Promise>} + */ + async getAllRowsColumnContent(page: Page, column: string): Promise { + let rowContent: string; + const rowsNumber: number = await this.getNumberOfOrdersInPage(page); + const allRowsContentTable: string[] = []; + + for (let i = 1; i <= rowsNumber; i++) { + if (column === 'total_paid_tax_incl') { + rowContent = (await this.getOrderATIPrice(page, i)).toString(); + } else { + rowContent = await this.getTextColumn(page, column, i); + } + allRowsContentTable.push(rowContent); + } + + return allRowsContentTable; + } + + /** + * Get order from table in csv format + * @param page {Page} Browser tab + * @param row {number} Order row on table + * @returns {Promise} + */ + async getOrderInCsvFormat(page: Page, row: number): Promise { + const order = await this.getOrderFromTable(page, row); + + return `${order.id};` + + `${order.reference};` + + `${order.newClient === 'Yes' ? 1 : 0};` + + `${order.delivery.split(' ').length > 1 ? `"${order.delivery}";` : `${order.delivery};`}` + + `"${order.customer}";` + + `${order.totalPaid};` + + `${order.payment.split(' ').length > 1 ? `"${order.payment}";` : `${order.payment};`}` + + `${order.status.split(' ').length > 1 ? `"${order.status}";` : `${order.status};`}`; + } + + /** + * Set order status + * @param page {Page} Browser tab + * @param row {number} Order row in table + * @param status {OrderStatusData} Order status on table + * @returns {Promise} + */ + async setOrderStatus(page: Page, row: number, status: OrderStatusData): Promise { + await Promise.all([ + page.locator(this.updateStatusInTableButton(row)).click(), + this.waitForVisibleSelector(page, `${this.updateStatusInTableDropdown(row)}.show`), + ]); + await this.clickAndWaitForURL(page, this.updateStatusInTableDropdownChoice(row, status.id)); + await this.elementNotVisible(page, this.updateStatusInTableDropdownChoice(row, status.id), 2000); + + return this.getAlertSuccessBlockParagraphContent(page); + } + + /** + * Click on view invoice to download it + * @param page {Page} Browser tab + * @param row {number} Order row on table + * @returns {Promise} + */ + async downloadInvoice(page: Page, row: number): Promise { + return this.clickAndWaitForDownload(page, this.viewInvoiceRowLink(row)); + } + + /** + * Click on view delivery slip to download it + * @param page {Page} Browser tab + * @param row {number} Order row on table + * @returns {Promise} + */ + async downloadDeliverySlip(page: Page, row: number): Promise { + return this.clickAndWaitForDownload(page, this.viewDeliverySlipsRowLink(row)); + } + + /** + * Click on customer link to open view page in a new tab + * @param page {Page} Browser tab + * @param row {number} Order row on table + * @returns {Promise} New browser tab to work with + */ + async viewCustomer(page: Page, row: number): Promise { + return this.openLinkWithTargetBlank( + page, + `${this.tableColumn(row, 'customer')} a`, + this.userProfileIcon, + ); + } + + /** + * Get order total price + * @param page {Page} Browser tab + * @param row {number} Order row on table + * @returns {Promise} + */ + async getOrderATIPrice(page: Page, row: number = 1): Promise { + // Delete the first character (currency symbol) before getting price ATI + return parseFloat((await this.getTextColumn(page, 'total_paid_tax_incl', row)).substring(1)); + } + + /* Bulk actions methods */ + /** + * Select all orders + * @param page {Page} Browser tab + * @returns {Promise} + */ + async selectAllOrders(page: Page): Promise { + await Promise.all([ + this.waitForSelectorAndClick(page, this.selectAllRowsLabel), + this.waitForVisibleSelector(page, `${this.bulkActionsToggleButton}:not([disabled])`), + ]); + } + + /** + * Select some orders + * @param page {Page} Browser tab + * @param rows {Array} Array of which orders rows to change + * @returns {Promise} + */ + async selectOrdersRows(page: Page, rows: number[] = []): Promise { + for (let i = 0; i < rows.length; i++) { + await page.locator(this.tableColumnOrderBulkCheckboxLabel(rows[i])).click(); + } + await this.waitForVisibleSelector(page, `${this.bulkActionsToggleButton}:not([disabled])`); + } + + /** + * Click on bulk actions button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnBulkActionsButton(page: Page): Promise { + await Promise.all([ + page.locator(this.bulkActionsToggleButton).click(), + this.waitForVisibleSelector(page, `${this.bulkActionsToggleButton}[aria-expanded='true']`), + ]); + } + + /** + * Bulk open in new tabs + * @param page {Page} Browser tab + * @param isAllOrders {boolean} True if want to update all orders status + * @param rows {Array} Array of which orders rows to change (if allOrders = false) + * @returns {Promise} + */ + async bulkOpenInNewTabs(page: Page, isAllOrders: boolean = true, rows: number[] = []): Promise { + // Select all orders or some + if (isAllOrders) { + await this.selectAllOrders(page); + } else { + await this.selectOrdersRows(page, rows); + } + + // Open bulk actions button + await this.clickOnBulkActionsButton(page); + + // Click on open in new tabs + return this.openLinkWithTargetBlank(page, this.bulkOpenInNewTabsButton); + } + + /** + * Bulk change orders status + * @param page {Page} Browser tab + * @param status {string} New status to give to orders + * @param isAllOrders {boolean} True if want to update all orders status + * @param rows {Array|boolean} Array of which orders rows to change (if allOrders = false) + * @returns {Promise} + */ + async bulkUpdateOrdersStatus(page: Page, status: string, isAllOrders: boolean = true, rows: number[] = []): Promise { + // Select all orders or some + if (isAllOrders) { + await this.selectAllOrders(page); + } else { + await this.selectOrdersRows(page, rows); + } + + // Open bulk actions button + await this.clickOnBulkActionsButton(page); + + // Click on change order status button + await Promise.all([ + page.locator(this.bulkUpdateOrdersStatusButton).click(), + this.waitForVisibleSelector(page, `${this.updateOrdersStatusModal}:not([aria-hidden='true'])`), + ]); + + // Select new orders status in modal and confirm update + await this.selectByVisibleText(page, this.updateOrdersStatusModalSelect, status); + await page.locator(this.updateOrdersStatusModalButton).click(); + return this.getAlertSuccessBlockParagraphContent(page); + } + + /* Sort functions */ + /** + * Sort table by clicking on column name + * @param page {Page} Browser tab + * @param sortBy {string} Column to sort with + * @param sortDirection {string} Sort direction asc or desc + * @returns {Promise} + */ + async sortTable(page: Page, sortBy: string, sortDirection: string): Promise { + const sortColumnDiv = `${this.sortColumnDiv(sortBy)}[data-sort-direction='${sortDirection}']`; + const sortColumnSpanButton = this.sortColumnSpanButton(sortBy); + + let i = 0; + while (await this.elementNotVisible(page, sortColumnDiv, 2000) && i < 2) { + await this.clickAndWaitForURL(page, sortColumnSpanButton); + i += 1; + } + + await this.waitForVisibleSelector(page, sortColumnDiv, 20000); + } + + /* Pagination methods */ + /** + * Get pagination label + * @param page {Page} Browser tab + * @return {Promise} + */ + async getPaginationLabel(page: Page): Promise { + return this.getTextContent(page, this.paginationLabel); + } + + /** + * Select pagination limit + * @param page {Page} Browser tab + * @param number {number} Value of pagination limit to select + * @returns {Promise} + */ + async selectPaginationLimit(page: Page, number: number): Promise { + const currentUrl: string = page.url(); + + await Promise.all([ + this.selectByVisibleText(page, this.paginationLimitSelect, number), + page.waitForURL((url: URL): boolean => url.toString() !== currentUrl, {waitUntil: 'networkidle'}), + ]); + + return this.getPaginationLabel(page); + } + + /** + * Click on next + * @param page {Page} Browser tab + * @returns {Promise} + */ + async paginationNext(page: Page): Promise { + await this.clickAndWaitForURL(page, this.paginationNextLink); + + return this.getPaginationLabel(page); + } + + /** + * Click on previous + * @param page {Page} Browser tab + * @returns {Promise} + */ + async paginationPrevious(page: Page): Promise { + await this.clickAndWaitForURL(page, this.paginationPreviousLink); + + return this.getPaginationLabel(page); + } + + /* Preview order methods */ + /** + * Preview order + * @param page {Page} Browser tab + * @param row {number} Row in orders table + * @returns {Promise} + */ + async previewOrder(page: Page, row: number = 1): Promise { + await page.locator(this.tableColumn(row, 'id_order')).hover(); + await this.waitForSelectorAndClick(page, this.expandIcon(row)); + + return this.elementVisible(page, this.previewRow, 2000); + } + + /** + * Get shipping details + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getShippingDetails(page: Page): Promise { + return this.getTextContent(page, this.shippingDetails); + } + + /** + * Get customer email + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getCustomerEmail(page: Page): Promise { + return this.getTextContent(page, this.customerEmail); + } + + /** + * Get customer invoice address + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getCustomerInvoiceAddressDetails(page: Page): Promise { + return this.getTextContent(page, this.invoiceDetails); + } + + /** + * Get products number from table + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductsNumberFromTable(page: Page): Promise { + return this.getNumberFromText(page, this.productsNumber); + } + + /** + * Get product details from table + * @param page {Page} Browser tab + * @param row {number} Row in products table + * @returns {Promise} + */ + async getProductDetailsFromTable(page: Page, row: number = 1): Promise { + return this.getTextContent(page, this.productRowFromTable(row)); + } + + /** + * Click on more link + * @param page {Page} Browser tab + * @param row {number} Row in Products table + * @returns {Promise} + */ + async clickOnMoreLink(page: Page, row: number = 12): Promise { + await this.waitForSelectorAndClick(page, this.previewMoreProductsLink(row)); + await this.waitForVisibleSelector(page, this.productRowFromTable(row - 1)); + } + + /** + * Open orders details + * @param page {Page} Browser tab + * @returns {Promise} + */ + async openOrderDetails(page: Page): Promise { + await this.clickAndWaitForURL(page, this.previewOrderButton); + } +} + +export default new Order(); diff --git a/src/versions/8.0.0/pages/FO/classic/cart/index.ts b/src/versions/8.0.0/pages/FO/classic/cart/index.ts new file mode 100644 index 00000000..26c5dd76 --- /dev/null +++ b/src/versions/8.0.0/pages/FO/classic/cart/index.ts @@ -0,0 +1,458 @@ +// Import pages +import type {FoCartPageInterface} from '@interfaces/FO/cart'; +import FOBasePage from '@pages/FO/FOBasePage'; + +// Import data +import type {ProductAttribute} from '@data/types/product'; + +import type {Page} from 'playwright'; + +/** + * Cart page, contains functions that can be used on the page + * @class + * @extends FOBasePage + */ +class CartPage extends FOBasePage implements FoCartPageInterface { + public readonly pageTitle: string; + + public readonly cartRuleAlreadyUsedErrorText: string; + + public readonly cartRuleAlreadyInYourCartErrorText: string; + + public readonly cartRuleNotExistingErrorText: string; + + public readonly cartRuleMustEnterVoucherErrorText: string; + + public readonly cartRuleLimitUsageErrorText: string; + + public readonly cartRuleAlertMessageText: string; + + public readonly alertChooseDeliveryAddressWarningText: string; + + public readonly noItemsInYourCartMessage: string; + + protected productItem: (number: number) => string; + + protected productName: (number: number) => string; + + protected productRegularPrice: (number: number) => string; + + protected productDiscountPercentage: (number: number) => string; + + protected productPrice: (number: number) => string; + + protected productTotalPrice: (number: number) => string; + + protected productQuantity: (number: number) => string; + + protected productQuantityScrollUpButton: (number: number) => string; + + protected productQuantityScrollDownButton: (number: number) => string; + + protected productSize: (number: number) => string; + + protected productColor: (number: number) => string; + + protected productImage: (number: number) => string; + + protected readonly deleteIcon: (number: number) => string; + + private readonly itemsNumber: string; + + protected noItemsInYourCartSpan: string; + + protected alertMessage: string; + + private readonly subtotalDiscountValueSpan: string; + + private readonly cartTotalATI: string; + + private readonly blockPromoDiv: string; + + protected cartSummaryLine: (line: number) => string; + + protected cartRuleName: (line: number) => string; + + protected discountValue: (line: number) => string; + + protected promoCodeLink: string; + + private readonly promoCodeBlock: string; + + protected promoInput: string; + + private readonly addPromoCodeButton: string; + + private readonly promoCodeRemoveIcon: (line: number) => string; + + private readonly cartRuleAlertMessage: string; + + private readonly highlightPromoCodeBlock: string; + + private readonly highlightPromoCode: string; + + public readonly cartRuleChooseCarrierAlertMessageText: string; + + public readonly cartRuleCannotUseVoucherAlertMessageText: string; + + public readonly minimumAmountErrorMessage: string; + + public readonly errorNotificationForProductQuantity: string; + + private readonly alertWarning: string; + + protected proceedToCheckoutButton: string; + + private readonly disabledProceedToCheckoutButton: string; + + private readonly alertPromoCode: string; + + /** + * @constructs + * Setting up texts and selectors to use on cart page + */ + constructor(theme: string = 'classic') { + super(theme); + + this.pageTitle = 'Cart'; + this.cartRuleAlreadyUsedErrorText = 'This voucher has already been used'; + this.cartRuleAlreadyInYourCartErrorText = 'This voucher is already in your cart'; + this.cartRuleNotExistingErrorText = 'This voucher does not exist.'; + this.cartRuleMustEnterVoucherErrorText = 'You must enter a voucher code.'; + this.cartRuleLimitUsageErrorText = 'You cannot use this voucher anymore (usage limit reached)'; + this.cartRuleAlertMessageText = 'You cannot use this voucher'; + this.alertChooseDeliveryAddressWarningText = 'You must choose a delivery address' + + ' before applying this voucher to your order'; + this.noItemsInYourCartMessage = 'There are no more items in your cart'; + this.cartRuleChooseCarrierAlertMessageText = 'You must choose a carrier before applying this voucher to your order'; + this.cartRuleCannotUseVoucherAlertMessageText = 'You cannot use this voucher with this carrier'; + this.minimumAmountErrorMessage = 'The minimum amount to benefit from this promo code is'; + this.errorNotificationForProductQuantity = 'You can only buy 300 "Hummingbird printed t-shirt".' + + ' Please adjust the quantity in your cart to continue.'; + + // Selectors for cart page + // Shopping cart block selectors + this.productItem = (number: number) => `#main li:nth-of-type(${number})`; + this.productName = (number: number) => `${this.productItem(number)} div.product-line-info a`; + this.productRegularPrice = (number: number) => `${this.productItem(number)} span.regular-price`; + this.productDiscountPercentage = (number: number) => `${this.productItem(number)} span.discount-percentage`; + this.productPrice = (number: number) => `${this.productItem(number)} div.current-price span`; + this.productTotalPrice = (number: number) => `${this.productItem(number)} span.product-price`; + this.productQuantity = (number: number) => `${this.productItem(number)} div.input-group ` + + 'input.js-cart-line-product-quantity'; + this.productQuantityScrollUpButton = (number: number) => `${this.productItem(number)} ` + + 'button.js-increase-product-quantity.bootstrap-touchspin-up'; + this.productQuantityScrollDownButton = (number: number) => `${this.productItem(number)} ` + + 'button.js-decrease-product-quantity.bootstrap-touchspin-down'; + this.productSize = (number: number) => `${this.productItem(number)} div.product-line-info.size span.value`; + this.productColor = (number: number) => `${this.productItem(number)} div.product-line-info.color span.value`; + this.productImage = (number: number) => `${this.productItem(number)} span.product-image img`; + this.deleteIcon = (number: number) => `${this.productItem(number)} .remove-from-cart`; + this.noItemsInYourCartSpan = 'div.cart-grid-body div.cart-overview.js-cart span.no-items'; + + // Notifications + this.alertMessage = '#notifications div.notifications-container'; + + // Cart summary block selectors + this.itemsNumber = '#cart-subtotal-products span.label.js-subtotal'; + this.subtotalDiscountValueSpan = '#cart-subtotal-discount span.value'; + this.cartTotalATI = '.cart-summary-totals span.value'; + this.blockPromoDiv = '.block-promo'; + this.cartSummaryLine = (line: number) => `${this.blockPromoDiv} li:nth-child(${line}).cart-summary-line`; + this.cartRuleName = (line: number) => `${this.cartSummaryLine(line)} span.label`; + this.discountValue = (line: number) => `${this.cartSummaryLine(line)} div span`; + + // Promo code selectors + this.promoCodeBlock = '#main div.block-promo'; + this.promoCodeLink = '#main div.block-promo a[href=\'#promo-code\']'; + this.promoInput = '#promo-code input.promo-input'; + this.addPromoCodeButton = '#promo-code button.btn-primary'; + this.promoCodeRemoveIcon = (line: number) => `${this.cartSummaryLine(line)} a[data-link-action='remove-voucher']`; + this.cartRuleAlertMessage = '#promo-code div.alert-danger span.js-error-text'; + this.highlightPromoCodeBlock = `${this.blockPromoDiv} div ul.promo-discounts`; + this.highlightPromoCode = `${this.blockPromoDiv} li span.code`; + + this.alertWarning = '.checkout.cart-detailed-actions.card-block div.alert.alert-warning'; + + this.proceedToCheckoutButton = '#main div.checkout a'; + this.disabledProceedToCheckoutButton = '#main div.checkout .disabled'; + + this.alertPromoCode = '#promo-code div div span'; + } + + /* + Methods + */ + /** + * Get notification message + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getNotificationMessage(page: Page): Promise { + return this.getTextContent(page, this.alertMessage); + } + + /** + * Get no items in your cart message + * @param page {Page} Browser tab + */ + async getNoItemsInYourCartMessage(page: Page): Promise { + return this.getTextContent(page, this.noItemsInYourCartSpan); + } + + /** + * + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductsNumber(page: Page): Promise { + return this.getNumberFromText(page, this.itemsNumber); + } + + /** + * Get Product detail from cart + * @param page {Page} Browser tab + * @param row {number} Row number in the table + * @returns {Promise<{discountPercentage: string, image: string|null, quantity: number, totalPrice: number, + * price: number, regularPrice: number, name: string}>} + */ + async getProductDetail(page: Page, row: number): Promise<{ + discountPercentage: string, + image: string | null, + quantity: number, + totalPrice: number, + price: number, + regularPrice: number, + name: string, + }> { + return { + name: await this.getTextContent(page, this.productName(row)), + regularPrice: await this.getPriceFromText(page, this.productRegularPrice(row)), + price: await this.getPriceFromText(page, this.productPrice(row)), + discountPercentage: await this.getTextContent(page, this.productDiscountPercentage(row)), + image: await this.getAttributeContent(page, this.productImage(row), 'src'), + quantity: parseFloat(await this.getAttributeContent(page, this.productQuantity(row), 'value') ?? ''), + totalPrice: await this.getPriceFromText(page, this.productTotalPrice(row)), + }; + } + + /** + * Get product attributes + * @param page {Page} Browser tab + * @param row {number} Row number in the table + * @returns {Promise} + */ + async getProductAttributes(page: Page, row: number): Promise { + return [ + { + name: 'size', + value: await this.getTextContent(page, this.productSize(row)), + }, + { + name: 'color', + value: await this.getTextContent(page, this.productColor(row)), + }, + ]; + } + + /** + * Click on Proceed to checkout button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnProceedToCheckout(page: Page): Promise { + await this.waitForVisibleSelector(page, this.proceedToCheckoutButton); + await this.clickAndWaitForLoadState(page, this.proceedToCheckoutButton); + await this.elementNotVisible(page, this.proceedToCheckoutButton, 2000); + } + + /** + * To edit the product quantity + * @param page {Page} Browser tab + * @param productID {number} ID of the product + * @param quantity {number} New quantity of the product + * @returns {Promise} + */ + async editProductQuantity(page: Page, productID: number, quantity: number | string): Promise { + await this.setValue(page, this.productQuantity(productID), quantity); + // click on price to see that its changed + await page.locator(this.productPrice(productID)).click(); + } + + /** + * Set product quantity + * @param page {Page} Browser tab + * @param productRow {number} Row of the product + * @param quantity {number} New quantity of the product + * @returns {Promise} + */ + async setProductQuantity(page: Page, productRow: number = 1, quantity: number = 1): Promise { + const productQuantity: number = parseInt(await this.getAttributeContent(page, this.productQuantity(productRow), 'value'), 10); + + if (productQuantity < quantity) { + for (let i: number = 1; i < quantity; i++) { + await page.locator(this.productQuantityScrollUpButton(productRow)).click(); + await page.waitForTimeout(1000); + } + } else { + for (let i: number = productQuantity; i > quantity; i--) { + await page.locator(this.productQuantityScrollDownButton(productRow)).click(); + await page.waitForTimeout(1000); + } + } + + return parseInt(await this.getAttributeContent(page, this.productQuantity(productRow), 'value'), 10); + } + + /** + * Delete product + * @param page {Page} Browser tab + * @param productID {number} ID of the product + * @returns {Promise} + */ + async deleteProduct(page: Page, productID: number): Promise { + await this.waitForSelectorAndClick(page, this.deleteIcon(productID)); + } + + /** + * Get All tax included price + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getATIPrice(page: Page): Promise { + return this.getPriceFromText(page, this.cartTotalATI, 2000); + } + + /** + * Get subtotal discount value + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getSubtotalDiscountValue(page: Page): Promise { + return this.getPriceFromText(page, this.subtotalDiscountValueSpan, 2000); + } + + /** + * Is proceed to checkout button disabled + * @param page {Page} Browser tab + * @returns {boolean} + */ + isProceedToCheckoutButtonDisabled(page: Page): Promise { + return this.elementVisible(page, this.disabledProceedToCheckoutButton, 1000); + } + + /** + * Is alert warning for minimum purchase total visible + * @param page {Page} Browser tab + * @returns {boolean} + */ + isAlertWarningForMinimumPurchaseVisible(page: Page): Promise { + return this.elementVisible(page, this.alertWarning, 1000); + } + + /** + * Get alert warning + * @param page {Page} Browser tab + * @returns {Promise} + */ + getAlertWarning(page: Page): Promise { + return this.getTextContent(page, this.alertWarning); + } + + /** + * Get alert warning + * @param page {Page} Browser tab + * @returns {Promise} + */ + getAlertWarningForPromoCode(page: Page): Promise { + return this.getTextContent(page, this.alertPromoCode); + } + + /** + * Is cart rule name visible + * @param page {Page} Browser tab + * @param line {number} Cart summary line + * @returns {Promise} + */ + isCartRuleNameVisible(page: Page, line: number = 1): Promise { + return this.elementVisible(page, this.cartRuleName(line), 1000); + } + + /** + * Set promo code + * @param page {Page} Browser tab + * @param code {string} The promo code + * @param clickOnPromoCodeLink {boolean} True if we need to click on promo code link + * @returns {Promise} + */ + async addPromoCode(page: Page, code: string, clickOnPromoCodeLink: boolean = true): Promise { + if (clickOnPromoCodeLink) { + await page.locator(this.promoCodeLink).click(); + } + await this.setValue(page, this.promoInput, code); + await page.locator(this.addPromoCodeButton).click(); + await page.waitForTimeout(1000); + } + + /** + * Get highlight promo code + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getHighlightPromoCode(page: Page): Promise { + return this.getTextContent(page, this.highlightPromoCodeBlock); + } + + /** + * Click on highlight promo code + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnPromoCode(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.highlightPromoCode); + await page.locator(this.addPromoCodeButton).click(); + } + + /** + * Get cart rule name + * @param page {Page} Browser tab + * @param line {number} Cart summary line + * @returns {Promise} + */ + getCartRuleName(page: Page, line: number = 1): Promise { + return this.getTextContent(page, this.cartRuleName(line)); + } + + /** + * Get cart rule error text + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getCartRuleErrorMessage(page: Page): Promise { + return this.getTextContent(page, this.cartRuleAlertMessage); + } + + /** + * Get discount value + * @param page {Page} Browser tab + * @param line {number} Cart summary line + * @returns {Promise} + */ + getDiscountValue(page: Page, line: number = 1): Promise { + return this.getPriceFromText(page, this.discountValue(line), 2000); + } + + /** + * Remove voucher + * @param page {Page} Browser tab + * @param line {number} Cart summary line + * @returns {Promise} + */ + async removeVoucher(page: Page, line: number = 1): Promise { + await this.waitForSelectorAndClick(page, this.promoCodeRemoveIcon(line)); + await this.waitForHiddenSelector(page, this.promoCodeRemoveIcon(line)); + } +} + +const cartPage = new CartPage(); +export {cartPage, CartPage}; diff --git a/src/versions/8.0.0/pages/FO/classic/checkout/index.ts b/src/versions/8.0.0/pages/FO/classic/checkout/index.ts new file mode 100644 index 00000000..cfa03b3a --- /dev/null +++ b/src/versions/8.0.0/pages/FO/classic/checkout/index.ts @@ -0,0 +1,1250 @@ +// Import pages +import FOBasePage from '@pages/FO/FOBasePage'; + +// Import data +import AddressData from '@data/faker/address'; +import CustomerData from '@data/faker/customer'; +import CarrierData from '@data/faker/carrier'; + +import {ProductDetailsBasic} from '@data/types/product'; +import {FoCheckoutPageInterface} from '@interfaces/FO/checkout'; + +import type {Page} from 'playwright'; + +/** + * Checkout page, contains functions that can be used on the page + * @class + * @extends FOBasePage + */ +class CheckoutPage extends FOBasePage implements FoCheckoutPageInterface { + public readonly deleteAddressSuccessMessage: string; + + private readonly successAlert: string; + + private readonly checkoutPageBody: string; + + protected stepFormSuccess: string; + + public readonly messageIfYouSignOut: string; + + public readonly authenticationErrorMessage: string; + + private readonly paymentStepSection: string; + + private readonly paymentOptionInput: (name: string) => string; + + private readonly conditionToApproveLabel: string; + + private readonly conditionToApproveCheckbox: string; + + private readonly termsOfServiceLink: string; + + private readonly termsOfServiceModalDiv: string; + + private readonly paymentConfirmationButton: string; + + protected shippingValueSpan: string; + + private readonly blockPromoDiv: string; + + private readonly cartSummaryLine: (line: number) => string; + + private readonly cartRuleName: (line: number) => string; + + private readonly discountValue: (line: number) => string; + + private readonly noPaymentNeededElement: string; + + protected itemsNumber: string; + + protected showDetailsLink: string; + + private readonly productList: string; + + protected productRowLink: (productRow: number) => string; + + protected productDetailsImage: (productRow: number) => string; + + protected productDetailsName: (productRow: number) => string; + + protected productDetailsQuantity: (productRow: number) => string; + + protected productDetailsPrice: (productRow: number) => string; + + protected productDetailsAttributes: (productRow: number) => string; + + public readonly noPaymentNeededText: string; + + private readonly promoCodeArea: string; + + private readonly checkoutHavePromoInputArea: string; + + private readonly checkoutPromoCodeAddButton: string; + + public personalInformationStepForm: string; + + protected forgetPasswordLink: string; + + private readonly activeLink: string; + + private readonly checkoutSignInLink: string; + + private readonly checkoutGuestForm: string; + + private readonly checkoutGuestGenderInput: (pos: number) => string; + + private readonly checkoutGuestFirstnameInput: string; + + private readonly checkoutGuestLastnameInput: string; + + private readonly checkoutGuestEmailInput: string; + + private readonly checkoutGuestPasswordInput: string; + + private readonly checkoutGuestBirthdayInput: string; + + private readonly checkoutGuestOptinCheckbox: string; + + private readonly checkoutGuestCustomerPrivacyCheckbox: string; + + private readonly checkoutGuestNewsletterCheckbox: string; + + private readonly checkoutGuestGdprCheckbox: string; + + private readonly checkoutGuestContinueButton: string; + + protected signInHyperLink: string; + + protected checkoutSummary: string; + + private readonly checkoutPromoBlock: string; + + private readonly checkoutHavePromoCodeButton: string; + + private readonly checkoutRemoveDiscountLink: string; + + protected checkoutLoginForm: string; + + private readonly emailInput: string; + + private readonly passwordInput: string; + + protected personalInformationContinueButton: string; + + private readonly logoutMessage: string; + + private readonly personalInformationLogoutLink: string; + + protected personalInformationCustomerIdentity: string; + + protected personalInformationEditLink: string; + + protected loginErrorMessage: string; + + protected addressStepSection: string; + + private readonly addressStepContent: string; + + private readonly addressStepCreateAddressForm: string; + + private readonly addressStepAliasInput: string; + + private readonly addressStepCompanyInput: string; + + private readonly addressStepAddress1Input: string; + + private readonly addressStepPostCodeInput: string; + + private readonly addressStepCityInput: string; + + protected addressStepCountrySelect: string; + + private readonly addressStepPhoneInput: string; + + protected stateInput: string; + + private readonly addressStepUseSameAddressCheckbox: string; + + private readonly addressStepContinueButton: string; + + private readonly addressStepSubmitButton: string; + + protected addressStepEditButton: string; + + protected addAddressButton: string; + + protected addInvoiceAddressButton: string; + + private readonly differentInvoiceAddressLink: string; + + private readonly invoiceAddressesBlock: string; + + private readonly invoiceAddressSection: string; + + protected deliveryStepSection: string; + + protected deliveryStepEditButton: string; + + private readonly deliveryStepCarriersList: string; + + protected deliveryOptions: string; + + private readonly deliveryOptionsRadioButton: string; + + protected deliveryOptionLabel: (id: number) => string; + + private readonly deliveryOptionNameSpan: (id: number) => string; + + protected deliveryOptionAllNamesSpan: string; + + private readonly deliveryOptionAllPricesSpan: string; + + private readonly deliveryMessage: string; + + private readonly deliveryStepContinueButton: string; + + protected deliveryOption: (carrierID: number) => string; + + protected deliveryStepCarrierName: (carrierID: number) => string; + + protected deliveryStepCarrierDelay: (carrierID: number) => string; + + protected deliveryStepCarrierPrice: (carrierID: number) => string; + + private readonly deliveryAddressBlock: string; + + private readonly deliveryAddressSection: string; + + protected deliveryAddressPosition: (position: number) => string; + + protected invoiceAddressPosition: (position: number) => string; + + protected deliveryAddressEditButton: (addressID: number) => string; + + protected deliveryAddressDeleteButton: (addressID: number) => string; + + private readonly deliveryAddressRadioButton: (addressID: number) => string; + + private readonly invoiceAddressRadioButton: (addressID: number) => string; + + private readonly cartTotalATI: string; + + private readonly cartRuleAlertMessage: string; + + private readonly cartRuleAlertMessageText: string; + + private readonly giftCheckbox: string; + + private readonly giftMessageTextarea: string; + + private readonly recyclableGiftCheckbox: string; + + private readonly cartSubtotalGiftWrappingDiv: string; + + private readonly cartSubtotalGiftWrappingValueSpan: string; + + /** + * @constructs + * Setting up texts and selectors to use on checkout page + */ + constructor(theme: string = 'classic') { + super(theme); + this.cartRuleAlertMessageText = 'You cannot use this voucher with this carrier'; + this.deleteAddressSuccessMessage = 'Address successfully deleted.'; + this.noPaymentNeededText = 'No payment needed for this order'; + this.messageIfYouSignOut = 'If you sign out now, your cart will be emptied.'; + this.authenticationErrorMessage = 'Authentication failed.'; + + // Selectors + this.successAlert = '#notifications article.alert-success'; + this.checkoutPageBody = 'body#checkout'; + this.stepFormSuccess = '.-complete'; + + // Personal information form + this.personalInformationStepForm = '#checkout-personal-information-step'; + // Order as a guest selectors + this.activeLink = `${this.personalInformationStepForm} .nav-link.active`; + this.checkoutSignInLink = `${this.personalInformationStepForm} a[href="#checkout-login-form"]`; + this.checkoutGuestForm = '#checkout-guest-form'; + this.checkoutGuestGenderInput = (pos) => `${this.checkoutGuestForm} input[name='id_gender'][value='${pos}']`; + this.checkoutGuestFirstnameInput = `${this.checkoutGuestForm} input[name='firstname']`; + this.checkoutGuestLastnameInput = `${this.checkoutGuestForm} input[name='lastname']`; + this.checkoutGuestEmailInput = `${this.checkoutGuestForm} input[name='email']`; + this.checkoutGuestPasswordInput = `${this.checkoutGuestForm} input[name='password']`; + this.checkoutGuestBirthdayInput = `${this.checkoutGuestForm} input[name='birthday']`; + this.checkoutGuestOptinCheckbox = `${this.checkoutGuestForm} input[name='optin']`; + this.checkoutGuestCustomerPrivacyCheckbox = `${this.checkoutGuestForm} input[name='customer_privacy']`; + this.checkoutGuestNewsletterCheckbox = `${this.checkoutGuestForm} input[name='newsletter']`; + this.checkoutGuestGdprCheckbox = `${this.checkoutGuestForm} input[name='psgdpr']`; + this.checkoutGuestContinueButton = `${this.checkoutGuestForm} button[name='continue']`; + // Sign in selectors + this.signInHyperLink = `${this.personalInformationStepForm} a[href="#checkout-login-form"]`; + this.forgetPasswordLink = '#login-form div.forgot-password a[href*=password-recovery]'; + this.checkoutLoginForm = `${this.personalInformationStepForm} #checkout-login-form`; + this.emailInput = `${this.checkoutLoginForm} input[name='email']`; + this.passwordInput = `${this.checkoutLoginForm} input[name='password']`; + this.personalInformationContinueButton = `${this.checkoutLoginForm} #login-form footer button`; + this.logoutMessage = `${this.personalInformationStepForm} p:nth-child(3) small`; + this.personalInformationLogoutLink = `${this.personalInformationStepForm} a[href*=mylogout]`; + this.personalInformationCustomerIdentity = `${this.personalInformationStepForm} p.identity`; + this.personalInformationEditLink = `${this.personalInformationStepForm} span.step-edit.text-muted`; + this.loginErrorMessage = `${this.checkoutLoginForm} li.alert-danger`; + + // Addresses step selectors + this.addressStepSection = '#checkout-addresses-step'; + this.addressStepContent = `${this.addressStepSection} div.content`; + this.addressStepCreateAddressForm = `${this.addressStepSection} .js-address-form`; + this.addressStepAliasInput = '#field-alias'; + this.addressStepCompanyInput = '#field-company'; + this.addressStepAddress1Input = '#field-address1'; + this.addressStepPostCodeInput = '#field-postcode'; + this.addressStepCityInput = '#field-city'; + this.addressStepCountrySelect = '#field-id_country'; + this.addressStepPhoneInput = '#field-phone'; + this.stateInput = '#field-id_state'; + this.addressStepUseSameAddressCheckbox = '#use_same_address'; + this.addressStepContinueButton = `${this.addressStepSection} button[name='confirm-addresses']`; + this.addressStepSubmitButton = `${this.addressStepSection} button[type=submit]`; + this.addressStepEditButton = `${this.addressStepSection} span.step-edit`; + this.addAddressButton = '#checkout-addresses-step p.add-address a'; + this.addInvoiceAddressButton = '#checkout-addresses-step p.add-address a[href*="invoice"]'; + this.differentInvoiceAddressLink = '#checkout-addresses-step form a[data-link-action="different-invoice-address"]'; + // Delivery address selectors + this.deliveryAddressBlock = '#delivery-addresses'; + this.deliveryAddressSection = `${this.deliveryAddressBlock} article.js-address-item`; + this.deliveryAddressEditButton = (addressID: number) => `#id_address_delivery-address-${addressID} a.edit-address`; + this.deliveryAddressDeleteButton = (addressID: number) => `#id_address_delivery-address-${addressID} a.delete-address`; + this.deliveryAddressRadioButton = (addressID: number) => `#id_address_delivery-address-${addressID} ` + + 'input[name="id_address_delivery"]'; + // Invoice address selectors + this.invoiceAddressesBlock = '#invoice-addresses'; + this.invoiceAddressSection = `${this.invoiceAddressesBlock} article.js-address-item`; + this.deliveryAddressPosition = (position: number) => `#delivery-addresses article:nth-child(${position})`; + this.invoiceAddressPosition = (position: number) => `#invoice-addresses article:nth-child(${position})`; + this.invoiceAddressRadioButton = (addressID: number) => `#id_address_invoice-address-${addressID}` + + ' input[name="id_address_invoice"]'; + + // Shipping method selectors + this.deliveryStepSection = '#checkout-delivery-step'; + this.deliveryStepEditButton = `${this.deliveryStepSection} span.step-edit`; + this.deliveryStepCarriersList = `${this.deliveryStepSection} .delivery-options-list`; + this.deliveryOptions = '#js-delivery div.delivery-options'; + this.deliveryOptionsRadioButton = 'input[id*=\'delivery_option_\']'; + this.deliveryOptionLabel = (id: number) => `${this.deliveryStepSection} label[for='delivery_option_${id}']`; + this.deliveryOptionNameSpan = (id: number) => `${this.deliveryOptionLabel(id)} span.carrier-name`; + this.deliveryOptionAllNamesSpan = '#js-delivery .delivery-option .carriere-name-container span.carrier-name'; + this.deliveryOptionAllPricesSpan = '#js-delivery .delivery-option span.carrier-price'; + this.deliveryMessage = '#delivery_message'; + this.deliveryStepContinueButton = `${this.deliveryStepSection} button[name='confirmDeliveryOption']`; + this.deliveryOption = (carrierID: number) => `${this.deliveryOptions} label[for=delivery_option_${carrierID}] span.carrier`; + this.deliveryStepCarrierName = (carrierID: number) => `${this.deliveryOption(carrierID)}-name`; + this.deliveryStepCarrierDelay = (carrierID: number) => `${this.deliveryOption(carrierID)}-delay`; + this.deliveryStepCarrierPrice = (carrierID: number) => `${this.deliveryOption(carrierID)}-price`; + + // Payment step selectors + this.paymentStepSection = '#checkout-payment-step'; + this.paymentOptionInput = (name: string) => `${this.paymentStepSection} input[name='payment-option']` + + `[data-module-name='${name}']`; + this.conditionToApproveLabel = `${this.paymentStepSection} #conditions-to-approve label`; + this.conditionToApproveCheckbox = '#conditions_to_approve\\[terms-and-conditions\\]'; + this.termsOfServiceLink = '#cta-terms-and-conditions-0'; + this.termsOfServiceModalDiv = '#modal div.js-modal-content'; + this.paymentConfirmationButton = `${this.paymentStepSection} #payment-confirmation button:not([disabled])`; + this.noPaymentNeededElement = `${this.paymentStepSection} div.content > p.cart-payment-step-not-needed-info`; + + // Checkout summary selectors + this.checkoutSummary = '#js-checkout-summary'; + this.checkoutPromoBlock = `${this.checkoutSummary} div.block-promo`; + this.checkoutHavePromoCodeButton = `${this.checkoutPromoBlock} p.promo-code-button a`; + this.checkoutRemoveDiscountLink = `${this.checkoutPromoBlock} a[data-link-action='remove-voucher'] i`; + this.cartTotalATI = '.cart-summary-totals span.value'; + this.cartRuleAlertMessage = '#promo-code div.alert-danger span.js-error-text'; + this.promoCodeArea = '#promo-code'; + this.checkoutHavePromoInputArea = `${this.promoCodeArea} input.promo-input`; + this.checkoutPromoCodeAddButton = `${this.promoCodeArea} button.btn-primary`; + this.shippingValueSpan = '#cart-subtotal-shipping span.value'; + this.blockPromoDiv = '.block-promo'; + this.cartSummaryLine = (line: number) => `${this.blockPromoDiv} li:nth-child(${line}).cart-summary-line`; + this.cartRuleName = (line: number) => `${this.cartSummaryLine(line)} span.label`; + this.discountValue = (line: number) => `${this.cartSummaryLine(line)} div span`; + + // Cart details selectors + this.itemsNumber = `${this.checkoutSummary} div.cart-summary-products.js-cart-summary-products p:nth-child(1)`; + this.showDetailsLink = `${this.checkoutSummary} div.cart-summary-products.js-cart-summary-products a.js-show-details`; + this.productList = '#cart-summary-product-list'; + this.productRowLink = (productRow: number) => `${this.productList} ul li:nth-child(${productRow})`; + this.productDetailsImage = (productRow: number) => `${this.productRowLink(productRow)} div.media-left a img`; + this.productDetailsName = (productRow: number) => `${this.productRowLink(productRow)} div span.product-name`; + this.productDetailsQuantity = (productRow: number) => `${this.productRowLink(productRow)} ` + + 'div.media-body span.product-quantity'; + this.productDetailsPrice = (productRow: number) => `${this.productRowLink(productRow)} div.media-body ` + + 'span.product-price.float-xs-right'; + this.productDetailsAttributes = (productRow: number) => `${this.productRowLink(productRow)} div.media-body ` + + 'div.product-line-info'; + + // Gift selectors + this.giftCheckbox = '#input_gift'; + this.giftMessageTextarea = '#gift_message'; + this.recyclableGiftCheckbox = '#input_recyclable'; + this.cartSubtotalGiftWrappingDiv = '#cart-subtotal-gift_wrapping'; + this.cartSubtotalGiftWrappingValueSpan = `${this.cartSubtotalGiftWrappingDiv} span.value`; + } + + /* + Methods + */ + + /** + * Check if we are in checkout Page + * @param page {Page} Browser tab + * @return {Promise} + */ + async isCheckoutPage(page: Page): Promise { + return this.elementVisible(page, this.checkoutPageBody, 1000); + } + + /** + * Check if step is completed + * @param page {Page} Browser tab + * @param stepSelector {string} String of the step to check + * @returns {Promise} + */ + async isStepCompleted(page: Page, stepSelector: string): Promise { + return this.elementVisible(page, `${stepSelector}${this.stepFormSuccess}`, 1000); + } + + /** + * Click on show details link + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnShowDetailsLink(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.showDetailsLink); + + return this.elementVisible(page, `${this.showDetailsLink}[aria-expanded=true]`, 1000); + } + + /** + * Get product details + * @param page {Page} Browser tab + * @param productRow {number} Product row in details block + * @returns {Promise + */ + async getProductDetails(page: Page, productRow: number): Promise { + return { + image: await this.getAttributeContent(page, this.productDetailsImage(productRow), 'src') ?? '', + name: await this.getTextContent(page, this.productDetailsName(productRow)), + quantity: await this.getNumberFromText(page, this.productDetailsQuantity(productRow)), + price: await this.getPriceFromText(page, this.productDetailsPrice(productRow)), + }; + } + + /** + * Get product details + * @param page {Page} Browser tab + * @param productRow {number} Product row in details block + * @returns {Promise { + return this.getTextContent(page, this.productDetailsAttributes(productRow)); + } + + /** + * Get product details + * @param page {Page} Browser tab + * @param productRow {number} Product row in details block + * @returns {Promise { + return this.clickAndWaitForURL(page, this.productDetailsImage(productRow)); + } + + /** + * Get product details + * @param page {Page} Browser tab + * @param productRow {number} Product row in details block + * @returns {Promise { + return this.openLinkWithTargetBlank(page, this.productDetailsName(productRow)); + } + + /** + * Get items number + * @param page {Page} Browser tab + * @returns {Promise { + return this.getTextContent(page, this.itemsNumber); + } + + // Methods for personal information step + /** + * Click on sign in + * @param page {Page} Browser tab + * @return {Promise} + */ + async clickOnSignIn(page: Page): Promise { + await page.locator(this.signInHyperLink).click(); + } + + /** + * Logout customer + * @param page {Page} Browser tab + * @return {Promise} + */ + async logOutCustomer(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.personalInformationLogoutLink); + + return this.isStepCompleted(page, this.personalInformationStepForm); + } + + /** + * Go to password reminder page + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToPasswordReminderPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.forgetPasswordLink); + } + + /** + * Click on edit personal information step + * @param page {Page} Browser tab + * @return {Promise} + */ + async clickOnEditPersonalInformationStep(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.personalInformationEditLink); + } + + /** + * Get customer identity + * @param page {Page} Browser tab + * @return {Promise} + */ + async getCustomerIdentity(page: Page): Promise { + return this.getTextContent(page, this.personalInformationCustomerIdentity); + } + + /** + * Get logout message + * @param page {Page} Browser tab + * @return {Promise} + */ + async getLogoutMessage(page: Page): Promise { + return this.getTextContent(page, this.logoutMessage); + } + + /** + * Login in FO + * @param page {Page} Browser tab + * @param customer {object} Customer's information (email and password) + * @return {Promise} + */ + async customerLogin(page: Page, customer: any): Promise { + await this.waitForVisibleSelector(page, this.emailInput); + await this.setValue(page, this.emailInput, customer.email); + await this.setValue(page, this.passwordInput, customer.password); + await this.clickAndWaitForLoadState(page, this.personalInformationContinueButton); + + return this.isStepCompleted(page, this.personalInformationStepForm); + } + + /** + * Get login error message + * @param page {Page} Browser tab + * @return {Promise} + */ + async getLoginError(page: Page): Promise { + return this.getTextContent(page, this.loginErrorMessage); + } + + /** + * Get active link from personal information block + * @param page {Page} Browser tab + * @returns {Promise} + */ + getActiveLinkFromPersonalInformationBlock(page: Page): Promise { + return this.getTextContent(page, this.activeLink); + } + + /** + * Is password input required + * @param page {Page} Browser tab + * @returns {Promise} + */ + isPasswordRequired(page: Page): Promise { + return this.elementVisible(page, `${this.checkoutGuestPasswordInput}:required`, 1000); + } + + /** + * Fill personal information form and click on continue + * @param page {Page} Browser tab + * @param customerData {CustomerData} Guest Customer's information to fill on form + * @return {Promise} + */ + async setGuestPersonalInformation(page: Page, customerData: CustomerData): Promise { + await this.setChecked(page, this.checkoutGuestGenderInput(customerData.socialTitle === 'Mr.' ? 1 : 2)); + + await this.setValue(page, this.checkoutGuestFirstnameInput, customerData.firstName); + await this.setValue(page, this.checkoutGuestLastnameInput, customerData.lastName); + await this.setValue(page, this.checkoutGuestEmailInput, customerData.email); + await this.setValue(page, this.checkoutGuestPasswordInput, customerData.password); + + // Fill birthday input + await this.setValue( + page, + this.checkoutGuestBirthdayInput, + `${customerData.monthOfBirth.padStart(2, '0')}/` + + `${customerData.dayOfBirth.padStart(2, '0')}/` + + `${customerData.yearOfBirth}`, + ); + + if (customerData.partnerOffers) { + await this.setChecked(page, this.checkoutGuestOptinCheckbox); + } + + if (customerData.newsletter) { + await this.setChecked(page, this.checkoutGuestNewsletterCheckbox); + } + + // Check customer privacy input if visible + if (await this.elementVisible(page, this.checkoutGuestCustomerPrivacyCheckbox, 500)) { + await this.setChecked(page, this.checkoutGuestCustomerPrivacyCheckbox); + } + + // Check gdpr input if visible + if (await this.elementVisible(page, this.checkoutGuestGdprCheckbox, 500)) { + await this.setChecked(page, this.checkoutGuestGdprCheckbox); + } + + // Click on continue + await page.locator(this.checkoutGuestContinueButton).click(); + + return this.isStepCompleted(page, this.personalInformationStepForm); + } + + // Methods for Addresses step + + /** + * Get address ID + * @param page {Page} Browser tab + * @param row {number} The row of the address + */ + async getDeliveryAddressID(page: Page, row: number = 1): Promise { + const addressSelectorValue = await this.getAttributeContent(page, this.deliveryAddressPosition(row), 'id'); + + if (addressSelectorValue === '') { + return 0; + } + const text: string = (/\d+/g.exec(addressSelectorValue) ?? '').toString(); + + return parseInt(text, 10); + } + + /** + * Get invoice address ID + * @param page {Page} Browser tab + * @param row {number} The row of the address + */ + async getInvoiceAddressID(page: Page, row: number = 1): Promise { + const addressSelectorValue = await this.getAttributeContent(page, this.invoiceAddressPosition(row), 'id'); + + if (addressSelectorValue === '') { + return 0; + } + const text: string = (/\d+/g.exec(addressSelectorValue) ?? '').toString(); + + return parseInt(text, 10); + } + + /** + * Click on edit address + * @param page {Page} Browser tab + * @param row {number} The row of the address + */ + async clickOnEditAddress(page: Page, row: number = 1): Promise { + const addressID = await this.getDeliveryAddressID(page, row); + await this.waitForSelectorAndClick(page, this.deliveryAddressEditButton(addressID)); + } + + /** + * Delete address + * @param page {Page} Browser tab + * @param row {number} The row of the address + */ + async deleteAddress(page: Page, row: number = 1): Promise { + const addressID = await this.getDeliveryAddressID(page, row); + await this.waitForSelectorAndClick(page, this.deliveryAddressDeleteButton(addressID)); + + return this.getTextContent(page, this.successAlert); + } + + /** + * Select delivery address + * @param page {Page} Browser tab + * @param row {number} The row of the address + */ + async selectDeliveryAddress(page: Page, row: number = 1): Promise { + const addressID = await this.getDeliveryAddressID(page, row); + await this.setChecked(page, this.deliveryAddressRadioButton(addressID), true); + } + + /** + * Select invoice address + * @param page {Page} Browser tab + * @param row {number} The row of the address + */ + async selectInvoiceAddress(page: Page, row: number = 1): Promise { + const addressID = await this.getInvoiceAddressID(page, row); + await this.setChecked(page, this.invoiceAddressRadioButton(addressID), true); + } + + /** + * Click on continue button from address step + * @param page {Page} Browser tab + */ + async clickOnContinueButtonFromAddressStep(page: Page): Promise { + await page.locator(this.addressStepContinueButton).click(); + + return this.isStepCompleted(page, this.addressStepSection); + } + + /** + * Is address form visible + * @param page {Page} Browser tab + */ + isAddressFormVisible(page: Page): Promise { + return this.elementVisible(page, this.addressStepCreateAddressForm, 2000); + } + + /** + * Fill address form, used for delivery and invoice addresses + * @param page {Page} Browser tab + * @param address {AddressData} Address's information to fill form with + * @returns {Promise} + */ + async fillAddressForm(page: Page, address: AddressData): Promise { + if (await this.elementVisible(page, this.addressStepAliasInput)) { + await this.setValue(page, this.addressStepAliasInput, address.alias); + } + await this.setValue(page, this.addressStepPhoneInput, address.phone); + await this.setValue(page, this.addressStepCompanyInput, address.company); + // Contact + await this.setValue(page, this.addressStepPhoneInput, address.phone); + + // Address + await this.setValue(page, this.addressStepAddress1Input, address.address); + await this.setValue(page, this.addressStepPostCodeInput, address.postalCode); + await this.setValue(page, this.addressStepCityInput, address.city); + await this.selectByVisibleText(page, this.addressStepCountrySelect, address.country); + if (await this.elementVisible(page, this.stateInput, 1000)) { + await this.selectByVisibleText(page, this.stateInput, address.state); + } + } + + /** + * Set invoice address + * @param page {Page} Browser tab + * @param invoiceAddress {AddressData} Address's information to fill form with + */ + async setInvoiceAddress(page: Page, invoiceAddress: AddressData): Promise { + await this.fillAddressForm(page, invoiceAddress); + + if (await this.elementVisible(page, this.addressStepContinueButton, 2000)) { + await page.locator(this.addressStepContinueButton).click(); + } else { + await page.locator(this.addressStepSubmitButton).click(); + } + + return this.isStepCompleted(page, this.addressStepSection); + } + + /** + * Set address step + * @param page {Page} Browser tab + * @param deliveryAddress {AddressData|null} Address's information to add (for delivery) + * @param invoiceAddress {AddressData|null} Address's information to add (for invoice) + * @returns {Promise} + */ + async setAddress(page: Page, deliveryAddress: AddressData, invoiceAddress: AddressData | null = null): Promise { + // Set delivery address + await this.fillAddressForm(page, deliveryAddress); + + // Set invoice address if not null + if (invoiceAddress !== null) { + await this.setChecked(page, this.addressStepUseSameAddressCheckbox, false); + await page.locator(this.addressStepContinueButton).click(); + await this.fillAddressForm(page, invoiceAddress); + } else { + await this.setChecked(page, this.addressStepUseSameAddressCheckbox, true); + } + + if (await this.elementVisible(page, this.addressStepContinueButton, 2000)) { + await page.locator(this.addressStepContinueButton).click(); + } else { + await page.locator(this.addressStepSubmitButton).click(); + } + + return this.isStepCompleted(page, this.addressStepSection); + } + + /** + * Get number od addresses + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getNumberOfAddresses(page: Page): Promise { + await this.waitForSelector(page, this.deliveryAddressBlock, 'visible'); + + return page.locator(this.deliveryAddressSection).count(); + } + + /** + * Get number od addresses + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getNumberOfInvoiceAddresses(page: Page): Promise { + await this.waitForSelector(page, this.invoiceAddressesBlock, 'visible'); + + return page.locator(this.invoiceAddressSection).count(); + } + + /** + * Click on edit addresses step + * @param page {Page} Browser tab + */ + async clickOnEditAddressesStep(page: Page): Promise { + if (!await this.elementVisible(page, this.deliveryAddressBlock, 1000)) { + await this.waitForSelectorAndClick(page, this.addressStepEditButton); + } + } + + /** + * Click on new address button + * @param page {Page} Browser tab + */ + async clickOnAddNewAddressButton(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.addAddressButton); + } + + /** + * Click on different invoice address link + * @param page {Page} Browser tab + */ + async clickOnDifferentInvoiceAddressLink(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.differentInvoiceAddressLink); + } + + /** + * Is invoice address block visible + * @param page {Page} Browser tab + */ + async isInvoiceAddressBlockVisible(page: Page): Promise { + return this.elementVisible(page, this.invoiceAddressesBlock, 3000); + } + + /** + * Click on new invoice address button + * @param page {Page} Browser tab + */ + async clickOnAddNewInvoiceAddressButton(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.addInvoiceAddressButton); + } + + // Methods for Shipping methods step + + /** + * Go to Delivery Step and check that Address step is complete + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToDeliveryStep(page: Page): Promise { + await this.clickAndWaitForLoadState(page, this.addressStepContinueButton); + + return this.isStepCompleted(page, this.addressStepSection); + } + + /** + * Check if the Delivery Step is displayed + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isDeliveryStep(page: Page): Promise { + await this.waitForVisibleSelector(page, this.addressStepContent); + + return this.elementVisible(page, this.addressStepContent, 1000); + } + + /** + * Choose delivery address + * @param page {Page} Browser tab + * @param position {number} Position of address to choose + * @returns {Promise} + */ + async chooseDeliveryAddress(page: Page, position: number = 1): Promise { + await this.waitForSelectorAndClick(page, this.deliveryAddressPosition(position)); + } + + /** + * Go to shipping Step and check that address step is complete + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToShippingStep(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.deliveryStepSection); + } + + // Methods for shipping method step + + /** + * Choose shipping method + * @param page {Page} Browser tab + * @param shippingMethodID {number} Position of the shipping method + */ + async chooseShippingMethod(page: Page, shippingMethodID: number): Promise { + await this.waitForSelectorAndClick(page, this.deliveryOptionLabel(shippingMethodID)); + } + + /** + * Get order message + * @param page {Page} Browser tab + */ + async getOrderMessage(page: Page): Promise { + return this.getTextContent(page, this.deliveryMessage); + } + + /** + * Choose shipping method and add a comment + * @param page {Page} Browser tab + * @param shippingMethodID {number} Position of the shipping method + * @param comment {string} Comment to add after selecting a shipping method + * @returns {Promise} + */ + async chooseShippingMethodAndAddComment(page: Page, shippingMethodID: number, comment: string = ''): Promise { + await this.waitForSelectorAndClick(page, this.deliveryOptionLabel(shippingMethodID)); + await this.setValue(page, this.deliveryMessage, comment); + + return this.goToPaymentStep(page); + } + + /** + * Choose shipping method and add a comment + * @param page {Page} Browser tab + * @param shippingMethodID {number} Position of the shipping method + * @param comment {string} Comment to add after selecting a shipping method + * @returns {Promise} + */ + async chooseShippingMethodWithoutValidation(page: Page, shippingMethodID: number, comment: string = ''): Promise { + await this.clickAndWaitForURL(page, this.deliveryOptionLabel(shippingMethodID)); + await this.setValue(page, this.deliveryMessage, comment); + } + + /** + * Is shipping method exist + * @param page {Page} Browser tab + * @param shippingMethodID {number} Position of the shipping method + * @returns {Promise} + */ + isShippingMethodVisible(page: Page, shippingMethodID: number): Promise { + return this.elementVisible(page, this.deliveryOptionLabel(shippingMethodID), 2000); + } + + /** + * Get all carriers prices + * @param page {Page} Browser tab + * @returns {Promise>} + */ + async getAllCarriersPrices(page: Page): Promise { + return (await page + .locator(this.deliveryOptionAllPricesSpan) + .allTextContents()) + .filter((el: string | null): el is string => el !== null); + } + + /** + * Get shipping value + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getShippingCost(page: Page): Promise { + await page.waitForTimeout(2000); + + return this.getTextContent(page, this.shippingValueSpan); + } + + /** + * Get all carriers names + * @param page {Page} Browser tab + * @returns {Promise>} + */ + async getAllCarriersNames(page: Page): Promise<(string | null)[]> { + return page.locator(this.deliveryOptionAllNamesSpan).allTextContents(); + } + + /** + * Get carrier data + * @param page {Page} Browser tab + * @param carrierID {number} The carrier row in list + */ + async getCarrierData(page: Page, carrierID: number = 1): Promise { + const priceText: string = await this.getTextContent(page, this.deliveryStepCarrierPrice(carrierID)); + + return new CarrierData({ + name: await this.getTextContent(page, this.deliveryStepCarrierName(carrierID)), + delay: await this.getTextContent(page, this.deliveryStepCarrierDelay(carrierID)), + price: parseFloat(priceText), + priceText, + }); + } + + /** + * Go to Payment Step and check that delivery step is complete + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToPaymentStep(page: Page): Promise { + await this.clickAndWaitForLoadState(page, this.deliveryStepContinueButton); + + return this.isStepCompleted(page, this.deliveryStepSection); + } + + /** + * Click on edit shipping method step + * @param page {Page} Browser tab + */ + async clickOnEditShippingMethodStep(page: Page): Promise { + if (!await this.elementVisible(page, this.deliveryStepCarriersList, 1000)) { + await this.waitForSelectorAndClick(page, this.deliveryStepEditButton); + } + } + + // Methods for payment methods step + + /** + * Is confirm button visible and enabled + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isPaymentConfirmationButtonVisibleAndEnabled(page: Page): Promise { + // small side effect note, the selector is the one that checks for disabled + return this.elementVisible(page, this.paymentConfirmationButton, 1000); + } + + /** + * Get No payment needed block content + * @param page {Page} Browser tab + * @returns {Promise} + */ + getNoPaymentNeededBlockContent(page: Page): Promise { + return this.getTextContent(page, this.noPaymentNeededElement); + } + + /** + * Get selected shipping method name + * @param page {Page} Browser tab + * @return {Promise} + */ + async getSelectedShippingMethod(page: Page): Promise { + const selectedOptionId = parseInt( + await this.getAttributeContent(page, `${this.deliveryOptionsRadioButton}[checked]`, 'value') ?? '0', + 10, + ); + + // Return text of the selected option + if (selectedOptionId !== 0) { + return this.getTextContent(page, this.deliveryOptionNameSpan(selectedOptionId)); + } + throw new Error('No selected option was found'); + } + + /** + * Set promo code + * @param page {Page} Browser tab + * @param code {string} The promo code + * @param clickOnCheckoutPromoCodeLink {boolean} True if we need to click on promo code link + * @returns {Promise} + */ + async addPromoCode(page: Page, code: string, clickOnCheckoutPromoCodeLink: boolean = true): Promise { + if (clickOnCheckoutPromoCodeLink) { + await page.locator(this.checkoutHavePromoCodeButton).click(); + } + await this.setValue(page, this.checkoutHavePromoInputArea, code); + await page.locator(this.checkoutPromoCodeAddButton).click(); + } + + /** + * Get cart rule name + * @param page {Page} Browser tab + * @param line {number} Cart rule line + * @return {string} + */ + getCartRuleName(page: Page, line: number = 1): Promise { + return this.getTextContent(page, this.cartRuleName(line)); + } + + /** + * Choose payment method and validate Order + * @param page {Page} Browser tab + * @param paymentModuleName {string} The chosen payment method + * @return {Promise} + */ + async choosePaymentAndOrder(page: Page, paymentModuleName: string): Promise { + if (await this.elementVisible(page, this.paymentOptionInput(paymentModuleName), 1000)) { + await page.locator(this.paymentOptionInput(paymentModuleName)).click(); + } + await Promise.all([ + this.waitForVisibleSelector(page, this.paymentConfirmationButton), + page.locator(this.conditionToApproveLabel).click(), + ]); + await this.clickAndWaitForURL(page, this.paymentConfirmationButton); + } + + /** + * Get All tax included price + * @param page {Page} Browser tab + * @returns {Promise} + */ + getATIPrice(page: Page): Promise { + return this.getPriceFromText(page, this.cartTotalATI, 2000); + } + + /** + * Delete the discount + * @param page {Page} Browser tab + * @returns {Promise} + */ + async removePromoCode(page: Page): Promise { + await page.locator(this.checkoutRemoveDiscountLink).click(); + + return this.elementNotVisible(page, this.checkoutRemoveDiscountLink, 1000); + } + + /** + * Get cart rule error text + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getCartRuleErrorMessage(page: Page): Promise { + return this.getTextContent(page, this.cartRuleAlertMessage); + } + + /** + * Order when no payment is needed + * @param page {Page} Browser tab + * @returns {Promise} + */ + async orderWithoutPaymentMethod(page: Page): Promise { + // Click on terms of services checkbox if visible + if (await this.elementVisible(page, this.conditionToApproveLabel, 500)) { + await Promise.all([ + this.waitForVisibleSelector(page, this.paymentConfirmationButton), + page.locator(this.conditionToApproveLabel).click(), + ]); + } + + // Validate the order + await this.clickAndWaitForURL(page, this.paymentConfirmationButton); + } + + /** + * Check payment method existence + * @param page {Page} Browser tab + * @param paymentModuleName {string} The payment module name + * @returns {Promise} + */ + async isPaymentMethodExist(page: Page, paymentModuleName: string): Promise { + return this.elementVisible(page, this.paymentOptionInput(paymentModuleName), 2000); + } + + /** + * Check if checkbox of condition to approve is visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + isConditionToApproveCheckboxVisible(page: Page): Promise { + return this.elementVisible(page, this.conditionToApproveCheckbox, 1000); + } + + /** + * Get terms of service page title + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getTermsOfServicePageTitle(page: Page): Promise { + await page.locator(this.termsOfServiceLink).click(); + + return this.getTextContent(page, this.termsOfServiceModalDiv); + } + + /** + * Check if gift checkbox is visible + * @param page {Page} Browser tab + * @return {Promise} + */ + isGiftCheckboxVisible(page: Page): Promise { + return this.elementVisible(page, this.giftCheckbox, 1000); + } + + /** + * Set gift checkbox + * @param page {Page} Browser tab + * @returns {Promise} + */ + async setGiftCheckBox(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.giftCheckbox); + } + + /** + * Is gift message textarea visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + isGiftMessageTextareaVisible(page: Page): Promise { + return this.elementVisible(page, this.giftMessageTextarea, 2000); + } + + /** + * Set gift message + * @param page {Page} Browser tab + * @param message {string} Message to set + * @returns {Promise} + */ + async setGiftMessage(page: Page, message: string): Promise { + await this.setValue(page, this.giftMessageTextarea, message); + } + + /** + * Check if recycled packaging checkbox is visible + * @param page {Page} Browser tab + * @return {Promise} + */ + isRecycledPackagingCheckboxVisible(page: Page): Promise { + return this.elementVisible(page, this.recyclableGiftCheckbox, 1000); + } + + /** + * Set recycled packaging checkbox + * @param page {Page} Browser tab + * @param toCheck {boolean} True if we need to check recycle packaging checkbox + * @returns {Promise} + */ + async setRecycledPackagingCheckbox(page: Page, toCheck: boolean = true): Promise { + await this.setChecked(page, this.recyclableGiftCheckbox, toCheck); + } + + /** + * Get gift price from cart summary + * @param page {Page} Browser tab + * @return {Promise} + */ + async getGiftPrice(page: Page): Promise { + await this.setChecked(page, this.giftCheckbox, true); + + return this.getTextContent(page, this.cartSubtotalGiftWrappingValueSpan); + } +} + +const checkoutPage = new CheckoutPage(); +export {checkoutPage, CheckoutPage}; diff --git a/src/versions/8.0.0/pages/FO/classic/checkout/orderConfirmation.ts b/src/versions/8.0.0/pages/FO/classic/checkout/orderConfirmation.ts new file mode 100644 index 00000000..3763480b --- /dev/null +++ b/src/versions/8.0.0/pages/FO/classic/checkout/orderConfirmation.ts @@ -0,0 +1,120 @@ +// Import pages +import type {FoCheckoutOrderConfirmationPageInterface} from '@interfaces/FO/checkout/orderConfirmation'; +import FOBasePage from '@pages/FO/FOBasePage'; + +import type {Page} from 'playwright'; + +/** + * Order confirmation page, contains functions that can be used on the page + * @class + * @extends FOBasePage + */ +class OrderConfirmationPage extends FOBasePage implements FoCheckoutOrderConfirmationPageInterface { + public readonly pageTitle: string; + + public readonly orderConfirmationCardTitle: string; + + protected orderConfirmationCardSection: string; + + protected orderConfirmationCardTitleH3: string; + + private readonly orderSummaryContent: string; + + protected orderReferenceValue: string; + + protected customerSupportLink: string; + + private readonly orderConfirmationTable: string; + + private readonly giftWrappingRow: string; + + protected orderDetailsTable: string; + + protected paymentMethodRow: string; + + /** + * @constructs + * Setting up texts and selectors to use on order confirmation page + */ + constructor(theme: string = 'classic') { + super(theme); + + this.pageTitle = 'Order confirmation'; + this.orderConfirmationCardTitle = 'Your order is confirmed'; + + // Selectors + this.orderConfirmationCardSection = '#content-hook_order_confirmation'; + this.orderConfirmationCardTitleH3 = `${this.orderConfirmationCardSection} h3.card-title`; + this.orderSummaryContent = '#order-summary-content'; + this.orderReferenceValue = '#order-reference-value'; + this.customerSupportLink = '#content-hook_payment_return a'; + this.orderConfirmationTable = 'div.order-confirmation-table'; + this.giftWrappingRow = `${this.orderConfirmationTable} tr:nth-child(3)`; + this.orderDetailsTable = 'div#order-details'; + this.paymentMethodRow = `${this.orderDetailsTable} li:nth-child(2)`; + } + + /* + Methods + */ + /** + * Check if final summary is visible + * @param page {Page} Browser tab + * @returns {boolean} + */ + async isFinalSummaryVisible(page: Page): Promise { + return this.elementVisible(page, this.orderSummaryContent, 2000); + } + + /** + * Get order confirmation card title + * @param page {Page} Browser tab + * @return {Promise} + */ + async getOrderConfirmationCardTitle(page: Page): Promise { + return this.getTextContent(page, this.orderConfirmationCardTitleH3); + } + + /** + * Get and return the order reference value + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getOrderReferenceValue(page: Page): Promise { + const orderRefText = await this.getTextContent(page, this.orderReferenceValue); + + return (orderRefText.split(':'))[1].trim(); + } + + /** + * Click on the 'customer support' link + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToContactUsPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.customerSupportLink); + } + + /** + * Return the payment method + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getPaymentMethod(page: Page): Promise { + const text = await this.getTextContent(page, this.paymentMethodRow); + + return (text.split(':'))[1].trim(); + } + + /** + * Get gift wrapping value + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getGiftWrappingValue(page: Page): Promise { + return this.getNumberFromText(page, this.giftWrappingRow); + } +} + +const orderConfirmationPage = new OrderConfirmationPage(); +export {orderConfirmationPage, OrderConfirmationPage};