diff --git a/package.json b/package.json index 363df682..886d6e5d 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,7 @@ "typescript": "^5.4.5", "validate-branch-name": "^1.3.0", "vite": "^5.2.11", - "vite-plugin-sass": "^0.1.0", - "vitest": "^1.6.0" + "vite-plugin-sass": "^0.1.0" }, "dependencies": { "@commercetools/api-request-builder": "^6.0.0", @@ -66,6 +65,7 @@ "@commercetools/sdk-middleware-http": "^7.0.4", "@types/hammerjs": "^2.0.45", "@types/js-cookie": "^3.0.6", + "@types/sinon": "^17.0.3", "@types/uuid": "^9.0.8", "autoprefixer": "^10.4.19", "hammerjs": "^2.0.8", @@ -73,16 +73,19 @@ "js-cookie": "^3.0.5", "materialize-css": "^1.0.0-rc.2", "modern-normalize": "^2.0.0", + "msw": "^2.3.1", "nouislider": "^15.7.1", "plop": "^4.0.1", "postcode-validator": "^3.8.20", "sharp": "^0.33.3", + "sinon": "^18.0.0", "swiper": "^11.1.3", "uuid": "^9.0.1", "vite-plugin-checker": "^0.6.4", "vite-plugin-image-optimizer": "^1.1.7", "vite-plugin-svg-spriter": "^1.0.0", - "vite-tsconfig-paths": "^4.3.2" + "vite-tsconfig-paths": "^4.3.2", + "vitest": "^1.6.0" }, "homepage": "https://github.com/stardustmeg/ecommerce-application#readme", "license": "ISC" diff --git a/postcss.config.js b/postcss.config.js index a47ef4f9..7738160a 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { plugins: { autoprefixer: {}, }, diff --git a/src/app/App/tests/App.spec.ts b/src/app/App/tests/App.spec.ts index af57744d..a09cb434 100644 --- a/src/app/App/tests/App.spec.ts +++ b/src/app/App/tests/App.spec.ts @@ -2,6 +2,10 @@ import AppModel from '../model/AppModel.ts'; const app = new AppModel(); +/** + * @vitest-environment jsdom + */ + describe('Checking AppModel class', () => { it('application successfully created', () => { expect(app.start()).toBe(true); diff --git a/src/entities/Coupon/test/coupon.spec.ts b/src/entities/Coupon/test/coupon.spec.ts new file mode 100644 index 00000000..0bcf8f54 --- /dev/null +++ b/src/entities/Coupon/test/coupon.spec.ts @@ -0,0 +1,30 @@ +import type { CartCoupon } from '@/shared/types/cart.ts'; + +import sinon from 'sinon'; + +import CouponModel from '../model/CouponModel.ts'; + +/** + * @vitest-environment jsdom + */ +const coupon: CartCoupon = { + coupon: { + cartDiscount: '', + discountCode: '', + id: '', + }, + value: 10, +}; + +const deleteCallback = sinon.fake(); +const couponModel = new CouponModel(coupon, deleteCallback); + +describe('Checking CustomerApi', () => { + it('should check if root is defined', () => { + expect(couponModel).toBeDefined(); + }); + + it('should check if CustomerApi is an instance of CustomerApi', () => { + expect(couponModel).toBeInstanceOf(CouponModel); + }); +}); diff --git a/src/entities/Post/test/post.spec.ts b/src/entities/Post/test/post.spec.ts new file mode 100644 index 00000000..d8b31f8a --- /dev/null +++ b/src/entities/Post/test/post.spec.ts @@ -0,0 +1,22 @@ +import postsData from '@/shared/Posts/posts.ts'; +import sinon from 'sinon'; + +import PostView from '../view/PostView.ts'; + +/** + * @vitest-environment jsdom + */ + +const deleteCallback = sinon.fake(); +const posts = postsData; +const post = new PostView(posts[0], deleteCallback); + +describe('Checking post', () => { + it('should check if post is defined', () => { + expect(post).toBeDefined(); + }); + + it('should check if post is an instance of PostView', () => { + expect(post).toBeInstanceOf(PostView); + }); +}); diff --git a/src/entities/Summary/test/summary.spec.ts b/src/entities/Summary/test/summary.spec.ts new file mode 100644 index 00000000..95a44907 --- /dev/null +++ b/src/entities/Summary/test/summary.spec.ts @@ -0,0 +1,20 @@ +import sinon from 'sinon'; + +import SummaryModel from '../model/SummaryModel.ts'; + +/** + * @vitest-environment jsdom + */ + +const deleteCallback = sinon.fake(); +const summaryModel = new SummaryModel({ en: '', ru: '' }, deleteCallback); + +describe('Checking summaryModel', () => { + it('should check if summaryModel is defined', () => { + expect(summaryModel).toBeDefined(); + }); + + it('should check if summaryModel is an instance of SummaryModel', () => { + expect(summaryModel).toBeInstanceOf(SummaryModel); + }); +}); diff --git a/src/features/InputFieldValidator/test/InputFieldValidator.spec.ts b/src/features/InputFieldValidator/test/InputFieldValidator.spec.ts index fce04ee2..c81760bc 100644 --- a/src/features/InputFieldValidator/test/InputFieldValidator.spec.ts +++ b/src/features/InputFieldValidator/test/InputFieldValidator.spec.ts @@ -2,6 +2,10 @@ import type { InputFieldParams, InputFieldValidatorParams } from '@/shared/types import InputFieldValidatorModel from '../model/InputFieldValidatorModel.ts'; +/** + * @vitest-environment jsdom + */ + const validatorParams: InputFieldValidatorParams = { maxLength: 10, minLength: 2, diff --git a/src/pages/AboutUsPage/view/AboutUsPageView.ts b/src/pages/AboutUsPage/view/AboutUsPageView.ts index 267b31e5..b64ceb4c 100644 --- a/src/pages/AboutUsPage/view/AboutUsPageView.ts +++ b/src/pages/AboutUsPage/view/AboutUsPageView.ts @@ -1,7 +1,11 @@ +import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; +import { LINK_DETAILS } from '@/shared/constants/links.ts'; import { PAGE_DESCRIPTION } from '@/shared/constants/pages.ts'; +import SVG_DETAILS from '@/shared/constants/svg.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import createSVGUse from '@/shared/utils/createSVGUse.ts'; import styles from './aboutUsPageView.module.scss'; @@ -34,12 +38,27 @@ class AboutUsPageView { tag: 'div', }); - this.page.append(this.createTitle(), this.cardsList); + this.page.append(this.createTitle(), this.cardsList, this.createRSSLogo().getHTML()); this.parent.append(this.page); return this.page; } + private createRSSLogo(): LinkModel { + const logo = new LinkModel({ + attrs: { + href: 'https://rs.school', + target: LINK_DETAILS.BLANK, + }, + classes: [styles.logo], + }); + + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.RSS_LOGO)); + logo.getHTML().append(svg); + return logo; + } + private createTitle(): HTMLHeadingElement { const title = createBaseElement({ cssClasses: [styles.title], diff --git a/src/pages/AboutUsPage/view/aboutUsPageView.module.scss b/src/pages/AboutUsPage/view/aboutUsPageView.module.scss index 04c5cdf3..36211330 100644 --- a/src/pages/AboutUsPage/view/aboutUsPageView.module.scss +++ b/src/pages/AboutUsPage/view/aboutUsPageView.module.scss @@ -33,3 +33,28 @@ justify-content: center; } } + +.logo { + display: flex; + margin: 0 auto; + margin-top: var(--small-offset); + width: max-content; + + svg { + width: 11rem; + height: 4rem; + transition: fill 0.1s; + + use { + transition: fill 0.1s; + } + } + + @media (hover: hover) { + &:hover { + svg { + fill: var(--steam-green-1200); + } + } + } +} diff --git a/src/pages/BlogPage/test/blogPage.spec.ts b/src/pages/BlogPage/test/blogPage.spec.ts new file mode 100644 index 00000000..dbcbe814 --- /dev/null +++ b/src/pages/BlogPage/test/blogPage.spec.ts @@ -0,0 +1,22 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import BlogPageModel from '../model/BlogPageModel.ts'; + +/** + * @vitest-environment jsdom + */ + +const parent = createBaseElement({ + tag: 'div', +}); +const blog = new BlogPageModel(parent); + +describe('Checking blog', () => { + it('should check if post is defined', () => { + expect(blog).toBeDefined(); + }); + + it('should check if blog is an instance of BlogPageModel', () => { + expect(blog).toBeInstanceOf(BlogPageModel); + }); +}); diff --git a/src/pages/CartPage/test/cart.spec.ts b/src/pages/CartPage/test/cart.spec.ts new file mode 100644 index 00000000..6b1ae1ca --- /dev/null +++ b/src/pages/CartPage/test/cart.spec.ts @@ -0,0 +1,22 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import CartPageModel from '../model/CartPageModel.ts'; + +/** + * @vitest-environment jsdom + */ + +const parent = createBaseElement({ + tag: 'div', +}); +const cart = new CartPageModel(parent); + +describe('Checking cart', () => { + it('should check if post is defined', () => { + expect(cart).toBeDefined(); + }); + + it('should check if cart is an instance of CartPageModel', () => { + expect(cart).toBeInstanceOf(CartPageModel); + }); +}); diff --git a/src/shared/API/cart/CartApi.ts b/src/shared/API/cart/CartApi.ts index 9c4ec6cb..2716c694 100644 --- a/src/shared/API/cart/CartApi.ts +++ b/src/shared/API/cart/CartApi.ts @@ -1,4 +1,4 @@ -import type { AddCartItem, Cart, CartProduct, EditCartItem } from '@/shared/types/cart.ts'; +import type { Cart } from '@/shared/types/cart.ts'; import type { CartPagedQueryResponse, Cart as CartResponse, @@ -12,41 +12,13 @@ import { CURRENCY } from '@/shared/constants/product.ts'; import getApiClient, { type ApiClient } from '../sdk/client.ts'; -enum Actions { - addLineItem = 'addLineItem', - changeLineItemQuantity = 'changeLineItemQuantity', - removeLineItem = 'removeLineItem', -} -export class CartApi { +export default class CartApi { private client: ApiClient; constructor() { this.client = getApiClient(); } - public async addProduct(cart: Cart, addCartItem: AddCartItem): Promise { - const data = await this.client - .apiRoot() - .me() - .carts() - .withId({ ID: cart.id }) - .post({ - body: { - actions: [ - { - action: Actions.addLineItem, - productId: addCartItem.productId, - quantity: addCartItem.quantity, - variantId: addCartItem.variantId, - }, - ], - version: cart.version, - }, - }) - .execute(); - return data; - } - public async create(): Promise> { const myCart: MyCartDraft = { currency: CURRENCY, @@ -79,49 +51,6 @@ export class CartApi { return data; } - public async deleteProduct(cart: Cart, product: CartProduct): Promise { - const data = await this.client - .apiRoot() - .me() - .carts() - .withId({ ID: cart.id }) - .post({ - body: { - actions: [ - { - action: Actions.removeLineItem, - lineItemId: product.lineItemId, - }, - ], - version: cart.version, - }, - }) - .execute(); - return data; - } - - public async editProductCount(cart: Cart, editCartItem: EditCartItem): Promise { - const data = await this.client - .apiRoot() - .me() - .carts() - .withId({ ID: cart.id }) - .post({ - body: { - actions: [ - { - action: Actions.changeLineItemQuantity, - lineItemId: editCartItem.lineId, - quantity: editCartItem.quantity, - }, - ], - version: cart.version, - }, - }) - .execute(); - return data; - } - public async getActiveCart(): Promise> { const data = await this.client.apiRoot().me().activeCart().get().execute(); return data; @@ -137,14 +66,14 @@ export class CartApi { return data; } - public async setAnonymousId(cart: Cart, actions: CartSetAnonymousIdAction): Promise { + public async setAnonymousId(cart: Cart, actions: CartSetAnonymousIdAction[]): Promise { const data = await this.client .apiRoot() .carts() .withId({ ID: cart.id }) .post({ body: { - actions: [actions], + actions, version: cart.version, }, }) @@ -168,11 +97,3 @@ export class CartApi { return data; } } - -const createCartApi = (): CartApi => new CartApi(); - -const cartApi = createCartApi(); - -export default function getCartApi(): CartApi { - return cartApi; -} diff --git a/src/shared/API/cart/model/CartModel.ts b/src/shared/API/cart/model/CartModel.ts index 2650c8f1..70f81a94 100644 --- a/src/shared/API/cart/model/CartModel.ts +++ b/src/shared/API/cart/model/CartModel.ts @@ -21,10 +21,12 @@ import getProductModel from '../../product/model/ProductModel.ts'; import FilterProduct from '../../product/utils/filter.ts'; import { Attribute, FilterFields } from '../../types/type.ts'; import { isCart, isCartPagedQueryResponse, isClientResponse } from '../../types/validation.ts'; -import getCartApi, { type CartApi } from '../CartApi.ts'; +import CartApi from '../CartApi.ts'; enum ACTIONS { addDiscountCode = 'addDiscountCode', + addLineItem = 'addLineItem', + changeLineItemQuantity = 'changeLineItemQuantity', removeDiscountCode = 'removeDiscountCode', removeLineItem = 'removeLineItem', setAnonymousId = 'setAnonymousId', @@ -39,7 +41,7 @@ export class CartModel { private root: CartApi; constructor() { - this.root = getCartApi(); + this.root = new CartApi(); this.getCart() .then(() => { const { anonymousId } = getStore().getState(); @@ -215,10 +217,12 @@ export class CartModel { private async updateCartCustomer(anonymousId: string): Promise { if (this.cart) { - const actions: CartSetAnonymousIdAction = { - action: ACTIONS.setAnonymousId, - anonymousId, - }; + const actions: CartSetAnonymousIdAction[] = [ + { + action: ACTIONS.setAnonymousId, + anonymousId, + }, + ]; const dataSetId = await this.root.setAnonymousId(this.cart, actions); this.cart = this.getCartFromData(dataSetId); } @@ -274,7 +278,19 @@ export class CartModel { } public async addProductToCart(addCartItem: AddCartItem): Promise { - const data = await this.root.addProduct(await this.getCart(), addCartItem); + if (!this.cart) { + this.cart = await this.getCart(); + } + + const actions: MyCartUpdateAction[] = [ + { + action: ACTIONS.addLineItem, + productId: addCartItem.productId, + quantity: addCartItem.quantity, + variantId: addCartItem.variantId, + }, + ]; + const data = await this.root.updateCart(this.cart, actions); this.cart = this.getCartFromData(data); this.dispatchUpdate(); return this.cart; @@ -331,11 +347,17 @@ export class CartModel { return this.cart; } - public async deleteProductFromCart(products: CartProduct): Promise { + public async deleteProductFromCart(cartItem: CartProduct): Promise { if (!this.cart) { this.cart = await this.getCart(); } - const data = await this.root.deleteProduct(this.cart, products); + const actions: MyCartUpdateAction[] = [ + { + action: ACTIONS.removeLineItem, + lineItemId: cartItem.lineItemId, + }, + ]; + const data = await this.root.updateCart(this.cart, actions); this.cart = this.getCartFromData(data); this.dispatchUpdate(); return this.cart; @@ -345,7 +367,16 @@ export class CartModel { if (!this.cart) { this.cart = await this.getCart(); } - const data = await this.root.editProductCount(this.cart, editCartItem); + + const actions: MyCartUpdateAction[] = [ + { + action: ACTIONS.changeLineItemQuantity, + lineItemId: editCartItem.lineId, + quantity: editCartItem.quantity, + }, + ]; + + const data = await this.root.updateCart(this.cart, actions); this.cart = this.getCartFromData(data); this.dispatchUpdate(); return this.cart; diff --git a/src/shared/API/cart/tests/cart.spec.ts b/src/shared/API/cart/tests/cart.spec.ts new file mode 100644 index 00000000..23c37fd6 --- /dev/null +++ b/src/shared/API/cart/tests/cart.spec.ts @@ -0,0 +1,161 @@ +import type { AddCartItem, CartProduct, EditCartItem } from '@/shared/types/cart.ts'; + +import CartApi from '../CartApi.ts'; +import getCartModel, { CartModel } from '../model/CartModel.ts'; + +/** + * @vitest-environment jsdom + */ + +const root = new CartApi(); + +describe('Checking CartApi', () => { + it('should check if root is defined', () => { + expect(root).toBeDefined(); + }); + + it('should check if CartApi is an instance of CartApi', () => { + expect(root).toBeInstanceOf(CartApi); + }); +}); + +describe('Checking CartModel', () => { + const cartModel = getCartModel(); + it('should check if root is defined', () => { + expect(cartModel).toBeDefined(); + }); + + it('should check if CartModel is an instance of CartModel', () => { + expect(cartModel).toBeInstanceOf(CartModel); + }); + + it('should get cart', async () => { + const cart = await cartModel.getCart(); + expect(cart).toBeDefined(); + expect(cart).toHaveProperty('anonymousId'); + expect(cart).toHaveProperty('discountsCart'); + expect(cart).toHaveProperty('discountsProduct'); + expect(cart).toHaveProperty('id'); + expect(cart).toHaveProperty('products'); + expect(cart).toHaveProperty('total'); + expect(cart).toHaveProperty('version'); + }); + + it('should add coupon', async () => { + const cart = await cartModel.getCart(); + expect(cart).toBeDefined(); + const cartCoupon = await cartModel.addCoupon('coupon'); + expect(cartCoupon).toBeDefined(); + expect(cartCoupon).toHaveProperty('anonymousId'); + expect(cartCoupon).toHaveProperty('discountsCart'); + expect(cartCoupon).toHaveProperty('discountsProduct'); + expect(cartCoupon).toHaveProperty('id'); + expect(cartCoupon).toHaveProperty('products'); + expect(cartCoupon).toHaveProperty('total'); + expect(cartCoupon).toHaveProperty('version'); + }); + + it('should add product to cart', async () => { + const cart = await cartModel.getCart(); + expect(cart).toBeDefined(); + const lineItem: AddCartItem = { + name: '', + productId: 'd72d63d8-b116-4948-8b4f-035ea3adbb39', + quantity: 1, + variantId: 1, + }; + const cartProduct = await cartModel.addProductToCart(lineItem); + expect(cartProduct).toBeDefined(); + expect(cartProduct).toHaveProperty('anonymousId'); + expect(cartProduct).toHaveProperty('discountsCart'); + expect(cartProduct).toHaveProperty('discountsProduct'); + expect(cartProduct).toHaveProperty('id'); + expect(cartProduct).toHaveProperty('products'); + expect(cartProduct).toHaveProperty('total'); + expect(cartProduct).toHaveProperty('version'); + }); + + it('should clear cart in model', () => { + const cart = cartModel.clear(); + expect(cart).equal(true); + }); + + it('should clear cart', async () => { + const cart = await cartModel.getCart(); + expect(cart).toBeDefined(); + const cartProduct = await cartModel.clearCart(); + expect(cartProduct).toBeDefined(); + expect(cartProduct).toHaveProperty('anonymousId'); + expect(cartProduct).toHaveProperty('discountsCart'); + expect(cartProduct).toHaveProperty('discountsProduct'); + expect(cartProduct).toHaveProperty('id'); + expect(cartProduct).toHaveProperty('products'); + expect(cartProduct).toHaveProperty('total'); + expect(cartProduct).toHaveProperty('version'); + }); + + it('should create cart', async () => { + const cartProduct = await cartModel.create(); + expect(cartProduct).toBeDefined(); + expect(cartProduct).toHaveProperty('anonymousId'); + expect(cartProduct).toHaveProperty('discountsCart'); + expect(cartProduct).toHaveProperty('discountsProduct'); + expect(cartProduct).toHaveProperty('id'); + expect(cartProduct).toHaveProperty('products'); + expect(cartProduct).toHaveProperty('total'); + expect(cartProduct).toHaveProperty('version'); + }); + + it('should delete coupon', async () => { + const cartProduct = await cartModel.deleteCoupon(''); + expect(cartProduct).toBeDefined(); + expect(cartProduct).toHaveProperty('anonymousId'); + expect(cartProduct).toHaveProperty('discountsCart'); + expect(cartProduct).toHaveProperty('discountsProduct'); + expect(cartProduct).toHaveProperty('id'); + expect(cartProduct).toHaveProperty('products'); + expect(cartProduct).toHaveProperty('total'); + expect(cartProduct).toHaveProperty('version'); + }); + + it('should delete product from cart', async () => { + const cartItem: CartProduct = { + images: '', + key: '', + lineItemId: '', + name: [], + price: 0, + priceCouponDiscount: 0, + productId: '', + quantity: 0, + size: null, + totalPrice: 0, + totalPriceCouponDiscount: 0, + }; + const cartProduct = await cartModel.deleteProductFromCart(cartItem); + expect(cartProduct).toBeDefined(); + expect(cartProduct).toHaveProperty('anonymousId'); + expect(cartProduct).toHaveProperty('discountsCart'); + expect(cartProduct).toHaveProperty('discountsProduct'); + expect(cartProduct).toHaveProperty('id'); + expect(cartProduct).toHaveProperty('products'); + expect(cartProduct).toHaveProperty('total'); + expect(cartProduct).toHaveProperty('version'); + }); + + it('should edit product count', async () => { + const editCartItem: EditCartItem = { + lineId: '', + quantity: 0, + }; + const cartProduct = await cartModel.editProductCount(editCartItem); + expect(cartProduct).toBeDefined(); + expect(cartProduct).toHaveProperty('anonymousId'); + expect(cartProduct).toHaveProperty('discountsCart'); + expect(cartProduct).toHaveProperty('discountsProduct'); + expect(cartProduct).toHaveProperty('id'); + expect(cartProduct).toHaveProperty('products'); + expect(cartProduct).toHaveProperty('total'); + expect(cartProduct).toHaveProperty('version'); + }); +}); diff --git a/src/shared/API/cart/tests/cartApi.spec.ts b/src/shared/API/cart/tests/cartApi.spec.ts deleted file mode 100644 index 54640f00..00000000 --- a/src/shared/API/cart/tests/cartApi.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import getCartApi, { CartApi } from '../CartApi.ts'; - -const root = getCartApi(); - -describe('Checking CartApi', () => { - it('should check if root is defined', () => { - expect(root).toBeDefined(); - }); - - it('should check if CartApi is an instance of CartApi', () => { - expect(root).toBeInstanceOf(CartApi); - }); -}); diff --git a/src/shared/API/customer/CustomerApi.ts b/src/shared/API/customer/CustomerApi.ts index 2c7e9959..5ac9ff19 100644 --- a/src/shared/API/customer/CustomerApi.ts +++ b/src/shared/API/customer/CustomerApi.ts @@ -26,7 +26,7 @@ const CART_MERGE_MODE = 'MergeWithExistingCustomerCart'; const CART_TYPE_ID = 'cart'; const EMAIL = 'email'; -export class CustomerApi { +export default class CustomerApi { private client: ApiClient; constructor() { @@ -118,12 +118,6 @@ export class CustomerApi { return data; } - public async deleteCustomer(ID: string, version: number): Promise> { - const data = await this.client.adminRoot().customers().withId({ ID }).delete({ queryArgs: { version } }).execute(); - await this.logoutUser(); - return data; - } - public async editCustomer(actions: MyCustomerUpdateAction[], version: number): Promise> { const data = await this.client.apiRoot().me().post({ body: { actions, version } }).execute(); return data; @@ -189,11 +183,3 @@ export class CustomerApi { return data; } } - -const createCustomerApi = (): CustomerApi => new CustomerApi(); - -const customerApi = createCustomerApi(); - -export default function getCustomerApi(): CustomerApi { - return customerApi; -} diff --git a/src/shared/API/customer/model/CustomerModel.ts b/src/shared/API/customer/model/CustomerModel.ts index bea7fe43..77ebc13f 100644 --- a/src/shared/API/customer/model/CustomerModel.ts +++ b/src/shared/API/customer/model/CustomerModel.ts @@ -17,7 +17,7 @@ import { isCustomerResponse, isCustomerSignInResultResponse, } from '../../types/validation.ts'; -import getCustomerApi, { type CustomerApi } from '../CustomerApi.ts'; +import CustomerApi from '../CustomerApi.ts'; const CUSTOMER_FIELD = 'customer'; @@ -43,7 +43,7 @@ export class CustomerModel { private root: CustomerApi; constructor() { - this.root = getCustomerApi(); + this.root = new CustomerApi(); } public static actionAddAddress(address: Address): MyCustomerUpdateAction { @@ -233,11 +233,6 @@ export class CustomerModel { return this.getCustomerFromData(data); } - public async deleteCustomer(customer: User): Promise { - const data = await this.root.deleteCustomer(customer.id, customer.version); - return this.getCustomerFromData(data) !== null; - } - public async editCustomer(actions: MyCustomerUpdateAction[], customer: User): Promise { const data = await this.root.editCustomer(actions, customer.version); return this.getCustomerFromData(data); diff --git a/src/shared/API/customer/tests/Customer.spec.ts b/src/shared/API/customer/tests/Customer.spec.ts index e051c3b3..29b4077c 100644 --- a/src/shared/API/customer/tests/Customer.spec.ts +++ b/src/shared/API/customer/tests/Customer.spec.ts @@ -2,6 +2,10 @@ import type { Address, User } from '@/shared/types/user.ts'; import getCustomerModel, { CustomerModel } from '../model/CustomerModel.ts'; +/** + * @vitest-environment jsdom + */ + const customerModel = getCustomerModel(); describe('Checking Customer Model', () => { @@ -25,7 +29,7 @@ describe('Checking Customer Model', () => { id: '1', lastName: 'Smith', locale: 'en', - password: 'Qqq11', + password: 'Qqq11qq11', shippingAddress: [], version: 0, }; @@ -53,13 +57,8 @@ describe('Checking Customer Model', () => { }); it('should return true for valid email', async () => { - const result = await customerModel.hasEmail('jane@doe.com'); - expect(result?.email).toBe('jane@doe.com'); - }); - - it('should return false for invalid email', async () => { - const result = await customerModel.hasEmail('gettingstarted@example.com'); - expect(result).toBe(null); + const result = await customerModel.hasEmail(user.email); + expect(result?.email).toBe(user.email); }); it('should register a new customer', async () => { @@ -159,11 +158,4 @@ describe('Checking Customer Model', () => { expect(editPassword).toHaveProperty('version'); } }); - - it('should delete the customer', async () => { - if (editPassword) { - const deleteCustomer = await customerModel.deleteCustomer(editPassword); - expect(deleteCustomer).toBe(true); - } - }); }); diff --git a/src/shared/API/customer/tests/CustomerApi.spec.ts b/src/shared/API/customer/tests/CustomerApi.spec.ts index caa9c9ef..94d05b17 100644 --- a/src/shared/API/customer/tests/CustomerApi.spec.ts +++ b/src/shared/API/customer/tests/CustomerApi.spec.ts @@ -1,6 +1,10 @@ -import getCustomerApi, { CustomerApi } from '../CustomerApi.ts'; +import CustomerApi from '../CustomerApi.ts'; -const root = getCustomerApi(); +/** + * @vitest-environment jsdom + */ + +const root = new CustomerApi(); describe('Checking CustomerApi', () => { it('should check if root is defined', () => { diff --git a/src/shared/API/discount/DiscountApi.ts b/src/shared/API/discount/DiscountApi.ts index 04d9075c..6a310b1c 100644 --- a/src/shared/API/discount/DiscountApi.ts +++ b/src/shared/API/discount/DiscountApi.ts @@ -6,7 +6,7 @@ import type { import getApiClient, { type ApiClient } from '../sdk/client.ts'; -export class DiscountApi { +export default class DiscountApi { private client: ApiClient; constructor() { @@ -23,11 +23,3 @@ export class DiscountApi { return data; } } - -const createDiscountApi = (): DiscountApi => new DiscountApi(); - -const discountApi = createDiscountApi(); - -export default function getDiscountApi(): DiscountApi { - return discountApi; -} diff --git a/src/shared/API/discount/model/DiscountModel.ts b/src/shared/API/discount/model/DiscountModel.ts index 4034eb76..98daf32d 100644 --- a/src/shared/API/discount/model/DiscountModel.ts +++ b/src/shared/API/discount/model/DiscountModel.ts @@ -15,7 +15,7 @@ import { isDiscountCodePagedQueryResponse, isProductDiscountPagedQueryResponse, } from '../../types/validation.ts'; -import getDiscountApi, { type DiscountApi } from '../DiscountApi.ts'; +import DiscountApi from '../DiscountApi.ts'; export class DiscountModel { private coupons: Coupon[] = []; @@ -23,7 +23,7 @@ export class DiscountModel { private root: DiscountApi; constructor() { - this.root = getDiscountApi(); + this.root = new DiscountApi(); this.init().catch(showErrorMessage); } diff --git a/src/shared/API/product/ProductApi.ts b/src/shared/API/product/ProductApi.ts index c147a546..e1ff50c2 100644 --- a/src/shared/API/product/ProductApi.ts +++ b/src/shared/API/product/ProductApi.ts @@ -22,7 +22,7 @@ enum Facets { enum QueryParams { range = 'range', } -export class ProductApi { +export default class ProductApi { private client: ApiClient; constructor() { @@ -95,11 +95,3 @@ export class ProductApi { return data; } } - -const createProductApi = (): ProductApi => new ProductApi(); - -const productApi = createProductApi(); - -export default function getProductApi(): ProductApi { - return productApi; -} diff --git a/src/shared/API/product/model/ProductModel.ts b/src/shared/API/product/model/ProductModel.ts index 1fa89e28..3c61de4b 100644 --- a/src/shared/API/product/model/ProductModel.ts +++ b/src/shared/API/product/model/ProductModel.ts @@ -14,8 +14,6 @@ import type { import { PRICE_FRACTIONS } from '@/shared/constants/product.ts'; import { getLevel, getSize } from '@/shared/utils/size.ts'; -import type { ProductApi } from '../ProductApi.ts'; - import { Attribute, type CategoriesProductCount, @@ -40,7 +38,7 @@ import { isRangeFacetResult, isTermFacetResult, } from '../../types/validation.ts'; -import getProductApi from '../ProductApi.ts'; +import ProductApi from '../ProductApi.ts'; enum ProductConstant { categoriesId = 'categories.id', @@ -60,7 +58,7 @@ export class ProductModel { private root: ProductApi; constructor() { - this.root = getProductApi(); + this.root = new ProductApi(); } private adaptCategoryPagedQueryToClient(data: CategoryPagedQueryResponse): Category[] { diff --git a/src/shared/API/product/tests/Product.spec.ts b/src/shared/API/product/tests/Product.spec.ts index c1a54a83..49634190 100644 --- a/src/shared/API/product/tests/Product.spec.ts +++ b/src/shared/API/product/tests/Product.spec.ts @@ -2,6 +2,10 @@ import { type localization } from '@/shared/types/product.ts'; import getProductModel, { ProductModel } from '../model/ProductModel.ts'; +/** + * @vitest-environment jsdom + */ + const productModel = getProductModel(); describe('Checking Product Model', () => { diff --git a/src/shared/API/product/tests/ProductApi.spec.ts b/src/shared/API/product/tests/ProductApi.spec.ts index fda6af90..fbc31dd7 100644 --- a/src/shared/API/product/tests/ProductApi.spec.ts +++ b/src/shared/API/product/tests/ProductApi.spec.ts @@ -1,6 +1,10 @@ -import getProductApi, { ProductApi } from '../ProductApi.ts'; +import ProductApi from '../ProductApi.ts'; -const root = getProductApi(); +/** + * @vitest-environment jsdom + */ + +const root = new ProductApi(); describe('Checking ProductApi', () => { it('should check if root is defined', () => { diff --git a/src/shared/API/product/tests/filter.spec.ts b/src/shared/API/product/tests/filter.spec.ts new file mode 100644 index 00000000..a474bad6 --- /dev/null +++ b/src/shared/API/product/tests/filter.spec.ts @@ -0,0 +1,69 @@ +import type { PriceRange } from '../../types/type.ts'; + +import { FilterFields } from '../../types/type.ts'; +import FilterProduct from '../utils/filter.ts'; + +/** + * @vitest-environment jsdom + */ + +describe('Checking filter', () => { + const filter = new FilterProduct(); + it('should check if FilterProduct is defined', () => { + expect(filter).toBeDefined(); + }); + + it('should check if filter is an instance of FilterProduct', () => { + expect(filter).toBeInstanceOf(FilterProduct); + }); + + it('should return category filter', () => { + const filter = new FilterProduct(); + filter.addFilter(FilterFields.CATEGORY, 'id'); + const result = filter.getFilter(); + expect(result).toEqual(['categories.id:subtree("id")', 'variants.price.centAmount: range(0 to *)']); + }); + + it('should return product filter', () => { + const filter = new FilterProduct(); + filter.addFilter(FilterFields.ID, 'id'); + const result = filter.getFilter(); + expect(result).toEqual(['id:"id"', 'variants.price.centAmount: range(0 to *)']); + }); + + it('should return new_arrival filter', () => { + const filter = new FilterProduct(); + filter.addFilter(FilterFields.NEW_ARRIVAL, 'true'); + const result = filter.getFilter(); + expect(result).toEqual(['variants.attributes.new_arrival:true', 'variants.price.centAmount: range(0 to *)']); + }); + + it('should return price filter', () => { + const filter = new FilterProduct(); + const priceMinMax: PriceRange = { + max: 160, + min: 0, + }; + const price: PriceRange = { + max: 10, + min: 5, + }; + filter.addFilter(FilterFields.PRICE, price); + const result = filter.getFilter(priceMinMax); + expect(result).toEqual(['variants.price.centAmount: range(500 to 1000)']); + }); + + it('should return sale filter', () => { + const filter = new FilterProduct(); + filter.addFilter(FilterFields.SALE, 'true'); + const result = filter.getFilter(); + expect(result).toEqual(['variants.price.centAmount: range(0 to *)', 'variants.prices.discounted:exists']); + }); + + it('should return sale filter', () => { + const filter = new FilterProduct(); + filter.addFilter(FilterFields.SIZE, 'M'); + const result = filter.getFilter(); + expect(result).toEqual(['variants.attributes.size.key:"M"', 'variants.price.centAmount: range(0 to *)']); + }); +}); diff --git a/src/shared/API/sdk/client.ts b/src/shared/API/sdk/client.ts index 6b6edbc1..bc4c7f0a 100644 --- a/src/shared/API/sdk/client.ts +++ b/src/shared/API/sdk/client.ts @@ -28,13 +28,10 @@ const URL_HTTP = 'https://api.europe-west1.gcp.commercetools.com'; const USE_SAVE_TOKEN = true; const httpMiddlewareOptions: HttpMiddlewareOptions = { - fetch, host: URL_HTTP, }; export class ApiClient { - private adminConnection: ByProjectKeyRequestBuilder; - private anonymConnection: ByProjectKeyRequestBuilder | null = null; private authConnection: ByProjectKeyRequestBuilder | null = null; @@ -61,7 +58,6 @@ export class ApiClient { } else { this.anonymConnection = this.createAnonymConnection(); } - this.adminConnection = this.createAdminConnection(); } private addAuthMiddleware( @@ -97,16 +93,6 @@ export class ApiClient { } } - private createAdminConnection(): ByProjectKeyRequestBuilder { - const defaultOptions = this.getDefaultOptions(); - const client = this.getDefaultClient(); - - client.withClientCredentialsFlow(defaultOptions); - - this.adminConnection = this.getConnection(client.build()); - return this.adminConnection; - } - private createAnonymConnection(): ByProjectKeyRequestBuilder { const defaultOptions = this.getDefaultOptions(TokenType.ANONYM); const client = this.getDefaultClient(); @@ -157,7 +143,6 @@ export class ApiClient { clientId: this.clientID, clientSecret: this.clientSecret, }, - fetch, host: URL_AUTH, projectKey: this.projectKey, scopes: this.scopes, @@ -165,10 +150,6 @@ export class ApiClient { }; } - public adminRoot(): ByProjectKeyRequestBuilder { - return this.adminConnection; - } - public apiRoot(): ByProjectKeyRequestBuilder { let client = (this.authConnection && this.isAuth) || (this.authConnection && !this.anonymConnection) diff --git a/src/shared/API/shopping-list/ShoppingListApi.ts b/src/shared/API/shopping-list/ShoppingListApi.ts index ab110cdc..55fe17e4 100644 --- a/src/shared/API/shopping-list/ShoppingListApi.ts +++ b/src/shared/API/shopping-list/ShoppingListApi.ts @@ -15,7 +15,7 @@ import type { OptionsRequest } from '../types/type.ts'; import makeSortRequest from '../product/utils/sort.ts'; import getApiClient, { type ApiClient } from '../sdk/client.ts'; -export class ShoppingListApi { +export default class ShoppingListApi { private client: ApiClient; constructor() { @@ -151,11 +151,3 @@ export class ShoppingListApi { return data; } } - -const createShoppingListApi = (): ShoppingListApi => new ShoppingListApi(); - -const shoppingListApi = createShoppingListApi(); - -export default function getShoppingListApi(): ShoppingListApi { - return shoppingListApi; -} diff --git a/src/shared/API/shopping-list/model/ShoppingListModel.ts b/src/shared/API/shopping-list/model/ShoppingListModel.ts index 831f0ea5..39f35c40 100644 --- a/src/shared/API/shopping-list/model/ShoppingListModel.ts +++ b/src/shared/API/shopping-list/model/ShoppingListModel.ts @@ -20,7 +20,7 @@ import { isShoppingList, isShoppingListPagedQueryResponse, } from '../../types/validation.ts'; -import getShoppingListApi, { type ShoppingListApi } from '../ShoppingListApi.ts'; +import ShoppingListApi from '../ShoppingListApi.ts'; enum ACTIONS { addLineItem = 'addLineItem', @@ -37,7 +37,7 @@ export class ShoppingListModel { private subscribers: ShoppingListChangeHandler[] = []; constructor() { - this.root = getShoppingListApi(); + this.root = new ShoppingListApi(); this.getShoppingList() .then(() => { const { anonymousId } = getStore().getState(); diff --git a/src/shared/API/shopping-list/test/shoppingList.spec.ts b/src/shared/API/shopping-list/test/shoppingList.spec.ts new file mode 100644 index 00000000..e6d7abf4 --- /dev/null +++ b/src/shared/API/shopping-list/test/shoppingList.spec.ts @@ -0,0 +1,69 @@ +import type { ShoppingListProduct } from '@/shared/types/shopping-list.ts'; + +import ShoppingListApi from '../ShoppingListApi.ts'; +import getShoppingListModel, { ShoppingListModel } from '../model/ShoppingListModel.ts'; + +/** + * @vitest-environment jsdom + */ + +const root = new ShoppingListApi(); + +describe('Checking ShoppingListApi', () => { + it('should check if root is defined', () => { + expect(root).toBeDefined(); + }); + + it('should check if root is an instance of ShoppingListApi', () => { + expect(root).toBeInstanceOf(ShoppingListApi); + }); +}); + +describe('Checking ShoppingListModel', () => { + const shopModel = getShoppingListModel(); + it('should check if shopModel is defined', () => { + expect(shopModel).toBeDefined(); + }); + + it('should check if shopModel is an instance of ShoppingListModel', () => { + expect(shopModel).toBeInstanceOf(ShoppingListModel); + }); + + it('should get ShopList', async () => { + const shop = await shopModel.getShoppingList(); + expect(shop).toBeDefined(); + expect(shop).toHaveProperty('anonymousId'); + expect(shop).toHaveProperty('id'); + expect(shop).toHaveProperty('products'); + expect(shop).toHaveProperty('version'); + }); + + it('should add product to ShopList', async () => { + const shop = await shopModel.getShoppingList(); + expect(shop).toBeDefined(); + const shopProduct = await shopModel.addProduct(''); + expect(shopProduct).toBeDefined(); + expect(shopProduct).toHaveProperty('anonymousId'); + expect(shopProduct).toHaveProperty('id'); + expect(shopProduct).toHaveProperty('products'); + expect(shopProduct).toHaveProperty('version'); + }); + + it('should clear shopModel in model', () => { + const shop = shopModel.clear(); + expect(shop).equal(true); + }); + + it('should delete product from shopModel', async () => { + const product: ShoppingListProduct = { + lineItemId: '', + productId: '', + }; + const shopProduct = await shopModel.deleteProduct(product); + expect(shopProduct).toBeDefined(); + expect(shopProduct).toHaveProperty('anonymousId'); + expect(shopProduct).toHaveProperty('id'); + expect(shopProduct).toHaveProperty('products'); + expect(shopProduct).toHaveProperty('version'); + }); +}); diff --git a/src/shared/API/types/validation.ts b/src/shared/API/types/validation.ts index a334eb98..2be23a7b 100644 --- a/src/shared/API/types/validation.ts +++ b/src/shared/API/types/validation.ts @@ -12,7 +12,6 @@ import type { FacetRange, FacetTerm, LocalizedString, - Product, ProductDiscountPagedQueryResponse, ProductPagedQueryResponse, ProductProjection, @@ -22,7 +21,6 @@ import type { ShoppingListPagedQueryResponse, TermFacetResult, } from '@commercetools/platform-sdk'; -import type { TokenStore } from '@commercetools/sdk-client-v2'; export function isClientResponse(data: unknown): data is ClientResponse { return Boolean( @@ -80,21 +78,6 @@ export function isCategoryPagedQueryResponse(data: unknown): data is CategoryPag ); } -export function isProductPagedQueryResponse(data: unknown): data is ProductPagedQueryResponse { - return Boolean( - typeof data === 'object' && - data && - 'count' in data && - typeof data.count === 'number' && - 'limit' in data && - typeof data.limit === 'number' && - 'total' in data && - typeof data.total === 'number' && - 'results' in data && - typeof Array.isArray(data.results), - ); -} - export function isLocalizationObj(data: unknown): data is LocalizedString { return Boolean(typeof data === 'object' && data && Object.keys(data).every((key) => typeof key === 'string')); } @@ -110,27 +93,6 @@ export function isAttributePlainEnumValue(data: unknown): data is AttributePlain ); } -export function isProductResponse(data: unknown): data is Product { - return Boolean( - typeof data === 'object' && - data && - 'id' in data && - typeof data.id === 'string' && - 'key' in data && - typeof data.id === 'string' && - 'masterData' in data && - typeof data.masterData === 'object' && - data.masterData !== null && - 'staged' in data.masterData && - typeof data.masterData.staged === 'object' && - data.masterData.staged !== null && - 'categories' in data.masterData.staged && - 'description' in data.masterData.staged && - 'name' in data.masterData.staged && - 'variants' in data.masterData.staged, - ); -} - export function isProductProjectionPagedSearchResponse(data: unknown): data is ProductPagedQueryResponse { return Boolean(typeof data === 'object' && data && 'facets' in data && typeof data.facets === 'object'); } @@ -161,25 +123,6 @@ export function isProductProjection(data: unknown): data is ProductProjection { ); } -export function isProductProjectionPagedQueryResponseWithFacet( - data: unknown, -): data is ProductProjectionPagedQueryResponse { - return Boolean( - typeof data === 'object' && - data && - 'count' in data && - typeof data.count === 'number' && - 'limit' in data && - typeof data.limit === 'number' && - 'total' in data && - typeof data.total === 'number' && - 'facets' in data && - typeof data.facets === 'object' && - 'results' in data && - Array.isArray(data.results), - ); -} - export function isRangeFacetResult(data: unknown): data is RangeFacetResult { return Boolean( typeof data === 'object' && data && 'ranges' in data && Array.isArray(data.ranges) && data.ranges.length, @@ -232,19 +175,6 @@ export function isErrorResponse(data: unknown): data is ErrorResponse { ); } -export function isTokenType(data: unknown): data is TokenStore { - return Boolean( - typeof data === 'object' && - data && - 'expirationTime' in data && - typeof data.expirationTime === 'string' && - 'refreshToken' in data && - typeof data.refreshToken === 'string' && - 'token' in data && - typeof data.token === 'string', - ); -} - export function isCart(data: unknown): data is Cart { return Boolean( typeof data === 'object' && diff --git a/src/shared/Button/tests/Button.spec.ts b/src/shared/Button/tests/Button.spec.ts index dd47b6cc..391ae399 100644 --- a/src/shared/Button/tests/Button.spec.ts +++ b/src/shared/Button/tests/Button.spec.ts @@ -1,5 +1,9 @@ import ButtonModel from '../model/ButtonModel.ts'; +/** + * @vitest-environment jsdom + */ + const button = new ButtonModel({ action: { key: 'click', diff --git a/src/shared/Input/tests/Input.spec.ts b/src/shared/Input/tests/Input.spec.ts index 5928e9bd..19924ef5 100644 --- a/src/shared/Input/tests/Input.spec.ts +++ b/src/shared/Input/tests/Input.spec.ts @@ -2,6 +2,10 @@ import type { InputParams } from '@/shared/types/form.ts'; import InputModel from '../model/InputModel.ts'; +/** + * @vitest-environment jsdom + */ + const params: InputParams = { autocomplete: 'on', id: 'password', diff --git a/src/shared/Posts/test/posts.spec.ts b/src/shared/Posts/test/posts.spec.ts new file mode 100644 index 00000000..a074472f --- /dev/null +++ b/src/shared/Posts/test/posts.spec.ts @@ -0,0 +1,16 @@ +import postsData from '../posts.ts'; + +/** + * @vitest-environment jsdom + */ + +describe('Checking posts', () => { + const posts = postsData; + it('should check if post is defined', () => { + expect(posts).toBeDefined(); + }); + + it('should check if posts is an instance of CartPageModel', () => { + expect(Array.isArray(posts)).toBe(true); + }); +}); diff --git a/src/shared/ServerMessage/test/ServerMessage.spec.ts b/src/shared/ServerMessage/test/ServerMessage.spec.ts index 3406f2f5..797b5003 100644 --- a/src/shared/ServerMessage/test/ServerMessage.spec.ts +++ b/src/shared/ServerMessage/test/ServerMessage.spec.ts @@ -1,6 +1,10 @@ import serverMessageModel from '../model/ServerMessageModel.ts'; import ServerMessageView from '../view/ServerMessageView.ts'; +/** + * @vitest-environment jsdom + */ + describe('check serverMessageModel', () => { it('serverMessageModel instance should be defined', () => { expect(serverMessageModel).toBeDefined(); diff --git a/src/shared/Store/test.spec.ts b/src/shared/Store/test.spec.ts index 0945a75e..deee3955 100644 --- a/src/shared/Store/test.spec.ts +++ b/src/shared/Store/test.spec.ts @@ -6,6 +6,10 @@ import * as actions from './actions.ts'; import * as observer from './observer.ts'; import { rootReducer } from './reducer.ts'; +/** + * @vitest-environment jsdom + */ + describe('Checking Store', () => { const mockStore = getStore(); it('should check if store is defined', () => { diff --git a/src/shared/constants/svg.ts b/src/shared/constants/svg.ts index 3b921243..bee6ef48 100644 --- a/src/shared/constants/svg.ts +++ b/src/shared/constants/svg.ts @@ -19,6 +19,7 @@ const SVG_DETAIL = { NOT_FOUND: 'notFound', OPEN_EYE: 'openEye', PROFILE: 'userCircle', + RSS_LOGO: 'rssLogo', STAR: 'star', SVG_URL: 'http://www.w3.org/2000/svg', SWITCH_LANGUAGE: { diff --git a/src/shared/img/svg/rssLogo.svg b/src/shared/img/svg/rssLogo.svg new file mode 100644 index 00000000..618095e2 --- /dev/null +++ b/src/shared/img/svg/rssLogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/shared/services/localStorage.spec.ts b/src/shared/services/localStorage.spec.ts index deb0083e..ed36a224 100644 --- a/src/shared/services/localStorage.spec.ts +++ b/src/shared/services/localStorage.spec.ts @@ -1,5 +1,9 @@ import { clearLocalStorage, saveCurrentStateToLocalStorage } from './localStorage.ts'; +/** + * @vitest-environment jsdom + */ + describe('Checking Local Storage', () => { it('{} should return {}', () => { expect(saveCurrentStateToLocalStorage({})).toStrictEqual({}); diff --git a/src/shared/utils/tests.spec.ts b/src/shared/utils/tests.spec.ts index 1f74c3a6..4b4c57b3 100644 --- a/src/shared/utils/tests.spec.ts +++ b/src/shared/utils/tests.spec.ts @@ -7,6 +7,10 @@ import formattedText from './formattedText.ts'; import getCountryIndex from './getCountryIndex.ts'; import observeCurrentLanguage from './observeCurrentLanguage.ts'; +/** + * @vitest-environment jsdom + */ + describe('Checking formattedText function', () => { it('should return formatted text for basic functionality', () => { expect(formattedText('test')).toBe('Test'); diff --git a/src/test/mocks/handler.ts b/src/test/mocks/handler.ts new file mode 100644 index 00000000..5eec6968 --- /dev/null +++ b/src/test/mocks/handler.ts @@ -0,0 +1,12 @@ +import cart from './handlers/cart.ts'; +import category from './handlers/category.ts'; +import customer from './handlers/customer.ts'; +import discount from './handlers/discount.ts'; +import product from './handlers/product.ts'; +import project from './handlers/project.ts'; +import shoppingList from './handlers/shopping-list.ts'; +import token from './handlers/token.ts'; + +const handlers = [...token, ...category, ...product, ...cart, ...customer, ...discount, ...shoppingList, ...project]; + +export default handlers; diff --git a/src/test/mocks/handlers/cart.ts b/src/test/mocks/handlers/cart.ts new file mode 100644 index 00000000..d53a0a6b --- /dev/null +++ b/src/test/mocks/handlers/cart.ts @@ -0,0 +1,257 @@ +import { HttpResponse, http } from 'msw'; + +const carts = { + anonymousId: 'f6b6183e-cc73-4b19-a65e-f408fc785e89', + cartState: 'Merged', + createdAt: '2024-06-14T15:51:18.227Z', + createdBy: { + anonymousId: 'c3eeabe1-a0fb-49a6-ab10-f3bf19d933c8', + clientId: 'BPJMQ0wyC-4dA_1sd04SlJQx', + isPlatformClient: false, + }, + customLineItems: [], + customerId: 'a9a1b24b-a467-4850-9a1c-c4030dff9949', + deleteDaysAfterLastModification: 2, + directDiscounts: [], + discountCodes: [], + id: '311ee12e-a1a3-4ec2-b69e-0e6f03fcba36', + inventoryMode: 'None', + itemShippingAddresses: [], + lastMessageSequenceNumber: 1, + lastModifiedAt: '2024-06-16T08:44:18.913Z', + lastModifiedBy: { + anonymousId: 'f6b6183e-cc73-4b19-a65e-f408fc785e89', + clientId: 'BPJMQ0wyC-4dA_1sd04SlJQx', + isPlatformClient: false, + }, + lineItems: [ + { + addedAt: '2024-06-14T15:51:41.583Z', + discountedPricePerQuantity: [], + id: '0d56624b-946b-4ac0-b6a7-178f06a3472a', + lastModifiedAt: '2024-06-14T15:51:41.583Z', + lineItemMode: 'Standard', + name: { + en: 'Zamioculcas', + ru: 'Замиокулькас', + }, + perMethodTaxRate: [], + price: { + id: '48eacec5-a0bc-4302-92da-654f37b9d0d3', + key: '79357868957074_1', + value: { + centAmount: 1500, + currencyCode: 'USD', + fractionDigits: 2, + type: 'centPrecision', + }, + }, + priceMode: 'Platform', + productId: '8247d798-ca84-4b90-ae1c-d7e5490f58b7', + productKey: '79357868957074', + productType: { + id: '986435e1-45b9-4f14-bcee-9ae3921e5d3c', + typeId: 'product-type', + version: 30, + }, + quantity: 1, + state: [ + { + quantity: 1, + state: { + id: '8baf5a56-683c-4e5f-a411-0ce8e62cc86e', + typeId: 'state', + }, + }, + ], + taxedPricePortions: [], + totalPrice: { + centAmount: 1500, + currencyCode: 'USD', + fractionDigits: 2, + type: 'centPrecision', + }, + variant: { + assets: [ + { + id: 'e629929b-9c3f-4141-8e6e-77608a84d138', + key: 'img-1', + name: { + en: 'img-1', + }, + sources: [ + { + uri: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-1.png', + }, + ], + tags: [], + }, + { + id: '18260724-dc23-46c7-8af1-2749b474a7ef', + key: 'img-2', + name: { + en: 'img-2', + }, + sources: [ + { + uri: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-2.png', + }, + ], + tags: [], + }, + { + id: '0487e650-c792-45d8-aff7-e1dff5e23254', + key: 'img-3', + name: { + en: 'img-3', + }, + sources: [ + { + uri: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-3.png', + }, + ], + tags: [], + }, + { + id: 'da50912b-eaed-4738-a432-65dfadd964b3', + key: 'img-4', + name: { + en: 'img-4', + }, + sources: [ + { + uri: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-4.png', + }, + ], + tags: [], + }, + ], + attributes: [ + { + name: 'new_arrival', + value: [false], + }, + { + name: 'size', + value: [ + { + key: 'S', + label: 'S', + }, + ], + }, + { + name: 'full_description', + value: { + en: 'Zamiokulkas needs plenty of sunlight, suitable temperature conditions are in the range of 18 to 25 degrees Celsius, in winter 16-18. Water abundantly, but not often, about once a week. Daily spraying will be useful. In spring and summer, feed once a month, using fertilizers for cacti and succulents.', + ru: 'Замиокулькас нуждается в большом количестве солнечного света, подходящие температурные условия находятся в диапазоне от 18 до 25 градусов тепла, зимой 16-18. Поливают обильно, но не часто, примерно раз в неделю. Будут полезны ежедневные опрыскивания. Весной и летом подкармливают раз в месяц, используя удобрения для кактусов и суккулентов.', + }, + }, + { + name: 'level', + value: [ + { + key: '1', + label: '1', + }, + ], + }, + ], + id: 1, + images: [ + { + dimensions: { + h: 500, + w: 500, + }, + label: 'img-1', + url: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-1.png', + }, + { + dimensions: { + h: 500, + w: 500, + }, + label: 'img-2', + url: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-2.png', + }, + { + dimensions: { + h: 500, + w: 500, + }, + label: 'img-3', + url: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-3.png', + }, + { + dimensions: { + h: 500, + w: 500, + }, + label: 'img-4', + url: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-4.png', + }, + ], + key: '79357868957074_1', + prices: [ + { + id: '48eacec5-a0bc-4302-92da-654f37b9d0d3', + key: '79357868957074_1', + value: { + centAmount: 1500, + currencyCode: 'USD', + fractionDigits: 2, + type: 'centPrecision', + }, + }, + ], + sku: '79357868957074_1', + }, + }, + ], + origin: 'Customer', + refusedGifts: [], + shipping: [], + shippingMode: 'Single', + taxCalculationMode: 'LineItemLevel', + taxMode: 'Platform', + taxRoundingMode: 'HalfEven', + totalLineItemQuantity: 1, + totalPrice: { + centAmount: 1500, + currencyCode: 'USD', + fractionDigits: 2, + type: 'centPrecision', + }, + type: 'Cart', + version: 120, + versionModifiedAt: '2024-06-16T08:44:18.913Z', +}; + +const cartList = { + count: 1, + limit: 20, + offset: 0, + results: [carts], + total: 1, +}; + +const handlers = [ + http.get(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/carts`, () => + HttpResponse.json(cartList), + ), + http.post(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/carts`, () => + HttpResponse.json(cartList), + ), + http.post(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/carts/*`, () => + HttpResponse.json(carts), + ), + http.get(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/carts/*`, () => + HttpResponse.json(carts), + ), + http.post(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/carts/*`, () => + HttpResponse.json(carts), + ), +]; + +export default handlers; diff --git a/src/test/mocks/handlers/category.ts b/src/test/mocks/handlers/category.ts new file mode 100644 index 00000000..72031c81 --- /dev/null +++ b/src/test/mocks/handlers/category.ts @@ -0,0 +1,64 @@ +import { HttpResponse, http } from 'msw'; + +const categoryList = { + count: 1, + limit: 20, + offset: 0, + results: [ + { + ancestors: [ + { + id: 'cbc4591a-cdd8-4f93-840b-5cb40e47ada8', + typeId: 'category', + }, + ], + assets: [], + createdAt: '2024-04-27T13:33:02.558Z', + createdBy: { + isPlatformClient: true, + }, + description: { + en: 'Seeds', + ru: 'Семена', + }, + externalId: 'seeds', + id: '8e12968e-dc4e-4e19-a8cd-994fe1acf3e6', + key: 'seeds', + lastMessageSequenceNumber: 3, + lastModifiedAt: '2024-06-08T08:07:45.907Z', + lastModifiedBy: { + isPlatformClient: true, + }, + metaDescription: { + en: 'Seeds', + ru: 'Семена', + }, + metaTitle: { + en: 'seeds', + }, + name: { + en: 'Seeds', + ru: 'Семена', + }, + orderHint: '0.31', + parent: { + id: 'cbc4591a-cdd8-4f93-840b-5cb40e47ada8', + typeId: 'category', + }, + slug: { + en: 'seeds', + ru: 'seeds', + }, + version: 15, + versionModifiedAt: '2024-06-08T08:07:45.907Z', + }, + ], + total: 1, +}; +const handlers = [ + http.get(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/categories`, () => + HttpResponse.json(categoryList), + ), +]; + +export default handlers; diff --git a/src/test/mocks/handlers/customer.ts b/src/test/mocks/handlers/customer.ts new file mode 100644 index 00000000..555df968 --- /dev/null +++ b/src/test/mocks/handlers/customer.ts @@ -0,0 +1,120 @@ +import { HttpResponse, http } from 'msw'; + +const me = { + addresses: [ + { + city: 'Anytown', + country: 'DK', + email: 'test-test@example.com', + firstName: 'Jane', + id: 'sbSHz6YF', + lastName: 'Smith', + postalCode: '1234', + state: '', + streetName: 'Main Street 123', + streetNumber: '', + }, + ], + authenticationMode: 'Password', + billingAddressIds: ['sbSHz6YF'], + createdAt: '2024-06-16T08:57:27.551Z', + createdBy: { + anonymousId: '533d1e77-4d6f-4567-a6b0-6a332a11892f', + clientId: 'BPJMQ0wyC-4dA_1sd04SlJQx', + isPlatformClient: false, + }, + dateOfBirth: '1990-01-01', + defaultBillingAddressId: 'sbSHz6YF', + defaultShippingAddressId: 'sbSHz6YF', + email: 'test-test@example.com', + firstName: 'Jane', + id: '9b59ebfc-76b3-4904-af39-e0ee2f2a7d7a', + isEmailVerified: false, + lastMessageSequenceNumber: 1, + lastModifiedAt: '2024-06-16T08:57:27.551Z', + lastModifiedBy: { + anonymousId: '533d1e77-4d6f-4567-a6b0-6a332a11892f', + clientId: 'BPJMQ0wyC-4dA_1sd04SlJQx', + isPlatformClient: false, + }, + lastName: 'Smith', + locale: 'en', + password: '****HOM=', + shippingAddressIds: ['sbSHz6YF'], + stores: [], + version: 1, + versionModifiedAt: '2024-06-16T08:57:27.551Z', +}; +const customer = { + cart: { + anonymousId: '533d1e77-4d6f-4567-a6b0-6a332a11892f', + cartState: 'Active', + createdAt: '2024-06-16T08:54:22.342Z', + createdBy: { + anonymousId: '533d1e77-4d6f-4567-a6b0-6a332a11892f', + clientId: 'BPJMQ0wyC-4dA_1sd04SlJQx', + isPlatformClient: false, + }, + customLineItems: [], + customerId: '9b59ebfc-76b3-4904-af39-e0ee2f2a7d7a', + deleteDaysAfterLastModification: 2, + directDiscounts: [], + discountCodes: [], + id: 'a97100b4-e2b8-4e6d-96b0-efa8973df56f', + inventoryMode: 'None', + itemShippingAddresses: [], + lastMessageSequenceNumber: 1, + lastModifiedAt: '2024-06-16T08:54:22.342Z', + lastModifiedBy: { + anonymousId: '533d1e77-4d6f-4567-a6b0-6a332a11892f', + clientId: 'BPJMQ0wyC-4dA_1sd04SlJQx', + isPlatformClient: false, + }, + lineItems: [], + origin: 'Customer', + refusedGifts: [], + shipping: [], + shippingMode: 'Single', + taxCalculationMode: 'LineItemLevel', + taxMode: 'Platform', + taxRoundingMode: 'HalfEven', + totalPrice: { + centAmount: 0, + currencyCode: 'USD', + fractionDigits: 2, + type: 'centPrecision', + }, + type: 'Cart', + version: 2, + versionModifiedAt: '2024-06-16T08:57:27.571Z', + }, + customer: me, +}; + +const meList = { + count: 1, + limit: 20, + offset: 0, + results: [me], + total: 1, +}; + +const handlers = [ + http.post(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/signup`, () => + HttpResponse.json(customer), + ), + http.post(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/login`, () => + HttpResponse.json(customer), + ), + http.get(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/customers`, () => + HttpResponse.json(meList), + ), + http.post(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me`, () => + HttpResponse.json(me), + ), + http.post(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/password`, () => + HttpResponse.json(me), + ), +]; + +export default handlers; diff --git a/src/test/mocks/handlers/discount.ts b/src/test/mocks/handlers/discount.ts new file mode 100644 index 00000000..c7fa61f8 --- /dev/null +++ b/src/test/mocks/handlers/discount.ts @@ -0,0 +1,57 @@ +import { HttpResponse, http } from 'msw'; + +const discountList = { + count: 4, + limit: 20, + offset: 0, + results: [ + { + cartDiscounts: [ + { + id: 'ff195f35-c6f6-4959-a69b-2cd626e6ce60', + typeId: 'cart-discount', + }, + ], + code: 'first-key-1', + createdAt: '2024-04-29T07:55:04.369Z', + createdBy: { + isPlatformClient: true, + user: { + id: '9cd5771e-eb9b-4e70-8f16-a91c52216948', + typeId: 'user', + }, + }, + description: { + en: 'first', + }, + groups: [], + id: 'fcff07da-a136-4cbd-bedb-a089c0818089', + isActive: true, + lastMessageSequenceNumber: 1, + lastModifiedAt: '2024-04-29T07:55:13.613Z', + lastModifiedBy: { + isPlatformClient: true, + user: { + id: '9cd5771e-eb9b-4e70-8f16-a91c52216948', + typeId: 'user', + }, + }, + maxApplicationsPerCustomer: 1, + name: { + en: 'first', + }, + references: [], + version: 2, + versionModifiedAt: '2024-04-29T07:55:13.613Z', + }, + ], + total: 4, +}; + +const handlers = [ + http.get(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/discount-codes`, () => + HttpResponse.json(discountList), + ), +]; + +export default handlers; diff --git a/src/test/mocks/handlers/product.ts b/src/test/mocks/handlers/product.ts new file mode 100644 index 00000000..c8163eb4 --- /dev/null +++ b/src/test/mocks/handlers/product.ts @@ -0,0 +1,315 @@ +import { HttpResponse, http } from 'msw'; + +const productList = { + count: 1, + limit: 9, + offset: 0, + results: [ + { + categories: [ + { + id: '4de8975a-b68f-49e6-a8ea-c1e97c51c2a8', + typeId: 'category', + }, + ], + categoryOrderHints: {}, + createdAt: '2024-05-18T05:58:06.974Z', + description: { + en: 'Zamioculcas (Latin: Zamioculcas) is a houseplant of the Aroid family, distributed in Africa. This genus is represented by a single species - Zamioculcas zamielistny, in nature it can be found in tropical forests. The name of the plant was given because of the similarity to the leaves of zamia. Zamiokulkas is a herbaceous plant with a tuberous rhizome. The thin stem has evenly spaced dark green glossy leaves, which are oval in shape with a pointed tip.', + ru: 'Замиокулькас (лат. Zamioculcas) - это домашнее растение семейства Ароидные, распространенное на территории Африки. Данный род представлен единственным видом - Замиокулькас замиелистный, в природе его можно встретить в тропических лесах. Название растению дали из-за сходства с листьями замии. Замиокулькас представляет собой травянистое растение с клубнеобразным корневищем. На тонком стебле равномерно располагаются тёмно-зеленые глянцевые листья, они имеют овальную форму с заострением на конце.', + }, + hasStagedChanges: false, + id: '8247d798-ca84-4b90-ae1c-d7e5490f58b7', + key: '79357868957074', + lastModifiedAt: '2024-05-26T13:44:24.877Z', + masterVariant: { + assets: [ + { + id: 'e629929b-9c3f-4141-8e6e-77608a84d138', + key: 'img-1', + name: { + en: 'img-1', + }, + sources: [ + { + uri: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-1.png', + }, + ], + tags: [], + }, + { + id: '18260724-dc23-46c7-8af1-2749b474a7ef', + key: 'img-2', + name: { + en: 'img-2', + }, + sources: [ + { + uri: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-2.png', + }, + ], + tags: [], + }, + { + id: '0487e650-c792-45d8-aff7-e1dff5e23254', + key: 'img-3', + name: { + en: 'img-3', + }, + sources: [ + { + uri: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-3.png', + }, + ], + tags: [], + }, + { + id: 'da50912b-eaed-4738-a432-65dfadd964b3', + key: 'img-4', + name: { + en: 'img-4', + }, + sources: [ + { + uri: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-4.png', + }, + ], + tags: [], + }, + ], + attributes: [ + { + name: 'new_arrival', + value: [false], + }, + { + name: 'size', + value: [ + { + key: 'S', + label: 'S', + }, + ], + }, + { + name: 'full_description', + value: { + en: 'Zamiokulkas needs plenty of sunlight, suitable temperature conditions are in the range of 18 to 25 degrees Celsius, in winter 16-18. Water abundantly, but not often, about once a week. Daily spraying will be useful. In spring and summer, feed once a month, using fertilizers for cacti and succulents.', + ru: 'Замиокулькас нуждается в большом количестве солнечного света, подходящие температурные условия находятся в диапазоне от 18 до 25 градусов тепла, зимой 16-18. Поливают обильно, но не часто, примерно раз в неделю. Будут полезны ежедневные опрыскивания. Весной и летом подкармливают раз в месяц, используя удобрения для кактусов и суккулентов.', + }, + }, + { + name: 'level', + value: [ + { + key: '1', + label: '1', + }, + ], + }, + ], + id: 1, + images: [ + { + dimensions: { + h: 500, + w: 500, + }, + label: 'img-1', + url: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-1.png', + }, + { + dimensions: { + h: 500, + w: 500, + }, + label: 'img-2', + url: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-2.png', + }, + { + dimensions: { + h: 500, + w: 500, + }, + label: 'img-3', + url: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-3.png', + }, + { + dimensions: { + h: 500, + w: 500, + }, + label: 'img-4', + url: 'https://raw.githubusercontent.com/stardustmeg/greenshop-db/main/79357868957074/img-4.png', + }, + ], + isMatchingVariant: true, + key: '79357868957074_1', + prices: [ + { + id: '48eacec5-a0bc-4302-92da-654f37b9d0d3', + key: '79357868957074_1', + value: { + centAmount: 1500, + currencyCode: 'USD', + fractionDigits: 2, + type: 'centPrecision', + }, + }, + ], + sku: '79357868957074_1', + }, + metaDescription: { + en: 'house plants', + }, + metaTitle: { + en: 'house plants', + }, + name: { + en: 'Zamioculcas', + ru: 'Замиокулькас', + }, + priceMode: 'Embedded', + productType: { + id: '986435e1-45b9-4f14-bcee-9ae3921e5d3c', + typeId: 'product-type', + }, + published: true, + searchKeywords: { + en: [ + { + text: 'zamioculcas', + }, + ], + ru: [ + { + text: 'замиокулькас', + }, + ], + }, + slug: { + en: 'zamioculcas', + ru: 'zamioculcas', + }, + taxCategory: { + id: '47d25f8a-b0df-4a76-8929-7115bf2bcbc1', + typeId: 'tax-category', + }, + variants: [ + { + assets: [], + attributes: [ + { + name: 'new_arrival', + value: [false], + }, + { + name: 'size', + value: [ + { + key: 'M', + label: 'M', + }, + ], + }, + { + name: 'full_description', + value: { + en: 'Zamiokulkas needs plenty of sunlight, suitable temperature conditions are in the range of 18 to 25 degrees Celsius, in winter 16-18. Water abundantly, but not often, about once a week. Daily spraying will be useful. In spring and summer, feed once a month, using fertilizers for cacti and succulents.', + ru: 'Замиокулькас нуждается в большом количестве солнечного света, подходящие температурные условия находятся в диапазоне от 18 до 25 градусов тепла, зимой 16-18. Поливают обильно, но не часто, примерно раз в неделю. Будут полезны ежедневные опрыскивания. Весной и летом подкармливают раз в месяц, используя удобрения для кактусов и суккулентов.', + }, + }, + { + name: 'level', + value: [ + { + key: '1', + label: '1', + }, + ], + }, + ], + id: 2, + images: [], + isMatchingVariant: true, + key: '79357868957074_2', + prices: [ + { + id: 'baf88e8a-23f6-4a52-ae18-45e5bfb93b12', + key: '79357868957074_2', + value: { + centAmount: 2500, + currencyCode: 'USD', + fractionDigits: 2, + type: 'centPrecision', + }, + }, + ], + sku: '79357868957074_2', + }, + { + assets: [], + attributes: [ + { + name: 'new_arrival', + value: [false], + }, + { + name: 'size', + value: [ + { + key: 'XL', + label: 'XL', + }, + ], + }, + { + name: 'full_description', + value: { + en: 'Zamiokulkas needs plenty of sunlight, suitable temperature conditions are in the range of 18 to 25 degrees Celsius, in winter 16-18. Water abundantly, but not often, about once a week. Daily spraying will be useful. In spring and summer, feed once a month, using fertilizers for cacti and succulents.', + ru: 'Замиокулькас нуждается в большом количестве солнечного света, подходящие температурные условия находятся в диапазоне от 18 до 25 градусов тепла, зимой 16-18. Поливают обильно, но не часто, примерно раз в неделю. Будут полезны ежедневные опрыскивания. Весной и летом подкармливают раз в месяц, используя удобрения для кактусов и суккулентов.', + }, + }, + { + name: 'level', + value: [ + { + key: '1', + label: '1', + }, + ], + }, + ], + id: 3, + images: [], + isMatchingVariant: true, + key: '79357868957074_4', + prices: [ + { + id: '5592000f-86c5-4e42-916b-9b62580e7904', + key: '79357868957074_4', + value: { + centAmount: 5600, + currencyCode: 'USD', + fractionDigits: 2, + type: 'centPrecision', + }, + }, + ], + sku: '79357868957074_4', + }, + ], + version: 25, + }, + ], + total: 1, +}; + +const handlers = [ + http.get( + `${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/product-projections/search`, + () => HttpResponse.json(productList), + ), +]; + +export default handlers; diff --git a/src/test/mocks/handlers/project.ts b/src/test/mocks/handlers/project.ts new file mode 100644 index 00000000..09813b75 --- /dev/null +++ b/src/test/mocks/handlers/project.ts @@ -0,0 +1,63 @@ +import { HttpResponse, http } from 'msw'; + +const project = { + carts: { + allowAddingUnpublishedProducts: false, + cartDiscountCache: { + enabled: false, + }, + countryTaxRateFallbackEnabled: false, + deleteDaysAfterLastModification: 90, + loadParsedDiscountsEnabled: false, + }, + countries: ['DE', 'AT', 'BE', 'AZ', 'BG', 'BY', 'CH', 'CZ', 'EE', 'ES', 'EU', 'EZ', 'RU', 'US', 'AE', 'AU'], + createdAt: '2024-04-26T19:16:17.394Z', + createdBy: { + isPlatformClient: true, + user: { + id: '9cd5771e-eb9b-4e70-8f16-a91c52216948', + typeId: 'user', + }, + }, + currencies: ['USD', 'RUB'], + key: 'green-shop', + languages: ['en', 'ru'], + lastModifiedAt: '2024-05-16T07:13:50.923Z', + lastModifiedBy: { + isPlatformClient: true, + }, + messages: { + deleteDaysAfterCreation: 15, + enabled: false, + }, + name: 'Green Shop', + searchIndexing: { + customers: { + lastModifiedAt: '2024-05-16T07:13:50.915Z', + lastModifiedBy: { + isPlatformClient: true, + }, + status: 'Activated', + }, + products: { + lastModifiedAt: '2024-05-05T18:13:15.638Z', + lastModifiedBy: { + isPlatformClient: true, + }, + status: 'Activated', + }, + }, + shoppingLists: { + deleteDaysAfterLastModification: 360, + }, + trialUntil: '2024-06', + version: 14, +}; + +const handlers = [ + http.get(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}`, () => + HttpResponse.json(project), + ), +]; + +export default handlers; diff --git a/src/test/mocks/handlers/shopping-list.ts b/src/test/mocks/handlers/shopping-list.ts new file mode 100644 index 00000000..4bc73995 --- /dev/null +++ b/src/test/mocks/handlers/shopping-list.ts @@ -0,0 +1,72 @@ +import { HttpResponse, http } from 'msw'; + +const shopList = { + anonymousId: '599456f9-bff8-46e7-8c9b-5f6e60dabb27', + createdAt: '2024-06-12T12:52:38.321Z', + createdBy: { + anonymousId: '599456f9-bff8-46e7-8c9b-5f6e60dabb27', + clientId: 'BPJMQ0wyC-4dA_1sd04SlJQx', + isPlatformClient: false, + }, + customer: { + id: 'a9a1b24b-a467-4850-9a1c-c4030dff9949', + typeId: 'customer', + }, + deleteDaysAfterLastModification: 2, + id: 'e70b4b40-2cd8-4109-87ce-8f35086c2852', + lastModifiedAt: '2024-06-16T08:44:19.700Z', + lastModifiedBy: { + clientId: 'BPJMQ0wyC-4dA_1sd04SlJQx', + customer: { + id: 'a9a1b24b-a467-4850-9a1c-c4030dff9949', + typeId: 'customer', + }, + isPlatformClient: false, + }, + lineItems: [], + name: { + en: 'Favorite', + ru: 'Favorite', + }, + textLineItems: [], + version: 3, + versionModifiedAt: '2024-06-16T08:44:19.700Z', +}; + +const shopListList = { + count: 1, + limit: 9, + offset: 0, + results: [shopList], + total: 1, +}; +const handlers = [ + http.get( + `${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/shopping-lists`, + () => HttpResponse.json(shopListList), + ), + http.get(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/shopping-lists/*`, () => + HttpResponse.json(shopListList), + ), + http.post( + `${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/shopping-lists`, + () => HttpResponse.json(shopList), + ), + http.post(`${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/shopping-lists`, () => + HttpResponse.json(shopList), + ), + http.post( + `${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/shopping-lists/*`, + () => HttpResponse.json(shopList), + ), + http.post( + `${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/shopping-lists/*`, + () => HttpResponse.json(shopList), + ), + http.delete( + `${import.meta.env.VITE_APP_CTP_API_URL}/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/me/shopping-lists/*`, + () => HttpResponse.json(shopList), + ), +]; + +export default handlers; diff --git a/src/test/mocks/handlers/token.ts b/src/test/mocks/handlers/token.ts new file mode 100644 index 00000000..2ff1421e --- /dev/null +++ b/src/test/mocks/handlers/token.ts @@ -0,0 +1,22 @@ +import { HttpResponse, http } from 'msw'; + +const token = { + access_token: '3pQ1oMrAIqDGIfiJ1Vj5txcKq7H7EtbC', + expires_in: 10800, + refresh_token: 'green-shop:IY1ZJDEe82gz7SsV2awdlo1CXYxA3UGpwJlZQmrc_XY', + scope: import.meta.env.VITE_APP_CTP_SCOPES.split(' '), + token_type: 'Bearer', +}; + +const handlers = [ + http.post( + `${import.meta.env.VITE_APP_CTP_AUTH_URL}/oauth/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/anonymous/token`, + () => HttpResponse.json(token), + ), + http.post( + `${import.meta.env.VITE_APP_CTP_AUTH_URL}/oauth/${import.meta.env.VITE_APP_CTP_PROJECT_KEY}/customers/token`, + () => HttpResponse.json(token), + ), +]; + +export default handlers; diff --git a/src/test/mocks/node.ts b/src/test/mocks/node.ts new file mode 100644 index 00000000..68afb227 --- /dev/null +++ b/src/test/mocks/node.ts @@ -0,0 +1,7 @@ +import { setupServer } from 'msw/node'; + +import handlers from './handler.ts'; + +const server = setupServer(...handlers); + +export default server; diff --git a/src/widgets/Blog/test/blog.spec.ts b/src/widgets/Blog/test/blog.spec.ts new file mode 100644 index 00000000..015cd5bf --- /dev/null +++ b/src/widgets/Blog/test/blog.spec.ts @@ -0,0 +1,22 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import BlogWidgetModel from '../model/BogWidgetModel.ts'; + +/** + * @vitest-environment jsdom + */ + +const parent = createBaseElement({ + tag: 'div', +}); +const blog = new BlogWidgetModel(parent); + +describe('Checking blog widget', () => { + it('should check if post is defined', () => { + expect(blog).toBeDefined(); + }); + + it('should check if cart is an instance of CartPageModel', () => { + expect(blog).toBeInstanceOf(BlogWidgetModel); + }); +}); diff --git a/src/widgets/ProductOrder/test/productOrder.spec.ts b/src/widgets/ProductOrder/test/productOrder.spec.ts new file mode 100644 index 00000000..62933c6a --- /dev/null +++ b/src/widgets/ProductOrder/test/productOrder.spec.ts @@ -0,0 +1,40 @@ +import type { CartProduct } from '@/shared/types/cart.ts'; + +import sinon from 'sinon'; + +import ProductOrderModel from '../model/ProductOrderModel.ts'; + +/** + * @vitest-environment jsdom + */ + +const productItem: CartProduct = { + images: '', + key: '', + lineItemId: '', + name: [ + { + language: '', + value: '', + }, + ], + price: 0, + priceCouponDiscount: 0, + productId: '', + quantity: 0, + size: null, + totalPrice: 0, + totalPriceCouponDiscount: 0, +}; +const deleteCallback = sinon.fake(); +const productCart = new ProductOrderModel(productItem, deleteCallback); + +describe('Checking productCart', () => { + it('should check if productCart is defined', () => { + expect(productCart).toBeDefined(); + }); + + it('should check if productCart is an instance of ProductOrderModel', () => { + expect(productCart).toBeInstanceOf(ProductOrderModel); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index e6d0350c..7e618340 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "module": "ESNext", "lib": ["ES2021", "DOM", "DOM.Iterable"], "skipLibCheck": true, + "esModuleInterop": true, /* Bundler mode */ "moduleResolution": "bundler", @@ -30,5 +31,5 @@ /* Testing */ "types": ["vitest/globals"] }, - "include": ["src"] + "include": ["src", "vitest.setup.ts", "vitest.config.ts"] } diff --git a/vite.config.js b/vite.config.js index 3b3ab0c2..9afc5dde 100644 --- a/vite.config.js +++ b/vite.config.js @@ -47,6 +47,7 @@ export default { ], resolve: { alias: { + '@': path.resolve(__dirname, './src'), 'node-fetch': 'isomorphic-fetch', }, }, diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..b1a80e30 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import path from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + globals: true, + include: ['src/**/*.spec.ts'], + root: __dirname, + setupFiles: ['vitest.setup.ts'], + }, +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 00000000..6318a8b8 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,20 @@ +import server from '@/test/mocks/node.ts'; + +beforeAll(() => { + server.listen({ + onUnhandledRequest: 'warn', + }); + + // Need it for debugging api requests + // server.events.on('request:start', ({ request }) => { + // console.log('Outgoing:', request.method, request.url); + // }); +}); + +afterEach(() => { + server.resetHandlers(); +}); + +afterAll(() => { + server.close(); +});