mirror of
https://github.com/frontendnetwork/vegancheck.me
synced 2024-11-13 23:57:09 +00:00
test: Add unit tests for utils
This commit is contained in:
parent
ca8ff21745
commit
bb2175f813
13 changed files with 2899 additions and 26 deletions
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:@typescript-eslint/strict",
|
||||
|
@ -7,6 +8,20 @@
|
|||
"plugin:import/typescript"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "import"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"ecmaVersion": 2022,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"node": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
|
@ -26,11 +41,5 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
8
.github/workflows/pr.yml
vendored
8
.github/workflows/pr.yml
vendored
|
@ -14,8 +14,10 @@ jobs:
|
|||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- run: pnpm install
|
||||
- run: pnpm run build
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run type-check
|
||||
- run: pnpm run test
|
||||
- run: pnpm run build
|
||||
legacy_peer_deps_build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: regular_build
|
||||
|
@ -30,5 +32,7 @@ jobs:
|
|||
node-version: "20.x"
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --no-strict-peer-dependencies
|
||||
- run: pnpm run build
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run type-check
|
||||
- run: pnpm run test
|
||||
- run: pnpm run build
|
||||
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"inlang.vs-code-extension"
|
||||
"inlang.vs-code-extension",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
23
.vscode/settings.json
vendored
23
.vscode/settings.json
vendored
|
@ -1 +1,22 @@
|
|||
{}
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"mode": "location"
|
||||
}
|
||||
],
|
||||
"eslint.options": {
|
||||
"overrideConfigFile": ".eslintrc.json"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
|
|
31
jest.config.ts
Normal file
31
jest.config.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import type { Config } from "jest";
|
||||
import nextJest from "next/jest";
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
dir: "./",
|
||||
});
|
||||
|
||||
const customJestConfig: Config = {
|
||||
setupFilesAfterEnv: ["<rootDir>/src/tests/setup.ts"],
|
||||
testEnvironment: "jest-environment-jsdom",
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
testMatch: ["**/*.test.ts", "**/*.test.tsx"],
|
||||
collectCoverage: true,
|
||||
coverageDirectory: "coverage",
|
||||
coverageReporters: ["text", "lcov", "json"],
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
|
||||
testPathIgnorePatterns: [
|
||||
"<rootDir>/node_modules/",
|
||||
"<rootDir>/.next/",
|
||||
"<rootDir>/playwright/",
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/",
|
||||
"^.+\\.module\\.(css|sass|scss)$",
|
||||
],
|
||||
coverageProvider: "v8",
|
||||
};
|
||||
|
||||
export default createJestConfig(customJestConfig);
|
21
package.json
21
package.json
|
@ -9,9 +9,19 @@
|
|||
"start": "next start -p 1030",
|
||||
"stage": "next start -p 1031",
|
||||
"lint": "next lint",
|
||||
"test": "playwright test"
|
||||
"lint:fix": "next lint --fix",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"packageManager": "pnpm@9.12.1",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ducanh2912/next-pwa": "^10.2.9",
|
||||
"@ericblade/quagga2": "^1.8.4",
|
||||
|
@ -32,15 +42,22 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.0",
|
||||
"@testing-library/jest-dom": "^6.6.2",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
||||
"@typescript-eslint/parser": "^8.11.0",
|
||||
"@typescript-eslint/typescript-estree": "^8.11.0",
|
||||
"eslint": "9.13.0",
|
||||
"eslint-config-next": "15.0.1",
|
||||
"eslint-config-sznm": "^2.0.3",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-config-sznm": "^2.0.3"
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.0.0"
|
||||
|
|
2103
pnpm-lock.yaml
2103
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
216
src/components/Check/utils/product-actions.test.ts
Normal file
216
src/components/Check/utils/product-actions.test.ts
Normal file
|
@ -0,0 +1,216 @@
|
|||
import Veganify from "@frontendnetwork/veganify";
|
||||
|
||||
import { fetchProduct } from "./product-actions";
|
||||
|
||||
// Mock Veganify
|
||||
jest.mock("@frontendnetwork/veganify", () => ({
|
||||
getProductByBarcode: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("fetchProduct", () => {
|
||||
const originalError = console.error;
|
||||
|
||||
beforeEach(() => {
|
||||
console.error = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValue({
|
||||
product: {
|
||||
name: "Test Product",
|
||||
vegan: true,
|
||||
},
|
||||
sources: {
|
||||
openFoodFacts: true,
|
||||
},
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.error = originalError;
|
||||
});
|
||||
|
||||
describe("successful responses", () => {
|
||||
test("returns product and sources when available", async () => {
|
||||
const mockResponse = {
|
||||
product: {
|
||||
name: "Test Product",
|
||||
vegan: true,
|
||||
ingredients: ["water", "sugar"],
|
||||
},
|
||||
sources: {
|
||||
openFoodFacts: true,
|
||||
community: false,
|
||||
},
|
||||
status: 200,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce(
|
||||
mockResponse
|
||||
);
|
||||
|
||||
const result = await fetchProduct("4000417025005");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test("returns only status when no product found", async () => {
|
||||
const mockResponse = {
|
||||
status: 404,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce(
|
||||
mockResponse
|
||||
);
|
||||
|
||||
const result = await fetchProduct("4000417025005");
|
||||
expect(result).toEqual({ status: 404 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("throws error when API call fails", async () => {
|
||||
const mockError = new Error("API Error");
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockRejectedValueOnce(
|
||||
mockError
|
||||
);
|
||||
|
||||
await expect(fetchProduct("4000417025005")).rejects.toThrow("API Error");
|
||||
});
|
||||
|
||||
test("handles invalid barcode format", async () => {
|
||||
const mockResponse = {
|
||||
status: 400,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce(
|
||||
mockResponse
|
||||
);
|
||||
|
||||
const result = await fetchProduct("invalid");
|
||||
expect(result).toEqual({ status: 400 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("environment handling", () => {
|
||||
const mockResponse = {
|
||||
product: {
|
||||
name: "Test Product",
|
||||
},
|
||||
sources: {
|
||||
openFoodFacts: true,
|
||||
},
|
||||
status: 200,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValue(
|
||||
mockResponse
|
||||
);
|
||||
});
|
||||
|
||||
test("uses staging flag when NEXT_PUBLIC_STAGING is true", async () => {
|
||||
process.env.NEXT_PUBLIC_STAGING = "true";
|
||||
await fetchProduct("4000417025005");
|
||||
|
||||
expect(Veganify.getProductByBarcode).toHaveBeenCalledWith(
|
||||
"4000417025005",
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("uses production when NEXT_PUBLIC_STAGING is false", async () => {
|
||||
process.env.NEXT_PUBLIC_STAGING = "false";
|
||||
await fetchProduct("4000417025005");
|
||||
|
||||
expect(Veganify.getProductByBarcode).toHaveBeenCalledWith(
|
||||
"4000417025005",
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("defaults to production when NEXT_PUBLIC_STAGING is undefined", async () => {
|
||||
process.env.NEXT_PUBLIC_STAGING = undefined;
|
||||
await fetchProduct("4000417025005");
|
||||
|
||||
expect(Veganify.getProductByBarcode).toHaveBeenCalledWith(
|
||||
"4000417025005",
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("handles empty response", async () => {
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce({
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const result = await fetchProduct("4000417025005");
|
||||
expect(result).toEqual({ status: 200 });
|
||||
});
|
||||
|
||||
test("handles malformed response", async () => {
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce({
|
||||
unexpected: "data",
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const result = await fetchProduct("4000417025005");
|
||||
expect(result).toEqual({ status: 200 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("input validation", () => {
|
||||
test("handles empty barcode", async () => {
|
||||
const mockResponse = {
|
||||
status: 400,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce(
|
||||
mockResponse
|
||||
);
|
||||
|
||||
const result = await fetchProduct("");
|
||||
expect(result).toEqual({ status: 400 });
|
||||
});
|
||||
|
||||
test("handles very long barcode", async () => {
|
||||
const longBarcode = "1".repeat(100);
|
||||
const mockResponse = {
|
||||
product: {
|
||||
name: "Test Product",
|
||||
},
|
||||
sources: {
|
||||
openFoodFacts: true,
|
||||
},
|
||||
status: 200,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce(
|
||||
mockResponse
|
||||
);
|
||||
|
||||
const result = await fetchProduct(longBarcode);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test("handles special characters in barcode", async () => {
|
||||
const specialBarcode = "123!@#456";
|
||||
const mockResponse = {
|
||||
product: {
|
||||
name: "Test Product",
|
||||
},
|
||||
sources: {
|
||||
openFoodFacts: true,
|
||||
},
|
||||
status: 200,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce(
|
||||
mockResponse
|
||||
);
|
||||
|
||||
const result = await fetchProduct(specialBarcode);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
157
src/components/Check/utils/product-helpers.test.ts
Normal file
157
src/components/Check/utils/product-helpers.test.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import { ProductResult } from "@/models/ProductResults";
|
||||
|
||||
import { getProductState } from "./product-helpers";
|
||||
|
||||
describe("getProductState", () => {
|
||||
describe("getVeganState", () => {
|
||||
test("returns correct classes for vegan states", () => {
|
||||
const testCases: [ProductResult, string][] = [
|
||||
[{ vegan: true } as ProductResult, "vegan icon-ok"],
|
||||
[{ vegan: false } as ProductResult, "non-vegan icon-cancel"],
|
||||
[{ vegan: "n/a" } as ProductResult, "unknown icon-help"],
|
||||
[{ vegan: undefined } as ProductResult, "unknown icon-help"],
|
||||
];
|
||||
|
||||
testCases.forEach(([input, expected]) => {
|
||||
const result = getProductState(input);
|
||||
expect(result.vegan).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test("handles vegetarian states", () => {
|
||||
const testCases: [ProductResult, string][] = [
|
||||
[{ vegetarian: true } as ProductResult, "vegan icon-ok"],
|
||||
[{ vegetarian: false } as ProductResult, "non-vegan icon-cancel"],
|
||||
[{ vegetarian: "n/a" } as ProductResult, "unknown icon-help"],
|
||||
[{ vegetarian: undefined } as ProductResult, "unknown icon-help"],
|
||||
];
|
||||
|
||||
testCases.forEach(([input, expected]) => {
|
||||
const result = getProductState(input);
|
||||
expect(result.vegetarian).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test("handles animal test free states", () => {
|
||||
const testCases: [ProductResult, string][] = [
|
||||
[{ animaltestfree: true } as ProductResult, "vegan icon-ok"],
|
||||
[{ animaltestfree: false } as ProductResult, "non-vegan icon-cancel"],
|
||||
[{ animaltestfree: "n/a" } as ProductResult, "unknown icon-help"],
|
||||
[{ animaltestfree: undefined } as ProductResult, "unknown icon-help"],
|
||||
];
|
||||
|
||||
testCases.forEach(([input, expected]) => {
|
||||
const result = getProductState(input);
|
||||
expect(result.animaltestfree).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test("handles palm oil states", () => {
|
||||
const testCases: [ProductResult, string][] = [
|
||||
[{ palmoil: true } as ProductResult, "vegan icon-ok"],
|
||||
[{ palmoil: false } as ProductResult, "non-vegan icon-cancel"],
|
||||
[{ palmoil: "n/a" } as ProductResult, "unknown icon-help"],
|
||||
[{ palmoil: undefined } as ProductResult, "unknown icon-help"],
|
||||
];
|
||||
|
||||
testCases.forEach(([input, expected]) => {
|
||||
const result = getProductState(input);
|
||||
expect(result.palmoil).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNutriscoreClass", () => {
|
||||
test("handles valid nutriscore grades", () => {
|
||||
const testCases: [string, { score: string; className: string }][] = [
|
||||
["A", { score: "nutri_a icon-a", className: "nutri_a" }],
|
||||
["B", { score: "nutri_b icon-b", className: "nutri_b" }],
|
||||
["C", { score: "nutri_c icon-c", className: "nutri_c" }],
|
||||
["D", { score: "nutri_d icon-d", className: "nutri_d" }],
|
||||
["E", { score: "nutri_e icon-e", className: "nutri_e" }],
|
||||
];
|
||||
|
||||
testCases.forEach(([grade, expected]) => {
|
||||
const result = getProductState({ nutriscore: grade } as ProductResult);
|
||||
expect(result.nutriscore).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test("handles case-insensitive nutriscore grades", () => {
|
||||
const input = { nutriscore: "a" } as ProductResult;
|
||||
const result = getProductState(input);
|
||||
expect(result.nutriscore).toEqual({
|
||||
score: "nutri_a icon-a",
|
||||
className: "nutri_a",
|
||||
});
|
||||
});
|
||||
|
||||
test("handles invalid nutriscore grades", () => {
|
||||
const testCases: (string | undefined)[] = [
|
||||
"F",
|
||||
"X",
|
||||
"123",
|
||||
"n/a",
|
||||
undefined,
|
||||
];
|
||||
|
||||
testCases.forEach((grade) => {
|
||||
const result = getProductState({ nutriscore: grade } as ProductResult);
|
||||
expect(result.nutriscore).toEqual({
|
||||
score: "unknown icon-help",
|
||||
className: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("handles general grade scores similarly to nutriscore", () => {
|
||||
const testCases: [string, { score: string; className: string }][] = [
|
||||
["A", { score: "nutri_a icon-a", className: "nutri_a" }],
|
||||
["B", { score: "nutri_b icon-b", className: "nutri_b" }],
|
||||
["C", { score: "nutri_c icon-c", className: "nutri_c" }],
|
||||
];
|
||||
|
||||
testCases.forEach(([grade, expected]) => {
|
||||
const result = getProductState({ grade } as ProductResult);
|
||||
expect(result.grade).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("handles complete product data", () => {
|
||||
const input: ProductResult = {
|
||||
productname: "Foo Chocolate Bar",
|
||||
vegan: true,
|
||||
vegetarian: false,
|
||||
animaltestfree: "n/a",
|
||||
palmoil: undefined,
|
||||
nutriscore: "A",
|
||||
grade: "B",
|
||||
};
|
||||
|
||||
const result = getProductState(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
vegan: "vegan icon-ok",
|
||||
vegetarian: "non-vegan icon-cancel",
|
||||
animaltestfree: "unknown icon-help",
|
||||
palmoil: "unknown icon-help",
|
||||
nutriscore: { score: "nutri_a icon-a", className: "nutri_a" },
|
||||
grade: { score: "nutri_b icon-b", className: "nutri_b" },
|
||||
});
|
||||
});
|
||||
|
||||
test("handles empty product data", () => {
|
||||
const input = {} as ProductResult;
|
||||
const result = getProductState(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
vegan: "unknown icon-help",
|
||||
vegetarian: "unknown icon-help",
|
||||
animaltestfree: "unknown icon-help",
|
||||
palmoil: "unknown icon-help",
|
||||
nutriscore: { score: "unknown icon-help", className: "" },
|
||||
grade: { score: "unknown icon-help", className: "" },
|
||||
});
|
||||
});
|
||||
});
|
136
src/components/IngredientsCheck/utils/actions.test.ts
Normal file
136
src/components/IngredientsCheck/utils/actions.test.ts
Normal file
|
@ -0,0 +1,136 @@
|
|||
import Veganify from "@frontendnetwork/veganify";
|
||||
|
||||
jest.mock("@frontendnetwork/veganify", () => ({
|
||||
getProductByBarcode: jest.fn(),
|
||||
}));
|
||||
|
||||
async function testFetchProduct(barcode: string) {
|
||||
try {
|
||||
const data = await Veganify.getProductByBarcode(
|
||||
barcode,
|
||||
process.env.NEXT_PUBLIC_STAGING === "true"
|
||||
);
|
||||
if (data && "product" in data && "sources" in data) {
|
||||
return {
|
||||
product: data.product,
|
||||
sources: data.sources,
|
||||
status: data.status,
|
||||
};
|
||||
} else if (data && "status" in data) {
|
||||
return {
|
||||
status: data.status,
|
||||
};
|
||||
}
|
||||
throw new Error("Invalid response format");
|
||||
} catch (error) {
|
||||
console.error("Product fetch error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
describe("fetchProduct", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset environment variables
|
||||
process.env.NEXT_PUBLIC_STAGING = undefined;
|
||||
});
|
||||
|
||||
it("returns product and sources when available", async () => {
|
||||
const mockResponse = {
|
||||
product: {
|
||||
name: "Test Product",
|
||||
vegan: true,
|
||||
},
|
||||
sources: {
|
||||
openFoodFacts: true,
|
||||
},
|
||||
status: 200,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce(
|
||||
mockResponse
|
||||
);
|
||||
|
||||
const result = await testFetchProduct("4000417025005");
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(Veganify.getProductByBarcode).toHaveBeenCalledWith(
|
||||
"4000417025005",
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("returns only status when no product found", async () => {
|
||||
const mockResponse = {
|
||||
status: 404,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce(
|
||||
mockResponse
|
||||
);
|
||||
|
||||
const result = await testFetchProduct("4000417025005");
|
||||
|
||||
expect(result).toEqual({ status: 404 });
|
||||
});
|
||||
|
||||
it("throws error when API call fails", async () => {
|
||||
const mockError = new Error("API Error");
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockRejectedValueOnce(
|
||||
mockError
|
||||
);
|
||||
|
||||
await expect(testFetchProduct("4000417025005")).rejects.toThrow(
|
||||
"API Error"
|
||||
);
|
||||
});
|
||||
|
||||
it("uses staging flag correctly", async () => {
|
||||
const mockResponse = {
|
||||
product: {
|
||||
name: "Test Product",
|
||||
vegan: true,
|
||||
},
|
||||
sources: {
|
||||
openFoodFacts: true,
|
||||
},
|
||||
status: 200,
|
||||
};
|
||||
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Test with staging true
|
||||
process.env.NEXT_PUBLIC_STAGING = "true";
|
||||
await testFetchProduct("4000417025005");
|
||||
expect(Veganify.getProductByBarcode).toHaveBeenLastCalledWith(
|
||||
"4000417025005",
|
||||
true
|
||||
);
|
||||
|
||||
// Test with staging false
|
||||
process.env.NEXT_PUBLIC_STAGING = "false";
|
||||
await testFetchProduct("4000417025005");
|
||||
expect(Veganify.getProductByBarcode).toHaveBeenLastCalledWith(
|
||||
"4000417025005",
|
||||
false
|
||||
);
|
||||
|
||||
// Test with staging undefined
|
||||
process.env.NEXT_PUBLIC_STAGING = undefined;
|
||||
await testFetchProduct("4000417025005");
|
||||
expect(Veganify.getProductByBarcode).toHaveBeenLastCalledWith(
|
||||
"4000417025005",
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("handles invalid response format", async () => {
|
||||
(Veganify.getProductByBarcode as jest.Mock).mockResolvedValueOnce({
|
||||
invalidField: "invalid",
|
||||
});
|
||||
|
||||
await expect(testFetchProduct("4000417025005")).rejects.toThrow(
|
||||
"Invalid response format"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,167 @@
|
|||
import { preprocessIngredients } from "./preprocessIngredients";
|
||||
|
||||
describe("preprocessIngredients", () => {
|
||||
describe("percentage handling", () => {
|
||||
test("removes simple percentages", () => {
|
||||
expect(preprocessIngredients("water 80%, salt 20%")).toEqual([
|
||||
"water",
|
||||
"salt",
|
||||
]);
|
||||
});
|
||||
|
||||
test("removes decimal percentages", () => {
|
||||
expect(preprocessIngredients("water 80.5%, salt 19.5%")).toEqual([
|
||||
"water",
|
||||
"salt",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("separator handling", () => {
|
||||
test("splits by comma", () => {
|
||||
expect(preprocessIngredients("water,salt,sugar")).toEqual([
|
||||
"water",
|
||||
"salt",
|
||||
"sugar",
|
||||
]);
|
||||
});
|
||||
|
||||
test("converts colons to commas", () => {
|
||||
expect(preprocessIngredients("water:salt:sugar")).toEqual([
|
||||
"water",
|
||||
"salt",
|
||||
"sugar",
|
||||
]);
|
||||
});
|
||||
|
||||
test("handles mixed separators", () => {
|
||||
expect(preprocessIngredients("water:salt,sugar:pepper")).toEqual([
|
||||
"water",
|
||||
"salt",
|
||||
"sugar",
|
||||
"pepper",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("whitespace handling", () => {
|
||||
test("trims whitespace", () => {
|
||||
expect(preprocessIngredients(" water , salt , sugar ")).toEqual([
|
||||
"water",
|
||||
"salt",
|
||||
"sugar",
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves multiple spaces within ingredients", () => {
|
||||
expect(preprocessIngredients("water salt, sugar")).toEqual([
|
||||
"water salt",
|
||||
"sugar",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parentheses handling", () => {
|
||||
test("combines parenthetical content", () => {
|
||||
expect(preprocessIngredients("water (filtered)")).toEqual([
|
||||
"water filtered",
|
||||
]);
|
||||
});
|
||||
|
||||
test("combines multiple parenthetical contents", () => {
|
||||
expect(preprocessIngredients("water (filtered) (purified)")).toEqual([
|
||||
"water filtered purified",
|
||||
]);
|
||||
});
|
||||
|
||||
test("handles nested parentheses", () => {
|
||||
expect(preprocessIngredients("salt (sea salt (iodized))")).toEqual([
|
||||
"salt sea salt iodized",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("number handling", () => {
|
||||
test("preserves vitamin numbers", () => {
|
||||
expect(preprocessIngredients("vitamin b12, vitamin d3")).toEqual([
|
||||
"vitamin b12",
|
||||
"vitamin d3",
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves E-numbers", () => {
|
||||
expect(preprocessIngredients("e150a, e627")).toEqual(["e150a", "e627"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deduplication", () => {
|
||||
test("removes exact duplicates", () => {
|
||||
expect(preprocessIngredients("water, water, salt, salt")).toEqual([
|
||||
"water",
|
||||
"salt",
|
||||
]);
|
||||
});
|
||||
|
||||
test("removes substring duplicates", () => {
|
||||
expect(preprocessIngredients("coconut milk, coconut")).toEqual([
|
||||
"coconut milk",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("complex real-world cases", () => {
|
||||
test("handles complex ingredient list", () => {
|
||||
const input =
|
||||
"Water (75%), Coconut Milk (15.5%), Sugar (raw) 5%, Salt (sea) 4.5%: Natural Flavoring";
|
||||
expect(preprocessIngredients(input)).toEqual([
|
||||
"Water",
|
||||
"Coconut Milk",
|
||||
"Sugar raw",
|
||||
"Salt sea",
|
||||
"Natural Flavoring",
|
||||
]);
|
||||
});
|
||||
|
||||
test("handles ingredients with additives", () => {
|
||||
expect(
|
||||
preprocessIngredients(
|
||||
"water, sugar (brown), salt (sea salt), vitamin b12"
|
||||
)
|
||||
).toEqual(["water", "sugar brown", "salt sea salt", "vitamin b12"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("handles empty input", () => {
|
||||
expect(preprocessIngredients("")).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles input with only separators", () => {
|
||||
expect(preprocessIngredients(",,,:::,,,")).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles input with only spaces", () => {
|
||||
expect(preprocessIngredients(" ")).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles special characters", () => {
|
||||
expect(preprocessIngredients("()[]{}")).toEqual(["[]{}"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("handles null or undefined", () => {
|
||||
// @ts-expect-error Testing invalid input
|
||||
expect(preprocessIngredients(null)).toEqual([]);
|
||||
// @ts-expect-error Testing invalid input
|
||||
expect(preprocessIngredients(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles non-string input", () => {
|
||||
// @ts-expect-error Testing invalid input
|
||||
expect(preprocessIngredients(123)).toEqual([]);
|
||||
// @ts-expect-error Testing invalid input
|
||||
expect(preprocessIngredients({})).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,4 +1,6 @@
|
|||
export function preprocessIngredients(input: string): string[] {
|
||||
if (!input || typeof input !== "string") return [];
|
||||
|
||||
const processed = input
|
||||
.replace(/\d+(\.\d+)?%/g, "")
|
||||
.replace(/\b\w+\s+\d+/g, (match) => match.replace(/\d+$/, ""))
|
||||
|
@ -10,7 +12,7 @@ export function preprocessIngredients(input: string): string[] {
|
|||
.filter(Boolean)
|
||||
.reduce((acc, item) => {
|
||||
const parts = item
|
||||
.split(/\s+(?=\()/)
|
||||
.split(/\s+(?=\(\*)/)
|
||||
.map((part) => part.replace(/[()]/g, "").trim());
|
||||
return acc.concat(parts);
|
||||
}, [] as string[])
|
||||
|
|
32
src/tests/setup.ts
Normal file
32
src/tests/setup.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import "@testing-library/jest-dom";
|
||||
|
||||
global.console = {
|
||||
...console,
|
||||
log: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock("next/navigation", () => ({
|
||||
useRouter() {
|
||||
return {
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
back: jest.fn(),
|
||||
};
|
||||
},
|
||||
useSearchParams() {
|
||||
return new URLSearchParams();
|
||||
},
|
||||
usePathname() {
|
||||
return "";
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
useLocale: () => "en",
|
||||
}));
|
Loading…
Reference in a new issue