diff --git a/.gitignore b/.gitignore index 2ea9ab1e..8618acfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /node_modules/* -dist +/dist + +## IDE +/.vscode/ diff --git a/package-lock.json b/package-lock.json index 3a73bec5..7a85c805 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@prestashop-core/ui-testing", - "version": "0.0.0", + "version": "0.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@prestashop-core/ui-testing", - "version": "0.0.0", + "version": "0.0.3", "license": "MIT", "dependencies": { + "@faker-js/faker": "^8.3.1", "@playwright/test": "^1.40.1", "semver": "^7.5.4" }, @@ -296,6 +297,21 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.3.1.tgz", + "integrity": "sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3832,6 +3848,11 @@ "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true }, + "@faker-js/faker": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.3.1.tgz", + "integrity": "sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw==" + }, "@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", diff --git a/package.json b/package.json index 652419bf..f39cd85d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://github.com/PrestaShop/ui-testing-library#readme", "dependencies": { + "@faker-js/faker": "^8.3.1", "@playwright/test": "^1.40.1", "semver": "^7.5.4" }, diff --git a/src/data/demo/customers.ts b/src/data/demo/customers.ts new file mode 100644 index 00000000..bb71e92f --- /dev/null +++ b/src/data/demo/customers.ts @@ -0,0 +1,18 @@ +import SocialTitles from '@data/demo/socialTitles'; +import CustomerData from '@data/faker/customer'; + +export default { + johnDoe: new CustomerData({ + id: 2, + socialTitle: SocialTitles.Mr.name, + firstName: 'John', + lastName: 'DOE', + birthDate: new Date('1970-01-15'), + email: 'pub@prestashop.com', + password: '123456789', + enabled: true, + newsletter: true, + partnerOffers: true, + defaultCustomerGroup: 'Customer', + }), +}; diff --git a/src/data/demo/groups.ts b/src/data/demo/groups.ts new file mode 100644 index 00000000..2f2735ef --- /dev/null +++ b/src/data/demo/groups.ts @@ -0,0 +1,22 @@ +import GroupData from '@data/faker/group'; + +export default { + visitor: new GroupData({ + id: 1, + name: 'Visitor', + discount: 0, + shownPrices: true, + }), + guest: new GroupData({ + id: 2, + name: 'Guest', + discount: 0, + shownPrices: true, + }), + customer: new GroupData({ + id: 3, + name: 'Customer', + discount: 0, + shownPrices: true, + }), +}; diff --git a/src/data/demo/modules.ts b/src/data/demo/modules.ts new file mode 100644 index 00000000..17761320 --- /dev/null +++ b/src/data/demo/modules.ts @@ -0,0 +1,59 @@ +import ModuleData from '@data/faker/module'; + +export default { + blockwishlist: new ModuleData({ + tag: 'blockwishlist', + name: 'Wishlist', + }), + psApiResources: new ModuleData({ + tag: 'ps_apiresources', + name: 'PrestaShop API Resources', + }), + psCashOnDelivery: new ModuleData({ + tag: 'ps_cashondelivery', + name: 'Cash on delivery (COD)', + }), + psCheckPayment: new ModuleData({ + tag: 'ps_checkpayment', + name: 'Payments by check', + }), + psEmailAlerts: new ModuleData({ + tag: 'ps_emailalerts', + name: 'Mail alerts', + releaseZip: 'https://github.com/PrestaShop/ps_emailalerts/releases/download/v2.4.2/ps_emailalerts.zip', + }), + psEmailSubscription: new ModuleData({ + tag: 'ps_emailsubscription', + name: 'Newsletter subscription', + }), + psFacetedSearch: new ModuleData({ + tag: 'ps_facetedsearch', + name: 'Faceted search', + releaseZip: 'https://github.com/PrestaShop/ps_facetedsearch/releases/download/v3.14.1/ps_facetedsearch.zip', + }), + psThemeCusto: new ModuleData({ + tag: 'ps_themecusto', + name: 'Theme Customization', + }), + contactForm: new ModuleData({ + tag: 'contactform', + name: 'Contact form', + }), + themeCustomization: new ModuleData({ + tag: 'ps_themecusto', + name: 'Theme Customization', + }), + availableQuantities: new ModuleData({ + tag: 'statsstock', + name: 'Available quantities', + }), + mainMenu: new ModuleData({ + tag: 'ps_mainmenu', + name: 'Main menu', + }), + keycloak: new ModuleData({ + tag: 'keycloak_connector_demo', + name: 'Keycloak OAuth2 connector demo', + releaseZip: 'https://github.com/PrestaShop/keycloak_connector_demo/releases/download/v1.0.4/keycloak_connector_demo.zip', + }), +}; diff --git a/src/data/demo/socialTitles.ts b/src/data/demo/socialTitles.ts new file mode 100644 index 00000000..250c485c --- /dev/null +++ b/src/data/demo/socialTitles.ts @@ -0,0 +1,10 @@ +export default { + Mr: { + id: 1, + name: 'Mr.', + }, + Mrs: { + id: 2, + name: 'Mrs', + }, +}; diff --git a/src/data/demo/titles.ts b/src/data/demo/titles.ts new file mode 100644 index 00000000..2847234b --- /dev/null +++ b/src/data/demo/titles.ts @@ -0,0 +1,14 @@ +import TitleData from '@data/faker/title'; + +export default { + Mrs: new TitleData({ + id: 2, + name: 'Mrs.', + gender: 'Female', + }), + Mr: new TitleData({ + id: 1, + name: 'Mr.', + gender: 'Male', + }), +}; diff --git a/src/data/faker/customer.ts b/src/data/faker/customer.ts new file mode 100644 index 00000000..deb85986 --- /dev/null +++ b/src/data/faker/customer.ts @@ -0,0 +1,117 @@ +import Groups from '@data/demo/groups'; +import Titles from '@data/demo/titles'; +import type GroupData from '@data/faker/group'; +import type TitleData from '@data/faker/title'; +import type {CustomerCreator} from '@data/types/customer'; + +import {faker} from '@faker-js/faker'; + +const genders: string[] = Object.values(Titles).map((title: TitleData) => title.name); +const groups: string[] = Object.values(Groups).map((group: GroupData) => group.name); +const risksRating: string[] = ['None', 'Low', 'Medium', 'High']; + +/** + * Create new customer to use on creation form on customer page on BO and FO + * @class + */ +export default class CustomerData { + public readonly id: number; + + public readonly socialTitle: string; + + public readonly firstName: string; + + public readonly lastName: string; + + public email: string; + + public password: string; + + public readonly birthDate: Date; + + public readonly yearOfBirth: string; + + public readonly monthOfBirth: string; + + public readonly dayOfBirth: string; + + public readonly enabled: boolean; + + public readonly partnerOffers: boolean; + + public readonly newsletter: boolean; + + public readonly defaultCustomerGroup: string; + + public readonly company: string; + + public readonly allowedOutstandingAmount: number; + + public readonly riskRating: string; + + /** + * Constructor for class CustomerData + * @param customerToCreate {CustomerCreator} Could be used to force the value of some members + */ + constructor(customerToCreate: CustomerCreator = {}) { + /** @type {number} ID of the customer */ + this.id = customerToCreate.id || 0; + + /** @type {string} Social title of the customer (Mr, Mrs) */ + this.socialTitle = customerToCreate.socialTitle || faker.helpers.arrayElement(genders); + + /** @type {string} Firstname of the customer */ + this.firstName = customerToCreate.firstName || faker.person.firstName(); + + /** @type {string} Lastname of the customer */ + this.lastName = customerToCreate.lastName || faker.person.lastName(); + + /** @type {string} Email for the customer account */ + this.email = customerToCreate.email || faker.internet.email( + { + firstName: this.firstName, + lastName: this.lastName, + provider: 'prestashop.com', + }, + ); + + /** @type {string} Password for the customer account */ + this.password = customerToCreate.password === undefined ? faker.internet.password() : customerToCreate.password; + + /** @type {Date} Birthdate of the customer */ + this.birthDate = customerToCreate.birthDate || faker.date.between({from: '1950-01-01', to: '2000-12-31'}); + + /** @type {string} Year of the birth 'yyyy' */ + this.yearOfBirth = customerToCreate.yearOfBirth || this.birthDate.getFullYear().toString(); + + /** @type {string} Month of the birth 'mm' */ + this.monthOfBirth = customerToCreate.monthOfBirth || (`0${this.birthDate.getMonth() + 1}`).slice(-2); + + /** @type {string} Day of the birth 'dd' */ + this.dayOfBirth = customerToCreate.dayOfBirth || (`0${this.birthDate.getDate()}`).slice(-2).toString(); + + /** @type {boolean} Status of the customer */ + this.enabled = customerToCreate.enabled === undefined ? true : customerToCreate.enabled; + + /** @type {boolean} True to enable partner offers */ + this.partnerOffers = customerToCreate.partnerOffers === undefined ? true : customerToCreate.partnerOffers; + + /** @type {string} Default group for the customer */ + this.defaultCustomerGroup = customerToCreate.defaultCustomerGroup || faker.helpers.arrayElement(groups); + + /** @type {boolean} True to enable sending newsletter to the customer */ + this.newsletter = customerToCreate.newsletter === undefined ? false : customerToCreate.newsletter; + + /** @type {string} Company for the customer */ + this.company = customerToCreate.company || faker.company.name(); + + /** @type {Number} Allowed outstanding amount for the customer */ + this.allowedOutstandingAmount = customerToCreate.allowedOutstandingAmount || faker.number.int({ + min: 0, + max: 100, + }); + + /** @type {string} Risk rating for the customer */ + this.riskRating = customerToCreate.riskRating || faker.helpers.arrayElement(risksRating); + } +} diff --git a/src/data/faker/group.ts b/src/data/faker/group.ts new file mode 100644 index 00000000..2d44c389 --- /dev/null +++ b/src/data/faker/group.ts @@ -0,0 +1,47 @@ +import {GroupCreator} from '@data/types/group'; + +import {faker} from '@faker-js/faker'; + +const priceDisplayMethod: string[] = ['Tax included', 'Tax excluded']; + +/** + * Create new group to use on creation form on group page on BO + * @class + */ +export default class GroupData { + public readonly id: number; + + public readonly name: string; + + public readonly frName: string; + + public readonly discount: number; + + public readonly priceDisplayMethod: string; + + public readonly shownPrices: boolean; + + /** + * Constructor for class GroupData + * @param groupToCreate {GroupCreator} Could be used to force the value of some members + */ + constructor(groupToCreate: GroupCreator = {}) { + /** @type {number} ID of the group */ + this.id = groupToCreate.id || 0; + + /** @type {string} Name of the group */ + this.name = groupToCreate.name || faker.person.jobType(); + + /** @type {string} French name of the group */ + this.frName = groupToCreate.frName || this.name; + + /** @type {number} Basic discount for the group */ + this.discount = groupToCreate.discount || 0; + + /** @type {string} Price display method of the group */ + this.priceDisplayMethod = groupToCreate.priceDisplayMethod || faker.helpers.arrayElement(priceDisplayMethod); + + /** @type {boolean} True to show prices for the group */ + this.shownPrices = groupToCreate.shownPrices === undefined ? true : groupToCreate.shownPrices; + } +} diff --git a/src/data/faker/module.ts b/src/data/faker/module.ts new file mode 100644 index 00000000..55c57e11 --- /dev/null +++ b/src/data/faker/module.ts @@ -0,0 +1,27 @@ +import type {ModuleDataCreator} from '@data/types/module'; + +/** + * @class + */ +export default class ModuleData { + public readonly tag: string; + + public readonly name: string; + + public readonly releaseZip: string; + + /** + * Constructor for class ModuleData + * @param valueToCreate {ModuleDataCreator} Could be used to force the value of some members + */ + constructor(valueToCreate: ModuleDataCreator = {}) { + /** @type {string} Technical Name of the module */ + this.tag = valueToCreate.tag || ''; + + /** @type {string} Name of the module */ + this.name = valueToCreate.name || ''; + + /** @type {string} Release URL */ + this.releaseZip = valueToCreate.releaseZip || ''; + } +} diff --git a/src/data/faker/title.ts b/src/data/faker/title.ts new file mode 100644 index 00000000..19dea5aa --- /dev/null +++ b/src/data/faker/title.ts @@ -0,0 +1,54 @@ +// Import data +import {TitleCreator} from '@data/types/title'; + +import {faker} from '@faker-js/faker'; + +const genders: string[] = ['Male', 'Female', 'Neutral']; + +/** + * Create new title to use on title form on BO + * @class + */ +export default class TitleData { + public readonly id: number; + + public readonly name: string; + + public readonly frName: string; + + public readonly gender: string; + + public readonly imageName: string; + + public readonly imageWidth: number; + + public readonly imageHeight: number; + + /** + * Constructor for class TitleData + * @param titleToCreate {TitleCreator} Could be used to force the value of some members + */ + constructor(titleToCreate: TitleCreator = {}) { + /** @type {number} ID of the title */ + this.id = titleToCreate.id || 0; + + // Title name should contain at most 20 characters + /** @type {string} Name of the title */ + this.name = titleToCreate.name || (faker.lorem.word()).substring(0, 19).trim(); + + /** @type {string} French name of the title */ + this.frName = titleToCreate.frName || this.name; + + /** @type {string} Gender type of the title */ + this.gender = titleToCreate.gender || faker.helpers.arrayElement(genders); + + /** @type {string} Name of the image to add to the title */ + this.imageName = titleToCreate.imageName || faker.system.commonFileName('png'); + + /** @type {number} Width of the image */ + this.imageWidth = titleToCreate.imageWidth || 16; + + /** @type {number} Height of the image */ + this.imageHeight = titleToCreate.imageHeight || 16; + } +} diff --git a/src/data/types/cart.ts b/src/data/types/cart.ts new file mode 100644 index 00000000..e4432af9 --- /dev/null +++ b/src/data/types/cart.ts @@ -0,0 +1,9 @@ +export type CartProductDetails = { + name: string + price: number + quantity: number + cartProductsCount: number + cartSubtotal: number + cartShipping: string + totalTaxIncl: number +}; diff --git a/src/data/types/customer.ts b/src/data/types/customer.ts new file mode 100644 index 00000000..4855d4e8 --- /dev/null +++ b/src/data/types/customer.ts @@ -0,0 +1,20 @@ +export type CustomerCreator = { + id?: number + socialTitle?: string + firstName?: string + lastName?: string + birthdate?: string + yearOfBirth?: string + monthOfBirth?: string + dayOfBirth?: string + email?: string + password?: string + birthDate?: Date + enabled?: boolean + newsletter?: boolean + partnerOffers?: boolean + defaultCustomerGroup?: string + company?: string + allowedOutstandingAmount?: number + riskRating?: string +}; \ No newline at end of file diff --git a/src/data/types/group.ts b/src/data/types/group.ts new file mode 100644 index 00000000..4da91e45 --- /dev/null +++ b/src/data/types/group.ts @@ -0,0 +1,8 @@ +export type GroupCreator = { + id?: number + name?: string + frName?: string + discount?: number + priceDisplayMethod?: string + shownPrices?: boolean +}; diff --git a/src/data/types/module.ts b/src/data/types/module.ts new file mode 100644 index 00000000..b9a83cae --- /dev/null +++ b/src/data/types/module.ts @@ -0,0 +1,5 @@ +export type ModuleDataCreator = { + tag?: string + name?: string + releaseZip?: string +}; diff --git a/src/data/types/product.ts b/src/data/types/product.ts new file mode 100644 index 00000000..502176a5 --- /dev/null +++ b/src/data/types/product.ts @@ -0,0 +1,248 @@ +type ProductAttribute = { + name: string + value: string +}; + +type ProductAttributes = { + name: string + values: string[] +}; + +type ProductCreator = { + id?: number + name?: string + nameFR?: string + defaultImage?: string | null + coverImage?: string | null + thumbImage?: string | null + thumbImageFR?: string | null + category?: string + type?: string + status?: boolean + applyChangesToAllStores?: boolean + summary?: string + description?: string + reference?: string + mpn?: string | null + upc?: string | null + ean13?: string | null + isbn?: string | null + features?: ProductFeatures[] + files?: ProductFiles[] + displayCondition?: boolean + condition?: string + customizations?: ProductCustomizations[] + quantity?: number + tax?: number + price?: number + retailPrice?: number + finalPrice?: number + priceTaxExcluded?: number + onSale?: boolean + productHasCombinations?: boolean + attributes?: ProductAttributes[] + pack?: ProductPackItem[] + taxRule?: string + ecoTax?: number + specificPrice?: ProductSpecificPrice + minimumQuantity?: number + stockLocation?: string + lowStockLevel?: number + labelWhenInStock?: string + labelWhenOutOfStock?: string + behaviourOutOfStock?: string + customization?: ProductCustomization + downloadFile?: boolean + fileName?: string + allowedDownload?: number + expirationDate?: string | null + numberOfDays?: number | null + packageDimensionWeight?: number + packageDimensionDepth?: number + packageDimensionHeight?: number + packageDimensionWidth?: number + deliveryTime?: string + combinations?: ProductCombination[] + metaTitle?: string | null + metaDescription?: string | null + friendlyUrl?: string | null +}; + +type ProductCombination = { + name: string + price: number +}; + +type ProductFeatures = { + featureName: string, + preDefinedValue?: string, + customizedValueEn?: string, + customizedValueFr?: string, +} + +type ProductFiles = { + fileName: string, + description: string, + file: string, +} + +type ProductCombinationOptions = { + reference: string + impactOnPriceTExc: number + quantity: number + minimalQuantity?: number +} + +type ProductCombinationBulk = { + stocks: ProductCombinationBulkStock + retailPrice: ProductCombinationBulkRetailPrice + specificReferences: ProductCombinationBulkSpecificReferences +} + +type ProductCombinationBulkRetailPrice = { + costPriceToEnable: boolean + costPrice?: number + impactOnPriceTIncToEnable: boolean + impactOnPriceTInc?: number + impactOnWeightToEnable: boolean + impactOnWeight?: number +} + +type ProductCombinationBulkSpecificReferences = { + referenceToEnable: boolean + reference?: string +} + +type ProductCombinationBulkStock = { + quantityToEnable: boolean + quantity?: number + minimalQuantityToEnable: boolean + minimalQuantity?: number + stockLocationToEnable: boolean + stockLocation?: string +}; + +type ProductCustomization = { + label: string + type: string + required: boolean +}; + +type ProductCustomizations = { + label: string + type: string + required: boolean +}; + +type ProductDetailsBasic = { + image: string + name: string + price: number + quantity: number +}; + +type ProductDetails = ProductDetailsBasic & { + summary: string + description: string + shipping?: string + subtotal?: number +}; + +type ProductDiscount = { + name: string + type: string + value: string +}; + +type ProductFilterMinMax = { + min: number + max: number +} + +type ProductHeaderSummary = { + imageUrl: string + reference: string + quantity: string + priceTaxIncl: string + priceTaxExc: string + mpn: string + upc: string + ean_13: string + isbn: string +}; + +type ProductInformations = { + name: string + price: number + summary: string + description: string +}; + +type ProductImageUrls = { + coverImage: string + thumbImage: string +}; + +type ProductPackItem = { + reference: string + quantity: number +}; + +type ProductPackInformation = ProductPackItem & { + image: string + name: string +}; + +type ProductPackOptions = { + quantity: number + minimalQuantity: number + packQuantitiesOption: string +}; + +type ProductReviewCreator = { + reviewTitle?: string; + reviewContent?: string; + reviewRating?: number; +}; + +type ProductSpecificPrice = { + attributes: number | null + discount: number + startingAt: number + reductionType: string +}; + +type ProductStockMovement = { + dateTime: string + quantity: number + employee: string +}; + +export type { + ProductAttribute, + ProductAttributes, + ProductCombination, + ProductCombinationOptions, + ProductCombinationBulk, + ProductCombinationBulkRetailPrice, + ProductCombinationBulkSpecificReferences, + ProductCombinationBulkStock, + ProductCreator, + ProductCustomization, + ProductDetails, + ProductDetailsBasic, + ProductDiscount, + ProductFilterMinMax, + ProductHeaderSummary, + ProductImageUrls, + ProductInformations, + ProductPackItem, + ProductPackInformation, + ProductPackOptions, + ProductReviewCreator, + ProductSpecificPrice, + ProductStockMovement, + ProductFeatures, + ProductFiles, + ProductCustomizations, +}; diff --git a/src/data/types/title.ts b/src/data/types/title.ts new file mode 100644 index 00000000..8e742fc0 --- /dev/null +++ b/src/data/types/title.ts @@ -0,0 +1,9 @@ +export type TitleCreator = { + id?: number + name?: string + frName?: string + gender?: string + imageName?: string + imageWidth?: number + imageHeight?: number +}; diff --git a/src/index.ts b/src/index.ts index 9e0e718b..4ca16010 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ // Export data +export type { CartProductDetails } from '@data/types/cart'; export type { GlobalInstall, GlobalFO, @@ -16,9 +17,11 @@ export type { WaitForNavigationWaitUntil, } from '@data/types/playwright'; -// Export interfaces -export type {DashboardPageInterface} from '@interfaces/BO/dashboard'; -export type {LoginPageInterface} from '@interfaces/BO/login'; +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 dataSocialTitles} from '@data/demo/socialTitles'; +export {default as dataTitles} from '@data/demo/titles'; // Export Pages export * as CommonPage from '@pages/commonPage'; @@ -26,6 +29,16 @@ 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 boModuleManagerPage} from '@pages/BO/modules/moduleManager'; +// Export Pages FO +export * as FOBasePage from '@pages/FO/FOBasePage'; +export {default as foCategoryPage} from '@pages/FO/category'; +export {default as foHomePage} from '@pages/FO/home'; +export {default as foLoginPage} from '@pages/FO/login'; + +// Export Modules +export {default as modBlockwishlistBoMain} from '@pages/BO/modules/blockwishlist'; +export {default as modBlockwishlistBoStatistics} from '@pages/BO/modules/blockwishlist/statistics'; // Export utils export {default as testContext} from '@utils/testContext'; diff --git a/src/interfaces/BO/index.ts b/src/interfaces/BO/index.ts index 3d22fe11..d307563d 100644 --- a/src/interfaces/BO/index.ts +++ b/src/interfaces/BO/index.ts @@ -1,5 +1,5 @@ import type {CommonPageInterface} from '@interfaces/index'; -import type {Page} from '@playwright/test'; +import type {Frame, Page} from '@playwright/test'; export interface BOBasePagePageInterface extends CommonPageInterface { readonly ordersParentLink: string; @@ -134,6 +134,8 @@ export interface BOBasePagePageInterface extends CommonPageInterface { readonly multistoreLink: string; + closeSfToolBar(page: Frame | Page): Promise; goToSubMenu(page: Page, parentSelector: string, linkSelector: string): Promise; logoutBO(page: Page): Promise; + viewMyShop(page: Page): Promise; } diff --git a/src/interfaces/BO/modules/blockwishlist/index.ts b/src/interfaces/BO/modules/blockwishlist/index.ts new file mode 100644 index 00000000..5fc747d5 --- /dev/null +++ b/src/interfaces/BO/modules/blockwishlist/index.ts @@ -0,0 +1,9 @@ +import {BOBasePagePageInterface} from '@interfaces/BO'; +import type { Page } from '@playwright/test'; + +export interface ModuleBlockwishlistMainPageInterface extends BOBasePagePageInterface { + readonly pageTitle: string; + + isTabActive(page: Page, name: 'Configuration'|'Statistics'): Promise; + goToStatisticsTab(page: Page): Promise; +} diff --git a/src/interfaces/BO/modules/blockwishlist/statistics.ts b/src/interfaces/BO/modules/blockwishlist/statistics.ts new file mode 100644 index 00000000..c03ba490 --- /dev/null +++ b/src/interfaces/BO/modules/blockwishlist/statistics.ts @@ -0,0 +1,9 @@ +import {BOBasePagePageInterface} from '@interfaces/BO'; +import type { Page } from '@playwright/test'; + +export interface ModuleBlockwishlistStatisticsPageInterface extends BOBasePagePageInterface { + readonly pageTitle: string; + + getTextForEmptyTable(page: Page): Promise; + refreshStatistics(page: Page): Promise; +} diff --git a/src/interfaces/BO/modules/moduleManager/index.ts b/src/interfaces/BO/modules/moduleManager/index.ts new file mode 100644 index 00000000..d7fdc819 --- /dev/null +++ b/src/interfaces/BO/modules/moduleManager/index.ts @@ -0,0 +1,11 @@ +import type ModuleData from '@data/faker/module'; + +import {BOBasePagePageInterface} from '@interfaces/BO'; +import type {Page} from '@playwright/test'; + +export interface ModuleManagerPageInterface extends BOBasePagePageInterface { + pageTitle: string; + + goToConfigurationPage(page: Page, moduleTag: string): Promise; + searchModule(page: Page, module: ModuleData): Promise; +} diff --git a/src/interfaces/FO/category/index.ts b/src/interfaces/FO/category/index.ts new file mode 100644 index 00000000..450f672b --- /dev/null +++ b/src/interfaces/FO/category/index.ts @@ -0,0 +1,10 @@ +import {FOBasePagePageInterface} from '@interfaces/FO'; +import type { Page } from '@playwright/test'; + +export interface FoCategoryPageInterface extends FOBasePagePageInterface { + readonly messageAddedToWishlist: string; + + addToWishList(page: Page, idxProduct: number): Promise; + isAddedToWishlist(page: Page, idxProduct: number): Promise; + isCategoryPage(page: Page): Promise; +} diff --git a/src/interfaces/FO/home/index.ts b/src/interfaces/FO/home/index.ts new file mode 100644 index 00000000..0c453390 --- /dev/null +++ b/src/interfaces/FO/home/index.ts @@ -0,0 +1,7 @@ +import {FOBasePagePageInterface} from '@interfaces/FO'; +import type { Page } from '@playwright/test'; + +export interface FoHomePageInterface extends FOBasePagePageInterface { + goToAllProductsPage(page: Page): Promise; + isHomePage(page: Page): Promise; +} diff --git a/src/interfaces/FO/index.ts b/src/interfaces/FO/index.ts new file mode 100644 index 00000000..09f9a8b7 --- /dev/null +++ b/src/interfaces/FO/index.ts @@ -0,0 +1,9 @@ +import type {CommonPageInterface} from '@interfaces/index'; +import type { Page } from '@playwright/test'; + +export interface FOBasePagePageInterface extends CommonPageInterface { + changeLanguage(page: Page, lang: string): Promise; + goToLoginPage(page: Page): Promise; + isCustomerConnected(page: Page): Promise; + logout(page: Page): Promise; +} diff --git a/src/interfaces/FO/login/index.ts b/src/interfaces/FO/login/index.ts new file mode 100644 index 00000000..12fe09f6 --- /dev/null +++ b/src/interfaces/FO/login/index.ts @@ -0,0 +1,9 @@ +import type CustomerData from '@data/faker/customer'; +import {FOBasePagePageInterface} from '@interfaces/FO'; +import type { Page } from '@playwright/test'; + +export interface FoLoginPageInterface extends FOBasePagePageInterface { + readonly pageTitle: string; + + customerLogin(page: Page, customer: CustomerData, waitForNavigation?: boolean): Promise; +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 0773b635..a4bd66f4 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -1,7 +1,7 @@ -import type {Page} from '@playwright/test'; +import type {BrowserContext, Page} from '@playwright/test'; export interface CommonPageInterface { + closePage(browserContext: BrowserContext, page: Page, tabId?: number): Promise; getPageTitle(page: Page): Promise; - goTo(page: Page, url: string): Promise; } diff --git a/src/pages/BO/modules/blockwishlist/index.ts b/src/pages/BO/modules/blockwishlist/index.ts new file mode 100644 index 00000000..3955a561 --- /dev/null +++ b/src/pages/BO/modules/blockwishlist/index.ts @@ -0,0 +1,15 @@ +import type {ModuleBlockwishlistMainPageInterface} from '@interfaces/BO/modules/blockwishlist/index'; +import semver from 'semver'; + +const psVersion = process.env.PS_VERSION ?? '0.0.0'; + +/* eslint-disable global-require */ +function requirePage(): ModuleBlockwishlistMainPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/BO/modules/blockwishlist/index'); + } + return require('@versions/8.0.0/pages/BO/modules/blockwishlist/index'); +} +/* eslint-enable global-require */ + +export default requirePage(); diff --git a/src/pages/BO/modules/blockwishlist/statistics.ts b/src/pages/BO/modules/blockwishlist/statistics.ts new file mode 100644 index 00000000..a68b5d0c --- /dev/null +++ b/src/pages/BO/modules/blockwishlist/statistics.ts @@ -0,0 +1,15 @@ +import type {ModuleBlockwishlistStatisticsPageInterface} from '@interfaces/BO/modules/blockwishlist/statistics'; +import semver from 'semver'; + +const psVersion = process.env.PS_VERSION ?? '0.0.0'; + +/* eslint-disable global-require */ +function requirePage(): ModuleBlockwishlistStatisticsPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/BO/modules/blockwishlist/statistics'); + } + return require('@versions/8.0.0/pages/BO/modules/blockwishlist/statistics'); +} +/* eslint-enable global-require */ + +export default requirePage(); diff --git a/src/pages/BO/modules/moduleConfiguration/index.ts b/src/pages/BO/modules/moduleConfiguration/index.ts new file mode 100644 index 00000000..e8683156 --- /dev/null +++ b/src/pages/BO/modules/moduleConfiguration/index.ts @@ -0,0 +1,36 @@ +import BOBasePage from '@pages/BO/BOBasePage'; + +import type {Page} from 'playwright'; + +/** + * Module configuration page, contains selectors and functions for the page. + * Can be used as a base page for specific module configuration page. + * @class + * @extends BOBasePage + */ +export default class ModuleConfiguration extends BOBasePage { + private readonly pageHeadSubtitle: string; + + /** + * @constructs + * Setting up titles and selectors to use on module configuration page + */ + constructor() { + super(); + + // Header selectors + this.pageHeadSubtitle = '.page-subtitle'; + } + + /* Methods */ + + /** + * Get module name from page title + * @return {Promise} + */ + async getPageSubtitle(page: Page): Promise { + return this.getTextContent(page, this.pageHeadSubtitle); + } +} + +module.exports = ModuleConfiguration; diff --git a/src/pages/BO/modules/moduleManager/index.ts b/src/pages/BO/modules/moduleManager/index.ts new file mode 100644 index 00000000..115e60e8 --- /dev/null +++ b/src/pages/BO/modules/moduleManager/index.ts @@ -0,0 +1,15 @@ +import type {ModuleManagerPageInterface} from '@interfaces/BO/modules/moduleManager'; +import semver from 'semver'; + +const psVersion = process.env.PS_VERSION ?? '0.0.0'; + +/* eslint-disable global-require */ +function requirePage(): ModuleManagerPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/BO/modules/moduleManager').moduleManager; + } + return require('@versions/8.0.0/pages/BO/modules/moduleManager').moduleManager; +} +/* eslint-enable global-require */ + +export default requirePage(); diff --git a/src/pages/FO/FOBasePage.ts b/src/pages/FO/FOBasePage.ts new file mode 100644 index 00000000..6dbd2fe9 --- /dev/null +++ b/src/pages/FO/FOBasePage.ts @@ -0,0 +1,779 @@ +// Import pages +import { FOBasePagePageInterface } from '@interfaces/FO'; +import CommonPage from '@pages/commonPage'; + +import type {Page} from 'playwright'; + +/** + * FO parent page, contains functions that can be used on all FO page + * @class + * @extends CommonPage + */ +export default class FOBasePage extends CommonPage implements FOBasePagePageInterface { + public readonly content: string; + + private readonly desktopLogo: string; + + private readonly desktopLogoLink: string; + + private readonly breadCrumb: string; + + private readonly breadCrumbLink: (link: string) => string; + + private readonly cartProductsCount: string; + + private readonly cartLink: string; + + private readonly userInfoLink: string; + + private readonly accountLink: string; + + private readonly logoutLink: string; + + private readonly contactLink: string; + + private readonly categoryMenu: (id: number) => string; + + private readonly languageSelectorDiv: string; + + private readonly defaultLanguageSpan: string; + + private readonly languageSelectorExpandIcon: string; + + private readonly languageSelectorList: string; + + private readonly languageSelectorMenuItemLink: (language: string) => string; + + private readonly currencySelectorDiv: string; + + private readonly defaultCurrencySpan: string; + + private readonly currencySelectorExpandIcon: string; + + private readonly currencySelectorMenuItemLink: (currency: string) => string; + + private readonly currencySelect: string; + + private readonly searchInput: string; + + private readonly autocompleteSearchResult: string; + + private readonly autocompleteSearchResultItem: string; + + private readonly autocompleteSearchResultItemLink: (nthChild: number) => string; + + private readonly pricesDropLink: string; + + private readonly newProductsLink: string; + + private readonly bestSalesLink: string; + + private readonly deliveryLink: string; + + private readonly legalNoticeLink: string; + + private readonly termsAndConditionsOfUseLink: string; + + private readonly aboutUsLink: string; + + private readonly securePaymentLink: string; + + private readonly contactUsLink: string; + + private readonly siteMapLink: string; + + private readonly storesLink: string; + + private readonly footerAccountList: string; + + private readonly informationLink: string; + + private readonly orderTrackingLink: string; + + private readonly signInLink: string; + + private readonly createAccountLink: string; + + private readonly addressesLink: string; + + private readonly addFirstAddressLink: string; + + private readonly ordersLink: string; + + private readonly creditSlipsLink: string; + + private readonly vouchersLink: string; + + private readonly wishListLink: string; + + private readonly signOutLink: string; + + private readonly wrapperContactBlockDiv: string; + + private readonly footerLinksDiv: string; + + private readonly wrapperDiv: (position: number) => string; + + private readonly wrapperTitle: (position: number) => string; + + private readonly wrapperSubmenu: (position: number) => string; + + private readonly wrapperSubmenuItemLink: (position: number) => string; + + private readonly copyrightLink: string; + + protected readonly alertSuccessBlock: string; + + protected readonly notificationsBlock: string; + + protected readonly userMenuDropdown: string; + + protected readonly currencySelector: string; + + protected readonly languageSelector: string; + + private readonly cartProductsCountHummingbird: string; + + protected readonly navbarLink: string; + + private readonly hCopyrightLink: string; + + protected readonly hSearchInput: string; + + protected theme: string; + + /** + * @constructs + * Setting up texts and selectors to use on all FO pages + */ + constructor(theme: string = 'classic') { + super(); + + this.theme = theme; + + // Selectors for home page + // Header links + this.content = '#content'; + this.desktopLogo = '#_desktop_logo'; + this.desktopLogoLink = `${this.desktopLogo} a`; + this.breadCrumb = '#wrapper div nav.breadcrumb'; + this.breadCrumbLink = (link) => `${this.breadCrumb} a[href*=${link}]`; + this.cartProductsCount = '#_desktop_cart .cart-products-count'; + this.cartLink = '#_desktop_cart a'; + this.userInfoLink = '#_desktop_user_info'; + this.accountLink = `${this.userInfoLink} .user-info a[href*="/my-account"]`; + this.logoutLink = `${this.userInfoLink} .user-info a[href*="/?mylogout="]`; + this.contactLink = '#contact-link'; + this.categoryMenu = (id) => `#category-${id} > a`; + this.languageSelectorDiv = '#_desktop_language_selector'; + this.defaultLanguageSpan = `${this.languageSelectorDiv} button span`; + this.languageSelectorExpandIcon = `${this.languageSelectorDiv} i.expand-more`; + this.languageSelectorList = `${this.languageSelectorDiv} .js-dropdown.open`; + this.languageSelectorMenuItemLink = (language) => `${this.languageSelectorDiv} ul li ` + + `a[data-iso-code='${language}']`; + this.currencySelectorDiv = '#_desktop_currency_selector'; + this.defaultCurrencySpan = `${this.currencySelectorDiv} button span`; + this.currencySelectorExpandIcon = `${this.currencySelectorDiv} i.expand-more`; + this.currencySelectorMenuItemLink = (currency) => `${this.currencySelectorExpandIcon} ul li a[title='${currency}']`; + this.currencySelect = 'select[aria-labelledby=\'currency-selector-label\']'; + this.searchInput = '#search_widget input.ui-autocomplete-input'; + this.autocompleteSearchResult = '.ui-autocomplete'; + this.autocompleteSearchResultItem = `${this.autocompleteSearchResult} .ui-menu-item`; + this.autocompleteSearchResultItemLink = (nthChild) => `${this.autocompleteSearchResult} ` + + `.ui-menu-item:nth-child(${nthChild}) a`; + + // Footer links + // Products links selectors + this.pricesDropLink = '#link-product-page-prices-drop-1'; + this.newProductsLink = '#link-product-page-new-products-1'; + this.bestSalesLink = '#link-product-page-best-sales-1'; + // Our company links selectors + this.deliveryLink = '#link-cms-page-1-2'; + this.legalNoticeLink = '#link-cms-page-2-2'; + this.termsAndConditionsOfUseLink = '#link-cms-page-3-2'; + this.aboutUsLink = '#link-cms-page-4-2'; + this.securePaymentLink = '#link-cms-page-5-2'; + this.contactUsLink = '#link-static-page-contact-2'; + this.siteMapLink = '#link-static-page-sitemap-2'; + this.storesLink = '#link-static-page-stores-2'; + // Your account links selectors + this.footerAccountList = '#footer_account_list'; + this.informationLink = `${this.footerAccountList} a[title='Information']`; + this.orderTrackingLink = `${this.footerAccountList} a[title='Order tracking']`; + this.signInLink = `${this.footerAccountList} a[href*='/my-account']`; + this.createAccountLink = `${this.footerAccountList} a[title='Create account']`; + this.addressesLink = `${this.footerAccountList} a[title='Addresses']`; + this.addFirstAddressLink = `${this.footerAccountList} a[title='Add first address']`; + this.ordersLink = `${this.footerAccountList} a[title='Orders']`; + this.creditSlipsLink = `${this.footerAccountList} a[title='Credit slips']`; + this.vouchersLink = `${this.footerAccountList} a[title='Vouchers']`; + this.wishListLink = `${this.footerAccountList} a[title='My wishlists']`; + this.signOutLink = `${this.footerAccountList} a[title='Log me out']`; + + // Store information + this.wrapperContactBlockDiv = '#footer div.block-contact'; + + this.footerLinksDiv = '#footer .links'; + this.wrapperDiv = (position) => `${this.footerLinksDiv} .wrapper:nth-child(${position})`; + this.wrapperTitle = (position) => `${this.wrapperDiv(position)} p`; + this.wrapperSubmenu = (position) => `${this.wrapperDiv(position)} ul[id*='footer_sub_menu']`; + this.wrapperSubmenuItemLink = (position) => `${this.wrapperSubmenu(position)} li a`; + + // Copyright + this.copyrightLink = '#footer div.footer-container a[href*="www.prestashop-project.org"]'; + + // Alert block selectors + this.alertSuccessBlock = '.alert-success ul li'; + this.notificationsBlock = '#notifications'; + + // Hummingbird + this.userMenuDropdown = '#userMenuButton'; + this.currencySelector = '#currency-selector'; + this.languageSelector = '#language-selector'; + this.cartProductsCountHummingbird = '#_desktop_cart .header-block__action-btn span.header-block__badge'; + this.navbarLink = '.navbar-brand'; + this.hCopyrightLink = '#footer div.footer__main p.copyright a[href*="www.prestashop-project.org"]'; + this.hSearchInput = '#search_widget .js-search-input'; + } + + // Header methods + /** + * Go to header link + * @param page {Page} Browser tab + * @param link {string} Header selector that contain link to click on to + * @param hasPageChange {boolean} + * @returns {Promise} + */ + async clickOnHeaderLink(page: Page, link: string, hasPageChange: boolean = true): Promise { + let selector; + + switch (link) { + case 'Contact us': + selector = this.contactLink; + break; + + case 'Sign in': + selector = this.userInfoLink; + break; + + case 'Cart': + selector = this.cartLink; + break; + + case 'Logo': + selector = this.theme === 'hummingbird' ? this.navbarLink : this.desktopLogoLink; + break; + + default: + throw new Error(`The page ${link} was not found`); + } + + if (hasPageChange) { + return this.clickAndWaitForURL(page, selector); + } + return this.clickAndWaitForLoadState(page, selector); + } + + /** + * Get breadcrumb text + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getBreadcrumbText(page: Page): Promise { + return this.getTextContent(page, this.breadCrumb); + } + + /** + * Click on bread crumb link + * @param page {Page} Browser tab + * @param link {string} Link to click on + * @returns {Promise} + */ + async clickOnBreadCrumbLink(page: Page, link: string): Promise { + const currentUrl: string = page.url(); + + await page.locator(this.breadCrumbLink(link)).first().click(); + await page.waitForURL((url: URL): boolean => url.toString() !== currentUrl, {waitUntil: 'networkidle'}); + } + + /** + * Go to the home page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToHomePage(page: Page): Promise { + if (this.theme === 'hummingbird') { + await this.waitForVisibleSelector(page, this.navbarLink); + await this.clickAndWaitForLoadState(page, this.navbarLink); + return; + } + + await this.waitForVisibleSelector(page, this.desktopLogo); + await this.clickAndWaitForLoadState(page, this.desktopLogoLink); + } + + /** + * Go to login Page + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToLoginPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.userInfoLink); + } + + /** + * Logout from FO + * @param page {Page} Browser tab + * @return {Promise} + */ + async logout(page: Page): Promise { + if (this.theme === 'hummingbird') { + await page.locator(this.userMenuDropdown).click(); + await this.clickAndWaitForLoadState(page, this.logoutLink); + await this.elementNotVisible(page, this.logoutLink, 2000); + + return; + } + await this.clickAndWaitForLoadState(page, this.logoutLink); + } + + /** + * Check if customer is connected + * @param page {Page} Browser tab + * @return {Promise} + */ + async isCustomerConnected(page: Page): Promise { + return this.elementVisible(page, this.theme === 'hummingbird' ? this.userMenuDropdown : this.logoutLink, 1000); + } + + /** + * Click on link to go to account page + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToMyAccountPage(page: Page): Promise { + if (this.theme === 'hummingbird') { + await page.locator(this.userMenuDropdown).click(); + await this.clickAndWaitForURL(page, this.accountLink); + + return; + } + await this.clickAndWaitForURL(page, this.accountLink); + } + + /** + * Is language list visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isLanguageListVisible(page: Page): Promise { + if (this.theme === 'hummingbird') { + return this.elementVisible(page, this.languageSelector, 1000); + } + return this.elementVisible(page, this.languageSelectorExpandIcon, 1000); + } + + /** + * Get shop language + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getShopLanguage(page: Page): Promise { + return this.getAttributeContent(page, 'html[lang]', 'lang'); + } + + /** + * Change language in FO + * @param page {Page} Browser tab + * @param lang {string} Language to choose on the select (ex: en or fr) + * @return {Promise} + */ + async changeLanguage(page: Page, lang: string = 'en'): Promise { + if (this.theme === 'hummingbird') { + const textContent = await page + .locator(`${this.languageSelector} option[data-iso-code='${lang}']`) + .textContent(); + + await this.selectByVisibleText(page, this.languageSelector, textContent!); + return; + } + await Promise.all([ + page.locator(this.languageSelectorExpandIcon).click(), + this.waitForVisibleSelector(page, this.languageSelectorList), + ]); + await this.clickAndWaitForLoadState(page, this.languageSelectorMenuItemLink(lang)); + } + + /** + * Get default shop language + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getDefaultShopLanguage(page: Page): Promise { + if (this.theme === 'hummingbird') { + return page + .locator(this.languageSelector) + .evaluate((el: HTMLSelectElement): string => el.options[el.options.selectedIndex].textContent ?? ''); + } + return this.getTextContent(page, this.defaultLanguageSpan); + } + + /** + * Return true if language exist in FO + * @param page {Page} Browser tab + * @param lang {string} Language to check on the select (ex: en or fr) + * @return {Promise} + */ + async languageExists(page: Page, lang: string = 'en'): Promise { + await page.locator(this.languageSelectorExpandIcon).click(); + return this.elementVisible(page, this.languageSelectorMenuItemLink(lang), 1000); + } + + /** + * Change currency in FO + * @param page {Page} Browser tab + * @param isoCode {string} Iso code of the currency to choose + * @param symbol {string} Symbol of the currency to choose + * @return {Promise} + */ + async changeCurrency(page: Page, isoCode: string = 'EUR', symbol: string = '€'): Promise { + const currency = isoCode === symbol ? isoCode : `${isoCode} ${symbol}`; + + if (this.theme === 'hummingbird') { + await this.selectByVisibleText(page, this.currencySelector, currency); + return; + } + // If isoCode and symbol are the same, only isoCode id displayed in FO + const currentUrl: string = page.url(); + + await Promise.all([ + this.selectByVisibleText(page, this.currencySelect, currency, true), + page.waitForURL((url: URL): boolean => url.toString() !== currentUrl, {waitUntil: 'networkidle'}), + ]); + } + + /** + * Is currency dropdownExist + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isCurrencyDropdownExist(page: Page): Promise { + return this.elementVisible(page, this.currencySelectorExpandIcon, 1000); + } + + /** + * Get if currency exists on dropdown + * @param page {Page} Browser tab + * @param currencyName {string} Name of the currency to check + * @returns {Promise} + */ + async currencyExists(page: Page, currencyName: string = 'Euro'): Promise { + await page.locator(this.currencySelectorExpandIcon).click(); + return this.elementVisible(page, this.currencySelectorMenuItemLink(currencyName), 1000); + } + + /** + * Get default currency + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getDefaultCurrency(page: Page): Promise { + if (this.theme === 'hummingbird') { + return page + .locator(this.currencySelector) + .evaluate((el: HTMLSelectElement): string => el.options[el.options.selectedIndex].textContent ?? ''); + } + return this.getTextContent(page, this.defaultCurrencySpan); + } + + /** + * Go to category + * @param page {Page} Browser tab + * @param categoryID {number} Category id from the BO + * @returns {Promise} + */ + async goToCategory(page: Page, categoryID: number): Promise { + await this.clickAndWaitForURL(page, this.categoryMenu(categoryID)); + } + + /** + * Go to subcategory + * @param page {Page} Browser tab + * @param categoryID {number} Category id from the BO + * @param subCategoryID {number} Subcategory id from the BO + * @returns {Promise} + */ + async goToSubCategory(page: Page, categoryID: number, subCategoryID: number): Promise { + await page.locator(this.categoryMenu(categoryID)).hover(); + await this.clickAndWaitForURL(page, this.categoryMenu(subCategoryID)); + } + + /** + * Get store information + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getStoreInformation(page: Page): Promise { + return this.getTextContent(page, this.wrapperContactBlockDiv); + } + + /** + * Get cart notifications number + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getCartNotificationsNumber(page: Page): Promise { + return this.getNumberFromText( + page, + this.theme === 'hummingbird' ? this.cartProductsCountHummingbird : this.cartProductsCount, + 2000, + ); + } + + /** + * Go to cart page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToCartPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.cartLink); + } + + /** + * Close the autocomplete search result + * @param page {Page} Browser tab + * @returns {void} + */ + async closeAutocompleteSearch(page: Page): Promise { + await page.keyboard.press('Escape'); + } + + /** + * Check if there are autocomplete search result + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isAutocompleteSearchResultVisible(page: Page): Promise { + return this.elementVisible(page, this.autocompleteSearchResult, 2000); + } + + /** + * Check if there are autocomplete search result + * @param page {Page} Browser tab + * @param productName {string} Product name to search + * @returns {Promise} + */ + async hasAutocompleteSearchResult(page: Page, productName: string): Promise { + await this.setValue(page, this.searchInput, productName); + return this.isAutocompleteSearchResultVisible(page); + } + + /** + * Get autocomplete search result + * @param page {Page} Browser tab + * @param productName {string} Product name to search + * @returns {Promise} + */ + async getAutocompleteSearchResult(page: Page, productName: string): Promise { + await this.setValue(page, this.searchInput, productName); + await this.waitForVisibleSelector(page, this.autocompleteSearchResult); + return this.getTextContent(page, this.autocompleteSearchResult); + } + + /** + * Count autocomplete search result + * @param page {Page} Browser tab + * @param productName {string} Product name to search + * @returns {Promise} + */ + async countAutocompleteSearchResult(page: Page, productName: string): Promise { + await this.setValue(page, this.searchInput, productName); + await this.waitForVisibleSelector(page, this.autocompleteSearchResultItem); + return page.locator(this.autocompleteSearchResultItem).count(); + } + + /** + * Search product + * @param page {Page} Browser tab + * @param productName {string} Product name to search + * @returns {Promise} + */ + async searchProduct(page: Page, productName: string): Promise { + const currentUrl: string = page.url(); + + await this.setValue(page, this.theme === 'hummingbird' ? this.hSearchInput : this.searchInput, productName); + await page.keyboard.press('Enter'); + await page.waitForURL((url: URL): boolean => url.toString() !== currentUrl, {waitUntil: 'networkidle'}); + } + + /** + * Click autocomplete search on the nth result + * @param page {Page} Browser tab + * @param productName {string} Product name to search + * @param nthResult {number} Nth result to click + * @returns {Promise} + */ + async clickAutocompleteSearchResult(page: Page, productName: string, nthResult: number): Promise { + await this.setValue(page, this.searchInput, productName); + await this.waitForVisibleSelector(page, this.autocompleteSearchResultItem); + await this.clickAndWaitForURL(page, this.autocompleteSearchResultItemLink(nthResult)); + } + + // Footer methods + /** + * Get Title of Block that contains links in footer + * @param page {Page} Browser tab + * @param position {number} Position of the links on footer + * @returns {Promise} + */ + async getFooterLinksBlockTitle(page: Page, position: number): Promise { + return this.getTextContent(page, this.wrapperTitle(position)); + } + + /** + * Get text content of footer links + * @param page {Page} Browser tab + * @param position {number} Position of the links on footer + * @return {Promise>} + */ + async getFooterLinksTextContent(page: Page, position: number): Promise> { + return (await page + .locator(this.wrapperSubmenuItemLink(position)) + .allTextContents()) + .map((textContent) => textContent.trim()); + } + + /** + * Go to footer link + * @param page {Page} Browser tab + * @param textSelector {string} String displayed on footer link to click on + * @returns {Promise} + */ + async goToFooterLink(page: Page, textSelector: string): Promise { + let selector; + + switch (textSelector) { + case 'Prices drop': + selector = this.pricesDropLink; + break; + + case 'New products': + selector = this.newProductsLink; + break; + + case 'Best sellers': + selector = this.bestSalesLink; + break; + + case 'Delivery': + selector = this.deliveryLink; + break; + + case 'Legal Notice': + selector = this.legalNoticeLink; + break; + + case 'Terms and conditions of use': + selector = this.termsAndConditionsOfUseLink; + break; + + case 'About us': + selector = this.aboutUsLink; + break; + + case 'Secure payment': + selector = this.securePaymentLink; + break; + + case 'Contact us': + selector = this.contactUsLink; + break; + + case 'Sitemap': + selector = this.siteMapLink; + break; + + case 'Stores': + selector = this.storesLink; + break; + + case 'Information': + selector = this.informationLink; + break; + + case 'Order tracking': + selector = this.orderTrackingLink; + break; + + case 'Sign in': + selector = this.signInLink; + break; + + case 'Create account': + selector = this.createAccountLink; + break; + + case 'Addresses': + selector = this.addressesLink; + break; + + case 'Add first address': + selector = this.addFirstAddressLink; + break; + + case 'Orders': + selector = this.ordersLink; + break; + + case 'Credit slips': + selector = this.creditSlipsLink; + break; + + case 'Vouchers': + selector = this.vouchersLink; + break; + + case 'Wishlist': + selector = this.wishListLink; + break; + + case 'Sign out': + selector = this.signOutLink; + break; + + default: + throw new Error(`The page ${textSelector} was not found`); + } + + return this.clickAndWaitForURL(page, selector); + } + + /** + * Get copyright + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getCopyright(page: Page): Promise { + return this.getTextContent(page, this.theme === 'hummingbird' ? this.hCopyrightLink : this.copyrightLink); + } + + /** + * Check that currency is visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isCurrencyVisible(page: Page): Promise { + return this.elementVisible(page, this.currencySelectorDiv, 1000); + } + + /** + * Get the value of an input + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getSearchValue(page: Page): Promise { + return this.getInputValue(page, this.searchInput); + } +} + +module.exports = FOBasePage; diff --git a/src/pages/FO/category/index.ts b/src/pages/FO/category/index.ts new file mode 100644 index 00000000..9374cebe --- /dev/null +++ b/src/pages/FO/category/index.ts @@ -0,0 +1,15 @@ +import type {FoCategoryPageInterface} from '@interfaces/FO/category'; +import semver from 'semver'; + +const psVersion = process.env.PS_VERSION ?? '0.0.0'; + +/* eslint-disable global-require */ +function requirePage(): FoCategoryPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/FO/category'); + } + return require('@versions/8.0.0/pages/FO/category'); +} +/* eslint-enable global-require */ + +export default requirePage(); diff --git a/src/pages/FO/home/index.ts b/src/pages/FO/home/index.ts new file mode 100644 index 00000000..f0f1463f --- /dev/null +++ b/src/pages/FO/home/index.ts @@ -0,0 +1,15 @@ +import type {FoHomePageInterface} from '@interfaces/FO/home'; +import semver from 'semver'; + +const psVersion = process.env.PS_VERSION ?? '0.0.0'; + +/* eslint-disable global-require */ +function requirePage(): FoHomePageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/FO/home').homePage; + } + return require('@versions/8.0.0/pages/FO/home').homePage; +} +/* eslint-enable global-require */ + +export default requirePage(); diff --git a/src/pages/FO/login/index.ts b/src/pages/FO/login/index.ts new file mode 100644 index 00000000..6ee1a6f5 --- /dev/null +++ b/src/pages/FO/login/index.ts @@ -0,0 +1,15 @@ +import type {FoLoginPageInterface} from '@interfaces/FO/login'; +import semver from 'semver'; + +const psVersion = process.env.PS_VERSION ?? '0.0.0'; + +/* eslint-disable global-require */ +function requirePage(): FoLoginPageInterface { + if (semver.gte(psVersion, '8.0.0')) { + return require('@versions/8.0.0/pages/FO/login').loginPage; + } + return require('@versions/8.0.0/pages/FO/login').loginPage; +} +/* eslint-enable global-require */ + +export default requirePage(); diff --git a/src/pages/commonPage.ts b/src/pages/commonPage.ts index 6bc5a993..6fb2b4ab 100644 --- a/src/pages/commonPage.ts +++ b/src/pages/commonPage.ts @@ -1,5 +1,6 @@ // Import data import type {PageWaitForSelectorOptionsState, WaitForNavigationWaitUntil} from '@data/types/playwright'; +import { CommonPageInterface } from '@interfaces/index'; import type { BrowserContext, ElementHandle, JSHandle, FileChooser, Frame, Page, Locator, @@ -9,7 +10,7 @@ import type { * Parent page, contains functions that can be used in every page (BO, FO ...) * @class */ -export default class CommonPage { +export default class CommonPage implements CommonPageInterface { /** * Get page title * @param page {Page} Browser tab diff --git a/src/versions/8.0.0/pages/BO/modules/blockwishlist/index.ts b/src/versions/8.0.0/pages/BO/modules/blockwishlist/index.ts new file mode 100644 index 00000000..03520965 --- /dev/null +++ b/src/versions/8.0.0/pages/BO/modules/blockwishlist/index.ts @@ -0,0 +1,53 @@ +import {ModuleBlockwishlistMainPageInterface} from '@interfaces/BO/modules/blockwishlist'; +import ModuleConfiguration from '@pages/BO/modules/moduleConfiguration'; + +import type {Page} from 'playwright'; + +/** + * Module configuration page for module : blockwishlist, contains selectors and functions for the page + * @class + * @extends ModuleConfiguration + */ +class Blockwishlist extends ModuleConfiguration implements ModuleBlockwishlistMainPageInterface { + public readonly pageTitle: string; + + private readonly headTabs: string; + + private readonly headTab: string; + + private readonly headTabNamed: (name: string) => string; + + /** + * @constructs + */ + constructor() { + super(); + + this.pageTitle = `Configuration • ${global.INSTALL.SHOP_NAME}`; + + // Selectors + this.headTabs = '#head_tabs'; + this.headTab = `${this.headTabs} .nav-item`; + this.headTabNamed = (name: string) => `${this.headTab} #subtab-Wishlist${name}AdminController`; + } + + // Methods + /** + * @param page {Page} + * @returns Promise + */ + async goToStatisticsTab(page: Page): Promise { + await this.clickAndWaitForURL(page, this.headTabNamed('Statistics')); + } + + /** + * @param page {Page} + * @param name {'Configuration'|'Statistics'} + * @returns Promise + */ + async isTabActive(page: Page, name: 'Configuration'|'Statistics'): Promise { + return this.elementVisible(page, `${this.headTabNamed(name)}.active.current`, 1000); + } +} + +module.exports = new Blockwishlist(); diff --git a/src/versions/8.0.0/pages/BO/modules/blockwishlist/statistics.ts b/src/versions/8.0.0/pages/BO/modules/blockwishlist/statistics.ts new file mode 100644 index 00000000..aa496401 --- /dev/null +++ b/src/versions/8.0.0/pages/BO/modules/blockwishlist/statistics.ts @@ -0,0 +1,59 @@ +import {ModuleBlockwishlistStatisticsPageInterface} from '@interfaces/BO/modules/blockwishlist/statistics'; +import ModuleConfiguration from '@pages/BO/modules/moduleConfiguration'; + +import type {Page} from 'playwright'; + +/** + * Module configuration page for module : blockwishlist, contains selectors and functions for the page + * @class + * @extends ModuleConfiguration + */ +class BlockwishlistStatistics extends ModuleConfiguration implements ModuleBlockwishlistStatisticsPageInterface { + public readonly pageTitle: string; + + private readonly rowTopBar: string; + + private readonly refreshStatsButton: string; + + private readonly gridTable: string; + + private readonly gridTableBody: string; + + private readonly gridTableEmptyRow: string; + + /** + * @constructs + */ + constructor() { + super(); + + this.pageTitle = `Statistics • ${global.INSTALL.SHOP_NAME}`; + + // Selectors + this.rowTopBar = '.row.wishlist-stats-topbar'; + this.refreshStatsButton = `${this.rowTopBar} button.js-refresh`; + this.gridTable = '#statistics_all_time_grid_table'; + this.gridTableBody = `${this.gridTable} tbody`; + this.gridTableEmptyRow = `${this.gridTableBody} tr.empty_row`; + } + + // Methods + /** + * @param page {Page} + * @returns Promise + */ + async getTextForEmptyTable(page: Page): Promise { + return this.getTextContent(page, this.gridTableEmptyRow); + } + + /** + * @param page {Page} + * @returns Promise + */ + async refreshStatistics(page: Page): Promise { + await page.locator(this.refreshStatsButton).click(); + await page.waitForTimeout(2000); + } +} + +module.exports = new BlockwishlistStatistics(); diff --git a/src/versions/8.0.0/pages/BO/modules/moduleManager/index.ts b/src/versions/8.0.0/pages/BO/modules/moduleManager/index.ts new file mode 100644 index 00000000..89208a86 --- /dev/null +++ b/src/versions/8.0.0/pages/BO/modules/moduleManager/index.ts @@ -0,0 +1,553 @@ +import ModuleData from '@data/faker/module'; +import {ModuleManagerPageInterface} from '@interfaces/BO/modules/moduleManager'; +import BOBasePage from '@pages/BO/BOBasePage'; + +import type {Page} from 'playwright'; + +/** + * Module manager page, contains selectors and functions for the page + * @class + * @extends BOBasePage + */ +export class ModuleManager extends BOBasePage implements ModuleManagerPageInterface { + public pageTitle: string; + + public readonly disableModuleSuccessMessage: (moduleTag: string) => string; + + public readonly enableModuleSuccessMessage: (moduleTag: string) => string; + + public readonly resetModuleSuccessMessage: (moduleTag: string) => string; + + public readonly installModuleSuccessMessage: (moduleTag: string) => string; + + public readonly uninstallModuleSuccessMessage: (moduleTag: string) => string; + + public readonly uploadModuleSuccessMessage: string; + + private readonly alertsTab: string; + + private readonly searchModuleTagInput: string; + + private readonly searchModuleButton: string; + + private readonly uploadModuleButton: string; + + private readonly uploadModal: string; + + private readonly uploadModuleLink: string; + + private readonly uploadModuleModalSuccessMessage: string; + + private readonly uploadModuleModalCloseButton: string; + + private readonly topMenuDiv: string; + + private readonly bulkActionsButton: string; + + private readonly bulkActionsDropDownButton: string; + + private readonly bulkActionsDropDownList: string; + + private readonly bulkActionName: (action: string) => string; + + private readonly bulkActionsModal: string; + + private readonly bulkActionsModalConfirmButton: string; + + private readonly modulesListBlock: string; + + private readonly modulesListBlockTitle: string; + + private readonly allModulesBlock: string; + + private readonly moduleBlocks: string; + + private readonly moduleBlock: (moduleTag: string) => string; + + private readonly moduleCheckboxButton: (moduleTag: string) => string; + + private readonly seeMoreButton: (blockName: string) => string; + + private readonly seeLessButton: (blockName: string) => string; + + private readonly moduleListBlock: (blockName: string) => string; + + private readonly actionModuleButton: (moduleTag: string, action: string) => string; + + private readonly configureModuleButton: (moduleTag: string) => string; + + private readonly actionsDropdownButton: (moduleTag: string) => string; + + private readonly actionModuleButtonInDropdownList: (action: string) => string; + + private readonly modalConfirmAction: (moduleTag: string, action: string) => string; + + private readonly modalConfirmButton: (moduleTag: string, action: string) => string; + + private readonly modalConfirmCancel: (moduleTag: string, action: string) => string; + + private readonly modalConfirmUninstallForceDeletion: (moduleTag: string) => string; + + private readonly statusDropdownDiv: string; + + private readonly statusDropdownMenu: string; + + private readonly statusDropdownItemLink: (ref: number) => string; + + private readonly filterByAllModulesButton: string; + + private readonly categoriesSelectDiv: string; + + private readonly categoriesDropdownDiv: string; + + private readonly categoryDropdownItem: (cat: string) => string; + + /** + * @constructs + * Setting up titles and selectors to use on module manager page + */ + constructor() { + super(); + + this.pageTitle = 'Module manager •'; + this.disableModuleSuccessMessage = (moduleTag: string) => `Disable action on module ${moduleTag} succeeded.`; + this.enableModuleSuccessMessage = (moduleTag: string) => `Enable action on module ${moduleTag} succeeded.`; + this.resetModuleSuccessMessage = (moduleTag: string) => `Reset action on module ${moduleTag} succeeded.`; + this.installModuleSuccessMessage = (moduleTag: string) => `Install action on module ${moduleTag} succeeded.`; + this.uninstallModuleSuccessMessage = (moduleTag: string) => `Uninstall action on module ${moduleTag} succeeded.`; + this.uploadModuleSuccessMessage = 'Module installed!'; + + // Tabs + this.alertsTab = '#subtab-AdminModulesNotifications'; + + // Header Selectors + this.searchModuleTagInput = '#search-input-group input.pstaggerAddTagInput'; + this.searchModuleButton = '#module-search-button'; + this.uploadModuleButton = '#page-header-desc-configuration-add_module'; + this.uploadModal = '#importDropzone'; + this.uploadModuleLink = `${this.uploadModal} div.module-import-start p.module-import-start-main-text a`; + this.uploadModuleModalSuccessMessage = `${this.uploadModal} div.module-import-success p.module-import-success-msg`; + this.uploadModuleModalCloseButton = '#module-modal-import-closing-cross'; + + // Top menu + this.topMenuDiv = 'div.module-top-menu'; + this.bulkActionsButton = `${this.topMenuDiv} div.module-top-menu-item:nth-child(3)`; + this.bulkActionsDropDownButton = '#bulk-actions-dropdown'; + this.bulkActionsDropDownList = 'div.ps-dropdown-menu.dropdown-menu.module-category-selector.items-list.js-items-list.show'; + this.bulkActionName = (action: string) => `${this.bulkActionsDropDownList} a[data-display-name='${action}']`; + + // Bulk actions modal + this.bulkActionsModal = '#module-modal-bulk-confirm'; + this.bulkActionsModalConfirmButton = '#module-modal-confirm-bulk-ack'; + + // Filter by categories dropdown selectors + this.categoriesSelectDiv = '#categories'; + this.categoriesDropdownDiv = 'div.ps-dropdown-menu.dropdown-menu.module-category-selector'; + this.categoryDropdownItem = (cat: string) => `${this.categoriesDropdownDiv} a[data-category-display-name='${cat}']`; + + // Filter by status dropdown selectors + this.statusDropdownDiv = '#module-status-dropdown'; + this.statusDropdownMenu = 'div.ps-dropdown-menu[aria-labelledby=\'module-status-dropdown\']'; + this.statusDropdownItemLink = (ref: number) => `${this.statusDropdownMenu} a[data-status-ref='${ref}']`; + this.filterByAllModulesButton = '.module-status-reset'; + + // Modules list selectors + this.modulesListBlock = '.module-short-list:not([style=\'display: none;\'])'; + this.modulesListBlockTitle = `${this.modulesListBlock} span.module-search-result-title`; + this.allModulesBlock = `${this.modulesListBlock} .module-item-list`; + this.moduleBlocks = 'div.module-short-list'; + this.moduleBlock = (moduleTag: string) => `${this.allModulesBlock}[data-tech-name=${moduleTag}]`; + this.moduleCheckboxButton = (moduleTag: string) => `${this.moduleBlock(moduleTag)}` + + ' div.module-checkbox-bulk-list.md-checkbox label i'; + this.seeMoreButton = (blockName: string) => `#main-div div.module-short-list button.see-more[data-category=${blockName}]`; + this.seeLessButton = (blockName: string) => `#main-div div.module-short-list button.see-less[data-category=${blockName}]`; + this.moduleListBlock = (blockName: string) => `#modules-list-container-${blockName} div.module-item-list`; + + // Module actions selector + this.actionModuleButton = (moduleTag: string, action: string) => `div[data-tech-name=${moduleTag}]` + + ` button.module_action_menu_${action}`; + this.configureModuleButton = (moduleTag: string) => `div[data-tech-name=${moduleTag}]` + + ' div.module-actions a[href*=\'/action/configure\']'; + + // Module actions in dropdown selectors + this.actionsDropdownButton = (moduleTag: string) => `div[data-tech-name=${moduleTag}] button.dropdown-toggle`; + this.actionModuleButtonInDropdownList = (action: string) => 'div.btn-group.module-actions.show' + + ` button.module_action_menu_${action}`; + + // Modal confirmation selectors + this.modalConfirmAction = (moduleTag: string, action: string) => `#module-modal-confirm-${moduleTag}-${action}`; + this.modalConfirmButton = (moduleTag: string, action: string) => `${this.modalConfirmAction(moduleTag, action)}` + + ` div.modal-footer a.module_action_modal_${action}`; + this.modalConfirmCancel = (moduleTag: string, action: string) => `${this.modalConfirmAction(moduleTag, action)}` + + ' div.modal-footer input[type="button"][data-dismiss="modal"]'; + this.modalConfirmUninstallForceDeletion = (moduleTag: string) => `${this.modalConfirmAction(moduleTag, 'uninstall')}` + + ' #force_deletion'; + } + + /* + Methods + */ + + /** + * Go to the Alerts Tab + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToAlertsTab(page: Page): Promise { + await page.locator(this.alertsTab).click(); + } + + /** + * Upload module + * @param page {Page} Browser tab + * @param file {string} File to upload + * @return {Promise} + */ + async uploadModule(page: Page, file: string): Promise { + await this.waitForSelectorAndClick(page, this.uploadModuleButton); + + await this.uploadOnFileChooser(page, this.uploadModuleLink, [file]); + + return this.getTextContent(page, this.uploadModuleModalSuccessMessage); + } + + /** + * Close upload module modal + * @param page {Page} Browser tab + * @return {Promise} + */ + async closeUploadModuleModal(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.uploadModuleModalCloseButton); + + return this.elementNotVisible(page, this.uploadModal, 1000); + } + + /** + * Search Module in Page module Catalog + * @param page {Page} Browser tab + * @param module {ModuleData} Tag of the Module + * @return {Promise} + */ + async searchModule(page: Page, module: ModuleData): Promise { + await this.reloadPage(page); + await page.locator(this.searchModuleTagInput).fill(module.tag); + await page.locator(this.searchModuleButton).click(); + + return this.isModuleVisible(page, module); + } + + /** + * Return if the module is visible + * @param page {Page} Browser tab + * @param module {ModuleData} Tag of the Module + * @return {Promise} + */ + async isModuleVisible(page: Page, module: ModuleData): Promise { + return this.elementVisible(page, this.moduleBlock(module.tag), 10000); + } + + /** + * Get module name + * @param page {Page} Browser tab + * @param module {ModuleData} Tag of the Module + * @return {Promise} + */ + async getModuleName(page: Page, module: ModuleData): Promise { + return this.getAttributeContent(page, `${this.moduleBlock(module.tag)} [data-original-title]`, 'data-original-title'); + } + + /** + * Is bulk actions button disabled + * @param page {Page} Browser tab + * @return {Promise} + */ + async isBulkActionsButtonDisabled(page: Page): Promise { + return this.elementVisible(page, `${this.bulkActionsButton}.disabled`, 1000); + } + + /** + * Select module + * @param page {Page} Browser tab + * @param moduleTag {string} Technical name of the module + * @return {Promise} + */ + async selectModule(page: Page, moduleTag: string): Promise { + await page.locator(this.moduleCheckboxButton(moduleTag)).evaluate((el: HTMLElement) => el.click()); + } + + /** + * Bulk actions + * @param page {Page} Browser tab + * @param action {string} Action to set with bulk actions + * @return {Promise} + */ + async bulkActions(page: Page, action: string): Promise { + await this.closeGrowlMessage(page); + + await page.locator(this.bulkActionsDropDownButton).click(); + await this.waitForSelectorAndClick(page, this.bulkActionName(action)); + + await this.waitForVisibleSelector(page, this.bulkActionsModal); + await this.waitForSelectorAndClick(page, this.bulkActionsModalConfirmButton); + return this.getGrowlMessageContent(page); + } + + /** + * Click on button configure of a module + * @param page {Page} Browser tab + * @param moduleTag {string} Technical name of the module + * @return {Promise} + */ + async goToConfigurationPage(page: Page, moduleTag: string): Promise { + if (await this.elementNotVisible(page, this.configureModuleButton(moduleTag), 1000)) { + await Promise.all([ + page.locator(this.actionsDropdownButton(moduleTag)).click(), + this.waitForVisibleSelector(page, `${this.actionsDropdownButton(moduleTag)}[aria-expanded='true']`), + ]); + } + await page.locator(this.configureModuleButton(moduleTag)).click(); + } + + /** + * Filter modules by status + * @param page {Page} Browser tab + * @param status {string} Status to filter with + * @return {Promise} + */ + async filterByStatus(page: Page, status: string): Promise { + // Open dropdown + await page.locator(this.statusDropdownDiv).click(); + await this.waitForVisibleSelector(page, `${this.statusDropdownDiv}[aria-expanded='true']`); + + // Select dropdown item + let statusSelector: string; + + switch (status) { + case 'all-Modules': + statusSelector = this.filterByAllModulesButton; + break; + + case 'enabled': + statusSelector = this.statusDropdownItemLink(1); + break; + + case 'disabled': + statusSelector = this.statusDropdownItemLink(0); + break; + + case 'uninstalled': + statusSelector = this.statusDropdownItemLink(2); + break; + + case 'installed': + statusSelector = this.statusDropdownItemLink(3); + break; + + default: + throw new Error(`Status ${status} was not exist!`); + } + + await page.locator(statusSelector).click(); + await this.waitForVisibleSelector(page, `${this.statusDropdownDiv}[aria-expanded='false']`); + } + + /** + * Get number of blocks + * @param page {Page} Browser tab + * @return {Promise} + */ + async getNumberOfBlocks(page: Page): Promise { + return page.locator(this.moduleBlocks).count(); + } + + /** + * Get status of module (enable/disable/installed/uninstalled) + * @param page {Page} Browser tab + * @param moduleTag {string} Technical name of the module + * @param action {string} Status of the module to get + * @return {Promise} + */ + async isModuleStatus(page: Page, moduleTag: string, action: string): Promise { + return this.elementNotVisible(page, this.actionModuleButton(moduleTag, action), 1000); + } + + /** + * Get all modules status + * @param page {Page} Browser tab + * @param statusToFilterBy {string} Status to filter by + * @returns {Promise>} + */ + async getAllModulesStatus(page: Page, statusToFilterBy: string): Promise<{ name: string, status: boolean }[]> { + const modulesStatus: { name: string, status: boolean }[] = []; + const allModulesTechNames = await this.getAllModulesTechNames(page); + + for (let i = 0; i < allModulesTechNames.length; i++) { + const moduleTag: string | null = allModulesTechNames[i]; + + if (typeof moduleTag === 'string') { + const moduleStatus = await this.isModuleStatus(page, moduleTag, statusToFilterBy); + modulesStatus.push({name: moduleTag, status: moduleStatus}); + } + } + + return modulesStatus; + } + + /** + * Get All modules names + * @param page {Page} Browser tab + * @return {Promise>} + */ + async getAllModulesTechNames(page: Page): Promise<(string | null)[]> { + return page + .locator(this.allModulesBlock) + .evaluateAll( + (all) => all.map((el) => el.getAttribute('data-tech-name')), + ); + } + + /** + * Uninstall/install/enable/disable/reset module + * @param page {Page} Browser tab + * @param module {ModuleData} Module data to install/uninstall + * @param action {string} Action install/uninstall/enable/disable/reset + * @param cancel {boolean} Cancel the action + * @param forceDeletion {boolean} Delete module folder after uninstall + * @return {Promise} + */ + async setActionInModule( + page: Page, + module: ModuleData, + action: string, + cancel: boolean = false, + forceDeletion: boolean = false, + ): Promise { + await this.closeGrowlMessage(page); + + if (await this.elementVisible(page, this.actionModuleButton(module.tag, action), 1000)) { + await this.waitForSelectorAndClick(page, this.actionModuleButton(module.tag, action)); + if (action === 'disable' || action === 'uninstall' || action === 'reset') { + await this.waitForSelectorAndClick(page, this.modalConfirmButton(module.tag, action)); + } + } else { + await page.locator(this.actionsDropdownButton(module.tag)).click(); + await this.waitForVisibleSelector(page, `${this.actionsDropdownButton(module.tag)}[aria-expanded='true']`); + await this.waitForSelectorAndClick(page, this.actionModuleButtonInDropdownList(action)); + + if (cancel) { + await this.waitForSelectorAndClick(page, this.modalConfirmCancel(module.tag, action)); + await this.elementNotVisible(page, this.modalConfirmAction(module.tag, action), 10000); + return ''; + } + if (action === 'uninstall' && forceDeletion) { + await page.locator(this.modalConfirmUninstallForceDeletion(module.tag)).click(); + } + if (action === 'disable' || action === 'uninstall' || action === 'reset') { + await this.waitForSelectorAndClick(page, this.modalConfirmButton(module.tag, action)); + } + } + + return this.getGrowlMessageContent(page); + } + + /** + Returns the main action module action + * @param page {Page} Browser tab + * @param module {ModuleData} Module data + */ + async getMainActionInModule(page: Page, module: ModuleData): Promise { + const actions: string[] = [ + 'enable', + 'disable', + 'install', + 'configure', + ]; + + for (let i: number = 0; i < actions.length; i++) { + const action = actions[i]; + + if (await this.elementVisible(page, this.actionModuleButton(module.tag, action), 1000)) { + return action; + } + } + + return ''; + } + + /** + * Returns if the action module modal is visible + * @param page {Page} Browser tab + * @param module {ModuleData} Module data to install/uninstall + * @param action {string} Action install/uninstall/enable/disable/reset + * @return {Promise} + */ + async isModalActionVisible(page: Page, module: ModuleData, action: string): Promise { + return this.elementVisible(page, this.modalConfirmAction(module.tag, action)); + } + + /** + * Filter by category + * @param page {Page} Browser tab + * @param category {string} Name of module's category to filter with + * @return {Promise} + */ + async filterByCategory(page: Page, category: string): Promise { + await Promise.all([ + page.locator(this.categoriesSelectDiv).click(), + this.waitForVisibleSelector(page, `${this.categoriesSelectDiv}[aria-expanded='true']`), + ]); + await Promise.all([ + page.locator(this.categoryDropdownItem(category)).click(), + this.waitForVisibleSelector(page, `${this.categoriesSelectDiv}[aria-expanded='false']`), + ]); + } + + /** + * Get modules block title (administration / payment ...) + * @param page {Page} Browser tab + * @param position {number} Position of the module on the list + * @return {Promise} + */ + async getBlockModuleTitle(page: Page, position: number): Promise { + const modulesBlocks = await page.locator(this.modulesListBlockTitle).allTextContents(); + + return modulesBlocks[position - 1]; + } + + /** + * Click on see more button + * @param page {Page} Browser tab + * @param blockName {string} The block name + * @return {Promise} + */ + async clickOnSeeMoreButton(page: Page, blockName: string): Promise { + await this.waitForSelectorAndClick(page, this.seeMoreButton(blockName)); + + return this.elementVisible(page, this.seeLessButton(blockName), 1000); + } + + /** + * Click on see less button + * @param page {Page} Browser tab + * @param blockName {string} The block name + * @return {Promise} + */ + async clickOnSeeLessButton(page: Page, blockName: string): Promise { + await this.waitForSelectorAndClick(page, this.seeLessButton(blockName)); + + return this.elementVisible(page, this.seeMoreButton(blockName), 1000); + } + + /** + * Get number of modules in block + * @param page {Page} Browser tab + * @param blockName {string} The block name + * @return {Promise} + */ + async getNumberOfModulesInBlock(page: Page, blockName: string): Promise { + return page.locator(this.moduleListBlock(blockName)).count(); + } +} + +module.exports.ModuleManager = ModuleManager; +module.exports.moduleManager = new ModuleManager(); diff --git a/src/versions/8.0.0/pages/FO/category/index.ts b/src/versions/8.0.0/pages/FO/category/index.ts new file mode 100644 index 00000000..366e385b --- /dev/null +++ b/src/versions/8.0.0/pages/FO/category/index.ts @@ -0,0 +1,651 @@ +// Import pages +import type { FoCategoryPageInterface } from '@interfaces/FO/category'; +import FOBasePage from '@pages/FO/FOBasePage'; + +import type {Page} from 'playwright'; + +/** + * Category page, contains functions that can be used on the page + * @class + * @extends FOBasePage + */ +class Category extends FOBasePage implements FoCategoryPageInterface { + public readonly messageAddedToWishlist: string; + + private readonly bodySelector: string; + + private readonly mainSection: string; + + private readonly headerNamePage: string; + + private readonly productsSection: string; + + private readonly productListTop: string; + + private readonly productListDiv: string; + + private readonly pagesList: string; + + private readonly productItemListDiv: string; + + private readonly paginationText: string; + + private readonly paginationNext: string; + + private readonly paginationPrevious: string; + + private readonly sortByDiv: string; + + private readonly sortByButton: string; + + private readonly valueToSortBy: (sortBy: string) => string; + + private readonly sideBlockCategories: string; + + private readonly sideBlockCategoriesItem: string; + + private readonly sideBlockCategory: (text: string) => string; + + private readonly subCategoriesList: string; + + private readonly subCategoriesItem: (title: string) => string; + + private readonly productList: string; + + private readonly productArticle: (number: number) => string; + + private readonly productTitle: (number: number) => string; + + private readonly productPrice: (number: number) => string; + + private readonly productAttribute: (number: number, attribute: string) => string; + + private readonly productImg: (number: number) => string; + + private readonly productDescriptionDiv: (number: number) => string; + + private readonly productQuickViewLink: (number: number) => string; + + private readonly productAddToWishlist: (number: number) => string; + + private readonly quickViewModalDiv: string; + + private readonly quickViewModalProductImageCover: string; + + private readonly categoryDescription: string; + + private readonly searchFilters: string; + + private readonly searchFilter: (facetType: string) => string; + + private readonly searchFiltersCheckbox: (facetType: string) => string; + + private readonly searchFiltersRadio: (facetType: string) => string; + + private readonly searchFiltersDropdown: (facetType: string) => string; + + private readonly closeOneFilter: (row: number) => string; + + private readonly searchFiltersSlider: string; + + private readonly searchFilterPriceValues: string; + + private readonly clearAllFiltersLink: string; + + private readonly activeSearchFilters: string; + + private readonly wishlistModal: string; + + private readonly wishlistModalListItem: string; + + private readonly wishlistToast: string; + + /** + * @constructs + * Setting up texts and selectors to use on category page + */ + constructor() { + super(); + + // Message + this.messageAddedToWishlist = 'Product added'; + + // Selectors + this.bodySelector = '#category'; + this.mainSection = '#main'; + this.headerNamePage = '#js-product-list-header'; + this.productsSection = '#products'; + this.productListTop = '#js-product-list-top'; + this.productListDiv = '#js-product-list'; + this.productItemListDiv = `${this.productListDiv} .products div.product`; + this.sortByDiv = `${this.productsSection} div.sort-by-row`; + this.sortByButton = `${this.sortByDiv} button.select-title`; + this.valueToSortBy = (sortBy: string) => `${this.productListTop} .products-sort-order .dropdown-menu a[href*='${sortBy}']`; + + // Categories SideBlock + this.sideBlockCategories = '.block-categories'; + this.sideBlockCategoriesItem = `${this.sideBlockCategories} ul.category-sub-menu li`; + this.sideBlockCategory = (text: string) => `${this.sideBlockCategoriesItem} a:text("${text}")`; + + // SubCategories List + this.subCategoriesList = '#subcategories ul.subcategories-list'; + this.subCategoriesItem = (title: string) => `${this.subCategoriesList} li a[title="${title}"]`; + + // Products list + this.productList = '#js-product-list'; + this.productArticle = (number: number) => `${this.productList} .products div:nth-child(${number}) article`; + + this.productTitle = (number: number) => `${this.productArticle(number)} .product-title`; + this.productPrice = (number: number) => `${this.productArticle(number)} span.price`; + this.productAttribute = (number: number, attribute: string) => `${this.productArticle(number)} .product-${attribute}`; + this.productImg = (number: number) => `${this.productArticle(number)} img`; + this.productDescriptionDiv = (number: number) => `${this.productArticle(number)} div.product-description`; + this.productQuickViewLink = (number: number) => `${this.productArticle(number)} a.quick-view`; + this.productAddToWishlist = (number: number) => `${this.productArticle(number)} button.wishlist-button-add`; + + // Pagination selectors + this.pagesList = '.page-list'; + this.paginationText = `${this.productListDiv} .pagination div:nth-child(1)`; + this.paginationNext = '#js-product-list nav.pagination a[rel=\'next\']'; + this.paginationPrevious = '#js-product-list nav.pagination a[rel=\'prev\']'; + + // Quick View modal + this.quickViewModalDiv = 'div[id*=\'quickview-modal\']'; + this.quickViewModalProductImageCover = `${this.quickViewModalDiv} div.product-cover picture`; + this.categoryDescription = '#category-description'; + + // Filter + this.searchFilters = '#search_filters'; + this.searchFilter = (facetType: string) => `${this.searchFilters} section[data-type="${facetType}"] ul[id^="facet"]`; + this.searchFiltersCheckbox = (facetType: string) => `${this.searchFilter(facetType)} label.facet-label ` + + 'input[type="checkbox"]'; + this.searchFiltersRadio = (facetType: string) => `${this.searchFilter(facetType)} label.facet-label input[type="radio"]`; + this.searchFiltersDropdown = (facetType: string) => `${this.searchFilter(facetType)} .facet-dropdown`; + this.searchFiltersSlider = '.ui-slider-horizontal'; + this.searchFilterPriceValues = '[id*=facet_label]'; + this.clearAllFiltersLink = '#_desktop_search_filters_clear_all button.js-search-filters-clear-all'; + this.activeSearchFilters = '#js-active-search-filters'; + this.closeOneFilter = (row: number) => `#js-active-search-filters ul li:nth-child(${row}) a i`; + + // Wishlist + this.wishlistModal = '.wishlist-add-to .wishlist-modal.show'; + this.wishlistModalListItem = `${this.wishlistModal} ul.wishlist-list li.wishlist-list-item:nth-child(1)`; + this.wishlistToast = '.wishlist-toast .wishlist-toast-text'; + } + + /* Methods */ + /** + * Check if user is in category page + * @param page {Page} Browser tab + * @return {Promise} + */ + async isCategoryPage(page: Page): Promise { + return this.elementVisible(page, this.bodySelector, 2000); + } + + /** + * Get number of products displayed in category page + * @param page {Page} Browser tab + * @return {Promise} + */ + async getNumberOfProductsDisplayed(page: Page): Promise { + return page.locator(this.productItemListDiv).count(); + } + + /** + * Get number of all products + * @param page {Page} + * @returns {Promise} + */ + async getNumberOfProducts(page: Page): Promise { + return this.getNumberFromText(page, this.productListTop); + } + + /** + * Get the header name of the page + * @param page {Page} + * @returns {Promise} + */ + async getHeaderPageName(page: Page): Promise { + return page.locator(this.headerNamePage).innerText().valueOf(); + } + + /** + * Get sort by value from button + * @param page {Page} Browser tab + * @return {Promise} + */ + async getSortByValue(page: Page): Promise { + return this.getTextContent(page, this.sortByButton); + } + + /** + * Is Sort By Button Visible + * @param page {Page} Browser tab + * @return {Promise} + */ + async isSortButtonVisible(page: Page): Promise { + return this.elementVisible(page, this.sortByButton, 1000); + } + + /** + * Sort products list + * @param page {Page} Browser tab + * @param sortBy {string} Value to sort by + * @return {Promise} + */ + async sortProductsList(page: Page, sortBy: string): Promise { + await this.scrollTo(page, this.sortByButton); + await this.waitForSelectorAndClick(page, this.sortByButton); + await this.waitForVisibleSelector(page, `${this.sortByButton}[aria-expanded="true"]`); + await this.waitForSelectorAndClick(page, this.valueToSortBy(sortBy)); + await page.waitForTimeout(3000); + } + + /** + * Get all products attribute + * @param page {Page} Browser tab + * @param attribute {string} Attribute to get + * @returns {Promise} + */ + async getAllProductsAttribute(page: Page, attribute: string): Promise { + let rowContent: string; + const rowsNumber: number = await this.getNumberOfProducts(page); + const allRowsContentTable: string[] = []; + + for (let i = 1; i <= rowsNumber; i++) { + if (attribute === 'price-and-shipping') { + rowContent = await this.getTextContent(page, `${this.productAttribute(i, attribute)} span.price`); + } else { + rowContent = await this.getTextContent(page, this.productAttribute(i, attribute)); + } + allRowsContentTable.push(rowContent); + } + + return allRowsContentTable; + } + + /** + * Is pages list visible + * @param page {Page} Browser tab + * @return {Promise} + */ + async isPagesListVisible(page: Page): Promise { + return this.elementVisible(page, this.pagesList); + } + + /** + * Get pages list + * @param page {Page} Browser tab + * @return {Promise} + */ + async getPagesList(page: Page): Promise { + return this.getTextContent(page, this.pagesList); + } + + /** + * Get showing Items + * @param page {Page} Browser tab + * @return {Promise} + */ + async getShowingItems(page: Page): Promise { + return this.getTextContent(page, this.paginationText, true); + } + + /** + * Go to the next page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToNextPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.paginationNext); + } + + /** + * Go to previous page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToPreviousPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.paginationPrevious); + } + + /** + * Go to product page + * @param page {Page} Browser tab + * @param id {number} Index of product in list of products + * @returns {Promise} + */ + async goToProductPage(page: Page, id: number): Promise { + await this.clickAndWaitForURL(page, this.productAttribute(id, 'thumbnail')); + } + + // Quick view methods + /** + * Click on Quick view Product + * @param page {Page} Browser tab + * @param id {number} Index of product in list of products + * @return {Promise} + */ + async quickViewProduct(page: Page, id: number): Promise { + await page.locator(this.productImg(id)).hover(); + let displayed: boolean = false; + + /* eslint-disable no-await-in-loop */ + // Only way to detect if element is displayed is to get value of computed style 'product description' after hover + // and compare it with value 'block' + for (let i = 0; i < 10 && !displayed; i++) { + /* eslint-env browser */ + displayed = await page.evaluate( + (selector) => { + const element: HTMLElement | null = document.querySelector(selector); + + if (element === null) { + return false; + } + return window.getComputedStyle(element, ':after').getPropertyValue('display') === 'block'; + }, + this.productDescriptionDiv(id), + ); + await page.waitForTimeout(100); + } + /* eslint-enable no-await-in-loop */ + await Promise.all([ + this.waitForVisibleSelector(page, this.quickViewModalDiv), + page.locator(this.productQuickViewLink(id)).evaluate((el: HTMLElement) => el.click()), + ]); + } + + /** + * Is quick view product modal visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isQuickViewProductModalVisible(page: Page): Promise { + return this.elementVisible(page, this.quickViewModalDiv, 2000); + } + + /** + * Returns the URL of the main image in the quickview + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getQuickViewImageMain(page: Page): Promise { + return this.getAttributeContent(page, `${this.quickViewModalProductImageCover} source`, 'srcset'); + } + + /** + * Get category description + * @param page {Page} Browser tab + * @return {Promise} + */ + async getCategoryDescription(page: Page): Promise { + return this.getTextContent(page, this.categoryDescription, true); + } + + /** + * Returns the URL of the main image of a subcategory + * @param page {Page} Browser tab + * @param name {string} Name of a category + * @returns {Promise} + */ + async getCategoryImageMain(page: Page, name: string): Promise { + return this.getAttributeContent(page, `${this.subCategoriesItem(name)} source`, 'srcset'); + } + + /** + * Returns the position of a specific product in a list + * @param page {Page} Browser tab + * @param idProduct {number} ID of a product + * @return {Promise} + */ + async getNThChildFromIDProduct(page: Page, idProduct: number): Promise { + const productItemsLength = await this.getNumberOfProductsDisplayed(page); + + for (let idx: number = 1; idx <= productItemsLength; idx++) { + const attributeIdProduct = await this.getAttributeContent(page, this.productArticle(idx), 'data-id-product'); + + if (attributeIdProduct) { + if (idProduct === parseInt(attributeIdProduct, 10)) { + return idx; + } + } + } + + return null; + } + + //////////////////////////// + // Side Block : Categories + //////////////////////////// + /** + * Return if Side Block : Categories is visible + * @param page {Page} Browser tab + * @return {Promise} + */ + async hasBlockCategories(page: Page): Promise { + return this.elementVisible(page, this.sideBlockCategories, 1000); + } + + /** + * Return if the number of categories in side block + * @param page {Page} Browser tab + * @return {Promise} + */ + async getNumBlockCategories(page: Page): Promise { + return page.locator(this.sideBlockCategoriesItem).count(); + } + + /** + * Click on the category in side block + * @param page {Page} Browser tab + * @param categoryName {string} + * @return {Promise} + */ + async clickBlockCategory(page: Page, categoryName: string): Promise { + await this.clickAndWaitForURL(page, this.sideBlockCategory(categoryName)); + } + + ///////////////////////// + // Side Block : Filters + ///////////////////////// + /** + * Return if search filters are visible + * @param page {Page} Browser tab + * @return {Promise} + */ + async hasSearchFilters(page: Page): Promise { + return (await page.locator(this.searchFilters).count()) !== 0; + } + + /** + * Return if search filters use checkbox button + * @param page {Page} Browser tab + * @param facetType {string} Facet type + * @return {Promise} + */ + async isSearchFiltersCheckbox(page: Page, facetType: string): Promise { + return (await page.locator(this.searchFiltersCheckbox(facetType)).count()) !== 0; + } + + /** + * Filter by checkbox + * @param page {Page} Browser tab + * @param facetType {string} Type of filter + * @param checkboxName {string} Checkbox name + * @param toEnable {boolean} True if we need to enable + * @return {Promise} + */ + async filterByCheckbox(page: Page, facetType: string, checkboxName: string, toEnable: boolean): Promise { + await this.setChecked( + page, + `${this.searchFiltersCheckbox(facetType)}[data-search-url*=${checkboxName}]`, + toEnable, + true, + ); + await page.waitForTimeout(2000); + } + + /** + * Get active filters + * @param page {Page} Browser tab + * @return {Promise} + */ + async getActiveFilters(page: Page): Promise { + return this.getTextContent(page, this.activeSearchFilters); + } + + /** + * Get product href + * @param page {Page} Browser tab + * @param productRow {number} Product row + * @return {Promise} + */ + async getProductHref(page: Page, productRow: number): Promise { + return this.getAttributeContent(page, `${this.productArticle(productRow)} div.thumbnail-top a`, 'href'); + } + + /** + * Get product price + * @param page {Page} Browser tab + * @param productRow {number} Product row + * @return {Promise} + */ + async getProductPrice(page: Page, productRow: number): Promise { + return this.getNumberFromText(page, this.productPrice(productRow)); + } + + /** + * Clear all filters + * @param page {Page} Browser tab + * @return {Promise} + */ + async clearAllFilters(page: Page): Promise { + await page.locator(this.clearAllFiltersLink).click(); + + return this.elementNotVisible(page, this.activeSearchFilters, 2000); + } + + /** + * Close filter + * @param page {Page} Browser tab + * @param row {number} Row of the filter + * @return {Promise} + */ + async closeFilter(page: Page, row: number): Promise { + await page.locator(this.closeOneFilter(row)).click(); + } + + /** + * Is active filter not visible + * @param page {Page} Browser tab + * @return {Promise} + */ + async isActiveFilterNotVisible(page: Page): Promise { + return this.elementNotVisible(page, this.activeSearchFilters, 2000); + } + + /** + * Get maximum price from slider + * @param page {Page} Browser tab + * @return {Promise} + */ + async getMaximumPrice(page: Page): Promise { + const test = await this.getTextContent(page, this.searchFilterPriceValues); + + return (parseInt(test.split('€')[2], 10)); + } + + /** + * Get minimum price from slider + * @param page {Page} Browser tab + * @return {Promise} + */ + async getMinimumPrice(page: Page): Promise { + const test = await this.getTextContent(page, this.searchFilterPriceValues); + + return (parseInt(test.split('€')[1], 10)); + } + + /** + * Filter by price + * @param page {Page} Browser tab + * @param minPrice {number} Minimum price in the slider + * @param maxPrice {number} Maximum price in the slider + * @param filterFrom {number} The minimum value to filter + * @param filterTo {number} The maximum value to filter + * @return {Promise} + */ + async filterByPrice(page: Page, minPrice: number, maxPrice: number, filterFrom: number, filterTo: number): Promise { + const sliderTrack = page.locator(this.searchFiltersSlider); + const sliderOffsetWidth = await sliderTrack.evaluate((el) => el.getBoundingClientRect().width); + const pxOneEuro = sliderOffsetWidth / (maxPrice - minPrice); + + await sliderTrack.hover({force: true, position: {x: ((filterFrom - minPrice) * pxOneEuro), y: 0}}); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForTimeout(2000); + await sliderTrack.hover({force: true, position: {x: (filterTo - minPrice) * pxOneEuro, y: 0}}); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForTimeout(2000); + } + + /** + * Return if search filters use radio button + * @param page {Page} Browser tab + * @param facetType {string} Facet type + * @return {Promise} + */ + async isSearchFilterRadio(page: Page, facetType: string): Promise { + return (await page.locator(this.searchFiltersRadio(facetType)).count()) !== 0; + } + + /** + * Return if search filters use radio button + * @param page {Page} Browser tab + * @param facetType {string} Facet type + * @return {Promise} + */ + async isSearchFilterDropdown(page: Page, facetType: string): Promise { + return (await page.locator(this.searchFiltersDropdown(facetType)).count()) !== 0; + } + + /** + * Add a product (based on its index) to the first wishlist + * @param page {Page} + * @param idxProduct {number} + * @returns Promise + */ + async addToWishList(page: Page, idxProduct: number): Promise { + if (!(await this.isAddedToWishlist(page, idxProduct))) { + // Click on the heart + await page.locator(this.productAddToWishlist(idxProduct)).click(); + // Wait for the modal + await this.elementVisible(page, this.wishlistModal, 3000); + // Click on the first wishlist + await page.locator(this.wishlistModalListItem).click(); + // Wait for the toast + await this.elementVisible(page, this.wishlistToast, 3000); + + return this.getTextContent(page, this.wishlistToast); + } + + // Already added + return this.messageAddedToWishlist; + } + + /** + * Check if a product (based on its index) is added to a wishlist + * @param page {Page} + * @param idxProduct {number} + * @returns Promise + */ + async isAddedToWishlist(page: Page, idxProduct: number): Promise { + await page.waitForTimeout(1000); + + return ((await this.getTextContent(page, this.productAddToWishlist(idxProduct))) === 'favorite'); + } +} + +module.exports = new Category(); diff --git a/src/versions/8.0.0/pages/FO/home/index.ts b/src/versions/8.0.0/pages/FO/home/index.ts new file mode 100644 index 00000000..b0c657c4 --- /dev/null +++ b/src/versions/8.0.0/pages/FO/home/index.ts @@ -0,0 +1,881 @@ +// Import FO Pages +import FOBasePage from '@pages/FO/FOBasePage'; + +// Import data +import { CartProductDetails } from '@data/types/cart'; +import {ProductAttribute} from '@data/types/product'; + +import type {Page} from 'playwright'; +import { FoHomePageInterface } from '@interfaces/FO/home'; + +/** + * Home page, contains functions that can be used on the page + * @class + * @extends FOBasePage + */ +class HomePage extends FOBasePage implements FoHomePageInterface { + public readonly pageTitle: string; + + public readonly successAddToCartMessage: string; + + private readonly carouselSliderId: string; + + private readonly carouselControlDirectionLink: (direction: string) => string; + + private readonly carouselSliderInnerList: string; + + private readonly carouselSliderInnerListItems: string; + + private readonly carouselSliderURL: string; + + private readonly carouselSliderInnerListItem: (position: number) => string; + + private readonly homePageSection: string; + + private productsBlock: (blockId: number) => string; + + private readonly productsBlockTitle: (blockId: number) => string; + + private readonly productsBlockDiv: (blockId: number) => string; + + public productArticle: (number: number) => string; + + private readonly productImg: (number: number) => string; + + private readonly productDescriptionDiv: (number: number) => string; + + private readonly productQuickViewLink: (number: number) => string; + + private readonly productColorLink: (number: number, color: string) => string; + + private readonly allProductsBlockLink: (blockId: number) => string; + + private readonly totalProducts: string; + + private readonly productPrice: (number: number) => string; + + private readonly newFlag: (number: number) => string; + + private readonly bannerImg: string; + + private readonly customTextBlock: string; + + private readonly newsletterFormField: string; + + private readonly newsletterSubmitButton: string; + + private readonly subscriptionAlertMessage: string; + + private readonly quickViewModalDiv: string; + + private readonly quickViewCloseButton: string; + + private readonly quickViewProductName: string; + + private readonly quickViewRegularPrice: string; + + private readonly quickViewProductPrice: string; + + private readonly quickViewDiscountPercentage: string; + + private readonly quickViewTaxShippingDeliveryLabel: string; + + private readonly quickViewShortDescription: string; + + private readonly quickViewProductVariants: string; + + private readonly quickViewProductSize: string; + + private readonly quickViewProductColor: string; + + private readonly quickViewProductDimension: string; + + private readonly productAvailability: string; + + private readonly quickViewCoverImage: string; + + private readonly quickViewThumbImage: string; + + private readonly quickViewQuantityWantedInput: string; + + private readonly quickViewFacebookSocialSharing: string; + + private readonly quickViewTwitterSocialSharing: string; + + private readonly quickViewPinterestSocialSharing: string; + + private readonly addToCartButton: string; + + private readonly blockCartLabel: string; + + private readonly blockCartModalDiv: string; + + private readonly blockCartModalCloseButton: string; + + private readonly cartModalProductNameBlock: string; + + private readonly cartModalProductPriceBlock: string; + + private readonly cartModalProductSizeBlock: string; + + private readonly cartModalProductColorBlock: string; + + private readonly cartModalProductQuantityBlock: string; + + private readonly cartContentBlock: string; + + private readonly cartModalProductsCountBlock: string; + + private readonly cartModalShippingBlock: string; + + private readonly cartModalSubtotalBlock: string; + + private readonly cartModalProductTaxInclBlock: string; + + private readonly cartModalCheckoutLink: string; + + private readonly continueShoppingButton: string; + + public readonly successSubscriptionMessage: string; + + public readonly successSendVerificationEmailMessage: string; + + public readonly successSendConfirmationEmailMessage: string; + + public readonly alreadyUsedEmailMessage: string; + + public readonly productHummingbird: (number: number) => string; + + public readonly productImgHummingbird: (number: number) => string; + + public readonly quickViewButtonHummingbird: (number: number) => string; + + public readonly blockCartModalCloseButtonHummingbird: string; + + /** + * @constructs + * Setting up texts and selectors to use on home page + */ + constructor(theme: string = 'classic') { + super(theme); + + this.pageTitle = global.INSTALL.SHOP_NAME; + this.successAddToCartMessage = 'Product successfully added to your shopping cart'; + + // Selectors of slider + this.carouselSliderId = '#carousel'; + this.carouselControlDirectionLink = (direction: string) => `${this.carouselSliderId} a.${direction}.carousel-control`; + this.carouselSliderInnerList = `${this.carouselSliderId} ul.carousel-inner`; + this.carouselSliderInnerListItems = `${this.carouselSliderInnerList} li`; + this.carouselSliderURL = `${this.carouselSliderInnerListItems} a`; + this.carouselSliderInnerListItem = (position: number) => `${this.carouselSliderInnerListItems}:nth-child(${position})`; + + // Selectors for home page + this.homePageSection = 'section#content.page-home'; + this.productsBlock = (blockId: number) => `#content section:nth-child(${blockId})`; + this.productsBlockTitle = (blockId: number) => `${this.productsBlock(blockId)} h2`; + this.productsBlockDiv = (blockId: number) => `${this.productsBlock(blockId)} div.products div.js-product`; + this.productArticle = (number: number) => `${this.productsBlock(2)} .products div:nth-child(${number}) article`; + this.productImg = (number: number) => `${this.productArticle(number)} img`; + this.productDescriptionDiv = (number: number) => `${this.productArticle(number)} div.product-description`; + this.productQuickViewLink = (number: number) => `${this.productArticle(number)} a.quick-view`; + this.productColorLink = (number: number, color: string) => `${this.productArticle(number)} .variant-links` + + ` a[aria-label='${color}']`; + this.allProductsBlockLink = (blockId: number) => `#content section:nth-child(${blockId}) a.all-product-link`; + this.totalProducts = '#js-product-list-top .total-products > p'; + this.productPrice = (number: number) => `${this.productArticle(number)} span[aria-label="Price"]`; + this.newFlag = (number: number) => `${this.productArticle(number)} .product-flag.new`; + this.bannerImg = '.banner img'; + this.customTextBlock = '#custom-text'; + this.newsletterFormField = '.block_newsletter [name=email]'; + this.newsletterSubmitButton = '.block_newsletter [name="submitNewsletter"][value="Subscribe"]'; + + // Newsletter Subscription alert message + this.subscriptionAlertMessage = '.block_newsletter_alert'; + + // Quick View modal + this.quickViewModalDiv = 'div[id*=\'quickview-modal\']'; + this.quickViewCloseButton = `${this.quickViewModalDiv} button.close`; + this.quickViewProductName = `${this.quickViewModalDiv} h1`; + this.quickViewRegularPrice = `${this.quickViewModalDiv} span.regular-price`; + this.quickViewProductPrice = `${this.quickViewModalDiv} div.current-price span.current-price-value`; + this.quickViewDiscountPercentage = `${this.quickViewModalDiv} div.current-price span.discount-percentage`; + this.quickViewTaxShippingDeliveryLabel = `${this.quickViewModalDiv} div.tax-shipping-delivery-label`; + this.quickViewShortDescription = `${this.quickViewModalDiv} div#product-description-short`; + this.quickViewProductVariants = `${this.quickViewModalDiv} div.product-variants`; + this.quickViewProductSize = `${this.quickViewProductVariants} select#group_1`; + this.quickViewProductColor = `${this.quickViewProductVariants} ul#group_2`; + this.quickViewProductDimension = `${this.quickViewProductVariants} select#group_3`; + this.productAvailability = '#product-availability'; + this.quickViewCoverImage = `${this.quickViewModalDiv} img.js-qv-product-cover`; + this.quickViewThumbImage = `${this.quickViewModalDiv} img.js-thumb.selected`; + this.quickViewQuantityWantedInput = `${this.quickViewModalDiv} input#quantity_wanted`; + this.quickViewFacebookSocialSharing = `${this.quickViewModalDiv} .facebook a`; + this.quickViewTwitterSocialSharing = `${this.quickViewModalDiv} .twitter a`; + this.quickViewPinterestSocialSharing = `${this.quickViewModalDiv} .pinterest a`; + this.addToCartButton = `${this.quickViewModalDiv} button[data-button-action='add-to-cart']`; + + // Block Cart Modal + this.blockCartModalDiv = '#blockcart-modal'; + this.blockCartLabel = '#myModalLabel'; + this.blockCartModalCloseButton = `${this.blockCartModalDiv} button.close`; + this.cartModalProductNameBlock = `${this.blockCartModalDiv} .product-name`; + this.cartModalProductPriceBlock = `${this.blockCartModalDiv} .product-price`; + this.cartModalProductSizeBlock = `${this.blockCartModalDiv} .size strong`; + this.cartModalProductColorBlock = `${this.blockCartModalDiv} .color strong`; + this.cartModalProductQuantityBlock = `${this.blockCartModalDiv} .product-quantity`; + this.cartContentBlock = `${this.blockCartModalDiv} .cart-content`; + this.cartModalProductsCountBlock = `${this.cartContentBlock} .cart-products-count`; + this.cartModalShippingBlock = `${this.cartContentBlock} .shipping.value`; + this.cartModalSubtotalBlock = `${this.cartContentBlock} .subtotal.value`; + this.cartModalProductTaxInclBlock = `${this.cartContentBlock} .product-total .value`; + this.cartModalCheckoutLink = `${this.blockCartModalDiv} div.cart-content-btn a`; + this.continueShoppingButton = `${this.blockCartModalDiv} div.cart-content-btn button.btn-secondary`; + + // Newsletter subscription messages + this.successSubscriptionMessage = 'You have successfully subscribed to this newsletter.'; + this.successSendVerificationEmailMessage = 'A verification email has been sent. Please check your inbox.'; + this.successSendConfirmationEmailMessage = 'A confirmation email has been sent. Please check your inbox.'; + this.alreadyUsedEmailMessage = 'This email address is already registered.'; + + // Hummingbird + this.productHummingbird = (number: number) => `#content .products div:nth-child(${number})`; + this.productImgHummingbird = (number: number) => `${this.productHummingbird(number)} img`; + this.quickViewButtonHummingbird = (number: number) => `${this.productHummingbird(number)} .product-miniature__quickview ` + + 'button'; + this.blockCartModalCloseButtonHummingbird = `${this.blockCartModalDiv} button.btn-close`; + } + + /** + * + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductsNumber(page: Page): Promise { + return this.getNumberFromText(page, this.totalProducts); + } + + /** + * Check home page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isHomePage(page: Page): Promise { + return this.elementVisible(page, this.homePageSection, 3000); + } + + /** + * Click on right/left arrow of the slider + * @param page {Page} Browser tab + * @param direction {string} Direction to click on + * @returns {Promise} + */ + async clickOnLeftOrRightArrow(page: Page, direction: string): Promise { + await page.locator(this.carouselControlDirectionLink(direction)).click(); + } + + /** + * Is slider visible + * @param page {Page} Browser tab + * @param position {number} The slider position + * @returns {Promise} + */ + async isSliderVisible(page: Page, position: number): Promise { + await this.waitForVisibleSelector(page, this.carouselSliderId); + + return this.elementVisible(page, this.carouselSliderInnerListItem(position), 1000); + } + + /** + * Click on slider number + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getSliderURL(page: Page): Promise { + return this.getAttributeContent(page, this.carouselSliderURL, 'href'); + } + + /** + * Go to the product page + * @param page {Page} Browser tab + * @param id {number} Product id + * @returns {Promise} + */ + async goToProductPage(page: Page, id: number): Promise { + await this.clickAndWaitForURL(page, this.productImg(id)); + } + + /** + * Check product price + * @param page {Page} Browser tab + * @param id {number} index of product in list of products + * @return {Promise} + */ + async isPriceVisible(page: Page, id: number = 1): Promise { + return this.elementVisible(page, this.productPrice(id), 1000); + } + + /** + * Check new flag + * @param page {Page} Browser tab + * @param id {number} Index of product in list of products + * @returns {Promise} + */ + async isNewFlagVisible(page: Page, id: number = 1): Promise { + return this.elementVisible(page, this.newFlag(id), 1000); + } + + /** + * Goto home category page by clicking on all products + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToAllProductsPage(page: Page): Promise { + await this.goToAllProductsBlockPage(page, 1); + } + + /** + * Get products block title + * @param page {Page} Browser tab + * @param blockID {number} The block number in the page + * @returns {Promise} + */ + async getBlockTitle(page: Page, blockID: number = 1): Promise { + let columnSelector: string; + + switch (blockID) { + case 1: + columnSelector = this.productsBlockTitle(2); + break; + + case 2: + columnSelector = this.productsBlockTitle(5); + break; + + case 3: + columnSelector = this.productsBlockTitle(6); + break; + + case 4: + columnSelector = this.productsBlockTitle(7); + break; + + default: + throw new Error(`Block ${blockID} was not found`); + } + + return this.getTextContent(page, columnSelector); + } + + /** + * Get products block number + * @param blockID {number} The block number in the page + * @param page {Page} Browser tab + */ + async getProductsBlockNumber(page: Page, blockID: number = 1): Promise { + let columnSelector: string; + + switch (blockID) { + case 1: + columnSelector = this.productsBlockDiv(2); + break; + + case 2: + columnSelector = this.productsBlockDiv(5); + break; + + case 3: + columnSelector = this.productsBlockDiv(6); + break; + + case 4: + columnSelector = this.productsBlockDiv(7); + break; + + default: + throw new Error(`Block ${blockID} was not found`); + } + + return page.locator(columnSelector).count(); + } + + /** + * Go to all products + * @param page {Page} Browser tab + * @param blockID {number} The block number in the page + * @return {Promise} + */ + async goToAllProductsBlockPage(page: Page, blockID: number = 1): Promise { + let columnSelector: string; + + switch (blockID) { + case 1: + columnSelector = this.allProductsBlockLink(2); + break; + + case 2: + columnSelector = this.allProductsBlockLink(5); + break; + + case 3: + columnSelector = this.allProductsBlockLink(6); + break; + + case 4: + columnSelector = this.allProductsBlockLink(7); + break; + + default: + throw new Error(`Block ${blockID} was not found`); + } + + await this.clickAndWaitForURL(page, columnSelector); + } + + /** + * Is banner visible + * @param page {Page} Browser tab + */ + async isBannerVisible(page: Page): Promise { + return this.elementVisible(page, this.bannerImg, 1000); + } + + /** + * Is custom text block visible + * @param page {Page} Browser tab + */ + async isCustomTextBlockVisible(page: Page): Promise { + return this.elementVisible(page, this.customTextBlock, 1000); + } + + // Quick view methods + /** + * Click on Quick view Product + * @param page {Page} Browser tab + * @param id {number} Index of product in list of products + * @return {Promise} + */ + async quickViewProduct(page: Page, id: number): Promise { + if (this.theme === 'hummingbird') { + await page.locator(this.productImgHummingbird(id)).first().hover(); + await this.waitForVisibleSelector(page, this.quickViewButtonHummingbird(id)); + await page.locator(this.quickViewButtonHummingbird(id)).first().click(); + + return; + } + + await page.locator(this.productImg(id)).hover(); + let displayed: boolean = false; + + /* eslint-disable no-await-in-loop */ + // Only way to detect if element is displayed is to get value of computed style 'product description' after hover + // and compare it with value 'block' + for (let i = 0; i < 10 && !displayed; i++) { + /* eslint-env browser */ + displayed = await page.evaluate( + (selector: string): boolean => { + const element = document.querySelector(selector); + + if (!element) { + return false; + } + return window.getComputedStyle(element, ':after').getPropertyValue('display') === 'block'; + }, + this.productDescriptionDiv(id), + ); + await page.waitForTimeout(100); + } + /* eslint-enable no-await-in-loop */ + await Promise.all([ + this.waitForVisibleSelector(page, this.quickViewModalDiv), + page.locator(this.productQuickViewLink(id)).evaluate((el: HTMLElement) => el.click()), + ]); + } + + /** + * Is quick view product modal visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isQuickViewProductModalVisible(page: Page): Promise { + return this.elementVisible(page, this.quickViewModalDiv, 2000); + } + + /** + * Add product to cart with Quick view + * @param page {Page} Browser tab + * @param id {number} Index of product in list of products + * @param quantityWanted {number} Quantity to order + * @return {Promise} + */ + async addProductToCartByQuickView(page: Page, id: number, quantityWanted: number = 1): Promise { + await this.quickViewProduct(page, id); + await this.setValue(page, this.quickViewQuantityWantedInput, quantityWanted); + await Promise.all([ + this.waitForVisibleSelector(page, this.blockCartModalDiv), + page.locator(this.addToCartButton).click(), + ]); + + return this.getTextContent(page, this.blockCartLabel); + } + + /** + * Is add to cart button disabled + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isAddToCartButtonDisabled(page: Page): Promise { + return this.elementVisible(page, `${this.addToCartButton}[disabled]`, 1000); + } + + /** + * Change product attributes + * @param page {Page} Browser tab + * @param attributes {ProductAttribute} The attributes data (size, color, dimension) + * @returns {Promise} + */ + async changeAttributes(page: Page, attributes: ProductAttribute): Promise { + switch (attributes.name) { + case 'color': + await this.waitForSelectorAndClick(page, `${this.quickViewProductColor} input[title='${attributes.value}']`); + await this.waitForVisibleSelector( + page, + `${this.quickViewProductColor} input[title='${attributes.value}'][checked]`, + ); + break; + case 'dimension': + await Promise.all([ + page.waitForResponse((response) => response.url().includes('product&token=')), + this.selectByVisibleText(page, this.quickViewProductDimension, attributes.value), + ]); + break; + case 'size': + await this.selectByVisibleText(page, this.quickViewProductSize, attributes.value); + break; + default: + throw new Error(`${attributes.name} has not being in defined in "changeAttributes"`); + } + } + + /** + * Change product quantity + * @param page {Page} Browser tab + * @param quantity {number} The product quantity to change + * @returns {Promise} + */ + async changeQuantity(page: Page, quantity: number): Promise { + await this.setValue(page, this.quickViewQuantityWantedInput, quantity); + } + + /** + * Click on add to cart button from quick view modal + * @param page {Page} Browser tab + * @returns {Promise} + */ + async addToCartByQuickView(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.addToCartButton); + } + + /** + * Change attributes and add to cart + * @param page {Page} Browser tab + * @param attributes {ProductAttribute[]} The attributes data (size, color, quantity) + * @param quantity {number} The attributes data (size, color, quantity) + * @returns {Promise} + */ + async changeAttributesAndAddToCart(page: Page, attributes: ProductAttribute[], quantity: number): Promise { + for (let i: number = 0; i < attributes.length; i++) { + await this.changeAttributes(page, attributes[i]); + } + await this.changeQuantity(page, quantity); + await this.addToCartByQuickView(page); + } + + /** + * Get product with discount details from quick view modal + * @param page {Page} Browser tab + * @returns {Promise<{discountPercentage: string, thumbImage: string|null, price: number, taxShippingDeliveryLabel: string, + * regularPrice: number, coverImage: string|null, name: string, shortDescription: string}>} + */ + async getProductWithDiscountDetailsFromQuickViewModal(page: Page): Promise<{ + discountPercentage: string, + thumbImage: string | null, + price: number, + taxShippingDeliveryLabel: string, + regularPrice: number, + coverImage: string | null, + name: string, + shortDescription: string, + }> { + return { + name: await this.getTextContent(page, this.quickViewProductName), + regularPrice: parseFloat((await this.getTextContent(page, this.quickViewRegularPrice)).replace('€', '')), + price: parseFloat((await this.getTextContent(page, this.quickViewProductPrice)).replace('€', '')), + discountPercentage: await this.getTextContent(page, this.quickViewDiscountPercentage), + taxShippingDeliveryLabel: await this.getTextContent(page, this.quickViewTaxShippingDeliveryLabel), + shortDescription: await this.getTextContent(page, this.quickViewShortDescription), + coverImage: await this.getAttributeContent(page, this.quickViewCoverImage, 'src'), + thumbImage: await this.getAttributeContent(page, this.quickViewThumbImage, 'src'), + }; + } + + /** + * Get product details from quick view modal + * @param page {Page} Browser tab + * @returns {Promise<{thumbImage: string|null, price: number, taxShippingDeliveryLabel: string, + * coverImage: string|null, name: string, shortDescription: string}>} + */ + async getProductDetailsFromQuickViewModal(page: Page): Promise<{ + thumbImage: string | null, + price: number, + taxShippingDeliveryLabel: string, + coverImage: string | null, + name: string, + shortDescription: string, + }> { + return { + name: await this.getTextContent(page, this.quickViewProductName), + price: parseFloat((await this.getTextContent(page, this.quickViewProductPrice)).replace('€', '')), + taxShippingDeliveryLabel: await this.getTextContent(page, this.quickViewTaxShippingDeliveryLabel), + shortDescription: await this.getTextContent(page, this.quickViewShortDescription), + coverImage: await this.getAttributeContent(page, this.quickViewCoverImage, 'src'), + thumbImage: await this.getAttributeContent(page, this.quickViewThumbImage, 'src'), + }; + } + + /** + * Get selected attribute from quick view + * @param page {Page} Browser tab + * @param attribute {ProductAttribute} Attribute to get value + * @returns {Promise} + */ + async getSelectedAttributesFromQuickViewModal( + page: Page, + attribute: ProductAttribute, + ): Promise { + const attributes: ProductAttribute[] = []; + + if ('color' in attribute && 'size' in attribute) { + attributes.push({ + name: 'size', + value: await this.getAttributeContent(page, `${this.quickViewProductSize} option[selected]`, 'title'), + }); + attributes.push({ + name: 'color', + value: await this.getAttributeContent(page, `${this.quickViewProductColor} input[checked='checked']`, 'title'), + }); + } else { + attributes.push({ + name: 'dimension', + value: await this.getAttributeContent(page, `${this.quickViewProductDimension} option[selected]`, 'title'), + }); + } + return attributes; + } + + /** + * Get product attributes from quick view modal + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductAttributesFromQuickViewModal(page: Page): Promise { + return [ + { + name: 'size', + value: await this.getTextContent(page, this.quickViewProductSize), + }, + { + name: 'color', + value: await this.getTextContent(page, this.quickViewProductColor, false), + }, + ]; + } + + /** + * Close quick view modal + * @param page {Page} Browser tab + * @returns {Promise} + */ + async closeQuickViewModal(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.quickViewCloseButton); + + return this.elementNotVisible(page, this.quickViewModalDiv, 1000); + } + + /** + * Close block cart modal + * @param page {Page} Browser tab + * @returns {Promise} + */ + async closeBlockCartModal(page: Page): Promise { + if (this.theme === 'hummingbird') { + await this.waitForSelectorAndClick(page, this.blockCartModalCloseButtonHummingbird); + } else { + await this.waitForSelectorAndClick(page, this.blockCartModalCloseButton); + } + + return this.elementNotVisible(page, this.blockCartModalDiv, 1000); + } + + /** + * Select product color + * @param page {Page} Browser tab + * @param id {number} Id of the current product + * @param color {string} The color to select + * @returns {Promise} + */ + async selectProductColor(page: Page, id: number, color: string): Promise { + await page.locator(this.productImg(id)).hover(); + let displayed = false; + + /* eslint-disable no-await-in-loop */ + // Only way to detect if element is displayed is to get value of computed style 'product description' after hover + // and compare it with value 'block' + for (let i = 0; i < 10 && !displayed; i++) { + /* eslint-env browser */ + displayed = await page.evaluate( + (selector: string): boolean => { + const element = document.querySelector(selector); + + if (!element) { + return false; + } + return window.getComputedStyle(element, ':after').getPropertyValue('display') === 'block'; + }, + this.productDescriptionDiv(id), + ); + await page.waitForTimeout(100); + } + /* eslint-enable no-await-in-loop */ + + await this.clickAndWaitForURL(page, this.productColorLink(id, color)); + } + + /** + * Get product availability text + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductAvailabilityText(page: Page): Promise { + return this.getTextContent(page, this.productAvailability); + } + + /** + * Is add to cart button enabled + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isAddToCartButtonEnabled(page: Page): Promise { + return !await this.elementVisible(page, `${this.addToCartButton}[disabled]`, 1000); + } + + // Block cart modal methods + /** + * Is block cart modal visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + isBlockCartModalVisible(page: Page): Promise { + return this.elementVisible(page, this.blockCartModalDiv, 2000); + } + + /** + * Get product details from blockCart modal + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductDetailsFromBlockCartModal(page: Page): Promise { + return { + name: await this.getTextContent(page, this.cartModalProductNameBlock), + price: parseFloat((await this.getTextContent(page, this.cartModalProductPriceBlock)).replace('€', '')), + quantity: await this.getNumberFromText(page, this.cartModalProductQuantityBlock), + cartProductsCount: await this.getNumberFromText(page, this.cartModalProductsCountBlock), + cartSubtotal: parseFloat((await this.getTextContent(page, this.cartModalSubtotalBlock)).replace('€', '')), + cartShipping: await this.getTextContent(page, this.cartModalShippingBlock), + totalTaxIncl: parseFloat((await this.getTextContent(page, this.cartModalProductTaxInclBlock)).replace('€', '')), + }; + } + + /** + * Get product attributes from block cart modal + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductAttributesFromBlockCartModal(page: Page): Promise { + return [ + { + name: 'size', + value: await this.getTextContent(page, this.cartModalProductSizeBlock), + }, + { + name: 'color', + value: await this.getTextContent(page, this.cartModalProductColorBlock), + }, + ]; + } + + /** + * Click on proceed to checkout after adding product to cart (in modal homePage) + * @param page {Page} Browser tab + * @return {Promise} + */ + async proceedToCheckout(page: Page): Promise { + await this.clickAndWaitForURL(page, this.cartModalCheckoutLink); + await page.waitForLoadState('domcontentloaded'); + } + + /** + * Click on continue shopping + * @param page {Page} Browser tab + * @returns {Promise} + */ + async continueShopping(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.continueShoppingButton); + return this.elementNotVisible(page, this.blockCartModalDiv, 2000); + } + + /** + * Go to social sharing link + * @param page {Page} Browser tab + * @param socialSharing {string} The social network name + * @returns {Promise} + */ + async getSocialSharingLink(page: Page, socialSharing: string): Promise { + let selector; + + switch (socialSharing) { + case 'Facebook': + selector = this.quickViewFacebookSocialSharing; + break; + + case 'Twitter': + selector = this.quickViewTwitterSocialSharing; + break; + + case 'Pinterest': + selector = this.quickViewPinterestSocialSharing; + break; + + default: + throw new Error(`${socialSharing} was not found`); + } + + return this.getAttributeContent(page, selector, 'href'); + } + + /** + * Subscribe to the newsletter from the FO homepage + * @param page {Page} Browser tab + * @param email {string} Email to set on input + * @returns {Promise} + */ + async subscribeToNewsletter(page: Page, email: string): Promise { + await this.setValue(page, this.newsletterFormField, email); + await this.waitForSelectorAndClick(page, this.newsletterSubmitButton); + + return this.getTextContent(page, this.subscriptionAlertMessage); + } +} + +module.exports.HomePage = HomePage; +module.exports.homePage = new HomePage(); \ No newline at end of file diff --git a/src/versions/8.0.0/pages/FO/login/index.ts b/src/versions/8.0.0/pages/FO/login/index.ts new file mode 100644 index 00000000..b44d5075 --- /dev/null +++ b/src/versions/8.0.0/pages/FO/login/index.ts @@ -0,0 +1,128 @@ +import type CustomerData from '@data/faker/customer'; +import { FoLoginPageInterface } from '@interfaces/FO/login'; +import FOBasePage from '@pages/FO/FOBasePage'; + +import type {Page} from 'playwright'; + +/** + * Login page, contains functions that can be used on the page + * @class + * @extends FOBasePage + */ +class LoginPage extends FOBasePage implements FoLoginPageInterface { + public readonly pageTitle: string; + + public readonly loginErrorText: string; + + public readonly disabledAccountErrorText: string; + + private readonly loginForm: string; + + private readonly emailInput: string; + + private readonly passwordInput: string; + + private readonly signInButton: string; + + protected displayRegisterFormLink: string; + + protected passwordReminderLink: string; + + private readonly showPasswordButton: string; + + protected alertDangerTextBlock: string; + + /** + * @constructs + * Setting up texts and selectors to use on login page + */ + constructor(theme: string = 'classic') { + super(theme); + + this.pageTitle = 'Login'; + this.loginErrorText = 'Authentication failed.'; + this.disabledAccountErrorText = 'Your account isn\'t available at this time, please contact us'; + + // Selectors + this.loginForm = '#login-form'; + this.emailInput = `${this.loginForm} input[name='email']`; + this.passwordInput = `${this.loginForm} input[name='password']`; + this.signInButton = `${this.loginForm} button#submit-login`; + this.displayRegisterFormLink = 'div.no-account a[data-link-action=\'display-register-form\']'; + this.passwordReminderLink = '.forgot-password a'; + this.showPasswordButton = '#login-form button[data-action=show-password]'; + this.alertDangerTextBlock = '#content section.login-form div.help-block li.alert-danger'; + } + + /* + Methods + */ + + /** + * Login in FO + * @param page {Page} Browser tab + * @param customer {CustomerData} Customer's information (email and password) + * @param waitForNavigation {boolean} true to wait for navigation after the click on button + * @return {Promise} + */ + async customerLogin(page: Page, customer: CustomerData, waitForNavigation: boolean = true): Promise { + await this.setValue(page, this.emailInput, customer.email); + await this.setValue(page, this.passwordInput, customer.password); + if (waitForNavigation) { + await this.clickAndWaitForLoadState(page, this.signInButton); + await this.elementNotVisible(page, this.signInButton, 2000); + } else { + await page.locator(this.signInButton).click(); + } + } + + /** + * Get login error + * @param page {Page} Browser tab + * @return {Promise} + */ + async getLoginError(page: Page): Promise { + return this.getTextContent(page, this.alertDangerTextBlock); + } + + /** + * Get password type + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getPasswordType(page: Page): Promise { + return this.getAttributeContent(page, this.passwordInput, 'type'); + } + + /** + * Show password + * @param page {Page} Browser tab + * @returns {Promise} + */ + async showPassword(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.showPasswordButton); + + return this.getAttributeContent(page, this.passwordInput, 'type'); + } + + /** + * Go to create account page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToCreateAccountPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.displayRegisterFormLink); + } + + /** + * Go to the password reminder page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToPasswordReminderPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.passwordReminderLink); + } +} + +module.exports.LoginPage = LoginPage; +module.exports.loginPage = new LoginPage();