test: comprehensive test suite for Manoon storefront

Add complete testing infrastructure with Vitest:

Testing Stack:
- Vitest for unit/integration tests
- @testing-library/react for component tests
- @playwright/test for E2E (installed, ready to configure)
- MSW for API mocking

Test Coverage:
1. Webhook Handler Tests (src/__tests__/integration/api/webhooks/saleor.test.ts)
   - ORDER_CONFIRMED: Emails + analytics
   - ORDER_CREATED: No duplicates
   - ORDER_FULFILLED: Tracking info
   - ORDER_CANCELLED: Cancellation reason
   - ORDER_FULLY_PAID: Payment confirmation
   - Error handling (400/500 responses)
   - Currency handling (RSD preservation)

2. OrderNotificationService Tests
   - Email sending in all 4 languages (SR, EN, DE, FR)
   - Price formatting verification
   - Admin vs Customer templates
   - Address formatting
   - Tracking info handling

3. AnalyticsService Tests
   - Revenue tracking with correct currency
   - Duplicate prevention verification
   - Error handling (doesn't break flow)
   - Singleton pattern

4. Utility Tests
   - formatPrice: RSD, EUR, USD formatting
   - Decimal and zero handling

Fixtures:
- Realistic order data in src/__tests__/fixtures/orders.ts
- Multiple scenarios (with tracking, cancelled, etc.)

Scripts:
- npm test: Run tests in watch mode
- npm run test:run: Run once
- npm run test:coverage: Generate coverage report
- npm run test:e2e: Run Playwright tests

Coverage target: 80%+ for critical paths
This commit is contained in:
Unchained
2026-03-25 21:07:47 +02:00
parent c98677405a
commit 84b85f5291
10 changed files with 3716 additions and 3 deletions

View File

@@ -0,0 +1,227 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock OpenPanel before importing the service
const mockTrack = vi.fn().mockResolvedValue(undefined);
const mockRevenue = vi.fn().mockResolvedValue(undefined);
vi.mock("@openpanel/nextjs", () => ({
OpenPanel: vi.fn().mockImplementation(() => ({
track: mockTrack,
revenue: mockRevenue,
})),
}));
// Import after mock is set up
import { analyticsService } from "@/lib/services/AnalyticsService";
describe("AnalyticsService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("trackOrderReceived", () => {
it("should track order with all details", async () => {
await analyticsService.trackOrderReceived({
orderId: "order-123",
orderNumber: "1524",
total: 5479,
currency: "RSD",
itemCount: 3,
customerEmail: "test@example.com",
eventType: "ORDER_CONFIRMED",
});
expect(mockTrack).toHaveBeenCalledWith("order_received", {
order_id: "order-123",
order_number: "1524",
total: 5479,
currency: "RSD",
item_count: 3,
customer_email: "test@example.com",
event_type: "ORDER_CONFIRMED",
});
});
it("should handle large order values", async () => {
await analyticsService.trackOrderReceived({
orderId: "order-456",
orderNumber: "2000",
total: 500000, // Large amount
currency: "RSD",
itemCount: 100,
customerEmail: "bulk@example.com",
eventType: "ORDER_CONFIRMED",
});
expect(mockTrack).toHaveBeenCalledWith(
"order_received",
expect.objectContaining({
total: 500000,
item_count: 100,
})
);
});
it("should not throw if tracking fails", async () => {
mockTrack.mockRejectedValueOnce(new Error("Network error"));
await expect(
analyticsService.trackOrderReceived({
orderId: "order-123",
orderNumber: "1524",
total: 1000,
currency: "RSD",
itemCount: 1,
customerEmail: "test@example.com",
eventType: "ORDER_CONFIRMED",
})
).resolves.not.toThrow();
});
});
describe("trackRevenue", () => {
it("should track revenue with correct currency", async () => {
await analyticsService.trackRevenue({
amount: 5479,
currency: "RSD",
orderId: "order-123",
orderNumber: "1524",
});
expect(mockRevenue).toHaveBeenCalledWith(5479, {
currency: "RSD",
order_id: "order-123",
order_number: "1524",
});
});
it("should track revenue with different currencies", async () => {
// Test EUR
await analyticsService.trackRevenue({
amount: 100,
currency: "EUR",
orderId: "order-1",
orderNumber: "1000",
});
expect(mockRevenue).toHaveBeenCalledWith(100, {
currency: "EUR",
order_id: "order-1",
order_number: "1000",
});
// Test USD
await analyticsService.trackRevenue({
amount: 150,
currency: "USD",
orderId: "order-2",
orderNumber: "1001",
});
expect(mockRevenue).toHaveBeenCalledWith(150, {
currency: "USD",
order_id: "order-2",
order_number: "1001",
});
});
it("should log tracking for debugging", async () => {
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await analyticsService.trackRevenue({
amount: 5479,
currency: "RSD",
orderId: "order-123",
orderNumber: "1524",
});
expect(consoleSpy).toHaveBeenCalledWith(
"Tracking revenue: 5479 RSD for order 1524"
);
consoleSpy.mockRestore();
});
it("should not throw if revenue tracking fails", async () => {
mockRevenue.mockRejectedValueOnce(new Error("API error"));
await expect(
analyticsService.trackRevenue({
amount: 1000,
currency: "RSD",
orderId: "order-123",
orderNumber: "1524",
})
).resolves.not.toThrow();
});
it("should handle zero amount orders", async () => {
await analyticsService.trackRevenue({
amount: 0,
currency: "RSD",
orderId: "order-000",
orderNumber: "0000",
});
expect(mockRevenue).toHaveBeenCalledWith(0, {
currency: "RSD",
order_id: "order-000",
order_number: "0000",
});
});
});
describe("track", () => {
it("should track custom events", async () => {
await analyticsService.track("custom_event", {
property1: "value1",
property2: 123,
});
expect(mockTrack).toHaveBeenCalledWith("custom_event", {
property1: "value1",
property2: 123,
});
});
it("should not throw on tracking errors", async () => {
mockTrack.mockRejectedValueOnce(new Error("Tracking failed"));
await expect(
analyticsService.track("test_event", { test: true })
).resolves.not.toThrow();
});
});
describe("Singleton pattern", () => {
it("should return the same instance", () => {
// Import fresh to test singleton
const { analyticsService: service1 } = require("@/lib/services/AnalyticsService");
const { analyticsService: service2 } = require("@/lib/services/AnalyticsService");
expect(service1).toBe(service2);
});
});
describe("Error handling", () => {
it("should log errors but not throw", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mockTrack.mockRejectedValueOnce(new Error("Test error"));
await analyticsService.trackOrderReceived({
orderId: "order-123",
orderNumber: "1524",
total: 1000,
currency: "RSD",
itemCount: 1,
customerEmail: "test@example.com",
eventType: "ORDER_CONFIRMED",
});
expect(consoleErrorSpy).toHaveBeenCalled();
expect(consoleErrorSpy.mock.calls[0][0]).toContain("Failed to track order received");
consoleErrorSpy.mockRestore();
});
});
});