329 lines
9.7 KiB
JavaScript
329 lines
9.7 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|
|
});
|