const TagAnalysisService = require("../../src/services/tagAnalysis"); const ProductService = require("../../src/services/product"); const ProgressService = require("../../src/services/progress"); // Mock the dependencies jest.mock("../../src/services/product"); jest.mock("../../src/services/progress"); describe("TagAnalysisService", () => { let tagAnalysisService; let mockProductService; let mockProgressService; const mockProducts = [ { id: "product1", title: "Test Product 1", tags: ["sale", "featured", "new"], variants: [ { id: "variant1", price: "29.99" }, { id: "variant2", price: "39.99" }, ], }, { id: "product2", title: "Test Product 2", tags: ["sale", "clearance"], variants: [{ id: "variant3", price: "19.99" }], }, { id: "product3", title: "Test Product 3", tags: ["featured", "premium"], variants: [ { id: "variant4", price: "99.99" }, { id: "variant5", price: "149.99" }, ], }, { id: "product4", title: "Test Product 4", tags: ["new"], variants: [{ id: "variant6", price: "49.99" }], }, ]; beforeEach(() => { jest.clearAllMocks(); mockProductService = { debugFetchAllProductTags: jest.fn(), fetchProductsByTag: jest.fn(), }; mockProgressService = { info: jest.fn().mockResolvedValue(), error: jest.fn().mockResolvedValue(), }; ProductService.mockImplementation(() => mockProductService); ProgressService.mockImplementation(() => mockProgressService); tagAnalysisService = new TagAnalysisService(); }); describe("getTagAnalysis", () => { test("successfully analyzes product tags", async () => { mockProductService.debugFetchAllProductTags.mockResolvedValue( mockProducts ); const result = await tagAnalysisService.getTagAnalysis(250); expect(result).toHaveProperty("totalProducts", 4); expect(result).toHaveProperty("tagCounts"); expect(result).toHaveProperty("priceRanges"); expect(result).toHaveProperty("recommendations"); expect(result).toHaveProperty("analyzedAt"); // Verify tag counts are sorted by count (descending) expect(result.tagCounts[0].tag).toBe("sale"); // appears in 2 products expect(result.tagCounts[0].count).toBe(2); expect(result.tagCounts[0].percentage).toBe(50.0); expect(result.tagCounts[1].tag).toBe("featured"); // appears in 2 products expect(result.tagCounts[1].count).toBe(2); expect(result.tagCounts[1].percentage).toBe(50.0); }); test("calculates price ranges correctly", async () => { mockProductService.debugFetchAllProductTags.mockResolvedValue( mockProducts ); const result = await tagAnalysisService.getTagAnalysis(); // Check sale tag price range (products 1 and 2) const salePriceRange = result.priceRanges["sale"]; expect(salePriceRange).toBeDefined(); expect(salePriceRange.min).toBe(19.99); expect(salePriceRange.max).toBe(39.99); expect(salePriceRange.count).toBe(3); // 2 variants from product1 + 1 from product2 expect(salePriceRange.average).toBeCloseTo(29.99, 2); // (29.99 + 39.99 + 19.99) / 3 // Check featured tag price range (products 1 and 3) const featuredPriceRange = result.priceRanges["featured"]; expect(featuredPriceRange).toBeDefined(); expect(featuredPriceRange.min).toBe(29.99); expect(featuredPriceRange.max).toBe(149.99); expect(featuredPriceRange.count).toBe(4); // 2 from product1 + 2 from product3 }); test("generates appropriate recommendations", async () => { // Create more products to meet the minimum count requirement for caution tags const moreProducts = [ ...mockProducts, { id: "product5", title: "Product 5", tags: ["sale"], variants: [{ id: "v5", price: "25.99" }], }, { id: "product6", title: "Product 6", tags: ["clearance"], variants: [{ id: "v6", price: "15.99" }], }, { id: "product7", title: "Product 7", tags: ["clearance"], variants: [{ id: "v7", price: "12.99" }], }, ]; mockProductService.debugFetchAllProductTags.mockResolvedValue( moreProducts ); const result = await tagAnalysisService.getTagAnalysis(); expect(result.recommendations).toBeInstanceOf(Array); expect(result.recommendations.length).toBeGreaterThan(0); // Should have caution recommendation for 'sale' and 'clearance' tags const cautionRec = result.recommendations.find( (rec) => rec.type === "caution" ); expect(cautionRec).toBeDefined(); expect(cautionRec.tags).toContain("sale"); expect(cautionRec.tags).toContain("clearance"); }); test("handles empty product list", async () => { mockProductService.debugFetchAllProductTags.mockResolvedValue([]); await expect(tagAnalysisService.getTagAnalysis()).rejects.toThrow( "No products found for tag analysis" ); expect(mockProgressService.error).toHaveBeenCalledWith( expect.stringContaining("Tag analysis failed") ); }); test("handles products without tags", async () => { const productsWithoutTags = [ { id: "product1", title: "Product 1", tags: null, variants: [] }, { id: "product2", title: "Product 2", tags: [], variants: [] }, { id: "product3", title: "Product 3", variants: [] }, // no tags property ]; mockProductService.debugFetchAllProductTags.mockResolvedValue( productsWithoutTags ); const result = await tagAnalysisService.getTagAnalysis(); expect(result.totalProducts).toBe(3); expect(result.tagCounts).toHaveLength(0); expect(Object.keys(result.priceRanges)).toHaveLength(0); }); test("caches results for performance", async () => { mockProductService.debugFetchAllProductTags.mockResolvedValue( mockProducts ); // First call const result1 = await tagAnalysisService.getTagAnalysis(250); // Second call should use cache const result2 = await tagAnalysisService.getTagAnalysis(250); expect(mockProductService.debugFetchAllProductTags).toHaveBeenCalledTimes( 1 ); expect(result1).toEqual(result2); }); test("respects cache expiry", async () => { mockProductService.debugFetchAllProductTags.mockResolvedValue( mockProducts ); // Mock Date.now to control cache expiry const originalDateNow = Date.now; let mockTime = 1000000; Date.now = jest.fn(() => mockTime); // First call await tagAnalysisService.getTagAnalysis(250); // Advance time beyond cache expiry (5 minutes) mockTime += 6 * 60 * 1000; // Second call should fetch fresh data await tagAnalysisService.getTagAnalysis(250); expect(mockProductService.debugFetchAllProductTags).toHaveBeenCalledTimes( 2 ); // Restore original Date.now Date.now = originalDateNow; }); }); describe("Requirements Compliance", () => { test("meets requirement 7.1 - analyzes available product tags and counts", async () => { mockProductService.debugFetchAllProductTags.mockResolvedValue( mockProducts ); const result = await tagAnalysisService.getTagAnalysis(); // Should provide tag counts expect(result.tagCounts).toBeInstanceOf(Array); expect(result.tagCounts.length).toBeGreaterThan(0); result.tagCounts.forEach((tagInfo) => { expect(tagInfo).toHaveProperty("tag"); expect(tagInfo).toHaveProperty("count"); expect(typeof tagInfo.count).toBe("number"); expect(tagInfo.count).toBeGreaterThan(0); }); }); test("meets requirement 7.2 - shows sample products for selected tags", async () => { const mockSampleProducts = [ { id: "product1", title: "Test Product 1", tags: ["sale", "featured"], variants: [ { id: "variant1", title: "Default", price: "29.99", compareAtPrice: "39.99", }, ], }, ]; mockProductService.fetchProductsByTag.mockResolvedValue( mockSampleProducts ); const samples = await tagAnalysisService.getSampleProductsForTag("sale"); // Should return sample products with essential info expect(samples).toBeInstanceOf(Array); samples.forEach((product) => { expect(product).toHaveProperty("id"); expect(product).toHaveProperty("title"); expect(product).toHaveProperty("tags"); expect(product).toHaveProperty("variants"); }); }); test("meets requirement 7.3 - provides comprehensive tag analysis", async () => { mockProductService.debugFetchAllProductTags.mockResolvedValue( mockProducts ); const result = await tagAnalysisService.getTagAnalysis(); // Should provide comprehensive analysis expect(result).toHaveProperty("totalProducts"); expect(result).toHaveProperty("tagCounts"); expect(result).toHaveProperty("priceRanges"); expect(result).toHaveProperty("recommendations"); expect(result).toHaveProperty("analyzedAt"); // Tag counts should be sorted and include percentages expect(result.tagCounts[0].count).toBeGreaterThanOrEqual( result.tagCounts[1]?.count || 0 ); result.tagCounts.forEach((tag) => { expect(tag).toHaveProperty("percentage"); expect(typeof tag.percentage).toBe("number"); }); }); test("meets requirement 7.4 - provides tag recommendations", async () => { mockProductService.debugFetchAllProductTags.mockResolvedValue( mockProducts ); const result = await tagAnalysisService.getTagAnalysis(); // Should provide recommendations expect(result.recommendations).toBeInstanceOf(Array); result.recommendations.forEach((rec) => { expect(rec).toHaveProperty("type"); expect(rec).toHaveProperty("title"); expect(rec).toHaveProperty("description"); expect(rec).toHaveProperty("tags"); expect(rec).toHaveProperty("reason"); expect(rec).toHaveProperty("priority"); expect(rec).toHaveProperty("actionable"); expect(rec).toHaveProperty("estimatedImpact"); expect(Array.isArray(rec.tags)).toBe(true); }); }); }); });