diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..edb27a46 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +version: 2 + +updates: + - package-ecosystem: "npm" + directory: "/FRONTEND" + schedule: + interval: "weekly" + + labels: + - "dependencies" + + - package-ecosystem: "npm" + directory: "/BACKEND" + schedule: + interval: "weekly" + + labels: + - "dependencies" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + labels: + - "dependencies" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de2b8c80..4493d77a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,10 @@ jobs: NODE_ENV: test JWT_SECRET: testsecret12345678901234567890123 + - name: Run frontend coverage + run: npm run coverage + working-directory: FRONTEND + e2e-tests: name: Playwright E2E Tests runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index d7de12f3..7b4a6acd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist-ssr *.njsproj *.sln *.sw? +coverage/ diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..0de1b0a4 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +echo "Running frontend tests..." +npm run test --prefix FRONTEND -- --run || exit 1 \ No newline at end of file diff --git a/FRONTEND/package-lock.json b/FRONTEND/package-lock.json index 754b3c4f..1efb47cf 100644 --- a/FRONTEND/package-lock.json +++ b/FRONTEND/package-lock.json @@ -35,6 +35,7 @@ "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^1.6.1", "eslint": "^9.22.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", @@ -77,6 +78,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -7173,6 +7188,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7438,6 +7494,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -7582,6 +7648,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", diff --git a/FRONTEND/package.json b/FRONTEND/package.json index 58c38ea8..a7e9b1a1 100644 --- a/FRONTEND/package.json +++ b/FRONTEND/package.json @@ -8,7 +8,8 @@ "build": "vite build", "lint": "eslint .", "preview": "vite preview", - "test": "vitest" + "test": "vitest", + "coverage": "vitest run --coverage" }, "dependencies": { "@chakra-ui/icons": "^2.0.17", @@ -46,7 +47,8 @@ "vite": "^6.3.1", "vite-plugin-pwa": "^1.3.0", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^1.6.0" + "vitest": "^1.6.0", + "@vitest/coverage-v8": "^1.6.1" }, "vitest": { "environment": "jsdom" diff --git a/FRONTEND/src/components/ui/QuickViewModal.jsx b/FRONTEND/src/components/ui/QuickViewModal.jsx index 9a91a638..1c75d6c7 100644 --- a/FRONTEND/src/components/ui/QuickViewModal.jsx +++ b/FRONTEND/src/components/ui/QuickViewModal.jsx @@ -17,7 +17,7 @@ import { Link } from "react-router-dom"; import { useCartStore } from "../../store/cart"; import { useCurrencyStore } from "../../store/currency"; import { formatPrice } from "../../utils/currency"; - +import React from "react"; const QuickViewModal = ({ isOpen, onClose, product }) => { const { addToCart } = useCartStore(); const { currency, rates } = useCurrencyStore(); @@ -51,7 +51,9 @@ const QuickViewModal = ({ isOpen, onClose, product }) => { {formatPrice(product.price, currency, rates)} - {product.category && {product.category}} + {product.category && ( + {product.category} + )} {product.brand && ( diff --git a/FRONTEND/src/components/ui/ScrollToTop.jsx b/FRONTEND/src/components/ui/ScrollToTop.jsx index cb2eed3d..d6c97853 100644 --- a/FRONTEND/src/components/ui/ScrollToTop.jsx +++ b/FRONTEND/src/components/ui/ScrollToTop.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { IconButton, useColorModeValue } from "@chakra-ui/react"; import { LuArrowUp } from "react-icons/lu"; diff --git a/FRONTEND/src/components/ui/tests/ProductCard.test.jsx b/FRONTEND/src/components/ui/tests/ProductCard.test.jsx index 802c0967..c91f502d 100644 --- a/FRONTEND/src/components/ui/tests/ProductCard.test.jsx +++ b/FRONTEND/src/components/ui/tests/ProductCard.test.jsx @@ -1,3 +1,4 @@ +import React from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi } from "vitest"; @@ -34,8 +35,7 @@ vi.mock("../../../store/cart", () => ({ })); vi.mock("../QuickAddModal", () => ({ - default: ({ isOpen }) => - isOpen ?
Quick Add Modal
: null, + default: ({ isOpen }) => (isOpen ?
Quick Add Modal
: null), })); vi.mock("@chakra-ui/react", async () => { @@ -86,22 +86,18 @@ describe("ProductCard", () => { expect(screen.getByText("Quick Add Modal")).toBeInTheDocument(); }); - it("disables Add to Cart button when product is out of stock", () => { - render( - - ); - - const addToCartButton = screen.getByRole("button", { - name: `Add ${mockProduct.name} to cart`, - }); - - expect(addToCartButton).toBeDisabled(); - expect(addToCartButton).toHaveTextContent("Out of Stock"); + it("disables Add to Cart button when product is out of stock", () => { + render( + + ); + + const addToCartButton = screen.getByRole("button", { + name: `Add ${mockProduct.name} to cart`, }); it("applies correct layout properties and transition styles to the card container", () => { @@ -114,3 +110,4 @@ describe("ProductCard", () => { expect(computedStyle.transition).toBe("all 0.2s ease-in-out"); }); }); +}); diff --git a/FRONTEND/vitest.config.js b/FRONTEND/vitest.config.js new file mode 100644 index 00000000..03938019 --- /dev/null +++ b/FRONTEND/vitest.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + globals: true, + setupFiles: "./src/setupTests.js", + coverage: { + provider: "v8", + reporter: ["text", "html"], + }, + }, +}); diff --git a/e2e/accessibility.spec.js b/e2e/accessibility.spec.js new file mode 100644 index 00000000..c794bf18 --- /dev/null +++ b/e2e/accessibility.spec.js @@ -0,0 +1,39 @@ +import { test, expect } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; +import { BASE_URL } from "./helpers.js"; + +test.describe("Accessibility Audits", () => { + test("Homepage should not have serious or critical WCAG violations", async ({ + page, + }) => { + await page.goto(BASE_URL); + + await expect(page.locator("vite-error-overlay")).toHaveCount(0); + + const results = await new AxeBuilder({ page }).analyze(); + + const severeViolations = results.violations.filter( + (violation) => + violation.impact === "serious" || violation.impact === "critical" + ); + + expect(severeViolations).toEqual([]); + }); + + test("404 page should not have serious or critical WCAG violations", async ({ + page, + }) => { + await page.goto(`${BASE_URL}/this-route-does-not-exist`); + + await expect(page.locator("vite-error-overlay")).toHaveCount(0); + + const results = await new AxeBuilder({ page }).analyze(); + + const severeViolations = results.violations.filter( + (violation) => + violation.impact === "serious" || violation.impact === "critical" + ); + + expect(severeViolations).toEqual([]); + }); +}); diff --git a/package-lock.json b/package-lock.json index f4ac1038..905eeb8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "serverless-http": "^4.0.0" }, "devDependencies": { + "@axe-core/playwright": "^4.12.1", "@eslint/js": "^9.22.0", "@playwright/test": "^1.61.0", "cross-env": "^7.0.3", @@ -28,6 +29,19 @@ "prettier": "^3.2.5" } }, + "node_modules/@axe-core/playwright": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", + "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.12.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -430,6 +444,16 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", diff --git a/package.json b/package.json index 8fe7c469..c521a815 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "build": "npm install && npm install --prefix FRONTEND && npm run build --prefix FRONTEND", "start": "cross-env NODE_ENV=production node BACKEND/server.js", "test:e2e": "npx playwright test", - "test:e2e:ui": "npx playwright test --ui" + "test:e2e:ui": "npx playwright test --ui", + "prepare": "husky" }, "type": "module", "keywords": [], @@ -22,6 +23,7 @@ "serverless-http": "^4.0.0" }, "devDependencies": { + "@axe-core/playwright": "^4.12.1", "@eslint/js": "^9.22.0", "@playwright/test": "^1.61.0", "cross-env": "^7.0.3",