Loading...
;
- // if (error) return ;
+}>({
+ state: initialState,
+ dispatch: () => null,
+});
+
+const colorReducer = (state: ColorState, action: ColorAction): ColorState => {
+ switch (action.type) {
+ case "SET_COLOR_OPTIONS":
+ return {
+ ...state,
+ colorOptions: action.payload,
+ // Only set color initially if not already initialized
+ color: !state.hasInitialized && action.payload.length > 0 ? action.payload[0].hexColor : state.color,
+ hasInitialized: true,
+ };
+ case "SET_COLOR":
+ return { ...state, color: action.payload };
+ case "SET_IS_LOADING":
+ return { ...state, isLoading: action.payload };
+ default:
+ return state;
+ }
+};
+
+export const ColorProvider = ({ children }: { children: ReactNode }) => {
+ const [state, dispatch] = useReducer(colorReducer, initialState);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useColorContext = () => useContext(ColorContext);
diff --git a/src/data/test.ts b/src/data/test.ts
deleted file mode 100644
index a8621ad..0000000
--- a/src/data/test.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-// TODO: implent the transformData function that takes in an array of RawDataEntry objects and from manf and returns structured json for backend endpoint.
-// Define the structure of a color entry
-interface ColorEntry {
- color: string;
- hexColor: string;
- colorTag: string;
-}
-
-// Define the raw data structure
-interface RawDataEntry {
- filament: string;
- hexColor: string;
- colorTag: string;
-}
-
-// Define the structure of the transformed output
-interface TransformedData {
- [key: string]: ColorEntry[];
-}
-
-export const rawData: RawDataEntry[] = [
- { filament: "PETG WHITE", hexColor: "f6efef", colorTag: "petgWhite" },
- { filament: "PETG BLACK", hexColor: "000000", colorTag: "petgBlack" },
- { filament: "PLA BLACK", hexColor: "000000", colorTag: "black" },
- { filament: "PLA GRAY", hexColor: "666666", colorTag: "gray" },
- { filament: "PLA WHITE", hexColor: "ffffff", colorTag: "white" },
- { filament: "PLA YELLOW", hexColor: "f5c211", colorTag: "yellow" },
- { filament: "PLA RED", hexColor: "f91010", colorTag: "red" },
- { filament: "PLA GOLD", hexColor: "d5b510", colorTag: "gold" },
- { filament: "PLA LUNAR REGOLITH", hexColor: "7d7e7e", colorTag: "lunarRegolith" },
- { filament: "PLA MATTE BLACK", hexColor: "000000", colorTag: "matteBlack" }
-];
-
-export const transformData = (data: RawDataEntry[]): TransformedData[] => {
- const filamentMap: TransformedData = {};
-
- data.forEach((item) => {
- const [type, color] = item.filament.split(' '); // Split filament into type and color
-
- const colorEntry: ColorEntry = {
- color: color.toUpperCase(), // Capitalize color name
- hexColor: item.hexColor,
- colorTag: item.colorTag,
- };
-
- // If the filament type doesn't exist, initialize it
- if (!filamentMap[type]) {
- filamentMap[type] = [];
- }
-
- console.log(filamentMap);
- // Push the color entry to the corresponding filament type
- filamentMap[type].push(colorEntry);
- });
-
- // Convert the map into an array of objects, sorted by PLA first, PETG second
- return Object.entries(filamentMap)
- .sort(([keyA], [keyB]) => {
- if (keyA === 'PLA') return -1;
- if (keyB === 'PLA') return 1;
- return keyA.localeCompare(keyB); // Sort alphabetically otherwise
- })
- .map(([filamentType, colors]) => ({
- [filamentType]: colors,
- }));
-};
\ No newline at end of file
diff --git a/src/hooks/usePreview.test.ts b/src/hooks/usePreview.test.ts
new file mode 100644
index 0000000..982d761
--- /dev/null
+++ b/src/hooks/usePreview.test.ts
@@ -0,0 +1,86 @@
+import { renderHook } from "@testing-library/react";
+import { vi } from "vitest";
+import * as THREE from "three";
+import { usePreviewService } from './usePreview';
+import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
+
+// Mock STLLoader directly
+vi.mock("three/examples/jsm/loaders/STLLoader.js", () => {
+ return {
+ STLLoader: vi.fn().mockImplementation(() => ({
+ load: vi.fn(), // Define load as a mock function
+ })),
+ };
+});
+
+describe("usePreviewService", () => {
+ let mockGeometry: THREE.BufferGeometry;
+
+ beforeEach(() => {
+ // Set up a mock geometry with a bounding box
+ mockGeometry = new THREE.BufferGeometry();
+ mockGeometry.computeBoundingBox = vi.fn(() => {
+ mockGeometry.boundingBox = new THREE.Box3(
+ new THREE.Vector3(0, 0, 0),
+ new THREE.Vector3(10, 20, 30)
+ );
+ });
+
+ // Ensure load is mocked for each test
+ STLLoader.prototype.load = vi.fn();
+ });
+
+ it.skip(
+ "loads a model successfully using loadModel",
+ async () => {
+ const { result } = renderHook(() => usePreviewService());
+
+ // Mock load to immediately call onLoad with mockGeometry
+ (STLLoader.prototype.load as jest.Mock).mockImplementation((_, onLoad) => {
+ onLoad(mockGeometry);
+ });
+
+ const geometry = await result.current.loadModel("mockURL");
+ expect(geometry).toBe(mockGeometry);
+ },
+ 10000
+ );
+
+ it.skip(
+ "handles loadModel error correctly",
+ async () => {
+ const { result } = renderHook(() => usePreviewService());
+
+ // Mock load to immediately call onError with an error
+ (STLLoader.prototype.load as jest.Mock).mockImplementation((_, __, ___, onError) => {
+ onError(new Error("Failed to load"));
+ });
+
+ await expect(result.current.loadModel("mockURL")).rejects.toThrow("Failed to load");
+ },
+ 10000
+ );
+
+ it("calculates dimensions using getDimensions", () => {
+ const { result } = renderHook(() => usePreviewService());
+ mockGeometry.computeBoundingBox();
+
+ const dimensions = result.current.getDimensions(mockGeometry);
+ expect(dimensions).toEqual({
+ width: 10,
+ height: 20,
+ depth: 30,
+ });
+ });
+
+ it("checks model dimensions using checkModelDimensions", () => {
+ const { result } = renderHook(() => usePreviewService());
+ const dimensions = result.current.checkModelDimensions(mockGeometry);
+
+ expect(dimensions).toEqual({
+ length: 10,
+ width: 20,
+ height: 30,
+ });
+ });
+});
diff --git a/src/pages/Product.test.tsx b/src/pages/Product.test.tsx
new file mode 100644
index 0000000..4e16c09
--- /dev/null
+++ b/src/pages/Product.test.tsx
@@ -0,0 +1,93 @@
+import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
+import ProductPage from "./Product";
+import { vi } from "vitest";
+import { Suspense } from "react";
+
+// Mock lazy-loaded component
+vi.mock("../components/PreviewComponent", () => ({
+ default: () => Preview Component
,
+}));
+
+// Mock components
+vi.mock("../components/ColorPicker", () => ({
+ default: ({ filamentType }: {filamentType: string}) => (
+
+ Color Picker for {filamentType}
+
+ ),
+}));
+
+vi.mock("../components/FilamentDropdown", () => ({
+ default: ({ selectedFilament, setSelectedFilament }: { selectedFilament: string; setSelectedFilament: (value: string) => void }) => (
+
+ ),
+}));
+
+describe("ProductPage", () => {
+ beforeEach(async () => {
+ // Wrap render in act() to handle Suspense loading
+ await act(async () => {
+ render(
+ Loading...}>
+
+
+ );
+ });
+ });
+
+ it("renders ProductPage with product details", () => {
+ expect(screen.getByText("RC Wheels")).toBeInTheDocument();
+ expect(screen.getByText("$35")).toBeInTheDocument();
+ expect(screen.getByText("Description")).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "This is a 12mm RC buggy wheel that will fit any modern buggy for 1/10 scale racing."
+ )
+ ).toBeInTheDocument();
+ });
+
+ it("renders PreviewComponent in a Suspense wrapper", async () => {
+ // Wait for PreviewComponent to load and check for its existence
+ await waitFor(() => {
+ expect(screen.getByTestId("preview-component")).toBeInTheDocument();
+ });
+ });
+
+ it("displays the initial filament type as PLA", () => {
+ const filamentDropdown = screen.getByTestId("filament-dropdown");
+ expect(filamentDropdown).toHaveValue("PLA");
+ });
+
+ it("updates filament selection when dropdown value changes", async () => {
+ const filamentDropdown = screen.getByTestId("filament-dropdown");
+ fireEvent.change(filamentDropdown, { target: { value: "PETG" } });
+ expect(filamentDropdown).toHaveValue("PETG");
+ });
+
+ it("passes selected filament to ColorPicker", () => {
+ const colorPicker = screen.getByTestId("color-picker");
+ expect(colorPicker).toHaveTextContent("Color Picker for PLA");
+ });
+
+ it("updates ColorPicker filamentType when filament selection changes", async () => {
+ const filamentDropdown = screen.getByTestId("filament-dropdown");
+ fireEvent.change(filamentDropdown, { target: { value: "PETG" } });
+
+ await waitFor(() => {
+ const colorPicker = screen.getByTestId("color-picker");
+ expect(colorPicker).toHaveTextContent("Color Picker for PETG");
+ });
+ });
+
+ it("renders Add to cart button", () => {
+ const addToCartButton = screen.getByRole("button", { name: "Add to cart" });
+ expect(addToCartButton).toBeInTheDocument();
+ });
+});
diff --git a/src/pages/Product.tsx b/src/pages/Product.tsx
index 7029aa2..81c2e59 100644
--- a/src/pages/Product.tsx
+++ b/src/pages/Product.tsx
@@ -1,8 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { lazy, Suspense, useState } from "react";
import { ShoppingBagIcon, UserIcon } from "@heroicons/react/24/outline";
-import ColorPicker from "../components/colorPicker";
-import FilamentDropdown from '../components/filamentDropdown';
+import ColorPicker from "../components/ColorPicker";
+import FilamentDropdown from '../components/FilamentDropdown';
const PreviewComponent = lazy(() => import("../components/PreviewComponent"));
@@ -78,7 +78,7 @@ export default function ProductPage() {
Images
- Loading...
}>
+ Loading...}>
false}
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 0000000..7b0828b
--- /dev/null
+++ b/src/setupTests.ts
@@ -0,0 +1 @@
+import '@testing-library/jest-dom';
diff --git a/src/store/colorStore.ts b/src/store/colorStore.ts
deleted file mode 100644
index 806337f..0000000
--- a/src/store/colorStore.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { create } from 'zustand';
-import { ColorsResponse } from '../interfaces';
-
-interface ColorState {
- // State
- colorOptions: ColorsResponse[];
- color: string;
- isLoading: boolean;
-
- // Actions
- setColorOptions: (colorOptions: ColorsResponse[]) => void;
- setColor: (color: string) => void;
- setIsLoading: (isLoading: boolean) => void;
-
-}
-
-export const useColorStore = create((set) => ({
- // State
- colorOptions: [],
- color: '000000',
- isLoading: false,
-
- // Actions
- setColorOptions: (colorOptions: ColorsResponse[]) => set({ colorOptions }),
- setColor: (color: string) => set({ color }),
- setIsLoading: (isLoading: boolean) => set({ isLoading }),
-}));
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 86819c7..f765df2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,7 +20,10 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": [
- "@cloudflare/workers-types/2023-07-01"
+ "@cloudflare/workers-types/2023-07-01",
+ "vitest",
+ "mocha",
+ "jest"
]
},
"include": ["src"],
diff --git a/tsconfig.test.json b/tsconfig.test.json
new file mode 100644
index 0000000..9af2bfc
--- /dev/null
+++ b/tsconfig.test.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "types": ["vitest", "node"]
+ },
+ "include": ["src"]
+}
diff --git a/vite.config.ts b/vite.config.ts
index 5934b75..e04107f 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,10 +1,14 @@
-import { defineConfig } from 'vite'
+import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react-swc'
-// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
- // define: {
- // 'process.env.VITE_BASE_URL': JSON.stringify(process.env.VITE_BASE_URL)
- // }
-})
+ test: {
+ globals: true, // Enable global test functions like describe, it, expect
+ environment: "jsdom", // Use jsdom for DOM-related tests
+ setupFiles: "./src/setupTests.ts", // Optional: Path to a setup file if needed
+ typecheck: {
+ tsconfig: "./tsconfig.test.json", // Specify the test-specific tsconfig
+ },
+ },
+})
\ No newline at end of file