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",