diff --git a/src/page-objects/StorefrontPages.ts b/src/page-objects/StorefrontPages.ts index 2a9ed20..8a016b7 100644 --- a/src/page-objects/StorefrontPages.ts +++ b/src/page-objects/StorefrontPages.ts @@ -21,6 +21,8 @@ import { SearchSuggest } from './storefront/SearchSuggest'; import { CustomRegister } from './storefront/CustomRegister'; import { CheckoutOrderEdit } from './storefront/CheckoutOrderEdit'; import { AccountAddressCreate } from './storefront/AccountAddresssCreate'; +import { ContactForm } from './storefront/ContactForm'; +import { Wishlist } from './storefront/Wishlist'; export interface StorefrontPageTypes { StorefrontHome: Home; @@ -43,6 +45,8 @@ export interface StorefrontPageTypes { StorefrontSearchSuggest: SearchSuggest; StorefrontCustomRegister: CustomRegister; StorefrontCheckoutOrderEdit: CheckoutOrderEdit; + StorefrontContactForm: ContactForm; + StorefrontWishlist: Wishlist; } export const StorefrontPageObjects = { @@ -66,6 +70,8 @@ export const StorefrontPageObjects = { SearchSuggest, CustomRegister, CheckoutOrderEdit, + ContactForm, + Wishlist, } export const test = base.extend({ @@ -144,10 +150,17 @@ export const test = base.extend({ StorefrontCustomRegister: async ({ StorefrontPage }, use) => { await use(new CustomRegister(StorefrontPage)); - }, StorefrontCheckoutOrderEdit: async ({ StorefrontPage }, use) => { await use(new CheckoutOrderEdit(StorefrontPage)); }, + + StorefrontContactForm: async ({ StorefrontPage }, use) => { + await use(new ContactForm(StorefrontPage)); + }, + + StorefrontWishlist: async ({ StorefrontPage }, use) => { + await use(new Wishlist(StorefrontPage)); + }, }); diff --git a/src/page-objects/storefront/AccountLogin.ts b/src/page-objects/storefront/AccountLogin.ts index ab92621..f5d5df0 100644 --- a/src/page-objects/storefront/AccountLogin.ts +++ b/src/page-objects/storefront/AccountLogin.ts @@ -29,6 +29,13 @@ export class AccountLogin implements PageObject { public readonly postalCodeInput: Locator; public readonly registerButton: Locator; + // Inputs for reCaptcha + public readonly greCaptchaV2Container: Locator; + public readonly greCaptchaV2Input: Locator; + public readonly greCaptchaV3Input: Locator; + public readonly greCaptchaProtectionInformation: Locator; + public readonly greCaptchaBadge: Locator; + constructor(public readonly page: Page) { this.emailInput = page.getByLabel('Your email address'); this.passwordInput = page.getByLabel('Your password'); @@ -56,6 +63,12 @@ export class AccountLogin implements PageObject { this.logoutLink = page.getByRole('link', { name: 'Log out'}); this.successAlert = page.getByText('Successfully logged out.'); this.passwordUpdatedAlert = page.getByText('Your password has been updated.'); + + this.greCaptchaV2Container = this.page.locator('.grecaptcha-v2-container'); + this.greCaptchaV2Input = this.page.locator('.grecaptcha-v2-input'); + this.greCaptchaV3Input = this.page.locator('.grecaptcha_v3-input'); + this.greCaptchaProtectionInformation = this.page.locator('.grecaptcha-protection-information'); + this.greCaptchaBadge = this.page.locator('.grecaptcha-badge'); } url() { diff --git a/src/page-objects/storefront/ContactForm.ts b/src/page-objects/storefront/ContactForm.ts new file mode 100644 index 0000000..f6a19fe --- /dev/null +++ b/src/page-objects/storefront/ContactForm.ts @@ -0,0 +1,57 @@ +import type { Page, Locator } from '@playwright/test'; +import type { PageObject } from '../../types/PageObject'; +import { Home } from './Home'; + +export class ContactForm extends Home implements PageObject { + public readonly contactModal: Locator; + public readonly contactSuccessModal: Locator; + public readonly salutationSelect: Locator; + public readonly firstNameInput: Locator; + public readonly lastNameInput: Locator; + public readonly emailInput: Locator; + public readonly phoneInput: Locator; + public readonly subjectInput: Locator; + public readonly commentInput: Locator; + public readonly privacyPolicyCheckbox: Locator; + public readonly submitButton: Locator; + public readonly contactSuccessMessage: Locator; + public readonly cardTitle: Locator; + /** + * Captcha locators + */ + public readonly basicCaptcha: Locator; + public readonly basicCaptchaImage: Locator; + public readonly basicCaptchaRefreshButton: Locator; + public readonly basicCaptchaInput: Locator; + public readonly greCaptchaV2Container: Locator; + public readonly greCaptchaV2Input: Locator; + public readonly greCaptchaProtectionInformation: Locator; + + constructor(public readonly page: Page) { + super(page); + this.contactModal = this.page.getByRole('dialog').filter({ has: this.page.getByLabel('Contact', { exact: true }) }); + this.salutationSelect = this.contactModal.getByLabel('Salutation*'); + this.firstNameInput = this.contactModal.getByLabel('First name*'); + this.lastNameInput = this.contactModal.getByLabel('Last name*'); + this.emailInput = this.contactModal.getByLabel('Your email address*'); + this.phoneInput = this.contactModal.getByLabel('Phone*'); + this.subjectInput = this.contactModal.getByLabel('Subject*'); + this.commentInput = this.contactModal.getByLabel('Comment*'); + this.privacyPolicyCheckbox = this.contactModal.getByRole('checkbox', { name: 'By selecting continue you confirm that you have read and agree to our' }); + this.submitButton = this.contactModal.getByRole('button', { name: 'Submit' }); + this.contactSuccessModal = this.page.getByRole('dialog').filter({ has: this.page.locator('.confirm-message') }); + this.contactSuccessMessage = this.contactSuccessModal.locator('.confirm-message'); + this.cardTitle = this.contactModal.locator('.card-title'); + this.basicCaptcha = this.contactModal.locator('.basic-captcha'); + this.basicCaptchaImage = this.basicCaptcha.locator('img'); + this.basicCaptchaRefreshButton = this.basicCaptcha.locator('.basic-captcha-content-refresh-icon'); + this.basicCaptchaInput = this.basicCaptcha.locator('input[name="shopware_basic_captcha_confirm"]'); + this.greCaptchaV2Container = this.contactModal.locator('.grecaptcha-v2-container'); + this.greCaptchaV2Input = this.contactModal.locator('.grecaptcha-v2-input'); + this.greCaptchaProtectionInformation = this.contactModal.locator('.grecaptcha-protection-information'); +} + + url() { + return new Error('Function not implemented, because it is a modal page object').message; + } +} diff --git a/src/page-objects/storefront/Home.ts b/src/page-objects/storefront/Home.ts index ea862f8..215a115 100644 --- a/src/page-objects/storefront/Home.ts +++ b/src/page-objects/storefront/Home.ts @@ -18,6 +18,8 @@ export class Home implements PageObject { public readonly consentDialog: Locator; public readonly consentDialogTechnicallyRequiredCheckbox: Locator; public readonly consentDialogStatisticsCheckbox: Locator; + public readonly contactFormLink: Locator; + /** * @deprecated Use 'consentDialogMarketingCheckbox' instead */ @@ -28,6 +30,10 @@ export class Home implements PageObject { public readonly consentCookieBannerContainer: Locator; public readonly offcanvasBackdrop: Locator; + //wishlist + public readonly wishlistIcon: Locator; + public readonly wishlistBasket: Locator; + constructor(public readonly page: Page) { this.accountMenuButton = page.getByLabel('Your account'); this.closeGuestSessionButton = page.locator('.account-aside-btn'); @@ -63,6 +69,11 @@ export class Home implements PageObject { exact: true, }); this.offcanvasBackdrop = page.locator('.offcanvas-backdrop'); + this.contactFormLink = this.page.getByRole('listitem').getByTitle('Contact form', { exact: true }); + + //wishlist + this.wishlistIcon = page.locator('.header-wishlist-icon'); + this.wishlistBasket = page.locator('.header-wishlist-badge'); } async getMenuItemByCategoryName(categoryName: string): Promise> { @@ -97,6 +108,8 @@ export class Home implements PageObject { const productAddToShoppingCart = listingItem.getByRole('button', { name: 'Add to shopping cart', }); + const wishlistNotAddedIcon = listingItem.locator('.product-wishlist-not-added'); + const wishlistAddedIcon = listingItem.locator('.product-wishlist-added'); return { productImage: productImage, @@ -108,10 +121,12 @@ export class Home implements PageObject { productPrice: productPrice, productName: productName, productAddToShoppingCart: productAddToShoppingCart, + wishlistNotAddedIcon: wishlistNotAddedIcon, + wishlistAddedIcon: wishlistAddedIcon, }; } url() { return './'; } -} +} \ No newline at end of file diff --git a/src/services/TestDataService.ts b/src/services/TestDataService.ts index f6b0baf..e418a8b 100644 --- a/src/services/TestDataService.ts +++ b/src/services/TestDataService.ts @@ -33,6 +33,7 @@ import type { CustomFieldSet, CustomField, Tax, + ProductCrossSelling, } from '../types/ShopwareTypes'; import { expect } from '@playwright/test'; @@ -100,7 +101,7 @@ export class TestDataService { * * @private */ - private highPriorityEntities = ['order', 'product', 'landing_page', 'shipping_method', 'sales_channel_domain', 'sales_channel_currency', 'sales_channel_country', 'customer']; + private highPriorityEntities = ['order', 'product_cross_selling' , 'product', 'landing_page', 'shipping_method', 'sales_channel_domain', 'sales_channel_currency', 'sales_channel_country', 'customer']; /** * A registry of all created records. @@ -475,6 +476,27 @@ export class TestDataService { return propertyGroup; } + /** + * Creates a basic product cross-selling entity without products. + * + * @param productId - The uuid of the product to which the pproduct cross-selling should be assigned. + * @param overrides - Specific data overrides that will be applied to the property group data struct. + */ + async createProductCrossSelling(productId: string, overrides: Partial = {}): Promise { + const crossSellingStruct = this.getBasicCrossSellingStruct(productId, overrides); + + const response = await this.AdminApiClient.post('product-cross-selling?_response=detail', { + data: crossSellingStruct, + }); + expect(response.ok()).toBeTruthy(); + + const { data: productCrossSelling } = (await response.json()) as { data: ProductCrossSelling }; + + this.addCreatedRecord('product_cross_selling', productCrossSelling.id); + + return productCrossSelling; + } + /** * Creates a new tag which can be assigned to other entities. * @@ -1640,7 +1662,6 @@ export class TestDataService { deleteOperations[`delete-${record.resource}`].payload.push(record.payload); } }); - await this.AdminApiClient.post('_action/sync', { data: priorityDeleteOperations, }); @@ -2516,4 +2537,24 @@ export class TestDataService { return Object.assign({}, basicTaxStruct, overrides); } + + getBasicCrossSellingStruct(productId: string, overrides: Partial = {}) { + + const { id: productCrossSellingId, uuid: productCrossSellingUuid } = this.IdProvider.getIdPair(); + const productCrossSellingName = `${this.namePrefix}ProductCrossSelling-${productCrossSellingId}${this.nameSuffix}`; + + const defaultCrossSelling = { + id: productCrossSellingUuid, + productId: productId, + name: productCrossSellingName, + type: 'product_list', + position: 1, + active: true, + productStreamId: null, + sortingType: 'name', + limit: 10, + sortBy: 'name', + } + return Object.assign({}, defaultCrossSelling, overrides); + } } diff --git a/src/tasks/shop-customer-tasks.ts b/src/tasks/shop-customer-tasks.ts index 9607263..97d4178 100644 --- a/src/tasks/shop-customer-tasks.ts +++ b/src/tasks/shop-customer-tasks.ts @@ -24,6 +24,8 @@ import { OpenSearchSuggestPage } from './shop-customer/Search/OpenSearchSuggestP import { SearchForTerm } from './shop-customer/Search/SearchForTerm'; import { ValidateAccessibility } from './shop-customer/Accessibility/ValidateAccessibility'; +import { AddProductToWishlist } from './shop-customer/Wishlist/addProductToWishlist'; +import { RemoveProductFromWishlist } from './shop-customer/Wishlist/RemoveProductFromWishlist'; export const test = mergeTests( Login, @@ -45,5 +47,7 @@ export const test = mergeTests( OpenSearchResultPage, OpenSearchSuggestPage, SearchForTerm, - ValidateAccessibility + ValidateAccessibility, + AddProductToWishlist, + RemoveProductFromWishlist ); diff --git a/src/types/ShopwareTypes.ts b/src/types/ShopwareTypes.ts index 35b3e83..9a093aa 100644 --- a/src/types/ShopwareTypes.ts +++ b/src/types/ShopwareTypes.ts @@ -200,6 +200,10 @@ export type SalesChannelAnalytics = components['schemas']['SalesChannelAnalytics id: string, }; +export type ProductCrossSelling = components['schemas']['ProductCrossSelling'] & { + id: string, +}; + export interface RegistrationData { isCommercial: boolean; isGuest: boolean; diff --git a/tests/TestDataService/TestDataService.spec.ts b/tests/TestDataService/TestDataService.spec.ts index f2b88d9..3330db0 100644 --- a/tests/TestDataService/TestDataService.spec.ts +++ b/tests/TestDataService/TestDataService.spec.ts @@ -15,6 +15,7 @@ import { APIResponse, SalesChannelAnalytics, Tax, + ProductCrossSelling, } from '../../src'; test('Data Service', async ({ @@ -93,6 +94,10 @@ test('Data Service', async ({ const taxRate21 = await TestDataService.createTaxRate({ taxRate: 21.0 }); expect(taxRate21.taxRate).toEqual(21.0); + const crossSellingProduct = await TestDataService.createBasicProduct(); + const productCrossSelling = await TestDataService.createProductCrossSelling(crossSellingProduct.id, { name: 'Custom cross selling' }); + expect(productCrossSelling.name).toEqual('Custom cross selling'); + // Test data clean-up with deactivated cleansing process TestDataService.setCleanUp(false); const cleanUpFalseResponse = await TestDataService.cleanUp(); @@ -150,6 +155,10 @@ test('Data Service', async ({ const { data: databaseTaxRate21 } = (await taxRate21Response.json()) as { data: Tax }; expect(databaseTaxRate21.id).toBe(taxRate21.id); + const crossSellingProductResponse = await AdminApiContext.get(`./product-cross-selling/${productCrossSelling.id}?_response=detail`); + const { data: databaseCrossSellingProduct } = (await crossSellingProductResponse.json()) as { data: ProductCrossSelling }; + expect(databaseCrossSellingProduct.id).toBe(productCrossSelling.id); + // Test data clean-up with activated cleansing process TestDataService.setCleanUp(true); const cleanUpDeleteOperationsResponse = await TestDataService.cleanUp() as APIResponse;