This commit is contained in:
@@ -7,7 +7,9 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ui": "npx vitest --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.8",
|
||||
@@ -15,15 +17,22 @@
|
||||
"react-router-dom": "^7.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.5",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
||||
"@typescript-eslint/parser": "^8.67.0",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"@vitest/ui": "^4.1.11",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.4",
|
||||
"jsdom": "^30.0.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.2.2"
|
||||
"vite": "^8.2.2",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ type ImageGalleryProps = {
|
||||
product: Product;
|
||||
};
|
||||
|
||||
const ImageGallery: React.FC<ImageGalleryProps> = ({
|
||||
product,
|
||||
}: {
|
||||
product: Product;
|
||||
}) => {
|
||||
const ImageGallery: React.FC<ImageGalleryProps> = ({ product }: { product: Product }) => {
|
||||
const [selectedImage, setSelectedImage] = useState(product.images[0]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
@@ -24,8 +20,7 @@ const ImageGallery: React.FC<ImageGalleryProps> = ({
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
setSelectedIndex((prevIndex) => {
|
||||
const nextIndex =
|
||||
(prevIndex - 1 + product.images.length) % product.images.length;
|
||||
const nextIndex = (prevIndex - 1 + product.images.length) % product.images.length;
|
||||
setSelectedImage(product.images[nextIndex]);
|
||||
return nextIndex;
|
||||
});
|
||||
@@ -55,15 +50,10 @@ const ImageGallery: React.FC<ImageGalleryProps> = ({
|
||||
onClick={() => {
|
||||
setSelectedImage(image);
|
||||
setSelectedIndex(index);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<span className="screen-reader-text">{image}</span>
|
||||
<span className="image-selector-img-wrapper">
|
||||
<img
|
||||
src={image}
|
||||
alt={product.title}
|
||||
className="image-selector-img"
|
||||
/>
|
||||
<img src={image} alt={product.title} className="image-selector-img" />
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -72,11 +62,7 @@ const ImageGallery: React.FC<ImageGalleryProps> = ({
|
||||
|
||||
{/* Main Image Display */}
|
||||
<div className="image-selector-main-img-wrapper">
|
||||
<img
|
||||
src={selectedImage}
|
||||
alt={product.title}
|
||||
className="image-selector-main-img"
|
||||
/>
|
||||
<img src={selectedImage} alt={product.title} className="image-selector-main-img" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,9 +21,7 @@ const ProductListCard = ({ product }: { product: Product }) => {
|
||||
</h3>
|
||||
|
||||
<div className="product-card-desc-box">
|
||||
<p className="screen-reader-text">
|
||||
{Math.round(rating)} out of 5 stars
|
||||
</p>
|
||||
<p className="screen-reader-text">{Math.round(rating)} out of 5 stars</p>
|
||||
|
||||
<div className="product-card-ratings-wrapper">
|
||||
{[0, 1, 2, 3, 4].map((ratingNumber) => (
|
||||
@@ -56,12 +54,7 @@ export default ProductListCard;
|
||||
|
||||
function StarIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
|
||||
|
||||
@@ -35,5 +35,5 @@ const router = createBrowserRouter([
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -13,9 +13,7 @@ const ProductPage = () => {
|
||||
|
||||
const fetchProduct = async () => {
|
||||
try {
|
||||
const productRes = await fetch(
|
||||
`https://dummyjson.com/products/${productId}`
|
||||
);
|
||||
const productRes = await fetch(`https://dummyjson.com/products/${productId}`);
|
||||
const productData = await productRes.json();
|
||||
setProduct(productData);
|
||||
} catch (error) {
|
||||
@@ -67,16 +65,13 @@ const ProductPage = () => {
|
||||
key={rating}
|
||||
className="star-icons"
|
||||
style={{
|
||||
fill:
|
||||
product.rating > rating ? '#1e293b' : '#e5e7eb',
|
||||
fill: product.rating > rating ? '#1e293b' : '#e5e7eb',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="screen-reader-text">
|
||||
{product.rating} out of 5 stars
|
||||
</p>
|
||||
<p className="screen-reader-text">{product.rating} out of 5 stars</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,9 +84,7 @@ const ProductPage = () => {
|
||||
<div className="product-page-mt-6">
|
||||
<h3 className="screen-reader-text">Description</h3>
|
||||
|
||||
<p className="product-page-description">
|
||||
{product.description}
|
||||
</p>
|
||||
<p className="product-page-description">{product.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
@@ -157,9 +150,7 @@ function Reviews({ reviews }: { reviews: Review[] }) {
|
||||
<div className="product-page-reviews-text-box">
|
||||
<p className="product-page-text">{review.reviewerName}</p>
|
||||
|
||||
<p className="product-page-text">
|
||||
{new Date(review.date).toLocaleDateString()}
|
||||
</p>
|
||||
<p className="product-page-text">{new Date(review.date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<p className="product-page-reviews-comment">{review.comment}</p>
|
||||
@@ -178,25 +169,15 @@ function ChevronRightIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
{...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function StarIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import Hero from '../../src/components/Hero';
|
||||
|
||||
import { it, expect, describe } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
// TestSuite
|
||||
describe('Hero', () => {
|
||||
it('should render h1 element', () => {
|
||||
render(<Hero />);
|
||||
|
||||
// screen.debug();
|
||||
|
||||
const heading = screen.getByRole('heading', { level: 1 });
|
||||
|
||||
expect(heading).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the company name when passed', () => {
|
||||
render(<Hero companyName="My Company" />);
|
||||
|
||||
screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
|
||||
|
||||
// Arrange
|
||||
const heading = screen.getByRole('heading', { level: 1 });
|
||||
// Act & Assert
|
||||
expect(heading).toHaveTextContent('My Company');
|
||||
});
|
||||
|
||||
it('should render the company name Vogue Junction as default', () => {
|
||||
render(<Hero />);
|
||||
|
||||
screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
|
||||
|
||||
// Arrange
|
||||
const heading = screen.getByRole('heading', { level: 1 });
|
||||
// Act & Assert
|
||||
expect(heading).toHaveTextContent(/Vogue Junction/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { it, expect, describe } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import ImageGallery from '../components/ImageGallery';
|
||||
import { fakeProduct } from './__mocks__/product.mock';
|
||||
|
||||
// Dummy/Mock Daten
|
||||
const product = fakeProduct;
|
||||
|
||||
// Arrange
|
||||
const renderComponent = () => {
|
||||
render(<ImageGallery product={product} />);
|
||||
return {
|
||||
images: screen.getAllByAltText(product.title),
|
||||
buttons: screen.getAllByRole('button'),
|
||||
};
|
||||
};
|
||||
|
||||
describe('ImageGallery', () => {
|
||||
it('should render three buttons', () => {
|
||||
// Arrange
|
||||
const { buttons } = renderComponent();
|
||||
|
||||
expect(buttons).toHaveLength(3);
|
||||
});
|
||||
it('should render four images', () => {
|
||||
// Arrange
|
||||
const { images } = renderComponent();
|
||||
|
||||
expect(images).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should render the first image as main image', () => {
|
||||
const { images } = renderComponent();
|
||||
// screen.debug();
|
||||
const mainImage = images[images.length - 1]; // letzte gerenderte Bild ist das erste im product Object
|
||||
|
||||
expect(mainImage).toBeInTheDocument();
|
||||
expect(mainImage).toHaveAttribute(
|
||||
'src',
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/1.png',
|
||||
);
|
||||
});
|
||||
it('should change the main image when the last image is clicked', async () => {
|
||||
const { images, buttons } = renderComponent();
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
// fireEvent.click(buttons[2]);
|
||||
await user.click(buttons[2]); // best practice
|
||||
|
||||
const mainImage = images[images.length - 1];
|
||||
expect(mainImage).toHaveAttribute(
|
||||
'src',
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/3.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { it, expect, describe } from 'vitest';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { fakeProducts } from './__mocks__/products.mock';
|
||||
|
||||
import ProductList from '../components/ProductList';
|
||||
|
||||
const products = fakeProducts;
|
||||
|
||||
describe('ProductList', () => {
|
||||
it('should render the correct number of products', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ProductList data={products} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const headings = screen.getAllByRole('heading', { level: 3 });
|
||||
expect(headings).toHaveLength(products.length);
|
||||
});
|
||||
|
||||
it('should render the product list correctly with the title, image and price', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ProductList data={products} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// screen.debug(); // gerenderte Ergebnis in der Vitest-UI-Konsole sichtbar
|
||||
|
||||
products.forEach((product) => {
|
||||
const { title, thumbnail, price } = product;
|
||||
expect(screen.getByText(title)).toBeInTheDocument(); // title
|
||||
expect(screen.getByAltText(title)).toBeInTheDocument(); // image
|
||||
expect(screen.getByAltText(title)).toHaveAttribute('src', thumbnail); // image src
|
||||
expect(screen.getByText(`$${price}`)).toBeInTheDocument(); // price
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
export const fakeProduct = {
|
||||
id: 174,
|
||||
title: 'Prada Women Bag',
|
||||
description:
|
||||
"The Prada Women Bag is an iconic designer bag that exudes elegance and luxury. Crafted with precision and featuring the Prada logo, it's a statement piece for fashion enthusiasts.",
|
||||
category: 'womens-bags',
|
||||
price: 599.99,
|
||||
discountPercentage: 18.3,
|
||||
rating: 3.52,
|
||||
stock: 43,
|
||||
tags: ['fashion accessories', 'designer bags'],
|
||||
brand: 'Prada',
|
||||
sku: 'WXX4YTJM',
|
||||
weight: 4,
|
||||
dimensions: { width: 23.45, height: 16.1, depth: 5.78 },
|
||||
warrantyInformation: '1 week warranty',
|
||||
shippingInformation: 'Ships overnight',
|
||||
availabilityStatus: 'In Stock',
|
||||
reviews: [
|
||||
{
|
||||
rating: 5,
|
||||
comment: 'Excellent quality!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Samantha Howard',
|
||||
reviewerEmail: 'samantha.howard@x.dummyjson.com',
|
||||
},
|
||||
{
|
||||
rating: 4,
|
||||
comment: 'Would buy again!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Henry Hill',
|
||||
reviewerEmail: 'henry.hill@x.dummyjson.com',
|
||||
},
|
||||
{
|
||||
rating: 3,
|
||||
comment: 'Not worth the price!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Lucas Ramirez',
|
||||
reviewerEmail: 'lucas.ramirez@x.dummyjson.com',
|
||||
},
|
||||
],
|
||||
returnPolicy: '7 days return policy',
|
||||
minimumOrderQuantity: 1,
|
||||
meta: {
|
||||
createdAt: new Date('2024-05-23T08:56:21.627Z'),
|
||||
updatedAt: new Date('2024-05-23T08:56:21.627Z'),
|
||||
barcode: '4590351278063',
|
||||
qrCode: 'https://assets.dummyjson.com/public/qr-code.png',
|
||||
},
|
||||
images: [
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/1.png',
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/2.png',
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/3.png',
|
||||
],
|
||||
thumbnail: 'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/thumbnail.png',
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
export const fakeProducts = [
|
||||
{
|
||||
id: 174,
|
||||
title: 'Prada Women Bag',
|
||||
description:
|
||||
"The Prada Women Bag is an iconic designer bag that exudes elegance and luxury. Crafted with precision and featuring the Prada logo, it's a statement piece for fashion enthusiasts.",
|
||||
category: 'womens-bags',
|
||||
price: 599.99,
|
||||
discountPercentage: 18.3,
|
||||
rating: 3.52,
|
||||
stock: 43,
|
||||
tags: ['fashion accessories', 'designer bags'],
|
||||
brand: 'Prada',
|
||||
sku: 'WXX4YTJM',
|
||||
weight: 4,
|
||||
dimensions: { width: 23.45, height: 16.1, depth: 5.78 },
|
||||
warrantyInformation: '1 week warranty',
|
||||
shippingInformation: 'Ships overnight',
|
||||
availabilityStatus: 'In Stock',
|
||||
reviews: [
|
||||
{
|
||||
rating: 5,
|
||||
comment: 'Excellent quality!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Samantha Howard',
|
||||
reviewerEmail: 'samantha.howard@x.dummyjson.com',
|
||||
},
|
||||
{
|
||||
rating: 4,
|
||||
comment: 'Would buy again!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Henry Hill',
|
||||
reviewerEmail: 'henry.hill@x.dummyjson.com',
|
||||
},
|
||||
{
|
||||
rating: 3,
|
||||
comment: 'Not worth the price!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Lucas Ramirez',
|
||||
reviewerEmail: 'lucas.ramirez@x.dummyjson.com',
|
||||
},
|
||||
],
|
||||
returnPolicy: '7 days return policy',
|
||||
minimumOrderQuantity: 1,
|
||||
meta: {
|
||||
createdAt: new Date('2024-05-23T08:56:21.627Z'),
|
||||
updatedAt: new Date('2024-05-23T08:56:21.627Z'),
|
||||
barcode: '4590351278063',
|
||||
qrCode: 'https://assets.dummyjson.com/public/qr-code.png',
|
||||
},
|
||||
images: [
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/1.png',
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/2.png',
|
||||
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/3.png',
|
||||
],
|
||||
thumbnail: 'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/thumbnail.png',
|
||||
},
|
||||
{
|
||||
id: 90,
|
||||
title: 'Puma Future Rider Trainers',
|
||||
description:
|
||||
'The Puma Future Rider Trainers offer a blend of retro style and modern comfort. Perfect for casual wear, these trainers provide a fashionable and comfortable option for everyday use.',
|
||||
category: 'mens-shoes',
|
||||
price: 89.99,
|
||||
discountPercentage: 3.64,
|
||||
rating: 4.85,
|
||||
stock: 10,
|
||||
tags: ['footwear', 'casual shoes'],
|
||||
brand: 'Puma',
|
||||
sku: '64ORN32I',
|
||||
weight: 8,
|
||||
dimensions: { width: 14.58, height: 25.54, depth: 19.57 },
|
||||
warrantyInformation: '2 year warranty',
|
||||
shippingInformation: 'Ships in 1 month',
|
||||
availabilityStatus: 'In Stock',
|
||||
reviews: [
|
||||
{
|
||||
rating: 5,
|
||||
comment: 'Very happy with my purchase!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Lucas Allen',
|
||||
reviewerEmail: 'lucas.allen@x.dummyjson.com',
|
||||
},
|
||||
{
|
||||
rating: 4,
|
||||
comment: 'Awesome product!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Mason Pearson',
|
||||
reviewerEmail: 'mason.pearson@x.dummyjson.com',
|
||||
},
|
||||
{
|
||||
rating: 4,
|
||||
comment: 'Very satisfied!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Hunter Gordon',
|
||||
reviewerEmail: 'hunter.gordon@x.dummyjson.com',
|
||||
},
|
||||
],
|
||||
returnPolicy: '90 days return policy',
|
||||
minimumOrderQuantity: 9,
|
||||
meta: {
|
||||
createdAt: new Date('2024-05-23T08:56:21.627Z'),
|
||||
updatedAt: new Date('2024-05-23T08:56:21.627Z'),
|
||||
barcode: '3562849555769',
|
||||
qrCode: 'https://assets.dummyjson.com/public/qr-code.png',
|
||||
},
|
||||
images: [
|
||||
'https://cdn.dummyjson.com/products/images/mens-shoes/Puma%20Future%20Rider%20Trainers/1.png',
|
||||
'https://cdn.dummyjson.com/products/images/mens-shoes/Puma%20Future%20Rider%20Trainers/2.png',
|
||||
'https://cdn.dummyjson.com/products/images/mens-shoes/Puma%20Future%20Rider%20Trainers/3.png',
|
||||
'https://cdn.dummyjson.com/products/images/mens-shoes/Puma%20Future%20Rider%20Trainers/4.png',
|
||||
],
|
||||
thumbnail: 'https://cdn.dummyjson.com/products/images/mens-shoes/Puma%20Future%20Rider%20Trainers/thumbnail.png',
|
||||
},
|
||||
{
|
||||
id: 101,
|
||||
title: 'Apple AirPods Max Silver',
|
||||
description:
|
||||
'The Apple AirPods Max in Silver are premium over-ear headphones with high-fidelity audio, adaptive EQ, and active noise cancellation. Experience immersive sound in style.',
|
||||
category: 'mobile-accessories',
|
||||
price: 549.99,
|
||||
discountPercentage: 11.7,
|
||||
rating: 3.11,
|
||||
stock: 7,
|
||||
tags: ['electronics', 'over-ear headphones'],
|
||||
brand: 'Apple',
|
||||
sku: 'HPK82VDE',
|
||||
weight: 4,
|
||||
dimensions: { width: 7.73, height: 18.36, depth: 17.87 },
|
||||
warrantyInformation: '3 months warranty',
|
||||
shippingInformation: 'Ships in 1 month',
|
||||
availabilityStatus: 'In Stock',
|
||||
reviews: [
|
||||
{
|
||||
rating: 1,
|
||||
comment: 'Waste of money!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Harper Kelly',
|
||||
reviewerEmail: 'harper.kelly@x.dummyjson.com',
|
||||
},
|
||||
{
|
||||
rating: 3,
|
||||
comment: 'Not as described!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Abigail Rivera',
|
||||
reviewerEmail: 'abigail.rivera@x.dummyjson.com',
|
||||
},
|
||||
{
|
||||
rating: 5,
|
||||
comment: 'Excellent quality!',
|
||||
date: new Date('2024-05-23T08:56:21.627Z'),
|
||||
reviewerName: 'Nora Russell',
|
||||
reviewerEmail: 'nora.russell@x.dummyjson.com',
|
||||
},
|
||||
],
|
||||
returnPolicy: '90 days return policy',
|
||||
minimumOrderQuantity: 2,
|
||||
meta: {
|
||||
createdAt: new Date('2024-05-23T08:56:21.627Z'),
|
||||
updatedAt: new Date('2024-05-23T08:56:21.627Z'),
|
||||
barcode: '9261269777547',
|
||||
qrCode: 'https://assets.dummyjson.com/public/qr-code.png',
|
||||
},
|
||||
images: ['https://cdn.dummyjson.com/products/images/mobile-accessories/Apple%20AirPods%20Max%20Silver/1.png'],
|
||||
thumbnail:
|
||||
'https://cdn.dummyjson.com/products/images/mobile-accessories/Apple%20AirPods%20Max%20Silver/thumbnail.png',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { add } from '../utils/helpers';
|
||||
|
||||
describe('add', () => {
|
||||
it('should return 3 when 2 and 1 are passed', () => {
|
||||
expect(add(2, 1)).toBe(3);
|
||||
});
|
||||
|
||||
it('should return 0.3 when 0.1 and 0.2 are passed', () => {
|
||||
expect(add(0.1, 0.2)).toBe(0.3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
|
||||
// Test Suite
|
||||
describe('main', () => {
|
||||
// Test Case
|
||||
it('should pass', () => {
|
||||
// AAA - Arrange , Act , Assert
|
||||
const value = true; // Arrange
|
||||
// Act & Assert
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { test, describe, expect } from 'vitest';
|
||||
import { multiply } from '../utils/helpers';
|
||||
|
||||
describe('multiply', () => {
|
||||
test('Multiply returns 10 when 2 and 5 are passed', () => {
|
||||
expect(multiply(2, 5)).toBe(10);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// src/test/setup.ts
|
||||
// npm i @testing-library/jest-dom -D
|
||||
|
||||
import { expect, afterEach } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import * as matchers from '@testing-library/jest-dom/matchers';
|
||||
|
||||
// types für Testfunktionen
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
// add jest-dom matchers to expect
|
||||
expect.extend(matchers);
|
||||
|
||||
// Add cleanup function to run after each test
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// More setup code here, if needed
|
||||
@@ -0,0 +1,12 @@
|
||||
export const multiply = (a: number, b: number): number => {
|
||||
return Number(a) * Number(b);
|
||||
};
|
||||
|
||||
export const add = (a: number, b: number): number => {
|
||||
return parseFloat((Number(a) + Number(b)).toFixed(14).substring(0, 14));
|
||||
};
|
||||
|
||||
export default {
|
||||
multiply,
|
||||
add,
|
||||
};
|
||||
@@ -1,7 +1,12 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vitest/config'; // nicht mehr 'vite'
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: './src/tests/setupTests.ts',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user