Gherkin use case for cart features with Cypress + Cumcumber + typescript

Gherkin use case for cart features with Cypress + Cumcumber + typescript
This commit is contained in:
Yassin 2024-09-25 16:51:59 +02:00
parent f22b1ead60
commit 5a17eaf211
53 changed files with 3423 additions and 574 deletions

View File

@ -0,0 +1,7 @@
{
"stepDefinitions": [
"cypress/e2e/**/[filepath].ts",
"cypress/e2e/common-step-definitions/*.ts",
"cypress/support/step_definitions/*.ts"
]
}

3
.gitignore vendored
View File

@ -36,3 +36,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
#cursor
.cursorrules

View File

@ -33,6 +33,7 @@ Vercel is happy to partner and work with any commerce provider to help them get
Integrations enable upgraded or additional functionality for Next.js Commerce
- [Orama](https://github.com/oramasearch/nextjs-commerce) ([Demo](https://vercel-commerce.oramasearch.com/))
- Upgrades search to include typeahead with dynamic re-rendering, vector-based similarity search, and JS-based configuration.
- Search runs entirely in the browser for smaller catalogs or on a CDN for larger.

85
Test-Design.md Normal file
View File

@ -0,0 +1,85 @@
# Component Hierarchy and Data-Test Attributes
Voici un schéma Mermaid qui représente les composants et les hiérarchies des attributs data-test pour le composant `ThreeItemGrid`.
```mermaid
%%{init: {'theme': 'dark'}}%%
graph TD
%% Utilisation d'un vert plus foncé (#339966) avec du texte blanc (#fff) pour améliorer le contraste
classDef grid-item-class fill:#339966,stroke:#aaa,stroke-width:2px,color:#fff;
%% Bleu foncé (#336699) avec texte blanc (#fff) pour les conteneurs
classDef container-class fill:#336699,stroke:#aaa,stroke-width:2px,color:#fff;
%% Rouge foncé (#cc3333) avec texte blanc (#fff) pour les composants React
classDef react-component-class fill:#cc3333,stroke:#aaa,stroke-width:2px,color:#fff;
%% Subgraph représentant la structure globale de la grille
subgraph ThreeItemGrid
style ThreeItemGrid fill:#333,stroke:#aaa,stroke-width:2px;
%% Grid Item avec taille configurable
ThreeItemGridItem[⚛️ Three Item Grids]
class ThreeItemGridItem react-component-class
GridItem[🗂️ data-test: three-item-grid]
ThreeItemGridItem --> GridItem
class GridItem container-class
GridItemSize[🔍 data-test: grid-item <br>⬛ full / ⬜ half]
GridItem --> GridItemSize
class GridItemSize grid-item-class
%% Grid Product Link pour GridItemSize
GridItemSize --> GridProductLink[🔍 data-test: grid-product-link]
class GridProductLink grid-item-class
GridProductLink --> GridTileImage[⚛️ GridTileImage]
class GridTileImage react-component-class
GridTileImage --> GridTileContainer[🗂️ data-test: grid-tile-container]
class GridTileContainer container-class
GridTileContainer --> GridTileImageItem[🔍 data-test: grid-tile-image]
class GridTileImageItem grid-item-class
GridTileContainer --> Label[⚛️ Label]
class Label react-component-class
Label --> LabelContainer[🗂️ data-test: label-container]
class LabelContainer container-class
LabelContainer --> LabelContentWrapper[🗂️ data-test: label-content-wrapper]
class LabelContentWrapper container-class
LabelContentWrapper --> LabelTitleText[🔍 data-test: label-title-text]
class LabelTitleText grid-item-class
LabelContentWrapper --> Price[⚛️ Price]
class Price react-component-class
Price --> PriceAmount[🔍 data-test: price-amount]
class PriceAmount grid-item-class
Price --> PriceCurrencyCode[🔍 data-test: price-currency-code]
class PriceCurrencyCode grid-item-class
end
%% Subgraph pour la légende
subgraph Légende
direction LR
ReactComp[⚛️ Composants React]
class ReactComp react-component-class
DataTestCont[🗂️ Conteneur data-test]
class DataTestCont container-class
TestableElem[🔍 Éléments testables]
class TestableElem grid-item-class
end
```
[![](https://mermaid.ink/img/pako:eNqVVs1u20YQfpUFA0ENIKaWlcoWWwRo5aII4ACBraJAKx9W5FBaeLlLLJeJHUGX3nooeilQoCnQs4E8QO96E79A-wid3RUp_oh2woNE7sx837czQ-6svVBG4AVer7dmgumArPt6BQn0A9KPqLrubza93lzMxVLRdEVmZ-a-1yPfa8ZZRjWTgkT9XJA3oDRJeZ6RWIpwe0c-ezIaTSbj8VNC30BIopxouNFAFpyKEK1xHD8lqcwVocn2jjOpQBEOJJRCK5ppmIuQ0yw7g5gsFYt8piHx7RKJGefBDn-QaSWvIXhCKd3d-29ZpFfBcXozCCWXKjBkX-6Uf8Mhr2ocjyeTncYugRwyKwsE5Cqr6DKLlAlQDV0G81N1Xch8CXthYTjC66OEJanMqNAZuQAa6oo8ZZ596yBA6JpIh_-pIi_zhWsEBana3mWIisyEU4LBeahzBWTJ5YJiISMw61g6zrGWWRE5WymAl1jL77Coc0HwyvQt-tcMZSoflmh1GQjUZsNM_C5n1BCbEsVsmSu6MCqMa43H_P90_-f7f__5zRkcgjFlV87fZa0VdTi7LsT9lvj__f3Hz4Ygopr6GjJ8zbSBcy1tmvuqQxrx_RclTlVO6dBowUP8l-wdoIbff60KKF8p8tVCvbj_8J7EOefkc3L_4S-yojy-qoPUlBjEQ2rMevNlbVbotZIRdgo5Z-LadXEbtYZXEO8CTdzh3aTOwefocdWUV4nuUth0K4hnjMPLhC6haJTaYouptHxch-zdq3zToqyHmsfqxw8w-GX1D6ooQdpdUuffO7b2vGvgA-m2AtjDKbCd80C62-TndAG8SLR9qKHblccT69xKvAezyY1HRybr0V1pbHjVaFHeD_jdSx_jNtt46xwPK9gDPSqj4lpqmTHNYYbnSKuYToM2dt8cNG36MrarlF3MrxULy7fGPtTA7crjxXRuJd7XicxFexupsfnUGtssLqhLf4NhmisFIryd4mzUwRPuXHwzP7XpqghtUhBR8zx1xzklfHu3RHP1wDwvlwxJxBSEduw6v3Ar9tSfYvqKRE8bE0FNXundlXfnfIYbnuF-TVHLxp0WM9A-HzXsalBXkxq7OYu_5cV3ZfsLbjoBI1fvjI2jtxLSkUxv4CWgEsoinGXXJnju2Tl27gV4i5pxpuRzby426EpzLS9vRegFOLTAwFM4eq28IKY8w6c8xd3BGaOY_qRcTan4UcqkCIGIaaleueHZztDWxQvW3o0XHB-Nnx0_H05Ono-GJ6fDk4F36wVHm4H3zkIMnx25a3h8OpycfjEab_4HDOwk4w?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqVVs1u20YQfpUFA0ENIKaWlcoWWwRo5aII4ACBraJAKx9W5FBaeLlLLJeJHUGX3nooeilQoCnQs4E8QO96E79A-wid3RUp_oh2woNE7sx837czQ-6svVBG4AVer7dmgumArPt6BQn0A9KPqLrubza93lzMxVLRdEVmZ-a-1yPfa8ZZRjWTgkT9XJA3oDRJeZ6RWIpwe0c-ezIaTSbj8VNC30BIopxouNFAFpyKEK1xHD8lqcwVocn2jjOpQBEOJJRCK5ppmIuQ0yw7g5gsFYt8piHx7RKJGefBDn-QaSWvIXhCKd3d-29ZpFfBcXozCCWXKjBkX-6Uf8Mhr2ocjyeTncYugRwyKwsE5Cqr6DKLlAlQDV0G81N1Xch8CXthYTjC66OEJanMqNAZuQAa6oo8ZZ596yBA6JpIh_-pIi_zhWsEBana3mWIisyEU4LBeahzBWTJ5YJiISMw61g6zrGWWRE5WymAl1jL77Coc0HwyvQt-tcMZSoflmh1GQjUZsNM_C5n1BCbEsVsmSu6MCqMa43H_P90_-f7f__5zRkcgjFlV87fZa0VdTi7LsT9lvj__f3Hz4Ygopr6GjJ8zbSBcy1tmvuqQxrx_RclTlVO6dBowUP8l-wdoIbff60KKF8p8tVCvbj_8J7EOefkc3L_4S-yojy-qoPUlBjEQ2rMevNlbVbotZIRdgo5Z-LadXEbtYZXEO8CTdzh3aTOwefocdWUV4nuUth0K4hnjMPLhC6haJTaYouptHxch-zdq3zToqyHmsfqxw8w-GX1D6ooQdpdUuffO7b2vGvgA-m2AtjDKbCd80C62-TndAG8SLR9qKHblccT69xKvAezyY1HRybr0V1pbHjVaFHeD_jdSx_jNtt46xwPK9gDPSqj4lpqmTHNYYbnSKuYToM2dt8cNG36MrarlF3MrxULy7fGPtTA7crjxXRuJd7XicxFexupsfnUGtssLqhLf4NhmisFIryd4mzUwRPuXHwzP7XpqghtUhBR8zx1xzklfHu3RHP1wDwvlwxJxBSEduw6v3Ar9tSfYvqKRE8bE0FNXundlXfnfIYbnuF-TVHLxp0WM9A-HzXsalBXkxq7OYu_5cV3ZfsLbjoBI1fvjI2jtxLSkUxv4CWgEsoinGXXJnju2Tl27gV4i5pxpuRzby426EpzLS9vRegFOLTAwFM4eq28IKY8w6c8xd3BGaOY_qRcTan4UcqkCIGIaaleueHZztDWxQvW3o0XHB-Nnx0_H05Ono-GJ6fDk4F36wVHm4H3zkIMnx25a3h8OpycfjEab_4HDOwk4w)

View File

@ -80,9 +80,12 @@ export default async function ProductPage({ params }: { params: { handle: string
__html: JSON.stringify(productJsonLd)
}}
/>
<div className="mx-auto max-w-screen-2xl px-4">
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 lg:flex-row lg:gap-8 dark:border-neutral-800 dark:bg-black">
<div className="h-full w-full basis-full lg:basis-4/6">
<div className="mx-auto max-w-screen-2xl px-4" data-test="product-page">
<div
className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 lg:flex-row lg:gap-8 dark:border-neutral-800 dark:bg-black"
data-test="product-details"
>
<div className="h-full w-full basis-full lg:basis-4/6" data-test="product-image">
<Suspense
fallback={
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden" />
@ -97,7 +100,7 @@ export default async function ProductPage({ params }: { params: { handle: string
</Suspense>
</div>
<div className="basis-full lg:basis-2/6">
<div className="basis-full lg:basis-2/6" data-test="product-description">
<Suspense fallback={null}>
<ProductDescription product={product} />
</Suspense>
@ -116,18 +119,20 @@ async function RelatedProducts({ id }: { id: string }) {
if (!relatedProducts.length) return null;
return (
<div className="py-8">
<div className="py-8" data-test="product-related">
<h2 className="mb-4 text-2xl font-bold">Related Products</h2>
<ul className="flex w-full gap-4 overflow-x-auto pt-1">
{relatedProducts.map((product) => (
<li
key={product.handle}
className="aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5"
data-test="product-item"
>
<Link
className="relative h-full w-full"
href={`/product/${product.handle}`}
prefetch={true}
data-test="product-item-link"
>
<GridTileImage
alt={product.title}

View File

@ -12,14 +12,19 @@ export async function Carousel() {
const carouselProducts = [...products, ...products, ...products];
return (
<div className="w-full overflow-x-auto pb-6 pt-1">
<div className="w-full overflow-x-auto pb-6 pt-1" data-test="carousel-container">
<ul className="flex animate-carousel gap-4">
{carouselProducts.map((product, i) => (
<li
key={`${product.handle}${i}`}
className="relative aspect-square h-[30vh] max-h-[275px] w-2/3 max-w-[475px] flex-none md:w-1/3"
data-test={`carousel-item-${i}`}
>
<Link href={`/product/${product.handle}`} className="relative h-full w-full">
<Link
href={`/product/${product.handle}`}
className="relative h-full w-full"
data-test="product-link"
>
<GridTileImage
alt={product.title}
label={{
@ -30,6 +35,7 @@ export async function Carousel() {
src={product.featuredImage?.url}
fill
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw"
data-test="tile-image"
/>
</Link>
</li>

View File

@ -8,15 +8,20 @@ import { redirect } from 'next/navigation';
export async function addItem(prevState: any, selectedVariantId: string | undefined) {
let cartId = cookies().get('cartId')?.value;
console.log('Cart ID before adding item:', cartId);
if (!cartId || !selectedVariantId) {
console.error('Cart ID or selected variant ID is missing');
return 'Error adding item to cart';
}
try {
console.log('Attempting to add item to Shopify cart');
await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]);
console.log('Item added to Shopify cart successfully');
revalidateTag(TAGS.cart);
} catch (e) {
console.error('Error adding item to cart:', e);
return 'Error adding item to cart';
}
}

View File

@ -2,11 +2,10 @@
import { PlusIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { addItem } from 'components/cart/actions';
import { useProduct } from 'components/product/product-context';
import { Product, ProductVariant } from 'lib/shopify/types';
import { useFormState } from 'react-dom';
import { useCart } from './cart-context';
import { addItem } from './actions';
function SubmitButton({
availableForSale,
@ -21,19 +20,20 @@ function SubmitButton({
if (!availableForSale) {
return (
<button disabled className={clsx(buttonClasses, disabledClasses)}>
<button disabled className={clsx(buttonClasses, disabledClasses)} data-test="add-to-cart">
Out Of Stock
</button>
);
}
console.log(selectedVariantId);
// console.log(selectedVariantId);
if (!selectedVariantId) {
return (
<button
aria-label="Please select an option"
disabled
className={clsx(buttonClasses, disabledClasses)}
data-test="add-to-cart"
>
<div className="absolute left-0 ml-4">
<PlusIcon className="h-5" />
@ -49,6 +49,7 @@ function SubmitButton({
className={clsx(buttonClasses, {
'hover:opacity-90': true
})}
data-test="add-to-cart"
>
<div className="absolute left-0 ml-4">
<PlusIcon className="h-5" />
@ -62,27 +63,37 @@ export function AddToCart({ product }: { product: Product }) {
const { variants, availableForSale } = product;
const { addCartItem } = useCart();
const { state } = useProduct();
const [message, formAction] = useFormState(addItem, null);
// Trouver le variant correspondant à l'état actuel du produit
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every((option) => option.value === state[option.name.toLowerCase()])
);
const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined;
const selectedVariantId = variant?.id || defaultVariantId;
const actionWithVariant = formAction.bind(null, selectedVariantId);
const finalVariant = variants.find((variant) => variant.id === selectedVariantId)!;
const defaultVariant = variants.length === 1 ? variants[0] : undefined;
const selectedVariant = variant || defaultVariant;
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
event.stopPropagation(); // Assure-toi d'arrêter la propagation des événements
if (selectedVariant) {
addCartItem(selectedVariant, product); // Appel avec l'objet ProductVariant
// Appel côté serveur pour ajouter au panier via Shopify
try {
await addItem(null, selectedVariant.id); // Appel côté serveur
} catch (error) {
console.error('Error adding item to server cart:', error);
}
} else {
console.error('No variant selected');
}
};
return (
<form
action={async () => {
addCartItem(finalVariant, product);
await actionWithVariant();
}}
>
<SubmitButton availableForSale={availableForSale} selectedVariantId={selectedVariantId} />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
<form onSubmit={handleSubmit}>
<SubmitButton
availableForSale={availableForSale}
selectedVariantId={selectedVariant?.id} // Utilisation de l'ID du variant dans le bouton
/>
</form>
);
}

View File

@ -1,7 +1,14 @@
'use client';
import type { Cart, CartItem, Product, ProductVariant } from 'lib/shopify/types';
import React, { createContext, use, useContext, useMemo, useOptimistic } from 'react';
import React, {
createContext,
startTransition,
use,
useContext,
useMemo,
useOptimistic
} from 'react';
type UpdateType = 'plus' | 'minus' | 'delete';
@ -50,9 +57,10 @@ function createOrUpdateCartItem(
): CartItem {
const quantity = existingItem ? existingItem.quantity + 1 : 1;
const totalAmount = calculateItemCost(quantity, variant.price.amount);
const itemId = variant.id ?? existingItem?.id;
return {
id: existingItem?.id,
id: itemId,
quantity,
cost: {
totalAmount: {
@ -131,6 +139,10 @@ function cartReducer(state: Cart | undefined, action: CartAction): Cart {
}
case 'ADD_ITEM': {
const { variant, product } = action.payload;
if (!variant.id) {
return currentCart; // Ne pas ajouter l'article si l'ID est manquant
}
const existingItem = currentCart.lines.find((item) => item.merchandise.id === variant.id);
const updatedItem = createOrUpdateCartItem(existingItem, variant, product);
@ -156,11 +168,15 @@ export function CartProvider({
const [optimisticCart, updateOptimisticCart] = useOptimistic(initialCart, cartReducer);
const updateCartItem = (merchandiseId: string, updateType: UpdateType) => {
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { merchandiseId, updateType } });
startTransition(() => {
updateOptimisticCart({ type: 'UPDATE_ITEM', payload: { merchandiseId, updateType } });
});
};
const addCartItem = (variant: ProductVariant, product: Product) => {
updateOptimisticCart({ type: 'ADD_ITEM', payload: { variant, product } });
startTransition(() => {
updateOptimisticCart({ type: 'ADD_ITEM', payload: { variant, product } });
});
};
const value = useMemo(

View File

@ -3,7 +3,7 @@
import { XMarkIcon } from '@heroicons/react/24/outline';
import { removeItem } from 'components/cart/actions';
import type { CartItem } from 'lib/shopify/types';
import { useFormState } from 'react-dom';
import { useActionState } from 'react';
export function DeleteItemButton({
item,
@ -12,7 +12,7 @@ export function DeleteItemButton({
item: CartItem;
optimisticUpdate: any;
}) {
const [message, formAction] = useFormState(removeItem, null);
const [message, formAction] = useActionState(removeItem, null);
const merchandiseId = item.merchandise.id;
const actionWithVariant = formAction.bind(null, merchandiseId);

View File

@ -4,7 +4,7 @@ import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { updateItemQuantity } from 'components/cart/actions';
import type { CartItem } from 'lib/shopify/types';
import { useFormState } from 'react-dom';
import { useActionState } from 'react';
function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
return (
@ -17,6 +17,7 @@ function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
'ml-auto': type === 'minus'
}
)}
data-test={`cart-quantity-${type}-button`}
>
{type === 'plus' ? (
<PlusIcon className="h-4 w-4 dark:text-neutral-500" />
@ -36,7 +37,7 @@ export function EditItemQuantityButton({
type: 'plus' | 'minus';
optimisticUpdate: any;
}) {
const [message, formAction] = useFormState(updateItemQuantity, null);
const [message, formAction] = useActionState(updateItemQuantity, null);
const payload = {
merchandiseId: item.merchandise.id,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1

View File

@ -49,7 +49,7 @@ export default function CartModal() {
return (
<>
<button aria-label="Open cart" onClick={openCart}>
<button aria-label="Open cart" onClick={openCart} data-test="cart-open-button">
<OpenCart quantity={cart?.totalQuantity} />
</button>
<Transition show={isOpen}>
@ -63,7 +63,11 @@ export default function CartModal() {
leaveFrom="opacity-100 backdrop-blur-[.5px]"
leaveTo="opacity-0 backdrop-blur-none"
>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div
className="fixed inset-0 bg-black/30"
aria-hidden="true"
data-test="cart-modal-container"
/>
</Transition.Child>
<Transition.Child
as={Fragment}
@ -74,10 +78,15 @@ export default function CartModal() {
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<Dialog.Panel className="fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-neutral-200 bg-white/80 p-6 text-black backdrop-blur-xl md:w-[390px] dark:border-neutral-700 dark:bg-black/80 dark:text-white">
<Dialog.Panel
className="fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-neutral-200 bg-white/80 p-6 text-black backdrop-blur-xl md:w-[390px] dark:border-neutral-700 dark:bg-black/80 dark:text-white"
data-test="cart-side-container"
>
<div className="flex items-center justify-between">
<p className="text-lg font-semibold">My Cart</p>
<button aria-label="Close cart" onClick={closeCart}>
<p className="text-lg font-semibold" data-test="cart-title">
My Cart
</p>
<button aria-label="Close cart" onClick={closeCart} data-test="cart-close-button">
<CloseCart />
</button>
</div>
@ -85,7 +94,9 @@ export default function CartModal() {
{!cart || cart.lines.length === 0 ? (
<div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden">
<ShoppingCartIcon className="h-16" />
<p className="mt-6 text-center text-2xl font-bold">Your cart is empty.</p>
<p className="mt-6 text-center text-2xl font-bold" data-test="cart-empty">
Your cart is empty.
</p>
</div>
) : (
<div className="flex h-full flex-col justify-between overflow-hidden p-1">
@ -112,12 +123,13 @@ export default function CartModal() {
<li
key={i}
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
data-test="cart-item"
>
<div className="relative flex w-full flex-row justify-between px-1 py-4">
<div className="absolute z-40 -ml-1 -mt-2">
<DeleteItemButton item={item} optimisticUpdate={updateCartItem} />
</div>
<div className="flex flex-row">
<div className="flex flex-row" data-test="cart-product-container">
<div className="relative h-16 w-16 overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<Image
className="h-full w-full object-cover"
@ -128,6 +140,7 @@ export default function CartModal() {
item.merchandise.product.title
}
src={item.merchandise.product.featuredImage.url}
data-test="cart-product-image"
/>
</div>
<Link
@ -160,7 +173,12 @@ export default function CartModal() {
optimisticUpdate={updateCartItem}
/>
<p className="w-6 text-center">
<span className="w-full text-sm">{item.quantity}</span>
<span
className="w-full text-sm"
data-test="cart-product-quantity"
>
{item.quantity}
</span>
</p>
<EditItemQuantityButton
item={item}
@ -174,7 +192,10 @@ export default function CartModal() {
);
})}
</ul>
<div className="py-4 text-sm text-neutral-500 dark:text-neutral-400">
<div
className="py-4 text-sm text-neutral-500 dark:text-neutral-400"
data-test="payment-information"
>
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 dark:border-neutral-700">
<p>Taxes</p>
<Price
@ -217,6 +238,7 @@ function CheckoutButton() {
className="block w-full rounded-full bg-blue-600 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100"
type="submit"
disabled={pending}
data-test="checkout-button"
>
{pending ? <LoadingDots className="bg-white" /> : 'Proceed to Checkout'}
</button>

View File

@ -6,20 +6,24 @@ import Link from 'next/link';
function ThreeItemGridItem({
item,
size,
priority
priority,
index
}: {
item: Product;
size: 'full' | 'half';
priority?: boolean;
index: number;
}) {
return (
<div
className={size === 'full' ? 'md:col-span-4 md:row-span-2' : 'md:col-span-2 md:row-span-1'}
data-test={index !== undefined ? `grid-item-${size}-${index}` : `grid-item-${size}`}
>
<Link
className="relative block aspect-square h-full w-full"
href={`/product/${item.handle}`}
prefetch={true}
data-test="product-link"
>
<GridTileImage
src={item.featuredImage.url}
@ -52,10 +56,13 @@ export async function ThreeItemGrid() {
const [firstProduct, secondProduct, thirdProduct] = homepageItems;
return (
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]">
<ThreeItemGridItem size="full" item={firstProduct} priority={true} />
<ThreeItemGridItem size="half" item={secondProduct} priority={true} />
<ThreeItemGridItem size="half" item={thirdProduct} />
<section
className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]"
data-test="three-item-grid"
>
<ThreeItemGridItem size="full" item={firstProduct} index={0} priority={true} />
<ThreeItemGridItem size="half" item={secondProduct} index={1} priority={true} />
<ThreeItemGridItem size="half" item={thirdProduct} index={2} />
</section>
);
}

View File

@ -27,6 +27,7 @@ export function GridTileImage({
'border-neutral-200 dark:border-neutral-800': !active
}
)}
data-test="tile-container"
>
{props.src ? (
<Image
@ -34,6 +35,7 @@ export function GridTileImage({
'transition duration-300 ease-in-out group-hover:scale-105': isInteractive
})}
{...props}
data-test="tile-image"
/>
) : null}
{label ? (

View File

@ -17,9 +17,18 @@ const Label = ({
className={clsx('absolute bottom-0 left-0 flex w-full px-4 pb-4 @container/label', {
'lg:px-20 lg:pb-[35%]': position === 'center'
})}
data-test="label-container"
>
<div className="flex items-center rounded-full border bg-white/70 p-1 text-xs font-semibold text-black backdrop-blur-md dark:border-neutral-800 dark:bg-black/70 dark:text-white">
<h3 className="mr-4 line-clamp-2 flex-grow pl-2 leading-none tracking-tight">{title}</h3>
<div
className="flex items-center rounded-full border bg-white/70 p-1 text-xs font-semibold text-black backdrop-blur-md dark:border-neutral-800 dark:bg-black/70 dark:text-white"
data-test="label-content-wrapper"
>
<h3
className="mr-4 line-clamp-2 flex-grow pl-2 leading-none tracking-tight"
data-test="label-title-text"
>
{title}
</h3>
<Price
className="flex-none rounded-full bg-blue-600 p-2 text-white"
amount={amount}

View File

@ -15,7 +15,7 @@ export default async function Footer() {
const copyrightName = COMPANY_NAME || SITE_NAME || '';
return (
<footer className="text-sm text-neutral-500 dark:text-neutral-400">
<footer className="text-sm text-neutral-500 dark:text-neutral-400" data-test="footer">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6 border-t border-neutral-200 px-6 py-12 text-sm md:flex-row md:gap-12 md:px-4 min-[1320px]:px-0 dark:border-neutral-700">
<div>
<Link className="flex items-center gap-2 text-black md:pt-1 dark:text-white" href="/">

View File

@ -13,7 +13,7 @@ export async function Navbar() {
const menu = await getMenu('next-js-frontend-header-menu');
return (
<nav className="relative flex items-center justify-between p-4 lg:px-6">
<nav className="relative flex items-center justify-between p-4 lg:px-6" data-test="navbar">
<div className="block flex-none md:hidden">
<Suspense fallback={null}>
<MobileMenu menu={menu} />
@ -25,6 +25,7 @@ export async function Navbar() {
href="/"
prefetch={true}
className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6"
data-test="navbar-logo"
>
<LogoSquare />
<div className="ml-2 flex-none text-sm font-medium uppercase md:hidden lg:block">

View File

@ -36,6 +36,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
onClick={openMobileMenu}
aria-label="Open mobile menu"
className="flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors md:hidden dark:border-neutral-700 dark:text-white"
data-test="mobile-menu-button"
>
<Bars3Icon className="h-4" />
</button>

View File

@ -17,6 +17,7 @@ export default function Search() {
autoComplete="off"
defaultValue={searchParams?.get('q') || ''}
className="text-md w-full rounded-lg border bg-white px-4 py-2 text-black placeholder:text-neutral-500 md:text-sm dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
data-test="search-input"
/>
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
<MagnifyingGlassIcon className="h-4" />

View File

@ -26,6 +26,7 @@ function PathFilterItem({ item }: { item: PathFilterItem }) {
'underline underline-offset-4': active
}
)}
data-test="search-filter-item"
>
{item.title}
</DynamicTag>

View File

@ -3,7 +3,7 @@ import clsx from 'clsx';
const Price = ({
amount,
className,
currencyCode = 'USD',
currencyCode = 'EUR',
currencyCodeClassName
}: {
amount: string;
@ -11,13 +11,16 @@ const Price = ({
currencyCode: string;
currencyCodeClassName?: string;
} & React.ComponentProps<'p'>) => (
<p suppressHydrationWarning={true} className={className}>
<p suppressHydrationWarning={true} className={className} data-test="price-amount">
{`${new Intl.NumberFormat(undefined, {
style: 'currency',
currency: currencyCode,
currencyDisplay: 'narrowSymbol'
}).format(parseFloat(amount))}`}
<span className={clsx('ml-1 inline', currencyCodeClassName)}>{`${currencyCode}`}</span>
<span
className={clsx('ml-1 inline', currencyCodeClassName)}
data-test="price-currency-code"
>{`${currencyCode}`}</span>
</p>
);

View File

@ -8,8 +8,13 @@ export function ProductDescription({ product }: { product: Product }) {
return (
<>
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
<h1 className="mb-2 text-5xl font-medium">{product.title}</h1>
<div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white">
<h1 className="mb-2 text-5xl font-medium" data-test="product-title">
{product.title}
</h1>
<div
className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white"
data-test="price-container"
>
<Price
amount={product.priceRange.maxVariantPrice.amount}
currencyCode={product.priceRange.maxVariantPrice.currencyCode}

View File

@ -81,6 +81,7 @@ export function VariantSelector({
!isAvailableForSale
}
)}
data-test={`${option.name}-${value}`}
>
{value}
</button>

View File

@ -1,10 +1,28 @@
import { defineConfig } from 'cypress';
import createBundler from '@bahmutov/cypress-esbuild-preprocessor';
import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor';
import createEsbuildPlugin from '@badeball/cypress-cucumber-preprocessor/esbuild';
async function setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
await addCucumberPreprocessorPlugin(on, config);
on(
'file:preprocessor',
createBundler({
plugins: [createEsbuildPlugin(config)]
})
);
return config;
}
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {
// implement node event listeners here
}
specPattern: '**/*.feature',
setupNodeEvents
}
});

View File

@ -0,0 +1,23 @@
import { Given } from '@badeball/cypress-cucumber-preprocessor';
import { toSlug, verifyRequest } from 'cypress/utils/url';
Given('I am on the product detail page for {string}', (productName: string) => {
const slugifiedProductName = toSlug(productName);
cy.intercept('GET', `/product/${slugifiedProductName}`).as('getProduct');
cy.intercept('POST', '/product/*').as('postProduct');
// Visite de la page produit
cy.visit(`/product/${slugifiedProductName}`);
// Femrer la notification de NextJs
cy.get('body > main > section')
.find('button')
.should('exist')
.and('be.visible')
.and('not.be.disabled')
.trigger('click');
// Vérifie la réponse du produit
verifyRequest('@getProduct');
verifyRequest('@postProduct');
});

View File

@ -0,0 +1,64 @@
Feature: Add product to cart
As a customer
I want to add products to my shopping cart
So that I can purchase them later
Scenario: Add a product to the cart from the product page
Given I am on the product detail page for "<product-name>"
When I select "<color>" color of "<product-name>"
And I click "Add to Cart"
Then the product should be added to my shopping cart
Examples:
| color | product-name |
| Ice |The Complete Snowboard|
| Dawn |The Complete Snowboard|
| Powder |The Complete Snowboard|
| Electric |The Complete Snowboard|
| Sunset |The Complete Snowboard|
Scenario: Check that the cart total updates after adding a product
Given I am on the product detail page for "<product-name>"
When I click "Add to Cart"
And the "<product-name>" costs "<amount>" "<code-currency>"
Then the total in the shopping cart should be updated to "<amount>" "<code-currency>"
Examples:
| product-name | amount | code-currency |
| The Compare at Price Snowboard |785,95 | EUR |
Scenario: Cannot add a product to the cart without selecting a required option
Given I am on the product detail page for "<product-name>"
When I do not select a color
Then I Can not add the product in the shopping cart
Examples:
| product-name |
|The Complete Snowboard|
Scenario: Add a product to the cart that is out of stock
Given I am on the product detail page for "<product-name>"
And the "<product-name>" is out of stock
When I click "Add to Cart"
Then the product should not be added to my shopping cart
And "Add to Cart" button should have the name "Out Of Stock"
Examples:
| product-name |
|The Out of Stock Snowboard|
Scenario: Add multiple quantities of a product to the cart
Given I am on the product detail page for "<product-name>"
When I select "<color>" color of "<product-name>"
And I click "Add to Cart"
And I select "<quantity>" quantities
Then the cart should contain "<quantity>" items of "<product-name>"
Examples:
| color | product-name |quantity |
| Ice |The Complete Snowboard| 2 |
| Dawn |The Complete Snowboard| 2 |
| Powder |The Complete Snowboard| 2 |
| Electric |The Complete Snowboard| 2 |
| Sunset |The Complete Snowboard| 2 |

View File

@ -0,0 +1,209 @@
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor';
import { toSlug, verifyRequest } from 'cypress/utils/url';
Given(
'the {string} is out of stock',
(productName: string) => {
// Charger la fixture
cy.fixture('product-query.json').then((query) => {
// Insérer le nom du produit dans la requête
query.variables.productName = productName; // Nom du produit à interroger
// Faire la requête GraphQL
cy.request({
method: 'POST',
url: 'testify-automation.myshopify.com/api/2024-07/graphql', // URL complète de l'API GraphQL de Shopify
body: query,
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': '8bc822f670ae4977b83b0975fa80054b' // Token d'accès
},
failOnStatusCode: false // Empêche Cypress d'échouer automatiquement
}).then((response) => {
// Afficher la réponse dans la console pour le débogage
console.log('response : ', response);
// Vérifier le statut de la réponse
expect(response.status).to.eq(200);
// Vérifier si le produit existe dans la réponse
const product = response.body.data?.products?.edges[0]?.node;
expect(product).to.exist;
// Assertion pour le nom du produit
expect(product).to.have.property('title');
expect(product.title).to.equal(productName);
// Assertion du stock du produit
expect(product).to.have.property('totalInventory');
expect(product.totalInventory).to.equal(0);
});
});
}
);
When('I select {string} color of {string}', (expectedColor: string, productName: string) => {
const slugifiedProductName = toSlug(productName);
// Intercepter les requêtes pour sélectionner une variante et changer la couleur
cy.intercept('GET', `/product\/${slugifiedProductName}\?color=*`).as('getColorVariant');
// cy.intercept('POST', `/product\/${slugifiedProductName}`).as('selectColorVariant');
// Vérifier l'état du bouton "Add to Cart" avant la sélection de la couleur
cy.getBySel('add-to-cart').should('exist').and('be.visible').and('be.disabled');
// Vérifie la couleur attendue et sélectionne-la
cy.getBySel(`Color-${expectedColor}`)
.should('exist')
.and('be.visible')
.and('be.enabled')
.invoke('text')
.should('equal', `${expectedColor}`);
cy.getBySel(`Color-${expectedColor}`).click();
// Vérifie les requêtes après sélection de la couleur
// verifyRequest('@postProduct');
verifyRequest('@getColorVariant');
// verifyRequest('@selectColorVariant');
});
When(
'the {string} costs {string} {string}',
(productName: string, amount: string, currencyCode: string) => {
// Charger la fixture
cy.fixture('product-query.json').then((query) => {
// Insérer le nom du produit dans la requête
query.variables.productName = productName; // Nom du produit à interroger
// Faire la requête GraphQL
cy.request({
method: 'POST',
url: 'testify-automation.myshopify.com/api/2024-07/graphql', // URL complète de l'API GraphQL de Shopify
body: query,
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': '8bc822f670ae4977b83b0975fa80054b' // Token d'accès
},
failOnStatusCode: false // Empêche Cypress d'échouer automatiquement
}).then((response) => {
// Afficher la réponse dans la console pour le débogage
console.log('response : ', response);
// Vérifier le statut de la réponse
expect(response.status).to.eq(200);
// Vérifier si le produit existe dans la réponse
const product = response.body.data?.products?.edges[0]?.node;
expect(product).to.exist;
// Assertion pour le nom du produit
expect(product).to.have.property('title');
expect(product.title).to.equal(productName);
// Vérifier si les variantes existent
const variant = product.variants?.edges[0]?.node;
expect(variant).to.exist;
// Vérifier les informations sur le prix
const price = variant.price;
expect(price).to.have.property('amount');
expect(price.amount).to.equal(amount.replace(',', '.'));
expect(price).to.have.property('currencyCode');
expect(price.currencyCode).to.equal(currencyCode);
// Logs supplémentaires pour vérifier les variantes et les prix
product.variants.edges.forEach((variant: any) => {
cy.log(`Variant: ${variant.node.title}`);
cy.log(`Price: ${variant.node.price.amount} ${variant.node.price.currencyCode}`);
});
});
});
}
);
When('I click {string}', (buttonName: string) => {
// cy.intercept('POST', '/product/*').as('createNewCart');
const slugifiedButtonName = toSlug(buttonName);
// Vérifie l'existence et l'état du bouton
cy.getBySel(slugifiedButtonName).should('exist').and('be.visible');
// Vérifie que le cookie `cartId` est bien présent et défini
// cy.wait('@postProduct');
// cy.wait('@createNewCart');
cy.getCookie('cartId').should('exist').its('value').should('not.be.undefined');
// Clique sur le bouton
cy.getBySel(slugifiedButtonName).click({ force: true });
});
When('I do not select a color', () => {
cy.get('[data-test^="Color-"]').should('exist').and('be.visible');
});
When('I select {string} quantities', (quantity: string) => {
cy.getBySel('cart-quantity-plus-button').click();
Array.from({ length: parseInt(quantity) }).forEach(() => {});
});
Then(
'the cart should contain {string} items of {string}',
(quantity: string, productName: string) => {
cy.getBySel('cart-product-container')
.invoke('text')
.then((productProperty) => {
expect(productProperty).contains(productName);
});
// Vérifier que la quantité affichée correspond à la quantité sélectionnée
cy.getBySel('cart-product-quantity').should('have.text', quantity);
}
);
Then('the product should be added to my shopping cart', () => {
// Vérifie que le panier contient bien le produit
cy.getBySel('cart-open-button').invoke('text').should('equal', '1');
cy.getBySel('cart-item').should('have.length', 1);
});
Then(
'the total in the shopping cart should be updated to {string} {string}',
(amount: string, codeCurrency: string) => {
cy.get('[data-test="cart-side-container"] [data-test="price-amount"]')
.should('exist')
.and('be.visible')
.invoke('text')
.then((text) => {
// Vérifier que le texte contient la devise
expect(text).to.include(codeCurrency);
});
cy.getAmount(
'[data-test="payment-information"] > :nth-child(3) > [data-test="price-amount"]',
amount
);
}
);
Then('the product should not be added to my shopping cart', () => {
cy.getBySel('cart-open-button').click();
cy.getBySel('cart-item').should('not.exist');
cy.getBySel('cart-empty')
.should('exist')
.and('be.visible')
.invoke('text')
.then((label) => {
expect(label).to.be.equal('Your cart is empty.');
});
cy.getBySel('cart-close-button').click();
});
Then('I Can not add the product in the shopping cart', () => {
cy.getBySel('add-to-cart').should('exist').and('be.visible').and('be.disabled');
});
Then('"Add to Cart" button should have the name "Out Of Stock"', () => {
cy.getBySel('add-to-cart')
.should('exist')
.invoke('text')
.then((label) => {
expect(label).to.equals('Out Of Stock');
});
});

View File

@ -0,0 +1,13 @@
Feature: Display products on homepage
As a customer
I want to see the list of available products with their names and prices
So that I can decide what to buy
Scenario: Display list of products on the homepage
Given I am on the homepage
When the page loads
Then I should see a list of products
And each product should have an image
And each product should have a name
And each product should have a price
And the price should be displayed in EUR with two decimal points

View File

@ -0,0 +1,41 @@
import { When, Then } from '@badeball/cypress-cucumber-preprocessor';
// Étape pour attendre le chargement de la page
When('the page loads', () => {
cy.get('main').should('exist').and('be.visible'); // Attend 2 secondes pour que la page soit complètement chargée
});
// Étape pour vérifier que la liste des produits est visible
Then('I should see a list of products', () => {
cy.getBySel('product-link').should('exist').and('be.visible'); // Vérifie que la liste des produits est visible
});
// Étape pour vérifier que chaque produit a une image
Then('each product should have an image', () => {
cy.getBySel('tile-image').each(($el) => {
cy.wrap($el).should('have.attr', 'src').and('not.be.empty'); // Vérifie que chaque produit a une image
});
});
// Étape pour vérifier que chaque produit a un nom
Then('each product should have a name', () => {
cy.getBySel('label-title-text').each(($el) => {
cy.wrap($el).should('not.be.empty'); // Vérifie que chaque produit a un nom
});
});
// Étape pour vérifier que chaque produit a un prix
Then('each product should have a price', () => {
cy.getBySel('price-amount').each(($el) => {
cy.wrap($el).should('not.be.empty'); // Vérifie que chaque produit a un prix
});
});
// Étape pour vérifier que le prix est au format EUR avec deux décimales
Then('the price should be displayed in EUR with two decimal points', () => {
cy.getBySel('price-amount').each(($el) => {
cy.wrap($el)
.invoke('text')
.should('match', /^[€$£]?(\d{1,3}(,\d{3})*|\d+)\.\d{2}[A-Z]{3}$/); // Vérifie le format EUR avec deux décimales
});
});

View File

@ -0,0 +1,7 @@
import { Given } from '@badeball/cypress-cucumber-preprocessor';
// Étape pour aller à la page d'accueil
Given('I am on the homepage', () => {
cy.visit('/');
});

182
cypress/e2e/header.cy.ts Normal file
View File

@ -0,0 +1,182 @@
import { DATA_TEST_SELECTORS } from 'cypress/utils/constants';
describe('Header Tests', () => {
beforeEach(() => {
cy.visit('/');
// Femrer la notification de NextJs
cy.get('body > main > section')
.find('button')
.should('exist')
.and('be.visible')
.and('not.be.disabled')
.trigger('click');
});
it('should display the correct grid items', () => {
cy.visit('/');
cy.getBySel('navbar').should('be.visible');
cy.getBySel('navbar-logo').should('be.visible');
cy.getBySel('navbar-logo').should('have.attr', 'href', '/');
cy.getBySel('navbar-logo').click();
cy.location('pathname').should('eq', '/');
cy.getBySel('search-input').click().type('{enter}');
cy.location('pathname').should('eq', '/search');
cy.getBySel('search-filter-item').each((item: JQuery<HTMLElement>) => {
const text = item.text();
if (text === 'Hydrogen') {
cy.wrap(item).click();
}
});
cy.location('pathname').should('eq', '/search/hydrogen');
});
it('users can add products to the cart with quantity eq 1 and another product with quantity eq 2', () => {
/*
Aller vers le premier produit que nous allons ajouter au panier
Vérifier que nous atterisson sur le bon chemin du produit
Vérifier qu'on récupére bien les élements suivants :
- Image
- Titre
- Description
*/
cy.get(DATA_TEST_SELECTORS.productCarouselSection.items(2).product.link).click();
cy.location('pathname').should('eq', '/product/the-inventory-not-tracked-snowboard');
cy.getBySel('product-page').should('exist');
cy.getBySel('product-details').should('exist');
cy.getBySel('product-details').within(() => {
cy.getBySel('product-image').should('exist');
});
cy.getBySel('product-description').should('exist');
cy.getBySel('product-title')
.invoke('text')
.then((title: string) => {
expect(title).to.equal('The Inventory Not Tracked Snowboard');
});
/*
Préparer l'interception de l'appel au serveur afin d'être sûr que l'action est fini avant denclencher une nouvelle
- Nous allons jouer le scénario suivant :
1. Nous allons prendre un seul premier produit et le mettre au panier et vérifier qu'il a :
- Une image
- Un nom produit
- Un prix avec devise
- Quantité attendu
2. Nous allons prendre un deuxième produit en double et vérifier les mêmes informations que le premier produit.
*/
cy.intercept('POST', '/product/the-inventory-not-tracked-snowboard').as('addItemToCart'); // Associer une requête réseau
cy.getBySel('submit-button').should('exist').invoke('text').should('equal', 'Add To Cart');
cy.wait(500);
cy.getBySel('submit-button').click(); // J'actionne l'ajout au panier'
cy.wait('@addItemToCart'); // Attendre que la requête réseau soit terminée
cy.get('[id^="headlessui-dialog-panel"]').should('exist').and('be.visible'); // Vérifier que la model est présente
// Vérification du produit
cy.getBySel('cart-item').should('exist').and('have.length', 1); // Vérifier qu'on un seul produit dans le panier'
cy.getBySel('cart-product-quantity').eq(0).invoke('text').should('equal', '1'); // Vérifier qu'on a pris un seul (quantité) produit'
/* Vérification de l'image qu'elle
- Existe
- Visible
- est chargée
- Défini pour l'accessibilité
*/
cy.getBySel('cart-product-image')
.should('exist') // Vérifie que l'élément existe
.and('be.visible') // Vérifie que l'élément est visible
.then(($img) => {
// Vérifier que l'élément est bien une image
const img = $img[0] as HTMLImageElement;
// Utiliser 'expect' pour vérifier les propriétés de l'image
expect(img).to.have.property('naturalWidth').that.is.greaterThan(0); // Vérifier la largeur naturelle
expect(img).to.have.property('naturalHeight').that.is.greaterThan(0); // Vérifier la hauteur naturelle
expect(img).to.have.property('width').that.is.greaterThan(0); // Vérifier la largeur naturelle
expect(img).to.have.property('height').that.is.greaterThan(0); // Vérifier la hauteur naturelle
expect(img.src).to.not.be.empty; // Vérifier que l'URL de l'image n'est pas vide
expect(img.alt).to.not.be.empty; // Vérifier que l'attribut alt n'est pas vide
});
// Fermer la modal et vérifier qu'il y a 5 images'
cy.getBySel('cart-close-button').click(); // Fermer la modal pour pouvoir séléctionner les produits en dehors
cy.getBySel('product-related').within((product) => {
// Séléctionner la section aux produits liés
cy.wrap(product).find('h2').invoke('text').should('equal', 'Related Products'); // Vérifier le titre des produits présentés
cy.getBySel('product-item').should('have.length', 5);
cy.getBySel('tile-image')
.should('exist')
.and('be.visible')
.then(($img) => {
const img = $img[0] as HTMLImageElement;
// Utiliser 'expect' pour vérifier les propriétés de l'image
expect(img).to.have.property('naturalWidth').that.is.greaterThan(0); // Vérifier la largeur naturelle
expect(img).to.have.property('naturalHeight').that.is.greaterThan(0); // Vérifier la hauteur naturelle
expect(img).to.have.property('width').that.is.greaterThan(0); // Vérifier la largeur naturelle
expect(img).to.have.property('height').that.is.greaterThan(0); // Vérifier la hauteur naturelle
expect(img.src).to.not.be.empty; // Vérifier que l'URL de l'image n'est pas vide
expect(img.alt).to.not.be.empty; // Vérifier que l'attribut alt n'est pas vide
});
});
cy.getBySel('product-item-link').eq(2).click();
cy.intercept('POST', '/product/the-collection-snowboard-oxygen').as('addItem2ToCart'); // Associer une requête réseau
cy.getBySel('submit-button').should('exist').invoke('text').should('equal', 'Add To Cart');
cy.wait(100);
cy.getBySel('submit-button').click(); // J'actionne l'ajout au panier'
cy.wait('@addItem2ToCart'); // Attendre que la requête réseau soit terminée
cy.getBySel('cart-side-container').should('exist').and('be.visible'); // Vérifier que la model est présente
// Vérification du produit
cy.getBySel('cart-item').should('exist').and('have.length', 2); // Vérifier qu'on un seul produit dans le panier'
// cy.getBySel('cart-quantity-plus-button').eq(1).click().click(); // Augmenter la quantité de 2
cy.getBySel('cart-quantity-plus-button').eq(1).click().click();
cy.getBySel('cart-product-quantity')
.should('exist')
.and('be.visible')
.eq(1)
.invoke('text')
.should('equal', '3'); // Vérifier qu'on a une quantité de 3'
cy.getBySel('cart-quantity-minus-button').eq(1).click(); // Diminier la quantité de 1
cy.getBySel('cart-product-quantity')
.should('exist')
.and('be.visible')
.eq(1)
.invoke('text')
.should('equal', '2'); // Vérifier qu'on a une quantité de 2'
// Vérifier le montant total avec le checkout
cy.getAmount(
'[data-test="payment-information"] > :nth-child(3) > [data-test="price-amount"]',
'2924,90',
'€',
'EUR'
);
cy.getBySel('checkout-button').click();
cy.origin('https://testify-automation.myshopify.com/password', () => {
cy.url().should('equal', 'https://testify-automation.myshopify.com/password');
cy.get('#password').type('skifro');
// cy.get('button').click();
});
});
it.only('Vérifier les cookies', () => {
cy.getAllCookies().then((cookies) => {
if (cookies.length > 0) {
cookies.forEach((cookie) => {
cy.log(`Nom du cookie : ${cookie.name}`);
cy.log(`Valeur du cookie : ${cookie.value}`);
console.log(`Nom du cookie : ${cookie.name}`);
console.log(`Valeur du cookie : ${cookie.value}`);
});
// Tu peux accéder à ses propriétés comme `cookie.name`, `cookie.value`, etc.}
} else {
cy.log('Pas de cookie trouvés');
}
});
});
});

View File

@ -1,5 +0,0 @@
describe('Home Page', () => {
it('displays all 3 products on the home page', () => {
cy.visit('/');
});
});

View File

@ -0,0 +1,41 @@
import { DATA_TEST_SELECTORS } from 'cypress/utils/constants';
describe('Three Item Grid Tests', () => {
beforeEach(() => {
// Visiting the home page
cy.visit('/');
});
// List all selector that will be used for the home page
const threeGridItem = () => cy.get(DATA_TEST_SELECTORS.productGridSection.items().container);
const fullSizeGridItem = () =>
cy.get(DATA_TEST_SELECTORS.productGridSection.items('full', 0).container);
// First test case: checks if the grid displays the correct items
it('Verify all grid item', () => {
threeGridItem().should('exist').and('be.visible').and('have.length', 3);
// Checking if the first 'full size' grid item exists
fullSizeGridItem().should('exist');
// Verifying the price of the first full-size grid item (good to verify both amount and currency)
const fullSizeGridItemPrice = DATA_TEST_SELECTORS.productGridSection.items('full', 0).product
.price.amount;
cy.verifyProductPrice(
fullSizeGridItemPrice,
'€', // Currency symbol to be checked
'749.95', // Expected price amount
'EUR' // Expected currency code
);
});
// Second test case: checks if the user can navigate to the correct product page
it('should navigate to the correct product page', () => {
// Clicking the first product link in the product carousel section
cy.get(DATA_TEST_SELECTORS.productCarouselSection.items(0).product.link).click();
// Asserting that the correct product page is loaded by checking the URL
cy.url().should('include', '/product/');
cy.url().should('include', '/product/the-collection-snowboard-oxygen');
});
});

View File

@ -1,5 +1,61 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
"collections": [
"Automated Collection",
"hidden-homepage-carousel",
"hidden-homepage-featured-items",
"Home page",
"Hydrogen"
],
"products": [
{
"published": true,
"name": "Selling Plans Ski Wax",
"grams": 56.69904625,
"price": 24.95,
"gift_card": false,
"image_src": "https://cdn.shopify.com/s/files/1/0841/2905/5048/files/snowboard_wax.png?v=1724052616",
"variant_weight_unit": "oz",
"status": "active"
},
{
"published": null,
"name": "Special Selling Plans Ski Wax",
"grams": 70.8738078125,
"price": 49.95,
"gift_card": null,
"image_src": "https://cdn.shopify.com/s/files/1/0841/2905/5048/files/wax-special.png?v=1724052616",
"variant_weight_unit": "oz",
"status": null
},
{
"published": null,
"name": "Sample Selling Plans Ski Wax",
"grams": 14.1747615625,
"price": 9.95,
"gift_card": null,
"image_src": "https://cdn.shopify.com/s/files/1/0841/2905/5048/files/sample-normal-wax.png?v=1724052616",
"variant_weight_unit": "oz",
"status": null
},
{
"published": true,
"name": "Default Title",
"grams": 0.0,
"price": 749.95,
"gift_card": false,
"image_src": "https://cdn.shopify.com/s/files/1/0841/2905/5048/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724052614",
"variant_weight_unit": "kg",
"status": "active"
},
{
"published": true,
"name": "Default Title",
"grams": 0.0,
"price": 1025.0,
"gift_card": false,
"image_src": "https://cdn.shopify.com/s/files/1/0841/2905/5048/files/Main_d624f226-0a89-4fe1-b333-0d1548b43c06.jpg?v=1724052611",
"variant_weight_unit": "kg",
"status": "active"
}
]
}

View File

@ -0,0 +1,6 @@
{
"query": "query getProduct($productName: String!) { products(first: 1, query: $productName) { edges { node { title totalInventory variants(first: 10) { edges { node { title price { amount currencyCode } } } } } } } }",
"variables": {
"productName": ""
}
}

View File

@ -1,37 +1,62 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
import { destructPrice, omitSubstrings as getFloat } from '../utils/price';
declare global {
namespace Cypress {
interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
getBySel(
selector: string,
...args: Partial<
Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow
>[]
): Chainable<JQuery<HTMLElement>>;
verifyProductPrice(
selector: string,
expectedCurrencySymbol: string,
expectedPrice: string,
expectedCurrencyCode: string
): Chainable<void>;
getAmount(selector: string, priceExpected: string, ...substrings: string[]): Chainable<void>;
}
}
}
Cypress.Commands.add('getBySel', (selector, ...args) => {
return cy.get(`[data-test="${selector}"]`, ...args);
});
Cypress.Commands.add(
'verifyProductPrice',
(
selector: string,
expectedCurrencySymbol: string,
expectedPrice: string,
expectedCurrencyCode: string
) => {
cy.get(selector)
.should('be.visible')
.invoke('text')
.then((amount) => {
const { currencySymbol, price, currencyCode } = destructPrice(amount);
expect(currencySymbol).to.equal(expectedCurrencySymbol);
expect(price).to.equal(expectedPrice.toString());
expect(currencyCode).to.equal(expectedCurrencyCode);
});
}
);
Cypress.Commands.add('getAmount', (selector: string, amountExpected: string) => {
cy.get(selector)
.should('exist')
.and('be.visible')
.invoke('text')
.then((text) => {
const amount = text.replace(/[^\d,\.]/g, '');
expect(amount).to.be.equal(amountExpected);
});
});

View File

@ -14,7 +14,7 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,7 @@
import { Given } from '@badeball/cypress-cucumber-preprocessor';
// Étape pour aller à la page d'accueil
Given('I am on the homepage', () => {
cy.visit('/');
});

View File

@ -0,0 +1,59 @@
//---------------------------------------------------------------------------------------------------------------------------------------------------------------\\
/**
* Generates test identifiers for a grid item, reflecting the product elements.
* @param itemSize - The size of the grid item ('full' or 'half').
* @param itemIndex - Optional index of the item in the grid.
* @returns An object containing test identifiers mapped to product elements in the grid.
*/
function generateGridItemTestIds(itemSize?: 'full' | 'half' | undefined, itemIndex?: number) {
const size = itemSize !== undefined ? `-${itemSize}` : '';
const suffix = itemIndex !== undefined ? `-${itemIndex}` : '';
const itemSelector = `[data-test^="grid-item${size}${suffix}"]`;
return {
container: itemSelector,
product: {
link: `${itemSelector} > [data-test="product-link"]`,
image: `${itemSelector} > [data-test="product-link"] > [data-test="tile-container"] > [data-test="tile-image"]`,
name: `${itemSelector} > [data-test="product-link"] > [data-test="tile-container"] > [data-test="label-container"] > [data-test="label-content-wrapper"] > [data-test="label-title-text"]`,
price: {
amount: `${itemSelector} > [data-test="product-link"] > [data-test="tile-container"] > [data-test="label-container"] > [data-test="label-content-wrapper"] > [data-test="price-amount"]`,
currencyCode: `${itemSelector} > [data-test="product-link"] > [data-test="tile-container"] > [data-test="label-container"] > [data-test="label-content-wrapper"] > [data-test="price-currency-code"]`
}
}
};
}
/**
* Generates test identifiers for a carousel item, reflecting the product elements.
* @param itemIndex - The index of the item in the carousel.
* @returns An object containing test identifiers mapped to product elements in the carousel.
*/
function generateCarouselItemTestIds(itemIndex: number) {
const itemSelector = `[data-test="carousel-item-${itemIndex}"]`;
return {
container: itemSelector,
product: {
link: `${itemSelector} > [data-test="product-link"]`,
image: `${itemSelector} > [data-test="tile-container"] > [data-test="tile-image"]`,
name: `${itemSelector} > [data-test="tile-container"] > [data-test="label-container"] > [data-test="label-content-wrapper"] > [data-test="label-title-text"]`,
price: {
amount: `${itemSelector} > [data-test="tile-container"] > [data-test="label-container"] > [data-test="label-content-wrapper"] > [data-test="price-amount"]`,
currencyCode: `${itemSelector} > [data-test="tile-container"] > [data-test="label-container"] > [data-test="label-content-wrapper"] > [data-test="price-currency-code"]`
}
}
};
}
export const DATA_TEST_SELECTORS = {
productGridSection: {
container: '[data-test="three-item-grid"]',
items: (size?: 'full' | 'half' | undefined, index?: number) =>
generateGridItemTestIds(size, index)
},
productCarouselSection: {
container: '[data-test="carousel-container"]',
items: (index: number) => generateCarouselItemTestIds(index)
}
} as const;

19
cypress/utils/price.ts Normal file
View File

@ -0,0 +1,19 @@
export const destructPrice = (
amount: string
): { currencySymbol?: string; price?: string; currencyCode?: string } => {
const currencySymbol = amount.substring(0, 1);
const price = amount.substring(1, amount.length - 3);
const currencyCode = amount.slice(-3);
return { currencySymbol, price, currencyCode };
};
export const omitSubstrings = (str: string, ...substrings: string[]): string => {
let result = str;
substrings.forEach((substring) => {
const regex = new RegExp(substring, 'g');
result = result.replace(regex, '');
});
return result.trim();
};

View File

16
cypress/utils/url.ts Normal file
View File

@ -0,0 +1,16 @@
export function toSlug(input: string): string {
// 1. Convertir la chaîne en minuscules
let slug = input.toLowerCase();
// 2. Remplacer les caractères accentués ou spéciaux par leurs équivalents non accentués
slug = slug.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
// 3. Remplacer tous les caractères non alphanumériques par des tirets
slug = slug.replace(/[^a-z0-9]+/g, '-');
// 4. Supprimer les tirets de début ou de fin
return slug.replace(/^-+|-+$/g, '');
}
export const verifyRequest = (alias: string) => {
cy.wait(alias).its('response.statusCode').should('eq', 200);
};

10
docs/tests/homepage.md Normal file
View File

@ -0,0 +1,10 @@
Feature: Affichage des produits sur la page d'accueil
Scenario: Vérification de l'affichage des produits avec nom, image et prix
Given I am on the homepage
When the page is loaded
Then I should see a list of products
And each product should have an image
And each product should have a name
And each product should have a price
And the price should be displayed in EUR with two decimal points

99
docs/tests/test-design.md Normal file
View File

@ -0,0 +1,99 @@
# Conception des Tests
## Hiérarchie des Composants et Attributs Data-Test
### Home page
#### ThreeItemGrid Component
Voici un schéma Mermaid qui représente les composants et les hiérarchies des attributs data-test pour le composant `ThreeItemGrid`.
```mermaid
%%{init: {'theme': 'dark'}}%%
graph TD
%% Using a darker green (#339966) with white text (#fff) for better contrast
classDef grid-item-class fill:#339966,stroke:#aaa,stroke-width:2px,color:#fff;
%% Dark blue (#336699) with white text (#fff) for containers
classDef container-class fill:#336699,stroke:#aaa,stroke-width:2px,color:#fff;
%% Dark red (#cc3333) with white text (#fff) for React components
classDef react-component-class fill:#cc3333,stroke:#aaa,stroke-width:2px,color:#fff;
%% Subgraph representing the overall grid structure
subgraph ThreeItemGrid
style ThreeItemGrid fill:#333,stroke:#aaa,stroke-width:2px;
%% Grid Item with configurable size
ThreeItemGridItem[⚛️ Three Item Grids]
class ThreeItemGridItem react-component-class
GridItem[🗂️ data-test: three-item-grid]
ThreeItemGridItem --> GridItem
class GridItem container-class
GridItemSize[🔍 data-test: grid-item <br>⬛ full / ⬜ half]
GridItem --> GridItemSize
class GridItemSize grid-item-class
%% Grid Product Link for GridItemSize
GridItemSize --> GridProductLink[🔍 data-test: grid-product-link]
class GridProductLink grid-item-class
GridProductLink --> GridTileImage[⚛️ GridTileImage]
class GridTileImage react-component-class
GridTileImage --> GridTileContainer[🗂️ data-test: grid-tile-container]
class GridTileContainer container-class
GridTileContainer --> GridTileImageItem[🔍 data-test: grid-tile-image]
class GridTileImageItem grid-item-class
GridTileContainer --> Label[⚛️ Label]
class Label react-component-class
Label --> LabelContainer[🗂️ data-test: label-container]
class LabelContainer container-class
LabelContainer --> LabelContentWrapper[🗂️ data-test: label-content-wrapper]
class LabelContentWrapper container-class
LabelContentWrapper --> LabelTitleText[🔍 data-test: label-title-text]
class LabelTitleText grid-item-class
LabelContentWrapper --> Price[⚛️ Price]
class Price react-component-class
Price --> PriceAmount[🔍 data-test: price-amount]
class PriceAmount grid-item-class
PriceAmount --> PriceCurrencyCode[🔍 data-test: price-currency-code]
class PriceCurrencyCode grid-item-class
end
%% Adding explanatory notes as special nodes
Note1[Note: This component represents a set of products displayed in a grid format.]
Note2[Note: This is the main container of the grid that encompasses all product elements.]
Note3["Note: Defines the size of the item in the grid. Two sizes are possible:
⬛ full or ⬜ half."]
Note4[Note: Link to the specific product detail page.]
Note5[Note: Component that displays the main image of the product in the grid.]
Note6[Note: Container for the product image, labels, and other information.]
Note7[Note: Component to display the textual information of the product, such as the title and price.]
Note8[Note: Displays the product name in the grid.]
Note9[Note: Displays the product price amount with the currency.]
ThreeItemGridItem --> Note1
GridItem --> Note2
GridItemSize --> Note3
GridProductLink --> Note4
GridTileImage --> Note5
GridTileContainer --> Note6
Label --> Note7
LabelTitleText --> Note8
PriceAmount --> Note9
```
[![Grid Product Display](https://mermaid.ink/img/pako:eNqdV92K4zYUfhXhJUwX4rTO3yZuWVhmoCzslqWTUuh4LhRbTsTYkpHkzmSHuel92ZuFQrfQF9gH6PPMC7SP0CPJVvyTZGiHYSbROef7Pp1zrCPfezFPiBd6g8E9ZVSF6P5MbUlOzkJ0lmBxc_bwMBhELGIbgYstWl3oz4MB-kFStkEYaR8i0EYQwtAXzyaT5XI-f45uqdqi2y1VBClyp8CSpulzlHKB1kQpiIg5UwJLFbE4w1JekBRAaOJDSO6bJZTSLAsryKFUgt-Q8BnGuPrs39JEbcNxcTeMecZFqDm-rvRdgC60zkpiRM3ny-VJUVoNpowI2dDjFjt6NNr_0iNIAqRxPIGfk3K-JzhWwJ8XnBGmmqKENvnO1JJmkf-rtMtybYsrSCGIBFBdWugCxH8mAmeZKQwCpDJWpSARk3XEagt1fw0V-xY8IobgR6pdRtoGl7jT0oweDQGaTJiOt1mCSqR0Uwq8BmxJ3xPr2GLR_68ef__0918frMHGa5O8tv42V72owzmt1Tjsf_787RcNnmCFfUUkPCxKQ9mW1Tm6PiIL-f5Lh9OU4hw6rdblvoQtA__HX5vk7nFB36zFy8fPn1BaQrG-RI-f_0BbnKXXbZCWikuXxLYSvd59ELt1eSd4Aq2A3lB2Y9q1D9qCq3mrOB12eDOFdfAz8LjuqmtEHxPYdauJVzQjr3O8IXV_tBZ7TM7ydGPsXZtc53U1D_WM0a7Ay3dFP6jAgZxqjrZjb79V3x5ItRFAT2_fNM2JVPfJ3-A1yeokmy8tdLNyOqnWxWGdzGSmPY5ksR19LIUdrxYtSPsRDrniKW69hVvreFjBHuhJGQ1Xp2VFVUZWMCB6hbQalLb7eoL06V3ssTIeY34naOyeFvOlBW5WThfSujisVzkvWX8Lhbb52Bj7DDbomPami-M5L4UgLN6dw7XmCFtcufj66tMnbSL0qQlLqrH5Kkn0nCR3RYYZVlzsEOPAg7BEsiAxxRksJASCvoP14Er_DWE4ULmf7PuZC3FIEoV4iqpjUKKESgDfwa2BMjCbMQznbY7V6Nqijpuo8Kundg4dtu80DahXTbDaYoVgc0APu9FaYWBUdIhkcO8DITX25CryLDrcOwDKouvpW2Oa4QNkNf4IrW658QBkQVDBpaQwskPkphNMi3o8jSKvYppWuzCHtuKWR6cwpbGTlxDYEKiFg6lWOKvizl06zQartDWyYQ66WnUN2BReAc4dYJ09Pd5aQRppaB89OUSYJYiDXQCarQzlrEZ70ZfHa3EGVD-0JbRJI7YjcohkGW91Sxl__TgbTtPJNc-i4rlo7rsWzHBODm11eSrIwCP7WNobmLbWD46GiNjhW45p9Yj11sb7NXcnME1m17tT23SFNbVHrCn73tA-u00JI7afIaYK1cL-LKwti4h1zxCTGW_o5QTqQRN4MbrXB0TkmZeiyAvhIyMlvLlkkRexB3DFpeKXOxZ7IVyQydATvNxsvTDFmYRvZQHnD7mgGC7MuVstMPuJ87wOIQmF8-OtfRMzL2TGxQvvvTsvnM1HwXwcTCazxXQ8nwXjobfzwmA8G71YLMdfzaaz6XQ5DsYPQ--9AQ1GwXKynC4XwWIeBMF09vAvo7n2MQ?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqdV92K4zYUfhXhJUwX4rTO3yZuWVhmoCzslqWTUuh4LhRbTsTYkpHkzmSHuel92ZuFQrfQF9gH6PPMC7SP0CPJVvyTZGiHYSbROef7Pp1zrCPfezFPiBd6g8E9ZVSF6P5MbUlOzkJ0lmBxc_bwMBhELGIbgYstWl3oz4MB-kFStkEYaR8i0EYQwtAXzyaT5XI-f45uqdqi2y1VBClyp8CSpulzlHKB1kQpiIg5UwJLFbE4w1JekBRAaOJDSO6bJZTSLAsryKFUgt-Q8BnGuPrs39JEbcNxcTeMecZFqDm-rvRdgC60zkpiRM3ny-VJUVoNpowI2dDjFjt6NNr_0iNIAqRxPIGfk3K-JzhWwJ8XnBGmmqKENvnO1JJmkf-rtMtybYsrSCGIBFBdWugCxH8mAmeZKQwCpDJWpSARk3XEagt1fw0V-xY8IobgR6pdRtoGl7jT0oweDQGaTJiOt1mCSqR0Uwq8BmxJ3xPr2GLR_68ef__0918frMHGa5O8tv42V72owzmt1Tjsf_787RcNnmCFfUUkPCxKQ9mW1Tm6PiIL-f5Lh9OU4hw6rdblvoQtA__HX5vk7nFB36zFy8fPn1BaQrG-RI-f_0BbnKXXbZCWikuXxLYSvd59ELt1eSd4Aq2A3lB2Y9q1D9qCq3mrOB12eDOFdfAz8LjuqmtEHxPYdauJVzQjr3O8IXV_tBZ7TM7ydGPsXZtc53U1D_WM0a7Ay3dFP6jAgZxqjrZjb79V3x5ItRFAT2_fNM2JVPfJ3-A1yeokmy8tdLNyOqnWxWGdzGSmPY5ksR19LIUdrxYtSPsRDrniKW69hVvreFjBHuhJGQ1Xp2VFVUZWMCB6hbQalLb7eoL06V3ssTIeY34naOyeFvOlBW5WThfSujisVzkvWX8Lhbb52Bj7DDbomPami-M5L4UgLN6dw7XmCFtcufj66tMnbSL0qQlLqrH5Kkn0nCR3RYYZVlzsEOPAg7BEsiAxxRksJASCvoP14Er_DWE4ULmf7PuZC3FIEoV4iqpjUKKESgDfwa2BMjCbMQznbY7V6Nqijpuo8Kundg4dtu80DahXTbDaYoVgc0APu9FaYWBUdIhkcO8DITX25CryLDrcOwDKouvpW2Oa4QNkNf4IrW658QBkQVDBpaQwskPkphNMi3o8jSKvYppWuzCHtuKWR6cwpbGTlxDYEKiFg6lWOKvizl06zQartDWyYQ66WnUN2BReAc4dYJ09Pd5aQRppaB89OUSYJYiDXQCarQzlrEZ70ZfHa3EGVD-0JbRJI7YjcohkGW91Sxl__TgbTtPJNc-i4rlo7rsWzHBODm11eSrIwCP7WNobmLbWD46GiNjhW45p9Yj11sb7NXcnME1m17tT23SFNbVHrCn73tA-u00JI7afIaYK1cL-LKwti4h1zxCTGW_o5QTqQRN4MbrXB0TkmZeiyAvhIyMlvLlkkRexB3DFpeKXOxZ7IVyQydATvNxsvTDFmYRvZQHnD7mgGC7MuVstMPuJ87wOIQmF8-OtfRMzL2TGxQvvvTsvnM1HwXwcTCazxXQ8nwXjobfzwmA8G71YLMdfzaaz6XQ5DsYPQ--9AQ1GwXKynC4XwWIeBMF09vAvo7n2MQ)

View File

@ -214,15 +214,22 @@ export async function addToCart(
cartId: string,
lines: { merchandiseId: string; quantity: number }[]
): Promise<Cart> {
const res = await shopifyFetch<ShopifyAddToCartOperation>({
query: addToCartMutation,
variables: {
cartId,
lines
},
cache: 'no-store'
});
return reshapeCart(res.body.data.cartLinesAdd.cart);
try {
const res = await shopifyFetch<ShopifyAddToCartOperation>({
query: addToCartMutation,
variables: {
cartId,
lines
},
cache: 'no-store'
});
console.log('Response from Shopify:', res.status); // Log après réception de la réponse
return reshapeCart(res.body.data.cartLinesAdd.cart);
} catch (error) {
console.error('Error during add to cart:', error); // Log en cas d'erreur
throw new Error('Failed to add item to cart');
}
}
export async function removeFromCart(cartId: string, lineIds: string[]): Promise<Cart> {

View File

@ -10,32 +10,32 @@
"start": "next start",
"prettier": "prettier --write --ignore-unknown .",
"prettier:check": "prettier --check --ignore-unknown .",
"test": "pnpm prettier:check",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
"test": "pnpm prettier:check"
},
"dependencies": {
"@headlessui/react": "^2.1.2",
"@heroicons/react": "^2.1.5",
"clsx": "^2.1.1",
"geist": "^1.3.1",
"next": "15.0.0-canary.113",
"react": "19.0.0-rc-3208e73e-20240730",
"react-dom": "19.0.0-rc-3208e73e-20240730",
"next": "15.0.0-canary.155",
"react": "19.0.0-rc-206df66e-20240912",
"react-dom": "19.0.0-rc-206df66e-20240912",
"sonner": "^1.5.0"
},
"devDependencies": {
"@badeball/cypress-cucumber-preprocessor": "^20.1.2",
"@bahmutov/cypress-esbuild-preprocessor": "^2.2.3",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "20.14.12",
"@types/react": "18.3.3",
"@types/node": "22.5.5",
"@types/react": "18.3.5",
"@types/react-dom": "18.3.0",
"autoprefixer": "^10.4.19",
"cypress": "^13.13.3",
"cypress": "^13.14.2",
"postcss": "^8.4.39",
"prettier": "3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.6",
"typescript": "5.5.4"
"typescript": "5.6.2"
}
}

2638
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"target": "ES2022",
"lib": ["ES2022", "dom"],
"downlevelIteration": true,
"allowJs": true,
"skipLibCheck": true,
@ -9,20 +9,27 @@
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"module": "esnext",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"noUncheckedIndexedAccess": true,
"types": ["cypress", "node", "@types/node", "@badeball/cypress-cucumber-preprocessor"],
"plugins": [
{
"name": "next"
}
]
],
"paths": {
"@badeball/cypress-cucumber-preprocessor/*": [
"./node_modules/@badeball/cypress-cucumber-preprocessor/dist/subpath-entrypoints/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "cypress/**/*.ts"],
"exclude": ["node_modules"]
}