From 2d569376925119e2d6caaa982fd7b5cf900db9af Mon Sep 17 00:00:00 2001 From: Artur Tagisow Date: Thu, 21 Apr 2022 14:17:56 +0200 Subject: [PATCH 1/2] refactor: components for product list/grid M2-447 refactor: split product list and grid components refactor: move common props to composition function fix: paths fix: don't show empty when isShowProducts = false fix: don't set width on skeletons fix: wishlist icons appearing when not supposed to fix: can't add to cart in grid view refactor: squash this --- packages/theme/composables/useCart/useCart.ts | 5 +- packages/theme/getters/productGetters.ts | 2 +- .../components/views/CategoryProductGrid.vue | 108 +++++++ .../components/views/CategoryProductList.vue | 158 ++++++++++ .../__tests__/CategoryProductGrid.spec.ts | 51 +++ .../__tests__/CategoryProductList.spec.ts | 55 ++++ .../views/__tests__/productsMock.ts | 258 +++++++++++++++ .../category/components/views/transition.scss | 10 + .../views/useProductsWithCommonCardProps.ts | 67 ++++ .../theme/modules/catalog/pages/category.vue | 298 ++---------------- packages/theme/package.json | 1 + packages/theme/types/shims-vue.d.ts | 6 + yarn.lock | 7 + 13 files changed, 759 insertions(+), 267 deletions(-) create mode 100644 packages/theme/modules/catalog/category/components/views/CategoryProductGrid.vue create mode 100644 packages/theme/modules/catalog/category/components/views/CategoryProductList.vue create mode 100644 packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductGrid.spec.ts create mode 100644 packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductList.spec.ts create mode 100644 packages/theme/modules/catalog/category/components/views/__tests__/productsMock.ts create mode 100644 packages/theme/modules/catalog/category/components/views/transition.scss create mode 100644 packages/theme/modules/catalog/category/components/views/useProductsWithCommonCardProps.ts create mode 100644 packages/theme/types/shims-vue.d.ts diff --git a/packages/theme/composables/useCart/useCart.ts b/packages/theme/composables/useCart/useCart.ts index 6ae2c68e5..e935315e8 100644 --- a/packages/theme/composables/useCart/useCart.ts +++ b/packages/theme/composables/useCart/useCart.ts @@ -42,8 +42,7 @@ type UseCartApplyCouponParams = ComposableFunctionArgs<{ /** * Parameters accepted by the `isInCart` method in the {@link useCart} composable */ -type UseCartIsInCartParams = { - currentCart: CART +type UseCartIsInCartParams = { product: PRODUCT }; @@ -70,7 +69,7 @@ export interface UseCartInterface { /** Removes applied coupon from the cart */ removeCoupon(params: ComposableFunctionArgs<{}>): Promise; /** Checks wheter a `product` is in the `cart` */ - isInCart(params: UseCartIsInCartParams): boolean; + isInCart(params: UseCartIsInCartParams): boolean; /** Sets the contents of the cart */ setCart(newCart: CART): void; /** Returns the Items in the Cart as a `computed` property */ diff --git a/packages/theme/getters/productGetters.ts b/packages/theme/getters/productGetters.ts index 82e9fbb2c..ea32a89c8 100644 --- a/packages/theme/getters/productGetters.ts +++ b/packages/theme/getters/productGetters.ts @@ -21,7 +21,7 @@ export const getName = (product: ProductInterface): string => { return htmlDecode(product.name); }; -export const getSlug = (product: ProductInterface, category?: Category): string => { +export const getSlug = (product: ProductInterface, category?: Category | CategoryInterface): string => { const rewrites = product?.url_rewrites; let url = product?.sku ? `/p/${product.sku}` : ''; if (!rewrites || rewrites.length === 0) { diff --git a/packages/theme/modules/catalog/category/components/views/CategoryProductGrid.vue b/packages/theme/modules/catalog/category/components/views/CategoryProductGrid.vue new file mode 100644 index 000000000..72490a0fb --- /dev/null +++ b/packages/theme/modules/catalog/category/components/views/CategoryProductGrid.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/packages/theme/modules/catalog/category/components/views/CategoryProductList.vue b/packages/theme/modules/catalog/category/components/views/CategoryProductList.vue new file mode 100644 index 000000000..95eb4cf76 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/views/CategoryProductList.vue @@ -0,0 +1,158 @@ + + + + diff --git a/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductGrid.spec.ts b/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductGrid.spec.ts new file mode 100644 index 000000000..fd771c782 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductGrid.spec.ts @@ -0,0 +1,51 @@ +import { render } from '@testing-library/vue'; +import { createTestingPinia } from '@pinia/testing'; +import { createLocalVue } from '@vue/test-utils'; +import { PiniaVuePlugin } from 'pinia'; +import CategoryProductGrid from '../CategoryProductGrid.vue'; +import { productsMock } from './productsMock'; + +const localVue = createLocalVue(); +localVue.use(PiniaVuePlugin); +localVue.component('NuxtImg', { render(h) { return h('div'); } }); + +localVue.prototype.$nuxt = { + context: { + $vsf: { + $magento: { + config: { + imageProvider: '', + magentoBaseUrl: '', + }, + }, + }, + app: { + $fc: jest.fn((label) => label), + localePath: jest.fn(), + $vsf: { + $magento: { + config: { + state: '', + }, + }, + }, + }, + i18n: { + t: jest.fn((label) => label), + }, + }, +}; + +describe('CategoryProductGrid', () => { + it('shows skeleton loader when loading', async () => { + const { findAllByTestId } = render(CategoryProductGrid, { props: { loading: true, products: [] }, localVue, pinia: createTestingPinia() }); + const loadingSkeletons = await findAllByTestId('skeleton'); + expect(loadingSkeletons).not.toHaveLength(0); + }); + + it('shows products when loaded', async () => { + const { findAllByTestId } = render(CategoryProductGrid, { props: { loading: false, products: productsMock }, localVue, pinia: createTestingPinia() }); + const products = await findAllByTestId('product-card'); + expect(products).toHaveLength(productsMock.length); + }); +}); diff --git a/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductList.spec.ts b/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductList.spec.ts new file mode 100644 index 000000000..98e1d373a --- /dev/null +++ b/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductList.spec.ts @@ -0,0 +1,55 @@ +import { render } from '@testing-library/vue'; +import { createLocalVue } from '@vue/test-utils'; +import { PiniaVuePlugin } from 'pinia'; +import { createTestingPinia } from '@pinia/testing'; +import { productsMock } from './productsMock'; +import CategoryProductList from '../CategoryProductList.vue'; + +const localVue = createLocalVue(); +localVue.use(PiniaVuePlugin); +localVue.component('NuxtImg', { render(h) { return h('div'); } }); + +localVue.prototype.components = { + components: { + NuxtImg: 'div', + }, +}; + +localVue.prototype.$nuxt = { + context: { + app: { + $vsf: { + $magento: { + config: { + state: '', + }, + }, + }, + }, + i18n: { + t: jest.fn((label) => label), + }, + }, +}; + +describe('CategoryProductList', () => { + it('hides \'Add to wishlist\' button when logged out', () => {}); + + it('picks correct label for wishlist button', () => {}); + + it('shows skeleton loader when loading', async () => { + const { findAllByTestId } = render(CategoryProductList, { props: { loading: true, products: [] }, localVue, pinia: createTestingPinia() }); + const loadingSkeletons = await findAllByTestId('skeleton'); + expect(loadingSkeletons).not.toHaveLength(0); + }); + + it('shows products when loaded', async () => { + const { findAllByTestId } = render(CategoryProductList, { + props: { loading: false, products: productsMock }, + localVue, + pinia: createTestingPinia(), + }); + const products = await findAllByTestId('product-card'); + expect(products).toHaveLength(productsMock.length); + }); +}); diff --git a/packages/theme/modules/catalog/category/components/views/__tests__/productsMock.ts b/packages/theme/modules/catalog/category/components/views/__tests__/productsMock.ts new file mode 100644 index 000000000..480812293 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/views/__tests__/productsMock.ts @@ -0,0 +1,258 @@ +export const productsMock = [ + { + __typename: 'GroupedProduct', + uid: 'NTI=', + sku: '24-WG085_Group', + name: 'Set of Sprite Yoga Straps', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 0, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/l/u/luma-yoga-strap-set.jpg', position: null, disabled: null, label: 'Set of Sprite Yoga Straps', + }, + url_key: 'set-of-sprite-yoga-straps', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'set-of-sprite-yoga-straps.html' }, { __typename: 'UrlRewrite', url: 'gear/set-of-sprite-yoga-straps.html' }, { __typename: 'UrlRewrite', url: 'gear/fitness-equipment/set-of-sprite-yoga-straps.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 14 }, regular_price: { __typename: 'Money', currency: 'USD', value: 14 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 14 }, regular_price: { __typename: 'Money', currency: 'USD', value: 14 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'NQ==', name: 'Fitness Equipment', url_suffix: '.html', url_path: 'gear/fitness-equipment', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }], + review_count: 0, + reviews: { __typename: 'ProductReviews', items: [] }, + commonProps: { + title: 'Set of Sprite Yoga Straps', link: '/default/p/24-WG085_Group/set-of-sprite-yoga-straps.html', style: { '--index': 0 }, isAddedToCart: false, image: 'media/catalog/product/l/u/luma-yoga-strap-set.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$14.00', specialPrice: null, reviewsCount: 0, scoreRating: 0, + }, + }, { + __typename: 'BundleProduct', + uid: 'NDU=', + sku: '24-WG080', + name: 'Sprite Yoga Companion Kit', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 0, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/l/u/luma-yoga-kit-2.jpg', position: null, disabled: null, label: 'Sprite Yoga Companion Kit', + }, + url_key: 'sprite-yoga-companion-kit', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'sprite-yoga-companion-kit.html' }, { __typename: 'UrlRewrite', url: 'gear/sprite-yoga-companion-kit.html' }, { __typename: 'UrlRewrite', url: 'gear/fitness-equipment/sprite-yoga-companion-kit.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 77 }, regular_price: { __typename: 'Money', currency: 'USD', value: 77 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 61 }, regular_price: { __typename: 'Money', currency: 'USD', value: 61 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'NQ==', name: 'Fitness Equipment', url_suffix: '.html', url_path: 'gear/fitness-equipment', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }], + review_count: 0, + reviews: { __typename: 'ProductReviews', items: [] }, + commonProps: { + title: 'Sprite Yoga Companion Kit', link: '/default/p/24-WG080/sprite-yoga-companion-kit.html', style: { '--index': 1 }, isAddedToCart: false, image: 'media/catalog/product/l/u/luma-yoga-kit-2.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$61.00', specialPrice: null, reviewsCount: 0, scoreRating: 0, + }, + }, { + __typename: 'SimpleProduct', + uid: 'NDQ=', + sku: '24-WG02', + name: 'Didi Sport Watch', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 73, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/w/g/wg02-bk-0.jpg', position: null, disabled: null, label: 'Didi Sport Watch', + }, + url_key: 'didi-sport-watch', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'didi-sport-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/didi-sport-watch.html' }, { __typename: 'UrlRewrite', url: 'collections/didi-sport-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/watches/didi-sport-watch.html' }, { __typename: 'UrlRewrite', url: 'collections/yoga-new/didi-sport-watch.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 92 }, regular_price: { __typename: 'Money', currency: 'USD', value: 92 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 92 }, regular_price: { __typename: 'Money', currency: 'USD', value: 92 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'Ng==', name: 'Watches', url_suffix: '.html', url_path: 'gear/watches', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }, { + __typename: 'CategoryTree', uid: 'Nw==', name: 'Collections', url_suffix: '.html', url_path: 'collections', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'OA==', name: 'New Luma Yoga Collection', url_suffix: '.html', url_path: 'collections/yoga-new', breadcrumbs: null, + }], + review_count: 3, + reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 100, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '5' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }] }, + commonProps: { + title: 'Didi Sport Watch', link: '/default/p/24-WG02/didi-sport-watch.html', style: { '--index': 2 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg02-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$92.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.666_666_666_666_666_5, + }, + }, { + __typename: 'SimpleProduct', + uid: 'NDM=', + sku: '24-WG03', + name: 'Clamber Watch', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 53, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/w/g/wg03-gr-0.jpg', position: null, disabled: null, label: 'Clamber Watch', + }, + url_key: 'clamber-watch', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'clamber-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/clamber-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/watches/clamber-watch.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 54 }, regular_price: { __typename: 'Money', currency: 'USD', value: 54 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 54 }, regular_price: { __typename: 'Money', currency: 'USD', value: 54 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'Ng==', name: 'Watches', url_suffix: '.html', url_path: 'gear/watches', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }], + review_count: 3, + reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }] }, + commonProps: { + title: 'Clamber Watch', link: '/default/p/24-WG03/clamber-watch.html', style: { '--index': 3 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg03-gr-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$54.00', specialPrice: null, reviewsCount: 3, scoreRating: 2.666_666_666_666_666_5, + }, + }, { + __typename: 'SimpleProduct', + uid: 'NDI=', + sku: '24-WG01', + name: 'Bolo Sport Watch', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 67, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/w/g/wg01-bk-0.jpg', position: null, disabled: null, label: 'Bolo Sport Watch', + }, + url_key: 'bolo-sport-watch', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'bolo-sport-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/bolo-sport-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/watches/bolo-sport-watch.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 49 }, regular_price: { __typename: 'Money', currency: 'USD', value: 49 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 49 }, regular_price: { __typename: 'Money', currency: 'USD', value: 49 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'Ng==', name: 'Watches', url_suffix: '.html', url_path: 'gear/watches', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }], + review_count: 3, + reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }] }, + commonProps: { + title: 'Bolo Sport Watch', link: '/default/p/24-WG01/bolo-sport-watch.html', style: { '--index': 4 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg01-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$49.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.333_333_333_333_333_5, + }, + }, { + __typename: 'SimpleProduct', + uid: 'NDE=', + sku: '24-WG09', + name: 'Luma Analog Watch', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 80, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/w/g/wg09-gr-0.jpg', position: null, disabled: null, label: 'Luma Analog Watch', + }, + url_key: 'luma-analog-watch', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'luma-analog-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/luma-analog-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/watches/luma-analog-watch.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 43 }, regular_price: { __typename: 'Money', currency: 'USD', value: 43 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 43 }, regular_price: { __typename: 'Money', currency: 'USD', value: 43 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'Ng==', name: 'Watches', url_suffix: '.html', url_path: 'gear/watches', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }], + review_count: 2, + reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }] }, + commonProps: { + title: 'Luma Analog Watch', link: '/default/p/24-WG09/luma-analog-watch.html', style: { '--index': 5 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg09-gr-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$43.00', specialPrice: null, reviewsCount: 2, scoreRating: 4, + }, + }, { + __typename: 'SimpleProduct', + uid: 'NDA=', + sku: '24-MG02', + name: 'Dash Digital Watch', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 73, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/m/g/mg02-bk-0.jpg', position: null, disabled: null, label: 'Dash Digital Watch', + }, + url_key: 'dash-digital-watch', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'dash-digital-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/dash-digital-watch.html' }, { __typename: 'UrlRewrite', url: 'collections/dash-digital-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/watches/dash-digital-watch.html' }, { __typename: 'UrlRewrite', url: 'collections/yoga-new/dash-digital-watch.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 92 }, regular_price: { __typename: 'Money', currency: 'USD', value: 92 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 92 }, regular_price: { __typename: 'Money', currency: 'USD', value: 92 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'Ng==', name: 'Watches', url_suffix: '.html', url_path: 'gear/watches', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }, { + __typename: 'CategoryTree', uid: 'Nw==', name: 'Collections', url_suffix: '.html', url_path: 'collections', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'OA==', name: 'New Luma Yoga Collection', url_suffix: '.html', url_path: 'collections/yoga-new', breadcrumbs: null, + }], + review_count: 3, + reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }] }, + commonProps: { + title: 'Dash Digital Watch', link: '/default/p/24-MG02/dash-digital-watch.html', style: { '--index': 6 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg02-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$92.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.666_666_666_666_666_5, + }, + }, { + __typename: 'SimpleProduct', + uid: 'Mzk=', + sku: '24-MG05', + name: 'Cruise Dual Analog Watch', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 65, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/m/g/mg05-br-0.jpg', position: null, disabled: null, label: 'Cruise Dual Analog Watch', + }, + url_key: 'cruise-dual-analog-watch', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'cruise-dual-analog-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/cruise-dual-analog-watch.html' }, { __typename: 'UrlRewrite', url: 'collections/cruise-dual-analog-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/watches/cruise-dual-analog-watch.html' }, { __typename: 'UrlRewrite', url: 'collections/yoga-new/cruise-dual-analog-watch.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 55 }, regular_price: { __typename: 'Money', currency: 'USD', value: 55 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 55 }, regular_price: { __typename: 'Money', currency: 'USD', value: 55 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'Ng==', name: 'Watches', url_suffix: '.html', url_path: 'gear/watches', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }, { + __typename: 'CategoryTree', uid: 'Nw==', name: 'Collections', url_suffix: '.html', url_path: 'collections', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'OA==', name: 'New Luma Yoga Collection', url_suffix: '.html', url_path: 'collections/yoga-new', breadcrumbs: null, + }], + review_count: 4, + reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 100, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '5' }] }, { __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }] }, + commonProps: { + title: 'Cruise Dual Analog Watch', link: '/default/p/24-MG05/cruise-dual-analog-watch.html', style: { '--index': 7 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg05-br-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$55.00', specialPrice: null, reviewsCount: 4, scoreRating: 3.25, + }, + }, { + __typename: 'SimpleProduct', + uid: 'Mzg=', + sku: '24-MG03', + name: 'Summit Watch', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 47, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/m/g/mg03-br-0.jpg', position: null, disabled: null, label: 'Summit Watch', + }, + url_key: 'summit-watch', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'summit-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/summit-watch.html' }, { __typename: 'UrlRewrite', url: 'collections/summit-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/watches/summit-watch.html' }, { __typename: 'UrlRewrite', url: 'collections/yoga-new/summit-watch.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 54 }, regular_price: { __typename: 'Money', currency: 'USD', value: 54 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 54 }, regular_price: { __typename: 'Money', currency: 'USD', value: 54 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'Ng==', name: 'Watches', url_suffix: '.html', url_path: 'gear/watches', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }, { + __typename: 'CategoryTree', uid: 'Nw==', name: 'Collections', url_suffix: '.html', url_path: 'collections', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'OA==', name: 'New Luma Yoga Collection', url_suffix: '.html', url_path: 'collections/yoga-new', breadcrumbs: null, + }], + review_count: 3, + reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }] }, + commonProps: { + title: 'Summit Watch', link: '/default/p/24-MG03/summit-watch.html', style: { '--index': 8 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg03-br-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$54.00', specialPrice: null, reviewsCount: 3, scoreRating: 2.333_333_333_333_333_5, + }, + }, { + __typename: 'SimpleProduct', + uid: 'Mzc=', + sku: '24-MG01', + name: 'Endurance Watch', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 87, + thumbnail: { + __typename: 'ProductImage', url: 'https://magento2-instance.vuestorefront.io/media/catalog/product/cache/e594b79cbfbeb6488381857e5a9b6158/m/g/mg01-bk-0.jpg', position: null, disabled: null, label: 'Endurance Watch', + }, + url_key: 'endurance-watch', + url_rewrites: [{ __typename: 'UrlRewrite', url: 'endurance-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/endurance-watch.html' }, { __typename: 'UrlRewrite', url: 'gear/watches/endurance-watch.html' }], + price_range: { __typename: 'PriceRange', maximum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 49 }, regular_price: { __typename: 'Money', currency: 'USD', value: 49 } }, minimum_price: { __typename: 'ProductPrice', final_price: { __typename: 'Money', currency: 'USD', value: 49 }, regular_price: { __typename: 'Money', currency: 'USD', value: 49 } } }, + categories: [{ + __typename: 'CategoryTree', uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, + }, { + __typename: 'CategoryTree', uid: 'Ng==', name: 'Watches', url_suffix: '.html', url_path: 'gear/watches', breadcrumbs: [{ __typename: 'Breadcrumb', category_name: 'Gear', category_url_path: 'gear' }], + }], + review_count: 3, + reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 100, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '5' }] }, { __typename: 'ProductReview', average_rating: 100, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '5' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }] }, + commonProps: { + title: 'Endurance Watch', link: '/default/p/24-MG01/endurance-watch.html', style: { '--index': 9 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg01-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$49.00', specialPrice: null, reviewsCount: 3, scoreRating: 4.333_333_333_333_333, + }, + }]; diff --git a/packages/theme/modules/catalog/category/components/views/transition.scss b/packages/theme/modules/catalog/category/components/views/transition.scss new file mode 100644 index 000000000..d9ec5e52c --- /dev/null +++ b/packages/theme/modules/catalog/category/components/views/transition.scss @@ -0,0 +1,10 @@ +.slide { + &-enter { + opacity: 0; + transform: scale(0.5); + &-active { + transition: all 0.2s ease; + transition-delay: calc(0.1s * var(--index)); + } + } +} diff --git a/packages/theme/modules/catalog/category/components/views/useProductsWithCommonCardProps.ts b/packages/theme/modules/catalog/category/components/views/useProductsWithCommonCardProps.ts new file mode 100644 index 000000000..a55945583 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/views/useProductsWithCommonCardProps.ts @@ -0,0 +1,67 @@ +import { computed, Ref, useContext } from '@nuxtjs/composition-api'; +import { + Product, useImage, useUser, useWishlist, +} from '~/composables'; +import { + getName, getPrice, getProductSku, getProductThumbnailImage, getSlug, +} from '~/getters/productGetters'; +import { getAverageRating, getTotalReviews } from '~/getters/reviewGetters'; +import { useAddToCart } from '~/helpers/cart/addToCart'; + +export const useProductsWithCommonProductCardProps = (products: Ref) => { + const { getMagentoImage } = useImage(); + const { isInWishlist } = useWishlist(); + const { isAuthenticated } = useUser(); + const { isInCart } = useAddToCart(); + const context = useContext(); + + /** + * Most props of SfProductCard and SfProductCardHorizontal are the same. + * To avoid passing tens of props to both components two times, + * instead the below object is passed to them using `v-bind="product.commonProps"` + */ + const productsWithCommonProductCardProps = computed(() => products.value.map((product, index) => { + const imageProps = { + image: getMagentoImage(getProductThumbnailImage(product)), + imageTag: 'nuxt-img', + nuxtImgConfig: { fit: 'cover' }, + }; + + const wishlistProps = { + isInWishlist: isInWishlist({ product }), + isInWishlistIcon: isAuthenticated.value ? 'heart_fill' : '', + wishlistIcon: isAuthenticated.value ? 'heart' : '', + }; + + const price = getPrice(product); + + const priceProps = { + regularPrice: context.app.$fc(price.regular), + specialPrice: price.special && context.app.$fc(getPrice(product).special), + }; + + const reviewProps = { + reviewsCount: getTotalReviews(product), + scoreRating: getAverageRating(product), + }; + + const link = context.app.localePath(`/p/${getProductSku(product)}${getSlug(product, product.categories[0])}`); + + const commonProps = { + title: getName(product), + link, + style: { '--index': index }, // used for transition animation + isAddedToCart: isInCart({ product }), + ...imageProps, + ...wishlistProps, + ...priceProps, + ...reviewProps, + }; + + return { + ...product, + commonProps, + }; + })); + return { productsWithCommonProductCardProps }; +}; diff --git a/packages/theme/modules/catalog/pages/category.vue b/packages/theme/modules/catalog/pages/category.vue index d7f37ccf0..e6b37b58d 100644 --- a/packages/theme/modules/catalog/pages/category.vue +++ b/packages/theme/modules/catalog/pages/category.vue @@ -9,6 +9,7 @@ class="breadcrumbs" /> -
-
- - - -
-
-
- - + + + - - - - - + + + +
- - - - - -
import LazyHydrate from 'vue-lazy-hydration'; import { - SfButton, SfPagination, - SfProductCard, - SfProductCardHorizontal, - SfProperty, SfSelect, - SfPrice, SfHeading, } from '@storefront-ui/vue'; import { @@ -232,45 +110,36 @@ import { } from '@nuxtjs/composition-api'; import { CacheTagPrefix, useCache } from '@vue-storefront/cache'; import { facetGetters } from '~/getters'; -import { - getTotalReviews, getSlug, getProductThumbnailImage, getProductSku, getPrice, getAverageRating, getName, -} from '~/getters/productGetters'; import { useFacet, - useUser, useWishlist, - useImage, useUiHelpers, useUiState, } from '~/composables'; -import { AgnosticPagination } from '~/composables/types'; +import { AgnosticPagination, Product } from '~/composables/types'; import { useUrlResolver } from '~/composables/useUrlResolver'; import { useAddToCart } from '~/helpers/cart/addToCart'; import { useCategoryContent } from '~/modules/catalog/category/components/cms/useCategoryContent'; import { usePrice } from '~/modules/catalog/pricing/usePrice'; -import SkeletonLoader from '~/components/SkeletonLoader/index.vue'; import CategoryNavbar from '~/modules/catalog/category/components/navbar/CategoryNavbar.vue'; import type { ProductInterface, EntityUrl } from '~/modules/GraphQL/types'; -import CategoryBreadcrumbs from '../category/components/breadcrumbs/CategoryBreadcrumbs.vue'; import { useTraverseCategory } from '~/modules/catalog/category/helpers/useTraverseCategory'; +import CategoryBreadcrumbs from '~/modules/catalog/category/components/breadcrumbs/CategoryBreadcrumbs.vue'; + // TODO(addToCart qty, horizontal): https://github.com/vuestorefront/storefront-ui/issues/1606 export default defineComponent({ name: 'CategoryPage', components: { CategoryEmptyResults: () => import('~/modules/catalog/category/components/CategoryEmptyResults.vue'), CategoryFilters: () => import('~/modules/catalog/category/components/filters/CategoryFilters.vue'), - SkeletonLoader, + CmsContent: () => import('~/modules/catalog/category/components/cms/CmsContent.vue'), + CategoryProductGrid: () => import('~/modules/catalog/category/components/views/CategoryProductGrid.vue'), + CategoryProductList: () => import('~/modules/catalog/category/components/views/CategoryProductList.vue'), CategoryNavbar, CategoryBreadcrumbs, - CmsContent: () => import('~/modules/catalog/category/components/cms/CmsContent.vue'), - SfPrice, - SfButton, - SfProductCard, - SfProductCardHorizontal, SfPagination, SfSelect, - SfProperty, LazyHydrate, SfHeading, }, @@ -282,13 +151,12 @@ export default defineComponent({ const cmsContent = ref(''); const isShowCms = ref(false); const isShowProducts = ref(false); - const products = ssrRef([]); + const products = ssrRef([]); const sortBy = ref({}); const facets = ref([]); const pagination = ref({}); const { search: resolveUrl } = useUrlResolver(); - const { isAuthenticated } = useUser(); const { toggleFilterSidebar, changeToCategoryListView, @@ -302,7 +170,7 @@ export default defineComponent({ removeItem: removeItemFromWishlist, } = useWishlist(); const { result, search } = useFacet(); - const { addItemToCart, isInCart } = useAddToCart(); + const { addItemToCart } = useAddToCart(); const addItemToWishlist = async (product: ProductInterface) => { await (isInWishlist({ product }) @@ -360,11 +228,10 @@ export default defineComponent({ if (products.value.length > 0) { const skus = products.value.map((item) => item.sku); const priceData = await getPricesBySku(skus, pagination.value.itemsPerPage); - products.value = products.value.map((product) => { - const modifiedProduct = { ...product }; - modifiedProduct.price_range = priceData.items.find((item) => item.sku === product.sku)?.price_range; - return modifiedProduct; - }); + products.value = products.value.map((product) => ({ + ...product, + price_range: priceData.items.find((item) => item.sku === product.sku)?.price_range, + })); } isPriceLoaded.value = true; }); @@ -373,17 +240,9 @@ export default defineComponent({ uiHelpers.changeItemsPerPage(itemsPerPage, false); fetch(); }; - const { getMagentoImage, imageSizes } = useImage(); - const productsSkeletons = computed(() => (products.value.length > 0 ? products.value.length : 10)); + return { isPriceLoaded, - getTotalReviews, - getSlug, - getProductThumbnailImage, - getProductSku, - getPrice, - getAverageRating, - getName, ...uiHelpers, toggleFilterSidebar, isCategoryGridView, @@ -393,14 +252,9 @@ export default defineComponent({ addItemToCart, addItemToWishlist, facets, - isAuthenticated, - isInCart, - isInWishlist, pagination, products, sortBy, - getMagentoImage, - imageSizes, isShowCms, isShowProducts, cmsContent, @@ -409,7 +263,6 @@ export default defineComponent({ routeData, doChangeItemsPerPage, fetch, - productsSkeletons, }; }, }); @@ -498,79 +351,12 @@ export default defineComponent({ flex: 1; margin: 0; - &__grid { - justify-content: center; - @include for-desktop { - justify-content: flex-start; - } - } - - &__grid, - &__list { - display: flex; - flex-wrap: wrap; - align-content: flex-start; - } - - &__product-card { - --product-card-title-margin: var(--spacer-base) 0 0 0; - --product-card-title-font-weight: var(--font-weight--medium); - --product-card-title-margin: var(--spacer-xs) 0 0 0; - flex: 1 1 50%; - - @include for-desktop { - --product-card-title-font-weight: var(--font-weight--normal); - --product-card-add-button-bottom: var(--spacer-base); - --product-card-title-margin: var(--spacer-sm) 0 0 0; - } - } - - &__product-card-horizontal { - flex: 0 0 100%; - - @include for-mobile { - ::v-deep .sf-image { - --image-width: 5.3125rem; - --image-height: 7.0625rem; - } - } - - &__add-to-wishlist { - @include for-mobile { - margin: 1rem auto; - } - display: block; - } - } - - &__slide-enter { - opacity: 0; - transform: scale(0.5); - } - - &__slide-enter-active { - transition: all 0.2s ease; - transition-delay: calc(0.1s * var(--index)); - } - @include for-desktop { - &__grid { - margin: var(--spacer-sm) 0 0 var(--spacer-sm); - } &__pagination { display: flex; justify-content: flex-start; margin: var(--spacer-xl) 0 0 0; } - &__product-card-horizontal { - margin: var(--spacer-lg) 0; - } - &__product-card { - flex: 1 1 25%; - } - &__list { - margin: 0 0 0 var(--spacer-sm); - } } @include for-mobile { @@ -597,20 +383,6 @@ export default defineComponent({ } } -.loading { - margin: var(--spacer-3xl) auto; - - @include for-desktop { - margin-top: 6.25rem; - } - - &--categories { - @include for-desktop { - margin-top: 3.75rem; - } - } -} - ::v-deep .sf-sidebar__aside { --sidebar-z-index: 3; } diff --git a/packages/theme/package.json b/packages/theme/package.json index 2e23d0eee..663c78c86 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -63,6 +63,7 @@ "@babel/core": "^7.16.12", "@nuxt/types": "latest", "@nuxt/typescript-build": "^2.1.0", + "@pinia/testing": "^0.0.11", "@testing-library/jest-dom": "^5.16.1", "@testing-library/user-event": "^13.5.0", "@testing-library/vue": "^5.8.2", diff --git a/packages/theme/types/shims-vue.d.ts b/packages/theme/types/shims-vue.d.ts new file mode 100644 index 000000000..22c5174ea --- /dev/null +++ b/packages/theme/types/shims-vue.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import Vue from 'vue'; + + // eslint-disable-next-line unicorn/prefer-export-from + export default Vue; +} diff --git a/yarn.lock b/yarn.lock index 26e91f292..f3d4d6611 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3477,6 +3477,13 @@ dependencies: vue-demi "*" +"@pinia/testing@^0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@pinia/testing/-/testing-0.0.11.tgz#b913190ef06c1f1560d05958e294ab477c3499d0" + integrity sha512-iJxUHV0Uj1jS7zvN5oVtSZ2BRI4PlG1xbqPkMeJasPtbqsnoxBmkwputDzqX5Dd1I1z9gEfyYOeE2duK3xGWpQ== + dependencies: + vue-demi "*" + "@pm2/agent@~2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@pm2/agent/-/agent-2.0.1.tgz#0edffc54cd8ee2b12f90136264e7880f3f78c79d" From 50003ff06c285f2177cef27bb47dc8d6bf3df17c Mon Sep 17 00:00:00 2001 From: Artur Tagisow Date: Mon, 25 Apr 2022 16:37:36 +0200 Subject: [PATCH 2/2] test: add global vue/nuxt mocks for unit tests --- packages/theme/jest-setup.js | 31 ++++++++++++ .../__tests__/CategoryProductGrid.spec.ts | 37 +++----------- .../__tests__/CategoryProductList.spec.ts | 50 ++++++++----------- 3 files changed, 61 insertions(+), 57 deletions(-) diff --git a/packages/theme/jest-setup.js b/packages/theme/jest-setup.js index 0e4154888..16ce89e48 100644 --- a/packages/theme/jest-setup.js +++ b/packages/theme/jest-setup.js @@ -1,4 +1,35 @@ import '@testing-library/jest-dom'; import Vue from 'vue'; +import { config } from '@vue/test-utils'; + +config.stubs = { + NuxtImg: { render(h) { return h('img'); } }, +}; + +const $vsf = { + $magento: { + config: { + imageProvider: '', + magentoBaseUrl: '', + state: {}, + }, + }, +}; + +// mocks object returned by useContext() +Vue.prototype.$nuxt = { + context: { + $vsf, + app: { + // $vsf intentionally doubled in context top level AND in context.app - this is the way it's in the app + $vsf, + $fc: jest.fn((label) => label), + localePath: jest.fn((link) => link), + }, + i18n: { + t: jest.fn((label) => label), + }, + }, +}; Vue.directive('e2e', {}); diff --git a/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductGrid.spec.ts b/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductGrid.spec.ts index fd771c782..d1bb9d173 100644 --- a/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductGrid.spec.ts +++ b/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductGrid.spec.ts @@ -7,34 +7,6 @@ import { productsMock } from './productsMock'; const localVue = createLocalVue(); localVue.use(PiniaVuePlugin); -localVue.component('NuxtImg', { render(h) { return h('div'); } }); - -localVue.prototype.$nuxt = { - context: { - $vsf: { - $magento: { - config: { - imageProvider: '', - magentoBaseUrl: '', - }, - }, - }, - app: { - $fc: jest.fn((label) => label), - localePath: jest.fn(), - $vsf: { - $magento: { - config: { - state: '', - }, - }, - }, - }, - i18n: { - t: jest.fn((label) => label), - }, - }, -}; describe('CategoryProductGrid', () => { it('shows skeleton loader when loading', async () => { @@ -44,7 +16,14 @@ describe('CategoryProductGrid', () => { }); it('shows products when loaded', async () => { - const { findAllByTestId } = render(CategoryProductGrid, { props: { loading: false, products: productsMock }, localVue, pinia: createTestingPinia() }); + const { findAllByTestId } = render(CategoryProductGrid, { + props: { + loading: false, + products: productsMock, + }, + localVue, + pinia: createTestingPinia(), + }); const products = await findAllByTestId('product-card'); expect(products).toHaveLength(productsMock.length); }); diff --git a/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductList.spec.ts b/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductList.spec.ts index 98e1d373a..c66ca0bc1 100644 --- a/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductList.spec.ts +++ b/packages/theme/modules/catalog/category/components/views/__tests__/CategoryProductList.spec.ts @@ -7,38 +7,32 @@ import CategoryProductList from '../CategoryProductList.vue'; const localVue = createLocalVue(); localVue.use(PiniaVuePlugin); -localVue.component('NuxtImg', { render(h) { return h('div'); } }); - -localVue.prototype.components = { - components: { - NuxtImg: 'div', - }, -}; - -localVue.prototype.$nuxt = { - context: { - app: { - $vsf: { - $magento: { - config: { - state: '', - }, - }, - }, - }, - i18n: { - t: jest.fn((label) => label), - }, - }, -}; describe('CategoryProductList', () => { - it('hides \'Add to wishlist\' button when logged out', () => {}); - - it('picks correct label for wishlist button', () => {}); + it.each([ + [true, true], + [false, false], + ])('has correct \'Add to wishlist\' button visiblity when loggin state is %s', (isLoggedIn, expectedVisibility) => { + const { queryByTestId } = render(CategoryProductList, { + props: { + loading: false, + products: [productsMock[0]], + }, + localVue, + pinia: createTestingPinia({ initialState: { customer: { isLoggedIn } } }), + }); + expect(Boolean(queryByTestId('wishlist-button'))).toBe(expectedVisibility); + }); it('shows skeleton loader when loading', async () => { - const { findAllByTestId } = render(CategoryProductList, { props: { loading: true, products: [] }, localVue, pinia: createTestingPinia() }); + const { findAllByTestId } = render(CategoryProductList, { + props: { + loading: true, + products: [], + }, + localVue, + pinia: createTestingPinia(), + }); const loadingSkeletons = await findAllByTestId('skeleton'); expect(loadingSkeletons).not.toHaveLength(0); });