diff --git a/page_objects/components/__init__.py b/page_objects/components/__init__.py index b1eec683..10b77b89 100644 --- a/page_objects/components/__init__.py +++ b/page_objects/components/__init__.py @@ -1,6 +1,8 @@ from .account_button import AccountButton from .account_menu import AccountMenu +from .add_or_update_wishlist_modal import AddOrUpdateWishlistModal from .add_to_cart_button import AddToCartButton +from .add_to_wishlists_modal import AddToWishlistsModal from .address import Address from .address_form import AddressForm from .category_view_switcher import CategoryViewSwitcher @@ -9,6 +11,7 @@ from .clear_cart_modal import ClearCartModal from .component import Component from .currency_selector import CurrencySelector +from .delete_wishlist_modal import DeleteWishlistModal from .dropdown_filter import DropdownFilter from .edit_address_modal import EditAddressModal from .language_selector import LanguageSelector @@ -26,11 +29,14 @@ from .shipping_details_section import ShippingDetailsSection from .slider_filter import SliderFilter from .top_header import TopHeader +from .wishlist_card import WishlistCard __all__ = [ "AccountButton", "AccountMenu", + "AddOrUpdateWishlistModal", "AddToCartButton", + "AddToWishlistsModal", "Address", "AddressForm", "CategoryViewSwitcher", @@ -39,6 +45,7 @@ "ClearCartModal", "Component", "CurrencySelector", + "DeleteWishlistModal", "DropdownFilter", "EditAddressModal", "LanguageSelector", @@ -56,4 +63,5 @@ "ShippingDetailsSection", "SliderFilter", "TopHeader", + "WishlistCard", ] diff --git a/page_objects/components/add_or_update_wishlist_modal.py b/page_objects/components/add_or_update_wishlist_modal.py new file mode 100644 index 00000000..1a5fb943 --- /dev/null +++ b/page_objects/components/add_or_update_wishlist_modal.py @@ -0,0 +1,26 @@ +from playwright.sync_api import Locator + +from .component import Component + + +class AddOrUpdateWishlistModal(Component): + @property + def name_input(self) -> Locator: + return self._root.locator("[data-test-id='wishlist-name-input']") + + @property + def description_input(self) -> Locator: + return self._root.locator("[data-test-id='wishlist-description-input'] textarea") + + @property + def sharing_scope_select(self) -> Locator: + return self._root.locator("[data-test-id='wishlist-sharing-scope-select']") + + @property + def save_button(self) -> Locator: + return self._root.locator("[data-test-id='wishlist-settings-save-button']") + + def select_scope(self, label: str) -> None: + self.sharing_scope_select.click() + # The dropdown option is rendered in a portal outside the modal root. + self._root.page.get_by_role("option", name=label).click() diff --git a/page_objects/components/add_to_wishlists_modal.py b/page_objects/components/add_to_wishlists_modal.py new file mode 100644 index 00000000..0ce01550 --- /dev/null +++ b/page_objects/components/add_to_wishlists_modal.py @@ -0,0 +1,15 @@ +from playwright.sync_api import Locator + +from .component import Component + + +class AddToWishlistsModal(Component): + @property + def save_button(self) -> Locator: + return self._root.locator("[data-test-id='wishlist-modal-save-button']") + + def list_checkbox(self, list_id: str) -> Locator: + return self._root.locator(f"[data-test-id='wishlist-modal-list-checkbox-{list_id}']") + + def list_with_product_checkbox(self, list_id: str) -> Locator: + return self._root.locator(f"[data-test-id='wishlist-modal-list-with-product-checkbox-{list_id}']") diff --git a/page_objects/components/delete_wishlist_modal.py b/page_objects/components/delete_wishlist_modal.py new file mode 100644 index 00000000..3c06d90a --- /dev/null +++ b/page_objects/components/delete_wishlist_modal.py @@ -0,0 +1,9 @@ +from playwright.sync_api import Locator + +from .component import Component + + +class DeleteWishlistModal(Component): + @property + def delete_button(self) -> Locator: + return self._root.locator("[data-test-id='delete-button']") diff --git a/page_objects/components/product_card.py b/page_objects/components/product_card.py index 904d07c2..4f2b1856 100644 --- a/page_objects/components/product_card.py +++ b/page_objects/components/product_card.py @@ -23,6 +23,10 @@ def add_to_cart_button(self) -> AddToCartButton: root=self._root.locator("[data-test-id='add-to-cart-button']") ) + @property + def add_to_list_button(self) -> Locator: + return self._root.locator("[data-test-id='add-to-list-button']") + @property def variations_button(self) -> Locator: return self._root.locator( diff --git a/page_objects/components/wishlist_card.py b/page_objects/components/wishlist_card.py new file mode 100644 index 00000000..7e439d9e --- /dev/null +++ b/page_objects/components/wishlist_card.py @@ -0,0 +1,19 @@ +from playwright.sync_api import Locator + +from .component import Component + + +class WishlistCard(Component): + @property + def menu_button(self) -> Locator: + return self._root.locator("[data-test-id='wishlist-card-menu-button']") + + # Menu items are rendered in a portal outside the card root; the :visible + # filter narrows to the menu of the card that was just opened. + @property + def edit_menu_item(self) -> Locator: + return self._root.page.locator("[data-test-id='wishlist-card-edit-menu-item'] button:visible") + + @property + def remove_menu_item(self) -> Locator: + return self._root.page.locator("[data-test-id='wishlist-card-remove-menu-item'] button:visible") diff --git a/page_objects/pages/__init__.py b/page_objects/pages/__init__.py index 6a580cf9..2431ff8b 100644 --- a/page_objects/pages/__init__.py +++ b/page_objects/pages/__init__.py @@ -1,3 +1,5 @@ +from .account_list_details import AccountListDetailsPage +from .account_lists import AccountListsPage from .account_saved_for_later import AccountSavedForLaterPage from .cart import CartPage from .category import CategoryPage @@ -6,9 +8,12 @@ from .checkout_review_order import CheckoutReviewOrderPage from .checkout_shipping import CheckoutShippingPage from .home import HomePage +from .product import ProductPage from .sign_in import SignInPage __all__ = [ + "AccountListDetailsPage", + "AccountListsPage", "AccountSavedForLaterPage", "CartPage", "CategoryPage", @@ -17,5 +22,6 @@ "CheckoutReviewOrderPage", "CheckoutShippingPage", "HomePage", + "ProductPage", "SignInPage", ] diff --git a/page_objects/pages/account_list_details.py b/page_objects/pages/account_list_details.py new file mode 100644 index 00000000..35e02ab8 --- /dev/null +++ b/page_objects/pages/account_list_details.py @@ -0,0 +1,34 @@ +from playwright.sync_api import Locator, Page + +from core.global_settings import GlobalSettings +from page_objects.components.line_item import LineItem +from page_objects.layouts.main import MainLayout + + +class AccountListDetailsPage(MainLayout): + def __init__( + self, + global_settings: GlobalSettings, + page: Page, + list_id: str, + ) -> None: + super().__init__(global_settings=global_settings, page=page) + self._list_id = list_id + + @property + def url(self) -> str: + return f"{self._global_settings.frontend_base_url}/account/lists/{self._list_id}" + + @property + def line_items(self) -> Locator: + return self._page.locator("[data-product-sku]") + + @property + def add_all_to_cart_button(self) -> Locator: + return self._page.locator("[data-test-id='add-all-to-cart-button']") + + def find_line_item(self, sku: str) -> LineItem: + return LineItem(root=self._page.locator(f"[data-product-sku='{sku}']")) + + def navigate(self) -> None: + self._page.goto(url=self.url, wait_until="load") diff --git a/page_objects/pages/account_lists.py b/page_objects/pages/account_lists.py new file mode 100644 index 00000000..cc781ad0 --- /dev/null +++ b/page_objects/pages/account_lists.py @@ -0,0 +1,36 @@ +from playwright.sync_api import Locator + +from page_objects.components.add_or_update_wishlist_modal import ( + AddOrUpdateWishlistModal, +) +from page_objects.components.delete_wishlist_modal import DeleteWishlistModal +from page_objects.components.wishlist_card import WishlistCard +from page_objects.layouts.main import MainLayout + + +class AccountListsPage(MainLayout): + @property + def url(self) -> str: + return f"{self._global_settings.frontend_base_url}/account/lists" + + @property + def create_list_button(self) -> Locator: + return self.root.locator("[data-test-id='create-wishlist-button']") + + @property + def cards(self) -> Locator: + return self._page.locator("[data-test-id='wishlist-card']") + + @property + def settings_modal(self) -> AddOrUpdateWishlistModal: + return AddOrUpdateWishlistModal(root=self._page.locator("[data-test-id='add-or-update-wishlist-modal']")) + + @property + def delete_modal(self) -> DeleteWishlistModal: + return DeleteWishlistModal(root=self._page.locator("[data-test-id='delete-wishlist-modal']")) + + def find_card(self, name: str) -> WishlistCard: + return WishlistCard(root=self.cards.filter(has_text=name).first) + + def navigate(self) -> None: + self._page.goto(url=self.url, wait_until="load") diff --git a/page_objects/pages/product.py b/page_objects/pages/product.py new file mode 100644 index 00000000..2ac1ef06 --- /dev/null +++ b/page_objects/pages/product.py @@ -0,0 +1,26 @@ +from playwright.sync_api import Locator, Page + +from core.global_settings import GlobalSettings +from page_objects.layouts.main import MainLayout + + +class ProductPage(MainLayout): + def __init__( + self, + global_settings: GlobalSettings, + page: Page, + product_id: str, + ) -> None: + super().__init__(global_settings=global_settings, page=page) + self._product_id = product_id + + @property + def url(self) -> str: + return f"{self._global_settings.frontend_base_url}/product/{self._product_id}" + + @property + def add_to_list_button(self) -> Locator: + return self.root.locator("[data-test-id='add-to-list-button']") + + def navigate(self) -> None: + self._page.goto(url=self.url, wait_until="load") diff --git a/tests/e2e/test_wishlist_add_all_to_cart.py b/tests/e2e/test_wishlist_add_all_to_cart.py new file mode 100644 index 00000000..04635e54 --- /dev/null +++ b/tests/e2e/test_wishlist_add_all_to_cart.py @@ -0,0 +1,105 @@ +from uuid import uuid4 + +import allure +import pytest +from core.clients import GraphQLClient +from core.global_settings import GlobalSettings +from gql.operations import CartOperations, ShoppingListOperations +from gql.types.cart_item_input import CartItemInput +from page_objects.pages import AccountListDetailsPage, CartPage +from playwright.sync_api import Page, Response, expect +from tests.context import Context +from utils.polling_utils import poll_until + +_USERNAME = "acme_store_employee_1@acme.com" +_PHYSICAL_PRODUCT_ID = "smartphone-samsung-galaxy-a57-5g" +_PHYSICAL_PRODUCT_SKU = "smartphone-samsung-galaxy-a57-5g" +_VARIATION_PRODUCT_ID = "smartphone-google-pixel-10-indigo" +_VARIATION_PRODUCT_SKU = "smartphone-google-pixel-10-indigo" + + +def _is_cart_from_wishlist_mutation(response: Response) -> bool: + if "/graphql" not in response.url: + return False + post = (response.request.post_data or "").lower() + return "mutation" in post and "cart" in post + + +@pytest.mark.e2e +@pytest.mark.with_user(_USERNAME) +@allure.feature("Wishlist / Add products to cart (E2E)") +@allure.title("Add all wishlist products to cart from list details") +def test_wishlist_add_all_products_to_cart( + page: Page, + global_settings: GlobalSettings, + graphql_client: GraphQLClient, + ctx: Context, +) -> None: + ops = ShoppingListOperations(client=graphql_client) + cart_ops = CartOperations(client=graphql_client) + wishlist = ops.create_shopping_list( + store_id=ctx.store_id, + user_id=ctx.user_id, + name=f"E2E WL Cart {uuid4().hex[:6]}", + currency_code=ctx.currency_code, + culture_name=ctx.culture_name, + description="Created by wishlist add-to-cart E2E flow", + ) + try: + expected_skus = {_PHYSICAL_PRODUCT_SKU, _VARIATION_PRODUCT_SKU} + ops.add_items_to_shopping_list( + list_id=wishlist.id, + items=[ + CartItemInput(product_id=_PHYSICAL_PRODUCT_ID, quantity=1), + CartItemInput(product_id=_VARIATION_PRODUCT_ID, quantity=1), + ], + ) + seeded = poll_until( + fetch=lambda: ops.get_shopping_list(list_id=wishlist.id, culture_name=ctx.culture_name), + predicate=lambda wl: expected_skus.issubset({item.sku for item in wl.items}), + attempts=global_settings.poll_attempts, + interval=global_settings.poll_interval, + ) + assert seeded is not None, "Seeded products did not appear in wishlist" + + with allure.step("Open wishlist details and verify seeded products are shown"): + details_page = AccountListDetailsPage(global_settings=global_settings, page=page, list_id=wishlist.id) + details_page.navigate() + expect(details_page.line_items).to_have_count(2) + expect(details_page.find_line_item(_PHYSICAL_PRODUCT_SKU).root).to_be_visible() + expect(details_page.find_line_item(_VARIATION_PRODUCT_SKU).root).to_be_visible() + + with allure.step("Add all wishlist products to cart and verify cart contents"): + with page.expect_response(_is_cart_from_wishlist_mutation): + details_page.add_all_to_cart_button.click() + expect(details_page.cart_quantity_label).to_have_text("2") + + cart_page = CartPage(global_settings=global_settings, page=page) + cart_page.navigate() + expect(cart_page.line_items).to_have_count(2) + expect(cart_page.find_line_item(_PHYSICAL_PRODUCT_SKU).root).to_be_visible() + expect(cart_page.find_line_item(_VARIATION_PRODUCT_SKU).root).to_be_visible() + finally: + try: + ops.delete_shopping_list(list_id=wishlist.id) + except Exception as exc: + allure.attach( + f"Teardown of wishlist {wishlist.id} skipped: {exc}", + name=f"wishlist-teardown-{wishlist.id}", + attachment_type=allure.attachment_type.TEXT, + ) + try: + cart = cart_ops.get_cart( + store_id=ctx.store_id, + user_id=ctx.user_id, + currency_code=ctx.currency_code, + culture_name=ctx.culture_name, + ) + if cart: + cart_ops.delete_cart(cart_id=cart.id, user_id=ctx.user_id) + except Exception as exc: + allure.attach( + f"Cart teardown skipped: {exc}", + name="cart-teardown", + attachment_type=allure.attachment_type.TEXT, + ) diff --git a/tests/e2e/test_wishlist_add_product.py b/tests/e2e/test_wishlist_add_product.py new file mode 100644 index 00000000..60d5cbc5 --- /dev/null +++ b/tests/e2e/test_wishlist_add_product.py @@ -0,0 +1,107 @@ +from collections.abc import Callable +from uuid import uuid4 + +import allure +import pytest +from core.clients import GraphQLClient +from core.global_settings import GlobalSettings +from gql.operations import ShoppingListOperations +from page_objects.components import AddToWishlistsModal +from page_objects.pages import CategoryPage, ProductPage +from playwright.sync_api import Page, Response, expect +from tests.context import Context +from utils.polling_utils import poll_until + +_USERNAME = "acme_store_employee_1@acme.com" +_CATEGORY_PATH = "smartphones" +_PHYSICAL_PRODUCT_SKU = "smartphone-samsung-galaxy-a57-5g" +_VARIATION_PARENT_SKU = "smartphone-google-pixel-10-frost" +_VARIATION_PRODUCT_ID = "smartphone-google-pixel-10-indigo" +_VARIATION_PRODUCT_SKU = "smartphone-google-pixel-10-indigo" + + +def _is_wishlist_item_mutation(response: Response) -> bool: + if "/graphql" not in response.url: + return False + post = (response.request.post_data or "").lower() + return "mutation" in post and "wishlist" in post + + +def _open_from_grid(page: Page, global_settings: GlobalSettings) -> str: + category_page = CategoryPage(global_settings=global_settings, page=page, path=_CATEGORY_PATH) + category_page.navigate() + card = category_page.scroll_to_product(_PHYSICAL_PRODUCT_SKU) + expect(card.add_to_list_button).to_be_visible() + card.add_to_list_button.click() + return _PHYSICAL_PRODUCT_SKU + + +def _open_from_list_view(page: Page, global_settings: GlobalSettings) -> str: + category_page = CategoryPage(global_settings=global_settings, page=page, path=_CATEGORY_PATH) + category_page.navigate() + category_page.view_switcher.list_view_tab.click() + card = category_page.scroll_to_product(_VARIATION_PARENT_SKU) + expect(card.add_to_list_button).to_be_visible() + card.add_to_list_button.click() + return _VARIATION_PARENT_SKU + + +def _open_from_pdp(page: Page, global_settings: GlobalSettings) -> str: + product_page = ProductPage(global_settings=global_settings, page=page, product_id=_VARIATION_PRODUCT_ID) + product_page.navigate() + expect(product_page.add_to_list_button).to_be_visible() + product_page.add_to_list_button.click() + return _VARIATION_PRODUCT_SKU + + +@pytest.mark.e2e +@pytest.mark.with_user(_USERNAME) +@pytest.mark.parametrize( + "open_add_to_list,source_label", + [ + (_open_from_grid, "grid view"), + (_open_from_list_view, "list view"), + (_open_from_pdp, "product detail page"), + ], + ids=["grid-view", "list-view", "product-detail-page"], +) +@allure.feature("Wishlist / Add product (E2E)") +@allure.title("Add a product to a wishlist from {source_label}") +def test_wishlist_add_product( + page: Page, + global_settings: GlobalSettings, + graphql_client: GraphQLClient, + ctx: Context, + open_add_to_list: Callable[[Page, GlobalSettings], str], + source_label: str, +) -> None: + ops = ShoppingListOperations(client=graphql_client) + wishlist = ops.create_shopping_list( + store_id=ctx.store_id, + user_id=ctx.user_id, + name=f"E2E WL Add {source_label[:8]} {uuid4().hex[:6]}", + currency_code=ctx.currency_code, + culture_name=ctx.culture_name, + description=f"Created by wishlist E2E add-from-{source_label} flow", + ) + try: + expected_sku = open_add_to_list(page, global_settings) + + with allure.step(f"Select wishlist in modal and save (source: {source_label})"): + modal = AddToWishlistsModal(root=page.locator("[data-test-id='add-to-wishlists-modal']")) + expect(modal.root).to_be_visible() + modal.list_checkbox(wishlist.id).click() + with page.expect_response(_is_wishlist_item_mutation): + modal.save_button.click() + expect(modal.root).not_to_be_visible() + + with allure.step(f"Verify product '{expected_sku}' appears in the wishlist"): + updated = poll_until( + fetch=lambda: ops.get_shopping_list(list_id=wishlist.id, culture_name=ctx.culture_name), + predicate=lambda wl: expected_sku in {item.sku for item in wl.items}, + attempts=global_settings.poll_attempts, + interval=global_settings.poll_interval, + ) + assert updated is not None, f"Product '{expected_sku}' did not appear in wishlist {wishlist.id}" + finally: + ops.delete_shopping_list(list_id=wishlist.id) diff --git a/tests/e2e/test_wishlist_manage_lists.py b/tests/e2e/test_wishlist_manage_lists.py new file mode 100644 index 00000000..9dd8ff26 --- /dev/null +++ b/tests/e2e/test_wishlist_manage_lists.py @@ -0,0 +1,138 @@ +from uuid import uuid4 + +import allure +import pytest +from core.clients import GraphQLClient +from core.global_settings import GlobalSettings +from gql.operations import ShoppingListOperations +from page_objects.pages import AccountListsPage +from playwright.sync_api import Page, Response, expect +from tests.context import Context +from utils.polling_utils import poll_until + +_USERNAME = "acme_store_employee_1@acme.com" + +_SCOPE_LABELS = { + "Private": "Private", + "AnyoneAnonymous": "Anyone (readonly)", + "Organization": "Organization", +} + + +def _is_wishlist_manage_mutation(response: Response) -> bool: + if "/graphql" not in response.url: + return False + post = (response.request.post_data or "").lower() + return "mutation" in post and "wishlist" in post + + +@pytest.mark.e2e +@pytest.mark.with_user(_USERNAME) +@allure.feature("Wishlist / List management (E2E)") +@allure.title("Create, edit, and remove wishlists with Private, Any, and Organization scopes") +def test_wishlist_create_edit_remove_and_scopes( + page: Page, + global_settings: GlobalSettings, + graphql_client: GraphQLClient, + ctx: Context, +) -> None: + ops = ShoppingListOperations(client=graphql_client) + created_ids: list[str] = [] + lists_page = AccountListsPage(global_settings=global_settings, page=page) + lists_page.navigate() + + try: + # Capability check at top of test: corporate sharing scope is only available + # for users in organizations with sharing enabled. Probe once, then skip if absent. + lists_page.create_list_button.click() + probe_modal = lists_page.settings_modal + expect(probe_modal.root).to_be_visible() + has_scope_select = probe_modal.sharing_scope_select.count() > 0 + page.keyboard.press("Escape") + expect(probe_modal.root).not_to_be_visible() + if not has_scope_select: + pytest.skip("Corporate sharing scope selector is not available for this user") + + created: dict[str, str] = {} + for scope, label in _SCOPE_LABELS.items(): + with allure.step(f"Create wishlist with scope '{scope}'"): + lists_page.create_list_button.click() + settings_modal = lists_page.settings_modal + expect(settings_modal.root).to_be_visible() + + name = f"E2E WL {scope[:8]} {uuid4().hex[:6]}" + settings_modal.name_input.fill(name) + settings_modal.description_input.fill(f"{scope} scope created by E2E") + settings_modal.select_scope(label) + with page.expect_response(_is_wishlist_manage_mutation): + settings_modal.save_button.click() + expect(settings_modal.root).not_to_be_visible() + + card = lists_page.find_card(name) + expect(card.root).to_be_visible() + matching = [ + item + for item in ops.get_shopping_lists( + store_id=ctx.store_id, + user_id=ctx.user_id, + currency_code=ctx.currency_code, + culture_name=ctx.culture_name, + ) + if item.name == name + ] + assert matching, f"Wishlist '{name}' was not created" + created[scope] = matching[0].id + created_ids.append(matching[0].id) + with_scope = poll_until( + fetch=lambda lid=matching[0].id: ops.get_shopping_list(list_id=lid, culture_name=ctx.culture_name), + predicate=lambda wl, s=scope: (wl.sharing_setting.scope if wl.sharing_setting else None) == s, + attempts=global_settings.poll_attempts, + interval=global_settings.poll_interval, + ) + assert with_scope is not None, f"Wishlist {matching[0].id} did not reach scope '{scope}'" + + original_name = ops.get_shopping_list(list_id=created["Private"], culture_name=ctx.culture_name).name + edited_name = f"E2E WL Edited {uuid4().hex[:6]}" + + with allure.step("Edit wishlist name, description, and scope"): + card = lists_page.find_card(original_name) + card.menu_button.click() + card.edit_menu_item.click() + settings_modal = lists_page.settings_modal + expect(settings_modal.root).to_be_visible() + settings_modal.name_input.fill(edited_name) + settings_modal.description_input.fill("Edited by wishlist E2E") + settings_modal.select_scope(_SCOPE_LABELS["Organization"]) + with page.expect_response(_is_wishlist_manage_mutation): + settings_modal.save_button.click() + expect(settings_modal.root).not_to_be_visible() + expect(lists_page.find_card(edited_name).root).to_be_visible() + edited = poll_until( + fetch=lambda: ops.get_shopping_list(list_id=created["Private"], culture_name=ctx.culture_name), + predicate=lambda wl: (wl.sharing_setting.scope if wl.sharing_setting else None) == "Organization", + attempts=global_settings.poll_attempts, + interval=global_settings.poll_interval, + ) + assert edited is not None, "Edited wishlist did not reach Organization scope" + + with allure.step("Delete the edited wishlist"): + card = lists_page.find_card(edited_name) + card.menu_button.click() + card.remove_menu_item.click() + delete_modal = lists_page.delete_modal + expect(delete_modal.root).to_be_visible() + with page.expect_response(_is_wishlist_manage_mutation): + delete_modal.delete_button.click() + expect(delete_modal.root).not_to_be_visible() + expect(card.root).not_to_be_visible() + created_ids.remove(created["Private"]) + finally: + for list_id in created_ids: + try: + ops.delete_shopping_list(list_id=list_id) + except Exception as exc: + allure.attach( + f"Teardown of wishlist {list_id} skipped: {exc}", + name=f"wishlist-teardown-{list_id}", + attachment_type=allure.attachment_type.TEXT, + ) diff --git a/tests/e2e/test_wishlist_remove_product.py b/tests/e2e/test_wishlist_remove_product.py new file mode 100644 index 00000000..43b471a0 --- /dev/null +++ b/tests/e2e/test_wishlist_remove_product.py @@ -0,0 +1,83 @@ +from uuid import uuid4 + +import allure +import pytest +from core.clients import GraphQLClient +from core.global_settings import GlobalSettings +from gql.operations import ShoppingListOperations +from gql.types.cart_item_input import CartItemInput +from page_objects.components import AddToWishlistsModal +from page_objects.pages import CategoryPage +from playwright.sync_api import Page, Response, expect +from tests.context import Context +from utils.polling_utils import poll_until + +_USERNAME = "acme_store_employee_1@acme.com" +_CATEGORY_PATH = "smartphones" +_PHYSICAL_PRODUCT_ID = "smartphone-samsung-galaxy-a57-5g" +_PHYSICAL_PRODUCT_SKU = "smartphone-samsung-galaxy-a57-5g" + + +def _is_wishlist_item_mutation(response: Response) -> bool: + if "/graphql" not in response.url: + return False + post = (response.request.post_data or "").lower() + return "mutation" in post and "wishlist" in post + + +@pytest.mark.e2e +@pytest.mark.with_user(_USERNAME) +@allure.feature("Wishlist / Remove product (E2E)") +@allure.title("Remove a product from a wishlist via the add-to-list modal") +def test_wishlist_remove_product_via_add_to_list_modal( + page: Page, + global_settings: GlobalSettings, + graphql_client: GraphQLClient, + ctx: Context, +) -> None: + ops = ShoppingListOperations(client=graphql_client) + wishlist = ops.create_shopping_list( + store_id=ctx.store_id, + user_id=ctx.user_id, + name=f"E2E WL Remove {uuid4().hex[:6]}", + currency_code=ctx.currency_code, + culture_name=ctx.culture_name, + description="Created by wishlist E2E remove-via-modal flow", + ) + try: + ops.add_items_to_shopping_list( + list_id=wishlist.id, + items=[CartItemInput(product_id=_PHYSICAL_PRODUCT_ID, quantity=1)], + ) + seeded = poll_until( + fetch=lambda: ops.get_shopping_list(list_id=wishlist.id, culture_name=ctx.culture_name), + predicate=lambda wl: _PHYSICAL_PRODUCT_SKU in {item.sku for item in wl.items}, + attempts=global_settings.poll_attempts, + interval=global_settings.poll_interval, + ) + assert seeded is not None, "Seed product did not appear in wishlist" + + with allure.step("Open add-to-list modal for the seeded product from category grid"): + category_page = CategoryPage(global_settings=global_settings, page=page, path=_CATEGORY_PATH) + category_page.navigate() + card = category_page.scroll_to_product(_PHYSICAL_PRODUCT_SKU) + card.add_to_list_button.click() + modal = AddToWishlistsModal(root=page.locator("[data-test-id='add-to-wishlists-modal']")) + expect(modal.root).to_be_visible() + + with allure.step("Deselect the wishlist containing the product and save"): + modal.list_with_product_checkbox(wishlist.id).click() + with page.expect_response(_is_wishlist_item_mutation): + modal.save_button.click() + expect(modal.root).not_to_be_visible() + + with allure.step(f"Verify product '{_PHYSICAL_PRODUCT_SKU}' is removed from the wishlist"): + removed = poll_until( + fetch=lambda: ops.get_shopping_list(list_id=wishlist.id, culture_name=ctx.culture_name), + predicate=lambda wl: _PHYSICAL_PRODUCT_SKU not in {item.sku for item in wl.items}, + attempts=global_settings.poll_attempts, + interval=global_settings.poll_interval, + ) + assert removed is not None, f"Product '{_PHYSICAL_PRODUCT_SKU}' was not removed from wishlist" + finally: + ops.delete_shopping_list(list_id=wishlist.id)