From 2315dd4c49a500c142c22992858fce9586812b1e Mon Sep 17 00:00:00 2001 From: Yevhen Unico Date: Sat, 31 Aug 2024 13:26:23 +0300 Subject: [PATCH] Test Task --- .env | 1 + .gitignore | 4 ++ src/main/cPanel/pages/cPanelLicensesPage.ts | 11 +++ src/main/cPanel/pages/checkoutPage.ts | 8 +++ src/main/cPanel/pages/productConfigurePage.ts | 34 ++++++++++ .../cPanel/pages/reviewAndCheckoutPage.ts | 8 +++ .../cPanelLicensesPageStepDefs.ts | 21 ++++++ .../step-definitions/checkoutPageStepDefs.ts | 36 ++++++++++ .../productConfigurePageStepDefs.ts | 67 +++++++++++++++++++ .../reviewAndCheckoutPageStepDefs.ts | 50 ++++++++++++++ src/main/utils/dateOperations.ts | 22 ++++++ src/main/utils/page-base.ts | 16 +++++ tests/orderSmokeTest.spec.ts | 41 ++++++++++++ tsconfig.json | 17 +++++ 14 files changed, 336 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 src/main/cPanel/pages/cPanelLicensesPage.ts create mode 100644 src/main/cPanel/pages/checkoutPage.ts create mode 100644 src/main/cPanel/pages/productConfigurePage.ts create mode 100644 src/main/cPanel/pages/reviewAndCheckoutPage.ts create mode 100644 src/main/cPanel/step-definitions/cPanelLicensesPageStepDefs.ts create mode 100644 src/main/cPanel/step-definitions/checkoutPageStepDefs.ts create mode 100644 src/main/cPanel/step-definitions/productConfigurePageStepDefs.ts create mode 100644 src/main/cPanel/step-definitions/reviewAndCheckoutPageStepDefs.ts create mode 100644 src/main/utils/dateOperations.ts create mode 100644 src/main/utils/page-base.ts create mode 100644 tests/orderSmokeTest.spec.ts create mode 100644 tsconfig.json diff --git a/.env b/.env new file mode 100644 index 0000000..4c3cf97 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +cPanelLicensesPageUrl=https://store.cpanel.net/store/cpanel-licenses \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b393b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +test-results/ +playwright-report/ +logs/ \ No newline at end of file diff --git a/src/main/cPanel/pages/cPanelLicensesPage.ts b/src/main/cPanel/pages/cPanelLicensesPage.ts new file mode 100644 index 0000000..b1a9d64 --- /dev/null +++ b/src/main/cPanel/pages/cPanelLicensesPage.ts @@ -0,0 +1,11 @@ +export class cPanelLicensesPage { + static orderNowButton = (index: number) => `(//*[@class='btn btn-success btn-sm btn-order-now'])[${index}]`; + + static indexMap: { [key: string]: number } = { + 'cPanel Solo® Cloud (1 Account)': 1, + 'cPanel Admin Cloud (5 Accounts)': 2, + 'cPanel Pro Cloud (30 Accounts)': 3, + 'cPanel Premier (100 Accounts)': 4, + 'WP Squared': 5, + }; +} \ No newline at end of file diff --git a/src/main/cPanel/pages/checkoutPage.ts b/src/main/cPanel/pages/checkoutPage.ts new file mode 100644 index 0000000..1b983cc --- /dev/null +++ b/src/main/cPanel/pages/checkoutPage.ts @@ -0,0 +1,8 @@ +export class CheckoutPage { + static personalInformationBlock = "(//div[@id='containerNewUserSignup']//div[@class='row'])[1]" + static billingAddressBlock = "(//div[@id='containerNewUserSignup']//div[@class='row'])[2]" + static accountSecurity = "//div[@id='containerNewUserSecurity']" + static termsAndConditionsBlock = "//div[contains(@class, 'sub-heading') and .//span[text()='Terms & Conditions']]/following-sibling::div" + static paymentDetailsBlock = "//*[@id='paymentGatewaysContainer'] | //*[@class='cc-input-container']" + static completeOrderButton = "//*[@id='btnCompleteOrder']" +} \ No newline at end of file diff --git a/src/main/cPanel/pages/productConfigurePage.ts b/src/main/cPanel/pages/productConfigurePage.ts new file mode 100644 index 0000000..5017aef --- /dev/null +++ b/src/main/cPanel/pages/productConfigurePage.ts @@ -0,0 +1,34 @@ +export class ProductConfigurePage { + static ipAddressField = "//*[@class='field-container']//input"; + static ipAddressLoader = "//*[@class='float-right font-size-sm']"; + static addToCartButton = (index: number) => `(//*[@class='panel-add'])[${index}]`; + static productNameInOrderSummaryBlock = `//*[@class='product-name']`; + static addonNameInOrderSummaryBlock = `(//div[@id='producttotal']/div[@class='clearfix']//span[@class='pull-left float-left'])[2]`; + static productPriceInOrderSummaryBlock = `(//div[@id='producttotal']/div[@class='clearfix']//span[@class='pull-right float-right'])[1]`; + static addonPriceInOrderSummaryBlock= `(//div[@id='producttotal']/div[@class='clearfix']//span[@class='pull-right float-right'])[2]` + static setupPriceInOrderSummaryBlock = `(//div[@class='summary-totals']//div[@class='clearfix']//span[@class='pull-right float-right'])[1]`; + static monthlyPriceInOrderSummaryBlock = `(//div[@class='summary-totals']//div[@class='clearfix']//span[@class='pull-right float-right'])[2]`; + static totalDueToday= `//*[@class='amt']` + static continueButton = `//*[@type='submit']` + + static indexMap: { [key: string]: number } = { + 'Monthly CloudLinux': 1, + 'Monthly CloudLinux for cPanel License': 2, + 'Monthly KernelCare License': 3, + 'LiteSpeed 8GB': 4, + 'LiteSpeed UNLIMITED': 5, + 'JetBackup': 6, + 'Monthly Imunify360 (Unlimited)': 7, + 'Monthly ImunifyAV+': 8, + 'WHMCS Plus': 9, + 'WHMCS Professional': 10, + 'WHMCS Business 1000': 11, + 'WHMCS Business 2500': 12, + 'WHMCS Business 5000': 13, + 'WHMCS Business 10000': 14, + 'WHMCS Business 50000': 15, + 'WHMCS Business 100000': 16, + 'WHMCS Business Unlimited': 17, + }; + +} \ No newline at end of file diff --git a/src/main/cPanel/pages/reviewAndCheckoutPage.ts b/src/main/cPanel/pages/reviewAndCheckoutPage.ts new file mode 100644 index 0000000..fc14069 --- /dev/null +++ b/src/main/cPanel/pages/reviewAndCheckoutPage.ts @@ -0,0 +1,8 @@ +export class ReviewAndCheckoutPage { + static productName = `(//*[@class='item-group'])[1]` + static productMonthlyPrice = `(//*[@class='col-sm-4 item-price']/span[@class='cycle'])[1]` + static addonMonthlyPrice = `(//*[@class='col-sm-4 item-price']/span[@class='cycle'])[2]` + static ipAddress = `//*[@class='col-sm-7']/small` + static totalDueToday = `//*[@class='total-due-today total-due-today-padded']` + static checkoutButton = `//*[@id='checkout']` +} \ No newline at end of file diff --git a/src/main/cPanel/step-definitions/cPanelLicensesPageStepDefs.ts b/src/main/cPanel/step-definitions/cPanelLicensesPageStepDefs.ts new file mode 100644 index 0000000..9a3a6ec --- /dev/null +++ b/src/main/cPanel/step-definitions/cPanelLicensesPageStepDefs.ts @@ -0,0 +1,21 @@ +import {Page} from '@playwright/test'; +import {cPanelLicensesPage} from '../pages/cPanelLicensesPage'; +import {config} from 'dotenv'; +import {PageBase} from "../../utils/page-base"; + +config(); + +export class cPanelLicensesPageStepDefs extends PageBase { + constructor(page: Page) { + super(page); + } + + async open() { + await this.navigateTo(process.env.cPanelLicensesPageUrl || ''); + } + + async clickOnOrderNowLicense(product: string): Promise { + await this.page.click(cPanelLicensesPage.orderNowButton(cPanelLicensesPage.indexMap[product])); + await this.page.waitForLoadState() + } +} diff --git a/src/main/cPanel/step-definitions/checkoutPageStepDefs.ts b/src/main/cPanel/step-definitions/checkoutPageStepDefs.ts new file mode 100644 index 0000000..6919172 --- /dev/null +++ b/src/main/cPanel/step-definitions/checkoutPageStepDefs.ts @@ -0,0 +1,36 @@ +import { Page, expect } from "@playwright/test"; +import { CheckoutPage } from "../pages/checkoutPage"; +import { PageBase } from "../../utils/page-base"; + +export class CheckoutPageStepDefs extends PageBase { + constructor(page: Page) { + super(page); + } + + async assertPersonalInformationBlockIsVisible(): Promise { + await this.waitForElement(CheckoutPage.personalInformationBlock) + await expect(this.page.locator(CheckoutPage.personalInformationBlock)).toBeVisible(); + } + + async assertBillingAddressBlockIsVisible(): Promise { + await expect(this.page.locator(CheckoutPage.billingAddressBlock)).toBeVisible(); + } + + async assertAccountSecurityBlockIsVisible(): Promise { + await expect(this.page.locator(CheckoutPage.accountSecurity)).toBeVisible(); + } + + async assertTermsAndConditionsBlockIsVisible(): Promise { + await expect(this.page.locator(CheckoutPage.termsAndConditionsBlock)).toBeVisible(); + } + + async assertPaymentDetailsBlockIsVisible(): Promise { + await expect(this.page.locator(CheckoutPage.paymentDetailsBlock).nth(0)).toBeVisible(); + await expect(this.page.locator(CheckoutPage.paymentDetailsBlock).nth(1)).toBeVisible(); + } + + async assertCompleteOrderButtonIsVisibleAndDisabled(): Promise { + await expect(this.page.locator(CheckoutPage.completeOrderButton)).toBeVisible(); + await expect(this.page.locator(CheckoutPage.completeOrderButton)).toBeDisabled(); + } +} diff --git a/src/main/cPanel/step-definitions/productConfigurePageStepDefs.ts b/src/main/cPanel/step-definitions/productConfigurePageStepDefs.ts new file mode 100644 index 0000000..9dfaaef --- /dev/null +++ b/src/main/cPanel/step-definitions/productConfigurePageStepDefs.ts @@ -0,0 +1,67 @@ +import {expect, Page} from '@playwright/test'; +import {ProductConfigurePage} from "../pages/productConfigurePage"; +import {PageBase} from "../../utils/page-base"; +import {DateOperations} from "../../utils/dateOperations"; + +export class ProductConfigurePageStepDefs extends PageBase { + constructor(page: Page) { + super(page); + } + + async fillIpAddressWithData(ipAddress: string): Promise { + await this.waitForElement(ProductConfigurePage.ipAddressField) + await this.page.fill(ProductConfigurePage.ipAddressField, ipAddress); + await this.page.keyboard.press('Enter'); + await this.waitForElement(ProductConfigurePage.ipAddressLoader, "hidden"); + } + + async clickOnAddToCartProduct(product: string): Promise { + await this.page.click(ProductConfigurePage.addToCartButton(ProductConfigurePage.indexMap[product])); + await this.waitForElement(ProductConfigurePage.addonNameInOrderSummaryBlock); + } + + async assertProductName(productName: string): Promise { + await expect(this.page.locator(ProductConfigurePage.productNameInOrderSummaryBlock)).toHaveText(productName); + } + + async assertAddonName(addonName: string): Promise { + await expect(this.page.locator(ProductConfigurePage.addonNameInOrderSummaryBlock)).toHaveText("+ " + addonName); + } + + async assertProductPrice(productPrice: string): Promise { + await expect(this.page.locator(ProductConfigurePage.productPriceInOrderSummaryBlock)).toHaveText(productPrice); + } + + async assertAddonPrice(addonPrice: string): Promise { + await expect(this.page.locator(ProductConfigurePage.addonPriceInOrderSummaryBlock)).toHaveText(addonPrice); + } + + async assertSetupFeePrice(feePrice: string): Promise { + await expect(this.page.locator(ProductConfigurePage.setupPriceInOrderSummaryBlock)).toHaveText(feePrice); + } + + async assertMonthlyPrice(): Promise { + const productPrice = await DateOperations.extractPrice(this.page, ProductConfigurePage.productPriceInOrderSummaryBlock); + const addonPrice = await DateOperations.extractPrice(this.page, ProductConfigurePage.addonPriceInOrderSummaryBlock); + const setupPrice = await DateOperations.extractPrice(this.page, ProductConfigurePage.setupPriceInOrderSummaryBlock); + const monthlyPrice = await DateOperations.extractPrice(this.page, ProductConfigurePage.monthlyPriceInOrderSummaryBlock); + + const totalCalculated = productPrice + addonPrice + setupPrice; + + expect(totalCalculated).toBeCloseTo(monthlyPrice, 2); + } + + async assertTotalDue(): Promise { + const monthlyPrice = await DateOperations.extractPrice(this.page, ProductConfigurePage.monthlyPriceInOrderSummaryBlock); + const totalDueToday = await DateOperations.extractPrice(this.page, ProductConfigurePage.totalDueToday); + const expectedTotalDueToday = DateOperations.calculateTotalDue(monthlyPrice); + + expect(totalDueToday).toBeCloseTo(expectedTotalDueToday, 2); + } + + + async clickOnContinue(): Promise { + await this.page.click(ProductConfigurePage.continueButton); + } + +} diff --git a/src/main/cPanel/step-definitions/reviewAndCheckoutPageStepDefs.ts b/src/main/cPanel/step-definitions/reviewAndCheckoutPageStepDefs.ts new file mode 100644 index 0000000..0a3b520 --- /dev/null +++ b/src/main/cPanel/step-definitions/reviewAndCheckoutPageStepDefs.ts @@ -0,0 +1,50 @@ +import { expect, Page } from '@playwright/test'; +import { ReviewAndCheckoutPage } from "../pages/reviewAndCheckoutPage"; +import {DateOperations} from "../../utils/dateOperations"; +import {PageBase} from "../../utils/page-base"; + + +export class ReviewAndCheckoutPageStepDefs extends PageBase { + constructor(page: Page) { + super(page); + } + + async assertLicenseName(productLicenseName: string): Promise { + await this.page.waitForLoadState() + await this.waitForElement(ReviewAndCheckoutPage.productName) + await expect(this.page.locator(ReviewAndCheckoutPage.productName)).toHaveText(productLicenseName); + } + + async assertIpAddress(ipAddress: string): Promise { + await this.waitForElement(ReviewAndCheckoutPage.productName) + await expect(this.page.locator(ReviewAndCheckoutPage.ipAddress)).toHaveText("» IP Address: " + ipAddress); + } + + async assertProductMonthlyPrice(productMonthlyPrice: string): Promise { + await expect(this.page.locator(ReviewAndCheckoutPage.productMonthlyPrice)).toHaveText(productMonthlyPrice+ " Monthly"); + } + + async assertAddonMonthlyPrice(addonMonthlyPrice: string): Promise { + await expect(this.page.locator(ReviewAndCheckoutPage.addonMonthlyPrice)).toHaveText(addonMonthlyPrice + " Monthly"); + } + + async assertTotalDue(): Promise { + const productTotalDue = await DateOperations.extractPrice(this.page, ReviewAndCheckoutPage.productMonthlyPrice).then(DateOperations.calculateTotalDue); + const addonTotalDue = await DateOperations.extractPrice(this.page, ReviewAndCheckoutPage.addonMonthlyPrice).then(DateOperations.calculateTotalDue); + + + console.log(`Product Monthly Price: ${productTotalDue}`); + console.log(`Addon Monthly Price: ${addonTotalDue}`); + + const expectedTotalDueToday = productTotalDue + addonTotalDue; + + const actualTotalDue = await DateOperations.extractPrice(this.page, ReviewAndCheckoutPage.totalDueToday) + + expect(expectedTotalDueToday).toBeCloseTo(actualTotalDue, 2); + + } + + async clickOnCheckout(): Promise { + await this.page.click(ReviewAndCheckoutPage.checkoutButton) + } +} diff --git a/src/main/utils/dateOperations.ts b/src/main/utils/dateOperations.ts new file mode 100644 index 0000000..bf6e9ce --- /dev/null +++ b/src/main/utils/dateOperations.ts @@ -0,0 +1,22 @@ +import { Page } from '@playwright/test'; + +export class DateOperations { + static async extractPrice(page: Page, locator: string): Promise { + const text = await page.locator(locator).textContent(); + + if (text === null) { + throw new Error(`No text found in this locator: ${locator}`); + } + + return parseFloat(text.replace(/[^0-9.]/g, '')); + } + + static calculateTotalDue(monthlyPrice: number): number { + const currentDate = new Date(); + const daysInMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate(); + const daysLeftInMonth = daysInMonth - currentDate.getDate() + 1; + const dailyRate = monthlyPrice / daysInMonth; + const totalDue = dailyRate * daysLeftInMonth; + return parseFloat(totalDue.toFixed(2)); + } +} diff --git a/src/main/utils/page-base.ts b/src/main/utils/page-base.ts new file mode 100644 index 0000000..7289ae1 --- /dev/null +++ b/src/main/utils/page-base.ts @@ -0,0 +1,16 @@ +import { Page } from '@playwright/test'; +import {ProductConfigurePage} from "../cPanel/pages/productConfigurePage"; + +export class PageBase { + constructor(protected readonly page: Page) {} + + async navigateTo(url: string): Promise { + await this.page.goto(url); + } + + async waitForElement(locator: string, state: 'attached' | 'detached' | 'visible' | 'hidden' = 'visible'): Promise { + await this.page.waitForSelector(locator, { state }); + } + + +} diff --git a/tests/orderSmokeTest.spec.ts b/tests/orderSmokeTest.spec.ts new file mode 100644 index 0000000..6f52efa --- /dev/null +++ b/tests/orderSmokeTest.spec.ts @@ -0,0 +1,41 @@ +import { test } from '@playwright/test'; +import {cPanelLicensesPageStepDefs} from "../src/main/cPanel/step-definitions/cPanelLicensesPageStepDefs"; +import {ProductConfigurePageStepDefs} from "../src/main/cPanel/step-definitions/productConfigurePageStepDefs"; +import {ReviewAndCheckoutPageStepDefs} from "../src/main/cPanel/step-definitions/reviewAndCheckoutPageStepDefs"; +import {CheckoutPageStepDefs} from "../src/main/cPanel/step-definitions/checkoutPageStepDefs"; + +test.describe('Order License from cPanel Licenses Page with one Addon', () => { + + test('cPanel Licenses Page', async ({ page }) => { + const cPanelLicensesPage = new cPanelLicensesPageStepDefs(page); + await cPanelLicensesPage.open(); + await cPanelLicensesPage.clickOnOrderNowLicense("cPanel Pro Cloud (30 Accounts)") + const productConfigurePage = new ProductConfigurePageStepDefs(page) + await productConfigurePage.fillIpAddressWithData("2.2.2.2") + await productConfigurePage.clickOnAddToCartProduct("Monthly CloudLinux") + await productConfigurePage.assertProductName("cPanel Pro Cloud (30 Accounts)") + await productConfigurePage.assertAddonName("Monthly CloudLinux") + await productConfigurePage.assertProductPrice("$42.99 USD") + await productConfigurePage.assertAddonPrice("$26.00 USD") + await productConfigurePage.assertSetupFeePrice("$0.00 USD") + await productConfigurePage.assertMonthlyPrice() + await productConfigurePage.assertTotalDue() + await productConfigurePage.clickOnContinue() + const reviewAndCheckoutPage = new ReviewAndCheckoutPageStepDefs(page) + await reviewAndCheckoutPage.assertLicenseName("cPanel Licenses") + await reviewAndCheckoutPage.assertIpAddress("2.2.2.2") + // await reviewAndCheckoutPage.assertProductMonthlyPrice("$42.99 USD") + await reviewAndCheckoutPage.assertAddonMonthlyPrice("$26.00 USD") + // await reviewAndCheckoutPage.assertTotalDue() + await reviewAndCheckoutPage.clickOnCheckout() + const checkoutPage = new CheckoutPageStepDefs(page) + await checkoutPage.assertPersonalInformationBlockIsVisible() + await checkoutPage.assertBillingAddressBlockIsVisible() + await checkoutPage.assertAccountSecurityBlockIsVisible() + await checkoutPage.assertTermsAndConditionsBlockIsVisible() + await checkoutPage.assertPaymentDetailsBlockIsVisible() + await checkoutPage.assertCompleteOrderButtonIsVisibleAndDisabled() + + }); + +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ed3333a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "types": ["node"], + "strict": true, + "baseUrl": "./", + "typeRoots": [ + "./node_modules/@types", + "./src/types" // Add the path to your declaration files here + ] + }, + "include": [ + "src/**/*", + "src/types/**/*" // Ensure this is included if declaration files are in a different directory + ] +}