diff --git a/src/index.ts b/src/index.ts index ecb22dd2..e1a3e48c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,6 +60,17 @@ export {default as boModuleManagerPage} from '@pages/BO/modules/moduleManager'; export {default as boModuleManagerUninstalledModulesPage} from '@pages/BO/modules/moduleManager/uninstalledModules'; export {default as boOrdersPage} from '@pages/BO/orders'; export {default as boProductsPage} from '@pages/BO/catalog/products'; +export {default as boProductsCreatePage} from '@pages/BO/catalog/products/create'; +export {default as boProductsCreateTabCombinationsPage} from '@pages/BO/catalog/products/create/tabCombinations'; +export {default as boProductsCreateTabDescriptionPage} from '@pages/BO/catalog/products/create/tabDescription'; +export {default as boProductsCreateTabDetailsPage} from '@pages/BO/catalog/products/create/tabDetails'; +export {default as boProductsCreateTabOptionsPage} from '@pages/BO/catalog/products/create/tabOptions'; +export {default as boProductsCreateTabPackPage} from '@pages/BO/catalog/products/create/tabPack'; +export {default as boProductsCreateTabPricingPage} from '@pages/BO/catalog/products/create/tabPricing'; +export {default as boProductsCreateTabSeoPage} from '@pages/BO/catalog/products/create/tabSeo'; +export {default as boProductsCreateTabShippingPage} from '@pages/BO/catalog/products/create/tabShipping'; +export {default as boProductsCreateTabStocksPage} from '@pages/BO/catalog/products/create/tabStocks'; +export {default as boProductsCreateTabVirtualProductPage} from '@pages/BO/catalog/products/create/tabVirtualProduct'; // Export Pages FO export * as FOBasePage from '@pages/FO/FOBasePage'; // Export Pages FO/Classic diff --git a/src/interfaces/BO/catalog/products/create/index.ts b/src/interfaces/BO/catalog/products/create/index.ts new file mode 100644 index 00000000..e65100f2 --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/index.ts @@ -0,0 +1,10 @@ +import {type BOBasePagePageInterface} from '@interfaces/BO'; +import { Page } from '@playwright/test'; +import FakerProduct from '@data/faker/product'; + +export interface BOCatalogProductsCreatePageInterface extends BOBasePagePageInterface { + readonly pageTitle: string; + readonly saveProductButton: string; + + setProduct(page: Page, productData: FakerProduct): Promise; +} diff --git a/src/interfaces/BO/catalog/products/create/tabCombinations.ts b/src/interfaces/BO/catalog/products/create/tabCombinations.ts new file mode 100644 index 00000000..e5e3b98e --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabCombinations.ts @@ -0,0 +1,4 @@ +import {type BOBasePagePageInterface} from '@interfaces/BO'; + +export interface BOCatalogProductsCreateTabCombinationsPageInterface extends BOBasePagePageInterface { +} diff --git a/src/interfaces/BO/catalog/products/create/tabDescription.ts b/src/interfaces/BO/catalog/products/create/tabDescription.ts new file mode 100644 index 00000000..37ad8b31 --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabDescription.ts @@ -0,0 +1,7 @@ +import {type BOBasePagePageInterface} from '@interfaces/BO'; +import { Page } from '@playwright/test'; +import FakerProduct from '@data/faker/product'; + +export interface BOCatalogProductsCreateTabDescriptionPageInterface extends BOBasePagePageInterface { + setProductDescription(page: Page, productData: FakerProduct): Promise +} diff --git a/src/interfaces/BO/catalog/products/create/tabDetails.ts b/src/interfaces/BO/catalog/products/create/tabDetails.ts new file mode 100644 index 00000000..f764f246 --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabDetails.ts @@ -0,0 +1,7 @@ +import {type BOBasePagePageInterface} from '@interfaces/BO'; +import { Page } from '@playwright/test'; +import FakerProduct from '@data/faker/product'; + +export interface BOCatalogProductsCreateTabDetailsPageInterface extends BOBasePagePageInterface { + setProductDetails(page: Page, productData: FakerProduct): Promise; +} diff --git a/src/interfaces/BO/catalog/products/create/tabOptions.ts b/src/interfaces/BO/catalog/products/create/tabOptions.ts new file mode 100644 index 00000000..f4367ba8 --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabOptions.ts @@ -0,0 +1,4 @@ +import {type BOBasePagePageInterface} from '@interfaces/BO'; + +export interface BOCatalogProductsCreateTabOptionsPageInterface extends BOBasePagePageInterface { +} diff --git a/src/interfaces/BO/catalog/products/create/tabPack.ts b/src/interfaces/BO/catalog/products/create/tabPack.ts new file mode 100644 index 00000000..4d6f2bb4 --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabPack.ts @@ -0,0 +1,7 @@ +import {type ProductPackItem} from '@data/types/product'; +import {type BOBasePagePageInterface} from '@interfaces/BO'; +import {type Page} from '@playwright/test'; + +export interface BOCatalogProductsCreateTabPackPageInterface extends BOBasePagePageInterface { + setPackOfProducts(page: Page, packData: ProductPackItem[]): Promise +} diff --git a/src/interfaces/BO/catalog/products/create/tabPricing.ts b/src/interfaces/BO/catalog/products/create/tabPricing.ts new file mode 100644 index 00000000..3825401f --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabPricing.ts @@ -0,0 +1,7 @@ +import {type BOBasePagePageInterface} from '@interfaces/BO'; +import {type Page } from '@playwright/test'; +import FakerProduct from '@data/faker/product'; + +export interface BOCatalogProductsCreateTabPricingPageInterface extends BOBasePagePageInterface { + setProductPricing(page: Page, productData: FakerProduct): Promise +} diff --git a/src/interfaces/BO/catalog/products/create/tabSeo.ts b/src/interfaces/BO/catalog/products/create/tabSeo.ts new file mode 100644 index 00000000..b348f163 --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabSeo.ts @@ -0,0 +1,4 @@ +import {type BOBasePagePageInterface} from '@interfaces/BO'; + +export interface BOCatalogProductsCreateTabSeoPageInterface extends BOBasePagePageInterface { +} diff --git a/src/interfaces/BO/catalog/products/create/tabShipping.ts b/src/interfaces/BO/catalog/products/create/tabShipping.ts new file mode 100644 index 00000000..7c2a7a96 --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabShipping.ts @@ -0,0 +1,4 @@ +import {type BOBasePagePageInterface} from '@interfaces/BO'; + +export interface BOCatalogProductsCreateTabShippingPageInterface extends BOBasePagePageInterface { +} diff --git a/src/interfaces/BO/catalog/products/create/tabStocks.ts b/src/interfaces/BO/catalog/products/create/tabStocks.ts new file mode 100644 index 00000000..363af6db --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabStocks.ts @@ -0,0 +1,7 @@ +import FakerProduct from '@data/faker/product'; +import {type BOBasePagePageInterface} from '@interfaces/BO'; +import { Page } from '@playwright/test'; + +export interface BOCatalogProductsCreateTabStocksPageInterface extends BOBasePagePageInterface { + setProductStock(page: Page, productData: FakerProduct): Promise; +} diff --git a/src/interfaces/BO/catalog/products/create/tabVirtualProduct.ts b/src/interfaces/BO/catalog/products/create/tabVirtualProduct.ts new file mode 100644 index 00000000..5a5bd39b --- /dev/null +++ b/src/interfaces/BO/catalog/products/create/tabVirtualProduct.ts @@ -0,0 +1,7 @@ +import FakerProduct from '@data/faker/product'; +import {type BOBasePagePageInterface} from '@interfaces/BO'; +import { Page } from '@playwright/test'; + +export interface BOCatalogProductsCreateTabVirtualProductPageInterface extends BOBasePagePageInterface { + setVirtualProduct(page: Page, productData: FakerProduct): Promise; +} diff --git a/src/interfaces/BO/catalog/products/index.ts b/src/interfaces/BO/catalog/products/index.ts index 7146da77..e0f00fff 100644 --- a/src/interfaces/BO/catalog/products/index.ts +++ b/src/interfaces/BO/catalog/products/index.ts @@ -3,9 +3,17 @@ import {type BOBasePagePageInterface} from '@interfaces/BO'; import { type Page } from '@playwright/test'; export interface BOCatalogProductsPageInterface extends BOBasePagePageInterface { - pageTitle: string; + readonly pageTitle: string; + readonly modalCreateProduct: string; + clickOnAddNewProduct(page: Page): Promise; + clickOnConfirmDialogButton(page: Page): Promise; + clickOnDeleteProductButton(page: Page, row?: number): Promise; + clickOnNewProductButton(page: Page): Promise; filterProducts(page: Page, filterBy: string, value: string | ProductFilterMinMax, filterType: string): Promise; getNumberOfProductsFromList(page: Page): Promise; getTextColumn(page: Page, columnName: string, row: number): Promise; + resetAndGetNumberOfLines(page: Page): Promise; + resetFilter(page: Page): Promise; + selectProductType(page: Page, productType: string): Promise; } diff --git a/src/interfaces/BO/index.ts b/src/interfaces/BO/index.ts index d307563d..2468c1d1 100644 --- a/src/interfaces/BO/index.ts +++ b/src/interfaces/BO/index.ts @@ -2,6 +2,8 @@ import type {CommonPageInterface} from '@interfaces/index'; import type {Frame, Page} from '@playwright/test'; export interface BOBasePagePageInterface extends CommonPageInterface { + successfulDeleteMessage: string; + successfulUpdateMessage: string; readonly ordersParentLink: string; readonly ordersLink: string; @@ -135,6 +137,7 @@ export interface BOBasePagePageInterface extends CommonPageInterface { readonly multistoreLink: string; closeSfToolBar(page: Frame | Page): Promise; + getAlertSuccessBlockParagraphContent(page: Frame | Page): Promise; goToSubMenu(page: Page, parentSelector: string, linkSelector: string): Promise; logoutBO(page: Page): Promise; viewMyShop(page: Page): Promise; diff --git a/src/pages/BO/catalog/products/create/index.ts b/src/pages/BO/catalog/products/create/index.ts new file mode 100644 index 00000000..e0bc29e4 --- /dev/null +++ b/src/pages/BO/catalog/products/create/index.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreatePageInterface } from '@interfaces/BO/catalog/products/create'; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreatePageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/index'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabCombinations.ts b/src/pages/BO/catalog/products/create/tabCombinations.ts new file mode 100644 index 00000000..3bf42a5e --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabCombinations.ts @@ -0,0 +1,9 @@ +import { BOCatalogProductsCreateTabCombinationsPageInterface } from "@interfaces/BO/catalog/products/create/tabCombinations"; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabCombinationsPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabCombinations'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabDescription.ts b/src/pages/BO/catalog/products/create/tabDescription.ts new file mode 100644 index 00000000..98fe01db --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabDescription.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabDescriptionPageInterface } from '@interfaces/BO/catalog/products/create/tabDescription'; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabDescriptionPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabDescription'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabDetails.ts b/src/pages/BO/catalog/products/create/tabDetails.ts new file mode 100644 index 00000000..cb22867f --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabDetails.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabDetailsPageInterface } from "@interfaces/BO/catalog/products/create/tabDetails"; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabDetailsPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabDetails'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabOptions.ts b/src/pages/BO/catalog/products/create/tabOptions.ts new file mode 100644 index 00000000..aaa04e0d --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabOptions.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabOptionsPageInterface } from "@interfaces/BO/catalog/products/create/tabOptions"; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabOptionsPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabOptions'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabPack.ts b/src/pages/BO/catalog/products/create/tabPack.ts new file mode 100644 index 00000000..f69f4909 --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabPack.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabPackPageInterface } from '@interfaces/BO/catalog/products/create/tabPack'; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabPackPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabPack'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabPricing.ts b/src/pages/BO/catalog/products/create/tabPricing.ts new file mode 100644 index 00000000..e39f4994 --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabPricing.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabPricingPageInterface } from "@interfaces/BO/catalog/products/create/tabPricing"; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabPricingPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabPricing'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabSeo.ts b/src/pages/BO/catalog/products/create/tabSeo.ts new file mode 100644 index 00000000..c35f38b8 --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabSeo.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabSeoPageInterface } from "@interfaces/BO/catalog/products/create/tabSeo"; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabSeoPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabSeo'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabShipping.ts b/src/pages/BO/catalog/products/create/tabShipping.ts new file mode 100644 index 00000000..0db00d65 --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabShipping.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabShippingPageInterface } from '@interfaces/BO/catalog/products/create/tabShipping'; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabShippingPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabShipping'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabStocks.ts b/src/pages/BO/catalog/products/create/tabStocks.ts new file mode 100644 index 00000000..bbcfd96f --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabStocks.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabStocksPageInterface } from '@interfaces/BO/catalog/products/create/tabStocks'; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabStocksPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabStocks'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/pages/BO/catalog/products/create/tabVirtualProduct.ts b/src/pages/BO/catalog/products/create/tabVirtualProduct.ts new file mode 100644 index 00000000..0ec921c7 --- /dev/null +++ b/src/pages/BO/catalog/products/create/tabVirtualProduct.ts @@ -0,0 +1,9 @@ +import type { BOCatalogProductsCreateTabVirtualProductPageInterface } from '@interfaces/BO/catalog/products/create/tabVirtualProduct'; + +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +function requirePage(): BOCatalogProductsCreateTabVirtualProductPageInterface { + return require('@versions/develop/pages/BO/catalog/products/create/tabVirtualProduct'); +} +/* eslint-enable global-require, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/index.ts b/src/versions/develop/pages/BO/catalog/products/create/index.ts new file mode 100644 index 00000000..f52a91c6 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/index.ts @@ -0,0 +1,554 @@ +// Import pages +import BOBasePage from '@pages/BO/BOBasePage'; +import productsPage from '@pages/BO/catalog/products'; +import descriptionTab from '@pages/BO/catalog/products/create/tabDescription'; +import detailsTab from '@pages/BO/catalog/products/create/tabDetails'; +import stocksTab from '@pages/BO/catalog/products/create/tabStocks'; +import pricingTab from '@pages/BO/catalog/products/create/tabPricing'; +import packTab from '@pages/BO/catalog/products/create/tabPack'; +import tabVirtualProduct from '@pages/BO/catalog/products/create/tabVirtualProduct'; + +import type ProductData from '@data/faker/product'; +import type {ProductHeaderSummary} from '@data/types/product'; + +import type {Frame, Page} from 'playwright'; +import { BOCatalogProductsCreatePageInterface } from '@interfaces/BO/catalog/products/create'; + +/** + * Create Product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class CreateProductPage extends BOBasePage implements BOCatalogProductsCreatePageInterface{ + public readonly pageTitle: string; + + public readonly saveAndPublishButtonName: string; + + public readonly successfulDuplicateMessage: string; + + public readonly errorMessage: string; + + public readonly errorMessageWhenSummaryTooLong: (number: number) => string; + + private readonly selectStoresLink: string; + + private readonly submitStoreButton: string; + + private readonly storeCheckbox: (storeID: number) => string; + + private readonly productImageUrl: string; + + private readonly productName: string; + + private readonly productNameInput: (locale: string) => string; + + private readonly productNameLanguageButton: string; + + private readonly productNameLanguageDropdown: string; + + private readonly productNameLanguageDropdownItem: (locale: string) => string; + + private readonly productTypeLabel: string; + + private readonly productTypePreview: string; + + private readonly productTypePreviewLabel: string; + + private readonly productActiveSwitchButton: string; + + private readonly modifyAllShopsNameSwitchButton: string; + + private readonly productActiveSwitchButtonToggleInput: string; + + private readonly productHeaderSummary: string; + + private readonly productHeaderTaxExcluded: string; + + private readonly productHeaderTaxIncluded: string; + + private readonly productHeaderQuantity: string; + + private readonly productHeaderReferences: string; + + private readonly productHeaderReference: (type: string) => string; + + private readonly footerProductDropDown: string; + + private readonly previewProductButton: string; + + public readonly saveProductButton: string; + + private readonly deleteProductButton: string; + + private readonly deleteProductFooterModal: string; + + private readonly deleteProductSubmitButton: string; + + private readonly newProductButton: string; + + private readonly goToCatalogButton: string; + + private readonly duplicateProductButton: string; + + private readonly duplicateProductFooterModal: string; + + private readonly duplicateProductFooterModalConfirmSubmit: string; + + private readonly formProductPage: string; + + private readonly dangerMessageShortDescription: string; + + private readonly tabLink: (tabName: string) => string; + + private readonly modalSwitchType: string; + + private readonly modalSwitchTypeBtnChoice: (productType: string) => string; + + private readonly modalSwitchTypeBtnSubmit: string; + + private readonly modalConfirmType: string; + + private readonly modalConfirmTypeBtnSubmit: string; + + /** + * @constructs + * Setting up texts and selectors to use on products V2 page + */ + constructor() { + super(); + + this.pageTitle = 'Products'; + this.saveAndPublishButtonName = 'Save and publish'; + this.successfulDuplicateMessage = 'Successful duplication'; + this.errorMessage = 'Unable to update settings.'; + this.errorMessageWhenSummaryTooLong = (number: number) => `This field cannot be longer than ${number} characters.`; + + // Multistore selectors + this.selectStoresLink = '#header-multishop a.product-shops-action'; + this.storeCheckbox = (storeID: number) => `#product_shops div.shop-selector li:nth-child(${storeID}) label input +i +div`; + this.submitStoreButton = '#product_shops_buttons_submit'; + + // Header selectors + this.productActiveSwitchButton = '#product_header_active.ps-switch'; + this.modifyAllShopsNameSwitchButton = '#product_header_modify_all_shops_name +i'; + this.productActiveSwitchButtonToggleInput = `${this.productActiveSwitchButton} input`; + this.productImageUrl = '#product_header_cover_thumbnail'; + this.productName = '#product_header_name'; + this.productNameInput = (locale: string) => `${this.productName} div.js-locale-${locale} input`; + this.productNameLanguageButton = `${this.productName}_dropdown`; + this.productNameLanguageDropdown = `${this.productName} .dropdown .dropdown-menu`; + this.productNameLanguageDropdownItem = (locale: string) => `${this.productNameLanguageDropdown} span` + + `[data-locale="${locale}"]`; + this.productTypePreview = '.product-type-preview'; + this.productTypePreviewLabel = `${this.productTypePreview}-label`; + this.productTypeLabel = '.product-type-preview-label'; + this.productHeaderSummary = '.product-header-summary'; + this.productHeaderTaxExcluded = `${this.productHeaderSummary} div[data-role=price-tax-excluded]`; + this.productHeaderTaxIncluded = `${this.productHeaderSummary} div[data-role=price-tax-included]`; + this.productHeaderQuantity = `${this.productHeaderSummary} div[data-role=quantity]`; + this.productHeaderReferences = '.product-header-references'; + this.productHeaderReference = (type: string) => `${this.productHeaderReferences} .product-reference` + + `[data-reference-type="${type}"] span`; + + // Footer selectors + this.footerProductDropDown = '#product_footer_actions_dropdown'; + this.previewProductButton = '#product_footer_actions_preview'; + this.saveProductButton = '#product_footer_save'; + this.deleteProductButton = '#product_footer_actions_delete'; + + // Footer modal + this.deleteProductFooterModal = '#delete-product-footer-modal'; + this.deleteProductSubmitButton = `${this.deleteProductFooterModal} button.btn-confirm-submit`; + this.newProductButton = '#product_footer_actions_new_product'; + this.goToCatalogButton = '#product_footer_actions_catalog'; + this.duplicateProductButton = '#product_footer_actions_duplicate_product'; + this.duplicateProductFooterModal = '#duplicate-product-footer-modal'; + this.duplicateProductFooterModalConfirmSubmit = `${this.duplicateProductFooterModal} button.btn-confirm-submit`; + + // Form + this.formProductPage = 'form.product-page'; + this.dangerMessageShortDescription = '#product_description div.alert.alert-danger'; + + // Tab + this.tabLink = (tabName: string) => `#product_${tabName}-tab-nav`; + + // Modal : Switch Product Type + this.modalSwitchType = '#switch-product-type-modal'; + this.modalSwitchTypeBtnChoice = (productType: string) => `${this.modalSwitchType} button.product-type-choice` + + `[data-value="${productType}"]`; + this.modalSwitchTypeBtnSubmit = `${this.modalSwitchType} .modal-footer button.btn-confirm-submit`; + this.modalConfirmType = '#modal-confirm-product-type'; + this.modalConfirmTypeBtnSubmit = `${this.modalConfirmType} .modal-footer button.btn-confirm-submit`; + } + + /* + Methods + */ + + /** + * Select stores + * @param page {Page} Browser tab + * @param storeID {number} Store ID to select + * @returns {Promise} + */ + async selectStores(page: Page, storeID: number): Promise { + await this.waitForSelectorAndClick(page, this.selectStoresLink); + + const selectStoreFrame = await page.frame({name: 'modal-product-shops-iframe'}) as Frame; + await selectStoreFrame.locator(this.storeCheckbox(storeID + 1)).click(); + + await selectStoreFrame.locator(this.submitStoreButton).click(); + } + + /** + * Go to a tab + * @param page {Page} Browser tab + * @param tabName {'combinations'|'description'|'details'|'options'|'pricing'|'seo'|'shipping'|'stock'} Name of the tab + * @returns {Promise} + */ + async goToTab( + page: Page, + tabName: 'combinations' | 'description' | 'details' | 'options' | 'pricing' | 'seo' | 'shipping' | 'stock', + ): Promise { + await this.waitForSelectorAndClick(page, this.tabLink(tabName)); + await this.waitForVisibleSelector(page, `${this.tabLink(tabName)} a.active`, 2000); + } + + /** + * Is Tab active + * @param page {Page} Browser tab + * @param tabName {'combinations'|'description'|'details'|'options'|'pricing'|'seo'|'shipping'|'stock'} Name of the tab + * @returns {Promise} + */ + async isTabActive( + page: Page, + tabName: 'combinations' | 'description' | 'details' | 'options' | 'pricing' | 'seo' | 'shipping' | 'stock', + ): Promise { + return this.elementVisible(page, `${this.tabLink(tabName)} a.active`, 2000); + } + + /** + * Is Tab visible + * @param page {Page} Browser tab + * @param tabName {'combinations'|'description'|'details'|'options'|'pricing'|'seo'|'shipping'|'stock'} Name of the tab + * @returns {Promise} + */ + async isTabVisible( + page: Page, + tabName: 'combinations' | 'description' | 'details' | 'options' | 'pricing' | 'seo' | 'shipping' | 'stock', + ): Promise { + return this.elementVisible(page, `${this.tabLink(tabName)} a`, 2000); + } + + /** + * Get product header summary + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductHeaderSummary(page: Page): Promise { + return { + imageUrl: await this.getAttributeContent(page, this.productImageUrl, 'value'), + priceTaxExc: await this.getTextContent(page, this.productHeaderTaxExcluded), + priceTaxIncl: await this.getTextContent(page, this.productHeaderTaxIncluded), + quantity: await this.getTextContent(page, this.productHeaderQuantity, false), + reference: (await page.locator(this.productHeaderReference('reference')).count()) + ? await this.getTextContent(page, this.productHeaderReference('reference'), false) + : '', + mpn: (await page.locator(this.productHeaderReference('mpn')).count()) + ? await this.getTextContent(page, this.productHeaderReference('mpn'), false) + : '', + upc: (await page.locator(this.productHeaderReference('upc')).count()) + ? await this.getTextContent(page, this.productHeaderReference('upc'), false) + : '', + ean_13: (await page.locator(this.productHeaderReference('ean_13')).count()) + ? await this.getTextContent(page, this.productHeaderReference('ean_13'), false) + : '', + isbn: (await page.locator(this.productHeaderReference('isbn')).count()) + ? await this.getTextContent(page, this.productHeaderReference('isbn'), false) + : '', + }; + } + + /** + * Get product header summary + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductID(page: Page): Promise { + return parseInt(await this.getAttributeContent(page, this.formProductPage, 'data-product-id'), 10); + } + + /** + * Return the product type + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductType(page: Page): Promise { + const typeLabel = await this.getTextContent(page, this.productTypePreviewLabel); + + switch (typeLabel) { + case 'Standard product': + return 'standard'; + case 'Product with combinations': + return 'combinations'; + case 'Pack of products': + return 'pack'; + case 'Virtual product': + return 'virtual'; + default: + throw new Error(`Type ${typeLabel} is not defined`); + } + } + + /** + * Set product status + * @param page {Page} Browser tab + * @param status {boolean} The product status + * @returns {Promise} + */ + async setProductStatus(page: Page, status: boolean): Promise { + if (await this.getProductStatus(page) !== status) { + await this.clickAndWaitForLoadState(page, this.productActiveSwitchButton); + return true; + } + + return false; + } + + /** + * Apply changes to all stores + * @param page {Page} Browser tab + * @param status {boolean} True if we need to apply all changes + * @returns {Promise} + */ + async applyChangesToAllStores(page: Page, status: boolean): Promise { + await this.setChecked(page, this.modifyAllShopsNameSwitchButton, status, true); + } + + /** + * Get product status + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductStatus(page: Page): Promise { + // Get value of the check input + const inputValue = await this.getAttributeContent( + page, + `${this.productActiveSwitchButtonToggleInput}:checked`, + 'value', + ); + + // Return status=false if value='0' and true otherwise + return (inputValue !== '0'); + } + + /** + * Set product + * @param page {Page} Browser tab + * @param productData {ProductData} Data to set in new product page + * @returns {Promise} + */ + async setProduct(page: Page, productData: ProductData): Promise { + // Set status + await this.setProductStatus(page, productData.status); + // Set description + await descriptionTab.setProductDescription(page, productData); + // Set name + await this.setProductName(page, productData.name, 'en'); + await this.setProductName(page, productData.nameFR, 'fr'); + + await detailsTab.setProductDetails(page, productData); + + if (productData.type === 'virtual') { + await tabVirtualProduct.setVirtualProduct(page, productData); + } else if (productData.type !== 'combinations') { + await stocksTab.setProductStock(page, productData); + } + + if (productData.type === 'pack') { + await packTab.setPackOfProducts(page, productData.pack); + } + + await pricingTab.setProductPricing(page, productData); + + return this.saveProduct(page); + } + + /** + * Set product name + * @param page {Page} Browser tab + * @param name {string} Name of the product + * @param locale {string} Locale + * @returns {Promise} + */ + async setProductName(page: Page, name: string, locale: string = 'en'): Promise { + await this.waitForSelectorAndClick(page, this.productNameLanguageButton); + await this.waitForSelectorAndClick(page, this.productNameLanguageDropdownItem(locale)); + await page + .locator(this.productNameLanguageDropdownItem(locale)) + .evaluate((el: HTMLElement) => el.click()); + + await this.setValue(page, this.productNameInput(locale), name); + } + + /** + * Click on save product button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnSaveProductButton(page: Page): Promise { + await this.clickAndWaitForLoadState(page, this.saveProductButton); + } + + /** + * Save product + * @param page {Page} Browser tab + * @returns {Promise} + */ + async saveProduct(page: Page): Promise { + await this.clickAndWaitForURL(page, this.saveProductButton); + + return this.getAlertSuccessBlockParagraphContent(page); + } + + /** + * Get save button name + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getSaveButtonName(page: Page): Promise { + return this.getTextContent(page, this.saveProductButton); + } + + /** + * Preview product in new tab + * @param page {Page} Browser tab + * @return {Promise} + */ + async previewProduct(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.footerProductDropDown); + const newPage = await this.openLinkWithTargetBlank(page, this.previewProductButton, 'body a'); + const textBody = await this.getTextContent(newPage, 'body'); + + if (textBody.includes('[Debug] This page has moved')) { + await this.clickAndWaitForURL(newPage, 'a'); + } + return newPage; + } + + /** + * Delete product + * @param page {Page} Browser tab + * @returns {Promise} + */ + async deleteProduct(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.footerProductDropDown); + await this.waitForSelectorAndClick(page, this.deleteProductButton); + await this.waitForVisibleSelector(page, this.deleteProductFooterModal); + await this.clickAndWaitForURL(page, this.deleteProductSubmitButton); + + return productsPage.getAlertSuccessBlockParagraphContent(page); + } + + /** + * Go to catalog page + * @param page {Page} Browser tab + * @returns {Promise} + */ + async goToCatalogPage(page: Page): Promise { + await this.clickAndWaitForURL(page, this.goToCatalogButton); + } + + /** + * Click on new product button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnNewProductButton(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.footerProductDropDown); + await this.waitForSelectorAndClick(page, this.newProductButton); + + return this.elementVisible(page, productsPage.modalCreateProduct, 1000); + } + + /** + * Click on duplicate product button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async duplicateProduct(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.footerProductDropDown); + await this.waitForSelectorAndClick(page, this.duplicateProductButton); + + await this.waitForSelectorAndClick(page, this.duplicateProductFooterModalConfirmSubmit); + + return this.getAlertSuccessBlockParagraphContent(page); + } + + /** + * Choose product type + * @param page {Page} Browser tab + * @param productType {string} Data to choose in product type + * @returns {Promise} + */ + async chooseProductType(page: Page, productType: string): Promise { + const currentUrl: string = page.url(); + + await productsPage.selectProductType(page, productType); + await productsPage.clickOnAddNewProduct(page); + await page.waitForURL((url: URL): boolean => url.toString() !== currentUrl, {waitUntil: 'networkidle'}); + } + + /** + * Change product type + * @param page {Page} Browser tab + * @param productType {string} Data to choose in product type + * @returns {Promise} + */ + async changeProductType(page: Page, productType: string): Promise { + // Click on the type label + await page.locator(this.productTypePreview).click(); + // Modal "Change the product type" + await this.elementVisible(page, this.modalSwitchType, 2000); + await this.waitForSelectorAndClick(page, this.modalSwitchTypeBtnChoice(productType)); + await this.waitForSelectorAndClick(page, this.modalSwitchTypeBtnSubmit); + // Modal "Are you sure you want to change the product type?" + await this.elementVisible(page, this.modalConfirmType, 2000); + await this.elementVisible(page, this.modalConfirmTypeBtnSubmit, 2000); + await this.waitForSelectorAndClick(page!, this.modalConfirmTypeBtnSubmit); + + return this.getAlertSuccessBlockParagraphContent(page); + } + + /** + * Is choose product iframe visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isChooseProductIframeVisible(page: Page): Promise { + return !(await this.elementNotVisible(page, `${productsPage.modalCreateProduct} iframe`, 1000)); + } + + /** + * Return the product name + * @param page {Page} Browser tab + * @param locale {string} Locale + * @returns {Promise} + */ + async getProductName(page: Page, locale: string = 'en'): Promise { + return this.getAttributeContent(page, this.productNameInput(locale), 'value'); + } + + /** + * Get the error message when short description is too long + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getErrorMessageWhenSummaryIsTooLong(page: Page): Promise { + await this.clickAndWaitForURL(page, this.saveProductButton); + + return this.getTextContent(page, this.dangerMessageShortDescription); + } +} + +module.exports = new CreateProductPage(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabCombinations.ts b/src/versions/develop/pages/BO/catalog/products/create/tabCombinations.ts new file mode 100644 index 00000000..b0cd4398 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabCombinations.ts @@ -0,0 +1,1058 @@ +import { + ProductAttributes, + ProductCombinationBulk, + ProductCombinationBulkRetailPrice, + ProductCombinationBulkSpecificReferences, + ProductCombinationBulkStock, + ProductCombinationOptions, + ProductStockMovement, +} from '@data/types/product'; +import { BOCatalogProductsCreateTabCombinationsPageInterface } from '@interfaces/BO/catalog/products/create/tabCombinations'; +import BOBasePage from '@pages/BO/BOBasePage'; +import {expect, type Frame, type Page} from '@playwright/test'; + +/** + * Combinations tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class CombinationsTab extends BOBasePage implements BOCatalogProductsCreateTabCombinationsPageInterface { + public readonly generateCombinationsMessage: (number: number) => string; + + public readonly successfulGenerateCombinationsMessage: (number: number) => string; + + public readonly editCombinationsModalTitle: (number: number) => string; + + public readonly editCombinationsModalMessage: (number: number) => string; + + public readonly successfulUpdateMessage: string; + + private readonly combinationsTabLink: string; + + private readonly learnMoreButton: string; + + private readonly attributesAndFeaturesHyperLink: string; + + private readonly generateFirstCombinationsButton: string; + + private readonly generateCombinationButton: string; + + private readonly generateCombinationsModal: string; + + private readonly cancelButton: string; + + private readonly searchAttributesButton: string; + + private readonly generateCombinationsButtonOnModal: string; + + private readonly generateCombinationsCloseButton: string; + + private readonly saveCombinationEditButton: string; + + private readonly bulkActionsButton: string; + + private readonly bulkEditButton: string; + + private readonly bulkEditModal: string; + + private readonly bulkEditModalTitle: string; + + private readonly bulkEditModalStocksButton: string; + + private readonly bulkEditModalQuantitySwitchButton: (toEnable: number) => string; + + private readonly bulkEditModalQuantityInput: string; + + private readonly bulkEditModalMinimalQuantitySwitchButton: (toEnable: number) => string; + + private readonly bulkEditModalMinimalQuantityInput: string; + + private readonly bulkEditModalStockLocationSwitchButton: (toEnable: number) => string; + + private readonly selectAllAttributeCheckboxButton: (row: number) => string; + + private readonly bulkEditModalStockLocationInput: string; + + private readonly bulkEditModalSaveButton: string; + + private readonly bulkEditModalRetailPriceButton: string; + + private readonly bulkEditModalCostPriceSwitchButton: (toEnable: number) => string; + + private readonly bulkEditModalCostPriceInput: string; + + private readonly bulkEditModalImpactOnPriceTIncSwitchButton: (toEnable: number) => string; + + private readonly bulkEditModalImpactOnPriceTIncInput: string; + + private readonly bulkEditModalImpactOnWeightSwitchButton: (toEnable: number) => string; + + private readonly bulkEditModalImpactOnWeightInput: string; + + private readonly bulkEditModalSpecificReferences: string; + + private readonly bulkEditModalReferenceSwitchButton: (toEnable: number) => string; + + private readonly bulkEditModalReferenceInput: string; + + private readonly bulkCombinationProgressModal: string; + + private readonly bulkCombinationProgressBarDone: string; + + private readonly bulkCombinationProgressModalHeader: string; + + private readonly bulkCombinationProgressModalCloseButton: string; + + private readonly filterBySizeBlock: string; + + private readonly filterBySizeButton: string; + + private readonly filterBySizeDropDownMenu: string; + + private readonly filterBySizeCheckboxButton: (toEnable: number) => string; + + private readonly clearFilterButton: string; + + private readonly combinationsListTable: string; + + private readonly combinationListTableRow: (row: number) => string; + + private readonly combinationListTableColumn: (row: number, column: string) => string; + + private readonly combinationListTableActionsDropDown: (row: number) => string; + + private readonly combinationListTableActionsColumn: (row: number, action: string) => string; + + private readonly combinationListTableSelectAllButton: string; + + private readonly combinationListSelectAllDropDownMenu: string; + + private readonly combinationListBulkSelectAll: string; + + private readonly combinationListBulkSelectAllInPage: string; + + private readonly editCombinationIframe: string; + + private readonly editCombinationEditModal: string; + + private readonly editCombinationModal: string; + + private readonly editCombinationNameValue: string; + + private readonly editCombinationModalQuantityInput: string; + + private readonly editCombinationModalMinimalQuantityInput: string; + + private readonly editCombinationModalImpactOnPriceTExcInput: string; + + private readonly editCombinationModalReferenceInput: string; + + private readonly editCombinationModalSaveButton: string; + + private readonly editCombinationModalCloseButton: string; + + private readonly editCombinationCloseModal: string; + + private readonly editCombinationModalDiscardButton: string; + + private readonly combinationStockMovementsDate: (row: number) => string; + + private readonly combinationStockMovementsEmployeeName: (row: number) => string; + + private readonly combinationStockMovements: (row: number) => string; + + private readonly modalConfirmDeleteCombination: string; + + private readonly previousCombinationButton: string; + + private readonly nextCombinationButton: string; + + private readonly modalDeleteCombinationCancelButton: string; + + private readonly modalDeleteCombinationDeleteButton: string; + + private readonly tableHead: string; + + private readonly sortColumnDiv: (columnNumber: number) => string; + + private readonly sortColumnSpanButton: (columnNumber: number) => string; + + private readonly paginationBlock: string; + + private readonly paginationLabel: string; + + private readonly paginationLimitSelect: string; + + private readonly paginationNextLink: string; + + private readonly paginationPreviousLink: string; + + private readonly denyOrderRadioButton: string; + + private readonly allowOrderRadioButton: string; + + private readonly useDefaultBehaviourRadioButton: string; + + private readonly editDefaultBehaviourLink: string; + + private readonly labelWhenInStock: string; + + private readonly labelWhenOutOfStock: string; + + /** + * @constructs + * Setting up texts and selectors to use on combinations tab + */ + constructor() { + super(); + this.generateCombinationsMessage = (number: number) => `Generate ${number} combinations`; + this.successfulGenerateCombinationsMessage = (number: number) => `Successfully generated ${number} combinations.`; + this.editCombinationsModalTitle = (number: number) => `Edit ${number} combinations`; + this.editCombinationsModalMessage = (number: number) => `Editing ${number}/${number} combinations`; + this.successfulUpdateMessage = 'Update successful'; + + // Selectors in combinations tab + this.combinationsTabLink = '#product_combinations-tab-nav'; + this.learnMoreButton = '#combinations-empty-state a[href*=\'documentation\']'; + this.attributesAndFeaturesHyperLink = '#combinations-empty-state p.mx-auto.showcase-list-card__message a.alert-link'; + this.generateFirstCombinationsButton = '#combinations-empty-state button.generate-combinations-button'; + this.generateCombinationButton = '#combination-list-actions button.generate-combinations-button'; + this.generateCombinationsModal = '#product-combinations-generate div.modal.show'; + this.searchAttributesButton = `${this.generateCombinationsModal} input.attributes-search`; + this.generateCombinationsButtonOnModal = `${this.generateCombinationsModal} footer button.btn.btn-primary`; + this.generateCombinationsCloseButton = `${this.generateCombinationsModal} button.close`; + this.saveCombinationEditButton = '#save-combinations-edition'; + this.cancelButton = `${this.generateCombinationsModal} button.btn-outline-secondary`; + + // Bulk actions selectors + this.bulkActionsButton = '#combination-bulk-actions-btn'; + this.bulkEditButton = '#combination-bulk-form-btn'; + + // Bulk edit modal + this.bulkEditModal = '#bulk-combination-form-modal'; + this.bulkEditModalTitle = `${this.bulkEditModal} .modal-header .modal-title`; + // Edit stocks + this.bulkEditModalStocksButton = '#bulk_combination_stock_accordion_header h2 button'; + this.bulkEditModalQuantitySwitchButton = (toEnable: number) => '#bulk_combination_stock_disabling_switch_delta_quantity_' + + `${toEnable}`; + this.bulkEditModalQuantityInput = '#bulk_combination_stock_delta_quantity_delta'; + this.bulkEditModalMinimalQuantitySwitchButton = (toEnable: number) => '#bulk_combination_stock_disabling_switch_minimal' + + `_quantity_${toEnable}`; + this.bulkEditModalMinimalQuantityInput = '#bulk_combination_stock_minimal_quantity'; + this.bulkEditModalStockLocationSwitchButton = (toEnable: number) => '#bulk_combination_stock_disabling_switch_stock_' + + `location_${toEnable}`; + this.bulkEditModalStockLocationInput = '#bulk_combination_stock_stock_location'; + this.bulkEditModalSaveButton = '#bulk-combination-form-modal div.modal-footer button.btn-confirm-submit'; + // Edit retail price + this.bulkEditModalRetailPriceButton = '#bulk_combination_price_accordion_header h2 button'; + this.bulkEditModalCostPriceSwitchButton = (toEnable: number) => '#bulk_combination_price_disabling_switch_wholesale_' + + `price_${toEnable}`; + this.bulkEditModalCostPriceInput = '#bulk_combination_price_wholesale_price'; + this.bulkEditModalImpactOnPriceTIncSwitchButton = (toEnable: number) => '#bulk_combination_price_disabling_switch_price_' + + `tax_excluded_${toEnable}`; + this.bulkEditModalImpactOnPriceTIncInput = '#bulk_combination_price_price_tax_excluded'; + this.bulkEditModalImpactOnWeightSwitchButton = (toEnable: number) => '#bulk_combination_price_disabling_switch_weight_' + + `${toEnable}`; + this.bulkEditModalImpactOnWeightInput = '#bulk_combination_price_weight'; + // Edit specific references + this.bulkEditModalSpecificReferences = '#bulk_combination_references_accordion_header h2 button'; + this.bulkEditModalReferenceSwitchButton = (toEnable: number) => '#bulk_combination_references_disabling_switch_' + + `reference_${toEnable}`; + this.bulkEditModalReferenceInput = '#bulk_combination_references_reference'; + // Save progress modal + this.bulkCombinationProgressModal = '#bulk-combination-progress-modal'; + this.bulkCombinationProgressBarDone = '#modal_progressbar_done[aria-valuenow="100"]'; + this.bulkCombinationProgressModalHeader = '#bulk-combination-progress-modal div.progress-headline div'; + this.bulkCombinationProgressModalCloseButton = '#bulk-combination-progress-modal button.close-modal-button'; + + // Filter by size selectors + this.filterBySizeBlock = 'div.combinations-filters [data-role=filter-by-size-block]'; + this.filterBySizeDropDownMenu = `${this.filterBySizeBlock} div.dropdown-menu`; + this.filterBySizeButton = `${this.filterBySizeBlock} [data-role=filter-by-size-btn]`; + this.filterBySizeCheckboxButton = (id: number) => `${this.filterBySizeDropDownMenu} div:nth-child(${id}) ` + + '.md-checkbox-container'; + this.clearFilterButton = 'div.combinations-filters button.combinations-filters-clear'; + + // Selectors of combinations table + this.combinationsListTable = '#combination_list'; + this.combinationListTableRow = (row: number) => `#combination-list-row-${row - 1}`; + this.combinationListTableColumn = (row: number, column: string) => `td input#combination_list_${row - 1}_${column}`; + this.combinationListTableActionsDropDown = (row: number) => `#combination_list_${row - 1}_actions div a`; + this.combinationListTableActionsColumn = (row: number, action: string) => `td button#combination_list_${row - 1}` + + `_actions_${action}`; + this.combinationListTableSelectAllButton = '#bulk-all-selection-dropdown-button'; + this.combinationListSelectAllDropDownMenu = '#bulk-all-selection-dropdown .dropdown-menu.show'; + this.combinationListBulkSelectAll = '#bulk-all-selection-dropdown label[for="bulk-select-all"]'; + this.combinationListBulkSelectAllInPage = '#bulk-select-all-in-page + i'; + this.selectAllAttributeCheckboxButton = (row: number) => '#attributes-list-selector div.attributes-content' + + ` div:nth-child(${row}) div.attribute-group-header div`; + + // Edit combination modal + this.editCombinationIframe = '.combination-iframe'; + this.editCombinationEditModal = '#combination-edit-modal'; + this.editCombinationModal = '#combination-edit-modal div.combination-modal div.modal.show'; + this.editCombinationNameValue = '#combination_form_header div.combination-name-row span.text-preview-value'; + this.editCombinationModalQuantityInput = '#combination_form_stock_quantities_delta_quantity_delta'; + this.editCombinationModalMinimalQuantityInput = '#combination_form_stock_quantities_minimal_quantity'; + this.editCombinationModalImpactOnPriceTExcInput = '#combination_form_price_impact_price_tax_excluded'; + this.editCombinationModalReferenceInput = '#combination_form_references_reference'; + this.editCombinationModalSaveButton = `${this.editCombinationModal} footer button.btn-primary`; + this.editCombinationModalCloseButton = `${this.editCombinationModal} footer button.btn-close`; + this.editCombinationCloseModal = `${this.editCombinationEditModal} div.modal-prevent-close div.modal.show`; + this.editCombinationModalDiscardButton = `${this.editCombinationCloseModal} button.btn-primary`; + this.combinationStockMovementsDate = (row: number) => `#combination_form_stock_quantities_stock_movements_${row - 1}_` + + 'date + span'; + this.combinationStockMovementsEmployeeName = (row: number) => '#combination_form_stock_quantities_stock_movements_' + + `${row - 1}_employee_name + span`; + this.combinationStockMovements = (row: number) => `#combination_form_stock_quantities_stock_movements_${row - 1}` + + '_delta_quantity + span'; + this.previousCombinationButton = `${this.editCombinationModal} button.btn-previous-combination`; + this.nextCombinationButton = `${this.editCombinationModal} button.btn-next-combination`; + + // Delete combination modal + this.modalConfirmDeleteCombination = '#modal-confirm-delete-combination'; + this.modalDeleteCombinationCancelButton = `${this.modalConfirmDeleteCombination} button.btn-outline-secondary`; + this.modalDeleteCombinationDeleteButton = `${this.modalConfirmDeleteCombination} button.btn-confirm-submit`; + + // Sort Selectors + this.tableHead = `${this.combinationsListTable} thead`; + this.sortColumnDiv = (columnNumber: number) => `${this.tableHead} th:nth-child(${columnNumber}) ` + + 'div.ps-sortable-column[data-sort-col-name]'; + this.sortColumnSpanButton = (columnNumber: number) => `${this.sortColumnDiv(columnNumber)} span.ps-sort`; + + // Pagination selectors + this.paginationBlock = '#combinations-pagination'; + this.paginationLabel = '#pagination-info'; + this.paginationLimitSelect = `${this.paginationBlock} #paginator-limit`; + this.paginationNextLink = `${this.paginationBlock} .page-link.next:not(.disabled)`; + this.paginationPreviousLink = `${this.paginationBlock} .page-link.previous:not(.disabled)`; + + // When out of stock selectors + this.denyOrderRadioButton = '#product_combinations_availability_out_of_stock_type_0 +i'; + this.allowOrderRadioButton = '#product_combinations_availability_out_of_stock_type_1 +i'; + this.useDefaultBehaviourRadioButton = '#product_combinations_availability_out_of_stock_type_2 +i'; + this.editDefaultBehaviourLink = '#product_combinations_availability a[href*=configuration_fieldset_stock]'; + this.labelWhenInStock = '#product_combinations_availability_available_now_label_1'; + this.labelWhenOutOfStock = '#product_combinations_availability_available_later_label_1'; + } + + /* + Methods + */ + + // Methods for create combinations + /** + * Click on attributes & features link + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnAttributesAndFeaturesLink(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.combinationsTabLink); + + return this.openLinkWithTargetBlank(page, this.attributesAndFeaturesHyperLink, 'body'); + } + + /** + * Click on learn more button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnLearnMoreButton(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.combinationsTabLink); + + return this.openLinkWithTargetBlank(page, this.learnMoreButton, 'body', 'domcontentloaded'); + } + + /** + * Click on generate combination button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnGenerateCombinationButton(page: Page): Promise { + if (await this.elementVisible(page, this.generateCombinationButton, 2000)) { + await this.waitForSelectorAndClick(page, this.generateCombinationButton); + } else { + await this.waitForSelectorAndClick(page, this.generateFirstCombinationsButton); + } + + return this.elementVisible(page, this.generateCombinationsModal, 1000); + } + + /** + * Click on cancel button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnCancelButton(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.cancelButton); + + return this.elementNotVisible(page, this.generateCombinationsModal, 1000); + } + + /** + * Add combination + * @param page {Page} Browser tab + * @param combination {string} Attribute to set + * @returns {Promise} + */ + async selectAttribute(page: Page, combination: string): Promise { + await page.locator(this.searchAttributesButton).fill(combination); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + } + + /** + * Set product attributes + * @param page {Page} Browser tab + * @param attributes {ProductAttributes[]} Combinations of the product + * @returns {Promise} + */ + async setProductAttributes(page: Page, attributes: ProductAttributes[]): Promise { + await this.waitForSelectorAndClick(page, this.combinationsTabLink); + if (await this.elementVisible(page, this.generateCombinationButton, 2000)) { + await this.waitForSelectorAndClick(page, this.generateCombinationButton); + } else { + await this.waitForSelectorAndClick(page, this.generateFirstCombinationsButton); + } + + await this.waitForVisibleSelector(page, this.generateCombinationsModal); + + for (let i: number = 0; i < attributes.length; i++) { + for (let j: number = 0; j < attributes[i].values.length; j++) { + await this.selectAttribute(page, `${attributes[i].name} : ${attributes[i].values[j]}`); + } + } + /* eslint-enable */ + + return this.getTextContent(page, this.generateCombinationsButtonOnModal); + } + + /** + * Select all values + * @param page {Page} Browser tab + * @param attribute {string} + * @returns {Promise} + */ + async selectAllValues(page: Page, attribute: string): Promise { + switch (attribute) { + case 'size': + await page.locator(this.selectAllAttributeCheckboxButton(1)).click(); + break; + + case 'color': + await page.locator(this.selectAllAttributeCheckboxButton(2)).click(); + break; + + default: + throw new Error('Attribute is not existing'); + } + + return this.getTextContent(page, this.generateCombinationsButtonOnModal); + } + + /** + * Generate combinations + * @param page {Page} Browser tab + * @returns {Promise} + */ + async generateCombinations(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.generateCombinationsButtonOnModal); + + return this.getGrowlMessageContent(page); + } + + /** + * Check if generation modal is closed + * @param page {Page} Browser tab + * @returns {Promise} + */ + async generateCombinationModalIsClosed(page: Page): Promise { + return this.elementNotVisible(page, this.generateCombinationsModal, 1000); + } + + // Methods for edit/delete combinations + + /** + * Click on edit icon + * @param page {Page} Browser tab + * @param row {number} Row in table + * @returns {Promise} + */ + async clickOnEditIcon(page: Page, row: number = 1): Promise { + await this.waitForSelectorAndClick(page, `${this.combinationListTableActionsColumn(row, 'edit')}`); + + return this.elementVisible(page, this.editCombinationModal, 2000); + } + + /** + * Edit combination + * @param page {Page} Browser tab + * @param combinationData {ProductCombinationOptions} Data to set to edit combination + * @param row {number} Row in table + * @returns {Promise} + */ + async editCombination(page: Page, combinationData: ProductCombinationOptions, row: number = 1): Promise { + await this.setValue(page, `${this.combinationListTableColumn(row, 'reference')}`, combinationData.reference); + await this.setValue( + page, + `${this.combinationListTableColumn(row, 'impact_on_price_te')}`, + combinationData.impactOnPriceTExc, + ); + await this.setValue( + page, + `${this.combinationListTableColumn(row, 'delta_quantity_delta')}`, + combinationData.quantity, + ); + + return this.saveCombinationsForm(page); + } + + /** + * Edit combination row quantity + * @param page {Page} Browser tab + * @param row {number} Row in table + * @param quantity {number} Quantity value to set in quantity input + * @returns {Promise} + */ + async editCombinationRowQuantity(page: Page, row: number, quantity: number): Promise { + await page.locator(`${this.combinationListTableColumn(row, 'delta_quantity_delta')}`).click(); + await page.waitForTimeout(500); + await page.keyboard.down('ControlLeft'); + await page.keyboard.press('KeyA'); + await page.waitForTimeout(500); + await page.keyboard.up('ControlLeft'); + await this.setValue(page, `${this.combinationListTableColumn(row, 'delta_quantity_delta')}`, quantity); + await page.waitForTimeout(500); + await this.waitForVisibleSelector(page, this.saveCombinationEditButton); + } + + /** + * Save combinations form + * @param page {Page} Browser tab + * @returns {Promise} + */ + async saveCombinationsForm(page: Page): Promise { + await this.closeGrowlMessage(page); + + await this.waitForSelectorAndClick(page, this.saveCombinationEditButton); + + return this.getGrowlMessageContent(page); + } + + /** + * Click on next combination button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnNextCombinationButton(page: Page): Promise { + await page.locator(this.nextCombinationButton).click(); + await page.waitForTimeout(2000); + } + + /** + * Click on previous combination button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnPreviousCombinationButton(page: Page): Promise { + await page.locator(this.previousCombinationButton).click(); + await page.waitForTimeout(2000); + } + + /** + * Get combination name from modal + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getCombinationNameFromModal(page: Page): Promise { + const combinationFrame: Frame | null = page.frame({url: /sell\/catalog\/products\/combinations/gmi}); + expect(combinationFrame).not.toBeNull(); + + return this.getTextContent(combinationFrame!, this.editCombinationNameValue); + } + + /** + * Click on delete icon then (delete/cancel) + * @param page {Page} Browser tab + * @param action {string} Delete/cancel + * @param row {number} Row in table + * @returns {Promise} + */ + async clickOnDeleteIcon(page: Page, action: string, row: number = 1): Promise { + await this.waitForSelectorAndClick(page, this.combinationListTableActionsDropDown(row)); + await this.waitForSelectorAndClick(page, `${this.combinationListTableActionsColumn(row, 'delete')}`); + + if (action === 'cancel') { + await this.waitForSelectorAndClick(page, this.modalDeleteCombinationCancelButton); + return !(await this.elementNotVisible(page, this.modalConfirmDeleteCombination, 2000)); + } + + await this.waitForSelectorAndClick(page, this.modalDeleteCombinationDeleteButton); + + return this.getGrowlMessageContent(page); + } + + /** + * Edit combination from modal + * @param page {Page} Browser tab + * @param combinationData {ProductCombinationOptions} + * @returns {Promise} + */ + async editCombinationFromModal(page: Page, combinationData: ProductCombinationOptions): Promise { + await page.waitForTimeout(2000); + await this.waitForVisibleSelector(page, this.editCombinationIframe); + + const combinationFrame: Frame|null = page.frame({url: /sell\/catalog\/products\/combinations/gmi}); + expect(combinationFrame).not.toBeNull(); + + await this.setValue(combinationFrame!, this.editCombinationModalQuantityInput, combinationData.quantity); + if (combinationData.minimalQuantity) { + await this.setValue( + combinationFrame!, + this.editCombinationModalMinimalQuantityInput, + combinationData.minimalQuantity, + ); + } + await this.setValue( + combinationFrame!, + this.editCombinationModalImpactOnPriceTExcInput, + combinationData.impactOnPriceTExc, + ); + await this.setValue(combinationFrame!, this.editCombinationModalReferenceInput, combinationData.reference); + + await this.waitForSelectorAndClick(page, this.editCombinationModalSaveButton); + + return this.getAlertSuccessBlockParagraphContent(combinationFrame!); + } + + /** + * Get recent stock movements + * @param page {Page} Browser tab + * @param row {number} Row in table + * @returns {Promise} + */ + async getRecentStockMovements(page: Page, row: number = 1): Promise { + const combinationFrame: Frame|null = page.frame({url: /sell\/catalog\/products\/combinations/gmi}); + expect(combinationFrame).not.toBeNull(); + + return { + dateTime: await this.getTextContent(combinationFrame!, this.combinationStockMovementsDate(row)), + employee: await this.getTextContent(combinationFrame!, this.combinationStockMovementsEmployeeName(row)), + quantity: await this.getNumberFromText(combinationFrame!, this.combinationStockMovements(row)), + }; + } + + /** + * Close edit combination modal + * @param page {Page} Browser tab + * @returns {Promise} + */ + async closeEditCombinationModal(page: Page): Promise { + const combinationFrame: Frame|null = page.frame({url: /sell\/catalog\/products\/combinations/gmi}); + expect(combinationFrame).not.toBeNull(); + + await this.waitForSelectorAndClick(page, this.editCombinationModalCloseButton); + if (await this.elementVisible(page, this.editCombinationModalDiscardButton, 2000)) { + await this.waitForSelectorAndClick(page, this.editCombinationModalDiscardButton); + } + + return this.elementVisible(combinationFrame!, this.editCombinationModalQuantityInput, 2000); + } + + // Methods for sort + /** + * Sort table by clicking on column name + * @param page {Page} Browser tab + * @param sortBy {string} Column to sort with + * @param column {number} The number of columns + * @param sortDirection {string} Sort direction asc or desc + * @return {Promise} + */ + async sortTable(page: Page, sortBy: string, column: number, sortDirection: string = 'asc'): Promise { + const sortColumnDiv = `${this.sortColumnDiv(column)}[data-sort-direction='${sortDirection}']`; + const sortColumnSpanButton = this.sortColumnSpanButton(column); + + let i = 0; + while (await this.elementNotVisible(page, sortColumnDiv, 2000) && i < 2) { + await page.hover(this.sortColumnDiv(column)); + await this.waitForSelectorAndClick(page, sortColumnSpanButton); + i += 1; + } + } + + /** + * Get text column + * @param page {Frame|Page} Browser tab + * @param column {string} Column name to get text content + * @param row {number} Row on table + * @returns {Promise} + */ + async getTextColumn(page: Page, column: string, row: number = 1): Promise { + const selector: string = this.combinationListTableColumn(row, column); + let text: string | null = ''; + + switch (column) { + case 'combination_id': + text = await this.getTextContent(page, `${selector} + span`); + break; + case 'name': + text = await this.getTextContent(page, `${selector} + span`); + break; + case 'impact_on_price_te': + text = await this.getAttributeContent(page, selector, 'value'); + break; + case 'impact_on_price_ti': + text = await this.getAttributeContent(page, selector, 'value'); + break; + case 'final_price_te': + text = await this.getTextContent(page, `${selector} + span`); + break; + case 'quantity': + text = await this.getTextContent(page, selector); + break; + default: + // Do nothing + } + // click on search + + return text; + } + + /** + * Get content from all rows + * @param page {Page} Browser tab + * @param numberOfCombinations {number} Number of combinations + * @param column {string} Column name to get all rows text content + * @return {Promise} + */ + async getAllRowsColumnContent(page: Page, numberOfCombinations: number, column: string): Promise { + const allRowsContentTable: string[] = []; + + for (let i = 1; i <= numberOfCombinations; i++) { + const rowContent = await this.getTextColumn(page, column, i); + allRowsContentTable.push(rowContent); + } + + return allRowsContentTable; + } + + // Methods for pagination + /** + * Get pagination label + * @param page {Page} Browser tab + * @return {Promise} + */ + async getPaginationLabel(page: Page): Promise { + await page.waitForTimeout(2000); + + return this.getTextContent(page, this.paginationLabel); + } + + /** + * Select pagination limit + * @param page {Page} Browser tab + * @param number {number} Value of pagination limit to select + * @returns {Promise} + */ + async selectPaginationLimit(page: Page, number: number): Promise { + await this.selectByVisibleText(page, this.paginationLimitSelect, number); + + return this.getPaginationLabel(page); + } + + /** + * Click on next + * @param page {Page} Browser tab + * @returns {Promise} + */ + async paginationNext(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.paginationNextLink); + + return this.getPaginationLabel(page); + } + + /** + * Click on previous + * @param page {Page} Browser tab + * @returns {Promise} + */ + async paginationPrevious(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.paginationPreviousLink); + + return this.getPaginationLabel(page); + } + + // Methods for filter combinations by size + /** + * Get number of combinations displayed in list + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getNumberOfCombinationsFromList(page: Page): Promise { + const footerText = await this.getTextContent(page, this.paginationLabel); + + if (!footerText) { + return 0; + } + const regexMatch: RegExpMatchArray | null = footerText.match(/out of ([0-9]+)/); + + if (regexMatch === null) { + return 0; + } + + const regexResult: RegExpExecArray | null = /\d+/g.exec(regexMatch.toString()); + + if (regexResult === null) { + return 0; + } + + return parseInt(regexResult.toString(), 10); + } + + /** + * Filter combinations by color + * @param page {Page} Browser tab + * @param sizeID {number} Size number in list + * @returns {Promise} + */ + async filterCombinationsBySize(page: Page, sizeID: number): Promise { + await this.waitForSelectorAndClick(page, this.filterBySizeButton); + await this.waitForVisibleSelector(page, `${this.filterBySizeDropDownMenu}.show`); + await this.setChecked(page, this.filterBySizeCheckboxButton(sizeID)); + + await this.waitForSelectorAndClick(page, this.filterBySizeButton); + } + + /** + * Get filter button name + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getFilterBySizeButtonName(page: Page): Promise { + return this.getTextContent(page, this.filterBySizeButton); + } + + /** + * Clear filter + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clearFilter(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.clearFilterButton); + + return this.getNumberOfCombinationsFromList(page); + } + + // Methods for bulk actions + /** + * Select All combinations + * @param page {Page} Browser tab + * @param allCombinations {boolean} true to select all combinations, false to select all in page + * @returns {Promise} + */ + async selectAllCombinations(page: Page, allCombinations: boolean = true): Promise { + await this.waitForSelectorAndClick(page, this.combinationListTableSelectAllButton); + + await this.waitForVisibleSelector(page, this.combinationListSelectAllDropDownMenu); + if (allCombinations) { + await this.setChecked(page, this.combinationListBulkSelectAll); + } else await this.setChecked(page, this.combinationListBulkSelectAllInPage); + + return this.elementVisible(page, this.bulkActionsButton, 2000); + } + + /** + * Click on edit Combinations by bulk actions + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnEditCombinationsByBulkActions(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.bulkActionsButton); + await this.waitForSelectorAndClick(page, this.bulkEditButton); + + await this.waitForVisibleSelector(page, this.bulkEditModal); + + return this.getTextContent(page, this.bulkEditModalTitle); + } + + /** + * Bulk edit stock + * @param page {Frame|Page} Browser tab + * @param editStockData {ProductCombinationBulkStock} Data to set on bulk edit stock form + * @returns {Promise} + */ + async bulkEditStock(page: Frame | Page, editStockData: ProductCombinationBulkStock): Promise { + await this.waitForSelectorAndClick(page, this.bulkEditModalStocksButton); + + await this.setChecked( + page, + this.bulkEditModalQuantitySwitchButton(editStockData.quantityToEnable ? 1 : 0), + ); + if (editStockData.quantity) { + await this.setValue(page, this.bulkEditModalQuantityInput, editStockData.quantity); + } + + await this.setChecked( + page, + this.bulkEditModalMinimalQuantitySwitchButton(editStockData.minimalQuantityToEnable ? 1 : 0), + ); + if (editStockData.minimalQuantity) { + await this.setValue( + page, + this.bulkEditModalMinimalQuantityInput, + editStockData.minimalQuantity, + ); + } + + await this.setChecked( + page, + this.bulkEditModalStockLocationSwitchButton(editStockData.stockLocationToEnable ? 1 : 0), + ); + if (editStockData.stockLocation) { + await this.setValue( + page, + this.bulkEditModalStockLocationInput, + editStockData.stockLocation, + ); + } + + await this.waitForSelectorAndClick(page, this.bulkEditModalStocksButton); + } + + /** + * Bulk edit retail price + * @param page {Frame|Page} Browser tab + * @param editRetailPriceData {ProductCombinationBulkRetailPrice} Data to set on bulk edit retail price form + * @returns {Promise} + */ + async bulkEditRetailPrice(page: Frame | Page, editRetailPriceData: ProductCombinationBulkRetailPrice): Promise { + await this.waitForSelectorAndClick(page, this.bulkEditModalRetailPriceButton); + + await this.setChecked(page, this.bulkEditModalCostPriceSwitchButton(editRetailPriceData.costPriceToEnable ? 1 : 0), + ); + if (editRetailPriceData.costPrice) { + await this.setValue(page, this.bulkEditModalCostPriceInput, editRetailPriceData.costPrice); + } + + await this.setChecked(page, + this.bulkEditModalImpactOnPriceTIncSwitchButton(editRetailPriceData.impactOnPriceTIncToEnable ? 1 : 0), + ); + if (editRetailPriceData.impactOnPriceTInc) { + await this.setValue(page, this.bulkEditModalImpactOnPriceTIncInput, editRetailPriceData.impactOnPriceTInc); + } + + await this.setChecked( + page, + this.bulkEditModalImpactOnWeightSwitchButton(editRetailPriceData.impactOnWeightToEnable ? 1 : 0), + ); + if (editRetailPriceData.impactOnWeight) { + await this.setValue(page, this.bulkEditModalImpactOnWeightInput, editRetailPriceData.impactOnWeight); + } + + await this.waitForSelectorAndClick(page, this.bulkEditModalRetailPriceButton); + } + + /** + * Bulk edit references + * @param page {Frame|Page} Browser tab + * @param specificReferencesData {ProductCombinationBulkSpecificReferences} Data to set on specific references form + * @returns {Promise} + */ + async bulkEditSpecificPrice(page: Frame|Page, specificReferencesData: ProductCombinationBulkSpecificReferences): Promise { + await this.waitForSelectorAndClick(page, this.bulkEditModalSpecificReferences); + + await this.setChecked( + page, + this.bulkEditModalReferenceSwitchButton(specificReferencesData.referenceToEnable ? 1 : 0), + ); + if (specificReferencesData.reference) { + await this.setValue(page, this.bulkEditModalReferenceInput, specificReferencesData.reference); + } + } + + /** + * Edit combinations by bulk actions + * @param page {Page} Browser tab + * @param editCombinationsData {ProductCombinationBulk} Data to edit combination + * @returns {Promise} + */ + async editCombinationsByBulkActions(page: Page, editCombinationsData: ProductCombinationBulk): Promise { + const bulkEditCombinationFrame: Frame|null = page.frame('bulk-combination-form-modal-iframe'); + expect(bulkEditCombinationFrame).not.toBeNull(); + + // Edit stocks + if (editCombinationsData.stocks) { + await this.bulkEditStock(bulkEditCombinationFrame!, editCombinationsData.stocks); + } + // Edit retail price + if (editCombinationsData.retailPrice) { + await this.bulkEditRetailPrice(bulkEditCombinationFrame!, editCombinationsData.retailPrice); + } + // Edit specific references + if (editCombinationsData.specificReferences) { + await this.bulkEditSpecificPrice(bulkEditCombinationFrame!, editCombinationsData.specificReferences); + } + // Save and close progress modal + await this.waitForSelectorAndClick(page, this.bulkEditModalSaveButton); + await this.waitForVisibleSelector(page, this.bulkCombinationProgressModal); + await this.waitForVisibleSelector(page, this.bulkCombinationProgressBarDone); + const result = await this.getTextContent(page, this.bulkCombinationProgressModalHeader); + await this.waitForSelectorAndClick(page, this.bulkCombinationProgressModalCloseButton); + + // Return text in modal + return result; + } + + // Methods for when out of stock + /** + * Set option when out of stock + * @param page {Page} Browser tab + * @param option {string} Option to check + * @returns {Promise} + */ + async setOptionWhenOutOfStock(page: Page, option: string): Promise { + switch (option) { + case 'Deny orders': + await this.setChecked(page, this.denyOrderRadioButton); + break; + case 'Allow orders': + await this.setChecked(page, this.allowOrderRadioButton); + break; + case 'Use default behavior': + await this.setChecked(page, this.useDefaultBehaviourRadioButton); + break; + default: + throw new Error(`Option ${option} was not found`); + } + } + + /** + * Click on edit default behaviour link + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnEditDefaultBehaviourLink(page: Page): Promise { + return this.openLinkWithTargetBlank(page, this.editDefaultBehaviourLink); + } + + /** + * Set label when in stock + * @param page {Page} Browser tab + * @param label {string} Label to set when in stock in the input + * @returns {Promise} + */ + async setLabelWhenInStock(page: Page, label: string): Promise { + await this.setValue(page, this.labelWhenInStock, label); + } + + /** + * Set label when out of stock + * @param page {Page} Browser tab + * @param label {string} Label to set when out of stock in the input + */ + async setLabelWhenOutOfStock(page: Page, label: string): Promise { + await this.setValue(page, this.labelWhenOutOfStock, label); + } +} + +export default new CombinationsTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabDescription.ts b/src/versions/develop/pages/BO/catalog/products/create/tabDescription.ts new file mode 100644 index 00000000..d5768981 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabDescription.ts @@ -0,0 +1,529 @@ +import {ProductImageInformation} from '@data/types/product'; +import FakerProduct from '@data/faker/product'; +import { BOCatalogProductsCreateTabDescriptionPageInterface } from '@interfaces/BO/catalog/products/create/tabDescription'; +import BOBasePage from '@pages/BO/BOBasePage'; +import { Page } from '@playwright/test'; + +/** + * Description tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class DescriptionTab extends BOBasePage implements BOCatalogProductsCreateTabDescriptionPageInterface { + public readonly settingUpdatedMessage: string; + + private readonly descriptionTabLink: string; + + private readonly productImageDropZoneDiv: string; + + private readonly imagePreviewBlock: string; + + private readonly imagePreviewCover: string; + + private readonly productImage: string; + + private readonly productImageContainer: string; + + private readonly productImageDropZoneWindow: string; + + private readonly productImageDropZoneCover: string; + + private readonly productImageDropZoneBtnLang: string; + + private readonly productImageDropZoneDropdown: string; + + private readonly productImageDropZoneDropdownItem: (locale: string) => string; + + private readonly productImageDropZoneCaption: string; + + private readonly productImageDropZoneBtnSubmit: string; + + private readonly productImageDropZoneSelectAllLink: string; + + private readonly productImageDropZoneCloseButton: string; + + private readonly productImageDropZoneZoomIcon: string; + + private readonly productImageDropZoneZoomImage: string; + + private readonly productImageDropZoneCloseZoom: string; + + private readonly productImageDropZoneReplaceImageSelection: string; + + private readonly productImageDropZoneDeleteImageSelection: string; + + private readonly applyDeleteImageButton: string; + + private readonly productSummary: string; + + private readonly productSummaryTabLocale: (locale: string) => string; + + private readonly productSummaryTabContent: (locale: string) => string; + + private readonly productDescription: string; + + private readonly productDescriptionSourceCodeLink: string; + + private readonly productDescriptionTextBox: string; + + private readonly productDescriptionTextBoxSaveButton: string; + + private readonly productDescriptionTabLocale: (locale: string) => string; + + private readonly productDescriptionTabContent: (locale: string) => string; + + private readonly productDefaultCategory: string; + + private readonly addCategoryButton: string; + + private readonly addCategoryInput: string; + + private readonly applyCategoryButton: string; + + private readonly categoriesList: string; + + private readonly defaultCategorySelectButton: string; + + private readonly defaultCategoryList: (categoryRow: number) => string; + + private readonly deleteCategoryIcon: (categoryRow: number) => string; + + private readonly productManufacturer: string; + + private readonly productManufacturerSelectButton: string; + + private readonly relatedProductSelectButton: string; + + private readonly productManufacturerList: (brandRow: number) => string; + + /** + * @constructs + * Setting up texts and selectors to use on description tab + */ + constructor() { + super(); + + // Message + this.settingUpdatedMessage = 'Settings updated'; + + // Selectors in description tab + this.descriptionTabLink = '#product_description-tab-nav'; + // Image selectors + this.productImageDropZoneDiv = '#product-images-dropzone'; + this.imagePreviewBlock = `${this.productImageDropZoneDiv} div.dz-preview.openfilemanager`; + this.imagePreviewCover = `${this.productImageDropZoneDiv} div.dz-preview.is-cover`; + this.productImage = `${this.productImageDropZoneDiv} div.dz-preview.dz-image-preview.dz-complete`; + this.productImageContainer = '#product-images-container'; + this.productImageDropZoneWindow = `${this.productImageContainer} .dropzone-window`; + this.productImageDropZoneCover = `${this.productImageDropZoneWindow} #is-cover-checkbox`; + this.productImageDropZoneBtnLang = `${this.productImageDropZoneWindow} #product_dropzone_lang`; + this.productImageDropZoneDropdown = `${this.productImageDropZoneWindow} .locale-dropdown-menu.show`; + this.productImageDropZoneDropdownItem = (locale: string) => `${this.productImageDropZoneDropdown} span` + + `[data-locale="${locale}"]`; + this.productImageDropZoneCaption = `${this.productImageDropZoneWindow} #caption-textarea`; + this.productImageDropZoneBtnSubmit = `${this.productImageDropZoneWindow} button.save-image-settings`; + this.productImageDropZoneSelectAllLink = `${this.productImageDropZoneWindow} p.dropzone-window-select`; + this.productImageDropZoneCloseButton = `${this.productImageDropZoneWindow} div.dropzone-window-header-right` + + ' i[data-original-title="Close window"]'; + this.productImageDropZoneZoomIcon = `${this.productImageDropZoneWindow} div.dropzone-window-header-right` + + ' i[data-original-title="Zoom on selection"]'; + this.productImageDropZoneZoomImage = `${this.productImageContainer} div.pswp--open.pswp--visible`; + this.productImageDropZoneCloseZoom = `${this.productImageContainer} button.pswp__button--close`; + this.productImageDropZoneReplaceImageSelection = `${this.productImageContainer} div.dropzone-window-header-right` + + ' i[data-original-title="Replace selection"]'; + this.productImageDropZoneDeleteImageSelection = `${this.productImageContainer} div.dropzone-window-header-right` + + ' i[data-original-title="Delete selection"]'; + this.applyDeleteImageButton = `${this.productImageContainer} footer button.btn-primary`; + // Description & summary selectors + this.productSummary = '#product_description_description_short'; + this.productSummaryTabLocale = (locale: string) => `${this.productSummary} a[data-locale="${locale}"]`; + this.productSummaryTabContent = (locale: string) => `${this.productSummary} div.panel[data-locale="${locale}"]`; + this.productDescription = '#product_description_description'; + this.productDescriptionTabLocale = (locale: string) => `${this.productDescription} a[data-locale="${locale}"]`; + this.productDescriptionTabContent = (locale: string) => `${this.productDescription} div.panel[data-locale="${locale}"]`; + this.productDescriptionSourceCodeLink = `${this.productDescription} div.mce-widget[aria-label='Source code']`; + this.productDescriptionTextBox = '.mce-textbox'; + this.productDescriptionTextBoxSaveButton = '.mce-window div.mce-first button[type="button"]'; + // Categories selectors + this.productDefaultCategory = '#product_description_categories_default_category_id'; + this.addCategoryButton = '#product_description_categories_add_categories_btn'; + this.addCategoryInput = '#ps-select-product-category'; + this.applyCategoryButton = '#category_tree_selector_apply_btn'; + this.categoriesList = '#product_description_categories_product_categories'; + this.defaultCategorySelectButton = '#select2-product_description_categories_default_category_id-container'; + this.defaultCategoryList = (categoryRow: number) => '#select2-product_description_categories_default_category_id-results' + + ` li:nth-child(${categoryRow})`; + this.deleteCategoryIcon = (categoryRow: number) => `#product_description_categories_product_categories_${categoryRow}_name` + + ' + a.pstaggerClosingCross:not(.d-none)'; + // Brand selectors + this.productManufacturer = '#product_description_manufacturer'; + this.productManufacturerSelectButton = '#select2-product_description_manufacturer-container'; + this.productManufacturerList = (BrandRow: number) => '#select2-product_description_manufacturer-results' + + ` li:nth-child(${BrandRow})`; + // Related product selectors + this.relatedProductSelectButton = '#product_description_related_products_search_input'; + } + + /* + Methods + */ + + /** + * Get Number of images to set on the product + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getNumberOfImages(page: Page): Promise { + return page.locator(this.productImage).count(); + } + + /** + * Upload product image + * @param page {Page} Browser tab + * @param imagesPaths {Array} Paths of the images to add to the product + * @returns {Promise} + */ + async uploadProductImages(page: Page, imagesPaths: any[] = []): Promise { + const filteredImagePaths = imagesPaths.filter((el) => el !== null); + + if (filteredImagePaths !== null && filteredImagePaths.length !== 0) { + const numberOfImages = await this.getNumberOfImages(page); + await this.uploadOnFileChooser( + page, + numberOfImages === 0 ? this.productImageDropZoneDiv : this.imagePreviewBlock, + filteredImagePaths, + ); + } + } + + /** + * Add product images + * @param page {Page} Browser tab + * @param imagesPaths {Array} Paths of the images to add to the product + * @returns {Promise} + */ + async addProductImages(page: Page, imagesPaths: any[] = []): Promise { + const filteredImagePaths = imagesPaths.filter((el) => el !== null); + + if (filteredImagePaths !== null && filteredImagePaths.length !== 0) { + const numberOfImages = await this.getNumberOfImages(page); + await this.waitForVisibleSelector(page, numberOfImages === 0 ? this.productImageDropZoneDiv : this.imagePreviewBlock); + await this.uploadOnFileChooser( + page, + numberOfImages === 0 ? this.productImageDropZoneDiv : this.imagePreviewBlock, + filteredImagePaths, + ); + + await this.waitForVisibleSelector(page, this.imagePreviewBlock); + await this.waitForVisibleLocator(page.locator(this.productImage).nth(numberOfImages + filteredImagePaths.length - 1)); + } + } + + /** + * Get Product Image Information + * @param page {Page} Browser tab + * @param numImage {number} Number of the image + * @returns {Promise} + */ + async getProductImageInformation(page: Page, numImage: number): Promise { + await page.locator(this.productImage).nth(numImage - 1).click(); + + const isCover = await page.locator(this.productImageDropZoneCover).isChecked(); + + await page.locator(this.productImageDropZoneBtnLang).click(); + await this.elementVisible(page, this.productImageDropZoneDropdown); + await page.locator(this.productImageDropZoneDropdownItem('en')).click(); + const captionEN = await page + .locator(this.productImageDropZoneCaption) + .evaluate((node: HTMLTextAreaElement): string => node.value); + + await page.locator(this.productImageDropZoneBtnLang).click(); + await this.elementVisible(page, this.productImageDropZoneDropdown); + await page.locator(this.productImageDropZoneDropdownItem('fr')).click(); + const captionFR = await page + .locator(this.productImageDropZoneCaption) + .evaluate((node: HTMLTextAreaElement): string => node.value); + + await page.locator(this.productImageDropZoneCloseButton).click(); + + return { + id: parseInt(await page.locator(this.productImage).nth(numImage - 1).getAttribute('data-id') ?? '0', 10), + isCover, + position: numImage, + caption: { + en: captionEN, + fr: captionFR, + }, + }; + } + + /** + * Set Product Image Information + * @param page {Page} Browser tab + * @param numImage {number} Number of the image + * @param useAsCoverImage {boolean|undefined} Use as cover image + * @param captionEn {string|undefined} Caption in English + * @param captionFr {string|undefined} Caption in French + * @param selectAll {boolean|undefined} Select all + * @param toSave {boolean} True if we need to save + * @param toClose {boolean} True if we need to close + * @returns {Promise} + */ + async setProductImageInformation( + page: Page, + numImage: number, + useAsCoverImage: boolean | undefined, + captionEn: string | undefined, + captionFr: string | undefined, + selectAll: boolean | undefined = undefined, + toSave: boolean = true, + toClose: boolean = false, + ): Promise { + let returnValue: string|null = null; + // Select the image + await page.locator(this.productImage).nth(numImage - 1).click(); + + if (selectAll) { + await page.locator(this.productImageDropZoneSelectAllLink).click(); + } + + if (useAsCoverImage) { + await this.setCheckedWithIcon(page, this.productImageDropZoneCover, useAsCoverImage); + } + if (captionEn) { + await page.locator(this.productImageDropZoneBtnLang).click(); + await this.elementVisible(page, this.productImageDropZoneDropdown); + + await page.locator(this.productImageDropZoneDropdownItem('en')).click(); + await this.setValue(page, this.productImageDropZoneCaption, captionEn); + } + if (captionFr) { + await page.locator(this.productImageDropZoneBtnLang).click(); + await this.elementVisible(page, this.productImageDropZoneDropdown); + + await page.locator(this.productImageDropZoneDropdownItem('fr')).click(); + await this.setValue(page, this.productImageDropZoneCaption, captionFr); + } + + if (toSave) { + await page.locator(this.productImageDropZoneBtnSubmit).click(); + + returnValue = await this.getGrowlMessageContent(page); + } + if (toClose) { + await page.locator(this.productImageDropZoneCloseButton).click(); + } + return returnValue; + } + + /** + * Click on magnifying glass + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnMagnifyingGlass(page: Page): Promise { + await page.locator(this.productImageDropZoneZoomIcon).click(); + + return this.elementVisible(page, this.productImageDropZoneZoomImage, 1000); + } + + /** + * Close image zoom + * @param page {Page} Browser tab + * @returns {Promise} + */ + async closeImageZoom(page: Page): Promise { + await page.locator(this.productImageDropZoneCloseZoom).click(); + + return this.elementNotVisible(page, this.productImageDropZoneZoomImage, 1000); + } + + /** + * Replace image selection + * @param page {Page} Browser tab + * @param image {string} Browser tab + * @returns {Promise} + */ + async replaceImageSelection(page: Page, image: string): Promise { + await this.uploadOnFileChooser(page, this.productImageDropZoneReplaceImageSelection, [image]); + await page.locator(this.productImageDropZoneBtnSubmit).click(); + + return this.getGrowlMessageContent(page); + } + + /** + * Delete image + * @param page {Page} Browser tab + * @returns {Promise} + */ + async deleteImage(page: Page): Promise { + await this.closeGrowlMessage(page); + await page.locator(this.productImageDropZoneDeleteImageSelection).click(); + await page.locator(this.applyDeleteImageButton).click(); + + return this.getGrowlMessageContent(page); + } + + /** + * Set value on tinyMce textarea + * @param page {Page} Browser tab + * @param selector {string} Value of selector to use + * @param value {string} Text to set on tinymce input + * @returns {Promise} + */ + async setValueOnTinymceInput(page: Page, selector: string, value: string): Promise { + // Select all + await page.locator(`${selector} .mce-edit-area`).click({clickCount: 3}); + + // Delete all text + await page.keyboard.press('Backspace'); + + // Fill the text + await page.keyboard.type(value); + } + + /** + * Set description + * @param page {Page} Browser tab + * @param description {string} Data to set in description textarea + */ + async setDescription(page: Page, description: string): Promise { + await page.locator(this.productDescriptionTabLocale('en')).click(); + await this.elementVisible(page, `${this.productDescriptionTabLocale('en')}.active`); + await this.setValueOnTinymceInput(page, this.productDescriptionTabContent('en'), description); + } + + /** + * Set product description + * @param page {Page} Browser tab + * @param productData {FakerProduct} Data to set in description form + * @returns {Promise} + */ + async setProductDescription(page: Page, productData: FakerProduct): Promise { + await this.waitForSelectorAndClick(page, this.descriptionTabLink); + + await this.addProductImages(page, [productData.coverImage, productData.thumbImage]); + + await page.locator(this.productSummaryTabLocale('en')).click(); + await this.elementVisible(page, `${this.productSummaryTabLocale('en')}.active`); + await this.setValueOnTinymceInput(page, this.productSummaryTabContent('en'), productData.summary); + + await this.setDescription(page, productData.description); + } + + /** + * Set iframe in description + * @param page {Page} Browser tab + * @param description {string} Data to set in the description + */ + async setIframeInDescription(page: Page, description: string): Promise { + await this.waitForSelectorAndClick(page, this.descriptionTabLink); + await page.locator(this.productDescriptionSourceCodeLink).first().click(); + await this.setValue(page, this.productDescriptionTextBox, description); + await page.locator(this.productDescriptionTextBoxSaveButton).click(); + } + + /** + * Get Product ID Image Cover + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getProductIDImageCover(page: Page): Promise { + return parseInt(await this.getAttributeContent(page, this.imagePreviewCover, 'data-id'), 10); + } + + /** + * Returns the value of a form element + * @param page {Page} + * @param inputName {string} + * @param languageId {string | undefined} + */ + async getValue(page: Page, inputName: string, languageId?: string): Promise { + switch (inputName) { + case 'description': + return this.getTextContent(page, `${this.productDescription}_${languageId}`, false); + case 'id_category_default': + return page.locator(this.productDefaultCategory).evaluate((node: HTMLSelectElement) => node.value); + case 'manufacturer': + return page.locator(this.productManufacturer).evaluate((node: HTMLSelectElement) => node.value); + case 'summary': + return this.getTextContent(page, `${this.productSummary}_${languageId}`, false); + default: + throw new Error(`Input ${inputName} was not found`); + } + } + + /** + * Add new category + * @param page {Page} Browser tab + * @param categories {string[]} Browser tab + * @returns {Promise} + */ + async addNewCategory(page: Page, categories: string[]): Promise { + await page.locator(this.addCategoryButton).click(); + await this.waitForVisibleSelector(page, this.addCategoryInput); + for (let i: number = 0; i < categories.length; i++) { + await page.locator(this.addCategoryInput).pressSequentially(categories[i]); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + } + + await this.waitForSelectorAndClick(page, this.applyCategoryButton); + } + + /** + * Get selected categories + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getSelectedCategories(page: Page): Promise { + return this.getTextContent(page, this.categoriesList); + } + + /** + * Get selected categories + * @param page {Page} Browser tab + * @param categoryRow {number} Category row + * @returns {Promise} + */ + async chooseDefaultCategory(page: Page, categoryRow: number): Promise { + await page.locator(this.defaultCategorySelectButton).click(); + await page.locator(this.defaultCategoryList(categoryRow)).click(); + } + + /** + * Is delete category icon visible + * @param page {Page} Browser tab + * @param categoryRow {number} Category row + * @returns {Promise} + */ + async isDeleteCategoryIconVisible(page: Page, categoryRow: number): Promise { + return (await page.locator(this.deleteCategoryIcon(categoryRow)).count()) !== 0; + } + + /** + * Is delete category icon visible + * @param page {Page} Browser tab + * @param brandRow {number} Brand row + * @returns {Promise} + */ + async chooseBrand(page: Page, brandRow: number): Promise { + await page.locator(this.productManufacturerSelectButton).click(); + await page.locator(this.productManufacturerList(brandRow)).click(); + } + + /** + * Add related product + * @param page {Page} Browser tab + * @param productName {string} Product name + * @returns {Promise} + */ + async addRelatedProduct(page: Page, productName: string): Promise { + await page.locator(this.relatedProductSelectButton).fill(productName); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + } +} + +export default new DescriptionTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabDetails.ts b/src/versions/develop/pages/BO/catalog/products/create/tabDetails.ts new file mode 100644 index 00000000..60889e47 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabDetails.ts @@ -0,0 +1,424 @@ +// Import pages +import BOBasePage from '@pages/BO/BOBasePage'; +import boProductsCreatePage from '@pages/BO/catalog/products/create'; + +// Import data +import type ProductData from '@data/faker/product'; +import {ProductFeatures} from '@data/types/product'; + +import type {Frame, Page} from 'playwright'; +import { BOCatalogProductsCreateTabDetailsPageInterface } from '@interfaces/BO/catalog/products/create/tabDetails'; +import { expect } from '@playwright/test'; + +/** + * Details tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class DetailsTab extends BOBasePage implements BOCatalogProductsCreateTabDetailsPageInterface{ + public readonly featureCustomValueNotDefaultLanguageMessage: string; + + private readonly detailsTabLink: string; + + private readonly productEAN13Input: string; + + private readonly productISBNInput: string; + + private readonly productMPNInput: string; + + private readonly productReferenceInput: string; + + private readonly productUPCInput: string; + + private readonly productConditionSelect: string; + + private readonly confirmDeleteFeatureButton: string; + + private readonly tableFeatures: string; + + private readonly tableFeaturesRow: (nthChild: number) => string; + + private readonly tableFeaturesCellAction: (nthChild: number) => string; + + private readonly tableFeaturesBtnDelete: (nthChild: number) => string; + + private readonly manageFeaturesLink: string; + + private readonly manageAllFilesLink: string; + + private readonly searchFileInput: string; + + private readonly searchFileResult: string; + + private readonly addNewFileButton: string; + + private readonly createFileFrame: string; + + private readonly fileNameInput: string; + + private readonly fileDescriptionInput: string; + + private readonly attachFileButton: string; + + private readonly saveFileButton: string; + + private readonly deleteFileConfirmButton: string; + + private readonly noFileAttachedErrorAlert: string; + + private readonly addNewCustomizationButton: string; + + private readonly confirmDeleteCustomizationButton: string; + + private readonly deleteCustomizationModal: string; + + private readonly deleteFileModal: string; + + private readonly addFeatureFeatureSelect: string; + + private readonly addFeatureValueSelect: string; + + private readonly addFeatureValueButtonLang: string; + + private readonly addFeatureValueSpanLang: (lang: string) => string; + + private readonly addFeatureValueInputLang: (langId: number) => string; + + private readonly addFeatureButton: string; + + private readonly deleteFeatureModal: string; + + private readonly deleteFileIcon: (row: number) => string; + + private readonly displayCondition: (row: number) => string; + + private readonly customizationNameInput: (row: number) => string; + + private readonly customizationTypeSelect: (row: number) => string; + + private readonly deleteCustomizationIcon: (row: number) => string; + + private readonly customizationRequiredButton: (row: number, toEnable: number) => string; + + /** + * @constructs + * Setting up texts and selectors to use on details tab + */ + constructor() { + super(); + + // Messages + this.featureCustomValueNotDefaultLanguageMessage = 'The field "Custom value" is required at least in your default language.'; + + // Selectors in details tab + // References section + this.detailsTabLink = '#product_details-tab-nav'; + this.productReferenceInput = '#product_details_references_reference'; + this.productMPNInput = '#product_details_references_mpn'; + this.productUPCInput = '#product_details_references_upc'; + this.productEAN13Input = '#product_details_references_ean_13'; + this.productISBNInput = '#product_details_references_isbn'; + // Features section + this.addFeatureFeatureSelect = '#product_details_features_feature_id'; + this.addFeatureValueSelect = '#product_details_features_feature_value_id'; + this.addFeatureValueButtonLang = '#product_details_features_custom_value_dropdown'; + this.addFeatureValueSpanLang = (lang: string) => `${this.addFeatureValueButtonLang} + div.dropdown-menu.show` + + `> span[data-locale='${lang}']`; + this.addFeatureValueInputLang = (langId: number) => `#product_details_features_custom_value_${langId}`; + this.addFeatureButton = '#product_details_features_add_feature'; + this.deleteFeatureModal = '#modal-confirm-delete-feature-value'; + this.confirmDeleteFeatureButton = `${this.deleteFeatureModal} div.modal-footer button.btn-confirm-submit`; + this.tableFeatures = '#product_details_features_feature_collection tbody'; + this.tableFeaturesRow = (nthChild: number) => `${this.tableFeatures} tr:nth-child(${nthChild})`; + this.tableFeaturesCellAction = (nthChild: number) => `${this.tableFeaturesRow(nthChild)} td.feature-actions`; + this.tableFeaturesBtnDelete = (nthChild: number) => `${this.tableFeaturesCellAction(nthChild)} button`; + this.manageFeaturesLink = 'div.product-features-controls + div > a'; + // Attached files section + this.manageAllFilesLink = '#product_details div.small.font-secondary a[href*=\'sell/attachments/\']'; + this.searchFileInput = '#product_details_attachments_attached_files_search_input'; + this.searchFileResult = '#product_details_attachments_attached_files div.search-with-icon span div'; + this.addNewFileButton = '#product_details_attachments_add_attachment_btn'; + this.createFileFrame = '#modal-create-product-attachment'; + this.fileNameInput = '#attachment_name_1'; + this.fileDescriptionInput = '#attachment_file_description_1'; + this.attachFileButton = '#attachment_file'; + this.saveFileButton = '#main-div div.card-footer button'; + this.deleteFileIcon = (row: number) => `#product_details_attachments_attached_files_${row} i.entity-item-delete`; + this.deleteFileModal = '#modal-confirm-remove-entity'; + this.deleteFileConfirmButton = `${this.deleteFileModal} div.modal-footer button.btn-confirm-submit`; + this.noFileAttachedErrorAlert = '#product_details_attachments_attached_files div.alert-info p.alert-text'; + // Display condition section + this.displayCondition = (toEnable: number) => `#product_details_show_condition_${toEnable}`; + this.productConditionSelect = '#product_details_condition'; + // Customization section + this.addNewCustomizationButton = '#product_details_customizations_add_customization_field'; + this.customizationNameInput = (row: number) => `#product_details_customizations_customization_fields_${row}_name_1`; + this.customizationTypeSelect = (row: number) => `#product_details_customizations_customization_fields_${row}_type`; + this.customizationRequiredButton = (row: number, toEnable: number) => '#product_details_customizations_customization' + + `_fields_${row}_required_${toEnable}`; + this.deleteCustomizationIcon = (row: number) => `#product_details_customizations_customization_fields_${row}_remove` + + ' i.material-icons'; + this.deleteCustomizationModal = '#modal-confirm-delete-customization'; + this.confirmDeleteCustomizationButton = `${this.deleteCustomizationModal} div.modal-footer button.btn-confirm-submit`; + } + + /* + Methods + */ + /** + * Set product details + * @param page {Page} Browser tab + * @param productData {ProductData} Data to set in details form + * @returns {Promise} + */ + async setProductDetails(page: Page, productData: ProductData): Promise { + await this.waitForSelectorAndClick(page, this.detailsTabLink); + await this.setValue(page, this.productReferenceInput, productData.reference); + } + + /** + * Set value for EAN 13 + * @param page {Page} Browser tab + * @param value {string} Value + * @returns {Promise} + */ + async setEAN13(page: Page, value: string): Promise { + await this.setValue(page, this.productEAN13Input, value); + } + + /** + * Set value for setISBN + * @param page {Page} Browser tab + * @param value {string} Value + * @returns {Promise} + */ + async setISBN(page: Page, value: string): Promise { + await this.setValue(page, this.productISBNInput, value); + } + + /** + * Set value for MPN + * @param page {Page} Browser tab + * @param value {string} Value + * @returns {Promise} + */ + async setMPN(page: Page, value: string): Promise { + await this.setValue(page, this.productMPNInput, value); + } + + /** + * Set value for UPC + * @param page {Page} Browser tab + * @param value {string} Value + * @returns {Promise} + */ + async setUPC(page: Page, value: string): Promise { + await this.setValue(page, this.productUPCInput, value); + } + + /** + * Get error message in references form + * @param page {Page} Browser tab + * @param inputNumber {number} Input number to get error message + * @returns {Promise} + */ + async getErrorMessageInReferencesForm(page: Page, inputNumber: number): Promise { + await this.clickAndWaitForLoadState(page, boProductsCreatePage.saveProductButton); + + return this.getTextContent(page, `#product_details_references div:nth-child(${inputNumber}) div.alert-text`); + } + + /** + * Set feature + * @param page {Page} Browser tab + * @param productFeatures {ProductFeatures[]} Data to set on feature form + * @returns {Promise} + */ + async setFeature(page: Page, productFeatures: ProductFeatures[]): Promise { + for (let i: number = 0; i < productFeatures.length; i++) { + await this.selectByVisibleText(page, this.addFeatureFeatureSelect, productFeatures[i].featureName, true); + await this.waitForVisibleSelector(page, `${this.addFeatureValueSelect}:not([disabled])`); + + if (productFeatures[i].preDefinedValue) { + await this.selectByVisibleText(page, this.addFeatureValueSelect, productFeatures[i].preDefinedValue!, true); + } + if (productFeatures[i].customizedValueEn || productFeatures[i].customizedValueFr) { + await this.selectByValue(page, this.addFeatureValueSelect, -1, true); + if (productFeatures[i].customizedValueEn) { + await page.locator(this.addFeatureValueButtonLang).click(); + await page.locator(this.addFeatureValueSpanLang('en')).click(); + await this.waitForVisibleSelector(page, this.addFeatureValueInputLang(1)); + await this.setValue(page, this.addFeatureValueInputLang(1), productFeatures[i].customizedValueEn!); + } + if (productFeatures[i].customizedValueFr) { + await page.locator(this.addFeatureValueButtonLang).click(); + await page.locator(this.addFeatureValueSpanLang('fr')).click(); + await this.waitForVisibleSelector(page, this.addFeatureValueInputLang(2)); + await this.setValue(page, this.addFeatureValueInputLang(2), productFeatures[i].customizedValueFr!); + } + } + + await this.waitForVisibleSelector(page, `${this.addFeatureButton}:not([disabled])`); + await page.locator(this.addFeatureButton).click(); + } + } + + /** + * Delete all features + * @param page {Page} Browser tab + * @param productFeatures {ProductFeatures[]} Data to delete feature + * @returns {Promise} + */ + async deleteFeatures(page: Page, productFeatures: ProductFeatures[]): Promise { + for (let i: number = 0; i < productFeatures.length; i++) { + // Why tr:nth-child(2) : It's one-based selector and the first row is hidden ? + await this.waitForSelectorAndClick(page, this.tableFeaturesBtnDelete(2)); + await this.waitForSelectorAndClick(page, this.confirmDeleteFeatureButton); + } + } + + /** + * Click on "Manage Features" + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickonManageFeatures(page: Page): Promise { + return this.openLinkWithTargetBlank(page, this.manageFeaturesLink); + } + + /** + * Click on manage all files + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnManageAllFiles(page: Page): Promise { + return this.openLinkWithTargetBlank(page, this.manageAllFilesLink); + } + + /** + * Search file + * @param page {Page} Browser tab + * @param fileName {string} File name to search + * @returns {Promise} + */ + async searchFile(page: Page, fileName: string): Promise { + await this.setValue(page, this.searchFileInput, fileName); + await page.waitForTimeout(2000); + + return this.getTextContent(page, this.searchFileResult); + } + + /** + * Add new file + * @param page {Page} Browser tab + * @param productData {ProductData} Data to set on add file form + * @returns {Promise} + */ + async addNewFile(page: Page, productData: ProductData): Promise { + for (let i: number = 0; i < productData.files.length; i++) { + await this.waitForSelectorAndClick(page, this.addNewFileButton); + + await this.waitForVisibleSelector(page, this.createFileFrame); + + const newFileFrame: Frame | null = page.frame({name: 'modal-create-product-attachment-iframe'}); + expect(newFileFrame).not.toBeNull(); + + await this.setValue(newFileFrame!, this.fileNameInput, productData.files[i].fileName); + await this.setValue(newFileFrame!, this.fileDescriptionInput, productData.files[i].description); + await this.uploadFile(newFileFrame!, this.attachFileButton, productData.files[i].file); + await newFileFrame!.locator(this.saveFileButton).click(); + } + } + + /** + * Delete all files + * @param page {Page} Browser tab + * @param productData {ProductData} Data to delete file + * @returns {Promise} + */ + async deleteFiles(page: Page, productData: ProductData): Promise { + for (let i: number = 0; i < productData.files.length; i++) { + await this.waitForSelectorAndClick(page, this.deleteFileIcon(i)); + await this.waitForSelectorAndClick(page, this.deleteFileConfirmButton); + } + } + + /** + * Get no file attached message + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getNoFileAttachedMessage(page: Page): Promise { + return this.getTextContent(page, this.noFileAttachedErrorAlert); + } + + /** + * Set condition + * @param page {Page} Browser tab + * @param productData {ProductData} Data to set condition + * @returns {Promise} + */ + async setCondition(page: Page, productData: ProductData): Promise { + await this.setChecked(page, this.displayCondition(productData.displayCondition ? 1 : 0)); + await this.selectByVisibleText(page, this.productConditionSelect, productData.condition); + } + + /** + * Add new customization + * @param page {Page} Browser tab + * @param productData {ProductData} Data to add customization + * @returns {Promise} + */ + async addNewCustomizations(page: Page, productData: ProductData): Promise { + await this.waitForSelectorAndClick(page, this.detailsTabLink); + for (let i: number = 0; i < productData.customizations.length; i++) { + await this.waitForSelectorAndClick(page, this.addNewCustomizationButton); + + await this.setValue(page, this.customizationNameInput(i), productData.customizations[i].label); + await this.selectByVisibleText(page, this.customizationTypeSelect(i), productData.customizations[i].type); + await this.setChecked(page, this.customizationRequiredButton(i, productData.customizations[i].required ? 1 : 0)); + } + } + + /** + * Delete all customizations + * @param page {Page} Browser tab + * @param productData {ProductData} Data to delete customizations + * @returns {Promise} + */ + async deleteCustomizations(page: Page, productData: ProductData): Promise { + for (let i: number = 0; i < productData.customizations.length; i++) { + await this.waitForSelectorAndClick(page, this.deleteCustomizationIcon(i)); + await this.waitForSelectorAndClick(page, this.confirmDeleteCustomizationButton); + } + } + + /** + * @param page {Page} + * @param inputName {string} + */ + async getValue(page: Page, inputName: string): Promise { + switch (inputName) { + case 'condition': + return page + .locator(this.productConditionSelect) + .evaluate((el: HTMLSelectElement) => el.value); + case 'mpn': + return this.getAttributeContent(page, this.productMPNInput, 'value'); + case 'reference': + return this.getAttributeContent(page, this.productReferenceInput, 'value'); + case 'upc': + return this.getAttributeContent(page, this.productUPCInput, 'value'); + case 'ean13': + return this.getAttributeContent(page, this.productEAN13Input, 'value'); + case 'isbn': + return this.getAttributeContent(page, this.productISBNInput, 'value'); + case 'show_condition': + return (await this.isChecked(page, this.displayCondition(1))) ? '1' : '0'; + default: + throw new Error(`Input ${inputName} was not found`); + } + } +} + +export default new DetailsTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabOptions.ts b/src/versions/develop/pages/BO/catalog/products/create/tabOptions.ts new file mode 100644 index 00000000..3dabc5c0 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabOptions.ts @@ -0,0 +1,153 @@ +// Import pages +import BOBasePage from '@pages/BO/BOBasePage'; + +import type {Page} from 'playwright'; + +/** + * Options tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class OptionsTab extends BOBasePage { + private readonly productVisibilityRadio: string; + + private readonly productAvailableForOrderRadio: (toEnable: number) => string; + + private readonly productShowPricesRadio: (toEnable: number) => string; + + private readonly productOnlineOnlyRadio: (toEnable: number) => string; + + private readonly productOptionVisibilityRadio: (option: number) => string; + + private readonly supplierAssociatedCheckBox: (row: number) => string; + + private readonly defaultSupplierSection: string; + + private readonly supplierReferencesSection: string; + + /** + * @constructs + * Setting up texts and selectors to use on options tab + */ + constructor() { + super(); + + // Selectors in options tab + this.productOptionVisibilityRadio = (option: number) => `#product_options_visibility_visibility_${option}`; + this.productVisibilityRadio = 'input[name="product[options][visibility][visibility]"]'; + this.productAvailableForOrderRadio = (toEnable: number) => `#product_options_visibility_available_for_order_${toEnable}`; + this.productShowPricesRadio = (toEnable: number) => `#product_options_visibility_show_price_${toEnable}`; + this.productOnlineOnlyRadio = (toEnable: number) => `#product_options_visibility_online_only_${toEnable}`; + this.supplierAssociatedCheckBox = (row: number) => `#product_options_suppliers_supplier_ids div:nth-child(${row}) div`; + this.defaultSupplierSection = '#product_options_suppliers_default_supplier_id'; + this.supplierReferencesSection = '#product_options_product_suppliers'; + } + + /* + Methods + */ + /** + * Set visibility + * @param page {page} Browser tab + * @param visibility {string} Option to choose + * @returns {Promise} + */ + async setVisibility(page: Page, visibility: string): Promise { + switch (visibility) { + case 'everywhere': + await this.setChecked(page, this.productOptionVisibilityRadio(0)); + break; + case 'catalog_only': + await this.setChecked(page, this.productOptionVisibilityRadio(1)); + break; + case 'search_only': + await this.setChecked(page, this.productOptionVisibilityRadio(2)); + break; + case 'nowhere': + await this.setChecked(page, this.productOptionVisibilityRadio(3)); + break; + default: + throw new Error(`Option ${visibility} was not found`); + } + } + + /** + * Set available for order + * @param page {page} Browser tab + * @param toEnable {boolean} True if we need to enable available for order + * @returns {Promise} + */ + async setAvailableForOrder(page: Page, toEnable: boolean): Promise { + await this.setChecked(page, this.productAvailableForOrderRadio(toEnable ? 1 : 0)); + } + + /** + * Set show price + * @param page {page} Browser tab + * @param toEnable {boolean} True if we need to enable show price + * @returns {Promise} + */ + async setShowPrice(page: Page, toEnable: boolean): Promise { + await this.setChecked(page, this.productShowPricesRadio(toEnable ? 1 : 0)); + } + + /** + * Set web only + * @param page {page} Browser tab + * @param toEnable {boolean} True if we need to enable web only + * @returns {Promise} + */ + async setWebOnly(page: Page, toEnable: boolean): Promise { + await this.setChecked(page, this.productOnlineOnlyRadio(toEnable ? 1 : 0)); + } + + /** + * Choose supplier + * @param page {page} Browser tab + * @param supplierRow {number} Supplier to choose + * @returns {Promise} + */ + async chooseSupplier(page: Page, supplierRow: number): Promise { + await this.waitForSelectorAndClick(page, this.supplierAssociatedCheckBox(supplierRow)); + } + + /** + * Is default supplier section visible + * @param page {page} Browser tab + * @returns {Promise} + */ + async isDefaultSupplierSectionVisible(page: Page): Promise { + return this.elementVisible(page, this.defaultSupplierSection, 1000); + } + + /** + * Is supplier references section visible + * @param page {page} Browser tab + * @returns {Promise} + */ + async isSupplierReferencesSectionVisible(page: Page): Promise { + return this.elementVisible(page, this.supplierReferencesSection, 1000); + } + + /** + * Returns the value of a form element + * @param page {Page} + * @param inputName {string} + */ + async getValue(page: Page, inputName: string): Promise { + switch (inputName) { + case 'available_for_order': + return (await this.isChecked(page, this.productAvailableForOrderRadio(1))) ? '1' : '0'; + case 'online_only': + return (await this.isChecked(page, this.productShowPricesRadio(1))) ? '1' : '0'; + case 'show_price': + return (await this.isChecked(page, this.productShowPricesRadio(1))) ? '1' : '0'; + case 'visibility': + return this.getAttributeContent(page, `${this.productVisibilityRadio}[checked="checked"]`, 'value'); + default: + throw new Error(`Input ${inputName} was not found`); + } + } +} + +export default new OptionsTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabPack.ts b/src/versions/develop/pages/BO/catalog/products/create/tabPack.ts new file mode 100644 index 00000000..e6f0c192 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabPack.ts @@ -0,0 +1,363 @@ +// Import pages +import BOBasePage from '@pages/BO/BOBasePage'; + +import type {ProductPackInformation, ProductPackItem} from '@data/types/product'; + +import type {Locator, Page} from 'playwright'; +import {ProductPackOptions, ProductStockMovement} from '@data/types/product'; +import { BOCatalogProductsCreateTabPackPageInterface } from '@interfaces/BO/catalog/products/create/tabPack'; + +/** + * Pack tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class PackTab extends BOBasePage implements BOCatalogProductsCreateTabPackPageInterface { + private readonly packTabLink: string; + + private readonly searchProductInput: string; + + private readonly searchResult: string; + + private readonly packSearchResult: string; + + private readonly searchResultSuggestion: string; + + private readonly searchResultSuggestionRow: (productInSearchList: number) => string; + + private readonly listOfProducts: string; + + private readonly quantityInput: (productInList: number) => string; + + private readonly productRowInList: (productInList: number) => string; + + private readonly productInListLegend: (productInList: number) => string; + + private readonly deleteProductInListIcon: (productInList: number) => string; + + private readonly productInListImage: (productInList: number) => string; + + private readonly productInListName: (productInList: number) => string; + + private readonly productInListReference: (productInList: number) => string; + + private readonly productInListQuantity: (productInList: number) => string; + + private readonly alertDangerProductInPack: (productInList: number) => string; + + private readonly editQuantityBase: string; + + private readonly editQuantityInput: string; + + private readonly minimalQuantityInput: string; + + private readonly packStockTypeRadioButton: (buttonRow: number) => string; + + private readonly dateTimeRowInTable: (movementRow: number) => string; + + private readonly employeeRowInTable: (movementRow: number) => string; + + private readonly quantityRowInTable: (movementRow: number) => string; + + private readonly modalDeleteProduct: string; + + private readonly confirmDeleteButtonInModal: string; + + private readonly cancelDeleteButtonInModal: string; + + private readonly saveProductButton: string; + + /** + * @constructs + * Setting up texts and selectors to use on pack tab + */ + constructor() { + super(); + + // Selectors in pack tab + this.packTabLink = '#product_stock-tab-nav'; + // Search product selectors + this.searchProductInput = '#product_stock_packed_products_search_input'; + this.searchResult = '.tt-menu.tt-open'; + this.packSearchResult = `${this.searchResult} div.tt-dataset.tt-dataset-2`; + this.searchResultSuggestion = `${this.packSearchResult} div.search-suggestion`; + this.searchResultSuggestionRow = (productInSearchList: number) => `${this.searchResultSuggestion}:nth-child(` + + `${productInSearchList})`; + + // List of products in pack selectors + this.listOfProducts = '#product_stock_packed_products_list'; + this.quantityInput = (productInList: number) => `#product_stock_packed_products_${productInList}_quantity`; + this.productRowInList = (productInList: number) => `${this.listOfProducts} li:nth-child(${productInList})`; + this.productInListLegend = (productInList: number) => `${this.productRowInList(productInList)} div.packed-product-legend`; + this.deleteProductInListIcon = (productInList: number) => `${this.productInListLegend(productInList)} ` + + 'span i.entity-item-delete'; + this.productInListImage = (productInList: number) => `${this.productRowInList(productInList)} div.packed-product-image img`; + this.productInListName = (productInList: number) => `#product_stock_packed_products_${productInList - 1}_name`; + this.productInListReference = (productInList: number) => `${this.productInListLegend(productInList)} span.reference-preview`; + this.productInListQuantity = (productInList: number) => `#product_stock_packed_products_${productInList - 1}_quantity`; + this.alertDangerProductInPack = (productInList: number) => `${this.productRowInList(productInList)} div.alert-danger p`; + + // Modal delete product in pack selectors + this.modalDeleteProduct = '#modal-confirm-remove-entity'; + this.confirmDeleteButtonInModal = '#modal-confirm-remove-entity button.btn-confirm-submit'; + this.cancelDeleteButtonInModal = '#modal-confirm-remove-entity button.btn-outline-secondary'; + + // Stock movement table selectors + this.dateTimeRowInTable = (movementRow: number) => `#product_stock_quantities_stock_movements_${movementRow}_date ` + + '+ span'; + this.employeeRowInTable = (movementRow: number) => `#product_stock_quantities_stock_movements_${movementRow}_` + + 'employee_name + span'; + this.quantityRowInTable = (movementRow: number) => `#product_stock_quantities_stock_movements_${movementRow}_` + + 'delta_quantity + span'; + + // Edit quantity selectors + this.editQuantityBase = 'input#product_stock_quantities_delta_quantity_quantity'; + this.editQuantityInput = '#product_stock_quantities_delta_quantity_delta'; + this.minimalQuantityInput = '#product_stock_quantities_minimal_quantity'; + this.packStockTypeRadioButton = (buttonRow: number) => `#product_stock_pack_stock_type_${buttonRow} +i`; + + // Save button selector + this.saveProductButton = '#product_footer_save'; + } + + /* + Methods + */ + + // Methods to search product + /** + * Search product to add to the pack + * @param page {Page} Browser tab + * @param productName {string} Product name to search + */ + async searchProduct(page: Page, productName: string): Promise { + await this.waitForSelectorAndClick(page, this.packTabLink); + await page.locator(this.searchProductInput).fill(productName); + await this.waitForVisibleSelector(page, this.searchResult); + await page.waitForTimeout(1000); + + return this.getTextContent(page, this.packSearchResult); + } + + /** + * Get number of searched product + * @param page {Page} Browser tab + */ + async getNumberOfSearchedProduct(page: Page): Promise { + return page.locator(`${this.packSearchResult} div`).count(); + } + + /** + * Select product from list + * @param page {Page} Browser tab + * @param productInSearchList {number} The row of product in the search list + */ + async selectProductFromList(page: Page, productInSearchList: number): Promise { + let productPosition: number = 1; + + if ((await this.getNumberOfSearchedProduct(page)) > 1) { + productPosition = productInSearchList; + } + + await this.waitForSelectorAndClick(page, this.searchResultSuggestionRow(productPosition)); + return this.elementVisible(page, this.listOfProducts, 1000); + } + + // Methods to get products in pack + /** + * Get number of product in pack + * @param page {Page} Browser tab + */ + async getNumberOfProductsInPack(page: Page): Promise { + return page.locator(`${this.listOfProducts} li`).count(); + } + + /** + * Get product in pack information + * @param page {Page} Browser tab + * @param productInList {number} The row of product in pack + */ + async getProductInPackInformation(page: Page, productInList: number): Promise { + await this.waitForVisibleSelector(page, this.listOfProducts); + return { + image: await this.getAttributeContent(page, this.productInListImage(productInList), 'src'), + name: await this.getAttributeContent(page, this.productInListName(productInList), 'value'), + reference: await this.getTextContent(page, this.productInListReference(productInList)), + quantity: parseInt(await this.getAttributeContent(page, this.productInListQuantity(productInList), 'value'), 10), + }; + } + + // Methods to add/edit products in pack + /** + * Set product in pack quantity + * @param page {Page} Browser tab + * @param productInList {number} The row of product in pack + * @param quantity {number|string} The product quantity to set + */ + async setProductQuantity(page: Page, productInList: number, quantity: number|string): Promise { + await this.setValue(page, this.quantityInput(productInList), quantity); + } + + /** + * Save and get product in pack error message + * @param page {Page} Browser tab + * @param productInList {number} The row of product in pack + */ + async saveAndGetProductInPackErrorMessage(page: Page, productInList: number): Promise { + await page.locator(this.saveProductButton).click(); + + return this.getTextContent(page, this.alertDangerProductInPack(productInList)); + } + + /** + * Add product to pack + * @param page {Page} Browser tab + * @param product {string} Value of product name to set on input + * @param quantity {number} Value of quantity to set on input + */ + async addProductToPack(page: Page, product: string, quantity: number): Promise { + await this.searchProduct(page, product); + await this.waitForSelectorAndClick(page, this.searchResultSuggestionRow(1)); + await this.waitForVisibleSelector(page, this.listOfProducts); + const numberOfProducts: number = await this.getNumberOfProductsInPack(page); + + if (quantity) { + await this.setValue(page, this.quantityInput(numberOfProducts - 1), quantity); + } + } + + /** + * Add combination + * @param page {Page} Browser tab + * @param packData {ProductPackItem[]} Data of the pack + * @returns {Promise} + */ + async setPackOfProducts(page: Page, packData: ProductPackItem[]): Promise { + await this.waitForSelectorAndClick(page, this.packTabLink); + + for (let i = 0; i < packData.length; i += 1) { + await this.addProductToPack(page, packData[i].reference, packData[i].quantity); + } + } + + /** + * Get stock movements data + * @param page {Page} Browser tab + * @param movementRow {number} Movement row in table stock movements + */ + async getStockMovement(page: Page, movementRow: number): Promise { + return { + dateTime: await this.getTextContent(page, this.dateTimeRowInTable(movementRow - 1)), + employee: await this.getTextContent(page, this.employeeRowInTable(movementRow - 1), false), + quantity: await this.getNumberFromText(page, this.quantityRowInTable(movementRow - 1)), + }; + } + + // Methods to delete products in pack + /** + * Is delete modal visible + * @param page {Page} Browser tab + */ + async isDeleteModalVisible(page: Page): Promise { + return !(await this.elementNotVisible(page, this.modalDeleteProduct, 3000)); + } + + /** + * Cancel delete product from pack + * @param page {Page} Browser tab + */ + async cancelDeleteProductFromPack(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.cancelDeleteButtonInModal); + } + + /** + * Confirm delete product from pack + * @param page {Page} Browser tab + */ + async confirmDeleteProductFromPack(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.confirmDeleteButtonInModal); + } + + /** + * Delete product from pack + * @param page {Page} Browser tab + * @param productInList {number} The row of product in pack + * @param toDelete {boolean} True if we need to delete product, false to cancel delete + */ + async deleteProduct(page: Page, productInList: number, toDelete: boolean): Promise { + await this.waitForSelectorAndClick(page, this.deleteProductInListIcon(productInList)); + + await this.waitForVisibleSelector(page, this.modalDeleteProduct); + + if (toDelete) { + await this.confirmDeleteProductFromPack(page); + return this.getAlertSuccessBlockParagraphContent(page); + } + await this.cancelDeleteProductFromPack(page); + return this.isDeleteModalVisible(page); + } + + // Methods to edit pack of products + /** + * Edit pack of products + * @param page {Page} Browser tab + * @param packData {ProductPackOptions} Data to edit pack of products + */ + async editPackOfProducts(page: Page, packData: ProductPackOptions): Promise { + await this.editQuantity(page, packData.quantity); + await this.setValue(page, this.minimalQuantityInput, packData.minimalQuantity); + await this.editPackStockType(page, packData.packQuantitiesOption); + } + + /** + * Edit quantity + * @param page {Page} Browser tab + * @param quantity {number} Quantity + */ + async editQuantity(page: Page, quantity: number): Promise { + await this.setValue(page, this.editQuantityInput, quantity); + } + + /** + * Edit quantity + * @param page {Page} Browser tab + * @param packStockType {string} Quantity + */ + async editPackStockType(page: Page, packStockType: string): Promise { + let locator: Locator; + + switch (packStockType) { + case 'Use pack quantity': + locator = page.locator(this.packStockTypeRadioButton(0)); + break; + + case 'Use quantity of products in the pack': + locator = page.locator(this.packStockTypeRadioButton(1)); + break; + + case 'Use both, whatever is lower': + locator = page.locator(this.packStockTypeRadioButton(2)); + break; + + case 'Default (Use pack quantity)': + locator = page.locator(this.packStockTypeRadioButton(3)); + break; + + default: + throw new Error(`Radio button for ${packStockType} was not found`); + } + + await locator.click(); + } + + /** + * Get the value of the stock + * @param page {Page} Browser tab + * @return > + */ + async getStockValue(page: Page): Promise { + return parseInt(await this.getAttributeContent(page, this.editQuantityBase, 'value'), 10); + } +} + +export default new PackTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabPricing.ts b/src/versions/develop/pages/BO/catalog/products/create/tabPricing.ts new file mode 100644 index 00000000..07aed986 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabPricing.ts @@ -0,0 +1,457 @@ +import type FakerProduct from '@data/faker/product'; +import type {ProductSpecificPrice} from '@data/types/product'; +import { BOCatalogProductsCreateTabPricingPageInterface } from '@interfaces/BO/catalog/products/create/tabPricing'; +import BOBasePage from '@pages/BO/BOBasePage'; +import { Page } from '@playwright/test'; + +/** + * Pricing tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class PricingTab extends BOBasePage implements BOCatalogProductsCreateTabPricingPageInterface { + private readonly pricingTabLink: string; + + private readonly retailPriceInputTaxExcl: string; + + private readonly retailPriceInputTaxIncl: string; + + private readonly taxRuleID: string; + + private readonly taxRuleSelect: string; + + private readonly taxRuleSpan: string; + + private readonly taxRuleList: string; + + private readonly wholesalePriceInput: string; + + private readonly unitPriceInput: string; + + private readonly unityInput: string; + + private readonly onSaleCheckbox: string; + + private readonly productPricingSummarySection: string; + + private readonly priceTaxExcludedValue: string; + + private readonly priceTaxIncludedValue: string; + + private readonly unitPriceValue: string; + + private readonly marginValue: string; + + private readonly marginRateValue: string; + + private readonly wholeSalePriceValue: string; + + private readonly displayRetailPricePerUnit: (toEnable: number) => string; + + private readonly ecotaxInput: string; + + private readonly retailPricePerUnitInputTaxExcl: string; + + private readonly retailPricePerUnitInputTaxIncl: string; + + private readonly addSpecificPriceButton: string; + + private readonly specificPriceModal: string; + + private readonly startingAtInput: string; + + private readonly applyDiscountToInitialPrice: (value: boolean) => string; + + private readonly combinationSelectButton: string; + + private readonly combinationSelectResult: string; + + private readonly combinationToSelectButton: (idCombination: number) => string; + + private readonly applyDiscountOfInput: string; + + private readonly reductionType: string; + + private readonly saveAndPublishButton: string; + + private readonly closeSpecificPriceForm: string; + + private readonly pricingOnSaleCheckBox: string; + + private readonly specificPriceTable: string; + + private readonly catalogPriceRulesTable: string; + + private readonly deleteSpecificPriceModal: string; + + private readonly deleteSpecificPriceModalConfirmButton: string; + + private readonly showCatalogPriceRuleButton: string; + + private readonly manageCatalogPriceRuleLink: string; + + private readonly catalogPriceRuleRow: (row: number) => string; + + private readonly specificPriceTableRow: (row: number) => string; + + private readonly editSpecificPriceIcon: (row: number) => string; + + private readonly deleteSpecificPriceIcon: (row: number) => string; + + private readonly catalogPriceRuleRowColumn: (row: number, column: string) => string; + + /** + * @constructs + * Setting up texts and selectors to use on pricing tab + */ + constructor() { + super(); + + // Selectors in pricing tab + this.pricingTabLink = '#product_pricing-tab-nav'; + // Selectors in retail price section + this.retailPriceInputTaxExcl = '#product_pricing_retail_price_price_tax_excluded'; + this.retailPriceInputTaxIncl = '#product_pricing_retail_price_price_tax_included'; + this.taxRuleID = 'product_pricing_retail_price_tax_rules_group_id'; + this.taxRuleSelect = `#${this.taxRuleID}`; + this.taxRuleSpan = `#select2-${this.taxRuleID}-container`; + this.taxRuleList = `ul#select2-${this.taxRuleID}-results`; + // Selectors in cost price section + this.wholesalePriceInput = '#product_pricing_wholesale_price'; + // Selectors in retail price per unit section + this.displayRetailPricePerUnit = (toEnable: number) => `#product_pricing_disabling_switch_unit_price_${toEnable}`; + this.retailPricePerUnitInputTaxExcl = '#product_pricing_unit_price_price_tax_excluded'; + this.retailPricePerUnitInputTaxIncl = '#product_pricing_unit_price_price_tax_included'; + this.unitPriceInput = '#product_pricing_unit_price_price_tax_excluded'; + this.unityInput = '#product_pricing_unit_price_unity'; + // Selectors in summary section + this.productPricingSummarySection = '#product_pricing_summary'; + this.priceTaxExcludedValue = `${this.productPricingSummarySection} div.price-tax-excluded-value`; + this.priceTaxIncludedValue = `${this.productPricingSummarySection} div.price-tax-included-value`; + this.unitPriceValue = `${this.productPricingSummarySection} div.unit-price-value`; + this.marginValue = `${this.productPricingSummarySection} div.margin-value`; + this.marginRateValue = `${this.productPricingSummarySection} div.margin-rate-value`; + this.wholeSalePriceValue = `${this.productPricingSummarySection} div.wholesale-price-value`; + this.onSaleCheckbox = '#product_pricing_on_sale'; + this.ecotaxInput = '#product_pricing_retail_price_ecotax_tax_excluded'; + this.pricingOnSaleCheckBox = '#product_pricing div.form-group.checkbox-widget div.md-checkbox-inline'; + + // Selectors in specific Price section + this.addSpecificPriceButton = '#product_pricing_specific_prices_add_specific_price_btn'; + // Specific Price modal + this.specificPriceModal = '#modal-specific-price-form'; + this.closeSpecificPriceForm = `${this.specificPriceModal} div.modal-header button.close`; + // Combination Modal Bloc + this.combinationSelectButton = '#select2-specific_price_combination_id-container'; + this.combinationSelectResult = '#select2-specific_price_combination_id-results'; + this.combinationToSelectButton = (idCombination: number) => `li.select2-results__option:nth-child(${idCombination})`; + // Minimum number of units purchased Bloc + this.startingAtInput = '#specific_price_from_quantity'; + // Impact on price Bloc + this.applyDiscountToInitialPrice = (value: boolean) => '#specific_price_impact_disabling_switch_reduction_' + + `${value ? '1' : '0'}`; + this.applyDiscountOfInput = '#specific_price_impact_reduction_value'; + this.reductionType = '#specific_price_impact_reduction_type'; + // Footer + this.saveAndPublishButton = `${this.specificPriceModal} div.modal-footer button.btn-confirm-submit`; + + // Selectors is specific price table + this.specificPriceTable = '#specific-prices-list-table'; + this.specificPriceTableRow = (row: number) => `${this.specificPriceTable} tr:nth-child(${row})`; + this.editSpecificPriceIcon = (row: number) => `${this.specificPriceTableRow(row)} td button.js-edit-specific-price-btn`; + this.deleteSpecificPriceIcon = (row: number) => `${this.specificPriceTableRow(row)} td button.js-delete-specific-price-btn`; + this.deleteSpecificPriceModal = '#modal-confirm-delete-combination'; + this.deleteSpecificPriceModalConfirmButton = `${this.deleteSpecificPriceModal} button.btn-confirm-submit`; + + // Selectors in catalog price rules section + this.showCatalogPriceRuleButton = '#product_pricing_show_catalog_price_rules'; + this.manageCatalogPriceRuleLink = '#product_pricing a[href*=\'AdminSpecificPriceRule\']'; + this.catalogPriceRulesTable = '#catalog-price-rules-list-table'; + this.catalogPriceRuleRow = (row: number) => `${this.catalogPriceRulesTable} tbody tr:nth-child(${row})`; + this.catalogPriceRuleRowColumn = (row: number, column: string) => `${this.catalogPriceRuleRow(row)} td.${column}`; + } + + /* + Methods + */ + + /** + * Set product pricing + * @param page {Page} Browser tab + * @param productData {FakerProduct} Data to set in pricing form + * @returns {Promise} + */ + async setProductPricing(page: Page, productData: FakerProduct): Promise { + await this.waitForSelectorAndClick(page, this.pricingTabLink); + // Select tax rule by ID + await Promise.all([ + this.waitForSelectorAndClick(page, this.taxRuleSpan), + this.waitForVisibleSelector(page, this.taxRuleList), + ]); + await page.locator(`li:has-text('${productData.taxRule}')`).click(); + // We define the price after the tax because the tax impact the priceTaxIncluded + await this.setRetailPrice(page, false, productData.price); + if (productData.onSale) { + await page.locator(this.pricingOnSaleCheckBox).click(); + } + } + + /** + * Set tax rule + * @param page {Page} Browser tab + * @param taxRule {string} Tax rule to select + * @returns {Promise} + */ + async setTaxRule(page: Page, taxRule: string): Promise { + await Promise.all([ + this.waitForSelectorAndClick(page, this.taxRuleSpan), + this.waitForVisibleSelector(page, this.taxRuleList), + ]); + await page.locator(`li:has-text('${taxRule}')`).click(); + } + + /** + * Set retail price + * @param page {Page} Browser tab + * @param isTaxExcluded {boolean} is Tax Excluded + * @param price {number} Retail price + * @returns {Promise} + */ + async setRetailPrice(page: Page, isTaxExcluded: boolean, price: number): Promise { + await this.setValue( + page, + isTaxExcluded ? this.retailPriceInputTaxExcl : this.retailPriceInputTaxIncl, + price, + ); + } + + /** + * Set cost price + * @param page {Page} Browser tab + * @param costPrice {number} + * @returns {Promise} + */ + async setCostPrice(page: Page, costPrice: number): Promise { + await this.setValue(page, this.wholesalePriceInput, costPrice); + } + + /** + * Set display retail price per unit + * @param page {Page} Browser tab + * @param toEnable {boolean} True if we need to check display retail price per unit + * @returns {Promise} + */ + async setDisplayRetailPricePerUnit(page: Page, toEnable: true): Promise { + await this.setChecked(page, this.displayRetailPricePerUnit(toEnable ? 1 : 0)); + } + + /** + * Set retail price per unit + * @param page {Page} Browser tab + * @param isTaxExcluded {boolean} is Tax Excluded + * @param price {number} Retail price + * @param unit {string} Unit + * @returns {Promise} + */ + async setRetailPricePerUnit(page: Page, isTaxExcluded: boolean, price: number, unit: string): Promise { + await this.setValue( + page, + isTaxExcluded ? this.retailPricePerUnitInputTaxExcl : this.retailPricePerUnitInputTaxIncl, + price, + ); + await this.setValue(page, this.unityInput, unit); + } + + /** + * Get summary + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getSummary(page: Page) { + await this.waitForSelectorAndClick(page, this.pricingTabLink); + return { + priceTaxExcludedValue: await this.getTextContent(page, this.priceTaxExcludedValue), + priceTaxIncludedValue: await this.getTextContent(page, this.priceTaxIncludedValue), + marginValue: await this.getTextContent(page, this.marginValue), + marginRateValue: await this.getTextContent(page, this.marginRateValue), + WholesalePriceValue: await this.getTextContent(page, this.wholeSalePriceValue), + }; + } + + /** + * Get unit price + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getUnitPriceValue(page: Page): Promise { + return this.getTextContent(page, this.unitPriceValue); + } + + /** + * Set display on sale flag + * @param page {Page} Browser tab + * @returns {Promise} + */ + async setDisplayOnSaleFlag(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.pricingOnSaleCheckBox); + } + + /** + * Click on add specific price button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnAddSpecificPriceButton(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.pricingTabLink); + + await Promise.all([ + page.locator(this.addSpecificPriceButton).click(), + this.waitForVisibleSelector(page, `${this.specificPriceModal}.show`), + ]); + } + + /** + * Click on edit specific price icon + * @param page {Page} Browser tab + * @param row {number} Row to edit + * @returns {Promise} + */ + async clickOnEditSpecificPriceIcon(page: Page, row: number): Promise { + await this.waitForSelectorAndClick(page, this.pricingTabLink); + + await Promise.all([ + page.locator(this.editSpecificPriceIcon(row)).click(), + this.waitForVisibleSelector(page, `${this.specificPriceModal}.show`), + ]); + } + + /** + * Product specific price + * @param page {Page} Browser tab + * @param specificPriceData {ProductSpecificPrice} Data to set on specific price form + * @return {Promise} + */ + async setSpecificPrice(page: Page, specificPriceData: ProductSpecificPrice): Promise { + const addSpecificPriceFrame = page.frame({name: 'modal-specific-price-form-iframe'}); + + await this.setValue(addSpecificPriceFrame!, this.startingAtInput, specificPriceData.startingAt); + await this.setChecked(addSpecificPriceFrame!, this.applyDiscountToInitialPrice(true)); + + // Choose combinations if exist + if (specificPriceData.attributes) { + await addSpecificPriceFrame!.locator(this.combinationSelectButton).click(); + await this.waitForVisibleSelector(addSpecificPriceFrame!, this.combinationSelectResult); + await this.waitForSelectorAndClick(addSpecificPriceFrame!, this.combinationToSelectButton(specificPriceData.attributes)); + } + + await this.setValue(addSpecificPriceFrame!, this.applyDiscountOfInput, specificPriceData.discount); + await this.selectByVisibleText(addSpecificPriceFrame!, this.reductionType, specificPriceData.reductionType); + + // Save and get growl message + await page.locator(this.saveAndPublishButton).click(); + const successMessage = await this.getAlertSuccessBlockParagraphContent(addSpecificPriceFrame!); + await page.locator(this.closeSpecificPriceForm).click(); + + return successMessage; + } + + /** + * Delete specific price + * @param page {Page} Browser tab + * @param row {number} Row to edit + * @return {Promise} + */ + async deleteSpecificPrice(page: Page, row: number): Promise { + await page.locator(this.deleteSpecificPriceIcon(row)).click(); + await page.locator(this.deleteSpecificPriceModalConfirmButton).click(); + + return this.getGrowlMessageContent(page); + } + + /** + * Click on show catalog price rule button + * @param page {Page} Browser tab + * @return {Promise} + */ + async clickOnShowCatalogPriceRuleButton(page: Page): Promise { + await page.locator(this.showCatalogPriceRuleButton).click(); + } + + /** + * Click on hide catalog price rule button + * @param page {Page} Browser tab + * @return {Promise} + */ + async clickOnHideCatalogPriceRulesButton(page: Page): Promise { + await page.locator(this.showCatalogPriceRuleButton).click(); + + return this.elementVisible(page, this.catalogPriceRulesTable, 1000); + } + + /** + * Click on manage catalog price rule link + * @param page {Page} Browser tab + * @return {Promise} + */ + async clickOnManageCatalogPriceRuleLink(page: Page): Promise { + return this.openLinkWithTargetBlank(page, this.manageCatalogPriceRuleLink, 'body'); + } + + /** + * Get catalog price rule data + * @param page {Page} Browser tab + * @param row {number} Row of catalog price rule + */ + async getCatalogPriceRuleData(page: Page, row: number) { + return { + id: await this.getTextContent(page, this.catalogPriceRuleRowColumn(row, 'catalog-price-rule-id')), + name: await this.getTextContent(page, this.catalogPriceRuleRowColumn(row, 'name')), + currency: await this.getTextContent(page, this.catalogPriceRuleRowColumn(row, 'currency')), + country: await this.getTextContent(page, this.catalogPriceRuleRowColumn(row, 'country')), + group: await this.getTextContent(page, this.catalogPriceRuleRowColumn(row, 'group')), + store: await this.getTextContent(page, this.catalogPriceRuleRowColumn(row, 'shop')), + discount: await this.getTextContent(page, this.catalogPriceRuleRowColumn(row, 'impact')), + fromQuantity: await this.getNumberFromText(page, this.catalogPriceRuleRowColumn(row, 'from-qty')), + }; + } + + /** + * Returns the value of a form element + * @param page {Page} + * @param inputName {string} + */ + async getValue(page: Page, inputName: string): Promise { + switch (inputName) { + case 'ecotax': + return this.getAttributeContent(page, this.ecotaxInput, 'value'); + case 'id_tax_rules_group': + return page.locator(this.taxRuleSelect).evaluate((node: HTMLSelectElement) => node.value); + case 'on_sale': + return (await this.isChecked(page, this.onSaleCheckbox)) ? '1' : '0'; + case 'price': + return this.getAttributeContent(page, this.retailPriceInputTaxExcl, 'value'); + case 'unit_price': + return this.getAttributeContent(page, this.unitPriceInput, 'value'); + case 'unity': + return this.getAttributeContent(page, this.unityInput, 'value'); + case 'wholesale_price': + return this.getAttributeContent(page, this.wholesalePriceInput, 'value'); + default: + throw new Error(`Input ${inputName} was not found`); + } + } + + /** + * Set ecoTax value and save + * @param page {Page} Browser tab + * @param ecoTax {string} Eco tax value to set on eco tax input + * @returns {Promise} + */ + async addEcoTax(page: Page, ecoTax: number): Promise { + await this.waitForSelectorAndClick(page, this.pricingTabLink); + + await this.setValue(page, this.ecotaxInput, ecoTax); + } +} + +export default new PricingTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabSeo.ts b/src/versions/develop/pages/BO/catalog/products/create/tabSeo.ts new file mode 100644 index 00000000..eb0cddf0 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabSeo.ts @@ -0,0 +1,161 @@ +// Import pages +import BOBasePage from '@pages/BO/BOBasePage'; + +import type {Page} from 'playwright'; + +/** + * SEO tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class SeoTab extends BOBasePage { + private readonly productMetaTitleInput: string; + + private readonly productMetaDescriptionInput: string; + + private readonly productLinkRewriteInput: (langageId: number) =>string; + + private readonly productRedirectTypeSelect: string; + + private readonly productRedirectProduct: string; + + private readonly tagInput: string; + + private readonly alertText: string; + + private readonly generateURLFromNameButton: string; + + private readonly redirectionWhenOfflineSelect: string; + + private readonly searchOptionTargetInput: string; + + /** + * @constructs + * Setting up texts and selectors to use on Shipping tab + */ + constructor() { + super(); + + // Selectors in seo tab + this.productMetaTitleInput = '#product_seo_meta_title'; + this.productMetaDescriptionInput = '#product_seo_meta_description'; + this.productLinkRewriteInput = (langageId: number) => `#product_seo_link_rewrite_${langageId}`; + this.productRedirectTypeSelect = '#product_seo_redirect_option_type'; + this.productRedirectProduct = '#product_seo_redirect_option_target_0_id'; + this.tagInput = '#product_seo_tags_1-tokenfield'; + this.alertText = '#product_seo div.alert-danger div.alert-text'; + this.generateURLFromNameButton = '#product_seo button.reset-link-rewrite'; + this.redirectionWhenOfflineSelect = '#product_seo_redirect_option_type'; + this.searchOptionTargetInput = '#product_seo_redirect_option_target_search_input'; + } + + /* + Methods + */ + /** + * Set meta title + * @param page {Page} Browser tab + * @param metaTitle {string} Meta title to set in the input + * @returns {Promise} + */ + async setMetaTitle(page: Page, metaTitle: string): Promise { + await this.setValue(page, `${this.productMetaTitleInput}_1`, metaTitle); + } + + /** + * Set meta description + * @param page {Page} Browser tab + * @param metaDescription {string} Meta description to set in the input + * @returns {Promise} + */ + async setMetaDescription(page: Page, metaDescription: string): Promise { + await this.setValue(page, `${this.productMetaDescriptionInput}_1`, metaDescription); + } + + /** + * Set friendly URL + * @param page {Page} Browser tab + * @param friendlyUrl {string} Friendly URL to set in the input + * @returns {Promise} + */ + async setFriendlyUrl(page: Page, friendlyUrl: string): Promise { + await this.setValue(page, this.productLinkRewriteInput(1), friendlyUrl); + } + + /** + * Get error message of friendly URL + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getErrorMessageOfFriendlyUrl(page: Page): Promise { + return this.getTextContent(page, this.alertText); + } + + /** + * Click on generate URL from name button + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnGenerateUrlFromNameButton(page: Page): Promise { + await page.locator(this.generateURLFromNameButton).click(); + } + + /** + * Select redirect page + * @param page {Page} Browser tab + * @param redirectionPage {string} Redirect page to select + * @returns {Promise} + */ + async selectRedirectionPage(page: Page, redirectionPage: string): Promise { + await this.selectByVisibleText(page, this.redirectionWhenOfflineSelect, redirectionPage); + } + + /** + * Search option target + * @param page {Page} Browser tab + * @param target {string} Target to search + * @returns {Promise} + */ + async searchOptionTarget(page: Page, target: string): Promise { + await page.locator(this.searchOptionTargetInput).fill(target); + await page.waitForTimeout(1000); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + } + + /** + * Set tag + * @param page {Page} Browser tab + * @param tag {string} tag to set in the input + * @returns {Promise} + */ + async setTag(page: Page, tag: string): Promise { + await page.locator(this.tagInput).fill(tag); + await page.keyboard.press('Enter'); + } + + /** + * Returns the value of a form element + * @param page {Page} + * @param inputName {string} + * @param languageId {string | undefined} + */ + async getValue(page: Page, inputName: string, languageId?: string): Promise { + switch (inputName) { + case 'id_type_redirected': + return this.getAttributeContent(page, this.productRedirectProduct, 'value'); + case 'link_rewrite': + return this.getAttributeContent(page, this.productLinkRewriteInput(parseInt(languageId!, 10)), 'value'); + case 'meta_description': + return this.getTextContent(page, `${this.productMetaDescriptionInput}_${languageId}`, false); + case 'meta_title': + return this.getAttributeContent(page, `${this.productMetaTitleInput}_${languageId}`, 'value'); + case 'redirect_type': + return page.locator(this.productRedirectTypeSelect).evaluate((node: HTMLSelectElement) => node.value); + default: + throw new Error(`Input ${inputName} was not found`); + } + } +} + +export default new SeoTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabShipping.ts b/src/versions/develop/pages/BO/catalog/products/create/tabShipping.ts new file mode 100644 index 00000000..064442c2 --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabShipping.ts @@ -0,0 +1,208 @@ +// Import pages +import BOBasePage from '@pages/BO/BOBasePage'; +import type {Page} from 'playwright'; + +// Import data +import ProductData from '@data/faker/product'; + +/** + * Shipping tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class ShippingTab extends BOBasePage { + private readonly shippingTabLink: string; + + private readonly productDimensionsWidthInput: string; + + private readonly productDimensionsHeightInput: string; + + private readonly productDimensionsDepthInput: string; + + private readonly productDimensionsWeightInput: string; + + private readonly productDeliveryInStockInput: string; + + private readonly productDeliveryOutStockInput: string; + + private readonly productAdditionalShippingCostInput: string; + + private readonly productDeliveryTimeInput: string; + + private readonly deliveryTimeInStockProducts: string; + + private readonly deliveryTimeOutOfStockProducts: string; + + private readonly editDeliveryTimeLink: string; + + private readonly allCarriersSelect: string; + + private readonly availableCarrierCheckboxButton: (carrierID: number) => string; + + private readonly deliveryTimeType: (type: number) => string; + + /** + * @constructs + * Setting up texts and selectors to use on Shipping tab + */ + constructor() { + super(); + + // Selectors in Shipping tab + this.shippingTabLink = '#product_shipping-tab-nav'; + // Package dimension section + this.productDimensionsWidthInput = '#product_shipping_dimensions_width'; + this.productDimensionsHeightInput = '#product_shipping_dimensions_height'; + this.productDimensionsDepthInput = '#product_shipping_dimensions_depth'; + this.productDimensionsWeightInput = '#product_shipping_dimensions_weight'; + this.productDeliveryInStockInput = '#product_shipping_delivery_time_notes_in_stock'; + this.productDeliveryOutStockInput = '#product_shipping_delivery_time_notes_out_of_stock'; + // Delivery time section + this.deliveryTimeType = (type: number) => `#product_shipping_delivery_time_note_type_${type}`; + this.deliveryTimeInStockProducts = '#product_shipping_delivery_time_notes_in_stock_1'; + this.deliveryTimeOutOfStockProducts = '#product_shipping_delivery_time_notes_out_of_stock_1'; + this.editDeliveryTimeLink = '#product_shipping_delivery_time_note_type label a[href*="configure/shop/product-preferences"]'; + // Shipping fees section + this.productAdditionalShippingCostInput = '#product_shipping_additional_shipping_cost'; + this.productDeliveryTimeInput = 'input[name="product[shipping][delivery_time_note_type]"]'; + this.allCarriersSelect = '#carrier-checkboxes-dropdown button'; + this.availableCarrierCheckboxButton = (carrierID: number) => '#carrier-checkboxes-dropdown' + + ` div:nth-child(${carrierID}).md-checkbox label div`; + } + + /* + Methods + */ + /** + * Set package dimension + * @param page {Page} Browser tab + * @param productData {ProductData} Data to set in package dimension form + * @returns {Promise} + */ + async setPackageDimension(page: Page, productData: ProductData): Promise { + await this.waitForSelectorAndClick(page, this.shippingTabLink); + await this.setValue(page, this.productDimensionsWidthInput, productData.packageDimensionWidth); + await this.setValue(page, this.productDimensionsHeightInput, productData.packageDimensionHeight); + await this.setValue(page, this.productDimensionsDepthInput, productData.packageDimensionDepth); + await this.setValue(page, this.productDimensionsWeightInput, productData.packageDimensionWeight); + } + + /** + * Set delivery time + * @param page {Page} Browser tab + * @param deliveryTime {string} Delivery time value to check + * @returns {Promise} + */ + async setDeliveryTime(page: Page, deliveryTime: string): Promise { + switch (deliveryTime) { + case 'None': + await this.setChecked(page, this.deliveryTimeType(0)); + break; + case 'Default delivery time': + await this.setChecked(page, this.deliveryTimeType(1)); + break; + case 'Specific delivery time': + await this.setChecked(page, this.deliveryTimeType(2)); + break; + default: + throw new Error(`Button ${deliveryTime} was not found`); + } + } + + /** + * Set delivery time in stock + * @param page {Page} Browser tab + * @param numberOfDays {string} Number of days of delivery + * @returns {Promise} + */ + async setDeliveryTimeInStockProducts(page: Page, numberOfDays: string): Promise { + await this.setValue(page, this.deliveryTimeInStockProducts, numberOfDays); + } + + /** + * Set delivery time out of stock + * @param page {Page} Browser tab + * @param numberOfDays {string} Number of days of delivery + * @returns {Promise} + */ + async setDeliveryTimeOutOfStockProducts(page: Page, numberOfDays: string): Promise { + await this.setValue(page, this.deliveryTimeOutOfStockProducts, numberOfDays); + } + + /** + * Click on edit delivery time link + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnEditDeliveryTimeLink(page: Page): Promise { + return this.openLinkWithTargetBlank(page, this.editDeliveryTimeLink); + } + + /** + * Set additional shipping costs + * @param page {Page} Browser tab + * @param shippingCosts {number} Shipping cost + * @returns {Promise} + */ + async setAdditionalShippingCosts(page: Page, shippingCosts: number): Promise { + await this.waitForSelectorAndClick(page, this.shippingTabLink); + await this.setValue(page, this.productAdditionalShippingCostInput, shippingCosts); + } + + /** + * Select available carrier + * @param page {Page} Browser tab + * @param carrier {string} Carrier to choose + * @returns {Promise} + */ + async selectAvailableCarrier(page: Page, carrier: string): Promise { + await this.waitForSelectorAndClick(page, this.allCarriersSelect); + switch (carrier) { + case 'Click and collect': + await this.setChecked(page, this.availableCarrierCheckboxButton(1)); + break; + case 'My cheap carrier': + await this.setChecked(page, this.availableCarrierCheckboxButton(2)); + break; + case 'My carrier': + await this.setChecked(page, this.availableCarrierCheckboxButton(3)); + break; + case 'My light carrier': + await this.setChecked(page, this.availableCarrierCheckboxButton(4)); + break; + default: + throw new Error(`${carrier} was not found`); + } + } + + /** + * Returns the value of a form element + * @param page {Page} + * @param inputName {string} + * @param languageId {string | undefined} + */ + async getValue(page: Page, inputName: string, languageId?: string): Promise { + switch (inputName) { + case 'additional_delivery_times': + return this.getAttributeContent(page, `${this.productDeliveryTimeInput}[checked="checked"]`, 'value'); + case 'additional_shipping_cost': + return this.getAttributeContent(page, this.productAdditionalShippingCostInput, 'value'); + case 'delivery_in_stock': + return this.getAttributeContent(page, `${this.productDeliveryInStockInput}_${languageId}`, 'value'); + case 'delivery_out_stock': + return this.getAttributeContent(page, `${this.productDeliveryOutStockInput}_${languageId}`, 'value'); + case 'depth': + return this.getAttributeContent(page, this.productDimensionsDepthInput, 'value'); + case 'height': + return this.getAttributeContent(page, this.productDimensionsHeightInput, 'value'); + case 'weight': + return this.getAttributeContent(page, this.productDimensionsWeightInput, 'value'); + case 'width': + return this.getAttributeContent(page, this.productDimensionsWidthInput, 'value'); + default: + throw new Error(`Input ${inputName} was not found`); + } + } +} + +export default new ShippingTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabStocks.ts b/src/versions/develop/pages/BO/catalog/products/create/tabStocks.ts new file mode 100644 index 00000000..1d5cb9fe --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabStocks.ts @@ -0,0 +1,303 @@ +import type FakerProduct from '@data/faker/product'; +import type {ProductStockMovement} from '@data/types/product'; +import { BOCatalogProductsCreateTabStocksPageInterface } from '@interfaces/BO/catalog/products/create/tabStocks'; +import BOBasePage from '@pages/BO/BOBasePage'; +import { type Page } from '@playwright/test'; + +/** + * Stocks tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class StocksTab extends BOBasePage implements BOCatalogProductsCreateTabStocksPageInterface { + private readonly stocksTabLink: string; + + private readonly initialQuantitySpan: string; + + private readonly productQuantityInput: string; + + private readonly productMinimumQuantityInput: string; + + private readonly productStockLocationInput: string; + + private readonly productLowStockThresholdCheckbox: string; + + private readonly productLowStockThresholdInput: string; + + private readonly productLabelAvailableNowDropdown: string; + + private readonly productLabelAvailableNowDropdownItem: (locale: string) => string; + + private readonly productLabelAvailableNowInput: (languageId: string) => string; + + private readonly productLabelAvailableLaterDropdown: string; + + private readonly productLabelAvailableLaterDropdownItem: (locale: string) => string; + + private readonly productLabelAvailableLaterInput: (languageId: string) => string; + + private readonly productAvailableDateInput: string; + + private readonly behaviourOutOfStockInput: (id: number) => string; + + private readonly denyOrderRadioButton: string; + + private readonly allowOrderRadioButton: string; + + private readonly useDefaultBehaviourRadioButton: string; + + private readonly stockMovementsDiv: string; + + private readonly dateTimeRowInTable: (movementRow: number) => string; + + private readonly employeeRowInTable: (movementRow: number) => string; + + private readonly quantityRowInTable: (movementRow: number) => string; + + private readonly stockMovementsLink: string; + + /** + * @constructs + * Setting up texts and selectors to use on stocks tab + */ + constructor() { + super(); + + // Selectors in stocks tab + this.stocksTabLink = '#product_stock-tab-nav'; + this.initialQuantitySpan = '#product_stock_quantities_delta_quantity_quantity span'; + this.productQuantityInput = '#product_stock_quantities_delta_quantity_delta'; + this.productMinimumQuantityInput = '#product_stock_quantities_minimal_quantity'; + this.productStockLocationInput = '#product_stock_options_stock_location'; + this.productLowStockThresholdCheckbox = '#product_stock_options_disabling_switch_low_stock_threshold_1'; + this.productLowStockThresholdInput = '#product_stock_options_low_stock_threshold'; + this.productLabelAvailableNowDropdown = '#product_stock_availability_available_now_label_dropdown'; + this.productLabelAvailableNowDropdownItem = (locale: string) => '#product_stock_availability_available_now_label ' + + `span.dropdown-item[data-locale="${locale}"]`; + this.productLabelAvailableNowInput = (languageId: string) => `#product_stock_availability_available_now_label_${languageId}`; + this.productLabelAvailableLaterDropdown = '#product_stock_availability_available_later_label_dropdown'; + this.productLabelAvailableLaterDropdownItem = (locale: string) => '#product_stock_availability_available_later_label ' + + `span.dropdown-item[data-locale="${locale}"]`; + this.productLabelAvailableLaterInput = (languageId: string) => `#product_stock_availability_available_later_label_${ + languageId}`; + this.productAvailableDateInput = '#product_stock_availability_available_date'; + // When out of stock selectors + this.behaviourOutOfStockInput = (id: number) => `#product_stock_availability_out_of_stock_type_${id} +i`; + this.denyOrderRadioButton = this.behaviourOutOfStockInput(0); + this.allowOrderRadioButton = this.behaviourOutOfStockInput(1); + this.useDefaultBehaviourRadioButton = this.behaviourOutOfStockInput(2); + + // Stock movement table selectors + this.stockMovementsDiv = '#product_stock_quantities_stock_movements'; + this.dateTimeRowInTable = (movementRow: number) => `${this.stockMovementsDiv}_${movementRow}_date + span`; + this.employeeRowInTable = (movementRow: number) => `${this.stockMovementsDiv}_${movementRow}_employee_name + span`; + this.quantityRowInTable = (movementRow: number) => `${this.stockMovementsDiv}_${movementRow}_delta_quantity + span`; + this.stockMovementsLink = `${this.stockMovementsDiv} + div > a`; + } + + /* + Methods + */ + /** + * Get Product quantity + * @param page {Page} Browser tab + */ + async getProductQuantity(page:Page): Promise { + return parseInt(await this.getTextContent(page, this.initialQuantitySpan), 10); + } + + /** + * Set product quantity + * @param page {Page} Browser tab + * @param quantity {number} Quantity value to set on quantity input + * @returns {Promise} + */ + async setProductQuantity(page: Page, quantity: number): Promise { + await this.waitForSelectorAndClick(page, this.stocksTabLink); + const initialQuantity = await this.getProductQuantity(page); + await this.setQuantityDelta(page, quantity - initialQuantity); + } + + /** + * Set product stock + * @param page {Page} Browser tab + * @param productData {FakerProduct} Data to set in stock form + * @returns {Promise} + */ + async setProductStock(page:Page, productData: FakerProduct): Promise { + await this.waitForSelectorAndClick(page, this.stocksTabLink); + await this.setQuantityDelta(page, productData.quantity); + await this.setValue(page, this.productMinimumQuantityInput, productData.minimumQuantity); + await this.setStockLocation(page, productData.stockLocation); + + await this.setOptionWhenOutOfStock(page, productData.behaviourOutOfStock); + + await this.setLabelWhenInStock(page, productData.labelWhenInStock); + await this.setLabelWhenOutOfStock(page, productData.labelWhenOutOfStock); + } + + /** + * Set quantity delta + * @param page {Page} Browser tab + * @param quantity {number} Quantity delta + * @returns {Promise} + */ + async setQuantityDelta(page: Page, quantity: number): Promise { + await this.setValue(page, this.productQuantityInput, quantity); + } + + /** + * Set option when out of stock + * @param page {Page} Browser tab + * @param option {string} Option to check + * @returns {Promise} + */ + async setOptionWhenOutOfStock(page: Page, option: string): Promise { + switch (option) { + case 'Deny orders': + await this.setChecked(page, this.denyOrderRadioButton); + break; + case 'Allow orders': + await this.setChecked(page, this.allowOrderRadioButton); + break; + case 'Default behavior': + case 'Use default behavior': + await this.setChecked(page, this.useDefaultBehaviourRadioButton); + break; + default: + throw new Error(`Option ${option} was not found`); + } + } + + /** + * @param page {Page} + * @param inputName {string} + * @param languageId {string | undefined} + */ + async getValue(page: Page, inputName: string, languageId?: string): Promise { + switch (inputName) { + case 'available_date': + return this.getAttributeContent(page, this.productAvailableDateInput, 'value'); + case 'available_later': + return this.getAttributeContent(page, this.productLabelAvailableLaterInput(languageId!), 'value'); + case 'available_now': + return this.getAttributeContent(page, this.productLabelAvailableNowInput(languageId!), 'value'); + case 'low_stock_threshold': + return this.getAttributeContent(page, this.productLowStockThresholdInput, 'value'); + case 'low_stock_threshold_enabled': + return (await this.isChecked(page, this.productLowStockThresholdCheckbox)) ? '1' : '0'; + case 'minimal_quantity': + return this.getAttributeContent(page, this.productMinimumQuantityInput, 'value'); + case 'location': + return this.getAttributeContent(page, this.productStockLocationInput, 'value'); + default: + throw new Error(`Input ${inputName} was not found`); + } + } + + /** + * Get stock movements data + * @param page {Page} Browser tab + * @param movementRow {number} Movement row in table stock movements + */ + async getStockMovement(page: Page, movementRow: number): Promise { + return { + dateTime: await this.getTextContent(page, this.dateTimeRowInTable(movementRow - 1)), + employee: await this.getTextContent(page, this.employeeRowInTable(movementRow - 1), false), + quantity: await this.getNumberFromText(page, this.quantityRowInTable(movementRow - 1)), + }; + } + + /** + * Click on "View All Stock Movements" link + * @param page {Page} Browser tab + */ + async clickViewAllStockMovements(page: Page): Promise { + return this.openLinkWithTargetBlank(page, this.stockMovementsLink); + } + + /** + * Set the Minimum quantity for sale + * @param page {Page} Browser tab + * @param minimalQuantiy {number} Minimal Quantity + */ + async setMinimalQuantity(page: Page, minimalQuantiy: number): Promise { + await this.setValue(page, this.productMinimumQuantityInput, minimalQuantiy); + } + + /** + * Set the Stock location + * @param page {Page} Browser tab + * @param stockLocation {number} Stock location + */ + async setStockLocation(page: Page, stockLocation: string): Promise { + await this.setValue(page, this.productStockLocationInput, stockLocation); + } + + /** + * Enable/Disable the low stock alert by email + * @param page {Page} Browser tab + * @param statusAlert {boolean} Status + * @param thresholdValue {number} Threshold value + */ + async setLowStockAlertByEmail(page: Page, statusAlert: boolean, thresholdValue: number = 0): Promise { + const isLowStockAlertByEmail: boolean = (await this.getValue(page, 'low_stock_threshold_enabled') === '1'); + + if (isLowStockAlertByEmail !== statusAlert) { + await this.clickAndWaitForLoadState(page, this.productLowStockThresholdCheckbox); + } + + // Define the threshold only if the low stock alert is enabled + if (statusAlert) { + await this.setValue(page, this.productLowStockThresholdInput, thresholdValue); + } + } + + /** + * Set label when in stock + * @param page {Page} Browser tab + * @param label {string} Label to set when in stock in the input + * @returns {Promise} + */ + async setLabelWhenInStock(page: Page, label: string): Promise { + await this.waitForSelectorAndClick(page, this.productLabelAvailableNowDropdown); + await page + .locator(this.productLabelAvailableNowDropdownItem('en')) + .evaluate((el: HTMLElement) => el.click()); + await this.setValue(page, this.productLabelAvailableNowInput('1'), label); + } + + /** + * Set label when out of stock + * @param page {Page} Browser tab + * @param label {string} Label to set when out of stock in the input + */ + async setLabelWhenOutOfStock(page: Page, label: string): Promise { + await this.waitForSelectorAndClick(page, this.productLabelAvailableLaterDropdown); + await page + .locator(this.productLabelAvailableLaterDropdownItem('en')) + .evaluate((el: HTMLElement) => el.click()); + await this.setValue(page, this.productLabelAvailableLaterInput('1'), label); + } + + /** + * Set availability date + * @param page {Page} Browser tab + * @param date {string} Label to set when availability date in the input + */ + async setAvailabilityDate(page: Page, date: string): Promise { + await this.setValue(page, this.productAvailableDateInput, date); + } + + /** + * Is quantity input visible + * @param page {Page} Browser tab + * @returns {Promise} + */ + async isQuantityInputVisible(page: Page): Promise { + await this.waitForSelectorAndClick(page, this.stocksTabLink); + return this.elementVisible(page, this.productQuantityInput, 1000); + } +} + +export default new StocksTab(); diff --git a/src/versions/develop/pages/BO/catalog/products/create/tabVirtualProduct.ts b/src/versions/develop/pages/BO/catalog/products/create/tabVirtualProduct.ts new file mode 100644 index 00000000..31010acf --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/products/create/tabVirtualProduct.ts @@ -0,0 +1,171 @@ +import FakerProduct from '@data/faker/product'; +import { BOCatalogProductsCreateTabVirtualProductPageInterface } from '@interfaces/BO/catalog/products/create/tabVirtualProduct'; +import BOBasePage from '@pages/BO/BOBasePage'; +import { type Page } from '@playwright/test'; + +/** + * Virtual product tab on new product V2 page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class VirtualProductTab extends BOBasePage implements BOCatalogProductsCreateTabVirtualProductPageInterface { + private readonly virtualProductTabLink: string; + + private readonly productQuantityInput: string; + + private readonly productMinimumQuantityInput: string; + + private readonly productFileSection: string; + + private readonly productChooseFile: (toCheck: number) => string; + + private readonly productFile: string; + + private readonly errorMessageInFileInput: string; + + private readonly productFileNameInput: string; + + private readonly productFileDownloadTimesLimit: string; + + private readonly productFileExpirationDate: string; + + private readonly productFileNumberOfDays: string; + + private readonly denyOrderRadioButton: string; + + private readonly allowOrderRadioButton: string; + + private readonly useDefaultBehaviourRadioButton: string; + + private readonly editDefaultBehaviourLink: string; + + private readonly labelWhenInStock: string; + + private readonly labelWhenOutOfStock: string; + + /** + * @constructs + * Setting up texts and selectors to use on Virtual product tab + */ + constructor() { + super(); + + // Selectors in virtual product tab + this.virtualProductTabLink = '#product_stock-tab-nav'; + this.productQuantityInput = '#product_stock_quantities_delta_quantity_delta'; + this.productMinimumQuantityInput = '#product_stock_quantities_minimal_quantity'; + this.productFileSection = '#product_stock_virtual_product_file'; + this.productChooseFile = (toCheck: number) => `#product_stock_virtual_product_file_has_file_${toCheck}`; + this.productFile = '#product_stock_virtual_product_file_file'; + this.errorMessageInFileInput = `${this.productFileSection} div.form-group.file-widget.has-error div.alert-danger p`; + this.productFileNameInput = '#product_stock_virtual_product_file_name'; + this.productFileDownloadTimesLimit = '#product_stock_virtual_product_file_download_times_limit'; + this.productFileExpirationDate = '#product_stock_virtual_product_file_expiration_date'; + this.productFileNumberOfDays = '#product_stock_virtual_product_file_access_days_limit'; + + // When out of stock selectors + this.denyOrderRadioButton = '#product_stock_availability_out_of_stock_type_0 +i'; + this.allowOrderRadioButton = '#product_stock_availability_out_of_stock_type_1 +i'; + this.useDefaultBehaviourRadioButton = '#product_stock_availability_out_of_stock_type_2 +i'; + this.editDefaultBehaviourLink = '#product_stock_availability a[href*=configuration_fieldset_stock]'; + this.labelWhenInStock = '#product_stock_availability_available_now_label_1'; + this.labelWhenOutOfStock = '#product_stock_availability_available_later_label_1'; + } + + /* + Methods + */ + + /** + * Set product quantity + * @param page {Page} Browser tab + * @param quantity {string} Product quantity to set in the input + * @returns {Promise} + */ + async setProductQuantity(page: Page, quantity: number): Promise { + await this.waitForSelectorAndClick(page, this.virtualProductTabLink); + await this.setValue(page, this.productQuantityInput, quantity); + } + + /** + * Set virtual product + * @param page {Page} Browser tab + * @param productData {FakerProduct} Data to set in virtual product form + * @returns {Promise} + */ + async setVirtualProduct(page: Page, productData: FakerProduct): Promise { + await this.setProductQuantity(page, productData.quantity); + await this.setValue(page, this.productMinimumQuantityInput, productData.minimumQuantity); + if (productData.downloadFile) { + await this.setChecked(page, this.productChooseFile(productData.downloadFile ? 1 : 0)); + await this.waitForVisibleSelector(page, this.productFile); + await this.uploadFile(page, this.productFile, productData.fileName); + await this.setValue(page, this.productFileNameInput, productData.fileName); + await this.setValue(page, this.productFileDownloadTimesLimit, productData.allowedDownload); + await this.setValue(page, this.productFileExpirationDate, productData.expirationDate!); + await this.setValue(page, this.productFileNumberOfDays, productData.numberOfDays!); + } + } + + /** + * Get error message in downloaded file input + * @param page {Page} Browser tab + * @returns {Promise} + */ + async getErrorMessageInDownloadFileInput(page: Page): Promise { + return this.getTextContent(page, this.errorMessageInFileInput); + } + + // Methods for when out of stock + /** + * Set option when out of stock + * @param page {Page} Browser tab + * @param option {string} Option to check + * @returns {Promise} + */ + async setOptionWhenOutOfStock(page: Page, option: string): Promise { + switch (option) { + case 'Deny orders': + await this.setChecked(page, this.denyOrderRadioButton); + break; + case 'Allow orders': + await this.setChecked(page, this.allowOrderRadioButton); + break; + case 'Use default behavior': + await this.setChecked(page, this.useDefaultBehaviourRadioButton); + break; + default: + throw new Error(`Option ${option} was not found`); + } + } + + /** + * Click on edit default behaviour link + * @param page {Page} Browser tab + * @returns {Promise} + */ + async clickOnEditDefaultBehaviourLink(page: Page): Promise { + return this.openLinkWithTargetBlank(page, this.editDefaultBehaviourLink); + } + + /** + * Set label when in stock + * @param page {Page} Browser tab + * @param label {string} Label to set when in stock in the input + * @returns {Promise} + */ + async setLabelWhenInStock(page: Page, label: string): Promise { + await this.setValue(page, this.labelWhenInStock, label); + } + + /** + * Set label when out of stock + * @param page {Page} Browser tab + * @param label {string} Label to set when out of stock in the input + */ + async setLabelWhenOutOfStock(page: Page, label: string): Promise { + await this.setValue(page, this.labelWhenOutOfStock, label); + } +} + +export default new VirtualProductTab();