From b334c79e1a53072352c4d09fa42f7d96bd16bc1b Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Tue, 28 Jan 2025 09:14:28 +0100 Subject: [PATCH] Migrate `@pages/BO/catalog/stocks/movements` from Core --- src/index.ts | 1 + src/interfaces/BO/catalog/stock/movements.ts | 24 + src/pages/BO/catalog/stock/movements.ts | 9 + .../pages/BO/catalog/stock/movements.ts | 457 ++++++++++++++++++ 4 files changed, 491 insertions(+) create mode 100644 src/interfaces/BO/catalog/stock/movements.ts create mode 100644 src/pages/BO/catalog/stock/movements.ts create mode 100644 src/versions/develop/pages/BO/catalog/stock/movements.ts diff --git a/src/index.ts b/src/index.ts index 8b4d8e32..7674261f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -292,6 +292,7 @@ export {default as boSqlManagerCreatePage} from '@pages/BO/advancedParameters/da export {default as boSqlManagerViewPage} from '@pages/BO/advancedParameters/database/sqlManager/view'; export {default as boStatisticsPage} from '@pages/BO/statistics'; export {default as boStockPage} from '@pages/BO/catalog/stock'; +export {default as boStockMovementsPage} from '@pages/BO/catalog/stock/movements'; export {default as boSuppliersCreate} from '@pages/BO/catalog/suppliers/create'; export {default as boTaxesPage} from '@pages/BO/international/taxes'; export {default as boTaxRulesPage} from '@pages/BO/international/taxes/taxRules'; diff --git a/src/interfaces/BO/catalog/stock/movements.ts b/src/interfaces/BO/catalog/stock/movements.ts new file mode 100644 index 00000000..30d39c31 --- /dev/null +++ b/src/interfaces/BO/catalog/stock/movements.ts @@ -0,0 +1,24 @@ +import {BOBasePagePageInterface} from '@interfaces/BO'; +import {type Page} from '@playwright/test'; + +export interface BOStockMovementsPageInterface extends BOBasePagePageInterface { + readonly emptyTableMessage: string; + readonly pageTitle: string; + + clickOnMovementTypeLink(page: Page, row: number): Promise; + getAdvancedFiltersMovementTypeChoices(page: Page): Promise; + getAllRowsColumnContent(page: Page, column: string): Promise; + getNumberOfElementInGrid(page: Page): Promise; + getTextColumnFromTable(page: Page, row: number, column: string): Promise; + getTextForEmptyTable(page: Page): Promise; + isAdvancedFiltersVisible(page: Page): Promise; + paginateTo(page: Page, pageNumber?: number): Promise; + resetAdvancedFilter(page: Page): Promise; + setAdvancedFiltersCategory(page: Page, categoryName: string, status?: boolean): Promise; + setAdvancedFiltersDate(page: Page, type: 'inf'|'sup', date: string, onChange?: boolean): Promise; + setAdvancedFiltersEmployee(page: Page, employeeName: string): Promise; + setAdvancedFiltersMovementType(page: Page, movementType: 'None'|'Employee Edition'|'Customer Order'): Promise; + setAdvancedFiltersStatus(page: Page, status: boolean | null): Promise; + setAdvancedFiltersVisible(page: Page): Promise; + sortTable(page: Page, sortBy: string, sortDirection: string): Promise; +} diff --git a/src/pages/BO/catalog/stock/movements.ts b/src/pages/BO/catalog/stock/movements.ts new file mode 100644 index 00000000..ba0ab191 --- /dev/null +++ b/src/pages/BO/catalog/stock/movements.ts @@ -0,0 +1,9 @@ +import type {BOStockMovementsPageInterface} from '@interfaces/BO/catalog/stock/movements'; + +/* eslint-disable global-require, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +function requirePage(): BOStockMovementsPageInterface { + return require('@versions/develop/pages/BO/catalog/stock/movements'); +} +/* eslint-enable global-require, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ + +export default requirePage(); diff --git a/src/versions/develop/pages/BO/catalog/stock/movements.ts b/src/versions/develop/pages/BO/catalog/stock/movements.ts new file mode 100644 index 00000000..7019169a --- /dev/null +++ b/src/versions/develop/pages/BO/catalog/stock/movements.ts @@ -0,0 +1,457 @@ +import {type BOStockMovementsPageInterface} from '@interfaces/BO/catalog/stock/movements'; +import BOBasePage from '@pages/BO/BOBasePage'; +import {type Page} from '@playwright/test'; +import type {PageFunction} from 'playwright-core/types/structs'; + +/** + * Movements page, contains functions that can be used on the page + * @class + * @extends BOBasePage + */ +class BOStockMovementsPage extends BOBasePage implements BOStockMovementsPageInterface { + public readonly pageTitle: string; + + public readonly emptyTableMessage: string; + + private readonly stocksNavItemLink: string; + + private readonly searchForm: string; + + private readonly searchInput: string; + + private readonly searchButton: string; + + private readonly advFiltersContainer: string; + + private readonly advFiltersBlock: string; + + private readonly advFiltersButton: string; + + private readonly advFiltersFilterMvtTypeSelect: string; + + private readonly advFiltersFilterEmployeeSelect: string; + + private readonly advFiltersFilterDateSupInput: string; + + private readonly advFiltersFilterDateInfInput: string; + + private readonly advFiltersFilterCategoriesExpandBtn: string; + + private readonly advFiltersFilterCategoriesLabel: string; + + private readonly advFiltersFilterStatusEnabled: string; + + private readonly advFiltersFilterStatusDisabled: string; + + private readonly advFiltersFilterStatusAll: string; + + private readonly gridTable: string; + + private readonly tableBody: string; + + private readonly tableRows: string; + + private readonly tableRow: (row: number) => string; + + private readonly tableRowEmpty: string; + + private readonly tableProductId: (row: number) => string; + + private readonly tableProductNameColumn: (row: number) => string; + + private readonly tableProductReferenceColumn: (row: number) => string; + + private readonly tableTypeColumn: (row: number) => string; + + private readonly tableTypeColumnLink: (row: number) => string; + + private readonly tableProductDateColumn: (row: number) => string; + + private readonly tableQuantityColumn: (row: number) => string; + + private readonly tableHead: string; + + private readonly sortColumnDiv: (column: string) => string; + + private readonly sortColumnSpanButton: (column: string) => string; + + private readonly productListLoading: string; + + private readonly paginationList: string; + + private readonly paginationListItem: string; + + private readonly paginationListItemLink: (id: number) => string; + + /** + * @constructs + * Setting up texts and selectors to use on movements page + */ + constructor() { + super(); + + this.pageTitle = `Stock • ${global.INSTALL.SHOP_NAME}`; + this.emptyTableMessage = 'No product matches your search. Try changing search terms.'; + + // Header selectors + this.stocksNavItemLink = '#head_tabs li:nth-child(1) > a'; + + // Simple filter selectors + this.searchForm = 'form.search-form'; + this.searchInput = `${this.searchForm} .search-input input.input`; + this.searchButton = `${this.searchForm} button.search-button`; + + // Advanced filter + this.advFiltersContainer = '#filters-container'; + this.advFiltersBlock = '#filters'; + this.advFiltersButton = `${this.advFiltersContainer} button[data-target="${this.advFiltersBlock}"]`; + this.advFiltersFilterMvtTypeSelect = `${this.advFiltersBlock} #id_stock_mvt_reason select`; + this.advFiltersFilterEmployeeSelect = `${this.advFiltersBlock} #id_employee select`; + this.advFiltersFilterDateSupInput = `${this.advFiltersBlock} div.date input.datepicker-sup`; + this.advFiltersFilterDateInfInput = `${this.advFiltersBlock} div.date input.datepicker-inf`; + this.advFiltersFilterCategoriesExpandBtn = `${this.advFiltersBlock} .filter-categories button[data-action="expand"]`; + this.advFiltersFilterCategoriesLabel = `${this.advFiltersBlock} .filter-categories ul.tree li.tree-item span.tree-label`; + this.advFiltersFilterStatusEnabled = `${this.advFiltersBlock} label[for="enable"]`; + this.advFiltersFilterStatusDisabled = `${this.advFiltersBlock} label[for="disable"]`; + this.advFiltersFilterStatusAll = `${this.advFiltersBlock} label[for="all"]`; + + // Table selectors + this.gridTable = '.stock-movements table.table'; + this.tableBody = `${this.gridTable} tbody`; + this.tableRows = `${this.tableBody} tr`; + this.tableRow = (row: number) => `${this.tableRows}:nth-child(${row})`; + this.tableRowEmpty = `${this.tableRow(1)} div.ps-alert.alert.alert-warning`; + this.tableProductId = (row: number) => `${this.tableRow(row)} td:nth-child(1)`; + this.tableProductNameColumn = (row: number) => `${this.tableRow(row)} td:nth-child(2) div.media-body p`; + this.tableProductReferenceColumn = (row: number) => `${this.tableRow(row)} td:nth-child(3)`; + this.tableTypeColumn = (row: number) => `${this.tableRow(row)} td:nth-child(4)`; + this.tableTypeColumnLink = (row: number) => `${this.tableTypeColumn(row)} a`; + this.tableQuantityColumn = (row: number) => `${this.tableRow(row)} td:nth-child(5) span.qty-number`; + this.tableProductDateColumn = (row: number) => `${this.tableRow(row)} td:nth-child(6)`; + + // Sort Selectors + this.tableHead = `${this.gridTable} thead`; + this.sortColumnDiv = (column: string) => `${this.tableHead} div.ps-sortable-column[data-sort-col-name='${column}']`; + this.sortColumnSpanButton = (column: string) => `${this.sortColumnDiv(column)} span.ps-sort`; + + // Loader + this.productListLoading = `${this.tableRow(1)} td:nth-child(1) div.ps-loader`; + + // Pagination + this.paginationList = 'nav ul.pagination'; + this.paginationListItem = `${this.paginationList} li.page-item`; + this.paginationListItemLink = (id: number) => `${this.paginationListItem}:nth-child(${id}) a`; + } + + /* Header methods */ + /** + * Go to stocks page + * @param page {Page} Browser tab + * @return {Promise} + */ + async goToSubTabStocks(page: Page): Promise { + await page.locator(this.stocksNavItemLink).click(); + await this.waitForVisibleSelector(page, `${this.stocksNavItemLink}.active`); + } + + /** + * Filter by a word + * @param page {Page} Browser tab + * @param value {string} Value to set on filter input + * @returns {Promise} + */ + async simpleFilter(page: Page, value: string): Promise { + await page.locator(this.searchInput).fill(value); + await Promise.all([ + page.locator(this.searchButton).click(), + this.waitForVisibleSelector(page, this.productListLoading), + ]); + await this.waitForHiddenSelector(page, this.productListLoading); + } + + /** + * Display advanced filter + * @param page {Page} Browser tab + * @return {Promise} + */ + async setAdvancedFiltersVisible(page: Page): Promise { + if (await this.elementNotVisible(page, this.advFiltersBlock, 2000)) { + await page.locator(this.advFiltersButton).click(); + await this.elementVisible(page, this.advFiltersBlock, 2000); + } + } + + /** + * Return if Advanced Filter block is visible + * @param page {Page} Browser tab + * @return {Promise} + */ + async isAdvancedFiltersVisible(page: Page): Promise { + return this.elementVisible(page, this.advFiltersBlock, 2000); + } + + /** + * Get choices from the advanced filter "Movement Type" + * @param page {Page} Browser tab + * @return {Promise} + */ + async getAdvancedFiltersMovementTypeChoices(page: Page): Promise { + await this.elementVisible(page, this.advFiltersFilterMvtTypeSelect, 5000); + + return page + .locator(`${this.advFiltersFilterMvtTypeSelect} option`) + .allTextContents(); + } + + /** + * Set Filter "Categories" + * @param page {Page} Browser tab + * @param categoryName {string} Name of the category + * @param status {boolean} Status of the checkbox + * @return {Promise} + */ + async setAdvancedFiltersCategory(page: Page, categoryName: string, status: boolean = true): Promise { + await page.locator(this.advFiltersFilterCategoriesExpandBtn).click(); + + // Choose category to filter with + const args = { + selector: this.advFiltersFilterCategoriesLabel, + categoryName, + status, + }; + const fn: {categoryClick: PageFunction<{ + selector: string, + categoryName: string, + status: boolean + // eslint-disable-next-line no-eval + }, boolean>} = eval(`({ + async categoryClick(args) { + /* eslint-env browser */ + const allCategories = [...await document.querySelectorAll(args.selector)]; + const category = await allCategories.find((el) => el.textContent === args.categoryName); + + if (category === undefined) { + return false; + } + + const checkbox = await category.parentNode.querySelector('input'); + if (checkbox.checked !== args.status) { + checkbox.click(); + } + + return true; + } + })`); + const found = await page.evaluate(fn.categoryClick, args); + + if (!found) { + throw new Error(`${categoryName} not found as a category`); + } + if (await this.elementVisible(page, this.productListLoading, 5000)) { + await this.waitForHiddenSelector(page, this.productListLoading); + } + await page.waitForTimeout(10000); + } + + /** + * Set Filter "Date" + * @param page {Page} Browser tab + * @param type {'inf'|'sup'} Type + * @param date {string} Date + * @param onChange {boolean} Dispatch event change + * @return {Promise} + */ + async setAdvancedFiltersDate(page: Page, type: 'inf'|'sup', date: string, onChange: boolean = false): Promise { + const selector: string = type === 'inf' ? this.advFiltersFilterDateInfInput : this.advFiltersFilterDateSupInput; + + await this.waitForVisibleSelector(page, selector); + await this.setValueOnDateTimePickerInput(page, selector, date, onChange); + if (onChange) { + if (await this.elementVisible(page, this.productListLoading, 5000)) { + await this.waitForHiddenSelector(page, this.productListLoading); + } + await page.waitForTimeout(10000); + } + } + + /** + * Set Filter "Employee" + * @param page {Page} Browser tab + * @param employeeName {string} Employee Name + * @return {Promise} + */ + async setAdvancedFiltersEmployee(page: Page, employeeName: string): Promise { + await this.waitForVisibleSelector(page, this.advFiltersFilterEmployeeSelect); + await this.selectByVisibleText(page, this.advFiltersFilterEmployeeSelect, employeeName, true); + await page.waitForResponse('**/api/stock-movements/**'); + if (await this.elementVisible(page, this.productListLoading, 5000)) { + await this.waitForHiddenSelector(page, this.productListLoading); + } + await page.waitForTimeout(10000); + } + + /** + * Set Filter "Movement Type" + * @param page {Page} Browser tab + * @param movementType {'None'|'Employee Edition'|'Customer Order'} Movement type + * @return {Promise} + */ + async setAdvancedFiltersMovementType(page: Page, movementType: 'None'|'Employee Edition'|'Customer Order'): Promise { + await this.waitForVisibleSelector(page, this.advFiltersFilterMvtTypeSelect); + await this.selectByVisibleText(page, this.advFiltersFilterMvtTypeSelect, movementType); + if (await this.elementVisible(page, this.productListLoading, 5000)) { + await this.waitForHiddenSelector(page, this.productListLoading); + } + await page.waitForTimeout(10000); + } + + /** + * Set Filter "Status" + * @param page {Page} Browser tab + * @param status {boolean|null} Status + * @return {Promise} + */ + async setAdvancedFiltersStatus(page: Page, status: boolean | null): Promise { + let selector: string; + + if (status === null) { + selector = this.advFiltersFilterStatusAll; + } else { + selector = status ? this.advFiltersFilterStatusEnabled : this.advFiltersFilterStatusDisabled; + } + + await this.waitForVisibleSelector(page, selector); + await page.locator(selector).click(); + if (await this.elementVisible(page, this.productListLoading, 1000)) { + await this.waitForHiddenSelector(page, this.productListLoading); + } + await page.waitForTimeout(10000); + } + + /** + * Reset Advanced filter + * @param page {Page} Browser tab + * @return {Promise} + */ + async resetAdvancedFilter(page: Page): Promise { + await this.reloadPage(page); + await page.waitForResponse('**/api/stock-movements/**'); + await this.waitForHiddenSelector(page, this.productListLoading, 30000); + } + + /** + * Click on edit feature + * @param page {Page} Browser tab + * @param row {number} Feature row in table + * @return {Promise} + */ + async clickOnMovementTypeLink(page: Page, row: number): Promise { + await this.waitForVisibleSelector(page, this.tableTypeColumnLink(row)); + return this.openLinkWithTargetBlank(page, this.tableTypeColumnLink(row)); + } + + /* Table methods */ + /** + * Get text from column in table + * @param page {Page} Browser tab + * @param row {number} Row on table + * @param column {string} Column to get text value + * @return {Promise} + */ + async getTextColumnFromTable(page: Page, row: number, column: string): Promise { + let productAttribute = ''; + + if (await this.elementVisible(page, `${this.tableProductNameColumn(row)} small`, 1000)) { + productAttribute = await this.getTextContent(page, `${this.tableProductNameColumn(row)} small`); + } + switch (column) { + case 'product_id': + return this.getTextContent(page, this.tableProductId(row)); + case 'product_name': + return (await this.getTextContent(page, this.tableProductNameColumn(row))).replace(productAttribute, ''); + case 'reference': + return this.getTextContent(page, this.tableProductReferenceColumn(row)); + case 'quantity': + return (await this.getTextContent(page, this.tableQuantityColumn(row))).replace(' ', ''); + case 'date_add': + return this.getTextContent(page, this.tableProductDateColumn(row)); + default: + throw new Error(`${column} was not find as column in this table`); + } + } + + /** + Get text for empty table + * @param page {Page} Browser tab + * @return {Promise} + */ + async getTextForEmptyTable(page: Page): Promise { + return this.getTextContent(page, this.tableRowEmpty); + } + + /** + * Get number of element in movements grid + * @param page {Page} Browser tab + * @return {Promise} + */ + async getNumberOfElementInGrid(page: Page): Promise { + return page.locator(this.tableRows).count(); + } + + /** + * Get content from all rows + * @param page {Page} Browser tab + * @param column {string} Column name to get all rows content + * @return {Promise>} + */ + async getAllRowsColumnContent(page: Page, column: string): Promise { + await this.waitForHiddenSelector(page, this.productListLoading); + const rowsNumber = await this.getNumberOfElementInGrid(page); + const allRowsContentTable: string[] = []; + + for (let i = 1; i <= rowsNumber; i++) { + const rowContent = await this.getTextColumnFromTable(page, i, column); + allRowsContentTable.push(rowContent); + } + + return allRowsContentTable; + } + + /** + * Sort table by clicking on column name + * @param page {Page} Browser tab + * @param sortBy {string} Column to sort with + * @param sortDirection {string} Sort direction asc or desc + * @return {Promise} + */ + async sortTable(page: Page, sortBy: string, sortDirection: string): Promise { + await this.waitForHiddenSelector(page, this.productListLoading); + const sortColumnDiv = `${this.sortColumnDiv(sortBy)}[data-sort-direction='${sortDirection}']`; + const sortColumnSpanButton = this.sortColumnSpanButton(sortBy); + + let i: number = 0; + while (await this.elementNotVisible(page, sortColumnDiv, 2000) && i < 2) { + await this.waitForSelectorAndClick(page, sortColumnSpanButton); + i += 1; + } + + await this.waitForHiddenSelector(page, this.productListLoading); + } + + /** + * Paginate to page + * @param page {Page} Browser tab + * @param pageNumber {number} Value of page to go + * @return {Promise} + */ + async paginateTo(page: Page, pageNumber: number = 1): Promise { + await page.locator(this.paginationListItemLink(pageNumber)).click(); + if (await this.elementVisible(page, this.productListLoading, 1000)) { + await this.waitForHiddenSelector(page, this.productListLoading); + } + + return this.getNumberFromText(page, `${this.paginationListItem}.active`); + } +} + +module.exports = new BOStockMovementsPage();